Commit 5f90cc89 authored by nanahira's avatar nanahira

random duel things

parent aff3079d
Pipeline #43255 failed with stages
in 74 minutes and 37 seconds
FROM node:lts-trixie-slim as base FROM node:lts-trixie-slim as base
LABEL Author="Nanahira <nanahira@momobako.com>" LABEL Author="Nanahira <nanahira@momobako.com>"
RUN apt update && apt -y install python3 build-essential && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/* RUN apt update && apt -y install python3 build-essential libpq-dev && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/*
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY ./package*.json ./ COPY ./package*.json ./
...@@ -12,7 +12,8 @@ RUN npm run build ...@@ -12,7 +12,8 @@ RUN npm run build
FROM base FROM base
ENV NODE_ENV production ENV NODE_ENV production
RUN npm ci && npm cache clean --force RUN npm ci && npm install --no-save pg-native && npm cache clean --force
COPY --from=builder /usr/src/app/dist ./dist COPY --from=builder /usr/src/app/dist ./dist
ENV NODE_PG_FORCE_NATIVE=true
CMD [ "npm", "start" ] CMD [ "npm", "start" ]
host: "::" host: "::"
port: 7911 port: 7911
dbHost: ""
dbPort: 5432
dbUser: postgres
dbPass: ""
dbName: srvpro2
dbNoInit: 0
redisUrl: "" redisUrl: ""
logLevel: info logLevel: info
wsPort: 0 wsPort: 0
...@@ -33,6 +39,15 @@ windbotEndpoint: http://127.0.0.1:2399 ...@@ -33,6 +39,15 @@ windbotEndpoint: http://127.0.0.1:2399
windbotMyIp: 127.0.0.1 windbotMyIp: 127.0.0.1
enableReconnect: 1 enableReconnect: 1
reconnectTimeout: 180000 reconnectTimeout: 180000
enableRandomDuel: 1
randomDuelBlankPassModes:
- S
- M
randomDuelNoRematchCheck: 0
randomDuelRecordMatchScores: 1
randomDuelDisableChat: 0
randomDuelReadyTime: 20
randomDuelHangTimeout: 90
sideTimeoutMinutes: 3 sideTimeoutMinutes: 3
hostinfoLflist: 0 hostinfoLflist: 0
hostinfoRule: 0 hostinfoRule: 0
......
...@@ -17,13 +17,15 @@ ...@@ -17,13 +17,15 @@
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"ipaddr.js": "^2.3.0", "ipaddr.js": "^2.3.0",
"koishipro-core.js": "^1.3.4", "koishipro-core.js": "^1.3.4",
"nfkit": "^1.0.31", "nfkit": "^1.0.32",
"p-queue": "6.6.2", "p-queue": "6.6.2",
"pg": "^8.18.0",
"pino": "^10.3.1", "pino": "^10.3.1",
"pino-pretty": "^13.1.3", "pino-pretty": "^13.1.3",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"typed-reflector": "^1.0.14", "typed-reflector": "^1.0.14",
"typed-struct": "^2.7.1", "typed-struct": "^2.7.1",
"typeorm": "^0.3.28",
"ws": "^8.19.0", "ws": "^8.19.0",
"yaml": "^2.8.2", "yaml": "^2.8.2",
"ygopro-cdb-encode": "^1.0.2", "ygopro-cdb-encode": "^1.0.2",
...@@ -768,7 +770,6 @@ ...@@ -768,7 +770,6 @@
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"string-width": "^5.1.2", "string-width": "^5.1.2",
...@@ -786,7 +787,6 @@ ...@@ -786,7 +787,6 @@
"version": "6.2.2", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
...@@ -799,7 +799,6 @@ ...@@ -799,7 +799,6 @@
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^6.0.1" "ansi-regex": "^6.0.1"
...@@ -1390,7 +1389,6 @@ ...@@ -1390,7 +1389,6 @@
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"engines": { "engines": {
...@@ -1449,6 +1447,12 @@ ...@@ -1449,6 +1447,12 @@
"@sinonjs/commons": "^3.0.1" "@sinonjs/commons": "^3.0.1"
} }
}, },
"node_modules/@sqltools/formatter": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==",
"license": "MIT"
},
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.1", "version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
...@@ -2195,7 +2199,6 @@ ...@@ -2195,7 +2199,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
...@@ -2205,7 +2208,6 @@ ...@@ -2205,7 +2208,6 @@
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
...@@ -2217,6 +2219,15 @@ ...@@ -2217,6 +2219,15 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/ansis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
"integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==",
"license": "ISC",
"engines": {
"node": ">=14"
}
},
"node_modules/anymatch": { "node_modules/anymatch": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
...@@ -2231,6 +2242,15 @@ ...@@ -2231,6 +2242,15 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/app-root-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz",
"integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/aragami": { "node_modules/aragami": {
"version": "1.2.10", "version": "1.2.10",
"resolved": "https://registry.npmjs.org/aragami/-/aragami-1.2.10.tgz", "resolved": "https://registry.npmjs.org/aragami/-/aragami-1.2.10.tgz",
...@@ -2413,7 +2433,6 @@ ...@@ -2413,7 +2433,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-js": { "node_modules/base64-js": {
...@@ -2456,7 +2475,6 @@ ...@@ -2456,7 +2475,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
...@@ -2712,7 +2730,6 @@ ...@@ -2712,7 +2730,6 @@
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"string-width": "^4.2.0", "string-width": "^4.2.0",
...@@ -2727,14 +2744,12 @@ ...@@ -2727,14 +2744,12 @@
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cliui/node_modules/string-width": { "node_modules/cliui/node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
...@@ -2749,7 +2764,6 @@ ...@@ -2749,7 +2764,6 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^4.0.0", "ansi-styles": "^4.0.0",
...@@ -2794,7 +2808,6 @@ ...@@ -2794,7 +2808,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
...@@ -2807,7 +2820,6 @@ ...@@ -2807,7 +2820,6 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/colorette": { "node_modules/colorette": {
...@@ -2852,7 +2864,6 @@ ...@@ -2852,7 +2864,6 @@
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
...@@ -2893,6 +2904,12 @@ ...@@ -2893,6 +2904,12 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT"
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
...@@ -2914,7 +2931,6 @@ ...@@ -2914,7 +2931,6 @@
"version": "1.7.1", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz",
"integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"babel-plugin-macros": "^3.1.0" "babel-plugin-macros": "^3.1.0"
...@@ -3000,6 +3016,18 @@ ...@@ -3000,6 +3016,18 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
...@@ -3018,7 +3046,6 @@ ...@@ -3018,7 +3046,6 @@
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
...@@ -3045,7 +3072,6 @@ ...@@ -3045,7 +3072,6 @@
"version": "9.2.2", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/encoded-buffer": { "node_modules/encoded-buffer": {
...@@ -3134,7 +3160,6 @@ ...@@ -3134,7 +3160,6 @@
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
...@@ -3630,7 +3655,6 @@ ...@@ -3630,7 +3655,6 @@
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
...@@ -3713,7 +3737,6 @@ ...@@ -3713,7 +3737,6 @@
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": "6.* || 8.* || >= 10.*" "node": "6.* || 8.* || >= 10.*"
...@@ -3784,7 +3807,6 @@ ...@@ -3784,7 +3807,6 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"foreground-child": "^3.1.0", "foreground-child": "^3.1.0",
...@@ -4156,7 +4178,6 @@ ...@@ -4156,7 +4178,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
...@@ -4243,7 +4264,6 @@ ...@@ -4243,7 +4264,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/istanbul-lib-coverage": { "node_modules/istanbul-lib-coverage": {
...@@ -4321,7 +4341,6 @@ ...@@ -4321,7 +4341,6 @@
"version": "3.4.3", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/cliui": "^8.0.2" "@isaacs/cliui": "^8.0.2"
...@@ -5245,7 +5264,6 @@ ...@@ -5245,7 +5264,6 @@
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
...@@ -5270,7 +5288,6 @@ ...@@ -5270,7 +5288,6 @@
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
...@@ -5313,9 +5330,9 @@ ...@@ -5313,9 +5330,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nfkit": { "node_modules/nfkit": {
"version": "1.0.31", "version": "1.0.32",
"resolved": "https://registry.npmjs.org/nfkit/-/nfkit-1.0.31.tgz", "resolved": "https://registry.npmjs.org/nfkit/-/nfkit-1.0.32.tgz",
"integrity": "sha512-Gj/WaJK5fFrhmIAcdpzG4P6gtgsIU+4uQPjza+2fhtgNj2RbOAdcMKWbSAcSgYt0wqzUqu4uVoNhsiJThDTVmQ==", "integrity": "sha512-y+UoxDBs6JV4CSBZkidBGK4GfzJ1Qev8uU4m4oClWGs09oxOCh6TQqnOGRaZY1yCmD8yzYcED+8waSMU4WS5fg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/node-int64": { "node_modules/node-int64": {
...@@ -5490,7 +5507,6 @@ ...@@ -5490,7 +5507,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true,
"license": "BlueOak-1.0.0" "license": "BlueOak-1.0.0"
}, },
"node_modules/pako": { "node_modules/pako": {
...@@ -5555,7 +5571,6 @@ ...@@ -5555,7 +5571,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
...@@ -5565,7 +5580,6 @@ ...@@ -5565,7 +5580,6 @@
"version": "1.11.1", "version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"lru-cache": "^10.2.0", "lru-cache": "^10.2.0",
...@@ -5582,9 +5596,98 @@ ...@@ -5582,9 +5596,98 @@
"version": "10.4.3", "version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/pg": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
"integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.11.0",
"pg-pool": "^3.11.0",
"pg-protocol": "^1.11.0",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.3.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
"integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz",
"integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz",
"integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz",
"integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
...@@ -5766,6 +5869,45 @@ ...@@ -5766,6 +5869,45 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
...@@ -5988,7 +6130,6 @@ ...@@ -5988,7 +6130,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
...@@ -6201,11 +6342,50 @@ ...@@ -6201,11 +6342,50 @@
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/sha.js": {
"version": "2.4.12",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
"integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
"license": "(MIT AND BSD-3-Clause)",
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1",
"to-buffer": "^1.2.0"
},
"bin": {
"sha.js": "bin.js"
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sha.js/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"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/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"shebang-regex": "^3.0.0" "shebang-regex": "^3.0.0"
...@@ -6218,7 +6398,6 @@ ...@@ -6218,7 +6398,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
...@@ -6228,7 +6407,6 @@ ...@@ -6228,7 +6407,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
...@@ -6293,6 +6471,22 @@ ...@@ -6293,6 +6471,22 @@
"dev": true, "dev": true,
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/sql-highlight": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz",
"integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==",
"funding": [
"https://github.com/scriptcoded/sql-highlight?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/scriptcoded"
}
],
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/sql.js": { "node_modules/sql.js": {
"version": "1.14.0", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.0.tgz", "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.0.tgz",
...@@ -6355,7 +6549,6 @@ ...@@ -6355,7 +6549,6 @@
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"eastasianwidth": "^0.2.0", "eastasianwidth": "^0.2.0",
...@@ -6374,7 +6567,6 @@ ...@@ -6374,7 +6567,6 @@
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
...@@ -6389,14 +6581,12 @@ ...@@ -6389,14 +6581,12 @@
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/string-width/node_modules/ansi-regex": { "node_modules/string-width/node_modules/ansi-regex": {
"version": "6.2.2", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
...@@ -6409,7 +6599,6 @@ ...@@ -6409,7 +6599,6 @@
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^6.0.1" "ansi-regex": "^6.0.1"
...@@ -6425,7 +6614,6 @@ ...@@ -6425,7 +6614,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
...@@ -6439,7 +6627,6 @@ ...@@ -6439,7 +6627,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
...@@ -6869,6 +7056,114 @@ ...@@ -6869,6 +7056,114 @@
} }
} }
}, },
"node_modules/typeorm": {
"version": "0.3.28",
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz",
"integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==",
"license": "MIT",
"dependencies": {
"@sqltools/formatter": "^1.2.5",
"ansis": "^4.2.0",
"app-root-path": "^3.1.0",
"buffer": "^6.0.3",
"dayjs": "^1.11.19",
"debug": "^4.4.3",
"dedent": "^1.7.0",
"dotenv": "^16.6.1",
"glob": "^10.5.0",
"reflect-metadata": "^0.2.2",
"sha.js": "^2.4.12",
"sql-highlight": "^6.1.0",
"tslib": "^2.8.1",
"uuid": "^11.1.0",
"yargs": "^17.7.2"
},
"bin": {
"typeorm": "cli.js",
"typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js",
"typeorm-ts-node-esm": "cli-ts-node-esm.js"
},
"engines": {
"node": ">=16.13.0"
},
"funding": {
"url": "https://opencollective.com/typeorm"
},
"peerDependencies": {
"@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
"@sap/hana-client": "^2.14.22",
"better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0",
"ioredis": "^5.0.4",
"mongodb": "^5.8.0 || ^6.0.0",
"mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0",
"mysql2": "^2.2.5 || ^3.0.1",
"oracledb": "^6.3.0",
"pg": "^8.5.1",
"pg-native": "^3.0.0",
"pg-query-stream": "^4.0.0",
"redis": "^3.1.1 || ^4.0.0 || ^5.0.14",
"sql.js": "^1.4.0",
"sqlite3": "^5.0.3",
"ts-node": "^10.7.0",
"typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0"
},
"peerDependenciesMeta": {
"@google-cloud/spanner": {
"optional": true
},
"@sap/hana-client": {
"optional": true
},
"better-sqlite3": {
"optional": true
},
"ioredis": {
"optional": true
},
"mongodb": {
"optional": true
},
"mssql": {
"optional": true
},
"mysql2": {
"optional": true
},
"oracledb": {
"optional": true
},
"pg": {
"optional": true
},
"pg-native": {
"optional": true
},
"pg-query-stream": {
"optional": true
},
"redis": {
"optional": true
},
"sql.js": {
"optional": true
},
"sqlite3": {
"optional": true
},
"ts-node": {
"optional": true
},
"typeorm-aurora-data-api-driver": {
"optional": true
}
}
},
"node_modules/typeorm/node_modules/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0"
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
...@@ -6986,6 +7281,19 @@ ...@@ -6986,6 +7281,19 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/v8-to-istanbul": { "node_modules/v8-to-istanbul": {
"version": "9.3.0", "version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
...@@ -7015,7 +7323,6 @@ ...@@ -7015,7 +7323,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
...@@ -7069,7 +7376,6 @@ ...@@ -7069,7 +7376,6 @@
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^6.1.0", "ansi-styles": "^6.1.0",
...@@ -7088,7 +7394,6 @@ ...@@ -7088,7 +7394,6 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^4.0.0", "ansi-styles": "^4.0.0",
...@@ -7106,14 +7411,12 @@ ...@@ -7106,14 +7411,12 @@
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/wrap-ansi-cjs/node_modules/string-width": { "node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
...@@ -7128,7 +7431,6 @@ ...@@ -7128,7 +7431,6 @@
"version": "6.2.2", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
...@@ -7141,7 +7443,6 @@ ...@@ -7141,7 +7443,6 @@
"version": "6.2.3", "version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
...@@ -7154,7 +7455,6 @@ ...@@ -7154,7 +7455,6 @@
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^6.0.1" "ansi-regex": "^6.0.1"
...@@ -7207,11 +7507,19 @@ ...@@ -7207,11 +7507,19 @@
} }
} }
}, },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
...@@ -7243,7 +7551,6 @@ ...@@ -7243,7 +7551,6 @@
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cliui": "^8.0.1", "cliui": "^8.0.1",
...@@ -7262,7 +7569,6 @@ ...@@ -7262,7 +7569,6 @@
"version": "21.1.1", "version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
...@@ -7272,14 +7578,12 @@ ...@@ -7272,14 +7578,12 @@
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/yargs/node_modules/string-width": { "node_modules/yargs/node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
......
...@@ -10,6 +10,7 @@ import { RoomModule } from './room/room-module'; ...@@ -10,6 +10,7 @@ import { RoomModule } from './room/room-module';
import { SqljsFactory, SqljsLoader } from './services/sqljs'; import { SqljsFactory, SqljsLoader } from './services/sqljs';
import { FeatsModule } from './feats/feats-module'; import { FeatsModule } from './feats/feats-module';
import { MiddlewareRx } from './services/middleware-rx'; import { MiddlewareRx } from './services/middleware-rx';
import { TypeormFactory, TypeormLoader } from './services/typeorm';
const core = createAppContext() const core = createAppContext()
.provide(ConfigService, { .provide(ConfigService, {
...@@ -24,6 +25,10 @@ const core = createAppContext() ...@@ -24,6 +25,10 @@ const core = createAppContext()
useFactory: SqljsFactory, useFactory: SqljsFactory,
merge: ['SQL'], merge: ['SQL'],
}) })
.provide(TypeormLoader, {
useFactory: TypeormFactory,
merge: ['database'],
})
.define(); .define();
export type Context = typeof core; export type Context = typeof core;
......
...@@ -27,9 +27,7 @@ export class ClientHandler { ...@@ -27,9 +27,7 @@ export class ClientHandler {
// ws/reverse-ws should already have IP from connection metadata, skip overwrite // ws/reverse-ws should already have IP from connection metadata, skip overwrite
return next(); return next();
} }
this.ctx await this.ctx.get(() => IpResolver).setClientIp(
.get(() => IpResolver)
.setClientIp(
client, client,
msg.real_ip === '0.0.0.0' ? undefined : msg.real_ip, msg.real_ip === '0.0.0.0' ? undefined : msg.real_ip,
); );
...@@ -38,7 +36,7 @@ export class ClientHandler { ...@@ -38,7 +36,7 @@ export class ClientHandler {
}) })
.middleware(YGOProCtosPlayerInfo, async (msg, client, next) => { .middleware(YGOProCtosPlayerInfo, async (msg, client, next) => {
if (!client.ip) { if (!client.ip) {
this.ctx.get(() => IpResolver).setClientIp(client); await this.ctx.get(() => IpResolver).setClientIp(client);
} }
const [name, vpass] = msg.name.split('$'); const [name, vpass] = msg.name.split('$');
client.name = name; client.name = name;
......
import { CacheKey } from 'aragami';
import { Context } from '../app'; import { Context } from '../app';
import { Client } from './client'; import { Client } from './client';
import * as ipaddr from 'ipaddr.js'; import * as ipaddr from 'ipaddr.js';
import { YGOProCtosDisconnect } from '../utility/ygopro-ctos-disconnect';
const IP_RESOLVER_TTL = 24 * 60 * 60 * 1000;
class ConnectedIpCountCache {
@CacheKey()
ip!: string;
count = 0;
}
class BadIpCountCache {
@CacheKey()
ip!: string;
count = 0;
}
export class IpResolver { export class IpResolver {
private logger = this.ctx.createLogger('IpResolver'); private logger = this.ctx.createLogger('IpResolver');
private connectedIpCount = new Map<string, number>();
private badIpCount = new Map<string, number>();
private trustedProxies: Array<[ipaddr.IPv4 | ipaddr.IPv6, number]> = []; private trustedProxies: Array<[ipaddr.IPv4 | ipaddr.IPv6, number]> = [];
constructor(private ctx: Context) { constructor(private ctx: Context) {
...@@ -26,6 +42,11 @@ export class IpResolver { ...@@ -26,6 +42,11 @@ export class IpResolver {
{ count: this.trustedProxies.length }, { count: this.trustedProxies.length },
'Trusted proxies initialized', 'Trusted proxies initialized',
); );
this.ctx.middleware(YGOProCtosDisconnect, async (_msg, client, next) => {
await this.releaseClientIp(client);
return next();
});
} }
toIpv4(ip: string): string { toIpv4(ip: string): string {
...@@ -73,7 +94,7 @@ export class IpResolver { ...@@ -73,7 +94,7 @@ export class IpResolver {
* @param xffIp Optional X-Forwarded-For IP * @param xffIp Optional X-Forwarded-For IP
* @returns true if client should be rejected (bad IP or too many connections) * @returns true if client should be rejected (bad IP or too many connections)
*/ */
setClientIp(client: Client, xffIp?: string): boolean { async setClientIp(client: Client, xffIp?: string): Promise<boolean> {
const prevIp = client.ip; const prevIp = client.ip;
// Priority: passed xffIp > client.xffIp() > client.physicalIp() // Priority: passed xffIp > client.xffIp() > client.physicalIp()
...@@ -89,13 +110,9 @@ export class IpResolver { ...@@ -89,13 +110,9 @@ export class IpResolver {
// Decrement count for previous IP // Decrement count for previous IP
if (prevIp) { if (prevIp) {
const prevCount = this.connectedIpCount.get(prevIp) || 0; const prevCount = await this.getConnectedIpCount(prevIp);
if (prevCount > 0) { if (prevCount > 0) {
if (prevCount === 1) { await this.setConnectedIpCount(prevIp, prevCount - 1);
this.connectedIpCount.delete(prevIp);
} else {
this.connectedIpCount.set(prevIp, prevCount - 1);
}
} }
} }
...@@ -110,7 +127,7 @@ export class IpResolver { ...@@ -110,7 +127,7 @@ export class IpResolver {
const noConnectCountLimit = this.ctx.config.getBoolean( const noConnectCountLimit = this.ctx.config.getBoolean(
'NO_CONNECT_COUNT_LIMIT', 'NO_CONNECT_COUNT_LIMIT',
); );
let connectCount = this.connectedIpCount.get(newIp) || 0; let connectCount = await this.getConnectedIpCount(newIp);
if ( if (
!noConnectCountLimit && !noConnectCountLimit &&
...@@ -119,13 +136,11 @@ export class IpResolver { ...@@ -119,13 +136,11 @@ export class IpResolver {
!this.isTrustedProxy(newIp) !this.isTrustedProxy(newIp)
) { ) {
connectCount++; connectCount++;
this.connectedIpCount.set(newIp, connectCount);
} else {
this.connectedIpCount.set(newIp, connectCount);
} }
await this.setConnectedIpCount(newIp, connectCount);
// Check if IP should be rejected // Check if IP should be rejected
const badCount = this.badIpCount.get(newIp) || 0; const badCount = await this.getBadIpCount(newIp);
if (badCount > 5 || connectCount > 10) { if (badCount > 5 || connectCount > 10) {
this.logger.info( this.logger.info(
{ ip: newIp, badCount, connectCount }, { ip: newIp, badCount, connectCount },
...@@ -142,9 +157,9 @@ export class IpResolver { ...@@ -142,9 +157,9 @@ export class IpResolver {
* Mark an IP as bad (increment bad count) * Mark an IP as bad (increment bad count)
* @param ip The IP address to mark as bad * @param ip The IP address to mark as bad
*/ */
addBadIp(ip: string): void { async addBadIp(ip: string): Promise<void> {
const currentCount = this.badIpCount.get(ip) || 0; const currentCount = await this.getBadIpCount(ip);
this.badIpCount.set(ip, currentCount + 1); await this.setBadIpCount(ip, currentCount + 1);
this.logger.warn( this.logger.warn(
{ ip, count: currentCount + 1 }, { ip, count: currentCount + 1 },
'Bad IP count incremented', 'Bad IP count incremented',
...@@ -154,30 +169,80 @@ export class IpResolver { ...@@ -154,30 +169,80 @@ export class IpResolver {
/** /**
* Get the current connection count for an IP * Get the current connection count for an IP
*/ */
getConnectedIpCount(ip: string): number { async getConnectedIpCount(ip: string): Promise<number> {
return this.connectedIpCount.get(ip) || 0; const data = await this.ctx.aragami.get(ConnectedIpCountCache, ip);
return data?.count || 0;
} }
/** /**
* Get the bad count for an IP * Get the bad count for an IP
*/ */
getBadIpCount(ip: string): number { async getBadIpCount(ip: string): Promise<number> {
return this.badIpCount.get(ip) || 0; const data = await this.ctx.aragami.get(BadIpCountCache, ip);
return data?.count || 0;
} }
/** /**
* Clear all connection counts (useful for testing or maintenance) * Clear all connection counts (useful for testing or maintenance)
*/ */
clearConnectionCounts(): void { async clearConnectionCounts(): Promise<void> {
this.connectedIpCount.clear(); await this.ctx.aragami.clear(ConnectedIpCountCache);
this.logger.debug('Connection counts cleared'); this.logger.debug('Connection counts cleared');
} }
/** /**
* Clear all bad IP counts (useful for testing or maintenance) * Clear all bad IP counts (useful for testing or maintenance)
*/ */
clearBadIpCounts(): void { async clearBadIpCounts(): Promise<void> {
this.badIpCount.clear(); await this.ctx.aragami.clear(BadIpCountCache);
this.logger.debug('Bad IP counts cleared'); this.logger.debug('Bad IP counts cleared');
} }
private async setConnectedIpCount(ip: string, count: number) {
if (count <= 0) {
await this.ctx.aragami.del(ConnectedIpCountCache, ip);
return;
}
await this.ctx.aragami.set(
ConnectedIpCountCache,
{
ip,
count,
},
{
key: ip,
ttl: IP_RESOLVER_TTL,
},
);
}
private async setBadIpCount(ip: string, count: number) {
if (count <= 0) {
await this.ctx.aragami.del(BadIpCountCache, ip);
return;
}
await this.ctx.aragami.set(
BadIpCountCache,
{
ip,
count,
},
{
key: ip,
ttl: IP_RESOLVER_TTL,
},
);
}
private async releaseClientIp(client: Client) {
const ip = client.ip;
if (!ip) {
return;
}
const currentCount = await this.getConnectedIpCount(ip);
if (currentCount <= 0) {
return;
}
await this.setConnectedIpCount(ip, currentCount - 1);
}
} }
...@@ -45,7 +45,9 @@ export class WsServer { ...@@ -45,7 +45,9 @@ export class WsServer {
this.wss = new WebSocketServer({ server: this.httpServer }); this.wss = new WebSocketServer({ server: this.httpServer });
this.wss.on('connection', (ws, req) => { this.wss.on('connection', (ws, req) => {
this.handleConnection(ws, req); this.handleConnection(ws, req).catch((err) => {
this.logger.error({ err }, 'Error handling WebSocket connection');
});
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
...@@ -64,13 +66,17 @@ export class WsServer { ...@@ -64,13 +66,17 @@ export class WsServer {
}); });
} }
private handleConnection(ws: WebSocket, req: IncomingMessage): void { private async handleConnection(
ws: WebSocket,
req: IncomingMessage,
): Promise<void> {
const client = new WsClient(this.ctx, ws, req); const client = new WsClient(this.ctx, ws, req);
if (this.ctx.get(() => IpResolver).setClientIp(client, client.xffIp())) if (await this.ctx.get(() => IpResolver).setClientIp(client, client.xffIp())) {
return; return;
}
client.hostname = req.headers.host?.split(':')[0] || ''; client.hostname = req.headers.host?.split(':')[0] || '';
const handler = this.ctx.get(() => ClientHandler); const handler = this.ctx.get(() => ClientHandler);
handler.handleClient(client).catch((err) => { await handler.handleClient(client).catch((err) => {
this.logger.error({ err }, 'Error handling client'); this.logger.error({ err }, 'Error handling client');
}); });
} }
......
...@@ -12,6 +12,19 @@ export const defaultConfig = { ...@@ -12,6 +12,19 @@ export const defaultConfig = {
HOST: '::', HOST: '::',
// Main server port for YGOPro clients. Format: integer string. // Main server port for YGOPro clients. Format: integer string.
PORT: '7911', PORT: '7911',
// PostgreSQL host. Empty means database disabled.
DB_HOST: '',
// PostgreSQL port. Format: integer string.
DB_PORT: '5432',
// PostgreSQL username.
DB_USER: 'postgres',
// PostgreSQL password.
DB_PASS: '',
// PostgreSQL database name.
DB_NAME: 'srvpro2',
// Skip schema initialization/synchronize when set to '1'.
// Boolean parse rule (default false): ''/'0'/'false'/'null' => false, otherwise true.
DB_NO_INIT: '0',
// Redis connection URL. Format: URL string. Empty means disabled. // Redis connection URL. Format: URL string. Empty means disabled.
REDIS_URL: '', REDIS_URL: '',
// Log level. Format: lowercase string (e.g. info/debug/warn/error). // Log level. Format: lowercase string (e.g. info/debug/warn/error).
...@@ -79,6 +92,27 @@ export const defaultConfig = { ...@@ -79,6 +92,27 @@ export const defaultConfig = {
ENABLE_RECONNECT: '1', ENABLE_RECONNECT: '1',
// Reconnect timeout after disconnect. Format: integer string in milliseconds (ms). // Reconnect timeout after disconnect. Format: integer string in milliseconds (ms).
RECONNECT_TIMEOUT: '180000', RECONNECT_TIMEOUT: '180000',
// Enable random duel feature.
// Boolean parse rule (default false): ''/'0'/'false'/'null' => false, otherwise true.
ENABLE_RANDOM_DUEL: '1',
// Random duel modes that can be matched by blank pass.
// Format: comma-separated mode names. The first item is used as default type.
RANDOM_DUEL_BLANK_PASS_MODES: 'S,M',
// Disable rematch checking for random duel.
// Boolean parse rule (default false): ''/'0'/'false'/'null' => false, otherwise true.
RANDOM_DUEL_NO_REMATCH_CHECK: '0',
// Record random match scores (effective only when database is enabled).
// Boolean parse rule (default false): ''/'0'/'false'/'null' => false, otherwise true.
RANDOM_DUEL_RECORD_MATCH_SCORES: '1',
// Disable chat in random duel rooms.
// Boolean parse rule (default false): ''/'0'/'false'/'null' => false, otherwise true.
RANDOM_DUEL_DISABLE_CHAT: '0',
// Random duel ready countdown before kicking the only unready player in Begin stage.
// Format: integer string in seconds (s). '0' or negative disables the feature.
RANDOM_DUEL_READY_TIME: '20',
// Random duel AFK timeout while waiting for player action.
// Format: integer string in seconds (s). '0' or negative disables the feature.
RANDOM_DUEL_HANG_TIMEOUT: '90',
// Side deck timeout in minutes during siding stage. // Side deck timeout in minutes during siding stage.
// Format: integer string. '0' or negative disables the feature. // Format: integer string. '0' or negative disables the feature.
SIDE_TIMEOUT_MINUTES: '3', SIDE_TIMEOUT_MINUTES: '3',
......
export const MAX_ROOM_NAME_LENGTH = 19;
...@@ -35,6 +35,20 @@ export const TRANSLATIONS = { ...@@ -35,6 +35,20 @@ export const TRANSLATIONS = {
side_remain_part2: ' minutes.', side_remain_part2: ' minutes.',
side_overtime: 'You exceeded side changing time and were kicked by system.', side_overtime: 'You exceeded side changing time and were kicked by system.',
side_overtime_room: ' exceeded side changing time and was kicked by system.', side_overtime_room: ' exceeded side changing time and was kicked by system.',
kicked_by_system: 'was evicted from the game by server.',
kick_count_down:
' seconds later this player will be evicted for not getting ready or starting the game.',
afk_warn_part1: 'no operation too long, will be disconnected after ',
afk_warn_part2: ' seconds',
random_duel_enter_room_waiting: 'Your opponent is ready, start now!',
random_duel_enter_room_new: 'Game created, waiting for random opponent.',
random_duel_enter_room_single:
'Single mode room. Password M for match mode, T for tag mode.',
random_duel_enter_room_match:
'Match mode room. Password S for single mode, T for tag mode.',
random_duel_enter_room_tag:
'Tag mode room. Password S for single mode, M for match mode.',
chat_disabled: 'Chat is disabled in this room.',
}, },
'zh-CN': { 'zh-CN': {
update_required: '请更新你的客户端版本', update_required: '请更新你的客户端版本',
...@@ -69,5 +83,18 @@ export const TRANSLATIONS = { ...@@ -69,5 +83,18 @@ export const TRANSLATIONS = {
side_remain_part2: '分钟。', side_remain_part2: '分钟。',
side_overtime: '你更换副卡组超时,已被系统踢出。', side_overtime: '你更换副卡组超时,已被系统踢出。',
side_overtime_room: '更换副卡组超时,已被系统踢出。', side_overtime_room: '更换副卡组超时,已被系统踢出。',
kicked_by_system: '被系统请出了房间',
kick_count_down: '秒后若不准备或开始游戏将被请出房间',
afk_warn_part1: '已经很久没有操作了,若继续挂机,将于',
afk_warn_part2: '秒后被请出房间',
random_duel_enter_room_waiting: '对手已经在等你了,开始决斗吧!',
random_duel_enter_room_new: '已建立随机对战房间,正在等待对手!',
random_duel_enter_room_single:
'您进入了单局模式房间,密码输入 M 进入比赛模式,输入 T 进入双打模式。',
random_duel_enter_room_match:
'您进入了比赛模式房间,密码输入 S 进入单局模式,输入 T 进入双打模式。',
random_duel_enter_room_tag:
'您进入了双打模式房间,密码输入 S 进入单局模式,输入 M 进入比赛模式。',
chat_disabled: '本房间禁止聊天。',
}, },
}; };
...@@ -4,14 +4,18 @@ import { ContextState } from '../app'; ...@@ -4,14 +4,18 @@ import { ContextState } from '../app';
import { Welcome } from './welcome'; import { Welcome } from './welcome';
import { PlayerStatusNotify } from './player-status-notify'; import { PlayerStatusNotify } from './player-status-notify';
import { Reconnect } from './reconnect'; import { Reconnect } from './reconnect';
import { WindbotModule } from '../windbot'; import { WindbotModule } from './windbot';
import { SideTimeout } from './side-timeout'; import { SideTimeout } from './side-timeout';
import { RandomDuelModule } from './random-duel';
import { WaitForPlayerProvider } from './wait-for-player-provider';
export const FeatsModule = createAppContext<ContextState>() export const FeatsModule = createAppContext<ContextState>()
.provide(ClientVersionCheck) .provide(ClientVersionCheck)
.use(WindbotModule)
.provide(Welcome) .provide(Welcome)
.provide(PlayerStatusNotify) .provide(PlayerStatusNotify)
.provide(Reconnect) .provide(Reconnect)
.provide(WaitForPlayerProvider)
.provide(SideTimeout) .provide(SideTimeout)
.use(RandomDuelModule)
.use(WindbotModule)
.define(); .define();
export * from './client-version-check';
export * from './random-duel';
export * from './reconnect';
export * from './wait-for-player-provider';
export * from './module';
export * from './provider';
export * from './score.entity';
import { createAppContext } from 'nfkit';
import { ContextState } from '../../app';
import { RandomDuelProvider } from './provider';
export const RandomDuelModule = createAppContext<ContextState>()
.provide(RandomDuelProvider)
.define();
import { CacheKey } from 'aragami';
import { ChatColor, YGOProCtosChat } from 'ygopro-msg-encode';
import { Context } from '../../app';
import { Client } from '../../client';
import { MAX_ROOM_NAME_LENGTH } from '../../constants/room';
import {
DuelStage,
OnRoomFinalize,
OnRoomJoinPlayer,
Room,
RoomManager,
} from '../../room';
import { fillRandomString } from '../../utility/fill-random-string';
import { CanReconnectCheck } from '../reconnect';
import { WaitForPlayerProvider } from '../wait-for-player-provider';
import { RandomDuelScore } from './score.entity';
const RANDOM_DUEL_TTL = 24 * 60 * 60 * 1000;
const BUILTIN_RANDOM_TYPES = [
'S',
'M',
'T',
'TOR',
'TR',
'OOR',
'OR',
'TOMR',
'TMR',
'OOMR',
'OMR',
'CR',
'CMR',
];
class RandomDuelOpponentCache {
@CacheKey()
ip!: string;
opponentIp = '';
}
declare module '../../room' {
interface Room {
randomType?: string;
randomDuelMaxPlayer?: number;
}
}
export class RandomDuelProvider {
private logger = this.ctx.createLogger(this.constructor.name);
private roomManager = this.ctx.get(() => RoomManager);
private waitForPlayerProvider = this.ctx.get(() => WaitForPlayerProvider);
enabled = this.ctx.config.getBoolean('ENABLE_RANDOM_DUEL');
noRematchCheck = this.ctx.config.getBoolean('RANDOM_DUEL_NO_REMATCH_CHECK');
disableChat = this.ctx.config.getBoolean('RANDOM_DUEL_DISABLE_CHAT');
private recordMatchScoresConfigured = this.ctx.config.getBoolean(
'RANDOM_DUEL_RECORD_MATCH_SCORES',
);
private waitForPlayerReadyTimeoutMs =
Math.max(0, this.ctx.config.getInt('RANDOM_DUEL_READY_TIME') || 0) * 1000;
private waitForPlayerHangTimeoutMs =
Math.max(0, this.ctx.config.getInt('RANDOM_DUEL_HANG_TIMEOUT') || 0) *
1000;
private waitForPlayerLongAgoBackoffMs = Math.max(
0,
this.waitForPlayerHangTimeoutMs - 19_000,
);
private blankPassModes = this.resolveBlankPassModes();
private supportedTypes = this.resolveSupportedTypes();
constructor(private ctx: Context) {
if (!this.enabled) {
return;
}
this.waitForPlayerProvider.registerTick({
roomFilter: (room) => !!room.randomType,
raadyTimeoutMs: this.waitForPlayerReadyTimeoutMs,
hangTimeoutMs: this.waitForPlayerHangTimeoutMs,
longAgoBackoffMs: this.waitForPlayerLongAgoBackoffMs,
});
if (this.recordMatchScoresConfigured && !this.ctx.database) {
this.logger.warn(
'RANDOM_DUEL_RECORD_MATCH_SCORES is enabled but database is unavailable',
);
}
this.ctx.middleware(CanReconnectCheck, async (msg, _client, next) => {
if (msg.room.randomType && this.getDisconnectedCount(msg.room) > 1) {
return msg.no();
}
return next();
});
this.ctx.middleware(OnRoomJoinPlayer, async (event, client, next) => {
await this.updateOpponentRelation(event.room, client);
return next();
});
this.ctx.middleware(OnRoomFinalize, async (event, _client, next) => {
await this.recordMatchResult(event.room);
return next();
});
this.ctx.middleware(YGOProCtosChat, async (msg, client, next) => {
if (!this.disableChat || !client.roomName) {
return next();
}
const room = this.roomManager.findByName(client.roomName);
if (!room?.randomType) {
return next();
}
await client.sendChat('#{chat_disabled}', ChatColor.BABYBLUE);
return;
});
}
get defaultType() {
return this.blankPassModes[0] || 'S';
}
resolveRandomType(pass: string): string | undefined {
if (!this.enabled) {
return undefined;
}
const type = pass.trim().toUpperCase();
if (!type) {
return '';
}
if (this.supportedTypes.has(type)) {
return type;
}
return undefined;
}
async findOrCreateRandomRoom(type: string, playerIp: string) {
const found = await this.findRandomRoom(type, playerIp);
if (found) {
const foundType = found.randomType || type || this.defaultType;
found.randomType = foundType;
found.noHost = true;
found.randomDuelMaxPlayer = this.resolveRandomDuelMaxPlayer(foundType);
found.welcome = '#{random_duel_enter_room_waiting}';
this.applyWelcomeType(found, foundType);
return found;
}
const randomType = type || this.defaultType;
const roomName = this.generateRandomRoomName(randomType);
if (!roomName) {
return undefined;
}
const room = await this.roomManager.findOrCreateByName(roomName);
room.randomType = randomType;
room.noHost = true;
room.randomDuelMaxPlayer = this.resolveRandomDuelMaxPlayer(randomType);
room.welcome = '#{random_duel_enter_room_new}';
this.applyWelcomeType(room, randomType);
return room;
}
private resolveBlankPassModes() {
const modes = this.ctx.config
.getStringArray('RANDOM_DUEL_BLANK_PASS_MODES')
.map((s) => s.trim().toUpperCase())
.filter((s) => !!s);
const uniqModes = Array.from(new Set(modes));
if (!uniqModes.length) {
return ['S', 'M'];
}
return uniqModes;
}
private resolveSupportedTypes() {
return new Set([...BUILTIN_RANDOM_TYPES, ...this.blankPassModes]);
}
private canMatchType(roomType: string, targetType: string) {
if (!targetType) {
return (
roomType === this.defaultType || this.blankPassModes.includes(roomType)
);
}
return roomType === targetType;
}
private resolveRandomDuelMaxPlayer(type: string) {
return type === 'T' ? 4 : 2;
}
private getDisconnectedCount(room: Room) {
return room.playingPlayers.filter((player) => !!player.disconnected).length;
}
private async findRandomRoom(type: string, playerIp: string) {
for (const room of this.roomManager.allRooms()) {
if (
!room.randomType ||
room.finalizing ||
room.duelStage !== DuelStage.Begin ||
room.windbot
) {
continue;
}
if (!this.canMatchType(room.randomType, type)) {
continue;
}
const maxPlayer =
room.randomDuelMaxPlayer ||
this.resolveRandomDuelMaxPlayer(room.randomType);
const playingCount = room.playingPlayers.length;
if (playingCount <= 0 || playingCount >= maxPlayer) {
continue;
}
if (!this.noRematchCheck) {
const host = room.playingPlayers.find((p) => p.isHost);
if (host?.ip) {
const lastOpponentIp = await this.getLastOpponent(playerIp);
if (lastOpponentIp && lastOpponentIp === host.ip) {
continue;
}
}
}
return room;
}
return undefined;
}
private generateRandomRoomName(type: string) {
const prefix = `${type},RANDOM#`;
for (let i = 0; i < 1000; i += 1) {
const name = fillRandomString(prefix, MAX_ROOM_NAME_LENGTH);
if (!this.roomManager.findByName(name)) {
return name;
}
}
return undefined;
}
private applyWelcomeType(room: Room, type: string) {
if (type === 'S') {
room.welcome2 = '#{random_duel_enter_room_single}';
return;
}
if (type === 'M') {
room.welcome2 = '#{random_duel_enter_room_match}';
return;
}
if (type === 'T') {
room.welcome2 = '#{random_duel_enter_room_tag}';
return;
}
room.welcome2 = '';
}
private async updateOpponentRelation(room: Room, client: Client) {
if (!room.randomType || !client.ip) {
return;
}
const host = room.playingPlayers.find((player) => player.isHost);
if (host && host !== client && host.ip) {
await this.setLastOpponent(host.ip, client.ip);
await this.setLastOpponent(client.ip, host.ip);
return;
}
await this.setLastOpponent(client.ip, '');
}
private async getLastOpponent(ip: string) {
const data = await this.ctx.aragami.get(RandomDuelOpponentCache, ip);
return data?.opponentIp || '';
}
private async setLastOpponent(ip: string, opponentIp: string) {
await this.ctx.aragami.set(
RandomDuelOpponentCache,
{
ip,
opponentIp,
},
{
key: ip,
ttl: RANDOM_DUEL_TTL,
},
);
}
private get recordMatchScoresEnabled() {
return this.recordMatchScoresConfigured && !!this.ctx.database;
}
private async recordMatchResult(room: Room) {
if (!this.recordMatchScoresEnabled || room.randomType !== 'M') {
return;
}
const duelPos0Player = room.getDuelPosPlayers(0)[0];
const duelPos1Player = room.getDuelPosPlayers(1)[0];
if (!duelPos0Player || !duelPos1Player) {
return;
}
const [score0, score1] = room.score;
if (score0 === score1) {
return;
}
if (score0 > score1) {
await this.recordWin(duelPos0Player.name_vpass || duelPos0Player.name);
await this.recordLose(duelPos1Player.name_vpass || duelPos1Player.name);
return;
}
await this.recordWin(duelPos1Player.name_vpass || duelPos1Player.name);
await this.recordLose(duelPos0Player.name_vpass || duelPos0Player.name);
}
private async getOrCreateScore(name: string) {
const repo = this.ctx.database?.getRepository(RandomDuelScore);
if (!repo) {
return undefined;
}
let score = await repo.findOneBy({ name });
if (!score) {
score = repo.create({ name });
}
return score;
}
private async recordWin(name: string) {
if (!name) {
return;
}
const repo = this.ctx.database?.getRepository(RandomDuelScore);
if (!repo) {
return;
}
const score = await this.getOrCreateScore(name);
if (!score) {
return;
}
score.win();
await repo.save(score);
}
private async recordLose(name: string) {
if (!name) {
return;
}
const repo = this.ctx.database?.getRepository(RandomDuelScore);
if (!repo) {
return;
}
const score = await this.getOrCreateScore(name);
if (!score) {
return;
}
score.lose();
await repo.save(score);
}
}
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
@Entity('random_duel_score')
export class RandomDuelScore {
@PrimaryColumn({ type: 'varchar', length: 64 })
name!: string;
@Index()
@Column('int', { default: 0 })
winCount = 0;
@Index()
@Column('int', { default: 0 })
loseCount = 0;
@Index()
@Column('int', { default: 0 })
fleeCount = 0;
@Column('int', { default: 0 })
winCombo = 0;
win() {
this.winCount += 1;
this.winCombo += 1;
}
lose() {
this.loseCount += 1;
this.winCombo = 0;
}
flee() {
this.fleeCount += 1;
this.lose();
}
}
import { Client } from '../../client';
import { Room } from '../../room';
import { ValueContainer } from '../../utility/value-container';
export class CanReconnectCheck extends ValueContainer<boolean> {
constructor(
public client: Client,
public room: Room,
) {
super(true);
}
get canReconnect() {
return this.value;
}
no() {
return this.use(false);
}
}
...@@ -22,16 +22,16 @@ import { ...@@ -22,16 +22,16 @@ import {
ErrorMessageType, ErrorMessageType,
YGOProStocErrorMsg, YGOProStocErrorMsg,
} from 'ygopro-msg-encode'; } from 'ygopro-msg-encode';
import { Context } from '../app'; import { Context } from '../../app';
import { Client } from '../client'; import { Client } from '../../client';
import { DuelStage } from '../room/duel-stage'; import { DuelStage, Room, RoomManager } from '../../room';
import { Room } from '../room'; import { getSpecificFields } from '../../utility/metadata';
import { RoomManager } from '../room/room-manager'; import { YGOProCtosDisconnect } from '../../utility/ygopro-ctos-disconnect';
import { getSpecificFields } from '../utility/metadata'; import { isUpdateDeckPayloadEqual } from '../../utility/deck-compare';
import { YGOProCtosDisconnect } from '../utility/ygopro-ctos-disconnect'; import { CanReconnectCheck } from './can-reconnect-check';
import { isUpdateDeckPayloadEqual } from '../utility/deck-compare';
interface DisconnectInfo { interface DisconnectInfo {
key: string;
roomName: string; roomName: string;
clientPos: number; clientPos: number;
playerName: string; playerName: string;
...@@ -42,23 +42,24 @@ interface DisconnectInfo { ...@@ -42,23 +42,24 @@ interface DisconnectInfo {
type ReconnectType = 'normal' | 'kick'; type ReconnectType = 'normal' | 'kick';
declare module '../client' { declare module '../../client' {
interface Client { interface Client {
preReconnecting?: boolean; preReconnecting?: boolean;
reconnectType?: ReconnectType; reconnectType?: ReconnectType;
preReconnectRoomName?: string; // 临时保存重连的目标房间名 preReconnectRoomName?: string; // 临时保存重连的目标房间名
preReconnectDisconnectKey?: string;
} }
} }
declare module '../room' { declare module '../../room' {
interface Room { interface Room {
noReconnect?: boolean; noReconnect?: boolean;
isLooseReconnectRule?: boolean;
} }
} }
export class Reconnect { export class Reconnect {
private disconnectList = new Map<string, DisconnectInfo>(); private disconnectList = new Map<string, DisconnectInfo>();
private isLooseReconnectRule = false; // 宽松匹配模式,日后可能配置支持
private reconnectTimeout = this.ctx.config.getInt('RECONNECT_TIMEOUT'); // 超时时间,单位:毫秒(默认 180000ms = 3分钟) private reconnectTimeout = this.ctx.config.getInt('RECONNECT_TIMEOUT'); // 超时时间,单位:毫秒(默认 180000ms = 3分钟)
constructor(private ctx: Context) { constructor(private ctx: Context) {
...@@ -93,11 +94,16 @@ export class Reconnect { ...@@ -93,11 +94,16 @@ export class Reconnect {
return next(); // 正常断线处理 return next(); // 正常断线处理
} }
if (!this.canReconnect(client)) { const room = this.getClientRoom(client);
if (!room) {
return next();
}
if (!(await this.canReconnect(client, room))) {
return next(); // 正常断线处理 return next(); // 正常断线处理
} }
await this.registerDisconnect(client); await this.registerDisconnect(client, room);
// 不调用 next(),阻止踢人 // 不调用 next(),阻止踢人
}); });
...@@ -119,23 +125,24 @@ export class Reconnect { ...@@ -119,23 +125,24 @@ export class Reconnect {
}); });
} }
private canReconnect(client: Client): boolean { private async canReconnect(client: Client, room: Room): Promise<boolean> {
const room = this.getClientRoom(client); const canReconnect =
if (!room) {
return false;
}
return (
!client.isInternal && // 不是内部虚拟客户端 !client.isInternal && // 不是内部虚拟客户端
!room.noReconnect && !room.noReconnect &&
client.pos < NetPlayerType.OBSERVER && // 是玩家 client.pos < NetPlayerType.OBSERVER && // 是玩家
room.duelStage !== DuelStage.Begin // 游戏已开始 room.duelStage !== DuelStage.Begin; // 游戏已开始
if (!canReconnect) {
return false;
}
const check = await this.ctx.dispatch(
new CanReconnectCheck(client, room),
client,
); );
return !!check?.canReconnect;
} }
private async registerDisconnect(client: Client) { private async registerDisconnect(client: Client, room: Room) {
const room = this.getClientRoom(client)!; const key = this.getAuthorizeKey(client, room);
const key = this.getAuthorizeKey(client);
// 通知房间 // 通知房间
await room.sendChat( await room.sendChat(
...@@ -149,6 +156,7 @@ export class Reconnect { ...@@ -149,6 +156,7 @@ export class Reconnect {
}, this.reconnectTimeout); }, this.reconnectTimeout);
this.disconnectList.set(key, { this.disconnectList.set(key, {
key,
roomName: room.name, roomName: room.name,
clientPos: client.pos, clientPos: client.pos,
playerName: client.name, playerName: client.name,
...@@ -162,20 +170,14 @@ export class Reconnect { ...@@ -162,20 +170,14 @@ export class Reconnect {
newClient: Client, newClient: Client,
msg: YGOProCtosJoinGame, msg: YGOProCtosJoinGame,
): Promise<boolean> { ): Promise<boolean> {
const key = this.getAuthorizeKey(newClient);
const disconnectInfo = this.disconnectList.get(key);
let room: Room | undefined; let room: Room | undefined;
let oldClient: Client | undefined; let oldClient: Client | undefined;
let reconnectType: ReconnectType | undefined; let reconnectType: ReconnectType | undefined;
let disconnectInfo: DisconnectInfo | undefined;
// 1. 尝试正常断线重连 // 1. 尝试正常断线重连
disconnectInfo = this.findDisconnectInfo(newClient, msg.pass);
if (disconnectInfo) { if (disconnectInfo) {
// 验证房间名(msg.pass 就是房间名)
if (msg.pass !== disconnectInfo.roomName) {
return false;
}
// 获取房间 // 获取房间
const roomManager = this.ctx.get(() => RoomManager); const roomManager = this.ctx.get(() => RoomManager);
room = roomManager.findByName(disconnectInfo.roomName); room = roomManager.findByName(disconnectInfo.roomName);
...@@ -191,7 +193,7 @@ export class Reconnect { ...@@ -191,7 +193,7 @@ export class Reconnect {
// 2. 尝试踢人重连 // 2. 尝试踢人重连
if (!room) { if (!room) {
const kickTarget = this.findKickReconnectTarget(newClient); const kickTarget = await this.findKickReconnectTarget(newClient);
if (kickTarget) { if (kickTarget) {
room = this.getClientRoom(kickTarget)!; room = this.getClientRoom(kickTarget)!;
oldClient = kickTarget; oldClient = kickTarget;
...@@ -204,7 +206,13 @@ export class Reconnect { ...@@ -204,7 +206,13 @@ export class Reconnect {
} }
// 进入 pre_reconnect 阶段 // 进入 pre_reconnect 阶段
await this.sendPreReconnectInfo(newClient, room, oldClient, reconnectType); await this.sendPreReconnectInfo(
newClient,
room,
oldClient,
reconnectType,
disconnectInfo?.key,
);
return true; return true;
} }
...@@ -253,16 +261,20 @@ export class Reconnect { ...@@ -253,16 +261,20 @@ export class Reconnect {
client.preReconnecting = false; client.preReconnecting = false;
client.reconnectType = undefined; client.reconnectType = undefined;
client.preReconnectRoomName = undefined; client.preReconnectRoomName = undefined;
client.preReconnectDisconnectKey = undefined;
return client.disconnect(); return client.disconnect();
} }
client.preReconnecting = false; client.preReconnecting = false;
client.reconnectType = undefined; client.reconnectType = undefined;
client.preReconnectRoomName = undefined; client.preReconnectRoomName = undefined;
const preReconnectDisconnectKey = client.preReconnectDisconnectKey;
client.preReconnectDisconnectKey = undefined;
if (reconnectType === 'normal') { if (reconnectType === 'normal') {
const key = this.getAuthorizeKey(client); const disconnectInfo = preReconnectDisconnectKey
const disconnectInfo = this.disconnectList.get(key); ? this.disconnectList.get(preReconnectDisconnectKey)
: undefined;
if (!disconnectInfo) { if (!disconnectInfo) {
await client.sendChat('#{reconnect_failed}', ChatColor.RED); await client.sendChat('#{reconnect_failed}', ChatColor.RED);
return client.disconnect(); return client.disconnect();
...@@ -317,11 +329,13 @@ export class Reconnect { ...@@ -317,11 +329,13 @@ export class Reconnect {
room: Room, room: Room,
oldClient: Client, oldClient: Client,
reconnectType: ReconnectType, reconnectType: ReconnectType,
disconnectKey?: string,
) { ) {
// 设置 pre_reconnecting 状态 // 设置 pre_reconnecting 状态
client.preReconnecting = true; client.preReconnecting = true;
client.reconnectType = reconnectType; client.reconnectType = reconnectType;
client.preReconnectRoomName = room.name; // 保存目标房间名 client.preReconnectRoomName = room.name; // 保存目标房间名
client.preReconnectDisconnectKey = disconnectKey;
client.pos = oldClient.pos; client.pos = oldClient.pos;
// 发送房间信息 // 发送房间信息
...@@ -358,8 +372,8 @@ export class Reconnect { ...@@ -358,8 +372,8 @@ export class Reconnect {
): Promise<boolean> { ): Promise<boolean> {
if (reconnectType === 'normal') { if (reconnectType === 'normal') {
// 正常重连:验证 disconnectInfo 中的 startDeck // 正常重连:验证 disconnectInfo 中的 startDeck
const key = this.getAuthorizeKey(client); const key = client.preReconnectDisconnectKey;
const disconnectInfo = this.disconnectList.get(key); const disconnectInfo = key ? this.disconnectList.get(key) : undefined;
if (!disconnectInfo) { if (!disconnectInfo) {
return false; return false;
} }
...@@ -750,15 +764,15 @@ export class Reconnect { ...@@ -750,15 +764,15 @@ export class Reconnect {
return undefined; return undefined;
} }
private getAuthorizeKey(client: Client): string { private getAuthorizeKey(client: Client, room?: Room): string {
// 参考 srvpro 逻辑 // 参考 srvpro 逻辑
// 如果有 vpass 且不是宽松匹配模式,优先用 name_vpass // 如果有 vpass 且不是宽松匹配模式,优先用 name_vpass
if (!this.isLooseReconnectRule && client.vpass) { if (!room?.isLooseReconnectRule && client.vpass) {
return client.name_vpass; return client.name_vpass;
} }
// 宽松匹配模式或内部客户端 // 宽松匹配模式或内部客户端
if (this.isLooseReconnectRule) { if (room?.isLooseReconnectRule) {
return client.name || client.ip || 'undefined'; return client.name || client.ip || 'undefined';
} }
...@@ -791,11 +805,12 @@ export class Reconnect { ...@@ -791,11 +805,12 @@ export class Reconnect {
private clearDisconnectInfo(disconnectInfo: DisconnectInfo) { private clearDisconnectInfo(disconnectInfo: DisconnectInfo) {
clearTimeout(disconnectInfo.timeout); clearTimeout(disconnectInfo.timeout);
const key = this.getAuthorizeKey(disconnectInfo.oldClient); this.disconnectList.delete(disconnectInfo.key);
this.disconnectList.delete(key);
} }
private findKickReconnectTarget(newClient: Client): Client | undefined { private async findKickReconnectTarget(
newClient: Client,
): Promise<Client | undefined> {
const roomManager = this.ctx.get(() => RoomManager); const roomManager = this.ctx.get(() => RoomManager);
const allRooms = roomManager.allRooms(); const allRooms = roomManager.allRooms();
...@@ -807,6 +822,9 @@ export class Reconnect { ...@@ -807,6 +822,9 @@ export class Reconnect {
// 查找符合条件的在线玩家 // 查找符合条件的在线玩家
for (const player of room.playingPlayers) { for (const player of room.playingPlayers) {
if (!(await this.canReconnect(player, room))) {
continue;
}
// if (player.disconnected) { // if (player.disconnected) {
// continue; // 跳过已断线的玩家 // continue; // 跳过已断线的玩家
// } // }
...@@ -818,7 +836,7 @@ export class Reconnect { ...@@ -818,7 +836,7 @@ export class Reconnect {
// 宽松模式或匹配条件 // 宽松模式或匹配条件
const matchCondition = const matchCondition =
this.isLooseReconnectRule || room.isLooseReconnectRule ||
player.ip === newClient.ip || player.ip === newClient.ip ||
(newClient.vpass && newClient.vpass === player.vpass); (newClient.vpass && newClient.vpass === player.vpass);
...@@ -830,4 +848,29 @@ export class Reconnect { ...@@ -830,4 +848,29 @@ export class Reconnect {
return undefined; return undefined;
} }
private findDisconnectInfo(
newClient: Client,
roomName: string,
): DisconnectInfo | undefined {
const roomManager = this.ctx.get(() => RoomManager);
for (const disconnectInfo of this.disconnectList.values()) {
if (disconnectInfo.roomName !== roomName) {
continue;
}
const room = roomManager.findByName(disconnectInfo.roomName);
if (!room) {
this.clearDisconnectInfo(disconnectInfo);
continue;
}
const key = this.getAuthorizeKey(newClient, room);
if (key !== disconnectInfo.key) {
continue;
}
return disconnectInfo;
}
return undefined;
}
} }
export * from './can-reconnect-check';
import {
ChatColor,
NetPlayerType,
YGOProCtosChat,
YGOProCtosHandResult,
YGOProCtosResponse,
YGOProCtosTpResult,
YGOProCtosUpdateDeck,
YGOProMsgResponseBase,
} from 'ygopro-msg-encode';
import { Context } from '../app';
import { Client } from '../client';
import {
DuelStage,
OnRoomFinger,
OnRoomFinalize,
OnRoomLeavePlayer,
OnRoomSelectTp,
Room,
RoomManager,
} from '../room';
export interface WaitForPlayerConfig {
roomFilter: (room: Room) => boolean;
raadyTimeoutMs?: number;
hangTimeoutMs?: number;
longAgoBackoffMs: number;
}
declare module '../room' {
interface Room {
waitForPlayerPos?: number;
waitingForPlayerOther?: number[];
lastActiveTime?: Date;
waitForPlayerTickRuntimeId?: number;
waitForPlayerReadyDeadlineMs?: number;
waitForPlayerReadyWarnRemain?: number;
waitForPlayerReadyTargetPos?: number;
waitForPlayerHangWarnElapsed?: number;
}
}
interface WaitForPlayerTickRuntime {
id: number;
options: Required<WaitForPlayerConfig>;
ticking: boolean;
timer: ReturnType<typeof setInterval>;
}
export class WaitForPlayerProvider {
private logger = this.ctx.createLogger(this.constructor.name);
private roomManager = this.ctx.get(() => RoomManager);
private tickRuntimes = new Map<number, WaitForPlayerTickRuntime>();
private nextTickId = 1;
constructor(private ctx: Context) {
this.ctx.middleware(
YGOProMsgResponseBase,
async (_msg, client, next) => {
const room = this.getRoom(client);
if (!room || !this.hasTickForRoom(room)) {
return next();
}
try {
return await next();
} finally {
this.setWaitForPlayer(room, client);
this.refreshLastActiveTime(room);
}
},
true,
);
this.ctx.middleware(
YGOProCtosResponse,
async (_msg, client, next) => {
const room = this.getRoom(client);
if (!room || !this.hasTickForRoom(room)) {
return next();
}
try {
return await next();
} finally {
this.refreshLastActiveTime(room);
}
},
true,
);
this.ctx.middleware(
YGOProCtosHandResult,
async (_msg, client, next) => {
const room = this.getRoom(client);
if (!room || !this.hasTickForRoom(room)) {
return next();
}
try {
return await next();
} finally {
if (room.duelStage === DuelStage.Finger) {
this.resolveWaitForPlayer(room, client);
}
this.refreshLastActiveTime(room, this.getLongAgoBackoffMs(room));
}
},
true,
);
this.ctx.middleware(
YGOProCtosTpResult,
async (_msg, client, next) => {
const room = this.getRoom(client);
if (!room || !this.hasTickForRoom(room)) {
return next();
}
try {
return await next();
} finally {
if (room.duelStage === DuelStage.FirstGo) {
this.resolveWaitForPlayer(room, client);
}
this.refreshLastActiveTime(room);
}
},
true,
);
this.ctx.middleware(
YGOProCtosUpdateDeck,
async (_msg, client, next) => {
const room = this.getRoom(client);
if (!room || !this.hasTickForRoom(room) || room.duelStage !== DuelStage.Begin) {
return next();
}
try {
return await next();
} finally {
this.resolveWaitForPlayer(room, client);
this.refreshLastActiveTime(room);
}
},
true,
);
this.ctx.middleware(
YGOProCtosChat,
async (msg, client, next) => {
const room = this.getRoom(client);
if (!room || !this.hasTickForRoom(room)) {
return next();
}
const text = (msg.msg || '').trim();
if (text.startsWith('/')) {
return next();
}
if (
[DuelStage.Finger, DuelStage.FirstGo, DuelStage.Siding].includes(
room.duelStage,
)
) {
return next();
}
try {
return await next();
} finally {
this.refreshLastActiveTime(room);
}
},
true,
);
this.ctx.middleware(OnRoomFinger, async (event, _client, next) => {
const room = event.room;
if (!this.hasTickForRoom(room)) {
return next();
}
this.setWaitForPlayer(room, ...event.fingerPlayers);
this.refreshLastActiveTime(room, this.getLongAgoBackoffMs(room));
return next();
});
this.ctx.middleware(OnRoomSelectTp, async (event, _client, next) => {
const room = event.room;
if (!this.hasTickForRoom(room)) {
return next();
}
this.setWaitForPlayer(room, event.selector);
this.refreshLastActiveTime(room);
return next();
});
this.ctx.middleware(OnRoomLeavePlayer, async (event, _client, next) => {
const room = event.room;
if (!this.hasTickForRoom(room)) {
return next();
}
if (room.waitForPlayerPos === event.oldPos) {
room.waitForPlayerPos = undefined;
}
room.waitingForPlayerOther = (room.waitingForPlayerOther || []).filter(
(pos) => pos !== event.oldPos,
);
return next();
});
this.ctx.middleware(OnRoomFinalize, async (event, _client, next) => {
const room = event.room;
this.clearTickRoomState(room);
room.waitForPlayerPos = undefined;
room.waitingForPlayerOther = undefined;
room.lastActiveTime = undefined;
return next();
});
}
registerTick(options: WaitForPlayerConfig) {
const runtimeOptions: Required<WaitForPlayerConfig> = {
roomFilter: options.roomFilter,
raadyTimeoutMs: Math.max(0, options.raadyTimeoutMs || 0),
hangTimeoutMs: Math.max(0, options.hangTimeoutMs || 0),
longAgoBackoffMs: Math.max(0, options.longAgoBackoffMs || 0),
};
const id = this.nextTickId;
this.nextTickId += 1;
const runtime: WaitForPlayerTickRuntime = {
id,
options: runtimeOptions,
ticking: false,
timer: setInterval(() => {
void this.tickRuntime(id).catch((error) => {
this.logger.warn({ error, id }, 'Failed to tick wait-for-player');
});
}, 1000),
};
this.tickRuntimes.set(id, runtime);
return id;
}
private getRoom(client: Client): Room | undefined {
if (!client.roomName) {
return undefined;
}
return this.roomManager.findByName(client.roomName);
}
private getLongAgoBackoffMs(room: Room) {
return this.getFirstMatchedRuntime(room)?.options.longAgoBackoffMs || 0;
}
private getMatchedTickRuntimes(room: Room) {
return [...this.tickRuntimes.values()].filter((runtime) =>
runtime.options.roomFilter(room),
);
}
private getFirstMatchedRuntime(room: Room) {
return this.getMatchedTickRuntimes(room)[0];
}
private hasTickForRoom(room: Room) {
return !!this.getFirstMatchedRuntime(room);
}
private async tickRuntime(id: number) {
const runtime = this.tickRuntimes.get(id);
if (!runtime || runtime.ticking) {
return;
}
runtime.ticking = true;
try {
const nowMs = Date.now();
for (const room of this.roomManager.allRooms()) {
const firstMatchedRuntime = this.getFirstMatchedRuntime(room);
if (firstMatchedRuntime?.id !== id) {
if (room.waitForPlayerTickRuntimeId === id) {
this.clearRoomState(room);
}
continue;
}
if (room.waitForPlayerTickRuntimeId !== id) {
this.clearRoomState(room);
room.waitForPlayerTickRuntimeId = id;
}
await this.tickReadyTimeout(runtime, room, nowMs);
await this.tickHangTimeout(runtime, room, nowMs);
}
} finally {
runtime.ticking = false;
}
}
private clearTickRoomState(room: Room) {
this.clearRoomState(room);
}
private clearRoomState(room: Room) {
this.clearReadyState(room);
room.waitForPlayerHangWarnElapsed = undefined;
room.waitForPlayerTickRuntimeId = undefined;
}
private clearReadyState(room: Room) {
room.waitForPlayerReadyDeadlineMs = undefined;
room.waitForPlayerReadyWarnRemain = undefined;
room.waitForPlayerReadyTargetPos = undefined;
}
private getDisconnectedCount(room: Room) {
return room.playingPlayers.filter((player) => !!player.disconnected).length;
}
private getReadyTimeoutTarget(room: Room) {
const players = room.playingPlayers.filter(
(player) =>
!player.disconnected &&
player.roomName === room.name &&
player.pos < NetPlayerType.OBSERVER,
);
const requiredPlayerCount = room.players.length;
if (players.length < requiredPlayerCount) {
return undefined;
}
const unreadyPlayers = players.filter((player) => !player.deck);
if (unreadyPlayers.length !== 1) {
return undefined;
}
return unreadyPlayers[0];
}
private async tickReadyTimeout(
runtime: WaitForPlayerTickRuntime,
room: Room,
nowMs: number,
) {
if (
runtime.options.raadyTimeoutMs <= 0 ||
room.duelStage !== DuelStage.Begin ||
this.getDisconnectedCount(room) > 0
) {
this.clearReadyState(room);
return;
}
const target = this.getReadyTimeoutTarget(room);
if (!target) {
this.clearReadyState(room);
return;
}
if (room.waitForPlayerReadyTargetPos !== target.pos) {
room.waitForPlayerReadyTargetPos = target.pos;
room.waitForPlayerReadyDeadlineMs = nowMs + runtime.options.raadyTimeoutMs;
room.waitForPlayerReadyWarnRemain = undefined;
}
const readyDeadlineMs = room.waitForPlayerReadyDeadlineMs;
if (!readyDeadlineMs) {
this.clearReadyState(room);
return;
}
const remainSeconds = Math.ceil((readyDeadlineMs - nowMs) / 1000);
if (remainSeconds > 0) {
if (
remainSeconds % 5 === 0 &&
room.waitForPlayerReadyWarnRemain !== remainSeconds
) {
room.waitForPlayerReadyWarnRemain = remainSeconds;
await room.sendChat(
`${target.name} ${remainSeconds} #{kick_count_down}`,
remainSeconds <= 9 ? ChatColor.RED : ChatColor.LIGHTBLUE,
);
}
return;
}
const latestTarget = this.getReadyTimeoutTarget(room);
this.clearReadyState(room);
if (
!latestTarget ||
latestTarget.pos !== target.pos ||
latestTarget.disconnected ||
latestTarget.roomName !== room.name ||
!!latestTarget.deck
) {
return;
}
await room.sendChat(`${latestTarget.name} #{kicked_by_system}`, ChatColor.RED);
latestTarget.disconnect();
}
private async tickHangTimeout(
runtime: WaitForPlayerTickRuntime,
room: Room,
nowMs: number,
) {
if (
runtime.options.hangTimeoutMs <= 0 ||
room.duelStage === DuelStage.Begin ||
room.duelStage === DuelStage.Siding
) {
room.waitForPlayerHangWarnElapsed = undefined;
return;
}
if (this.getDisconnectedCount(room) > 0) {
return;
}
const waitingPos = room.waitForPlayerPos;
if (waitingPos == null) {
room.waitForPlayerHangWarnElapsed = undefined;
return;
}
const waitingPlayer = room.players[waitingPos];
if (
!waitingPlayer ||
waitingPlayer.pos !== waitingPos ||
waitingPlayer.pos >= NetPlayerType.OBSERVER ||
waitingPlayer.disconnected ||
waitingPlayer.roomName !== room.name
) {
room.waitForPlayerHangWarnElapsed = undefined;
return;
}
if (!room.lastActiveTime) {
return;
}
const elapsedMs = nowMs - room.lastActiveTime.getTime();
if (elapsedMs >= runtime.options.hangTimeoutMs) {
room.lastActiveTime = new Date(nowMs);
room.waitForPlayerHangWarnElapsed = undefined;
await room.sendChat(
`${waitingPlayer.name} #{kicked_by_system}`,
ChatColor.RED,
);
waitingPlayer.disconnect();
return;
}
const elapsedSeconds = Math.floor(elapsedMs / 1000);
if (
elapsedMs >= runtime.options.hangTimeoutMs - 20_000 &&
elapsedSeconds % 10 === 0 &&
room.waitForPlayerHangWarnElapsed !== elapsedSeconds
) {
room.waitForPlayerHangWarnElapsed = elapsedSeconds;
const remainSeconds = Math.ceil(
(runtime.options.hangTimeoutMs - elapsedMs) / 1000,
);
if (remainSeconds > 0) {
await room.sendChat(
`${waitingPlayer.name} #{afk_warn_part1}${remainSeconds}#{afk_warn_part2}`,
ChatColor.RED,
);
}
}
}
private setWaitForPlayer(room: Room, ...clients: (Client | undefined)[]) {
const playerPoses = clients
.filter(
(client): client is Client =>
!!client && client.pos < NetPlayerType.OBSERVER,
)
.map((client) => client.pos);
const uniquePoses = Array.from(new Set(playerPoses));
room.waitForPlayerPos = uniquePoses[0];
room.waitingForPlayerOther = uniquePoses.slice(1);
this.logger.debug(
{
roomName: room.name,
waitForPlayerPos: room.waitForPlayerPos,
waitingForPlayerOther: room.waitingForPlayerOther,
},
'Set wait for player',
);
}
private resolveWaitForPlayer(room: Room, client: Client) {
const waitingOthers = [...(room.waitingForPlayerOther || [])];
if (client.pos === room.waitForPlayerPos) {
room.waitForPlayerPos = waitingOthers.shift();
room.waitingForPlayerOther = waitingOthers;
return;
}
const otherIndex = waitingOthers.indexOf(client.pos);
if (otherIndex >= 0) {
waitingOthers.splice(otherIndex, 1);
room.waitingForPlayerOther = waitingOthers;
}
this.logger.debug(
{
roomName: room.name,
waitForPlayerPos: room.waitForPlayerPos,
waitingForPlayerOther: room.waitingForPlayerOther,
},
'Resolved wait for player',
);
}
private refreshLastActiveTime(room: Room, backoffMs = 0) {
room.lastActiveTime = new Date(Date.now() - Math.max(0, backoffMs));
this.logger.debug(
{
roomName: room.name,
lastActiveTime: room.lastActiveTime,
},
'Refreshed last active time for wait for player',
);
}
}
import { ChatColor, YGOProCtosJoinGame } from 'ygopro-msg-encode'; import { ChatColor, YGOProCtosJoinGame } from 'ygopro-msg-encode';
import { Context } from '../app'; import { Context } from '../../app';
import { WindBotProvider } from './windbot-provider'; import { WindBotProvider } from './windbot-provider';
import { RoomManager } from '../room'; import { RoomManager } from '../../room';
import { fillRandomString } from '../utility/fill-random-string'; import { MAX_ROOM_NAME_LENGTH } from '../../constants/room';
import { fillRandomString } from '../../utility/fill-random-string';
import { parseWindbotOptions } from './utility'; import { parseWindbotOptions } from './utility';
const getDisplayLength = (text: string) => const getDisplayLength = (text: string) =>
...@@ -25,6 +26,7 @@ export class JoinWindbotAi { ...@@ -25,6 +26,7 @@ export class JoinWindbotAi {
const existingRoom = this.roomManager.findByName(msg.pass); const existingRoom = this.roomManager.findByName(msg.pass);
if (existingRoom) { if (existingRoom) {
existingRoom.noHost = true;
return existingRoom.join(client); return existingRoom.join(client);
} }
...@@ -49,6 +51,7 @@ export class JoinWindbotAi { ...@@ -49,6 +51,7 @@ export class JoinWindbotAi {
lflist: -1, lflist: -1,
time_limit: 0, time_limit: 0,
}); });
room.noHost = true;
room.noReconnect = true; room.noReconnect = true;
room.windbot = { room.windbot = {
name: '', name: '',
...@@ -102,7 +105,7 @@ export class JoinWindbotAi { ...@@ -102,7 +105,7 @@ export class JoinWindbotAi {
} else { } else {
prefix = `${pass}#`; prefix = `${pass}#`;
} }
const roomName = fillRandomString(prefix, 19); const roomName = fillRandomString(prefix, MAX_ROOM_NAME_LENGTH);
if (!this.roomManager.findByName(roomName)) { if (!this.roomManager.findByName(roomName)) {
return roomName; return roomName;
} }
......
import { ChatColor, YGOProCtosJoinGame } from 'ygopro-msg-encode'; import { ChatColor, YGOProCtosJoinGame } from 'ygopro-msg-encode';
import { Context } from '../app'; import { Context } from '../../app';
import { WindBotProvider } from './windbot-provider'; import { WindBotProvider } from './windbot-provider';
import { RoomManager } from '../room'; import { RoomManager } from '../../room';
export class JoinWindbotToken { export class JoinWindbotToken {
private windbotProvider = this.ctx.get(() => WindBotProvider); private windbotProvider = this.ctx.get(() => WindBotProvider);
......
import { Observable, fromEvent, merge } from 'rxjs'; import { Observable, fromEvent, merge } from 'rxjs';
import { map, take } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
import WebSocket, { RawData } from 'ws'; import WebSocket, { RawData } from 'ws';
import { Context } from '../app'; import { Context } from '../../app';
import { Client } from '../client'; import { Client } from '../../client';
export class ReverseWsClient extends Client { export class ReverseWsClient extends Client {
constructor( constructor(
......
import { createAppContext } from 'nfkit'; import { createAppContext } from 'nfkit';
import { ContextState } from '../app'; import { ContextState } from '../../app';
import { WindBotProvider } from './windbot-provider'; import { WindBotProvider } from './windbot-provider';
import { WindbotSpawner } from './windbot-spawner'; import { WindbotSpawner } from './windbot-spawner';
......
...@@ -2,9 +2,9 @@ import cryptoRandomString from 'crypto-random-string'; ...@@ -2,9 +2,9 @@ import cryptoRandomString from 'crypto-random-string';
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
import { ChatColor } from 'ygopro-msg-encode'; import { ChatColor } from 'ygopro-msg-encode';
import WebSocket from 'ws'; import WebSocket from 'ws';
import { Context } from '../app'; import { Context } from '../../app';
import { ClientHandler } from '../client'; import { ClientHandler } from '../../client';
import { OnRoomFinalize, Room } from '../room'; import { OnRoomFinalize, Room } from '../../room';
import type { import type {
RequestWindbotJoinOptions, RequestWindbotJoinOptions,
WindbotData, WindbotData,
...@@ -12,13 +12,13 @@ import type { ...@@ -12,13 +12,13 @@ import type {
} from './utility'; } from './utility';
import { ReverseWsClient } from './reverse-ws-client'; import { ReverseWsClient } from './reverse-ws-client';
declare module '../client' { declare module '../../client' {
interface Client { interface Client {
windbot?: WindbotData; windbot?: WindbotData;
} }
} }
declare module '../room' { declare module '../../room' {
interface Room { interface Room {
windbot?: WindbotData; windbot?: WindbotData;
} }
......
import { ChildProcess, spawn } from 'node:child_process'; import { ChildProcess, spawn } from 'node:child_process';
import { Context } from '../app'; import { Context } from '../../app';
import { WindBotProvider } from './windbot-provider'; import { WindBotProvider } from './windbot-provider';
export class WindbotSpawner { export class WindbotSpawner {
......
import { createAppContext } from 'nfkit'; import { createAppContext } from 'nfkit';
import { ContextState } from '../app'; import { ContextState } from '../app';
import { ClientVersionCheck } from '../feats/client-version-check'; import { ClientVersionCheck } from '../feats';
import { JoinWindbotAi, JoinWindbotToken } from '../windbot'; import { JoinWindbotAi, JoinWindbotToken } from '../feats/windbot';
import { JoinRoom } from './join-room'; import { JoinRoom } from './join-room';
import { JoinFallback } from './fallback'; import { JoinFallback } from './fallback';
import { JoinPrechecks } from './join-prechecks'; import { JoinPrechecks } from './join-prechecks';
import { RandomDuelJoinHandler } from './random-duel-join-handler';
export const JoinHandlerModule = createAppContext<ContextState>() export const JoinHandlerModule = createAppContext<ContextState>()
.provide(ClientVersionCheck) .provide(ClientVersionCheck)
.provide(JoinPrechecks) .provide(JoinPrechecks)
.provide(JoinWindbotToken) .provide(JoinWindbotToken)
.provide(RandomDuelJoinHandler)
.provide(JoinWindbotAi) .provide(JoinWindbotAi)
.provide(JoinRoom) .provide(JoinRoom)
.provide(JoinFallback) .provide(JoinFallback)
......
import { ChatColor, YGOProCtosJoinGame } from 'ygopro-msg-encode';
import { Context } from '../app';
import { RandomDuelProvider } from '../feats';
export class RandomDuelJoinHandler {
private randomDuelProvider = this.ctx.get(() => RandomDuelProvider);
constructor(private ctx: Context) {
if (!this.randomDuelProvider.enabled) {
return;
}
this.ctx.middleware(YGOProCtosJoinGame, async (msg, client, next) => {
msg.pass = (msg.pass || '').trim();
const type = this.randomDuelProvider.resolveRandomType(msg.pass);
if (type == null) {
return next();
}
const room = await this.randomDuelProvider.findOrCreateRandomRoom(
type,
client.ip,
);
if (!room) {
return client.die('#{create_room_failed}', ChatColor.RED);
}
return room.join(client);
});
}
}
...@@ -2,8 +2,11 @@ export * from './room'; ...@@ -2,8 +2,11 @@ export * from './room';
export * from './room-manager'; export * from './room-manager';
export * from './duel-stage'; export * from './duel-stage';
export * from './room-event/on-room-finalize'; export * from './room-event/on-room-finalize';
export * from './room-event/on-room-finger';
export * from './room-event/on-room-game-start'; export * from './room-event/on-room-game-start';
export * from './room-event/on-room-join-player';
export * from './room-event/on-room-leave-player'; export * from './room-event/on-room-leave-player';
export * from './room-event/on-room-select-tp';
export * from './room-event/on-room-siding-ready'; export * from './room-event/on-room-siding-ready';
export * from './room-event/on-room-siding-start'; export * from './room-event/on-room-siding-start';
export * from './room-event/on-room-win'; export * from './room-event/on-room-win';
......
import { Client } from '../../client';
import { Room } from '../room';
import { RoomEvent } from './room-event';
export class OnRoomFinger extends RoomEvent {
constructor(room: Room, public fingerPlayers: [Client, Client]) {
super(room);
}
}
import { Client } from '../../client';
import { Room } from '../room';
import { RoomEvent } from './room-event';
export class OnRoomSelectTp extends RoomEvent {
constructor(room: Room, public selector: Client) {
super(room);
}
}
...@@ -52,6 +52,7 @@ import { ...@@ -52,6 +52,7 @@ import {
YGOProCtosTimeConfirm, YGOProCtosTimeConfirm,
YGOProMsgWaiting, YGOProMsgWaiting,
YGOProStocTimeLimit, YGOProStocTimeLimit,
YGOProMsgMatchKill,
} from 'ygopro-msg-encode'; } from 'ygopro-msg-encode';
import { DefaultHostInfoProvider } from './default-hostinfo-provder'; import { DefaultHostInfoProvider } from './default-hostinfo-provder';
import { import {
...@@ -96,6 +97,8 @@ import { OnRoomCreate } from './room-event/on-room-create'; ...@@ -96,6 +97,8 @@ import { OnRoomCreate } from './room-event/on-room-create';
import { OnRoomFinalize } from './room-event/on-room-finalize'; import { OnRoomFinalize } from './room-event/on-room-finalize';
import { OnRoomSidingStart } from './room-event/on-room-siding-start'; import { OnRoomSidingStart } from './room-event/on-room-siding-start';
import { OnRoomSidingReady } from './room-event/on-room-siding-ready'; import { OnRoomSidingReady } from './room-event/on-room-siding-ready';
import { OnRoomFinger } from './room-event/on-room-finger';
import { OnRoomSelectTp } from './room-event/on-room-select-tp';
const { OcgcoreScriptConstants } = _OcgcoreConstants; const { OcgcoreScriptConstants } = _OcgcoreConstants;
...@@ -124,6 +127,7 @@ export class Room { ...@@ -124,6 +127,7 @@ export class Room {
return (this.hostinfo.mode & 0x2) !== 0; return (this.hostinfo.mode & 0x2) !== 0;
} }
noHost = false;
players = new Array<Client | undefined>(this.isTag ? 4 : 2); players = new Array<Client | undefined>(this.isTag ? 4 : 2);
watchers = new Set<Client>(); watchers = new Set<Client>();
get playingPlayers() { get playingPlayers() {
...@@ -345,7 +349,7 @@ export class Room { ...@@ -345,7 +349,7 @@ export class Room {
async join(client: Client) { async join(client: Client) {
client.roomName = this.name; client.roomName = this.name;
client.isHost = !this.allPlayers.length; client.isHost = this.noHost ? false : !this.allPlayers.length;
const firstEmptyPlayerSlot = this.players.findIndex((p) => !p); const firstEmptyPlayerSlot = this.players.findIndex((p) => !p);
const isPlayer = const isPlayer =
firstEmptyPlayerSlot >= 0 && this.duelStage === DuelStage.Begin; firstEmptyPlayerSlot >= 0 && this.duelStage === DuelStage.Begin;
...@@ -401,10 +405,27 @@ export class Room { ...@@ -401,10 +405,27 @@ export class Room {
duelStage = DuelStage.Begin; duelStage = DuelStage.Begin;
duelRecords: DuelRecord[] = []; duelRecords: DuelRecord[] = [];
private overrideScore?: [number | undefined, number | undefined];
setOverrideScore(duelPos: 0 | 1, value: number) {
this.overrideScore = this.overrideScore || [undefined, undefined];
this.overrideScore[duelPos] = value;
}
get score() { get score() {
return [0, 1].map( const score: [number, number] = [0, 0];
(p) => this.duelRecords.filter((d) => d.winPosition === p).length, for (const duelRecord of this.duelRecords) {
); if (duelRecord.winPosition === 0 || duelRecord.winPosition === 1) {
score[duelRecord.winPosition] += 1;
}
}
for (const duelPos of [0, 1] as const) {
const override = this.overrideScore?.[duelPos];
if (override != null) {
score[duelPos] = override;
}
}
return score;
} }
private async sendReplays(client: Client) { private async sendReplays(client: Client) {
...@@ -437,7 +458,10 @@ export class Room { ...@@ -437,7 +458,10 @@ export class Room {
for (const p of this.watchers) { for (const p of this.watchers) {
p.send(new YGOProStocWaitingSide()); p.send(new YGOProStocWaitingSide());
} }
await this.ctx.dispatch(new OnRoomSidingStart(this), this.playingPlayers[0]); await this.ctx.dispatch(
new OnRoomSidingStart(this),
this.playingPlayers[0],
);
} }
get lastDuelRecord() { get lastDuelRecord() {
...@@ -451,7 +475,7 @@ export class Room { ...@@ -451,7 +475,7 @@ export class Room {
} catch {} } catch {}
} }
async win(winMsg: Partial<YGOProMsgWin>, forceWinMatch = false) { async win(winMsg: Partial<YGOProMsgWin>, forceWinMatch?: number) {
this.resetResponseState(); this.resetResponseState();
this.disposeOcgcore(); this.disposeOcgcore();
this.ocgcore = undefined; this.ocgcore = undefined;
...@@ -481,10 +505,16 @@ export class Room { ...@@ -481,10 +505,16 @@ export class Room {
if (lastDuelRecord) { if (lastDuelRecord) {
lastDuelRecord.winPosition = duelPos; lastDuelRecord.winPosition = duelPos;
} }
if (typeof forceWinMatch === 'number') {
const loseDuelPos = (1 - duelPos) as 0 | 1;
this.setOverrideScore(loseDuelPos, -Math.abs(forceWinMatch));
}
const score = this.score;
this.logger.debug( this.logger.debug(
`Player ${duelPos} wins the duel. Current score: ${this.score.join('-')}`, `Player ${duelPos} wins the duel. Current score: ${score.join('-')}`,
); );
const winMatch = forceWinMatch || this.score[duelPos] >= this.winMatchCount; const winMatch =
forceWinMatch != null || score[duelPos] >= this.winMatchCount;
if (!winMatch) { if (!winMatch) {
await this.changeSide(); await this.changeSide();
} }
...@@ -533,10 +563,9 @@ export class Room { ...@@ -533,10 +563,9 @@ export class Room {
p.send(client.prepareChangePacket(PlayerChangeState.LEAVE)); p.send(client.prepareChangePacket(PlayerChangeState.LEAVE));
}); });
} else { } else {
this.score[this.getDuelPos(client)] = -9;
await this.win( await this.win(
{ player: 1 - this.getIngameDuelPos(client), type: 0x4 }, { player: 1 - this.getIngameDuelPos(client), type: 0x4 },
true, 9,
); );
} }
if (client.isHost) { if (client.isHost) {
...@@ -805,6 +834,12 @@ export class Room { ...@@ -805,6 +834,12 @@ export class Room {
// Auto-ready: send PlayerChange READY to all players (client.deck 已设置,自动为 READY) // Auto-ready: send PlayerChange READY to all players (client.deck 已设置,自动为 READY)
const changeMsg = client.prepareChangePacket(); const changeMsg = client.prepareChangePacket();
this.allPlayers.forEach((p) => p.send(changeMsg)); this.allPlayers.forEach((p) => p.send(changeMsg));
if (this.noHost) {
const allReadyAndFull = this.players.every((player) => !!player?.deck);
if (allReadyAndFull) {
await this.startGame();
}
}
} else if (this.duelStage === DuelStage.Siding) { } else if (this.duelStage === DuelStage.Siding) {
// In Siding stage, send DUEL_START to the player who submitted deck // In Siding stage, send DUEL_START to the player who submitted deck
// Siding 阶段不发 DeckCount // Siding 阶段不发 DeckCount
...@@ -910,16 +945,27 @@ export class Room { ...@@ -910,16 +945,27 @@ export class Room {
this.firstgoPos = firstgoPos; this.firstgoPos = firstgoPos;
this.duelStage = DuelStage.FirstGo; this.duelStage = DuelStage.FirstGo;
const firstgoPlayer = this.getDuelPosPlayers(firstgoPos)[0]; const firstgoPlayer = this.getDuelPosPlayers(firstgoPos)[0];
if (!firstgoPlayer) {
return;
}
firstgoPlayer.send(new YGOProStocSelectTp()); firstgoPlayer.send(new YGOProStocSelectTp());
await this.ctx.dispatch(
new OnRoomSelectTp(this, firstgoPlayer),
firstgoPlayer,
);
} }
private async toFinger() { private async toFinger() {
this.duelStage = DuelStage.Finger; this.duelStage = DuelStage.Finger;
// 只有每方的第一个玩家猜拳 // 只有每方的第一个玩家猜拳
const fingerPlayers = [0, 1].map((p) => this.getDuelPosPlayers(p)[0]); const duelPos0 = this.getDuelPosPlayers(0)[0];
fingerPlayers.forEach((p) => { const duelPos1 = this.getDuelPosPlayers(1)[0];
p.send(new YGOProStocSelectHand()); if (!duelPos0 || !duelPos1) {
}); return;
}
duelPos0.send(new YGOProStocSelectHand());
duelPos1.send(new YGOProStocSelectHand());
await this.ctx.dispatch(new OnRoomFinger(this, [duelPos0, duelPos1]), duelPos0);
} }
@RoomMethod({ allowInDuelStages: DuelStage.Finger }) @RoomMethod({ allowInDuelStages: DuelStage.Finger })
...@@ -1618,8 +1664,13 @@ export class Room { ...@@ -1618,8 +1664,13 @@ export class Room {
); );
} }
return next(); return next();
})
.middleware(YGOProMsgMatchKill, async (message, next) => {
this.matchKilled = true;
return next();
}); });
private matchKilled = false;
private responsePos?: number; private responsePos?: number;
private async advance() { private async advance() {
...@@ -1660,7 +1711,7 @@ export class Room { ...@@ -1660,7 +1711,7 @@ export class Room {
const handled = await this.dispatchGameMsg(message); const handled = await this.dispatchGameMsg(message);
if (handled instanceof YGOProMsgWin) { if (handled instanceof YGOProMsgWin) {
return this.win(handled); return this.win(handled, this.matchKilled ? 1 : undefined);
} }
await this.routeGameMsg(handled); await this.routeGameMsg(handled);
} }
......
import { AppContext } from 'nfkit';
import { DataSource } from 'typeorm';
import { ConfigService } from './config';
import { Logger } from './logger';
import { RandomDuelScore } from '../feats/random-duel';
export class TypeormLoader {
constructor(private ctx: AppContext) {}
database: DataSource | undefined;
setDatabase(database: DataSource | undefined) {
this.database = database;
return this;
}
}
export const TypeormFactory = async (ctx: AppContext) => {
const loader = new TypeormLoader(ctx);
const config = ctx.get(ConfigService).config;
const logger = ctx.get(Logger).createLogger('TypeormLoader');
const host = config.getString('DB_HOST');
if (!host) {
logger.info('database disabled because DB_HOST is empty');
return loader.setDatabase(undefined);
}
const port = config.getInt('DB_PORT') || 5432;
const username = config.getString('DB_USER');
const password = config.getString('DB_PASS');
const database = config.getString('DB_NAME');
const synchronize = !config.getBoolean('DB_NO_INIT');
const dataSource = new DataSource({
type: 'postgres',
host,
port,
username,
password,
database,
synchronize,
entities: [RandomDuelScore],
});
try {
await dataSource.initialize();
logger.info(
{
host,
port,
database,
synchronize,
},
'Database initialized',
);
return loader.setDatabase(dataSource);
} catch (error) {
logger.error(
{
host,
port,
database,
err: error,
},
'Database initialization failed',
);
throw error;
}
};
export class ValueContainer<T> {
constructor(public value: T) {}
use(value: T) {
this.value = value;
return this;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment