diff --git a/package.json b/package.json index 72bc36f1..b36b5420 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "steamapi": "3.0.12", "steamid": "2.1.0", "tailwindcss": "3.4.16", + "umzug": "3.8.2", "ws": "8.18.0", "zod": "3.24.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c0943d0..1a12e5a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,9 @@ importers: tailwindcss: specifier: 3.4.16 version: 3.4.16 + umzug: + specifier: 3.8.2 + version: 3.8.2(@types/node@22.10.2) ws: specifier: 8.18.0 version: 8.18.0 @@ -779,6 +782,25 @@ packages: cpu: [x64] os: [win32] + '@rushstack/node-core-library@5.10.0': + resolution: {integrity: sha512-2pPLCuS/3x7DCd7liZkqOewGM0OzLyCacdvOe8j6Yrx9LkETGnxul1t7603bIaB8nUAooORcct9fFDOQMbWAgw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/terminal@0.14.3': + resolution: {integrity: sha512-csXbZsAdab/v8DbU1sz7WC2aNaKArcdS/FPmXMOXEj/JBBZMvDK0+1b4Qao0kkG0ciB1Qe86/Mb68GjH6/TnMw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@4.23.1': + resolution: {integrity: sha512-40jTmYoiu/xlIpkkRsVfENtBq4CW3R4azbL0Vmda+fMwHWqss6wwf/Cy/UJmMqIzpfYc2OTnjYP1ZLD3CmyeCA==} + '@sindresorhus/merge-streams@2.3.0': resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} @@ -795,6 +817,9 @@ packages: '@tsconfig/strictest@2.0.5': resolution: {integrity: sha512-ec4tjL2Rr0pkZ5hww65c+EEPYwxOi4Ryv+0MtjeaSQRJyq322Q27eOQiFbuNgw2hpL4hB1/W/HBGk3VKS43osg==} + '@types/argparse@1.0.38': + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/eslint@9.6.0': resolution: {integrity: sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==} @@ -933,6 +958,14 @@ packages: resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==} engines: {node: '>=12'} + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -944,6 +977,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.13.0: + resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -973,6 +1009,9 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1322,6 +1361,10 @@ packages: electron-to-chromium@1.5.5: resolution: {integrity: sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==} + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1536,6 +1579,10 @@ packages: resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} engines: {node: '>=14.14'} + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1665,6 +1712,10 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -1737,6 +1788,9 @@ packages: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1768,6 +1822,9 @@ packages: json-stringify-pretty-compact@4.0.0: resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==} + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -1864,6 +1921,10 @@ packages: resolution: {integrity: sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==} engines: {node: 20 || >=22} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} @@ -2176,6 +2237,10 @@ packages: resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} engines: {node: '>=12'} + pony-cause@2.1.11: + resolution: {integrity: sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==} + engines: {node: '>=12.0.0'} + postcss-calc@10.0.2: resolution: {integrity: sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg==} engines: {node: ^18.12 || ^20.9 || >=22.0} @@ -2600,6 +2665,11 @@ packages: secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -2677,6 +2747,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -2697,6 +2770,10 @@ packages: stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2735,6 +2812,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -2829,6 +2910,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@4.30.0: + resolution: {integrity: sha512-G6zXWS1dLj6eagy6sVhOMQiLtJdxQBHIA9Z6HFUNLOlr6MFOgzV8wvmidtPONfPtEUv0uZsy77XJNzTAfwPDaA==} + engines: {node: '>=16'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -2848,6 +2933,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + umzug@3.8.2: + resolution: {integrity: sha512-BEWEF8OJjTYVC56GjELeHl/1XjFejrD7aHzn+HldRJTx+pL1siBrKHZC8n4K/xL3bEzVA9o++qD1tK2CpZu4KA==} + engines: {node: '>=12'} + undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} @@ -2855,6 +2944,10 @@ packages: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -2993,6 +3086,9 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.5.0: resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} engines: {node: '>= 14'} @@ -3448,6 +3544,35 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.25.0': optional: true + '@rushstack/node-core-library@5.10.0(@types/node@22.10.2)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 7.0.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.8 + semver: 7.5.4 + optionalDependencies: + '@types/node': 22.10.2 + + '@rushstack/terminal@0.14.3(@types/node@22.10.2)': + dependencies: + '@rushstack/node-core-library': 5.10.0(@types/node@22.10.2) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 22.10.2 + + '@rushstack/ts-command-line@4.23.1(@types/node@22.10.2)': + dependencies: + '@rushstack/terminal': 0.14.3(@types/node@22.10.2) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + '@sindresorhus/merge-streams@2.3.0': {} '@tailwindcss/typography@0.5.15(tailwindcss@3.4.16)': @@ -3462,6 +3587,8 @@ snapshots: '@tsconfig/strictest@2.0.5': {} + '@types/argparse@1.0.38': {} + '@types/eslint@9.6.0': dependencies: '@types/estree': 1.0.5 @@ -3639,6 +3766,14 @@ snapshots: clean-stack: 4.2.0 indent-string: 5.0.0 + ajv-draft-04@1.0.0(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + + ajv-formats@3.0.1(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -3650,6 +3785,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.13.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -3676,6 +3818,10 @@ snapshots: arg@5.0.2: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} array-union@2.1.0: {} @@ -4048,6 +4194,8 @@ snapshots: electron-to-chromium@1.5.5: {} + emittery@0.13.1: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -4334,6 +4482,12 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fsevents@2.3.2: optional: true @@ -4472,6 +4626,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-lazy@4.0.0: {} + imurmurhash@0.1.4: {} indent-string@5.0.0: {} @@ -4526,6 +4682,8 @@ snapshots: jiti@1.21.6: {} + jju@1.4.0: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -4555,6 +4713,10 @@ snapshots: json-stringify-pretty-compact@4.0.0: {} + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + jsonfile@6.1.0: dependencies: universalify: 2.0.1 @@ -4646,6 +4808,10 @@ snapshots: lru-cache@11.0.1: {} + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + magic-string@0.30.12: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -4902,6 +5068,8 @@ snapshots: dependencies: queue-lit: 1.5.2 + pony-cause@2.1.11: {} + postcss-calc@10.0.2(postcss@8.4.49): dependencies: postcss: 8.4.49 @@ -5263,6 +5431,10 @@ snapshots: secure-json-parse@2.7.0: {} + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + semver@7.6.3: {} set-cookie-parser@2.6.0: {} @@ -5335,6 +5507,8 @@ snapshots: split2@4.2.0: {} + sprintf-js@1.0.3: {} + stackback@0.0.2: {} statuses@2.0.1: {} @@ -5350,6 +5524,8 @@ snapshots: stream-shift@1.0.3: {} + string-argv@0.3.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5396,6 +5572,10 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} svgo@3.3.2: @@ -5509,6 +5689,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@4.30.0: {} + type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -5528,10 +5710,22 @@ snapshots: typescript@5.7.2: {} + umzug@3.8.2(@types/node@22.10.2): + dependencies: + '@rushstack/ts-command-line': 4.23.1(@types/node@22.10.2) + emittery: 0.13.1 + fast-glob: 3.3.2 + pony-cause: 2.1.11 + type-fest: 4.30.0 + transitivePeerDependencies: + - '@types/node' + undici-types@6.20.0: {} unicorn-magic@0.1.0: {} + universalify@0.1.2: {} + universalify@2.0.1: {} unset-value@2.0.1: @@ -5654,6 +5848,8 @@ snapshots: y18n@5.0.8: {} + yallist@4.0.0: {} + yaml@2.5.0: {} yargs-parser@20.2.9: {} diff --git a/src/database/models/game-event.model.ts b/src/database/models/game-event.model.ts index 1c394149..012ac023 100644 --- a/src/database/models/game-event.model.ts +++ b/src/database/models/game-event.model.ts @@ -1,7 +1,7 @@ -import type { ObjectId } from 'mongodb' import type { Tf2Team } from '../../shared/types/tf2-team' import type { Tf2ClassName } from '../../shared/types/tf2-class-name' import type { Bot } from '../../shared/types/bot' +import type { SteamId64 } from '../../shared/types/steam-id-64' export enum GameEventType { gameCreated = 'created', @@ -35,7 +35,7 @@ export interface GameEnded { event: GameEventType.gameEnded at: Date reason: GameEndedReason - actor?: ObjectId + actor?: SteamId64 | Bot } export interface GameStarted { @@ -47,7 +47,7 @@ export interface GameServerAssigned { event: GameEventType.gameServerAssigned at: Date gameServerName: string - actor?: ObjectId + actor?: SteamId64 | Bot } export interface GameServerInitialized { @@ -58,36 +58,36 @@ export interface GameServerInitialized { export interface PlayerJoinedGameServer { event: GameEventType.playerJoinedGameServer at: Date - player: ObjectId + player: SteamId64 } export interface PlayerJoinedGameServerTeam { event: GameEventType.playerJoinedGameServerTeam at: Date - player: ObjectId + player: SteamId64 team: Tf2Team } export interface PlayerLeftGameServer { event: GameEventType.playerLeftGameServer at: Date - player: ObjectId + player: SteamId64 } export interface SubstituteRequested { event: GameEventType.substituteRequested at: Date - player: ObjectId + player: SteamId64 gameClass: Tf2ClassName - actor: ObjectId | Bot + actor: SteamId64 | Bot reason?: string | undefined } export interface PlayerReplaced { event: GameEventType.playerReplaced at: Date - replacee: ObjectId - replacement: ObjectId + replacee: SteamId64 + replacement: SteamId64 } export interface RoundEnded { diff --git a/src/database/models/game-slot.model.ts b/src/database/models/game-slot.model.ts index bfe82fef..974c2905 100644 --- a/src/database/models/game-slot.model.ts +++ b/src/database/models/game-slot.model.ts @@ -1,6 +1,6 @@ -import type { ObjectId } from 'mongodb' import type { Tf2Team } from '../../shared/types/tf2-team' import type { Tf2ClassName } from '../../shared/types/tf2-class-name' +import type { SteamId64 } from '../../shared/types/steam-id-64' export enum SlotStatus { active = 'active', @@ -15,7 +15,7 @@ export enum PlayerConnectionStatus { } export interface GameSlotModel { - player: ObjectId // TODO change to SteamId64 + player: SteamId64 team: Tf2Team gameClass: Tf2ClassName status: SlotStatus diff --git a/src/games/calculate-join-gameserver-timeout.test.ts b/src/games/calculate-join-gameserver-timeout.test.ts index 2cb5716b..c227a4d0 100644 --- a/src/games/calculate-join-gameserver-timeout.test.ts +++ b/src/games/calculate-join-gameserver-timeout.test.ts @@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { calculateJoinGameserverTimeout } from './calculate-join-gameserver-timeout' import { GameState, type GameModel, type GameNumber } from '../database/models/game.model' import type { SteamId64 } from '../shared/types/steam-id-64' -import { ObjectId } from 'mongodb' import { Tf2Team } from '../shared/types/tf2-team' import { Tf2ClassName } from '../shared/types/tf2-class-name' import { PlayerConnectionStatus, SlotStatus } from '../database/models/game-slot.model' @@ -15,7 +14,7 @@ vi.mock('../configuration', () => ({ }, })) -const players = vi.hoisted(() => new Map()) +const players = vi.hoisted(() => new Map()) vi.mock('../database/collections', () => ({ collections: { players: { @@ -32,15 +31,14 @@ describe('calculateJoinGameServerTimeout', () => { configuration.set('games.join_gameserver_timeout', 5 * 60 * 1000) configuration.set('games.rejoin_gameserver_timeout', 3 * 60 * 1000) steamId = 'FAKE_STEAM_ID' as SteamId64 - const _id = new ObjectId() - players.set(steamId, { _id }) + players.set(steamId, { steamId }) game = { number: 1 as GameNumber, state: GameState.created, map: 'cp_badlands', slots: [ { - player: _id, + player: steamId, team: Tf2Team.blu, gameClass: Tf2ClassName.scout, status: SlotStatus.active, @@ -71,14 +69,6 @@ describe('calculateJoinGameServerTimeout', () => { }) }) - describe('when the given player is not found', () => { - it('should throw an error', async () => { - await expect( - calculateJoinGameserverTimeout(game, 'ANOTHER_STEAM_ID' as SteamId64), - ).rejects.toThrow('player ANOTHER_STEAM_ID not found') - }) - }) - describe('when the game has been just created', () => { it('should return undefined', async () => { expect(await calculateJoinGameserverTimeout(game, steamId)).toBe(undefined) @@ -105,8 +95,8 @@ describe('calculateJoinGameServerTimeout', () => { game.events.push({ event: GameEventType.playerReplaced, at: new Date(2024, 0, 1, 12, 0), - replacement: players.get(steamId)!._id, - replacee: new ObjectId(), + replacement: steamId, + replacee: 'REPLACEE_STEAM_ID' as SteamId64, }) }) @@ -122,8 +112,8 @@ describe('calculateJoinGameServerTimeout', () => { game.events.push({ event: GameEventType.playerReplaced, at: new Date(2024, 0, 1, 12, 3), - replacement: players.get(steamId)!._id, - replacee: new ObjectId(), + replacement: steamId, + replacee: 'REPLACEE_STEAM_ID' as SteamId64, }) }) @@ -139,7 +129,7 @@ describe('calculateJoinGameServerTimeout', () => { game.events.push({ event: GameEventType.playerLeftGameServer, at: new Date(2024, 0, 1, 12, 0), - player: players.get(steamId)!._id, + player: steamId, }) }) @@ -155,7 +145,7 @@ describe('calculateJoinGameServerTimeout', () => { game.events.push({ event: GameEventType.playerLeftGameServer, at: new Date(2024, 0, 1, 12, 3), - player: players.get(steamId)!._id, + player: steamId, }) }) @@ -177,8 +167,7 @@ describe('calculateJoinGameServerTimeout', () => { beforeEach(() => { anotherSteamId = 'ANOTHER_STEAM_ID' as SteamId64 - const _id = new ObjectId() - players.set(anotherSteamId, { _id }) + players.set(anotherSteamId, { steamId: anotherSteamId }) }) it('should throw an error', async () => { @@ -204,7 +193,7 @@ describe('calculateJoinGameServerTimeout', () => { game.events.push({ event: GameEventType.playerLeftGameServer, at: new Date(2024, 0, 1, 12, 0), - player: players.get(steamId)!._id, + player: steamId, }) }) diff --git a/src/games/calculate-join-gameserver-timeout.ts b/src/games/calculate-join-gameserver-timeout.ts index ac187760..9f60a58f 100644 --- a/src/games/calculate-join-gameserver-timeout.ts +++ b/src/games/calculate-join-gameserver-timeout.ts @@ -1,7 +1,6 @@ import { configuration } from '../configuration' -import { collections } from '../database/collections' import { GameEventType } from '../database/models/game-event.model' -import { PlayerConnectionStatus } from '../database/models/game-slot.model' +import { PlayerConnectionStatus, SlotStatus } from '../database/models/game-slot.model' import { GameState, type GameModel } from '../database/models/game.model' import type { SteamId64 } from '../shared/types/steam-id-64' @@ -16,20 +15,15 @@ export async function calculateJoinGameserverTimeout( return } - const p = await collections.players.findOne({ steamId: player }) - if (p === null) { - throw new Error(`player ${player} not found`) - } - const disconnectedAt = game.events .filter(e => e.event === GameEventType.playerLeftGameServer) - .filter(e => e.player.equals(p._id)) + .filter(e => e.player === player) .sort((a, b) => b.at.getTime() - a.at.getTime()) .at(0)?.at const replacedAt = game.events .filter(e => e.event === GameEventType.playerReplaced) - .filter(e => e.replacement.equals(p._id)) + .filter(e => e.replacement === player) .sort((a, b) => b.at.getTime() - a.at.getTime()) .at(0)?.at @@ -52,11 +46,15 @@ export async function calculateJoinGameserverTimeout( } case GameState.started: { - const slot = game.slots.find(s => s.player.equals(p._id)) + const slot = game.slots.find(slot => slot.player === player) if (!slot) { throw new Error(`player ${player} not found in game ${game.number}`) } + if (slot.status !== SlotStatus.active) { + return undefined + } + if (slot.connectionStatus !== PlayerConnectionStatus.offline) { return undefined } diff --git a/src/games/create.ts b/src/games/create.ts index 33f2ee1d..68fdf7bb 100644 --- a/src/games/create.ts +++ b/src/games/create.ts @@ -33,7 +33,7 @@ export async function create( } return { - player: player._id, + player: player.steamId, team: slot.team, gameClass: slot.gameClass, status: SlotStatus.active, diff --git a/src/games/find-player-slot.ts b/src/games/find-player-slot.ts index 17ad68da..ea876f41 100644 --- a/src/games/find-player-slot.ts +++ b/src/games/find-player-slot.ts @@ -5,7 +5,7 @@ import type { SteamId64 } from '../shared/types/steam-id-64' export async function findPlayerSlot(game: GameModel, player: SteamId64) { for (const slot of game.slots.filter(s => s.status !== SlotStatus.replaced)) { - const ps = await collections.players.findOne({ _id: slot.player }) + const ps = await collections.players.findOne({ steamId: slot.player }) if (!ps) { throw new Error(`player in slot does not exist: ${slot.player.toString()}`) } diff --git a/src/games/force-end.ts b/src/games/force-end.ts index 04933d7f..a092c304 100644 --- a/src/games/force-end.ts +++ b/src/games/force-end.ts @@ -1,20 +1,10 @@ -import type { ObjectId } from 'mongodb' import { GameEndedReason, GameEventType } from '../database/models/game-event.model' import { SlotStatus } from '../database/models/game-slot.model' import { GameState, type GameNumber } from '../database/models/game.model' import type { SteamId64 } from '../shared/types/steam-id-64' import { update } from './update' -import { collections } from '../database/collections' export async function forceEnd(gameNumber: GameNumber, actor?: SteamId64) { - let _actor: ObjectId | undefined - if (actor) { - const a = await collections.players.findOne({ steamId: actor }) - if (a !== null) { - _actor = a._id - } - } - await update( { number: gameNumber, @@ -29,7 +19,7 @@ export async function forceEnd(gameNumber: GameNumber, actor?: SteamId64) { at: new Date(), event: GameEventType.gameEnded, reason: GameEndedReason.interrupted, - ...(_actor && { actor: _actor }), + ...(actor && { actor }), }, }, }, diff --git a/src/games/plugins/assign-active-game.ts b/src/games/plugins/assign-active-game.ts index f3c8f249..df9536e6 100644 --- a/src/games/plugins/assign-active-game.ts +++ b/src/games/plugins/assign-active-game.ts @@ -1,22 +1,21 @@ import fp from 'fastify-plugin' import { events } from '../../events' -import { collections } from '../../database/collections' import { players } from '../../players' +import { safe } from '../../utils/safe' export default fp( // eslint-disable-next-line @typescript-eslint/require-await async () => { - events.on('game:created', async ({ game }) => { - await Promise.all( - game.slots.map(async ({ player }) => { - const p = await collections.players.findOne({ _id: player }) - if (p === null) { - throw new Error(`player not found: ${player.toString()}`) - } - await players.update(p.steamId, { $set: { activeGame: game.number } }) - }), - ) - }) + events.on( + 'game:created', + safe(async ({ game }) => { + await Promise.all( + game.slots.map(async ({ player }) => { + await players.update(player, { $set: { activeGame: game.number } }) + }), + ) + }), + ) }, { name: 'assign active game' }, ) diff --git a/src/games/plugins/auto-substitute-players.ts b/src/games/plugins/auto-substitute-players.ts index 673cebbd..0498bede 100644 --- a/src/games/plugins/auto-substitute-players.ts +++ b/src/games/plugins/auto-substitute-players.ts @@ -4,9 +4,9 @@ import { configuration } from '../../configuration' import { tasks } from '../../tasks' import { requestSubstitute } from '../request-substitute' import { PlayerConnectionStatus, SlotStatus } from '../../database/models/game-slot.model' -import { collections } from '../../database/collections' import { whenGameEnds } from '../when-game-ends' import { calculateJoinGameserverTimeout } from '../calculate-join-gameserver-timeout' +import { safe } from '../../utils/safe' export default fp( async () => { @@ -34,47 +34,44 @@ export default fp( tasks.cancel('games:autoSubstitutePlayer', { gameNumber: game.number, player: replacee }) }) - events.on('game:gameServerInitialized', async ({ game }) => { - const joinTimeout = await configuration.get('games.join_gameserver_timeout') - if (joinTimeout === 0) { - return - } + events.on( + 'game:gameServerInitialized', + safe(async ({ game }) => { + const joinTimeout = await configuration.get('games.join_gameserver_timeout') + if (joinTimeout <= 0) { + return + } - const players = await Promise.all( game.slots .filter(slot => slot.status === SlotStatus.active) - .map(async slot => { - const player = await collections.players.findOne({ _id: slot.player }) - if (!player) { - throw new Error(`player not found: ${slot.player}`) - } + .map(({ player }) => player) + .forEach(player => { + tasks.schedule('games:autoSubstitutePlayer', joinTimeout, { + gameNumber: game.number, + player, + }) + }) + }), + ) - return player.steamId - }), - ) - players.forEach(player => { - tasks.schedule('games:autoSubstitutePlayer', joinTimeout, { + events.on( + 'game:playerReplaced', + safe(async ({ game, replacement }) => { + const timeout = await calculateJoinGameserverTimeout(game, replacement) + if (!timeout) { + return + } + + tasks.schedule('games:autoSubstitutePlayer', timeout.getTime() - Date.now(), { gameNumber: game.number, - player, + player: replacement, }) - }) - }) - - events.on('game:playerReplaced', async ({ game, replacement }) => { - const timeout = await calculateJoinGameserverTimeout(game, replacement) - if (!timeout) { - return - } - - tasks.schedule('games:autoSubstitutePlayer', timeout.getTime() - Date.now(), { - gameNumber: game.number, - player: replacement, - }) - }) + }), + ) events.on( 'game:playerConnectionStatusUpdated', - async ({ game, player, playerConnectionStatus }) => { + safe(async ({ game, player, playerConnectionStatus }) => { if (playerConnectionStatus !== PlayerConnectionStatus.offline) { return } @@ -88,7 +85,7 @@ export default fp( gameNumber: game.number, player, }) - }, + }), ) }, { diff --git a/src/games/plugins/free-players.ts b/src/games/plugins/free-players.ts index 647e2b35..0cdb80e5 100644 --- a/src/games/plugins/free-players.ts +++ b/src/games/plugins/free-players.ts @@ -2,7 +2,6 @@ import fp from 'fastify-plugin' import { tasks } from '../../tasks' import { players } from '../../players' import { configuration } from '../../configuration' -import { collections } from '../../database/collections' import { events } from '../../events' import { whenGameEnds } from '../when-game-ends' import { SlotStatus } from '../../database/models/game-slot.model' @@ -22,13 +21,9 @@ export default fp( [SlotStatus.active, SlotStatus.waitingForSubstitute].includes(slot.status), ) .map(async ({ gameClass, player }) => { - const p = await collections.players.findOne({ _id: player }) - if (p === null) { - throw new Error(`player not found: ${player.toString()}`) - } const queueCooldown = await configuration.get('games.join_queue_cooldown') const cooldownMs = queueCooldown[gameClass] ?? 0 - tasks.schedule('games.freePlayer', cooldownMs, { player: p.steamId }) + tasks.schedule('games.freePlayer', cooldownMs, { player }) }), ) }), diff --git a/src/games/plugins/match-event-handler.ts b/src/games/plugins/match-event-handler.ts index a89ebeef..f7be8470 100644 --- a/src/games/plugins/match-event-handler.ts +++ b/src/games/plugins/match-event-handler.ts @@ -7,7 +7,6 @@ import { PlayerConnectionStatus, SlotStatus } from '../../database/models/game-s import { update } from '../update' import { assertIsError } from '../../utils/assert-is-error' import { logger } from '../../logger' -import { collections } from '../../database/collections' import { safe } from '../../utils/safe' export default fp( @@ -75,11 +74,6 @@ export default fp( events.on( 'match/player:connected', safe(async ({ gameNumber, steamId }) => { - const player = await collections.players.findOne({ steamId }) - if (!player) { - throw new Error(`player with steamId ${steamId} not found`) - } - const game = await update( gameNumber, { @@ -90,12 +84,12 @@ export default fp( events: { at: new Date(), event: GameEventType.playerJoinedGameServer, - player: player._id, + player: steamId, }, }, }, { - arrayFilters: [{ 'element.player': { $eq: player._id } }], + arrayFilters: [{ 'element.player': { $eq: steamId } }], }, ) events.emit('game:playerConnectionStatusUpdated', { @@ -106,13 +100,9 @@ export default fp( }), ) - events.on('match/player:joinedTeam', async ({ gameNumber, steamId, team }) => { - try { - const player = await collections.players.findOne({ steamId }) - if (!player) { - throw new Error(`player with steamId ${steamId} not found`) - } - + events.on( + 'match/player:joinedTeam', + safe(async ({ gameNumber, steamId, team }) => { const game = await update( gameNumber, { @@ -123,13 +113,13 @@ export default fp( events: { at: new Date(), event: GameEventType.playerJoinedGameServerTeam, - player: player._id, + player: steamId, team, }, }, }, { - arrayFilters: [{ 'element.player': { $eq: player._id } }], + arrayFilters: [{ 'element.player': { $eq: steamId } }], }, ) events.emit('game:playerConnectionStatusUpdated', { @@ -137,19 +127,12 @@ export default fp( player: steamId, playerConnectionStatus: PlayerConnectionStatus.connected, }) - } catch (error) { - assertIsError(error) - logger.warn(error) - } - }) - - events.on('match/player:disconnected', async ({ gameNumber, steamId }) => { - try { - const player = await collections.players.findOne({ steamId }) - if (!player) { - throw new Error(`player with steamId ${steamId} not found`) - } + }), + ) + events.on( + 'match/player:disconnected', + safe(async ({ gameNumber, steamId }) => { const game = await update( gameNumber, { @@ -160,12 +143,12 @@ export default fp( events: { at: new Date(), event: GameEventType.playerLeftGameServer, - player: player._id, + player: steamId, }, }, }, { - arrayFilters: [{ 'element.player': { $eq: player._id } }], + arrayFilters: [{ 'element.player': { $eq: steamId } }], }, ) events.emit('game:playerConnectionStatusUpdated', { @@ -173,11 +156,8 @@ export default fp( player: steamId, playerConnectionStatus: PlayerConnectionStatus.offline, }) - } catch (error) { - assertIsError(error) - logger.warn(error) - } - }) + }), + ) events.on('match/score:final', async ({ gameNumber, team, score }) => { try { diff --git a/src/games/plugins/sync-clients.ts b/src/games/plugins/sync-clients.ts index 4218576e..618739ca 100644 --- a/src/games/plugins/sync-clients.ts +++ b/src/games/plugins/sync-clients.ts @@ -1,6 +1,4 @@ import fp from 'fastify-plugin' -import { collections } from '../../database/collections' -import { PlayerConnectionStatus } from '../../database/models/game-slot.model' import { GameState } from '../../database/models/game.model' import { events } from '../../events' import { ConnectInfo } from '../views/html/connect-info' @@ -48,18 +46,14 @@ export default fp(async app => { await Promise.all( after.slots.map(async slot => { - const beforeSlot = before.slots.find(s => s.player.equals(slot.player)) + const beforeSlot = before.slots.find(s => s.player === slot.player) if (!beforeSlot) { return } if (beforeSlot.shouldJoinBy !== slot.shouldJoinBy) { - const player = await collections.players.findOne({ _id: slot.player }) - if (!player) { - throw new Error(`no such player: ${slot.player.toString()}`) - } app.gateway - .toPlayers(player.steamId) + .toPlayers(slot.player) .broadcast(async actor => await ConnectInfo({ game: after, actor })) } }), @@ -80,86 +74,21 @@ export default fp(async app => { ) events.on( - 'match/player:connected', - safe(async ({ gameNumber, steamId }) => { - const player = await collections.players.findOne({ steamId }) - if (!player) { - throw new Error(`no such player: ${steamId}`) - } - const game = await collections.games.findOne({ number: gameNumber }) - if (!game) { - throw new Error(`game ${gameNumber} not found`) - } - - app.gateway.broadcast( - async () => - await PlayerConnectionStatusIndicator({ - steamId: player.steamId, - connectionStatus: PlayerConnectionStatus.joining, - }), - ) - app.gateway - .toPlayers(player.steamId) - .broadcast(async actor => await ConnectInfo({ game, actor })) - }), - ) - - events.on( - 'match/player:joinedTeam', - safe(async ({ gameNumber, steamId }) => { - const player = await collections.players.findOne({ steamId }) - if (!player) { - throw new Error(`no such player: ${steamId}`) - } - const game = await collections.games.findOne({ number: gameNumber }) - if (!game) { - throw new Error(`game ${gameNumber} not found`) - } - - app.gateway.broadcast( - async () => - await PlayerConnectionStatusIndicator({ - steamId: player.steamId, - connectionStatus: PlayerConnectionStatus.connected, - }), - ) - app.gateway - .toPlayers(player.steamId) - .broadcast(async actor => await ConnectInfo({ game, actor })) - }), - ) - - events.on( - 'match/player:disconnected', - safe(async ({ gameNumber, steamId }) => { - const player = await collections.players.findOne({ steamId }) - if (!player) { - throw new Error(`no such player: ${steamId}`) - } - const game = await collections.games.findOne({ number: gameNumber }) - if (!game) { - throw new Error(`game ${gameNumber} not found`) - } - + 'game:playerConnectionStatusUpdated', + safe(async ({ game, player, playerConnectionStatus }) => { app.gateway.broadcast( async () => await PlayerConnectionStatusIndicator({ - steamId: player.steamId, - connectionStatus: PlayerConnectionStatus.offline, + steamId: player, + connectionStatus: playerConnectionStatus, }), ) - app.gateway - .toPlayers(player.steamId) - .broadcast(async actor => await ConnectInfo({ game, actor })) + app.gateway.toPlayers(player).broadcast(async actor => await ConnectInfo({ game, actor })) }), ) events.on('game:substituteRequested', async ({ game, replacee }) => { - const r = await collections.players.findOne({ steamId: replacee }) - if (!r) { - throw new Error(`no such player: ${replacee}`) - } - const slot = game.slots.find(s => s.player.equals(r._id)) + const slot = game.slots.find(s => s.player === replacee) if (!slot) { throw new Error(`no such game slot: ${game.number} ${replacee}`) } diff --git a/src/games/plugins/update-should-join-by.ts b/src/games/plugins/update-should-join-by.ts index 9a3a92fc..c31d0a5a 100644 --- a/src/games/plugins/update-should-join-by.ts +++ b/src/games/plugins/update-should-join-by.ts @@ -5,7 +5,6 @@ import { update } from '../update' import { PlayerConnectionStatus, SlotStatus } from '../../database/models/game-slot.model' import { calculateJoinGameserverTimeout } from '../calculate-join-gameserver-timeout' import { safe } from '../../utils/safe' -import { collections } from '../../database/collections' export default fp( async () => { @@ -41,11 +40,6 @@ export default fp( return } - const r = await collections.players.findOne({ steamId: replacement }) - if (!r) { - throw new Error(`player not found: ${replacement}`) - } - await update( game.number, { @@ -56,7 +50,7 @@ export default fp( { arrayFilters: [ { - 'slot.player': r._id, + 'slot.player': replacement, }, ], }, @@ -76,11 +70,6 @@ export default fp( return } - const p = await collections.players.findOne({ steamId: player }) - if (!p) { - throw new Error(`player not found: ${player}`) - } - await update( game.number, { @@ -91,7 +80,7 @@ export default fp( { arrayFilters: [ { - 'slot.player': p._id, + 'slot.player': player, }, ], }, diff --git a/src/games/rcon/blacklist-player.ts b/src/games/rcon/blacklist-player.ts index 8bc5afd6..d1494232 100644 --- a/src/games/rcon/blacklist-player.ts +++ b/src/games/rcon/blacklist-player.ts @@ -1,21 +1,15 @@ -import { collections } from '../../database/collections' import type { GameModel } from '../../database/models/game.model' import type { SteamId64 } from '../../shared/types/steam-id-64' import { delGamePlayer } from './commands' import { withRcon } from './with-rcon' export async function blacklistPlayer(game: GameModel, steamId: SteamId64) { - const player = await collections.players.findOne({ steamId }) - if (!player) { - throw new Error(`player not found: ${steamId}`) - } - - const slot = game.slots.find(slot => slot.player.equals(player._id)) + const slot = game.slots.find(slot => slot.player === steamId) if (!slot) { throw new Error(`player not found in game: ${steamId}`) } return await withRcon(game, async ({ rcon }) => { - await rcon.send(delGamePlayer(player.steamId)) + await rcon.send(delGamePlayer(steamId)) }) } diff --git a/src/games/rcon/configure.ts b/src/games/rcon/configure.ts index e715e29b..33aa5f4b 100644 --- a/src/games/rcon/configure.ts +++ b/src/games/rcon/configure.ts @@ -150,9 +150,9 @@ async function compileConfig(game: GameModel, password: string): Promise slot.status !== SlotStatus.replaced) .map(async slot => { - const player = await collections.players.findOne({ _id: slot.player }) + const player = await collections.players.findOne({ steamId: slot.player }) if (player === null) { - throw new Error(`player ${slot.player.toString()} not found`) + throw new Error(`player ${slot.player} not found`) } return addGamePlayer(player.steamId, deburr(player.name), slot.team, slot.gameClass) }), diff --git a/src/games/rcon/whitelist-player.ts b/src/games/rcon/whitelist-player.ts index 937d7b44..8b795d4a 100644 --- a/src/games/rcon/whitelist-player.ts +++ b/src/games/rcon/whitelist-player.ts @@ -11,7 +11,7 @@ export async function whitelistPlayer(game: GameModel, steamId: SteamId64) { throw new Error(`player not found: ${steamId}`) } - const slot = game.slots.find(slot => slot.player.equals(player._id)) + const slot = game.slots.find(slot => slot.player === player.steamId) if (!slot) { throw new Error(`player not found in game: ${steamId}`) } diff --git a/src/games/replace-player.ts b/src/games/replace-player.ts index d636c16c..997604cf 100644 --- a/src/games/replace-player.ts +++ b/src/games/replace-player.ts @@ -20,7 +20,7 @@ export async function replacePlayer({ replacement: SteamId64 }): Promise { return await replacePlayerMutex.runExclusive(async () => { - logger.info({ number, replacee, replacement }, 'replacePlayer()') + logger.trace({ number, replacee, replacement }, 'games.replacePlayer()') const game = await collections.games.findOne({ number }) if (game === null) { @@ -31,24 +31,14 @@ export async function replacePlayer({ throw new Error(`game ${game.number} in wrong state: ${game.state}`) } - const slot = await findPlayerSlot(game, replacee) - if (slot === null) { + const slot = game.slots.find(({ player }) => player === replacee) + if (!slot) { throw new Error(`player slot unavailable (gameNumber=${game.number}, replacee=${replacee})`) } - const re = await collections.players.findOne({ steamId: replacee }) - if (!re) { - throw new Error(`replacee player not found: ${replacee}`) - } - - const rm = await collections.players.findOne({ steamId: replacement }) - if (!rm) { - throw new Error(`replacement player not found: ${replacement}`) - } - let newGame: GameModel - if (re._id.equals(rm._id)) { + if (replacee === replacement) { newGame = await update( { number }, { @@ -59,20 +49,25 @@ export async function replacePlayer({ events: { event: GameEventType.playerReplaced, at: new Date(), - replacee: re._id, - replacement: rm._id, + replacee, + replacement, }, }, }, { arrayFilters: [ { - $and: [{ 'slot.player': { $eq: re._id } }], + $and: [{ 'slot.player': { $eq: replacee } }], }, ], }, ) } else { + const rm = await collections.players.findOne({ steamId: replacement }) + if (!rm) { + throw new Error(`replacement player not found: ${replacement}`) + } + if (rm.activeGame !== undefined) { throw new Error(`player denied: player has active game`) } @@ -82,7 +77,7 @@ export async function replacePlayer({ { $push: { slots: { - player: rm._id, + player: replacement, team: slot.team, gameClass: slot.gameClass, status: SlotStatus.active, @@ -91,8 +86,8 @@ export async function replacePlayer({ events: { event: GameEventType.playerReplaced, at: new Date(), - replacee: re._id, - replacement: rm._id, + replacee, + replacement, }, }, }, @@ -109,7 +104,7 @@ export async function replacePlayer({ arrayFilters: [ { $and: [ - { 'slot.player': { $eq: re._id } }, + { 'slot.player': { $eq: replacee } }, { 'slot.status': { $eq: SlotStatus.waitingForSubstitute, @@ -126,18 +121,3 @@ export async function replacePlayer({ return game }) } - -async function findPlayerSlot(game: GameModel, player: SteamId64) { - for (const slot of game.slots.filter(s => s.status === SlotStatus.waitingForSubstitute)) { - const ps = await collections.players.findOne({ _id: slot.player }) - if (!ps) { - throw new Error(`player in slot does not exist: ${slot.player.toString()}`) - } - - if (ps.steamId === player) { - return slot - } - } - - return null -} diff --git a/src/games/request-substitute.ts b/src/games/request-substitute.ts index 5635aff6..c30d036c 100644 --- a/src/games/request-substitute.ts +++ b/src/games/request-substitute.ts @@ -4,9 +4,8 @@ import { SlotStatus } from '../database/models/game-slot.model' import { GameState, type GameModel, type GameNumber } from '../database/models/game.model' import { events } from '../events' import { logger } from '../logger' -import { isBot, type Bot } from '../shared/types/bot' +import { type Bot } from '../shared/types/bot' import type { SteamId64 } from '../shared/types/steam-id-64' -import { findPlayerSlot } from './find-player-slot' import { update } from './update' export async function requestSubstitute({ @@ -20,7 +19,7 @@ export async function requestSubstitute({ actor: SteamId64 | Bot reason?: string }): Promise { - logger.info({ number, replacee, actor, reason }, 'substitutePlayer()') + logger.trace({ number, replacee, actor, reason }, 'substitutePlayer()') const game = await collections.games.findOne({ number }) if (game === null) { @@ -31,29 +30,15 @@ export async function requestSubstitute({ throw new Error(`game ${game.number} in wrong state: ${game.state}`) } - const slot = await findPlayerSlot(game, replacee) - if (slot === null) { + const slot = game.slots.find(({ player }) => player === replacee) + if (!slot) { throw new Error(`player is not a member of game ${game.number}`) } - const r = await collections.players.findOne({ steamId: replacee }) - if (r === null) { - throw new Error(`replacee not found: ${replacee}`) + if (slot.status !== SlotStatus.active) { + throw new Error(`invalid slot status: ${slot.status}`) } - const a = await (async () => { - if (isBot(actor)) { - return actor - } - - const a = await collections.players.findOne({ steamId: actor }) - if (a === null) { - throw new Error(`actor not found: ${actor}`) - } - - return a._id - })() - const newGame = await update( { number }, { @@ -64,15 +49,15 @@ export async function requestSubstitute({ events: { event: GameEventType.substituteRequested, at: new Date(), - player: r._id, + player: replacee, gameClass: slot.gameClass, - actor: a, + actor, reason, }, }, }, { - arrayFilters: [{ 'slot.player': { $eq: r._id } }], + arrayFilters: [{ 'slot.player': { $eq: replacee } }], }, ) diff --git a/src/games/views/html/connect-info.tsx b/src/games/views/html/connect-info.tsx index a96c6f8b..4078674e 100644 --- a/src/games/views/html/connect-info.tsx +++ b/src/games/views/html/connect-info.tsx @@ -1,4 +1,3 @@ -import { collections } from '../../../database/collections' import { GameState, type GameModel } from '../../../database/models/game.model' import { IconCopy } from '../../../html/components/icons' import type { SteamId64 } from '../../../shared/types/steam-id-64' @@ -41,7 +40,7 @@ async function ConnectString(props: { game: GameModel; actor: SteamId64 | undefi break default: { const connectString = - ((await actorInGame(props.game, props.actor)) + (actorInGame(props.game, props.actor) ? props.game.connectString : props.game.stvConnectString) ?? '' csBoxContent = connectString @@ -76,15 +75,10 @@ async function ConnectString(props: { game: GameModel; actor: SteamId64 | undefi ) } -async function actorInGame(game: GameModel, actor?: SteamId64) { +function actorInGame(game: GameModel, actor?: SteamId64) { if (!actor) { return false } - const player = await collections.players.findOne({ steamId: actor }) - if (player === null) { - throw new Error(`player ${actor} does not exist`) - } - - return game.slots.some(slot => slot.player.equals(player._id)) + return game.slots.some(slot => slot.player === actor) } diff --git a/src/games/views/html/game-event-list.tsx b/src/games/views/html/game-event-list.tsx index f53340aa..8504312e 100644 --- a/src/games/views/html/game-event-list.tsx +++ b/src/games/views/html/game-event-list.tsx @@ -67,29 +67,38 @@ async function GameEventInfo(props: { event: GameEventModel; game: GameModel }) case GameEventType.gameCreated: return Game created case GameEventType.gameServerAssigned: - if (props.event.actor) { - const actor = await collections.players.findOne({ _id: props.event.actor }) - if (!actor) { - throw new Error(`actor not found: ${props.event.actor.toString()}`) - } - + if (!props.event.actor) { return ( - - {actor.name} - {' '} - assigned game server:{' '} + Game server assigned:{' '} {props.event.gameServerName} ) - } else { + } + + if (isBot(props.event.actor)) { return ( - Game server assigned:{' '} + Bot assigned game server:{' '} {props.event.gameServerName} ) } + + const actor = await collections.players.findOne({ steamId: props.event.actor }) + if (!actor) { + throw new Error(`actor not found: ${props.event.actor}`) + } + + return ( + + + {actor.name} + {' '} + assigned game server:{' '} + {props.event.gameServerName} + + ) case GameEventType.gameServerInitialized: return Game server initialized case GameEventType.gameStarted: @@ -97,30 +106,34 @@ async function GameEventInfo(props: { event: GameEventModel; game: GameModel }) case GameEventType.gameEnded: switch (props.event.reason) { case GameEndedReason.interrupted: - if (props.event.actor) { - const actor = await collections.players.findOne({ _id: props.event.actor }) - if (!actor) { - throw new Error(`actor not found: ${props.event.actor.toString()}`) - } - - return ( - - Game interrupted by{' '} - - {actor.name} - - - ) - } else { + if (!props.event.actor) { return Game interrupted } + + if (isBot(props.event.actor)) { + return Game interrupted by bot + } + + const actor = await collections.players.findOne({ steamId: props.event.actor }) + if (!actor) { + throw new Error(`actor not found: ${props.event.actor}`) + } + + return ( + + Game interrupted by{' '} + + {actor.name} + + + ) default: return Game ended } case GameEventType.substituteRequested: { - const player = await collections.players.findOne({ _id: props.event.player }) + const player = await collections.players.findOne({ steamId: props.event.player }) if (!player) { - throw new Error(`player not found: ${props.event.player.toString()}`) + throw new Error(`player not found: ${props.event.player}`) } if (props.event.actor) { @@ -128,9 +141,9 @@ async function GameEventInfo(props: { event: GameEventModel; game: GameModel }) if (isBot(props.event.actor)) { safeActorDesc = 'bot' } else { - const actor = await collections.players.findOne({ _id: props.event.actor }) + const actor = await collections.players.findOne({ steamId: props.event.actor }) if (!actor) { - throw new Error(`actor not found: ${props.event.actor.toString()}`) + throw new Error(`actor not found: ${props.event.actor}`) } safeActorDesc = ( @@ -170,16 +183,16 @@ async function GameEventInfo(props: { event: GameEventModel; game: GameModel }) } } case GameEventType.playerReplaced: { - const replacee = await collections.players.findOne({ _id: props.event.replacee }) + const replacee = await collections.players.findOne({ steamId: props.event.replacee }) if (!replacee) { throw new Error(`replacee not found: ${replacee}`) } - const replacement = await collections.players.findOne({ _id: props.event.replacement }) + const replacement = await collections.players.findOne({ steamId: props.event.replacement }) if (!replacement) { throw new Error(`replacement not found: ${replacement}`) } - const slot = props.game.slots.find(s => s.player.equals(replacement._id)) + const slot = props.game.slots.find(({ player }) => player === replacement.steamId) if (!slot) { throw new Error( `replacement slot not found (gameNumber=${props.game.number}; replacement=${props.event.replacement.toString()}`, diff --git a/src/games/views/html/game-slot-list.tsx b/src/games/views/html/game-slot-list.tsx index 1405a942..2be739bc 100644 --- a/src/games/views/html/game-slot-list.tsx +++ b/src/games/views/html/game-slot-list.tsx @@ -41,6 +41,7 @@ export function GameSlotList(props: { game: GameModel; actor?: SteamId64 | undef function slotsForTeam(slots: GameSlotModel[], team: Tf2Team) { return slots + .filter(slot => [SlotStatus.active, SlotStatus.waitingForSubstitute].includes(slot.status)) .filter(slot => slot.team === team) .sort((a, b) => tf2ClassOrder[b.gameClass] - tf2ClassOrder[a.gameClass]) } diff --git a/src/games/views/html/game-slot.tsx b/src/games/views/html/game-slot.tsx index 6daa1158..738a8069 100644 --- a/src/games/views/html/game-slot.tsx +++ b/src/games/views/html/game-slot.tsx @@ -12,7 +12,7 @@ export async function GameSlot(props: { slot: GameSlotModel actor: SteamId64 | undefined }) { - const player = await collections.players.findOne({ _id: props.slot.player }) + const player = await collections.players.findOne({ steamId: props.slot.player }) if (!player) { throw new Error(`no such player: ${props.slot.player.toString()}`) } @@ -102,10 +102,10 @@ async function GameSlotContent(props: { ) case SlotStatus.waitingForSubstitute: { - if (a && (props.slot.player.equals(a._id) || a.activeGame === undefined)) { + if (a && (props.slot.player === a.steamId || a.activeGame === undefined)) { return (