From 03d4185f52c5138e890cc2dc432a41a26b074d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Thu, 14 Mar 2024 19:41:27 -0700 Subject: [PATCH 0001/1128] Moved the server to Typescript (#1) * Moved the server to Typescript * Removed error console log --- .gitignore | 3 +- Server/Dockerfile | 4 +- Server/package-lock.json | 1292 +++++++++++---------------- Server/package.json | 21 +- Server/src/{Client.js => Client.ts} | 14 +- Server/src/{Lobby.js => Lobby.ts} | 22 +- Server/src/{main.js => main.ts} | 43 +- Server/tsconfig.json | 39 + 8 files changed, 640 insertions(+), 798 deletions(-) rename Server/src/{Client.js => Client.ts} (51%) rename Server/src/{Lobby.js => Lobby.ts} (78%) rename Server/src/{main.js => main.ts} (66%) create mode 100644 Server/tsconfig.json diff --git a/.gitignore b/.gitignore index eb97a7f8..b30e7fa5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ ref.lua node_modules .env -Config.lua \ No newline at end of file +Config.lua +Server/dist \ No newline at end of file diff --git a/Server/Dockerfile b/Server/Dockerfile index fdaf2dc1..c1401ed7 100644 --- a/Server/Dockerfile +++ b/Server/Dockerfile @@ -8,6 +8,8 @@ RUN npm install COPY . . +RUN npm run build + EXPOSE 8080 -CMD [ "node", "src/main.js" ] +CMD [ "node", "dist/main.js" ] diff --git a/Server/package-lock.json b/Server/package-lock.json index 97490a8c..1731d90e 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -1,766 +1,530 @@ { - "name": "Server", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "body-parser": "^1.20.2", - "cors": "^2.8.5", - "express": "^4.18.3", - "express-ws": "^5.0.2", - "net": "^1.0.2", - "uuid": "^9.0.1" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", - "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express-ws": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz", - "integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==", - "dependencies": { - "ws": "^7.4.6" - }, - "engines": { - "node": ">=4.5.0" - }, - "peerDependencies": { - "express": "^4.0.0 || ^5.0.0-alpha.1" - } - }, - "node_modules/express-ws/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/net": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz", - "integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "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" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", - "dependencies": { - "define-data-property": "^1.1.2", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - } - } + "name": "Server", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "net": "^1.0.2", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^20.11.27", + "@types/uuid": "^9.0.8", + "tsx": "^4.7.1", + "typescript": "^5.4.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/node": { + "version": "20.11.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz", + "integrity": "sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz", + "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/net": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz", + "integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.1.tgz", + "integrity": "sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==", + "dev": true, + "dependencies": { + "esbuild": "~0.19.10", + "get-tsconfig": "^4.7.2" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + } + } } diff --git a/Server/package.json b/Server/package.json index a92e0153..b0dd47dd 100644 --- a/Server/package.json +++ b/Server/package.json @@ -1,7 +1,18 @@ { - "dependencies": { - "net": "^1.0.2", - "uuid": "^9.0.1" - }, - "type": "module" + "scripts": { + "build": "tsc", + "dev": "tsx watch src/main.ts", + "start": "node ./dist/main.js" + }, + "dependencies": { + "net": "^1.0.2", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^20.11.27", + "@types/uuid": "^9.0.8", + "tsx": "^4.7.1", + "typescript": "^5.4.2" + }, + "type": "module" } diff --git a/Server/src/Client.js b/Server/src/Client.ts similarity index 51% rename from Server/src/Client.js rename to Server/src/Client.ts index 18902b3e..822c248d 100644 --- a/Server/src/Client.js +++ b/Server/src/Client.ts @@ -1,19 +1,27 @@ import { v4 as uuidv4 } from 'uuid' +import type Lobby from './Lobby' + +type SendFn = (data: string) => void class Client { - constructor(send) { + id: string + username: string + lobby: Lobby | null + send: SendFn + + constructor(send: SendFn) { this.id = uuidv4() this.lobby = null this.username = 'Guest' this.send = send } - setUsername = (username) => { + setUsername = (username: string) => { this.username = username this.lobby?.broadcast() } - setLobby = (lobby) => { + setLobby = (lobby: Lobby | null) => { this.lobby = lobby } } diff --git a/Server/src/Lobby.js b/Server/src/Lobby.ts similarity index 78% rename from Server/src/Lobby.js rename to Server/src/Lobby.ts index 89f35247..84bb8add 100644 --- a/Server/src/Lobby.js +++ b/Server/src/Lobby.ts @@ -1,6 +1,8 @@ +import type Client from "./Client" + const Lobbies = new Map() -const generateUniqueLobbyCode = () => { +const generateUniqueLobbyCode = (): string => { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' let result = '' for (let i = 0; i < 5; i++) { @@ -10,7 +12,11 @@ const generateUniqueLobbyCode = () => { } class Lobby { - constructor(host) { + code: string; + host: Client | null; + guest: Client | null; + + constructor(host: Client) { do { this.code = generateUniqueLobbyCode() } while (Lobbies.get(this.code)) @@ -21,12 +27,12 @@ class Lobby { host.send(`action:joinedLobby,code:${this.code}`) } - static get = (code) => { + static get = (code: string) => { return Lobbies.get(code) } - leave = (client) => { - if (this.host.id === client.id) { + leave = (client: Client) => { + if (this.host?.id === client.id) { this.host = this.guest this.guest = null } @@ -41,7 +47,7 @@ class Lobby { } } - join = (client) => { + join = (client: Client) => { if (this.guest) { client.send('action:error,message:Lobby is full or does not exist.') return @@ -53,6 +59,10 @@ class Lobby { } broadcast = () => { + if(!this.host) { + return; + } + let message = `action:lobbyInfo,host:${this.host.username}` if (this.guest?.username) { message += `,guest:${this.guest.username}` diff --git a/Server/src/main.js b/Server/src/main.ts similarity index 66% rename from Server/src/main.js rename to Server/src/main.ts index 01e5b386..1cb9c837 100644 --- a/Server/src/main.js +++ b/Server/src/main.ts @@ -1,36 +1,38 @@ -import net from 'net' +import net from 'node:net' import Client from './Client.js' import Lobby from './Lobby.js' const PORT = 8080 -const stringToJson = (str) => { - const obj = {} - str.split(',').forEach(part => { +// biome-ignore lint/suspicious/noExplicitAny: Object is parsed from string +const stringToJson = (str: string): any => { + // biome-ignore lint/suspicious/noExplicitAny: Object is parsed from string + const obj: any = {} + for(const part of str.split(',')) { const [key, value] = part.split(':') obj[key] = value - }) + } return obj } -const sendToSocket = (socket) => (data) => { +const sendToSocket = (socket: net.Socket) => (data: string) => { //console.log('Responding with ' + data) if (!socket) { //console.log('Socket is undefined') return } - socket.write(data + '\n') + socket.write(`${data}\n`) } -const usernameAction = (client, username) => { +const usernameAction = (client: Client, username: string) => { client.setUsername(username) } -const createLobbyAction = (client) => { +const createLobbyAction = (client: Client) => { new Lobby(client) } -const joinLobbyAction = (client, code) => { +const joinLobbyAction = (client: Client, code: string) => { const newLobby = Lobby.get(code) if (!newLobby) { client.send('action:error,message:Lobby is full or does not exist.') @@ -39,22 +41,23 @@ const joinLobbyAction = (client, code) => { newLobby.join(client) } -const leaveLobbyAction = (client) => { +const leaveLobbyAction = (client: Client) => { client.lobby?.leave(client) } -const lobbyInfoAction = (client) => { +const lobbyInfoAction = (client: Client) => { client.lobby?.broadcast() } const server = net.createServer((socket) => { const client = new Client(sendToSocket(socket)) //console.log('Client connected') - client.send(`action:connected`) + client.send("action:connected") socket.on('data', (data) => { const messages = data.toString().split('\n') - messages.forEach((msg) => { + + for(const msg of messages) { if (!msg) return //console.log('Recieved message ' + msg) try { @@ -78,9 +81,9 @@ const server = net.createServer((socket) => { } } catch (error) { console.error('Failed to parse message', error) - client.send(socket, `action:error,message:Failed to parse message`) + client.send("action:error,message:Failed to parse message") } - }) + } }) socket.on('end', () => { @@ -88,7 +91,11 @@ const server = net.createServer((socket) => { leaveLobbyAction(client) }) - socket.on('error', (err) => { + socket.on('error', (err: Error & { + errno: number, + code: string, + syscall: string + }) => { if (err.code === 'ECONNRESET') { console.warn('TCP connection reset by peer (client).') } else { @@ -99,5 +106,5 @@ const server = net.createServer((socket) => { }) server.listen(PORT, '0.0.0.0', () => { - //console.log(`Server listening on port ${PORT}`); + console.log(`Server listening on port ${PORT}`); }) \ No newline at end of file diff --git a/Server/tsconfig.json b/Server/tsconfig.json new file mode 100644 index 00000000..53c8c3f6 --- /dev/null +++ b/Server/tsconfig.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + /* Language and Environment */ + "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ES6", /* Specify what module code is generated. */ + "moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + + /* Interop Constraints */ + "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + + /* Completeness */ + "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} From b46dd1b138d93630b9ff5f60cc0d5028aec1e1ce Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 15 Mar 2024 04:58:53 +0000 Subject: [PATCH 0002/1128] Updated Server README to finish the server protocol plan --- Server/README.md | 196 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 191 insertions(+), 5 deletions(-) diff --git a/Server/README.md b/Server/README.md index a5de3a77..53d4d697 100644 --- a/Server/README.md +++ b/Server/README.md @@ -36,6 +36,59 @@ lobbyInfo: host, guest? *This will obviously need reworking for 8 players but it is the simplest way of doing it for now +--- + +stopGame +- Tells the client to return to the lobby. This should be sent if any client returns to lobby. + +--- + +startGame: deck, stake?, seed? +- Tells the client to start the run +- deck: Deck or challenge id to start the game with, must be a [deck type](#deck-types) or [challenge type](#deck-types) +- stake?: Stake to start the deck with, does not affect challenges, must be a number between 1 and 8 +- seed?: Seed that the clients will start the run with, must be a [seed type](#seed-types) + +--- + +startBlind +- Tells the client to start the next blind. This should be sent when both clients are ready. + +--- + +winGame +- Tells the client to force win the run. + +--- + +loseGame +- Tells the client to force lose the run. + +--- + +gameInfo: small?, big?, boss? +- Info to send to the client before each blind set is displayed, overwrites the blinds +- small?: Blind type to set the small blind in the set to, defaults to the normal small blind, must be a [blind type](#blind-types) +- big?: Blind type to set the big blind in the set to, defaults to the normal big blind, must be a [blind type](#blind-types) +- boss?: Blind type to set the boss in the set to, defaults to a random boss, must be a [blind type](#blind-types) + +--- + +playerInfo: lives +- Info to send to the client at the start of the game and whenever it is requested +- lives: Amount of lives the client currently has, must be a number + +--- + +enemyInfo: score, handsLeft +- Updates the client on their enemy's score and hands left. This should be sent when the enemy plays a hand + +--- + +endPvP: lost +- Needs to be sent at the end of a PvP blind, clients will wait for this +- lost: Whether the client lost the PvP, client will take this as a life lost (server should reflect this), must be a boolean value (client will interpret this as a string) + ### Client to Server username: username @@ -44,21 +97,154 @@ username: username --- -createLobby -- Request to make a lobby and be given a code. Response should be a 'joinedLobby' action +createLobby: type +- Request to make a lobby and be given a code. Expecting a 'joinedLobby' response. +- type: Requested gamemode type, must be a [server type](#server-types) --- joinLobby: code -- Request to join an existing lobby, by given code. Response should be a 'joinedLobby' action, or 'error' if the lobby cannot be joined +- Request to join an existing lobby, by given code. Expecting a 'joinedLobby' or 'error' response. - code: 5 letter code acting as a lobby ID --- leaveLobby -- Leave the joined lobby, this is also called on client connection destruction so it needs to be functional without providing a code +- Leave the joined lobby, a code is not provided because this should be *almost* equivalent to when a client socket is destroyed, so it needs to be functional without providing a code --- lobbyInfo -- Request for an accurate 'lobbyInfo' response, for the lobby the client is connected to \ No newline at end of file +- Request for an accurate 'lobbyInfo' response, for the lobby the client is connected to + +--- + +stopGame +- Client is returning to lobby. Server should send other clients back to lobby as well. + +--- + +startGame +- Request to start the run. Expecting a 'startGame' response. + +--- + +readyBlind +- Declare ready to start next blind. Expecting 'startBlind' response. + +--- + +playHand: score, handsLeft +- Client has played a hand. +- score: The total score of all hands played in the blind so far, must be a number +- handsLeft: The total number of hands left that the client can play this blind, must be a number + +--- + +gameInfo +- Request a gameInfo update. + +--- + +playerInfo +- Request a playerInfo update. + +--- + +enemyInfo +- Request an enemyInfo update. + +## Server Types + +- attrition + - Both players start with 4 lives + - Every set's boss should be PvP +- draft + - Both players start with 2 lives + - First 4 antes are normal, rest of antes are only PvP blinds + +## Game Types + +### Blind Types + +One of the following, left side is the values, right side is the corosponding in-game name: +- bl_small = Small Blind +- bl_big = Big Blind +- bl_ox = The Ox +- bl_hook = The Hook +- bl_mouth = The Mouth +- bl_fish = The Fish +- bl_club = The Club +- bl_manacle = The Manacle +- bl_tooth = The Tooth +- bl_wall = The Wall +- bl_house = The House +- bl_mark = The Mark +- bl_final_bell = Cerulean Bell +- bl_wheel = The Wheel +- bl_arm = The Arm +- bl_psychic = The Psychic +- bl_goad = The Goad +- bl_water = The Water +- bl_eye = The Eye +- bl_plant = The Plant +- bl_needle = The Needle +- bl_head = The Head +- bl_final_leaf = Verdant Leaf +- bl_final_vessel = Violet Vessel +- bl_window = The Window +- bl_serpent = The Serpent +- bl_pillar = The Pillar +- bl_flint = The Flint +- bl_final_acorn = Amber Acorn +- bl_final_heart = Crimson Heart +- **b1_pvp = Your Nemesis** <-- This is the blind that needs to be set for players to play against eachother's scores + +### Deck Types + +One of the following, left side is the values, right side is the corosponding in-game name: +- b_red = Red Deck +- b_blue = Blue Deck +- b_yellow = Yellow Deck +- b_green = Green Deck +- b_black = Black Deck +- b_magic = Magic Deck +- b_nebula = Nebula Deck +- b_ghost = Ghost Deck +- b_abandoned = Abandoned Deck +- b_checkered = Checkered Deck +- b_zodiac = Zodiac Deck +- b_painted = Painted Deck +- b_anaglyph = Anaglyph Deck +- b_plasma = Plasma Deck +- b_erratic = Erratic Deck + +### Challenge Types + +One of the following, left side is the values, right side is the corosponding in-game name: +- c_omelette_1 = The Omelette +- c_city_1 = 15 Minute City +- c_rich_1 = Rich get Richer +- c_knife_1 = On a Knife's Edge +- c_xray_1 = X-ray Vision +- c_mad_world_1 = Mad World +- c_luxury_1 = Luxury Tax +- c_non_perishable_1 = Non-Perishable +- c_medusa_1 = Medusa +- c_double_nothing_1 = Double or Nothing +- c_typecast_1 = Typecast +- c_inflation_1 = Inflation +- c_bram_poker_1 = Bram Poker +- c_fragile_1 = Fragile +- c_monolith_1 = Monolith +- c_blast_off_1 = Blast Off +- c_five_card_1 = Five-Card Draw +- c_golden_needle_1 = Golden Needle +- c_cruelty_1 = Cruelty +- c_jokerless_1 = Jokerless + +### Seed Type + +- String +- Exactly 8 Characters Long +- Only Uppercase Letters and Numbers \ No newline at end of file From e4d258941a31b997b9ce1caeff5a75ea434fe787 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 15 Mar 2024 05:02:46 +0000 Subject: [PATCH 0003/1128] Updated server protocol to include multiplayer deck --- Server/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Server/README.md b/Server/README.md index 53d4d697..f4570435 100644 --- a/Server/README.md +++ b/Server/README.md @@ -198,7 +198,7 @@ One of the following, left side is the values, right side is the corosponding in - bl_flint = The Flint - bl_final_acorn = Amber Acorn - bl_final_heart = Crimson Heart -- **b1_pvp = Your Nemesis** <-- This is the blind that needs to be set for players to play against eachother's scores +- **b1_pvp = Your Nemesis** <-- This is the blind that needs to be set for players to play against eachother's scores ### Deck Types @@ -242,6 +242,7 @@ One of the following, left side is the values, right side is the corosponding in - c_golden_needle_1 = Golden Needle - c_cruelty_1 = Cruelty - c_jokerless_1 = Jokerless +- **c_multiplayer_1 = Multiplayer Default** <-- This is the default deck until deck selection implementation (will be removed) ### Seed Type From b9e1738489c04c7ffa3cf9f74de00e3e050249f3 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 15 Mar 2024 05:04:40 +0000 Subject: [PATCH 0004/1128] Added seperation between actions and headers --- Server/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Server/README.md b/Server/README.md index f4570435..2ddec3ef 100644 --- a/Server/README.md +++ b/Server/README.md @@ -89,6 +89,8 @@ endPvP: lost - Needs to be sent at the end of a PvP blind, clients will wait for this - lost: Whether the client lost the PvP, client will take this as a life lost (server should reflect this), must be a boolean value (client will interpret this as a string) +--- + ### Client to Server username: username @@ -154,6 +156,8 @@ playerInfo enemyInfo - Request an enemyInfo update. +--- + ## Server Types - attrition From 8f27a945a87270ee8a428be7928d11a5a7afd17b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Fri, 15 Mar 2024 10:21:39 -0700 Subject: [PATCH 0005/1128] Added types for actions (#3) * Added types for actions * Added missing actions and fixed formatting --- Server/biome.json | 17 ++++ Server/package-lock.json | 156 ++++++++++++++++++++++++++++++++++ Server/package.json | 33 ++++---- Server/src/Client.ts | 34 ++++---- Server/src/Lobby.ts | 86 +++++++++++-------- Server/src/actionHandlers.ts | 58 +++++++++++++ Server/src/actions.ts | 98 +++++++++++++++++++++ Server/src/main.ts | 160 ++++++++++++++++------------------- 8 files changed, 484 insertions(+), 158 deletions(-) create mode 100644 Server/biome.json create mode 100644 Server/src/actionHandlers.ts create mode 100644 Server/src/actions.ts diff --git a/Server/biome.json b/Server/biome.json new file mode 100644 index 00000000..7ef51e75 --- /dev/null +++ b/Server/biome.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.6.1/schema.json", + "organizeImports": { + "enabled": true + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } +} diff --git a/Server/package-lock.json b/Server/package-lock.json index 1731d90e..07eb30de 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -9,12 +9,168 @@ "uuid": "^9.0.1" }, "devDependencies": { + "@biomejs/biome": "^1.6.1", "@types/node": "^20.11.27", "@types/uuid": "^9.0.8", "tsx": "^4.7.1", "typescript": "^5.4.2" } }, + "node_modules/@biomejs/biome": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.6.1.tgz", + "integrity": "sha512-SILQvA2S0XeaOuu1bivv6fQmMo7zMfr2xqDEN+Sz78pGbAKZnGmg0emsXjQWoBY/RVm9kPCgX+aGEpZZTYaM7w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.6.1", + "@biomejs/cli-darwin-x64": "1.6.1", + "@biomejs/cli-linux-arm64": "1.6.1", + "@biomejs/cli-linux-arm64-musl": "1.6.1", + "@biomejs/cli-linux-x64": "1.6.1", + "@biomejs/cli-linux-x64-musl": "1.6.1", + "@biomejs/cli-win32-arm64": "1.6.1", + "@biomejs/cli-win32-x64": "1.6.1" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.6.1.tgz", + "integrity": "sha512-KlvY00iB9T/vFi4m/GXxEyYkYnYy6aw06uapzUIIdiMMj7I/pmZu7CsZlzWdekVD0j+SsQbxdZMsb0wPhnRSsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.*" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.6.1.tgz", + "integrity": "sha512-jP4E8TXaQX5e3nvRJSzB+qicZrdIDCrjR0sSb1DaDTx4JPZH5WXq/BlTqAyWi3IijM+IYMjWqAAK4kOHsSCzxw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.*" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.6.1.tgz", + "integrity": "sha512-nxD1UyX3bWSl/RSKlib/JsOmt+652/9yieogdSC/UTLgVCZYOF7u8L/LK7kAa0Y4nA8zSPavAQTgko7mHC2ObA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.*" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.6.1.tgz", + "integrity": "sha512-YdkDgFecdHJg7PJxAMaZIixVWGB6St4yH08BHagO0fEhNNiY8cAKEVo2mcXlsnEiTMpeSEAY9VxLUrVT3IVxpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.*" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.6.1.tgz", + "integrity": "sha512-BYAzenlMF3QdngjNFw9QVBXKGNzeecqwF3pwDgUGEvU7OJpn1/lyVkJVxYPtVGRNdjQ9e6l/s8NjKuBpW/ZR4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.*" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.6.1.tgz", + "integrity": "sha512-aSISIDmxq04NNy7tm4x9rBk2vH0ub2VDIE4outEmdC2LBtEJoINiphlZagx/FvjbsqUfygent9QUSn0oREnAXg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.*" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.6.1.tgz", + "integrity": "sha512-/eCHQKZ1kEawUpkSuXq4urtxMsD1P1678OPG3zNKt3ru16AqqspLdO3jzBe3k74xCPYnQ36e9Yqc97Mo0qgPtg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.*" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.6.1.tgz", + "integrity": "sha512-5TUZbzBwnDLFxLVGEPsorNi6eC2Gt+z4Oei9Qvq0M/4c4/mjZ96ABgwao/tMxf4ZBr/qyy2YdvF+gX9Rc+xC0A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.*" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", diff --git a/Server/package.json b/Server/package.json index b0dd47dd..aaf47c94 100644 --- a/Server/package.json +++ b/Server/package.json @@ -1,18 +1,19 @@ { - "scripts": { - "build": "tsc", - "dev": "tsx watch src/main.ts", - "start": "node ./dist/main.js" - }, - "dependencies": { - "net": "^1.0.2", - "uuid": "^9.0.1" - }, - "devDependencies": { - "@types/node": "^20.11.27", - "@types/uuid": "^9.0.8", - "tsx": "^4.7.1", - "typescript": "^5.4.2" - }, - "type": "module" + "scripts": { + "build": "tsc", + "dev": "tsx watch src/main.ts", + "start": "node ./dist/main.js" + }, + "dependencies": { + "net": "^1.0.2", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@biomejs/biome": "^1.6.1", + "@types/node": "^20.11.27", + "@types/uuid": "^9.0.8", + "tsx": "^4.7.1", + "typescript": "^5.4.2" + }, + "type": "module" } diff --git a/Server/src/Client.ts b/Server/src/Client.ts index 822c248d..faf01c67 100644 --- a/Server/src/Client.ts +++ b/Server/src/Client.ts @@ -1,29 +1,29 @@ -import { v4 as uuidv4 } from 'uuid' -import type Lobby from './Lobby' +import { v4 as uuidv4 } from "uuid"; +import type Lobby from "./Lobby"; -type SendFn = (data: string) => void +type SendFn = (data: string) => void; class Client { - id: string - username: string - lobby: Lobby | null - send: SendFn + id: string; + username: string; + lobby: Lobby | null; + send: SendFn; constructor(send: SendFn) { - this.id = uuidv4() - this.lobby = null - this.username = 'Guest' - this.send = send + this.id = uuidv4(); + this.lobby = null; + this.username = "Guest"; + this.send = send; } setUsername = (username: string) => { - this.username = username - this.lobby?.broadcast() - } + this.username = username; + this.lobby?.broadcast(); + }; setLobby = (lobby: Lobby | null) => { - this.lobby = lobby - } + this.lobby = lobby; + }; } -export default Client \ No newline at end of file +export default Client; diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index 84bb8add..4a3e4e68 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -1,15 +1,17 @@ -import type Client from "./Client" +import type Client from "./Client"; +import type { ActionLobbyInfo } from "./actions"; +import { serializeAction } from "./main"; -const Lobbies = new Map() +const Lobbies = new Map(); const generateUniqueLobbyCode = (): string => { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - let result = '' + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + let result = ""; for (let i = 0; i < 5; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)) + result += chars.charAt(Math.floor(Math.random() * chars.length)); } - return Lobbies.get(result) ? generateUniqueLobbyCode() : result -} + return Lobbies.get(result) ? generateUniqueLobbyCode() : result; +}; class Lobby { code: string; @@ -18,58 +20,68 @@ class Lobby { constructor(host: Client) { do { - this.code = generateUniqueLobbyCode() - } while (Lobbies.get(this.code)) - Lobbies.set(this.code, this) - this.host = host - this.guest = null - host.setLobby(this) - host.send(`action:joinedLobby,code:${this.code}`) + this.code = generateUniqueLobbyCode(); + } while (Lobbies.get(this.code)); + Lobbies.set(this.code, this); + this.host = host; + this.guest = null; + host.setLobby(this); + host.send(serializeAction({ action: "joinedLobby", code: this.code })); } static get = (code: string) => { - return Lobbies.get(code) - } + return Lobbies.get(code); + }; leave = (client: Client) => { if (this.host?.id === client.id) { - this.host = this.guest - this.guest = null + this.host = this.guest; + this.guest = null; } if (this.guest?.id === client.id) { - this.guest = null + this.guest = null; } - client.setLobby(null) + client.setLobby(null); if (this.host === null) { - Lobbies.delete(this.code) + Lobbies.delete(this.code); } else { - this.broadcast() + this.broadcast(); } - } + }; join = (client: Client) => { if (this.guest) { - client.send('action:error,message:Lobby is full or does not exist.') - return + client.send( + serializeAction({ + action: "error", + message: "Lobby is full or does not exist.", + }), + ); + return; } - this.guest = client - client.setLobby(this) - client.send(`action:joinedLobby,code:${this.code}`) - this.broadcast() - } + this.guest = client; + client.setLobby(this); + client.send(serializeAction({ action: "joinedLobby", code: this.code })); + this.broadcast(); + }; broadcast = () => { - if(!this.host) { + if (!this.host) { return; } - let message = `action:lobbyInfo,host:${this.host.username}` + const action: ActionLobbyInfo = { + action: "lobbyInfo", + host: this.host.username, + }; + if (this.guest?.username) { - message += `,guest:${this.guest.username}` - this.guest.send(message) + action.guest = this.guest.username; + this.guest.send(serializeAction(action)); } - this.host.send(message) - } + + this.host.send(serializeAction(action)); + }; } -export default Lobby \ No newline at end of file +export default Lobby; diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts new file mode 100644 index 00000000..7ce1afbc --- /dev/null +++ b/Server/src/actionHandlers.ts @@ -0,0 +1,58 @@ +import type Client from "./Client"; +import Lobby from "./Lobby"; +import type { + ActionCreateLobby, + ActionFnArgs, + ActionHandlers, + ActionJoinLobby, + ActionUsername, +} from "./actions"; +import { serializeAction } from "./main"; + +const usernameAction = ( + { username }: ActionFnArgs, + client: Client, +) => { + client.setUsername(username); +}; + +const createLobbyAction = ( + { gameMode }: ActionFnArgs, + client: Client, +) => { + new Lobby(client); +}; + +const joinLobbyAction = ( + { code }: ActionFnArgs, + client: Client, +) => { + const newLobby = Lobby.get(code); + if (!newLobby) { + client.send( + serializeAction({ + action: "error", + message: "Lobby does not exist.", + }), + ); + return; + } + newLobby.join(client); +}; + +const leaveLobbyAction = (client: Client) => { + client.lobby?.leave(client); +}; + +const lobbyInfoAction = (client: Client) => { + client.lobby?.broadcast(); +}; + +// Declared partial for now untill all action handlers are defined +export const actionHandlers: Partial = { + username: usernameAction, + createLobby: createLobbyAction, + joinLobby: joinLobbyAction, + lobbyInfo: lobbyInfoAction, + leaveLobby: leaveLobbyAction, +}; diff --git a/Server/src/actions.ts b/Server/src/actions.ts new file mode 100644 index 00000000..5207e8ce --- /dev/null +++ b/Server/src/actions.ts @@ -0,0 +1,98 @@ +// Server to Client +export type ActionConnected = { action: "connected" }; +export type ActionError = { action: "error"; message: string }; +export type ActionJoinedLobby = { action: "joinedLobby"; code: string }; +export type ActionLobbyInfo = { + action: "lobbyInfo"; + host: string; + guest?: string; +}; +export type ActionStopGame = { action: "stopGame" }; +export type ActionStartGame = { + action: "startGame"; + deck: string; + stake?: number; + seed?: string; +}; +export type ActionStartBlind = { action: "startBlind" }; +export type ActionWinGame = { action: "winGame" }; +export type ActionLoseGame = { action: "loseGame" }; +export type ActionGameInfo = { + action: "gameInfo"; + small?: string; + big?: string; + boss?: string; +}; +export type ActionPlayerInfo = { action: "playerInfo"; lives: number }; +export type ActionEnemyInfo = { + action: "enemyInfo"; + score: number; + handsLeft: number; +}; +export type ActionEndPvP = { action: "endPvP"; lost: boolean }; + +export type ActionServerToClient = + | ActionConnected + | ActionError + | ActionJoinedLobby + | ActionLobbyInfo + | ActionStopGame + | ActionStartGame + | ActionStartBlind + | ActionWinGame + | ActionLoseGame + | ActionGameInfo + | ActionPlayerInfo + | ActionEnemyInfo + | ActionEndPvP; + +// Client to Server +export type ActionUsername = { action: "username"; username: string }; +export type ActionCreateLobby = { action: "createLobby"; gameMode: string }; +export type ActionJoinLobby = { action: "joinLobby"; code: string }; +export type ActionLeaveLobby = { action: "leaveLobby" }; +export type ActionLobbyInfoRequest = { action: "lobbyInfo" }; +export type ActionStopGameRequest = { action: "stopGame" }; +export type ActionStartGameRequest = { action: "startGame" }; +export type ActionReadyBlind = { action: "readyBlind" }; +export type ActionPlayHand = { + action: "playHand"; + score: number; + handsLeft: number; +}; +export type ActionGameInfoRequest = { action: "gameInfo" }; +export type ActionPlayerInfoRequest = { action: "playerInfo" }; +export type ActionEnemyInfoRequest = { action: "enemyInfo" }; + +export type ActionClientToServer = + | ActionUsername + | ActionCreateLobby + | ActionJoinLobby + | ActionLeaveLobby + | ActionLobbyInfoRequest + | ActionStopGameRequest + | ActionStartGameRequest + | ActionReadyBlind + | ActionPlayHand + | ActionGameInfoRequest + | ActionPlayerInfoRequest + | ActionEnemyInfoRequest; + +export type Action = ActionServerToClient | ActionClientToServer; + +export type ActionHandlers = { + [K in ActionClientToServer["action"]]: keyof ActionFnArgs< + Extract + > extends never + ? ( + // biome-ignore lint/suspicious/noExplicitAny: Function can receive any arguments + ...args: any[] + ) => void + : ( + action: ActionFnArgs>, + // biome-ignore lint/suspicious/noExplicitAny: Function can receive any arguments + ...args: any[] + ) => void; +}; + +export type ActionFnArgs = Omit; diff --git a/Server/src/main.ts b/Server/src/main.ts index 1cb9c837..6fc3cc51 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -1,110 +1,94 @@ -import net from 'node:net' -import Client from './Client.js' -import Lobby from './Lobby.js' +import net from "node:net"; +import Client from "./Client"; +import Lobby from "./Lobby"; +import type { ActionClientToServer, ActionServerToClient } from "./actions"; +import { actionHandlers } from "./actionHandlers"; -const PORT = 8080 +const PORT = 8080; // biome-ignore lint/suspicious/noExplicitAny: Object is parsed from string const stringToJson = (str: string): any => { // biome-ignore lint/suspicious/noExplicitAny: Object is parsed from string - const obj: any = {} - for(const part of str.split(',')) { - const [key, value] = part.split(':') - obj[key] = value + const obj: any = {}; + for (const part of str.split(",")) { + const [key, value] = part.split(":"); + obj[key] = value; } - return obj -} + return obj; +}; + +/** Serializes an action for transmission to the client */ +export const serializeAction = (action: ActionServerToClient): string => { + const entries = Object.entries(action); + const parts = entries + .filter(([_key, value]) => value !== undefined && value !== null) + .map(([key, value]) => `${key}:${value}`); + return parts.join(","); +}; const sendToSocket = (socket: net.Socket) => (data: string) => { - //console.log('Responding with ' + data) if (!socket) { - //console.log('Socket is undefined') - return - } - socket.write(`${data}\n`) -} - -const usernameAction = (client: Client, username: string) => { - client.setUsername(username) -} - -const createLobbyAction = (client: Client) => { - new Lobby(client) -} - -const joinLobbyAction = (client: Client, code: string) => { - const newLobby = Lobby.get(code) - if (!newLobby) { - client.send('action:error,message:Lobby is full or does not exist.') - return + return; } - newLobby.join(client) -} - -const leaveLobbyAction = (client: Client) => { - client.lobby?.leave(client) -} - -const lobbyInfoAction = (client: Client) => { - client.lobby?.broadcast() -} + socket.write(`${data}\n`); +}; const server = net.createServer((socket) => { - const client = new Client(sendToSocket(socket)) - //console.log('Client connected') - client.send("action:connected") + const client = new Client(sendToSocket(socket)); + client.send(serializeAction({ action: "connected" })); - socket.on('data', (data) => { - const messages = data.toString().split('\n') + socket.on("data", (data) => { + const messages = data.toString().split("\n"); - for(const msg of messages) { - if (!msg) return - //console.log('Recieved message ' + msg) + for (const msg of messages) { + if (!msg) return; try { - const message = stringToJson(msg) - switch (message.action) { - case 'username': - usernameAction(client, message.username) - break - case 'createLobby': - createLobbyAction(client) - break - case 'joinLobby': - joinLobbyAction(client, message.code) - break - case 'leaveLobby': - leaveLobbyAction(client) - break - case 'lobbyInfo': - lobbyInfoAction(client) - break - } + const message: ActionClientToServer = stringToJson(msg); + const { action, ...actionArgs } = message; + + // This only works for now, once we add more arguments + // we'll need to refactor this + // Maybe add a context type that includes everything + // connection related? + actionArgs + ? actionHandlers[action]?.(actionArgs, client) + : actionHandlers[action]?.(client); } catch (error) { - console.error('Failed to parse message', error) - client.send("action:error,message:Failed to parse message") + const failedToParseError = "Failed to parse message"; + console.error(failedToParseError, error); + client.send( + serializeAction({ + action: "error", + message: failedToParseError, + }), + ); } } - }) + }); - socket.on('end', () => { - //console.log('Client disconnected') - leaveLobbyAction(client) - }) + socket.on("end", () => { + actionHandlers.leaveLobby?.(client); + }); - socket.on('error', (err: Error & { - errno: number, - code: string, - syscall: string - }) => { - if (err.code === 'ECONNRESET') { - console.warn('TCP connection reset by peer (client).') - } else { - console.error('An unexpected error occurred:', err) - } - leaveLobbyAction(client) - }) -}) + socket.on( + "error", + ( + err: Error & { + errno: number; + code: string; + syscall: string; + }, + ) => { + if (err.code === "ECONNRESET") { + console.warn("TCP connection reset by peer (client)."); + } else { + console.error("An unexpected error occurred:", err); + } + actionHandlers.leaveLobby?.(client); + }, + ); +}); -server.listen(PORT, '0.0.0.0', () => { +server.listen(PORT, "0.0.0.0", () => { console.log(`Server listening on port ${PORT}`); -}) \ No newline at end of file +}); From 220e2d4a00443f9502d7b5b72d6fede043fc665f Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 15 Mar 2024 17:26:59 +0000 Subject: [PATCH 0006/1128] Implemented reconnect button when not connected to server --- Multiplayer/Core.lua | 2 +- Multiplayer/Game_UI.lua | 2 +- Multiplayer/Lobby.lua | 5 +++++ Multiplayer/Main_Menu.lua | 14 ++++++++++---- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Multiplayer/Core.lua b/Multiplayer/Core.lua index f0867c42..d929dd81 100644 --- a/Multiplayer/Core.lua +++ b/Multiplayer/Core.lua @@ -32,7 +32,7 @@ function SMODS.INIT.VirtualizedMultiplayer() require "Deck" require "Main_Menu" require "Utils".get_username() - Networking.authorize() + require "Networking".authorize() require "Mod_Description".load_description_gui() require "Game_UI" end diff --git a/Multiplayer/Game_UI.lua b/Multiplayer/Game_UI.lua index 83433c26..679ffd07 100644 --- a/Multiplayer/Game_UI.lua +++ b/Multiplayer/Game_UI.lua @@ -6,7 +6,7 @@ local Lobby = require "Lobby" -local Game_UI = {} +Game_UI = {} local create_UIBox_options_ref = create_UIBox_options function create_UIBox_options() diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 1380a480..41f67ff4 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -69,6 +69,11 @@ function G.FUNCS.copy_to_clipboard(arg_736_0) Utils.copy_to_clipboard(Lobby.code) end +function G.FUNCS.reconnect(arg_736_0) + Networking.authorize() + G.FUNCS:exit_overlay_menu() +end + function create_UIBox_view_code() local var_495_0 = 0.75 diff --git a/Multiplayer/Main_Menu.lua b/Multiplayer/Main_Menu.lua index bbf80fc6..fcd68101 100644 --- a/Multiplayer/Main_Menu.lua +++ b/Multiplayer/Main_Menu.lua @@ -322,18 +322,24 @@ function override_main_menu_play_button() button = "setup_run", minw = 5, }), - UIBox_button({ + Lobby.connected and UIBox_button({ label = {"Create Lobby"}, colour = G.C.GREEN, button = "create_lobby", minw = 5, - }), - UIBox_button({ + }) or nil, + Lobby.connected and UIBox_button({ label = {"Join Lobby"}, colour = G.C.RED, button = "join_lobby", minw = 5, - }), + }) or nil, + not Lobby.connected and UIBox_button({ + label = {"Reconnect"}, + colour = G.C.RED, + button = "reconnect", + minw = 5, + }) or nil, } })) end From e1d38df0a17418654689072cc1b7f34f7b2ac3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Fri, 15 Mar 2024 10:33:30 -0700 Subject: [PATCH 0007/1128] Fixed formatting for all files (#4) --- Server/biome.json | 40 +++++---- Server/package.json | 34 ++++---- Server/src/Client.ts | 42 ++++----- Server/src/Lobby.ts | 144 +++++++++++++++--------------- Server/src/actionHandlers.ts | 82 +++++++++--------- Server/src/actions.ts | 164 +++++++++++++++++------------------ Server/src/main.ts | 153 ++++++++++++++++---------------- Server/tsconfig.json | 62 ++++++------- 8 files changed, 365 insertions(+), 356 deletions(-) diff --git a/Server/biome.json b/Server/biome.json index 7ef51e75..763b026e 100644 --- a/Server/biome.json +++ b/Server/biome.json @@ -1,17 +1,27 @@ { - "$schema": "https://biomejs.dev/schemas/1.6.1/schema.json", - "organizeImports": { - "enabled": true - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - } + "$schema": "https://biomejs.dev/schemas/1.6.1/schema.json", + "organizeImports": { + "enabled": true + }, + "files": { + "ignore": ["dist"] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab", + "indentWidth": 2, + "lineEnding": "lf" + }, + "javascript": { + "formatter": { + "semicolons": "asNeeded", + "quoteStyle": "single" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } } diff --git a/Server/package.json b/Server/package.json index aaf47c94..5f456724 100644 --- a/Server/package.json +++ b/Server/package.json @@ -1,19 +1,19 @@ { - "scripts": { - "build": "tsc", - "dev": "tsx watch src/main.ts", - "start": "node ./dist/main.js" - }, - "dependencies": { - "net": "^1.0.2", - "uuid": "^9.0.1" - }, - "devDependencies": { - "@biomejs/biome": "^1.6.1", - "@types/node": "^20.11.27", - "@types/uuid": "^9.0.8", - "tsx": "^4.7.1", - "typescript": "^5.4.2" - }, - "type": "module" + "scripts": { + "build": "tsc", + "dev": "tsx watch src/main.ts", + "start": "node ./dist/main.js" + }, + "dependencies": { + "net": "^1.0.2", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@biomejs/biome": "^1.6.1", + "@types/node": "^20.11.27", + "@types/uuid": "^9.0.8", + "tsx": "^4.7.1", + "typescript": "^5.4.2" + }, + "type": "module" } diff --git a/Server/src/Client.ts b/Server/src/Client.ts index faf01c67..f657ee06 100644 --- a/Server/src/Client.ts +++ b/Server/src/Client.ts @@ -1,29 +1,29 @@ -import { v4 as uuidv4 } from "uuid"; -import type Lobby from "./Lobby"; +import { v4 as uuidv4 } from 'uuid' +import type Lobby from './Lobby' -type SendFn = (data: string) => void; +type SendFn = (data: string) => void class Client { - id: string; - username: string; - lobby: Lobby | null; - send: SendFn; + id: string + username: string + lobby: Lobby | null + send: SendFn - constructor(send: SendFn) { - this.id = uuidv4(); - this.lobby = null; - this.username = "Guest"; - this.send = send; - } + constructor(send: SendFn) { + this.id = uuidv4() + this.lobby = null + this.username = 'Guest' + this.send = send + } - setUsername = (username: string) => { - this.username = username; - this.lobby?.broadcast(); - }; + setUsername = (username: string) => { + this.username = username + this.lobby?.broadcast() + } - setLobby = (lobby: Lobby | null) => { - this.lobby = lobby; - }; + setLobby = (lobby: Lobby | null) => { + this.lobby = lobby + } } -export default Client; +export default Client diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index 4a3e4e68..11987c0d 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -1,87 +1,87 @@ -import type Client from "./Client"; -import type { ActionLobbyInfo } from "./actions"; -import { serializeAction } from "./main"; +import type Client from './Client' +import type { ActionLobbyInfo } from './actions' +import { serializeAction } from './main' -const Lobbies = new Map(); +const Lobbies = new Map() const generateUniqueLobbyCode = (): string => { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - let result = ""; - for (let i = 0; i < 5; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return Lobbies.get(result) ? generateUniqueLobbyCode() : result; -}; + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + let result = '' + for (let i = 0; i < 5; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)) + } + return Lobbies.get(result) ? generateUniqueLobbyCode() : result +} class Lobby { - code: string; - host: Client | null; - guest: Client | null; + code: string + host: Client | null + guest: Client | null - constructor(host: Client) { - do { - this.code = generateUniqueLobbyCode(); - } while (Lobbies.get(this.code)); - Lobbies.set(this.code, this); - this.host = host; - this.guest = null; - host.setLobby(this); - host.send(serializeAction({ action: "joinedLobby", code: this.code })); - } + constructor(host: Client) { + do { + this.code = generateUniqueLobbyCode() + } while (Lobbies.get(this.code)) + Lobbies.set(this.code, this) + this.host = host + this.guest = null + host.setLobby(this) + host.send(serializeAction({ action: 'joinedLobby', code: this.code })) + } - static get = (code: string) => { - return Lobbies.get(code); - }; + static get = (code: string) => { + return Lobbies.get(code) + } - leave = (client: Client) => { - if (this.host?.id === client.id) { - this.host = this.guest; - this.guest = null; - } - if (this.guest?.id === client.id) { - this.guest = null; - } - client.setLobby(null); - if (this.host === null) { - Lobbies.delete(this.code); - } else { - this.broadcast(); - } - }; + leave = (client: Client) => { + if (this.host?.id === client.id) { + this.host = this.guest + this.guest = null + } + if (this.guest?.id === client.id) { + this.guest = null + } + client.setLobby(null) + if (this.host === null) { + Lobbies.delete(this.code) + } else { + this.broadcast() + } + } - join = (client: Client) => { - if (this.guest) { - client.send( - serializeAction({ - action: "error", - message: "Lobby is full or does not exist.", - }), - ); - return; - } - this.guest = client; - client.setLobby(this); - client.send(serializeAction({ action: "joinedLobby", code: this.code })); - this.broadcast(); - }; + join = (client: Client) => { + if (this.guest) { + client.send( + serializeAction({ + action: 'error', + message: 'Lobby is full or does not exist.', + }), + ) + return + } + this.guest = client + client.setLobby(this) + client.send(serializeAction({ action: 'joinedLobby', code: this.code })) + this.broadcast() + } - broadcast = () => { - if (!this.host) { - return; - } + broadcast = () => { + if (!this.host) { + return + } - const action: ActionLobbyInfo = { - action: "lobbyInfo", - host: this.host.username, - }; + const action: ActionLobbyInfo = { + action: 'lobbyInfo', + host: this.host.username, + } - if (this.guest?.username) { - action.guest = this.guest.username; - this.guest.send(serializeAction(action)); - } + if (this.guest?.username) { + action.guest = this.guest.username + this.guest.send(serializeAction(action)) + } - this.host.send(serializeAction(action)); - }; + this.host.send(serializeAction(action)) + } } -export default Lobby; +export default Lobby diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 7ce1afbc..c288a67f 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -1,58 +1,58 @@ -import type Client from "./Client"; -import Lobby from "./Lobby"; +import type Client from './Client' +import Lobby from './Lobby' import type { - ActionCreateLobby, - ActionFnArgs, - ActionHandlers, - ActionJoinLobby, - ActionUsername, -} from "./actions"; -import { serializeAction } from "./main"; + ActionCreateLobby, + ActionFnArgs, + ActionHandlers, + ActionJoinLobby, + ActionUsername, +} from './actions' +import { serializeAction } from './main' const usernameAction = ( - { username }: ActionFnArgs, - client: Client, + { username }: ActionFnArgs, + client: Client, ) => { - client.setUsername(username); -}; + client.setUsername(username) +} const createLobbyAction = ( - { gameMode }: ActionFnArgs, - client: Client, + { gameMode }: ActionFnArgs, + client: Client, ) => { - new Lobby(client); -}; + new Lobby(client) +} const joinLobbyAction = ( - { code }: ActionFnArgs, - client: Client, + { code }: ActionFnArgs, + client: Client, ) => { - const newLobby = Lobby.get(code); - if (!newLobby) { - client.send( - serializeAction({ - action: "error", - message: "Lobby does not exist.", - }), - ); - return; - } - newLobby.join(client); -}; + const newLobby = Lobby.get(code) + if (!newLobby) { + client.send( + serializeAction({ + action: 'error', + message: 'Lobby does not exist.', + }), + ) + return + } + newLobby.join(client) +} const leaveLobbyAction = (client: Client) => { - client.lobby?.leave(client); -}; + client.lobby?.leave(client) +} const lobbyInfoAction = (client: Client) => { - client.lobby?.broadcast(); -}; + client.lobby?.broadcast() +} // Declared partial for now untill all action handlers are defined export const actionHandlers: Partial = { - username: usernameAction, - createLobby: createLobbyAction, - joinLobby: joinLobbyAction, - lobbyInfo: lobbyInfoAction, - leaveLobby: leaveLobbyAction, -}; + username: usernameAction, + createLobby: createLobbyAction, + joinLobby: joinLobbyAction, + lobbyInfo: lobbyInfoAction, + leaveLobby: leaveLobbyAction, +} diff --git a/Server/src/actions.ts b/Server/src/actions.ts index 5207e8ce..41608906 100644 --- a/Server/src/actions.ts +++ b/Server/src/actions.ts @@ -1,98 +1,98 @@ // Server to Client -export type ActionConnected = { action: "connected" }; -export type ActionError = { action: "error"; message: string }; -export type ActionJoinedLobby = { action: "joinedLobby"; code: string }; +export type ActionConnected = { action: 'connected' } +export type ActionError = { action: 'error'; message: string } +export type ActionJoinedLobby = { action: 'joinedLobby'; code: string } export type ActionLobbyInfo = { - action: "lobbyInfo"; - host: string; - guest?: string; -}; -export type ActionStopGame = { action: "stopGame" }; + action: 'lobbyInfo' + host: string + guest?: string +} +export type ActionStopGame = { action: 'stopGame' } export type ActionStartGame = { - action: "startGame"; - deck: string; - stake?: number; - seed?: string; -}; -export type ActionStartBlind = { action: "startBlind" }; -export type ActionWinGame = { action: "winGame" }; -export type ActionLoseGame = { action: "loseGame" }; + action: 'startGame' + deck: string + stake?: number + seed?: string +} +export type ActionStartBlind = { action: 'startBlind' } +export type ActionWinGame = { action: 'winGame' } +export type ActionLoseGame = { action: 'loseGame' } export type ActionGameInfo = { - action: "gameInfo"; - small?: string; - big?: string; - boss?: string; -}; -export type ActionPlayerInfo = { action: "playerInfo"; lives: number }; + action: 'gameInfo' + small?: string + big?: string + boss?: string +} +export type ActionPlayerInfo = { action: 'playerInfo'; lives: number } export type ActionEnemyInfo = { - action: "enemyInfo"; - score: number; - handsLeft: number; -}; -export type ActionEndPvP = { action: "endPvP"; lost: boolean }; + action: 'enemyInfo' + score: number + handsLeft: number +} +export type ActionEndPvP = { action: 'endPvP'; lost: boolean } export type ActionServerToClient = - | ActionConnected - | ActionError - | ActionJoinedLobby - | ActionLobbyInfo - | ActionStopGame - | ActionStartGame - | ActionStartBlind - | ActionWinGame - | ActionLoseGame - | ActionGameInfo - | ActionPlayerInfo - | ActionEnemyInfo - | ActionEndPvP; + | ActionConnected + | ActionError + | ActionJoinedLobby + | ActionLobbyInfo + | ActionStopGame + | ActionStartGame + | ActionStartBlind + | ActionWinGame + | ActionLoseGame + | ActionGameInfo + | ActionPlayerInfo + | ActionEnemyInfo + | ActionEndPvP // Client to Server -export type ActionUsername = { action: "username"; username: string }; -export type ActionCreateLobby = { action: "createLobby"; gameMode: string }; -export type ActionJoinLobby = { action: "joinLobby"; code: string }; -export type ActionLeaveLobby = { action: "leaveLobby" }; -export type ActionLobbyInfoRequest = { action: "lobbyInfo" }; -export type ActionStopGameRequest = { action: "stopGame" }; -export type ActionStartGameRequest = { action: "startGame" }; -export type ActionReadyBlind = { action: "readyBlind" }; +export type ActionUsername = { action: 'username'; username: string } +export type ActionCreateLobby = { action: 'createLobby'; gameMode: string } +export type ActionJoinLobby = { action: 'joinLobby'; code: string } +export type ActionLeaveLobby = { action: 'leaveLobby' } +export type ActionLobbyInfoRequest = { action: 'lobbyInfo' } +export type ActionStopGameRequest = { action: 'stopGame' } +export type ActionStartGameRequest = { action: 'startGame' } +export type ActionReadyBlind = { action: 'readyBlind' } export type ActionPlayHand = { - action: "playHand"; - score: number; - handsLeft: number; -}; -export type ActionGameInfoRequest = { action: "gameInfo" }; -export type ActionPlayerInfoRequest = { action: "playerInfo" }; -export type ActionEnemyInfoRequest = { action: "enemyInfo" }; + action: 'playHand' + score: number + handsLeft: number +} +export type ActionGameInfoRequest = { action: 'gameInfo' } +export type ActionPlayerInfoRequest = { action: 'playerInfo' } +export type ActionEnemyInfoRequest = { action: 'enemyInfo' } export type ActionClientToServer = - | ActionUsername - | ActionCreateLobby - | ActionJoinLobby - | ActionLeaveLobby - | ActionLobbyInfoRequest - | ActionStopGameRequest - | ActionStartGameRequest - | ActionReadyBlind - | ActionPlayHand - | ActionGameInfoRequest - | ActionPlayerInfoRequest - | ActionEnemyInfoRequest; + | ActionUsername + | ActionCreateLobby + | ActionJoinLobby + | ActionLeaveLobby + | ActionLobbyInfoRequest + | ActionStopGameRequest + | ActionStartGameRequest + | ActionReadyBlind + | ActionPlayHand + | ActionGameInfoRequest + | ActionPlayerInfoRequest + | ActionEnemyInfoRequest -export type Action = ActionServerToClient | ActionClientToServer; +export type Action = ActionServerToClient | ActionClientToServer export type ActionHandlers = { - [K in ActionClientToServer["action"]]: keyof ActionFnArgs< - Extract - > extends never - ? ( - // biome-ignore lint/suspicious/noExplicitAny: Function can receive any arguments - ...args: any[] - ) => void - : ( - action: ActionFnArgs>, - // biome-ignore lint/suspicious/noExplicitAny: Function can receive any arguments - ...args: any[] - ) => void; -}; + [K in ActionClientToServer['action']]: keyof ActionFnArgs< + Extract + > extends never + ? ( + // biome-ignore lint/suspicious/noExplicitAny: Function can receive any arguments + ...args: any[] + ) => void + : ( + action: ActionFnArgs>, + // biome-ignore lint/suspicious/noExplicitAny: Function can receive any arguments + ...args: any[] + ) => void +} -export type ActionFnArgs = Omit; +export type ActionFnArgs = Omit diff --git a/Server/src/main.ts b/Server/src/main.ts index 6fc3cc51..bf8231cb 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -1,94 +1,93 @@ -import net from "node:net"; -import Client from "./Client"; -import Lobby from "./Lobby"; -import type { ActionClientToServer, ActionServerToClient } from "./actions"; -import { actionHandlers } from "./actionHandlers"; +import net from 'node:net' +import Client from './Client' +import { actionHandlers } from './actionHandlers' +import type { ActionClientToServer, ActionServerToClient } from './actions' -const PORT = 8080; +const PORT = 8080 // biome-ignore lint/suspicious/noExplicitAny: Object is parsed from string const stringToJson = (str: string): any => { - // biome-ignore lint/suspicious/noExplicitAny: Object is parsed from string - const obj: any = {}; - for (const part of str.split(",")) { - const [key, value] = part.split(":"); - obj[key] = value; - } - return obj; -}; + // biome-ignore lint/suspicious/noExplicitAny: Object is parsed from string + const obj: any = {} + for (const part of str.split(',')) { + const [key, value] = part.split(':') + obj[key] = value + } + return obj +} /** Serializes an action for transmission to the client */ export const serializeAction = (action: ActionServerToClient): string => { - const entries = Object.entries(action); - const parts = entries - .filter(([_key, value]) => value !== undefined && value !== null) - .map(([key, value]) => `${key}:${value}`); - return parts.join(","); -}; + const entries = Object.entries(action) + const parts = entries + .filter(([_key, value]) => value !== undefined && value !== null) + .map(([key, value]) => `${key}:${value}`) + return parts.join(',') +} const sendToSocket = (socket: net.Socket) => (data: string) => { - if (!socket) { - return; - } - socket.write(`${data}\n`); -}; + if (!socket) { + return + } + socket.write(`${data}\n`) +} const server = net.createServer((socket) => { - const client = new Client(sendToSocket(socket)); - client.send(serializeAction({ action: "connected" })); + const client = new Client(sendToSocket(socket)) + client.send(serializeAction({ action: 'connected' })) - socket.on("data", (data) => { - const messages = data.toString().split("\n"); + socket.on('data', (data) => { + const messages = data.toString().split('\n') - for (const msg of messages) { - if (!msg) return; - try { - const message: ActionClientToServer = stringToJson(msg); - const { action, ...actionArgs } = message; + for (const msg of messages) { + if (!msg) return + try { + const message: ActionClientToServer = stringToJson(msg) + const { action, ...actionArgs } = message - // This only works for now, once we add more arguments - // we'll need to refactor this - // Maybe add a context type that includes everything - // connection related? - actionArgs - ? actionHandlers[action]?.(actionArgs, client) - : actionHandlers[action]?.(client); - } catch (error) { - const failedToParseError = "Failed to parse message"; - console.error(failedToParseError, error); - client.send( - serializeAction({ - action: "error", - message: failedToParseError, - }), - ); - } - } - }); + // This only works for now, once we add more arguments + // we'll need to refactor this + // Maybe add a context type that includes everything + // connection related? + actionArgs + ? actionHandlers[action]?.(actionArgs, client) + : actionHandlers[action]?.(client) + } catch (error) { + const failedToParseError = 'Failed to parse message' + console.error(failedToParseError, error) + client.send( + serializeAction({ + action: 'error', + message: failedToParseError, + }), + ) + } + } + }) - socket.on("end", () => { - actionHandlers.leaveLobby?.(client); - }); + socket.on('end', () => { + actionHandlers.leaveLobby?.(client) + }) - socket.on( - "error", - ( - err: Error & { - errno: number; - code: string; - syscall: string; - }, - ) => { - if (err.code === "ECONNRESET") { - console.warn("TCP connection reset by peer (client)."); - } else { - console.error("An unexpected error occurred:", err); - } - actionHandlers.leaveLobby?.(client); - }, - ); -}); + socket.on( + 'error', + ( + err: Error & { + errno: number + code: string + syscall: string + }, + ) => { + if (err.code === 'ECONNRESET') { + console.warn('TCP connection reset by peer (client).') + } else { + console.error('An unexpected error occurred:', err) + } + actionHandlers.leaveLobby?.(client) + }, + ) +}) -server.listen(PORT, "0.0.0.0", () => { - console.log(`Server listening on port ${PORT}`); -}); +server.listen(PORT, '0.0.0.0', () => { + console.log(`Server listening on port ${PORT}`) +}) diff --git a/Server/tsconfig.json b/Server/tsconfig.json index 53c8c3f6..ee2ba48f 100644 --- a/Server/tsconfig.json +++ b/Server/tsconfig.json @@ -1,39 +1,39 @@ { - "compilerOptions": { - /* Language and Environment */ - "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + "compilerOptions": { + /* Language and Environment */ + "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "moduleDetection": "auto" /* Control what method is used to detect module-format JS files. */, - /* Modules */ - "module": "ES6", /* Specify what module code is generated. */ - "moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */ - "resolveJsonModule": true, /* Enable importing .json files. */ + /* Modules */ + "module": "ES6" /* Specify what module code is generated. */, + "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, + "resolveJsonModule": true /* Enable importing .json files. */, - /* JavaScript Support */ - "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* JavaScript Support */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, + "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, + "maxNodeModuleJsDepth": 1 /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */, - /* Emit */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ + /* Emit */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, - /* Interop Constraints */ - "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + /* Interop Constraints */ + "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, + "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, + "strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */, + "noImplicitThis": true /* Enable error reporting when 'this' is given the type 'any'. */, + "useUnknownInCatchVariables": true /* Default catch clause variables as 'unknown' instead of 'any'. */, + "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, + "noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */, - /* Completeness */ - "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + /* Completeness */ + "skipDefaultLibCheck": true /* Skip type checking .d.ts files that are included with TypeScript. */, + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } } From 8b19d4d0872e1e6b915375be3114028f85ed3274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Fri, 15 Mar 2024 15:08:25 -0700 Subject: [PATCH 0008/1128] Fixed node resolution (#6) --- Server/src/Client.ts | 2 +- Server/src/Lobby.ts | 6 +++--- Server/src/actionHandlers.ts | 8 ++++---- Server/src/main.ts | 6 +++--- Server/tsconfig.json | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Server/src/Client.ts b/Server/src/Client.ts index f657ee06..efef946f 100644 --- a/Server/src/Client.ts +++ b/Server/src/Client.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from 'uuid' -import type Lobby from './Lobby' +import type Lobby from './Lobby.js' type SendFn = (data: string) => void diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index 11987c0d..34216644 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -1,6 +1,6 @@ -import type Client from './Client' -import type { ActionLobbyInfo } from './actions' -import { serializeAction } from './main' +import type Client from './Client.js' +import type { ActionLobbyInfo } from './actions.js' +import { serializeAction } from './main.js' const Lobbies = new Map() diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index c288a67f..5297828d 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -1,13 +1,13 @@ -import type Client from './Client' -import Lobby from './Lobby' +import type Client from './Client.js' +import Lobby from './Lobby.js' import type { ActionCreateLobby, ActionFnArgs, ActionHandlers, ActionJoinLobby, ActionUsername, -} from './actions' -import { serializeAction } from './main' +} from './actions.js' +import { serializeAction } from './main.js' const usernameAction = ( { username }: ActionFnArgs, diff --git a/Server/src/main.ts b/Server/src/main.ts index bf8231cb..01dbe0ba 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -1,7 +1,7 @@ import net from 'node:net' -import Client from './Client' -import { actionHandlers } from './actionHandlers' -import type { ActionClientToServer, ActionServerToClient } from './actions' +import Client from './Client.js' +import { actionHandlers } from './actionHandlers.js' +import type { ActionClientToServer, ActionServerToClient } from './actions.js' const PORT = 8080 diff --git a/Server/tsconfig.json b/Server/tsconfig.json index ee2ba48f..69259248 100644 --- a/Server/tsconfig.json +++ b/Server/tsconfig.json @@ -5,8 +5,8 @@ "moduleDetection": "auto" /* Control what method is used to detect module-format JS files. */, /* Modules */ - "module": "ES6" /* Specify what module code is generated. */, - "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, + "module": "NodeNext" /* Specify what module code is generated. */, + "moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */, "resolveJsonModule": true /* Enable importing .json files. */, /* JavaScript Support */ From 30058604be6308cec8507b75c650c076388255e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Fri, 15 Mar 2024 15:49:18 -0700 Subject: [PATCH 0009/1128] Implemented keepAlive in the server-side (#7) * Implemented keepAlive mechanism on the server * Implemented keepAlive response on the client * Fixed keepAlive action handler --- Multiplayer/Networking.lua | 17 +++++++++++- Server/README.md | 10 +++++++ Server/src/Client.ts | 10 ++++++- Server/src/actionHandlers.ts | 14 +++++++--- Server/src/actions.ts | 17 ++++++++---- Server/src/main.ts | 53 +++++++++++++++++++++++++++++++++--- 6 files changed, 106 insertions(+), 15 deletions(-) diff --git a/Multiplayer/Networking.lua b/Multiplayer/Networking.lua index 16e63f6c..6cf68d5d 100644 --- a/Multiplayer/Networking.lua +++ b/Multiplayer/Networking.lua @@ -53,6 +53,10 @@ local function action_error(message) Utils.overlay_message(message) end +local function action_keep_alive() + Networking.Client:send('action:keepAliveAck') +end + local game_update_ref = Game.update function Game.update(arg_298_0, arg_298_1) if Networking.Client then @@ -71,6 +75,8 @@ function Game.update(arg_298_0, arg_298_1) action_lobbyInfo(t.host, t.guest) elseif t.action == 'error' then action_error(t.message) + elseif t.action == 'keepAlive' then + action_keep_alive() end end until not data @@ -80,9 +86,18 @@ function Game.update(arg_298_0, arg_298_1) end function Networking.authorize() + sendDebugMessage(string.format("Attempting to connect to multiplayer server... URL: %s, PORT: %d", Config.URL, Config.PORT)) + Networking.Client = socket.tcp() + + Networking.Client:setoption('tcp-nodelay', true) + local connectionResult, errorMessage = Networking.Client:connect(Config.URL, Config.PORT) -- Not sure if I want to make these values public yet + + if connectionResult ~= 1 then + sendDebugMessage(string.format("%s", errorMessage)) + end + Networking.Client:settimeout(0) - Networking.Client:connect(Config.URL, Config.PORT) -- Not sure if I want to make these values public yet end function Networking.create_lobby() diff --git a/Server/README.md b/Server/README.md index 2ddec3ef..080c977d 100644 --- a/Server/README.md +++ b/Server/README.md @@ -158,6 +158,16 @@ enemyInfo --- +### Utility + +keepAlive +- Request a keepAliveAck response. + +--- + +keepAliveAck +- Send a response to the keepAlive request. + ## Server Types - attrition diff --git a/Server/src/Client.ts b/Server/src/Client.ts index efef946f..76161342 100644 --- a/Server/src/Client.ts +++ b/Server/src/Client.ts @@ -1,18 +1,26 @@ import { v4 as uuidv4 } from 'uuid' import type Lobby from './Lobby.js' +import type net from 'node:net' type SendFn = (data: string) => void +/* biome-ignore lint/complexity/noBannedTypes: + This is how the net module does it */ +type Address = net.AddressInfo | {} + class Client { id: string + // Could be useful later on to detect reconnects + address: Address username: string lobby: Lobby | null send: SendFn - constructor(send: SendFn) { + constructor(address: Address, send: SendFn) { this.id = uuidv4() this.lobby = null this.username = 'Guest' + this.address = address this.send = send } diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 5297828d..6c43b09f 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -2,7 +2,7 @@ import type Client from './Client.js' import Lobby from './Lobby.js' import type { ActionCreateLobby, - ActionFnArgs, + ActionHandlerArgs, ActionHandlers, ActionJoinLobby, ActionUsername, @@ -10,21 +10,21 @@ import type { import { serializeAction } from './main.js' const usernameAction = ( - { username }: ActionFnArgs, + { username }: ActionHandlerArgs, client: Client, ) => { client.setUsername(username) } const createLobbyAction = ( - { gameMode }: ActionFnArgs, + { gameMode }: ActionHandlerArgs, client: Client, ) => { new Lobby(client) } const joinLobbyAction = ( - { code }: ActionFnArgs, + { code }: ActionHandlerArgs, client: Client, ) => { const newLobby = Lobby.get(code) @@ -48,6 +48,11 @@ const lobbyInfoAction = (client: Client) => { client.lobby?.broadcast() } +const keepAliveAction = (client: Client) => { + // Send an ack back to the received keepAlive + client.send(serializeAction({ action: 'keepAliveAck' })) +} + // Declared partial for now untill all action handlers are defined export const actionHandlers: Partial = { username: usernameAction, @@ -55,4 +60,5 @@ export const actionHandlers: Partial = { joinLobby: joinLobbyAction, lobbyInfo: lobbyInfoAction, leaveLobby: leaveLobbyAction, + keepAlive: keepAliveAction, } diff --git a/Server/src/actions.ts b/Server/src/actions.ts index 41608906..8bae7ba9 100644 --- a/Server/src/actions.ts +++ b/Server/src/actions.ts @@ -78,21 +78,28 @@ export type ActionClientToServer = | ActionPlayerInfoRequest | ActionEnemyInfoRequest -export type Action = ActionServerToClient | ActionClientToServer +// Utility actions +export type ActionKeepAlive = { action: 'keepAlive' } +export type ActionKeepAliveAck = { action: 'keepAliveAck' } +export type ActionUtility = ActionKeepAlive | ActionKeepAliveAck + +export type Action = ActionServerToClient | ActionClientToServer | ActionUtility + +type HandledActions = ActionClientToServer | ActionUtility export type ActionHandlers = { - [K in ActionClientToServer['action']]: keyof ActionFnArgs< - Extract + [K in HandledActions['action']]: keyof ActionHandlerArgs< + Extract > extends never ? ( // biome-ignore lint/suspicious/noExplicitAny: Function can receive any arguments ...args: any[] ) => void : ( - action: ActionFnArgs>, + action: ActionHandlerArgs>, // biome-ignore lint/suspicious/noExplicitAny: Function can receive any arguments ...args: any[] ) => void } -export type ActionFnArgs = Omit +export type ActionHandlerArgs = Omit diff --git a/Server/src/main.ts b/Server/src/main.ts index 01dbe0ba..6e96adc0 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -1,10 +1,17 @@ import net from 'node:net' import Client from './Client.js' import { actionHandlers } from './actionHandlers.js' -import type { ActionClientToServer, ActionServerToClient } from './actions.js' +import type { Action, ActionClientToServer, ActionUtility } from './actions.js' const PORT = 8080 +/** The amount of milliseconds we wait before sending the initial keepalive packet */ +const KEEP_ALIVE_INITIAL_TIMEOUT = 5000 +/** The amount of milliseconds we wait after sending a new retry packet */ +const KEEP_ALIVE_RETRY_TIMEOUT = 2500 +/** The amount of retries we do before we declare the socket dead */ +const KEEP_ALIVE_RETRY_COUNT = 3 + // biome-ignore lint/suspicious/noExplicitAny: Object is parsed from string const stringToJson = (str: string): any => { // biome-ignore lint/suspicious/noExplicitAny: Object is parsed from string @@ -17,7 +24,7 @@ const stringToJson = (str: string): any => { } /** Serializes an action for transmission to the client */ -export const serializeAction = (action: ActionServerToClient): string => { +export const serializeAction = (action: Action): string => { const entries = Object.entries(action) const parts = entries .filter(([_key, value]) => value !== undefined && value !== null) @@ -33,17 +40,54 @@ const sendToSocket = (socket: net.Socket) => (data: string) => { } const server = net.createServer((socket) => { - const client = new Client(sendToSocket(socket)) + socket.allowHalfOpen = false + // Do not wait for packets to buffer, helps + // improve latency between responses + socket.setNoDelay() + + const client = new Client(socket.address(), sendToSocket(socket)) client.send(serializeAction({ action: 'connected' })) + let isRetry = false + let retryCount = 0 + + const retryTimer: ReturnType = setTimeout(() => { + // Ignore if not retry + if (!isRetry) { + return + } + + client.send(serializeAction({ action: 'keepAlive' })) + retryCount++ + + if (retryCount >= KEEP_ALIVE_RETRY_COUNT) { + socket.end() + } else { + retryTimer.refresh() + } + }, KEEP_ALIVE_RETRY_TIMEOUT) + + // Once the client connects, we start a timer + const keepAlive: ReturnType = setTimeout(() => { + client.send(serializeAction({ action: 'keepAlive' })) + isRetry = true + retryTimer.refresh() + }, KEEP_ALIVE_INITIAL_TIMEOUT) + socket.on('data', (data) => { + // Data received, reset keepAlive + isRetry = false + retryCount = 0 + keepAlive.refresh() + const messages = data.toString().split('\n') for (const msg of messages) { if (!msg) return try { - const message: ActionClientToServer = stringToJson(msg) + const message: ActionClientToServer | ActionUtility = stringToJson(msg) const { action, ...actionArgs } = message + console.log(`Received action ${action} from ${client.id}`) // This only works for now, once we add more arguments // we'll need to refactor this @@ -66,6 +110,7 @@ const server = net.createServer((socket) => { }) socket.on('end', () => { + console.log(`Client disconnected ${client.id}`) actionHandlers.leaveLobby?.(client) }) From 6f5efaffc24a4b2c7de1750507fba870d6e8086e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Fri, 15 Mar 2024 19:23:18 -0700 Subject: [PATCH 0010/1128] Moved networking logic to a new thread (#8) * Moved networking logic to another thread * Fixed formatting * Fixed Steamodded headers * Added name to mod authors in Core.lua * Fixed Steamodded footers --- Multiplayer/Action_Handlers.lua | 115 ++++++ Multiplayer/Core.lua | 55 +-- Multiplayer/Lobby.lua | 623 ++++++++++++++++---------------- Multiplayer/Main_Menu.lua | 87 +++-- Multiplayer/Networking.lua | 151 +++----- Multiplayer/Utils.lua | 55 ++- 6 files changed, 588 insertions(+), 498 deletions(-) create mode 100644 Multiplayer/Action_Handlers.lua diff --git a/Multiplayer/Action_Handlers.lua b/Multiplayer/Action_Handlers.lua new file mode 100644 index 00000000..88af2993 --- /dev/null +++ b/Multiplayer/Action_Handlers.lua @@ -0,0 +1,115 @@ +--- STEAMODDED HEADER +--- STEAMODDED SECONDARY FILE + +---------------------------------------------- +------------MOD ACTION_HANDLERS-------------------- +local Lobby = require "Lobby" + +ActionHandlers = {} +ActionHandlers.Client = {} + +function ActionHandlers.Client.send(msg) + love.thread.getChannel("uiToNetwork"):push(msg) +end + +-- Server to Client +function ActionHandlers.set_username(username) + Lobby.username = username or 'Guest' + if Lobby.connected then + ActionHandlers.Client.send('action:username,username:' .. Lobby.username) + end +end + +local function action_connected() + sendDebugMessage("Client connected to multiplayer server") + Lobby.connected = true + Lobby.update_connection_status() + ActionHandlers.Client.send('action:username,username:' .. Lobby.username) +end + +local function action_joinedLobby(code) + sendDebugMessage("Joining lobby " .. code) + Lobby.code = code + Lobby.update_connection_status() + ActionHandlers.lobby_info() +end + +local function action_lobbyInfo(host, guest) + Lobby.players = {} + table.insert(Lobby.players, { username = host }) + if guest ~= nil then + table.insert(Lobby.players, { username = guest }) + end + Lobby.update_player_usernames() +end + +local function action_error(message) + sendDebugMessage(message) + + Utils.overlay_message(message) +end + +local function action_keep_alive() + ActionHandlers.Client.send('action:keepAliveAck') +end + +-- Client to Server +function ActionHandlers.create_lobby() + ActionHandlers.Client.send('action:createLobby') +end + +function ActionHandlers.join_lobby(code) + ActionHandlers.Client.send('action:joinLobby,code:' .. code) +end + +function ActionHandlers.lobby_info() + ActionHandlers.Client.send('action:lobbyInfo') +end + +function ActionHandlers.leave_lobby() + ActionHandlers.Client.send('action:leaveLobby') +end + +-- Utils +function ActionHandlers.connect() + ActionHandlers.Client.send('connect') +end + +local function string_to_table(str) + local tbl = {} + for key, value in string.gmatch(str, '([^,]+):([^,]+)') do + tbl[key] = value + end + return tbl +end + +local game_update_ref = Game.update +function Game.update(arg_298_0, arg_298_1) + game_update_ref(arg_298_0, arg_298_1) + + repeat + local msg = love.thread.getChannel("networkToUi"):pop() + if msg then + local parsedAction = string_to_table(msg) + + sendDebugMessage('Client got ' .. parsedAction.action .. ' message') + + if parsedAction.action == 'connected' then + action_connected() + elseif parsedAction.action == 'joinedLobby' then + action_joinedLobby(parsedAction.code) + elseif parsedAction.action == 'lobbyInfo' then + action_lobbyInfo(parsedAction.host, parsedAction.guest) + elseif parsedAction.action == 'error' then + action_error(parsedAction.message) + elseif parsedAction.action == 'keepAlive' then + action_keep_alive() + end + end + until not msg +end + +return ActionHandlers + +---------------------------------------------- +------------MOD ACTION_HANDLERS END---------------- diff --git a/Multiplayer/Core.lua b/Multiplayer/Core.lua index d929dd81..5647d4a3 100644 --- a/Multiplayer/Core.lua +++ b/Multiplayer/Core.lua @@ -1,7 +1,7 @@ --- STEAMODDED HEADER --- MOD_NAME: Multiplayer --- MOD_ID: VirtualizedMultiplayer ---- MOD_AUTHOR: [virtualized] +--- MOD_AUTHOR: [virtualized, TGMM] --- MOD_DESCRIPTION: Allows players to compete with their friends! Contact @virtualized on discord for mod assistance. ---------------------------------------------- @@ -9,33 +9,40 @@ -- Credit to Nyoxide for this custom loader local moduleCache = {} +local relativeModPath = "Mods/Multiplayer/" local function customLoader(moduleName) - local filename = moduleName:gsub("%.", "/") .. ".lua" - if moduleCache[filename] then - return moduleCache[filename] - end - - local filePath = "Mods/Multiplayer/" .. filename - local fileContent = love.filesystem.read(filePath) - if fileContent then - local moduleFunc = assert(load(fileContent, "@"..filePath)) - moduleCache[filename] = moduleFunc - return moduleFunc - end - - return "\nNo module found: " .. moduleName + local filename = moduleName:gsub("%.", "/") .. ".lua" + if moduleCache[filename] then + return moduleCache[filename] + end + + local filePath = relativeModPath .. filename + local fileContent = love.filesystem.read(filePath) + if fileContent then + local moduleFunc = assert(load(fileContent, "@" .. filePath)) + moduleCache[filename] = moduleFunc + return moduleFunc + end + + return "\nNo module found: " .. moduleName end function SMODS.INIT.VirtualizedMultiplayer() - table.insert(package.loaders, 1, customLoader) - require "Blind" - require "Deck" - require "Main_Menu" - require "Utils".get_username() - require "Networking".authorize() - require "Mod_Description".load_description_gui() - require "Game_UI" + table.insert(package.loaders, 1, customLoader) + require "Blind" + require "Deck" + require "Main_Menu" + require "Utils".get_username() + require "Action_Handlers" + require "Mod_Description".load_description_gui() + require "Game_UI" + + CONFIG = require "Config" + NETWORKING_THREAD = love.thread.newThread(relativeModPath .. "Networking.lua") + NETWORKING_THREAD:start(CONFIG.URL, CONFIG.PORT) + + ActionHandlers.connect() end ---------------------------------------------- -------------MOD CORE END---------------------- \ No newline at end of file +------------MOD CORE END---------------------- diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 41f67ff4..37e4ad26 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -5,24 +5,24 @@ ------------MOD LOBBY------------------------- Lobby = { - connected = false, - temp_code = '', - code = nil, - type = "", - config = {}, - username = "Guest", - enemy = { - username = "dude_crusher69", - score = 0, - hands = 4 - }, - players = {} + connected = false, + temp_code = '', + code = nil, + type = "", + config = {}, + username = "Guest", + enemy = { + username = "dude_crusher69", + score = 0, + hands = 4 + }, + players = {} } Connection_Status_UI = nil local function get_connection_status_ui() - return UIBox({ + return UIBox({ definition = { n = G.UIT.ROOT, config = { @@ -34,7 +34,8 @@ local function get_connection_status_ui() n = G.UIT.T, config = { scale = 0.3, - text = (Lobby.code and 'In Lobby') or (Lobby.connected and 'Connected to Service') or 'WARN: Cannot Find Multiplayer Service', + text = (Lobby.code and 'In Lobby') or (Lobby.connected and 'Connected to Service') or + 'WARN: Cannot Find Multiplayer Service', colour = G.C.UI.TEXT_LIGHT } } @@ -53,10 +54,10 @@ local function get_connection_status_ui() end function Lobby.update_connection_status() - if Connection_Status_UI then - Connection_Status_UI:remove() - end - Connection_Status_UI = get_connection_status_ui() + if Connection_Status_UI then + Connection_Status_UI:remove() + end + Connection_Status_UI = get_connection_status_ui() end local gameMainMenuRef = Game.main_menu @@ -66,12 +67,12 @@ function Game.main_menu(arg_280_0, arg_280_1) end function G.FUNCS.copy_to_clipboard(arg_736_0) - Utils.copy_to_clipboard(Lobby.code) + Utils.copy_to_clipboard(Lobby.code) end function G.FUNCS.reconnect(arg_736_0) - Networking.authorize() - G.FUNCS:exit_overlay_menu() + ActionHandlers.connect() + G.FUNCS:exit_overlay_menu() end function create_UIBox_view_code() @@ -86,115 +87,115 @@ function create_UIBox_view_code() align = "cm" }, nodes = { - { - n = G.UIT.R, - config = { - padding = 0.5, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - text = Lobby.code, - shadow = true, - scale = var_495_0 * 0.6, - colour = G.C.UI.TEXT_LIGHT - } - } - } - }, - { - n = G.UIT.R, - config = { - padding = 0, - align = "cm" - }, - nodes = { - UIBox_button({ - label = {"Copy to Clipboard"}, - colour = G.C.BLUE, - button = "copy_to_clipboard", - minw = 5, - }) - } - } + { + n = G.UIT.R, + config = { + padding = 0.5, + align = "cm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = Lobby.code, + shadow = true, + scale = var_495_0 * 0.6, + colour = G.C.UI.TEXT_LIGHT + } + } + } + }, + { + n = G.UIT.R, + config = { + padding = 0, + align = "cm" + }, + nodes = { + UIBox_button({ + label = { "Copy to Clipboard" }, + colour = G.C.BLUE, + button = "copy_to_clipboard", + minw = 5, + }) + } + } } - } - } + } + } })) end function G.FUNCS.lobby_setup_run(arg_736_0) G.FUNCS.start_run(arg_736_0, { - stake = 1, - challenge = { - name = 'Multiplayer Deck', - id = 'c_multiplayer_1', - rules = { - custom = { - }, - modifiers = { - } - }, - jokers = { - }, - consumeables = { - }, - vouchers = { - }, - deck = { - type = 'Challenge Deck' - }, - restrictions = { - banned_cards = { - {id = 'j_diet_cola'}, -- Intention to disable skipping - {id = 'j_mr_bones'}, - {id = 'v_hieroglyph'}, - {id = 'v_petroglyph'}, - }, - banned_tags = { - }, - banned_other = { - } - } - } - }) + stake = 1, + challenge = { + name = 'Multiplayer Deck', + id = 'c_multiplayer_1', + rules = { + custom = { + }, + modifiers = { + } + }, + jokers = { + }, + consumeables = { + }, + vouchers = { + }, + deck = { + type = 'Challenge Deck' + }, + restrictions = { + banned_cards = { + { id = 'j_diet_cola' }, -- Intention to disable skipping + { id = 'j_mr_bones' }, + { id = 'v_hieroglyph' }, + { id = 'v_petroglyph' }, + }, + banned_tags = { + }, + banned_other = { + } + } + } + }) end function G.FUNCS.lobby_options(arg_736_0) G.FUNCS.overlay_menu({ definition = create_UIBox_generic_options({ - contents = { - { - n = G.UIT.R, - config = { - padding = 0, - align = "cm" - }, - nodes = { - { - n = G.UIT.R, - config = { - padding = 0.5, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - text = 'Not Implemented Yet', - shadow = true, - scale = 0.6, - colour = G.C.UI.TEXT_LIGHT - } - } - } - } - } - } - } - }) + contents = { + { + n = G.UIT.R, + config = { + padding = 0, + align = "cm" + }, + nodes = { + { + n = G.UIT.R, + config = { + padding = 0.5, + align = "cm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = 'Not Implemented Yet', + shadow = true, + scale = 0.6, + colour = G.C.UI.TEXT_LIGHT + } + } + } + } + } + } + } + }) }) end @@ -205,221 +206,223 @@ function G.FUNCS.view_code(arg_736_0) end function G.FUNCS.lobby_leave(arg_736_0) - Lobby.code = nil - Networking.leave_lobby() - Lobby.update_connection_status() + Lobby.code = nil + ActionHandlers.leave_lobby() + Lobby.update_connection_status() end local function create_UIBox_lobby_menu() - local text_scale = 0.45 - - local t = { - n = G.UIT.ROOT, - config = { - align = "cm", - colour = G.C.CLEAR - }, - nodes = { - { - n = G.UIT.C, - config = { - align = "bm" - }, - nodes = { - { - n = G.UIT.R, - config = { - align = "cm", - padding = 0.2, - r = 0.1, - emboss = 0.1, - colour = G.C.L_BLACK, - mid = true - }, - nodes = { - UIBox_button({ - id = 'lobby_menu_start', - button = "lobby_setup_run", - colour = G.C.BLUE, - minw = 3.65, - minh = 1.55, - label = {'START'}, - scale = text_scale*2, - col = true - }), - { - n = G.UIT.C, - config = { - align = "cm" - }, - nodes = { - UIBox_button{ - button = 'lobby_options', - colour = G.C.ORANGE, - minw = 3.15, - minh = 1.35, - label = {'LOBBY OPTIONS'}, - scale = text_scale * 1.2, - col = true - }, - { - n = G.UIT.C, - config = { - align = "cm", - minw = 0.2 - }, - nodes = {} - }, - { - n = G.UIT.C, - config = { - align = "tm", - minw = 2.65 - }, - nodes = { - { - n = G.UIT.R, - config = { - padding = 0.2, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - text = 'Connected Players:', - shadow = true, - scale = text_scale * 0.8, - colour = G.C.UI.TEXT_LIGHT - } - } - }, - }, - Lobby.players[1] and { - n = G.UIT.R, - config = { - padding = 0, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - text = Lobby.players[1].username, - shadow = true, - scale = text_scale * 0.8, - colour = G.C.UI.TEXT_LIGHT - } - } - } - } or nil, - Lobby.players[2] and { - n = G.UIT.R, - config = { - padding = 0, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - text = Lobby.players[2].username, - shadow = true, - scale = text_scale * 0.8, - colour = G.C.UI.TEXT_LIGHT - } - } - } - } or nil - } - }, - { - n = G.UIT.C, - config = { - align = "cm", - minw = 0.2 - }, - nodes = {} - }, - UIBox_button{ - button = 'view_code', - colour = G.C.PALE_GREEN, - minw = 3.15, - minh = 1.35, - label = {'VIEW CODE'}, - scale = text_scale * 1.2, - col = true - }, - } - }, - UIBox_button{ - id = 'lobby_menu_leave', - button = "lobby_leave", - colour = G.C.RED, - minw = 3.65, - minh = 1.55, - label = {'LEAVE'}, - scale = text_scale*1.5, - col = true - }, - } - }, - }}, - }} - return t + local text_scale = 0.45 + + local t = { + n = G.UIT.ROOT, + config = { + align = "cm", + colour = G.C.CLEAR + }, + nodes = { + { + n = G.UIT.C, + config = { + align = "bm" + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm", + padding = 0.2, + r = 0.1, + emboss = 0.1, + colour = G.C.L_BLACK, + mid = true + }, + nodes = { + UIBox_button({ + id = 'lobby_menu_start', + button = "lobby_setup_run", + colour = G.C.BLUE, + minw = 3.65, + minh = 1.55, + label = { 'START' }, + scale = text_scale * 2, + col = true + }), + { + n = G.UIT.C, + config = { + align = "cm" + }, + nodes = { + UIBox_button { + button = 'lobby_options', + colour = G.C.ORANGE, + minw = 3.15, + minh = 1.35, + label = { 'LOBBY OPTIONS' }, + scale = text_scale * 1.2, + col = true + }, + { + n = G.UIT.C, + config = { + align = "cm", + minw = 0.2 + }, + nodes = {} + }, + { + n = G.UIT.C, + config = { + align = "tm", + minw = 2.65 + }, + nodes = { + { + n = G.UIT.R, + config = { + padding = 0.2, + align = "cm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = 'Connected Players:', + shadow = true, + scale = text_scale * 0.8, + colour = G.C.UI.TEXT_LIGHT + } + } + }, + }, + Lobby.players[1] and { + n = G.UIT.R, + config = { + padding = 0, + align = "cm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = Lobby.players[1].username, + shadow = true, + scale = text_scale * 0.8, + colour = G.C.UI.TEXT_LIGHT + } + } + } + } or nil, + Lobby.players[2] and { + n = G.UIT.R, + config = { + padding = 0, + align = "cm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = Lobby.players[2].username, + shadow = true, + scale = text_scale * 0.8, + colour = G.C.UI.TEXT_LIGHT + } + } + } + } or nil + } + }, + { + n = G.UIT.C, + config = { + align = "cm", + minw = 0.2 + }, + nodes = {} + }, + UIBox_button { + button = 'view_code', + colour = G.C.PALE_GREEN, + minw = 3.15, + minh = 1.35, + label = { 'VIEW CODE' }, + scale = text_scale * 1.2, + col = true + }, + } + }, + UIBox_button { + id = 'lobby_menu_leave', + button = "lobby_leave", + colour = G.C.RED, + minw = 3.65, + minh = 1.55, + label = { 'LEAVE' }, + scale = text_scale * 1.5, + col = true + }, + } + }, + } + }, + } + } + return t end local function get_lobby_main_menu_UI() - return UIBox({ - definition = create_UIBox_lobby_menu(), - config = { - align="bmi", - offset = { - x = 0, - y = 10 - }, - major = G.ROOM_ATTACH, - bond = 'Weak' - } - }) + return UIBox({ + definition = create_UIBox_lobby_menu(), + config = { + align = "bmi", + offset = { + x = 0, + y = 10 + }, + major = G.ROOM_ATTACH, + bond = 'Weak' + } + }) end function display_lobby_main_menu_UI() - G.MAIN_MENU_UI = get_lobby_main_menu_UI() - G.MAIN_MENU_UI.alignment.offset.y = 0 - G.MAIN_MENU_UI:align_to_major() + G.MAIN_MENU_UI = get_lobby_main_menu_UI() + G.MAIN_MENU_UI.alignment.offset.y = 0 + G.MAIN_MENU_UI:align_to_major() - G.CONTROLLER:snap_to{node = G.MAIN_MENU_UI:get_UIE_by_ID('lobby_menu_start')} + G.CONTROLLER:snap_to { node = G.MAIN_MENU_UI:get_UIE_by_ID('lobby_menu_start') } end function Lobby.update_player_usernames() - if Lobby.code then - G.MAIN_MENU_UI:remove() - display_lobby_main_menu_UI() - end + if Lobby.code then + G.MAIN_MENU_UI:remove() + display_lobby_main_menu_UI() + end end local setMainMenuUIRef = set_main_menu_UI function set_main_menu_UI() - if Lobby.code then - display_lobby_main_menu_UI() - else - setMainMenuUIRef() - end + if Lobby.code then + display_lobby_main_menu_UI() + else + setMainMenuUIRef() + end end local in_lobby = false local gameUpdateRef = Game.update function Game:update(arg_298_1) - if (Lobby.code and not in_lobby) or (not Lobby.code and in_lobby) then - in_lobby = not in_lobby - G.F_NO_SAVING = in_lobby - self.FUNCS.go_to_menu() - end - gameUpdateRef(self, arg_298_1) + if (Lobby.code and not in_lobby) or (not Lobby.code and in_lobby) then + in_lobby = not in_lobby + G.F_NO_SAVING = in_lobby + self.FUNCS.go_to_menu() + end + gameUpdateRef(self, arg_298_1) end return Lobby ---------------------------------------------- -------------MOD LOBBY END--------------------- \ No newline at end of file +------------MOD LOBBY END--------------------- diff --git a/Multiplayer/Main_Menu.lua b/Multiplayer/Main_Menu.lua index fcd68101..6ef70008 100644 --- a/Multiplayer/Main_Menu.lua +++ b/Multiplayer/Main_Menu.lua @@ -6,7 +6,7 @@ local Utils = require "Utils" local Lobby = require "Lobby" -local Networking = require "Networking" +local ActionHandlers = require "Action_Handlers" MULTIPLAYER_VERSION = "0.1.0-MULTIPLAYER" @@ -79,16 +79,18 @@ function create_UIBox_create_lobby_button() { n = G.UIT.R, config = { - align = "tm", - padding = 0.05, - minw = 4, - minh = 1.5 + align = "tm", + padding = 0.05, + minw = 4, + minh = 1.5 }, nodes = { { n = G.UIT.T, config = { - text = Utils.wrapText("Both players start with 4 lives, every boss round is a competition between players where the player with the lower score loses a life.", 50), + text = Utils.wrapText( + "Both players start with 4 lives, every boss round is a competition between players where the player with the lower score loses a life.", + 50), shadow = true, scale = var_495_0 * 0.6, colour = G.C.UI.TEXT_LIGHT @@ -96,10 +98,15 @@ function create_UIBox_create_lobby_button() } } }, - create_toggle({label = "Lose lives on round loss", ref_table = Lobby.config, ref_value = 'death_on_round_loss'}), - create_toggle({label = "Different seeds", ref_table = Lobby.config, ref_value = 'different_seeds'}), + create_toggle({ + label = "Lose lives on round loss", + ref_table = Lobby.config, + ref_value = + 'death_on_round_loss' + }), + create_toggle({ label = "Different seeds", ref_table = Lobby.config, ref_value = 'different_seeds' }), UIBox_button({ - label = {"Start Lobby"}, + label = { "Start Lobby" }, colour = G.C.RED, button = "start_lobby", minw = 5, @@ -135,7 +142,9 @@ function create_UIBox_create_lobby_button() { n = G.UIT.T, config = { - text = Utils.wrapText("Both players play a set amount of antes simultaneously, then they play an ante where every round the player with the higher scorer wins, player with the most round wins in the final ante is the victor.", 50), + text = Utils.wrapText( + "Both players play a set amount of antes simultaneously, then they play an ante where every round the player with the higher scorer wins, player with the most round wins in the final ante is the victor.", + 50), shadow = true, scale = var_495_0 * 0.6, colour = G.C.UI.TEXT_LIGHT @@ -144,7 +153,7 @@ function create_UIBox_create_lobby_button() } }, UIBox_button({ - label = {"Coming Soon!"}, + label = { "Coming Soon!" }, colour = G.C.RED, minw = 5, }) @@ -170,16 +179,18 @@ function create_UIBox_create_lobby_button() { n = G.UIT.R, config = { - align = "tm", - padding = 0.05, - minw = 4, - minh = 1 + align = "tm", + padding = 0.05, + minw = 4, + minh = 1 }, nodes = { { n = G.UIT.T, config = { - text = Utils.wrapText("Both players play the first ante, then must keep beating the opponents previous score or lose.", 50), + text = Utils.wrapText( + "Both players play the first ante, then must keep beating the opponents previous score or lose.", + 50), shadow = true, scale = var_495_0 * 0.6, colour = G.C.UI.TEXT_LIGHT @@ -188,7 +199,7 @@ function create_UIBox_create_lobby_button() } }, UIBox_button({ - label = {"Coming Soon!"}, + label = { "Coming Soon!" }, colour = G.C.RED, minw = 5, }) @@ -223,7 +234,8 @@ function create_UIBox_create_lobby_button() { n = G.UIT.T, config = { - text = Utils.wrapText("Draft, except there are up to 8 players and every player only has 1 life.", 50), + text = Utils.wrapText( + "Draft, except there are up to 8 players and every player only has 1 life.", 50), shadow = true, scale = var_495_0 * 0.6, colour = G.C.UI.TEXT_LIGHT @@ -232,7 +244,7 @@ function create_UIBox_create_lobby_button() } }, UIBox_button({ - label = {"Coming Soon!"}, + label = { "Coming Soon!" }, colour = G.C.RED, minw = 5, }) @@ -246,7 +258,6 @@ function create_UIBox_create_lobby_button() } } })) - end function create_UIBox_join_lobby_button() @@ -256,22 +267,22 @@ function create_UIBox_join_lobby_button() { n = G.UIT.R, config = { - padding = 0, - align = "cm", + padding = 0, + align = "cm", }, nodes = { { n = G.UIT.R, config = { - padding = 0.5, - align = "cm" + padding = 0.5, + align = "cm" }, nodes = { { n = G.UIT.T, config = { scale = 0.6, - shadow = true, + shadow = true, text = 'Lobby Code:', colour = G.C.UI.TEXT_LIGHT } @@ -281,8 +292,8 @@ function create_UIBox_join_lobby_button() { n = G.UIT.R, config = { - padding = 0.5, - align = "cm" + padding = 0.5, + align = "cm" }, nodes = { create_text_input({ @@ -294,15 +305,15 @@ function create_UIBox_join_lobby_button() ref_value = 'temp_code', extended_corpus = false, keyboard_offset = 1, - minw = 5, + minw = 5, callback = function(val) - Networking.join_lobby(Lobby.temp_code) + ActionHandlers.join_lobby(Lobby.temp_code) end, }) } }, UIBox_button({ - label = {"Paste From Clipboard"}, + label = { "Paste From Clipboard" }, colour = G.C.RED, button = "join_from_clipboard", minw = 5, @@ -317,25 +328,25 @@ function override_main_menu_play_button() return (create_UIBox_generic_options({ contents = { UIBox_button({ - label = {"Singleplayer"}, + label = { "Singleplayer" }, colour = G.C.BLUE, button = "setup_run", minw = 5, }), Lobby.connected and UIBox_button({ - label = {"Create Lobby"}, + label = { "Create Lobby" }, colour = G.C.GREEN, button = "create_lobby", minw = 5, }) or nil, Lobby.connected and UIBox_button({ - label = {"Join Lobby"}, + label = { "Join Lobby" }, colour = G.C.RED, button = "join_lobby", minw = 5, }) or nil, not Lobby.connected and UIBox_button({ - label = {"Reconnect"}, + label = { "Reconnect" }, colour = G.C.RED, button = "reconnect", minw = 5, @@ -370,13 +381,13 @@ end function G.FUNCS.join_from_clipboard(arg_736_0) Lobby.temp_code = Utils.get_from_clipboard() - Networking.join_lobby(Lobby.temp_code) + ActionHandlers.join_lobby(Lobby.temp_code) end function G.FUNCS.start_lobby(arg_736_0) G.SETTINGS.paused = false - Networking.create_lobby() + ActionHandlers.create_lobby() end -- Modify play button to take you to mode select first @@ -384,8 +395,8 @@ local create_UIBox_main_menu_buttonsRef = create_UIBox_main_menu_buttons function create_UIBox_main_menu_buttons() local menu = create_UIBox_main_menu_buttonsRef() menu.nodes[1].nodes[1].nodes[1].nodes[1].config.button = "play_options" - return(menu) + return (menu) end ---------------------------------------------- -------------MOD MAIN MENU END----------------- \ No newline at end of file +------------MOD MAIN MENU END----------------- diff --git a/Multiplayer/Networking.lua b/Multiplayer/Networking.lua index 6cf68d5d..bddc868b 100644 --- a/Multiplayer/Networking.lua +++ b/Multiplayer/Networking.lua @@ -3,120 +3,75 @@ ---------------------------------------------- ------------MOD NETWORKING-------------------- -local Lobby = require "Lobby" -local Config = require "Config" -local socket = require "socket" -Networking = {} - -function string_to_table(str) - local tbl = {} - for key, value in string.gmatch(str, '([^,]+):([^,]+)') do - tbl[key] = value - end - return tbl -end - -function Networking.set_username(username) - Lobby.username = username or 'Guest' - if Lobby.connected then - Networking.Client:send('action:username,username:'..Lobby.username) - end -end - -local function action_connected() - sendDebugMessage("Client connected to multiplayer server") - Lobby.connected = true - Lobby.update_connection_status() - Networking.Client:send('action:username,username:'..Lobby.username) -end - -local function action_joinedLobby(code) - sendDebugMessage("Joining lobby " .. code) - Lobby.code = code - Lobby.update_connection_status() - Networking.lobby_info() -end +-- Code for networking stuff that runs in a separate thread -local function action_lobbyInfo(host, guest) - Lobby.players = {} - table.insert(Lobby.players, { username = host }) - if guest ~= nil then - table.insert(Lobby.players, { username = guest }) - end - Lobby.update_player_usernames() -end +-- Since threads run on a separate lua environment, we need to require +-- the necessary modules again +local CONFIG_URL, CONFIG_PORT = ... -local function action_error(message) - sendDebugMessage(message) +require 'love.filesystem' +SOCKET = require 'socket' - Utils.overlay_message(message) +-- Defining this again, for debugging this thread +local function initializeThreadDebugSocketConnection() + CLIENT = SOCKET.connect("localhost", 12346) + if not CLIENT then + print("Failed to connect to the debug server") + end end -local function action_keep_alive() - Networking.Client:send('action:keepAliveAck') +function SEND_THREAD_DEBUG_MESSAGE(message) + if CLIENT then + CLIENT:send(message .. "\n") + end end -local game_update_ref = Game.update -function Game.update(arg_298_0, arg_298_1) - if Networking.Client then - repeat - local data, error, partial = Networking.Client:receive() - if data then - local t = string_to_table(data) - - sendDebugMessage('Client got ' .. t.action .. ' message') - - if t.action == 'connected' then - action_connected() - elseif t.action == 'joinedLobby' then - action_joinedLobby(t.code) - elseif t.action == 'lobbyInfo' then - action_lobbyInfo(t.host, t.guest) - elseif t.action == 'error' then - action_error(t.message) - elseif t.action == 'keepAlive' then - action_keep_alive() - end - end - until not data - end - - game_update_ref(arg_298_0, arg_298_1) -end +initializeThreadDebugSocketConnection() -function Networking.authorize() - sendDebugMessage(string.format("Attempting to connect to multiplayer server... URL: %s, PORT: %d", Config.URL, Config.PORT)) +Networking = {} - Networking.Client = socket.tcp() +function Networking.connect() + SEND_THREAD_DEBUG_MESSAGE(string.format("Attempting to connect to multiplayer server... URL: %s, PORT: %d", CONFIG_URL, + CONFIG_PORT)) - Networking.Client:setoption('tcp-nodelay', true) - local connectionResult, errorMessage = Networking.Client:connect(Config.URL, Config.PORT) -- Not sure if I want to make these values public yet + Networking.Client = SOCKET.tcp() - if connectionResult ~= 1 then - sendDebugMessage(string.format("%s", errorMessage)) - end + Networking.Client:setoption('tcp-nodelay', true) + local connectionResult, errorMessage = Networking.Client:connect(CONFIG_URL, CONFIG_PORT) -- Not sure if I want to make these values public yet - Networking.Client:settimeout(0) -end + if connectionResult ~= 1 then + SEND_THREAD_DEBUG_MESSAGE(string.format("%s", errorMessage)) + end -function Networking.create_lobby() - Networking.Client:send('action:createLobby') + Networking.Client:settimeout(0) end -function Networking.join_lobby(code) - Networking.Client:send('action:joinLobby,code:' .. code) +-- TODO: Put this in a coroutine +while true do + -- Check for messages from the main thread + repeat + local msg = love.thread.getChannel("uiToNetwork"):pop() + if msg then + if msg:find("^action") ~= nil then + Networking.Client:send(msg .. "\n") + elseif msg == "connect" then + Networking.connect() + end + end + until not msg + + -- Do networking stuff + if Networking.Client then + repeat + local data, error, partial = Networking.Client:receive() + if data then + -- For now, we just send the string as is to the main thread + love.thread.getChannel("networkToUi"):push(data) + end + until not data + end end -function Networking.lobby_info() - Networking.Client:send('action:lobbyInfo') -end - -function Networking.leave_lobby() - Networking.Client:send('action:leaveLobby') -end - -return Networking - ---------------------------------------------- -------------MOD NETWORKING END---------------- \ No newline at end of file +------------MOD NETWORKING END---------------- diff --git a/Multiplayer/Utils.lua b/Multiplayer/Utils.lua index ba58d692..8210b9e2 100644 --- a/Multiplayer/Utils.lua +++ b/Multiplayer/Utils.lua @@ -2,9 +2,9 @@ --- STEAMODDED SECONDARY FILE ---------------------------------------------- -------------MOD DEBUG------------------------- +------------MOD UTILS------------------------- -local Networking = require "Networking" +local ActionHandlers = require "Action_Handlers" Utils = {} @@ -27,21 +27,21 @@ function Utils.serialize_table(val, name, skipnewlines, depth) if name then tmp = tmp .. name .. " = " end if type(val) == "table" then - tmp = tmp .. "{" .. (not skipnewlines and "\n" or "") + tmp = tmp .. "{" .. (not skipnewlines and "\n" or "") - for k, v in pairs(val) do - tmp = tmp .. Utils.serialize_table(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and "\n" or "") - end + for k, v in pairs(val) do + tmp = tmp .. Utils.serialize_table(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and "\n" or "") + end - tmp = tmp .. string.rep(" ", depth) .. "}" + tmp = tmp .. string.rep(" ", depth) .. "}" elseif type(val) == "number" then - tmp = tmp .. tostring(val) + tmp = tmp .. tostring(val) elseif type(val) == "string" then - tmp = tmp .. string.format("%q", val) + tmp = tmp .. string.format("%q", val) elseif type(val) == "boolean" then - tmp = tmp .. (val and "true" or "false") + tmp = tmp .. (val and "true" or "false") else - tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\"" + tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\"" end return tmp @@ -67,7 +67,7 @@ end local usernameFilePath = "Mods/Multiplayer/Saved/username.txt" function Utils.save_username(text) - Networking.set_username(text) + ActionHandlers.set_username(text) love.filesystem.write(usernameFilePath, text) end @@ -79,32 +79,31 @@ end function Utils.string_split(inputstr, sep) if sep == nil then - sep = "%s" + sep = "%s" end - local t={} - for str in string.gmatch(inputstr, "([^"..sep.."]+)") do - table.insert(t, str) + local t = {} + for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do + table.insert(t, str) end return t end function Utils.copy_to_clipboard(text) if G.F_LOCAL_CLIPBOARD then - G.CLIPBOARD = text - else - love.system.setClipboardText(text) - end + G.CLIPBOARD = text + else + love.system.setClipboardText(text) + end end function Utils.get_from_clipboard() if G.F_LOCAL_CLIPBOARD then - return G.F_LOCAL_CLIPBOARD - else - return love.system.getClipboardText() - end + return G.F_LOCAL_CLIPBOARD + else + return love.system.getClipboardText() + end end - function Utils.overlay_message(message) G.SETTINGS.paused = true @@ -114,8 +113,8 @@ function Utils.overlay_message(message) { n = G.UIT.R, config = { - padding = 0.5, - align = "cm", + padding = 0.5, + align = "cm", }, nodes = { { @@ -137,4 +136,4 @@ end return Utils ---------------------------------------------- -------------MOD DEBUG END--------------------- \ No newline at end of file +------------MOD DEBUG END--------------------- From 82c25293c01ebb190875679f9f4a9a2105771df8 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 16 Mar 2024 03:14:40 +0000 Subject: [PATCH 0011/1128] Created Disableable_Button and used with lobby start and options --- Multiplayer/Action_Handlers.lua | 11 +- Multiplayer/Deck.lua | 2 +- Multiplayer/Disableable_Button.lua | 115 ++++++++++ Multiplayer/Lobby.lua | 342 +++++++++++++++-------------- Multiplayer/Networking.lua | 2 +- Server/README.md | 3 +- 6 files changed, 298 insertions(+), 177 deletions(-) create mode 100644 Multiplayer/Disableable_Button.lua diff --git a/Multiplayer/Action_Handlers.lua b/Multiplayer/Action_Handlers.lua index 88af2993..c9f58270 100644 --- a/Multiplayer/Action_Handlers.lua +++ b/Multiplayer/Action_Handlers.lua @@ -30,15 +30,16 @@ end local function action_joinedLobby(code) sendDebugMessage("Joining lobby " .. code) Lobby.code = code - Lobby.update_connection_status() ActionHandlers.lobby_info() + Lobby.update_connection_status() end -local function action_lobbyInfo(host, guest) +local function action_lobbyInfo(host, guest, is_host) Lobby.players = {} - table.insert(Lobby.players, { username = host }) + Lobby.is_host = is_host == 'true' + Lobby.host = { username = host } if guest ~= nil then - table.insert(Lobby.players, { username = guest }) + Lobby.guest = { username = guest } end Lobby.update_player_usernames() end @@ -99,7 +100,7 @@ function Game.update(arg_298_0, arg_298_1) elseif parsedAction.action == 'joinedLobby' then action_joinedLobby(parsedAction.code) elseif parsedAction.action == 'lobbyInfo' then - action_lobbyInfo(parsedAction.host, parsedAction.guest) + action_lobbyInfo(parsedAction.host, parsedAction.guest, parsedAction.isHost) elseif parsedAction.action == 'error' then action_error(parsedAction.message) elseif parsedAction.action == 'keepAlive' then diff --git a/Multiplayer/Deck.lua b/Multiplayer/Deck.lua index 3e5cdf91..48d372fd 100644 --- a/Multiplayer/Deck.lua +++ b/Multiplayer/Deck.lua @@ -24,7 +24,7 @@ local c_multiplayer_1 = { }, restrictions = { banned_cards = { - {id = 'j_diet_cola'}, -- Intention to disable skipping + {id = 'j_diet_cola'}, {id = 'j_mr_bones'}, {id = 'v_hieroglyph'}, {id = 'v_petroglyph'}, diff --git a/Multiplayer/Disableable_Button.lua b/Multiplayer/Disableable_Button.lua new file mode 100644 index 00000000..db64ea06 --- /dev/null +++ b/Multiplayer/Disableable_Button.lua @@ -0,0 +1,115 @@ +--- STEAMODDED HEADER +--- STEAMODDED SECONDARY FILE + +---------------------------------------------- +------------MOD DISABLEABLE BUTTON------------ + +function Disableable_Button(args) + local enabled_table = args.enabled_ref_table or {} + local enabled = enabled_table[args.disable_ref_value] + args.colour = args.colour or G.C.RED + args.text_colour = args.text_colour or G.C.UI.TEXT_LIGHT + args.label = not enabled and args.disabled_text or args.label + + local button_component = UIBox_button(args) + button_component.nodes[1].config.button = enabled and args.button or nil + button_component.nodes[1].config.hover = enabled + button_component.nodes[1].config.shadow = enabled + button_component.nodes[1].config.colour = enabled and args.colour or G.C.UI.BACKGROUND_INACTIVE + button_component.nodes[1].nodes[1].nodes[1].colour = enabled and args.text_colour or G.C.UI.TEXT_INACTIVE + button_component.nodes[1].nodes[1].nodes[1].shadow = enabled + return button_component +end + +return Disableable_Button + +--[[ UIBox_button returns this +{ + n = G.UIT.C | G.UIT.R, + config = { + align = 'cm' + }, + nodes = { + { + n= G.UIT.C, + config={ + align = "cm", + padding = args.padding or 0, + r = 0.1, + hover = true, + colour = args.colour, + one_press = args.one_press, + button = (args.button ~= 'nil') and args.button or nil, + choice = args.choice, + chosen = args.chosen, + focus_args = args.focus_args, + minh = args.minh - 0.3*(args.count and 1 or 0), + shadow = true, + func = args.func, + id = args.id, + back_func = args.back_func, + ref_table = args.ref_table, + mid = args.mid + }, + nodes = { + n = G.UIT.R, + config = { + align = "cm", + padding = 0, + minw = args.minw, + maxw = args.maxw + }, + nodes = { + { + n = G.UIT.T, + config = { + text = v, + scale = args.scale, + colour = args.text_colour, + shadow = args.shadow, + focus_args = button_pip and args.focus_args or nil, + func = button_pip, + ref_table = args.ref_table + } + } + } + } + } + } +} +]]-- + +--[[ Reference disableable button +{ + n = G.UIT.R, + config={ + id = 'select_blind_button', + align = "cm", + ref_table = blind_choice.config, + colour = disabled and G.C.UI.BACKGROUND_INACTIVE or G.C.ORANGE, + minh = 0.6, + minw = 2.7, + padding = 0.07, + r = 0.1, + shadow = true, + hover = true, + one_press = true, + button = 'select_blind' + }, + nodes = { + { + n = G.UIT.T, + config = { + ref_table = G.GAME.round_resets.loc_blind_states, + ref_value = type, + scale = 0.45, + colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.UI.TEXT_LIGHT, + shadow = not disabled + } + } + } +} +]]-- + +---------------------------------------------- +------------MOD DISABLEABLE BUTTON END-------- \ No newline at end of file diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 37e4ad26..797ed886 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -4,19 +4,18 @@ ---------------------------------------------- ------------MOD LOBBY------------------------- +local Disableable_Button = require "Disableable_Button" + Lobby = { - connected = false, - temp_code = '', - code = nil, - type = "", - config = {}, - username = "Guest", - enemy = { - username = "dude_crusher69", - score = 0, - hands = 4 - }, - players = {} + connected = false, + temp_code = '', + code = nil, + type = "", + config = {}, + username = "Guest", + host = {}, + guest = nil, + is_host = false } Connection_Status_UI = nil @@ -212,164 +211,169 @@ function G.FUNCS.lobby_leave(arg_736_0) end local function create_UIBox_lobby_menu() - local text_scale = 0.45 + local text_scale = 0.45 - local t = { - n = G.UIT.ROOT, - config = { - align = "cm", - colour = G.C.CLEAR - }, - nodes = { - { - n = G.UIT.C, - config = { - align = "bm" - }, - nodes = { - { - n = G.UIT.R, - config = { - align = "cm", - padding = 0.2, - r = 0.1, - emboss = 0.1, - colour = G.C.L_BLACK, - mid = true - }, - nodes = { - UIBox_button({ - id = 'lobby_menu_start', - button = "lobby_setup_run", - colour = G.C.BLUE, - minw = 3.65, - minh = 1.55, - label = { 'START' }, - scale = text_scale * 2, - col = true - }), - { - n = G.UIT.C, - config = { - align = "cm" - }, - nodes = { - UIBox_button { - button = 'lobby_options', - colour = G.C.ORANGE, - minw = 3.15, - minh = 1.35, - label = { 'LOBBY OPTIONS' }, - scale = text_scale * 1.2, - col = true - }, - { - n = G.UIT.C, - config = { - align = "cm", - minw = 0.2 - }, - nodes = {} - }, - { - n = G.UIT.C, - config = { - align = "tm", - minw = 2.65 - }, - nodes = { - { - n = G.UIT.R, - config = { - padding = 0.2, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - text = 'Connected Players:', - shadow = true, - scale = text_scale * 0.8, - colour = G.C.UI.TEXT_LIGHT - } - } - }, - }, - Lobby.players[1] and { - n = G.UIT.R, - config = { - padding = 0, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - text = Lobby.players[1].username, - shadow = true, - scale = text_scale * 0.8, - colour = G.C.UI.TEXT_LIGHT - } - } - } - } or nil, - Lobby.players[2] and { - n = G.UIT.R, - config = { - padding = 0, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - text = Lobby.players[2].username, - shadow = true, - scale = text_scale * 0.8, - colour = G.C.UI.TEXT_LIGHT - } - } - } - } or nil - } - }, - { - n = G.UIT.C, - config = { - align = "cm", - minw = 0.2 - }, - nodes = {} - }, - UIBox_button { - button = 'view_code', - colour = G.C.PALE_GREEN, - minw = 3.15, - minh = 1.35, - label = { 'VIEW CODE' }, - scale = text_scale * 1.2, - col = true - }, - } - }, - UIBox_button { - id = 'lobby_menu_leave', - button = "lobby_leave", - colour = G.C.RED, - minw = 3.65, - minh = 1.55, - label = { 'LEAVE' }, - scale = text_scale * 1.5, - col = true - }, - } - }, - } - }, - } - } - return t + local t = { + n = G.UIT.ROOT, + config = { + align = "cm", + colour = G.C.CLEAR + }, + nodes = { + { + n = G.UIT.C, + config = { + align = "bm" + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm", + padding = 0.2, + r = 0.1, + emboss = 0.1, + colour = G.C.L_BLACK, + mid = true + }, + nodes = { + Disableable_Button{ + id = 'lobby_menu_start', + button = 'lobby_setup_run', + colour = G.C.BLUE, + minw = 3.65, + minh = 1.55, + label = {'START'}, + disabled_text = {'WAITING FOR', 'HOST TO START'}, + scale = text_scale * 2, + col = true, + disable_ref_table = Lobby, + disable_ref_value = 'is_host' + }, + { + n = G.UIT.C, + config = { + align = "cm" + }, + nodes = { + Disableable_Button{ + button = 'lobby_options', + colour = G.C.ORANGE, + minw = 3.15, + minh = 1.35, + label = {'LOBBY OPTIONS'}, + scale = text_scale * 1.2, + col = true, + disable_ref_table = Lobby, + disable_ref_value = 'is_host' + }, + { + n = G.UIT.C, + config = { + align = "cm", + minw = 0.2 + }, + nodes = {} + }, + { + n = G.UIT.C, + config = { + align = "tm", + minw = 2.65 + }, + nodes = { + { + n = G.UIT.R, + config = { + padding = 0.2, + align = "cm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = 'Connected Players:', + shadow = true, + scale = text_scale * 0.8, + colour = G.C.UI.TEXT_LIGHT + } + } + }, + }, + Lobby.host and Lobby.host.username and { + n = G.UIT.R, + config = { + padding = 0, + align = "cm" + }, + nodes = { + { + n = G.UIT.T, + config = { + ref_table = Lobby.host, + ref_value = 'username', + shadow = true, + scale = text_scale * 0.8, + colour = G.C.UI.TEXT_LIGHT + } + } + } + } or nil, + Lobby.guest and Lobby.guest.username and { + n = G.UIT.R, + config = { + padding = 0, + align = "cm" + }, + nodes = { + { + n = G.UIT.T, + config = { + ref_table = Lobby.guest, + ref_value = 'username', + shadow = true, + scale = text_scale * 0.8, + colour = G.C.UI.TEXT_LIGHT + } + } + } + } or nil + } + }, + { + n = G.UIT.C, + config = { + align = "cm", + minw = 0.2 + }, + nodes = {} + }, + UIBox_button{ + button = 'view_code', + colour = G.C.PALE_GREEN, + minw = 3.15, + minh = 1.35, + label = {'VIEW CODE'}, + scale = text_scale * 1.2, + col = true + }, + } + }, + UIBox_button{ + id = 'lobby_menu_leave', + button = "lobby_leave", + colour = G.C.RED, + minw = 3.65, + minh = 1.55, + label = {'LEAVE'}, + scale = text_scale*1.5, + col = true + }, + } + }, + }}, + }} + return t end local function get_lobby_main_menu_UI() diff --git a/Multiplayer/Networking.lua b/Multiplayer/Networking.lua index bddc868b..dbee6118 100644 --- a/Multiplayer/Networking.lua +++ b/Multiplayer/Networking.lua @@ -22,7 +22,7 @@ local function initializeThreadDebugSocketConnection() end function SEND_THREAD_DEBUG_MESSAGE(message) - if CLIENT then + if CLIENT and message then CLIENT:send(message .. "\n") end end diff --git a/Server/README.md b/Server/README.md index 080c977d..1131ed4c 100644 --- a/Server/README.md +++ b/Server/README.md @@ -29,10 +29,11 @@ joinedLobby: code --- -lobbyInfo: host, guest? +lobbyInfo: host, guest?, isHost - Gives clients info on the lobby state - host: Lobby host's username - guest?: Lobby guest's username +- isHost: Whether the client is the host, must be a boolean (client will interpret this as a string) *This will obviously need reworking for 8 players but it is the simplest way of doing it for now From 96710597a05e01f9c5415685247466dfd667c0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Fri, 15 Mar 2024 21:05:40 -0700 Subject: [PATCH 0012/1128] Fixed lobby info request (#10) --- Multiplayer/Action_Handlers.lua | 3 ++- Server/src/actionHandlers.ts | 1 + Server/src/main.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Multiplayer/Action_Handlers.lua b/Multiplayer/Action_Handlers.lua index c9f58270..cfe1cd22 100644 --- a/Multiplayer/Action_Handlers.lua +++ b/Multiplayer/Action_Handlers.lua @@ -56,7 +56,8 @@ end -- Client to Server function ActionHandlers.create_lobby() - ActionHandlers.Client.send('action:createLobby') + -- TODO: This is hardcoded to attrition for now, must be changed + ActionHandlers.Client.send('action:createLobby,gameMode:attrition') end function ActionHandlers.join_lobby(code) diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 6c43b09f..29e20fb4 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -20,6 +20,7 @@ const createLobbyAction = ( { gameMode }: ActionHandlerArgs, client: Client, ) => { + /** Also sets the client lobby to this newly created one */ new Lobby(client) } diff --git a/Server/src/main.ts b/Server/src/main.ts index 6e96adc0..23e9fdf2 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -93,7 +93,7 @@ const server = net.createServer((socket) => { // we'll need to refactor this // Maybe add a context type that includes everything // connection related? - actionArgs + Object.keys(actionArgs).length > 0 ? actionHandlers[action]?.(actionArgs, client) : actionHandlers[action]?.(client) } catch (error) { From 5e26636427df44cf9f2317f32b4ed5ae9d56fc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Fri, 15 Mar 2024 21:35:20 -0700 Subject: [PATCH 0013/1128] Formatted all lua files with StyLua (#11) * Formatted every lua file * Added folder vscode config to gitignore --- .gitignore | 3 +- Multiplayer/Action_Handlers.lua | 36 +- Multiplayer/Blind.lua | 77 +-- Multiplayer/Core.lua | 18 +- Multiplayer/Deck.lua | 79 ++- Multiplayer/Disableable_Button.lua | 34 +- Multiplayer/Game_UI.lua | 858 +++++++++++++++++++++-------- Multiplayer/Lobby.lua | 530 +++++++++--------- Multiplayer/Main_Menu.lua | 616 +++++++++++---------- Multiplayer/Mod_Description.lua | 76 +-- Multiplayer/Networking.lua | 11 +- Multiplayer/Utils.lua | 35 +- Multiplayer/example.Config.lua | 4 +- 13 files changed, 1387 insertions(+), 990 deletions(-) diff --git a/.gitignore b/.gitignore index b30e7fa5..6e146d1a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ ref.lua node_modules .env Config.lua -Server/dist \ No newline at end of file +Server/dist +Multiplayer/.vscode \ No newline at end of file diff --git a/Multiplayer/Action_Handlers.lua b/Multiplayer/Action_Handlers.lua index cfe1cd22..8765c909 100644 --- a/Multiplayer/Action_Handlers.lua +++ b/Multiplayer/Action_Handlers.lua @@ -3,7 +3,7 @@ ---------------------------------------------- ------------MOD ACTION_HANDLERS-------------------- -local Lobby = require "Lobby" +local Lobby = require("Lobby") ActionHandlers = {} ActionHandlers.Client = {} @@ -14,9 +14,9 @@ end -- Server to Client function ActionHandlers.set_username(username) - Lobby.username = username or 'Guest' + Lobby.username = username or "Guest" if Lobby.connected then - ActionHandlers.Client.send('action:username,username:' .. Lobby.username) + ActionHandlers.Client.send("action:username,username:" .. Lobby.username) end end @@ -24,7 +24,7 @@ local function action_connected() sendDebugMessage("Client connected to multiplayer server") Lobby.connected = true Lobby.update_connection_status() - ActionHandlers.Client.send('action:username,username:' .. Lobby.username) + ActionHandlers.Client.send("action:username,username:" .. Lobby.username) end local function action_joinedLobby(code) @@ -36,7 +36,7 @@ end local function action_lobbyInfo(host, guest, is_host) Lobby.players = {} - Lobby.is_host = is_host == 'true' + Lobby.is_host = is_host == "true" Lobby.host = { username = host } if guest ~= nil then Lobby.guest = { username = guest } @@ -51,35 +51,35 @@ local function action_error(message) end local function action_keep_alive() - ActionHandlers.Client.send('action:keepAliveAck') + ActionHandlers.Client.send("action:keepAliveAck") end -- Client to Server function ActionHandlers.create_lobby() -- TODO: This is hardcoded to attrition for now, must be changed - ActionHandlers.Client.send('action:createLobby,gameMode:attrition') + ActionHandlers.Client.send("action:createLobby,gameMode:attrition") end function ActionHandlers.join_lobby(code) - ActionHandlers.Client.send('action:joinLobby,code:' .. code) + ActionHandlers.Client.send("action:joinLobby,code:" .. code) end function ActionHandlers.lobby_info() - ActionHandlers.Client.send('action:lobbyInfo') + ActionHandlers.Client.send("action:lobbyInfo") end function ActionHandlers.leave_lobby() - ActionHandlers.Client.send('action:leaveLobby') + ActionHandlers.Client.send("action:leaveLobby") end -- Utils function ActionHandlers.connect() - ActionHandlers.Client.send('connect') + ActionHandlers.Client.send("connect") end local function string_to_table(str) local tbl = {} - for key, value in string.gmatch(str, '([^,]+):([^,]+)') do + for key, value in string.gmatch(str, "([^,]+):([^,]+)") do tbl[key] = value end return tbl @@ -94,17 +94,17 @@ function Game.update(arg_298_0, arg_298_1) if msg then local parsedAction = string_to_table(msg) - sendDebugMessage('Client got ' .. parsedAction.action .. ' message') + sendDebugMessage("Client got " .. parsedAction.action .. " message") - if parsedAction.action == 'connected' then + if parsedAction.action == "connected" then action_connected() - elseif parsedAction.action == 'joinedLobby' then + elseif parsedAction.action == "joinedLobby" then action_joinedLobby(parsedAction.code) - elseif parsedAction.action == 'lobbyInfo' then + elseif parsedAction.action == "lobbyInfo" then action_lobbyInfo(parsedAction.host, parsedAction.guest, parsedAction.isHost) - elseif parsedAction.action == 'error' then + elseif parsedAction.action == "error" then action_error(parsedAction.message) - elseif parsedAction.action == 'keepAlive' then + elseif parsedAction.action == "keepAlive" then action_keep_alive() end end diff --git a/Multiplayer/Blind.lua b/Multiplayer/Blind.lua index 3f164d85..ed114486 100644 --- a/Multiplayer/Blind.lua +++ b/Multiplayer/Blind.lua @@ -5,63 +5,64 @@ ------------MOD BLIND------------------------- local bl_pvp = { - name = 'Your Nemesis', - defeated = false, - order = 0, - dollars = 5, - mult = 0, - vars = {}, - debuff = {}, - pos = {x=0, y=25}, - boss_colour = HEX('ac3232'), - boss = {min = 1, max = 10}, - key = 'bl_pvp' + name = "Your Nemesis", + defeated = false, + order = 0, + dollars = 5, + mult = 0, + vars = {}, + debuff = {}, + pos = { x = 0, y = 25 }, + boss_colour = HEX("ac3232"), + boss = { min = 1, max = 10 }, + key = "bl_pvp", } -G.P_BLINDS['bl_pvp'] = bl_pvp +G.P_BLINDS["bl_pvp"] = bl_pvp local get_new_boss_ref = get_new_boss function get_new_boss() - if Lobby.code then - return 'bl_pvp' - else - local boss = get_new_boss_ref() - while boss == 'bl_pvp' do - boss = get_new_boss_ref() - end - return boss - end + if Lobby.code then + return "bl_pvp" + else + local boss = get_new_boss_ref() + while boss == "bl_pvp" do + boss = get_new_boss_ref() + end + return boss + end end local localize_ref = localize function localize(args, misc_cat) - if type(args) == 'table' and args.key == 'bl_pvp' and args.set == 'Blind' then - if args.type == 'name_text' then - return 'Your Nemesis' - elseif args.type == 'raw_descriptions' then - return { - 'Face another player,', 'most chips wins' - } - end + if type(args) == "table" and args.key == "bl_pvp" and args.set == "Blind" then + if args.type == "name_text" then + return "Your Nemesis" + elseif args.type == "raw_descriptions" then + return { + "Face another player,", + "most chips wins", + } + end end return localize_ref(args, misc_cat) end local create_UIBox_your_collection_blinds_ref = create_UIBox_your_collection_blinds function create_UIBox_your_collection_blinds(exit) - G.P_BLINDS['bl_pvp'] = nil - local res = create_UIBox_your_collection_blinds_ref(exit) - G.P_BLINDS['bl_pvp'] = bl_pvp - return res + G.P_BLINDS["bl_pvp"] = nil + local res = create_UIBox_your_collection_blinds_ref(exit) + G.P_BLINDS["bl_pvp"] = bl_pvp + return res end local set_discover_tallies_ref = set_discover_tallies function set_discover_tallies() - G.P_BLINDS['bl_pvp'] = nil - local res = set_discover_tallies_ref() - G.P_BLINDS['bl_pvp'] = bl_pvp - return res + G.P_BLINDS["bl_pvp"] = nil + local res = set_discover_tallies_ref() + G.P_BLINDS["bl_pvp"] = bl_pvp + return res end ---------------------------------------------- -------------MOD BLIND END--------------------- \ No newline at end of file +------------MOD BLIND END--------------------- diff --git a/Multiplayer/Core.lua b/Multiplayer/Core.lua index 5647d4a3..3b2ffb1c 100644 --- a/Multiplayer/Core.lua +++ b/Multiplayer/Core.lua @@ -29,15 +29,15 @@ end function SMODS.INIT.VirtualizedMultiplayer() table.insert(package.loaders, 1, customLoader) - require "Blind" - require "Deck" - require "Main_Menu" - require "Utils".get_username() - require "Action_Handlers" - require "Mod_Description".load_description_gui() - require "Game_UI" - - CONFIG = require "Config" + require("Blind") + require("Deck") + require("Main_Menu") + require("Utils").get_username() + require("Action_Handlers") + require("Mod_Description").load_description_gui() + require("Game_UI") + + CONFIG = require("Config") NETWORKING_THREAD = love.thread.newThread(relativeModPath .. "Networking.lua") NETWORKING_THREAD:start(CONFIG.URL, CONFIG.PORT) diff --git a/Multiplayer/Deck.lua b/Multiplayer/Deck.lua index 48d372fd..818c83a5 100644 --- a/Multiplayer/Deck.lua +++ b/Multiplayer/Deck.lua @@ -5,69 +5,62 @@ ------------MOD DECK-------------------------- local c_multiplayer_1 = { - name = 'Multiplayer Deck', - id = 'c_multiplayer_1', - rules = { - custom = { - }, - modifiers = { - } - }, - jokers = { - }, - consumeables = { - }, - vouchers = { - }, - deck = { - type = 'Challenge Deck' - }, - restrictions = { - banned_cards = { - {id = 'j_diet_cola'}, - {id = 'j_mr_bones'}, - {id = 'v_hieroglyph'}, - {id = 'v_petroglyph'}, - }, - banned_tags = { - }, - banned_other = { - } - } + name = "Multiplayer Deck", + id = "c_multiplayer_1", + rules = { + custom = {}, + modifiers = {}, + }, + jokers = {}, + consumeables = {}, + vouchers = {}, + deck = { + type = "Challenge Deck", + }, + restrictions = { + banned_cards = { + { id = "j_diet_cola" }, + { id = "j_mr_bones" }, + { id = "v_hieroglyph" }, + { id = "v_petroglyph" }, + }, + banned_tags = {}, + banned_other = {}, + }, } G.CHALLENGES[21] = c_multiplayer_1 local localize_ref = localize function localize(args, misc_cat) - if args == 'c_multiplayer_1' and misc_cat == 'challenge_names' then - return 'Multiplayer' + if args == "c_multiplayer_1" and misc_cat == "challenge_names" then + return "Multiplayer" end return localize_ref(args, misc_cat) end local set_discover_tallies_ref = set_discover_tallies function set_discover_tallies() - G.CHALLENGES[21] = nil - local res = set_discover_tallies_ref() - G.CHALLENGES[21] = c_multiplayer_1 - return res + G.CHALLENGES[21] = nil + local res = set_discover_tallies_ref() + G.CHALLENGES[21] = c_multiplayer_1 + return res end local challenge_list_ref = G.FUNCS.challenge_list G.FUNCS.challenge_list = function(e) - G.CHALLENGES[21] = nil - challenge_list_ref(e) - G.CHALLENGES[21] = c_multiplayer_1 + G.CHALLENGES[21] = nil + challenge_list_ref(e) + G.CHALLENGES[21] = c_multiplayer_1 end local challenges_ref = G.UIDEF.challenges function G.UIDEF.challenges(from_game_over) - G.CHALLENGES[21] = nil - local res = challenges_ref(from_game_over) - G.CHALLENGES[21] = c_multiplayer_1 - return res + G.CHALLENGES[21] = nil + local res = challenges_ref(from_game_over) + G.CHALLENGES[21] = c_multiplayer_1 + return res end ---------------------------------------------- -------------MOD DECK END---------------------- \ No newline at end of file +------------MOD DECK END---------------------- diff --git a/Multiplayer/Disableable_Button.lua b/Multiplayer/Disableable_Button.lua index db64ea06..cfaa89dd 100644 --- a/Multiplayer/Disableable_Button.lua +++ b/Multiplayer/Disableable_Button.lua @@ -5,20 +5,20 @@ ------------MOD DISABLEABLE BUTTON------------ function Disableable_Button(args) - local enabled_table = args.enabled_ref_table or {} - local enabled = enabled_table[args.disable_ref_value] - args.colour = args.colour or G.C.RED - args.text_colour = args.text_colour or G.C.UI.TEXT_LIGHT - args.label = not enabled and args.disabled_text or args.label + local enabled_table = args.enabled_ref_table or {} + local enabled = enabled_table[args.disable_ref_value] + args.colour = args.colour or G.C.RED + args.text_colour = args.text_colour or G.C.UI.TEXT_LIGHT + args.label = not enabled and args.disabled_text or args.label - local button_component = UIBox_button(args) - button_component.nodes[1].config.button = enabled and args.button or nil - button_component.nodes[1].config.hover = enabled - button_component.nodes[1].config.shadow = enabled - button_component.nodes[1].config.colour = enabled and args.colour or G.C.UI.BACKGROUND_INACTIVE - button_component.nodes[1].nodes[1].nodes[1].colour = enabled and args.text_colour or G.C.UI.TEXT_INACTIVE - button_component.nodes[1].nodes[1].nodes[1].shadow = enabled - return button_component + local button_component = UIBox_button(args) + button_component.nodes[1].config.button = enabled and args.button or nil + button_component.nodes[1].config.hover = enabled + button_component.nodes[1].config.shadow = enabled + button_component.nodes[1].config.colour = enabled and args.colour or G.C.UI.BACKGROUND_INACTIVE + button_component.nodes[1].nodes[1].nodes[1].colour = enabled and args.text_colour or G.C.UI.TEXT_INACTIVE + button_component.nodes[1].nodes[1].nodes[1].shadow = enabled + return button_component end return Disableable_Button @@ -77,7 +77,8 @@ return Disableable_Button } } } -]]-- +]] +-- --[[ Reference disableable button { @@ -109,7 +110,8 @@ return Disableable_Button } } } -]]-- +]] +-- ---------------------------------------------- -------------MOD DISABLEABLE BUTTON END-------- \ No newline at end of file +------------MOD DISABLEABLE BUTTON END-------- diff --git a/Multiplayer/Game_UI.lua b/Multiplayer/Game_UI.lua index 679ffd07..7dbe7955 100644 --- a/Multiplayer/Game_UI.lua +++ b/Multiplayer/Game_UI.lua @@ -4,276 +4,656 @@ ---------------------------------------------- ------------MOD GAME UI----------------------- -local Lobby = require "Lobby" +local Lobby = require("Lobby") Game_UI = {} local create_UIBox_options_ref = create_UIBox_options function create_UIBox_options() - if Lobby.code then - local current_seed = nil - local main_menu = nil - local your_collection = nil - local credits = nil + if Lobby.code then + local current_seed = nil + local main_menu = nil + local your_collection = nil + local credits = nil - G.E_MANAGER:add_event(Event({ - blockable = false, - func = function() - G.REFRESH_ALERTS = true - return true - end - })) + G.E_MANAGER:add_event(Event({ + blockable = false, + func = function() + G.REFRESH_ALERTS = true + return true + end, + })) - if G.STAGE == G.STAGES.RUN then - main_menu = UIBox_button{ label = {'Return to Lobby'}, button = "go_to_menu", minw = 5} - your_collection = UIBox_button{ label = {localize('b_collection')}, button = "your_collection", minw = 5, id = 'your_collection'} - current_seed = {n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={ - {n=G.UIT.C, config={align = "cm", padding = 0}, nodes={ - {n=G.UIT.T, config={text = localize('b_seed')..": ", scale = 0.4, colour = G.C.WHITE}} - }}, - {n=G.UIT.C, config={align = "cm", padding = 0, minh = 0.8}, nodes={ - {n=G.UIT.C, config={align = "cm", padding = 0, minh = 0.8}, nodes={ - {n=G.UIT.R, config={align = "cm", r = 0.1, colour = G.GAME.seeded and G.C.RED or G.C.BLACK, minw = 1.8, minh = 0.5, padding = 0.1, emboss = 0.05}, nodes={ - {n=G.UIT.C, config={align = "cm"}, nodes={ - {n=G.UIT.T, config={ text = tostring(G.GAME.pseudorandom.seed), scale = 0.43, colour = G.C.UI.TEXT_LIGHT, shadow = true}} - }} - }} - }} - }}, - UIBox_button({col = true, button = 'copy_seed', label = {localize('b_copy')}, colour = G.C.BLUE, scale = 0.3, minw = 1.3, minh = 0.5,}), - }} - end - if G.STAGE == G.STAGES.MAIN_MENU then - credits = UIBox_button{ label = {localize('b_credits')}, button = "show_credits", minw = 5} - end + if G.STAGE == G.STAGES.RUN then + main_menu = UIBox_button({ label = { "Return to Lobby" }, button = "go_to_menu", minw = 5 }) + your_collection = UIBox_button({ + label = { localize("b_collection") }, + button = "your_collection", + minw = 5, + id = "your_collection", + }) + current_seed = { + n = G.UIT.R, + config = { align = "cm", padding = 0.05 }, + nodes = { + { + n = G.UIT.C, + config = { align = "cm", padding = 0 }, + nodes = { + { + n = G.UIT.T, + config = { text = localize("b_seed") .. ": ", scale = 0.4, colour = G.C.WHITE }, + }, + }, + }, + { + n = G.UIT.C, + config = { align = "cm", padding = 0, minh = 0.8 }, + nodes = { + { + n = G.UIT.C, + config = { align = "cm", padding = 0, minh = 0.8 }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm", + r = 0.1, + colour = G.GAME.seeded and G.C.RED or G.C.BLACK, + minw = 1.8, + minh = 0.5, + padding = 0.1, + emboss = 0.05, + }, + nodes = { + { + n = G.UIT.C, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.T, + config = { + text = tostring(G.GAME.pseudorandom.seed), + scale = 0.43, + colour = G.C.UI.TEXT_LIGHT, + shadow = true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + UIBox_button({ + col = true, + button = "copy_seed", + label = { localize("b_copy") }, + colour = G.C.BLUE, + scale = 0.3, + minw = 1.3, + minh = 0.5, + }), + }, + } + end + if G.STAGE == G.STAGES.MAIN_MENU then + credits = UIBox_button({ label = { localize("b_credits") }, button = "show_credits", minw = 5 }) + end - local settings = UIBox_button({button = 'settings', label = {localize('b_settings')}, minw = 5, focus_args = {snap_to = true}}) - local high_scores = UIBox_button{ label = {localize('b_stats')}, button = "high_scores", minw = 5} + local settings = UIBox_button({ + button = "settings", + label = { localize("b_settings") }, + minw = 5, + focus_args = { snap_to = true }, + }) + local high_scores = UIBox_button({ label = { localize("b_stats") }, button = "high_scores", minw = 5 }) - local t = create_UIBox_generic_options({ contents = { - settings, - G.GAME.seeded and current_seed or nil, - main_menu, - high_scores, - your_collection, - credits - }}) - return t - else - return create_UIBox_options_ref() - end + local t = create_UIBox_generic_options({ + contents = { + settings, + G.GAME.seeded and current_seed or nil, + main_menu, + high_scores, + your_collection, + credits, + }, + }) + return t + else + return create_UIBox_options_ref() + end end local create_UIBox_blind_choice_ref = create_UIBox_blind_choice function create_UIBox_blind_choice(type, run_info) - if Lobby.code then - if not G.GAME.blind_on_deck then - G.GAME.blind_on_deck = 'Small' - end - if not run_info then G.GAME.round_resets.blind_states[G.GAME.blind_on_deck] = 'Select' end - - local disabled = false - type = type or 'Small' - - local blind_choice = { - config = G.P_BLINDS[G.GAME.round_resets.blind_choices[type]], - } - - blind_choice.animation = AnimatedSprite(0,0, 1.4, 1.4, G.ANIMATION_ATLAS['blind_chips'], blind_choice.config.pos) - blind_choice.animation:define_draw_steps({ - {shader = 'dissolve', shadow_height = 0.05}, - {shader = 'dissolve'} - }) - local extras = nil - local stake_sprite = get_stake_sprite(G.GAME.stake or 1, 0.5) - - G.GAME.orbital_choices = G.GAME.orbital_choices or {} - G.GAME.orbital_choices[G.GAME.round_resets.ante] = G.GAME.orbital_choices[G.GAME.round_resets.ante] or {} - - if not G.GAME.orbital_choices[G.GAME.round_resets.ante][type] then - local _poker_hands = {} - for k, v in pairs(G.GAME.hands) do - if v.visible then _poker_hands[#_poker_hands+1] = k end - end - - G.GAME.orbital_choices[G.GAME.round_resets.ante][type] = pseudorandom_element(_poker_hands, pseudoseed('orbital')) - end - - if type == 'Small' then - extras = nil - elseif type == 'Big' then - extras = nil - elseif not run_info then - local dt1 = DynaText({string = {{string = 'LIFE', colour = G.C.FILTER}}, colours = {G.C.BLACK}, scale = 0.55, silent = true, pop_delay = 4.5, shadow = true, bump = true, maxw = 3}) - local dt2 = DynaText({string = {{string = 'or', colour = G.C.WHITE}},colours = {G.C.CHANCE}, scale = 0.35, silent = true, pop_delay = 4.5, shadow = true, maxw = 3}) - local dt3 = DynaText({string = {{string = 'DEATH', colour = G.C.FILTER}}, colours = {G.C.BLACK}, scale = 0.55, silent = true, pop_delay = 4.5, shadow = true, bump = true, maxw = 3}) - extras = - {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.R, config={align = "cm", padding = 0.07, r = 0.1, colour = {0,0,0,0.12}, minw = 2.9}, nodes={ - {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.O, config={object = dt1}}, - }}, - {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.O, config={object = dt2}}, - }}, - {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.O, config={object = dt3}}, - }}, - }}, - }} - end - G.GAME.round_resets.blind_ante = G.GAME.round_resets.blind_ante or G.GAME.round_resets.ante + if Lobby.code then + if not G.GAME.blind_on_deck then + G.GAME.blind_on_deck = "Small" + end + if not run_info then + G.GAME.round_resets.blind_states[G.GAME.blind_on_deck] = "Select" + end - local loc_target = localize{type = 'raw_descriptions', key = blind_choice.config.key, set = 'Blind', vars = {localize(G.GAME.current_round.most_played_poker_hand, 'poker_hands')}} - local loc_name = localize{type = 'name_text', key = blind_choice.config.key, set = 'Blind'} - local blind_col = get_blind_main_colour(type) - local blind_amt = get_blind_amount(G.GAME.round_resets.blind_ante)*blind_choice.config.mult*G.GAME.starting_params.ante_scaling + local disabled = false + type = type or "Small" - if G.GAME.round_resets.blind_choices[type] == 'bl_pvp' then - blind_amt = '????' - end + local blind_choice = { + config = G.P_BLINDS[G.GAME.round_resets.blind_choices[type]], + } - local text_table = loc_target - - local blind_state = G.GAME.round_resets.blind_states[type] - local _reward = true - if G.GAME.modifiers.no_blind_reward and G.GAME.modifiers.no_blind_reward[type] then _reward = nil end - if blind_state == 'Select' then blind_state = 'Current' end - local run_info_colour = run_info and (blind_state == 'Defeated' and G.C.GREY or blind_state == 'Skipped' and G.C.BLUE or blind_state == 'Upcoming' and G.C.ORANGE or blind_state == 'Current' and G.C.RED or G.C.GOLD) - local t = - {n=G.UIT.R, config={id = type, align = "tm", func = 'blind_choice_handler', minh = not run_info and 10 or nil, ref_table = {deck = nil, run_info = run_info}, r = 0.1, padding = 0.05}, nodes={ - {n=G.UIT.R, config={align = "cm", colour = mix_colours(G.C.BLACK, G.C.L_BLACK, 0.5), r = 0.1, outline = 1, outline_colour = G.C.L_BLACK}, nodes={ - {n=G.UIT.R, config={align = "cm", padding = 0.2}, nodes={ - not run_info and {n=G.UIT.R, config={id = 'select_blind_button', align = "cm", ref_table = blind_choice.config, colour = disabled and G.C.UI.BACKGROUND_INACTIVE or G.C.ORANGE, minh = 0.6, minw = 2.7, padding = 0.07, r = 0.1, shadow = true, hover = true, one_press = true, button = 'select_blind'}, nodes={ - {n=G.UIT.T, config={ref_table = G.GAME.round_resets.loc_blind_states, ref_value = type, scale = 0.45, colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.UI.TEXT_LIGHT, shadow = not disabled}} - }} or - {n=G.UIT.R, config={id = 'select_blind_button', align = "cm", ref_table = blind_choice.config, colour = run_info_colour, minh = 0.6, minw = 2.7, padding = 0.07, r = 0.1, emboss = 0.08}, nodes={ - {n=G.UIT.T, config={text = localize(blind_state, 'blind_states'), scale = 0.45, colour = G.C.UI.TEXT_LIGHT, shadow = true}} - }} - }}, - {n=G.UIT.R, config={id = 'blind_name',align = "cm", padding = 0.07}, nodes={ - {n=G.UIT.R, config={align = "cm", r = 0.1, outline = 1, outline_colour = blind_col, colour = darken(blind_col, 0.3), minw = 2.9, emboss = 0.1, padding = 0.07, line_emboss = 1}, nodes={ - {n=G.UIT.O, config={object = DynaText({string = loc_name, colours = {disabled and G.C.UI.TEXT_INACTIVE or G.C.WHITE}, shadow = not disabled, float = not disabled, y_offset = -4, scale = 0.45, maxw =2.8})}}, - }}, - }}, - {n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={ - {n=G.UIT.R, config={id = 'blind_desc', align = "cm", padding = 0.05}, nodes={ - {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.R, config={align = "cm", minh = 1.5}, nodes={ - {n=G.UIT.O, config={object = blind_choice.animation}}, - }}, - text_table[1] and {n=G.UIT.R, config={align = "cm", minh = 0.7, padding = 0.05, minw = 2.9}, nodes={ - text_table[1] and {n=G.UIT.R, config={align = "cm", maxw = 2.8}, nodes={ - {n=G.UIT.T, config={id = blind_choice.config.key, ref_table = {val = ''}, ref_value = 'val', scale = 0.32, colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.WHITE, shadow = not disabled, func = 'HUD_blind_debuff_prefix'}}, - {n=G.UIT.T, config={text = text_table[1] or '-', scale = 0.32, colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.WHITE, shadow = not disabled}} - }} or nil, - text_table[2] and {n=G.UIT.R, config={align = "cm", maxw = 2.8}, nodes={ - {n=G.UIT.T, config={text = text_table[2] or '-', scale = 0.32, colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.WHITE, shadow = not disabled}} - }} or nil, - }} or nil, - }}, - {n=G.UIT.R, config={align = "cm",r = 0.1, padding = 0.05, minw = 3.1, colour = G.C.BLACK, emboss = 0.05}, nodes={ - {n=G.UIT.R, config={align = "cm", maxw = 3}, nodes={ - {n=G.UIT.T, config={text = localize('ph_blind_score_at_least'), scale = 0.3, colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.WHITE, shadow = not disabled}} - }}, - {n=G.UIT.R, config={align = "cm", minh = 0.6}, nodes={ - {n=G.UIT.O, config={w=0.5,h=0.5, colour = G.C.BLUE, object = stake_sprite, hover = true, can_collide = false}}, - {n=G.UIT.B, config={h=0.1,w=0.1}}, - {n=G.UIT.T, config={text = number_format(blind_amt), scale = score_number_scale(0.9, blind_amt), colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.RED, shadow = not disabled}} - }}, - _reward and {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.T, config={text = localize('ph_blind_reward'), scale = 0.35, colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.WHITE, shadow = not disabled}}, - {n=G.UIT.T, config={text = string.rep(localize("$"), blind_choice.config.dollars)..'+', scale = 0.35, colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.MONEY, shadow = not disabled}} - }} or nil, - }}, - }}, - }}, - }}, - {n=G.UIT.R, config={id = 'blind_extras', align = "cm"}, nodes={ - extras, - }} - - }} - return t - else - return create_UIBox_blind_choice_ref(type, run_info) - end + blind_choice.animation = + AnimatedSprite(0, 0, 1.4, 1.4, G.ANIMATION_ATLAS["blind_chips"], blind_choice.config.pos) + blind_choice.animation:define_draw_steps({ + { shader = "dissolve", shadow_height = 0.05 }, + { shader = "dissolve" }, + }) + local extras = nil + local stake_sprite = get_stake_sprite(G.GAME.stake or 1, 0.5) + + G.GAME.orbital_choices = G.GAME.orbital_choices or {} + G.GAME.orbital_choices[G.GAME.round_resets.ante] = G.GAME.orbital_choices[G.GAME.round_resets.ante] or {} + + if not G.GAME.orbital_choices[G.GAME.round_resets.ante][type] then + local _poker_hands = {} + for k, v in pairs(G.GAME.hands) do + if v.visible then + _poker_hands[#_poker_hands + 1] = k + end + end + + G.GAME.orbital_choices[G.GAME.round_resets.ante][type] = + pseudorandom_element(_poker_hands, pseudoseed("orbital")) + end + + if type == "Small" then + extras = nil + elseif type == "Big" then + extras = nil + elseif not run_info then + local dt1 = DynaText({ + string = { { string = "LIFE", colour = G.C.FILTER } }, + colours = { G.C.BLACK }, + scale = 0.55, + silent = true, + pop_delay = 4.5, + shadow = true, + bump = true, + maxw = 3, + }) + local dt2 = DynaText({ + string = { { string = "or", colour = G.C.WHITE } }, + colours = { G.C.CHANCE }, + scale = 0.35, + silent = true, + pop_delay = 4.5, + shadow = true, + maxw = 3, + }) + local dt3 = DynaText({ + string = { { string = "DEATH", colour = G.C.FILTER } }, + colours = { G.C.BLACK }, + scale = 0.55, + silent = true, + pop_delay = 4.5, + shadow = true, + bump = true, + maxw = 3, + }) + extras = { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm", padding = 0.07, r = 0.1, colour = { 0, 0, 0, 0.12 }, minw = 2.9 }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { n = G.UIT.O, config = { object = dt1 } }, + }, + }, + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { n = G.UIT.O, config = { object = dt2 } }, + }, + }, + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { n = G.UIT.O, config = { object = dt3 } }, + }, + }, + }, + }, + }, + } + end + G.GAME.round_resets.blind_ante = G.GAME.round_resets.blind_ante or G.GAME.round_resets.ante + + local loc_target = localize({ + type = "raw_descriptions", + key = blind_choice.config.key, + set = "Blind", + vars = { localize(G.GAME.current_round.most_played_poker_hand, "poker_hands") }, + }) + local loc_name = localize({ type = "name_text", key = blind_choice.config.key, set = "Blind" }) + local blind_col = get_blind_main_colour(type) + local blind_amt = get_blind_amount(G.GAME.round_resets.blind_ante) + * blind_choice.config.mult + * G.GAME.starting_params.ante_scaling + + if G.GAME.round_resets.blind_choices[type] == "bl_pvp" then + blind_amt = "????" + end + + local text_table = loc_target + + local blind_state = G.GAME.round_resets.blind_states[type] + local _reward = true + if G.GAME.modifiers.no_blind_reward and G.GAME.modifiers.no_blind_reward[type] then + _reward = nil + end + if blind_state == "Select" then + blind_state = "Current" + end + local run_info_colour = run_info + and ( + blind_state == "Defeated" and G.C.GREY + or blind_state == "Skipped" and G.C.BLUE + or blind_state == "Upcoming" and G.C.ORANGE + or blind_state == "Current" and G.C.RED + or G.C.GOLD + ) + local t = { + n = G.UIT.R, + config = { + id = type, + align = "tm", + func = "blind_choice_handler", + minh = not run_info and 10 or nil, + ref_table = { deck = nil, run_info = run_info }, + r = 0.1, + padding = 0.05, + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm", + colour = mix_colours(G.C.BLACK, G.C.L_BLACK, 0.5), + r = 0.1, + outline = 1, + outline_colour = G.C.L_BLACK, + }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm", padding = 0.2 }, + nodes = { + not run_info and { + n = G.UIT.R, + config = { + id = "select_blind_button", + align = "cm", + ref_table = blind_choice.config, + colour = disabled and G.C.UI.BACKGROUND_INACTIVE or G.C.ORANGE, + minh = 0.6, + minw = 2.7, + padding = 0.07, + r = 0.1, + shadow = true, + hover = true, + one_press = true, + button = "select_blind", + }, + nodes = { + { + n = G.UIT.T, + config = { + ref_table = G.GAME.round_resets.loc_blind_states, + ref_value = type, + scale = 0.45, + colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.UI.TEXT_LIGHT, + shadow = not disabled, + }, + }, + }, + } or { + n = G.UIT.R, + config = { + id = "select_blind_button", + align = "cm", + ref_table = blind_choice.config, + colour = run_info_colour, + minh = 0.6, + minw = 2.7, + padding = 0.07, + r = 0.1, + emboss = 0.08, + }, + nodes = { + { + n = G.UIT.T, + config = { + text = localize(blind_state, "blind_states"), + scale = 0.45, + colour = G.C.UI.TEXT_LIGHT, + shadow = true, + }, + }, + }, + }, + }, + }, + { + n = G.UIT.R, + config = { id = "blind_name", align = "cm", padding = 0.07 }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm", + r = 0.1, + outline = 1, + outline_colour = blind_col, + colour = darken(blind_col, 0.3), + minw = 2.9, + emboss = 0.1, + padding = 0.07, + line_emboss = 1, + }, + nodes = { + { + n = G.UIT.O, + config = { + object = DynaText({ + string = loc_name, + colours = { disabled and G.C.UI.TEXT_INACTIVE or G.C.WHITE }, + shadow = not disabled, + float = not disabled, + y_offset = -4, + scale = 0.45, + maxw = 2.8, + }), + }, + }, + }, + }, + }, + }, + { + n = G.UIT.R, + config = { align = "cm", padding = 0.05 }, + nodes = { + { + n = G.UIT.R, + config = { id = "blind_desc", align = "cm", padding = 0.05 }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm", minh = 1.5 }, + nodes = { + { n = G.UIT.O, config = { object = blind_choice.animation } }, + }, + }, + text_table[1] and { + n = G.UIT.R, + config = { + align = "cm", + minh = 0.7, + padding = 0.05, + minw = 2.9, + }, + nodes = { + text_table[1] + and { + n = G.UIT.R, + config = { align = "cm", maxw = 2.8 }, + nodes = { + { + n = G.UIT.T, + config = { + id = blind_choice.config.key, + ref_table = { val = "" }, + ref_value = "val", + scale = 0.32, + colour = disabled + and G.C.UI.TEXT_INACTIVE + or G.C.WHITE, + shadow = not disabled, + func = "HUD_blind_debuff_prefix", + }, + }, + { + n = G.UIT.T, + config = { + text = text_table[1] or "-", + scale = 0.32, + colour = disabled + and G.C.UI.TEXT_INACTIVE + or G.C.WHITE, + shadow = not disabled, + }, + }, + }, + } + or nil, + text_table[2] and { + n = G.UIT.R, + config = { align = "cm", maxw = 2.8 }, + nodes = { + { + n = G.UIT.T, + config = { + text = text_table[2] or "-", + scale = 0.32, + colour = disabled and G.C.UI.TEXT_INACTIVE + or G.C.WHITE, + shadow = not disabled, + }, + }, + }, + } or nil, + }, + } or nil, + }, + }, + { + n = G.UIT.R, + config = { + align = "cm", + r = 0.1, + padding = 0.05, + minw = 3.1, + colour = G.C.BLACK, + emboss = 0.05, + }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm", maxw = 3 }, + nodes = { + { + n = G.UIT.T, + config = { + text = localize("ph_blind_score_at_least"), + scale = 0.3, + colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.WHITE, + shadow = not disabled, + }, + }, + }, + }, + { + n = G.UIT.R, + config = { align = "cm", minh = 0.6 }, + nodes = { + { + n = G.UIT.O, + config = { + w = 0.5, + h = 0.5, + colour = G.C.BLUE, + object = stake_sprite, + hover = true, + can_collide = false, + }, + }, + { n = G.UIT.B, config = { h = 0.1, w = 0.1 } }, + { + n = G.UIT.T, + config = { + text = number_format(blind_amt), + scale = score_number_scale(0.9, blind_amt), + colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.RED, + shadow = not disabled, + }, + }, + }, + }, + _reward and { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.T, + config = { + text = localize("ph_blind_reward"), + scale = 0.35, + colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.WHITE, + shadow = not disabled, + }, + }, + { + n = G.UIT.T, + config = { + text = string.rep( + localize("$"), + blind_choice.config.dollars + ) .. "+", + scale = 0.35, + colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.MONEY, + shadow = not disabled, + }, + }, + }, + } or nil, + }, + }, + }, + }, + }, + }, + }, + }, + { n = G.UIT.R, config = { id = "blind_extras", align = "cm" }, nodes = { + extras, + } }, + }, + } + return t + else + return create_UIBox_blind_choice_ref(type, run_info) + end end function Game_UI.update_enemy() - if Lobby.code then - G.HUD_blind.alignment.offset.y = -10 - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.3, - blockable = false, - func = function() - G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_table = Lobby.enemy - G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_value = 'score' - G.HUD_blind:get_UIE_by_ID("HUD_blind").children[2].children[2].children[2].children[1].children[1].config.text = 'Current enemy score' - G.HUD_blind:get_UIE_by_ID("HUD_blind").children[2].children[2].children[2].children[3].children[1].config.text = 'Enemy hands left: ' - G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object.config.string = {{ref_table = Lobby.enemy, ref_value = 'hands'}} - G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object:update_text() - G.HUD_blind.alignment.offset.y = 0 - return true - end - })) - end + if Lobby.code then + G.HUD_blind.alignment.offset.y = -10 + G.E_MANAGER:add_event(Event({ + trigger = "after", + delay = 0.3, + blockable = false, + func = function() + G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_table = Lobby.enemy + G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_value = "score" + G.HUD_blind:get_UIE_by_ID("HUD_blind").children[2].children[2].children[2].children[1].children[1].config.text = + "Current enemy score" + G.HUD_blind:get_UIE_by_ID("HUD_blind").children[2].children[2].children[2].children[3].children[1].config.text = + "Enemy hands left: " + G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object.config.string = + { { ref_table = Lobby.enemy, ref_value = "hands" } } + G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object:update_text() + G.HUD_blind.alignment.offset.y = 0 + return true + end, + })) + end end function Game_UI.reset_blind_HUD() - if Lobby.code then - G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object.config.string = {{ref_table = G.GAME.blind, ref_value = 'loc_name'}} - G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:update_text() - G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_table = G.GAME.blind - G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_value = 'chip_text' - G.HUD_blind:get_UIE_by_ID("HUD_blind").children[2].children[2].children[2].children[1].children[1].config.text = localize('ph_blind_score_at_least') - G.HUD_blind:get_UIE_by_ID("HUD_blind").children[2].children[2].children[2].children[3].children[1].config.text = localize('ph_blind_reward') - G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object.config.string = {{ref_table = G.GAME.current_round, ref_value = 'dollars_to_be_earned'}} - G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object:update_text() - end + if Lobby.code then + G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object.config.string = + { { ref_table = G.GAME.blind, ref_value = "loc_name" } } + G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:update_text() + G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_table = G.GAME.blind + G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_value = "chip_text" + G.HUD_blind:get_UIE_by_ID("HUD_blind").children[2].children[2].children[2].children[1].children[1].config.text = + localize("ph_blind_score_at_least") + G.HUD_blind:get_UIE_by_ID("HUD_blind").children[2].children[2].children[2].children[3].children[1].config.text = + localize("ph_blind_reward") + G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object.config.string = + { { ref_table = G.GAME.current_round, ref_value = "dollars_to_be_earned" } } + G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object:update_text() + end end local update_draw_to_hand_ref = Game.update_draw_to_hand function Game:update_draw_to_hand(dt) - if Lobby.code then - if not G.STATE_COMPLETE and G.GAME.current_round.hands_played == 0 and - G.GAME.current_round.discards_used == 0 and - G.GAME.facing_blind then - if G.GAME.blind.name == 'Your Nemesis' then - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 1, - blockable = false, - func = function() - G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:pop_out(0) - Game_UI.update_enemy() - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.45, - blockable = false, - func = function() - G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object.config.string = {{ref_table = Lobby.enemy, ref_value = 'username'}} - G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:update_text() - G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:pop_in(0) - return true - end - })) - return true - end - })) - end - end - end - update_draw_to_hand_ref(self,dt) + if Lobby.code then + if + not G.STATE_COMPLETE + and G.GAME.current_round.hands_played == 0 + and G.GAME.current_round.discards_used == 0 + and G.GAME.facing_blind + then + if G.GAME.blind.name == "Your Nemesis" then + G.E_MANAGER:add_event(Event({ + trigger = "after", + delay = 1, + blockable = false, + func = function() + G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:pop_out(0) + Game_UI.update_enemy() + G.E_MANAGER:add_event(Event({ + trigger = "after", + delay = 0.45, + blockable = false, + func = function() + G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object.config.string = + { { ref_table = Lobby.enemy, ref_value = "username" } } + G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:update_text() + G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:pop_in(0) + return true + end, + })) + return true + end, + })) + end + end + end + update_draw_to_hand_ref(self, dt) end local blind_defeat_ref = Blind.defeat function Blind:defeat(silent) - blind_defeat_ref(self, silent) - Game_UI.reset_blind_HUD() + blind_defeat_ref(self, silent) + Game_UI.reset_blind_HUD() end return Game_UI ---------------------------------------------- -------------MOD GAME UI END------------------- \ No newline at end of file +------------MOD GAME UI END------------------- diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 797ed886..cf8ae680 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -4,18 +4,18 @@ ---------------------------------------------- ------------MOD LOBBY------------------------- -local Disableable_Button = require "Disableable_Button" +local Disableable_Button = require("Disableable_Button") Lobby = { - connected = false, - temp_code = '', - code = nil, - type = "", - config = {}, - username = "Guest", - host = {}, - guest = nil, - is_host = false + connected = false, + temp_code = "", + code = nil, + type = "", + config = {}, + username = "Guest", + host = {}, + guest = nil, + is_host = false, } Connection_Status_UI = nil @@ -26,29 +26,30 @@ local function get_connection_status_ui() n = G.UIT.ROOT, config = { align = "cm", - colour = G.C.UI.TRANSPARENT_DARK + colour = G.C.UI.TRANSPARENT_DARK, }, nodes = { { n = G.UIT.T, config = { scale = 0.3, - text = (Lobby.code and 'In Lobby') or (Lobby.connected and 'Connected to Service') or - 'WARN: Cannot Find Multiplayer Service', - colour = G.C.UI.TEXT_LIGHT - } - } - } + text = (Lobby.code and "In Lobby") + or (Lobby.connected and "Connected to Service") + or "WARN: Cannot Find Multiplayer Service", + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, }, config = { align = "tri", bond = "Weak", offset = { x = 0, - y = 0.9 + y = 0.9, }, - major = G.ROOM_ATTACH - } + major = G.ROOM_ATTACH, + }, }) end @@ -77,88 +78,83 @@ end function create_UIBox_view_code() local var_495_0 = 0.75 - return (create_UIBox_generic_options({ - contents = { - { - n = G.UIT.R, - config = { - padding = 0, - align = "cm" - }, - nodes = { - { - n = G.UIT.R, - config = { - padding = 0.5, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - text = Lobby.code, - shadow = true, - scale = var_495_0 * 0.6, - colour = G.C.UI.TEXT_LIGHT - } - } - } + return ( + create_UIBox_generic_options({ + contents = { + { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", }, - { - n = G.UIT.R, - config = { - padding = 0, - align = "cm" + nodes = { + { + n = G.UIT.R, + config = { + padding = 0.5, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, + config = { + text = Lobby.code, + shadow = true, + scale = var_495_0 * 0.6, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, }, - nodes = { - UIBox_button({ - label = { "Copy to Clipboard" }, - colour = G.C.BLUE, - button = "copy_to_clipboard", - minw = 5, - }) - } - } - } - } - } - })) + { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", + }, + nodes = { + UIBox_button({ + label = { "Copy to Clipboard" }, + colour = G.C.BLUE, + button = "copy_to_clipboard", + minw = 5, + }), + }, + }, + }, + }, + }, + }) + ) end function G.FUNCS.lobby_setup_run(arg_736_0) G.FUNCS.start_run(arg_736_0, { stake = 1, challenge = { - name = 'Multiplayer Deck', - id = 'c_multiplayer_1', + name = "Multiplayer Deck", + id = "c_multiplayer_1", rules = { - custom = { - }, - modifiers = { - } - }, - jokers = { - }, - consumeables = { - }, - vouchers = { + custom = {}, + modifiers = {}, }, + jokers = {}, + consumeables = {}, + vouchers = {}, deck = { - type = 'Challenge Deck' + type = "Challenge Deck", }, restrictions = { banned_cards = { - { id = 'j_diet_cola' }, -- Intention to disable skipping - { id = 'j_mr_bones' }, - { id = 'v_hieroglyph' }, - { id = 'v_petroglyph' }, + { id = "j_diet_cola" }, -- Intention to disable skipping + { id = "j_mr_bones" }, + { id = "v_hieroglyph" }, + { id = "v_petroglyph" }, }, - banned_tags = { - }, - banned_other = { - } - } - } + banned_tags = {}, + banned_other = {}, + }, + }, }) end @@ -170,37 +166,37 @@ function G.FUNCS.lobby_options(arg_736_0) n = G.UIT.R, config = { padding = 0, - align = "cm" + align = "cm", }, nodes = { { n = G.UIT.R, config = { padding = 0.5, - align = "cm" + align = "cm", }, nodes = { { n = G.UIT.T, config = { - text = 'Not Implemented Yet', + text = "Not Implemented Yet", shadow = true, scale = 0.6, - colour = G.C.UI.TEXT_LIGHT - } - } - } - } - } - } - } - }) + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + }, + }, + }, + }, + }), }) end function G.FUNCS.view_code(arg_736_0) G.FUNCS.overlay_menu({ - definition = create_UIBox_view_code() + definition = create_UIBox_view_code(), }) end @@ -211,169 +207,171 @@ function G.FUNCS.lobby_leave(arg_736_0) end local function create_UIBox_lobby_menu() - local text_scale = 0.45 + local text_scale = 0.45 - local t = { - n = G.UIT.ROOT, - config = { - align = "cm", - colour = G.C.CLEAR - }, - nodes = { - { - n = G.UIT.C, - config = { - align = "bm" - }, - nodes = { - { - n = G.UIT.R, - config = { - align = "cm", - padding = 0.2, - r = 0.1, - emboss = 0.1, - colour = G.C.L_BLACK, - mid = true - }, - nodes = { - Disableable_Button{ - id = 'lobby_menu_start', - button = 'lobby_setup_run', - colour = G.C.BLUE, - minw = 3.65, - minh = 1.55, - label = {'START'}, - disabled_text = {'WAITING FOR', 'HOST TO START'}, - scale = text_scale * 2, - col = true, - disable_ref_table = Lobby, - disable_ref_value = 'is_host' - }, - { - n = G.UIT.C, - config = { - align = "cm" - }, - nodes = { - Disableable_Button{ - button = 'lobby_options', - colour = G.C.ORANGE, - minw = 3.15, - minh = 1.35, - label = {'LOBBY OPTIONS'}, - scale = text_scale * 1.2, - col = true, - disable_ref_table = Lobby, - disable_ref_value = 'is_host' - }, - { - n = G.UIT.C, - config = { - align = "cm", - minw = 0.2 - }, - nodes = {} - }, - { - n = G.UIT.C, - config = { - align = "tm", - minw = 2.65 - }, - nodes = { - { - n = G.UIT.R, - config = { - padding = 0.2, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - text = 'Connected Players:', - shadow = true, - scale = text_scale * 0.8, - colour = G.C.UI.TEXT_LIGHT - } - } - }, - }, - Lobby.host and Lobby.host.username and { - n = G.UIT.R, - config = { - padding = 0, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - ref_table = Lobby.host, - ref_value = 'username', - shadow = true, - scale = text_scale * 0.8, - colour = G.C.UI.TEXT_LIGHT - } - } - } - } or nil, - Lobby.guest and Lobby.guest.username and { - n = G.UIT.R, - config = { - padding = 0, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - ref_table = Lobby.guest, - ref_value = 'username', - shadow = true, - scale = text_scale * 0.8, - colour = G.C.UI.TEXT_LIGHT - } - } - } - } or nil - } - }, - { - n = G.UIT.C, - config = { - align = "cm", - minw = 0.2 - }, - nodes = {} - }, - UIBox_button{ - button = 'view_code', - colour = G.C.PALE_GREEN, - minw = 3.15, - minh = 1.35, - label = {'VIEW CODE'}, - scale = text_scale * 1.2, - col = true - }, - } - }, - UIBox_button{ - id = 'lobby_menu_leave', - button = "lobby_leave", - colour = G.C.RED, - minw = 3.65, - minh = 1.55, - label = {'LEAVE'}, - scale = text_scale*1.5, - col = true - }, - } - }, - }}, - }} - return t + local t = { + n = G.UIT.ROOT, + config = { + align = "cm", + colour = G.C.CLEAR, + }, + nodes = { + { + n = G.UIT.C, + config = { + align = "bm", + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm", + padding = 0.2, + r = 0.1, + emboss = 0.1, + colour = G.C.L_BLACK, + mid = true, + }, + nodes = { + Disableable_Button({ + id = "lobby_menu_start", + button = "lobby_setup_run", + colour = G.C.BLUE, + minw = 3.65, + minh = 1.55, + label = { "START" }, + disabled_text = { "WAITING FOR", "HOST TO START" }, + scale = text_scale * 2, + col = true, + disable_ref_table = Lobby, + disable_ref_value = "is_host", + }), + { + n = G.UIT.C, + config = { + align = "cm", + }, + nodes = { + Disableable_Button({ + button = "lobby_options", + colour = G.C.ORANGE, + minw = 3.15, + minh = 1.35, + label = { "LOBBY OPTIONS" }, + scale = text_scale * 1.2, + col = true, + disable_ref_table = Lobby, + disable_ref_value = "is_host", + }), + { + n = G.UIT.C, + config = { + align = "cm", + minw = 0.2, + }, + nodes = {}, + }, + { + n = G.UIT.C, + config = { + align = "tm", + minw = 2.65, + }, + nodes = { + { + n = G.UIT.R, + config = { + padding = 0.2, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, + config = { + text = "Connected Players:", + shadow = true, + scale = text_scale * 0.8, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + }, + Lobby.host and Lobby.host.username and { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, + config = { + ref_table = Lobby.host, + ref_value = "username", + shadow = true, + scale = text_scale * 0.8, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + } or nil, + Lobby.guest and Lobby.guest.username and { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, + config = { + ref_table = Lobby.guest, + ref_value = "username", + shadow = true, + scale = text_scale * 0.8, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + } or nil, + }, + }, + { + n = G.UIT.C, + config = { + align = "cm", + minw = 0.2, + }, + nodes = {}, + }, + UIBox_button({ + button = "view_code", + colour = G.C.PALE_GREEN, + minw = 3.15, + minh = 1.35, + label = { "VIEW CODE" }, + scale = text_scale * 1.2, + col = true, + }), + }, + }, + UIBox_button({ + id = "lobby_menu_leave", + button = "lobby_leave", + colour = G.C.RED, + minw = 3.65, + minh = 1.55, + label = { "LEAVE" }, + scale = text_scale * 1.5, + col = true, + }), + }, + }, + }, + }, + }, + } + return t end local function get_lobby_main_menu_UI() @@ -383,11 +381,11 @@ local function get_lobby_main_menu_UI() align = "bmi", offset = { x = 0, - y = 10 + y = 10, }, major = G.ROOM_ATTACH, - bond = 'Weak' - } + bond = "Weak", + }, }) end @@ -396,7 +394,7 @@ function display_lobby_main_menu_UI() G.MAIN_MENU_UI.alignment.offset.y = 0 G.MAIN_MENU_UI:align_to_major() - G.CONTROLLER:snap_to { node = G.MAIN_MENU_UI:get_UIE_by_ID('lobby_menu_start') } + G.CONTROLLER:snap_to({ node = G.MAIN_MENU_UI:get_UIE_by_ID("lobby_menu_start") }) end function Lobby.update_player_usernames() diff --git a/Multiplayer/Main_Menu.lua b/Multiplayer/Main_Menu.lua index 6ef70008..a76c8734 100644 --- a/Multiplayer/Main_Menu.lua +++ b/Multiplayer/Main_Menu.lua @@ -4,9 +4,9 @@ ---------------------------------------------- ------------MOD MAIN MENU--------------------- -local Utils = require "Utils" -local Lobby = require "Lobby" -local ActionHandlers = require "Action_Handlers" +local Utils = require("Utils") +local Lobby = require("Lobby") +local ActionHandlers = require("Action_Handlers") MULTIPLAYER_VERSION = "0.1.0-MULTIPLAYER" @@ -18,7 +18,7 @@ function Game.main_menu(arg_280_0, arg_280_1) n = G.UIT.ROOT, config = { align = "cm", - colour = G.C.UI.TRANSPARENT_DARK + colour = G.C.UI.TRANSPARENT_DARK, }, nodes = { { @@ -26,340 +26,354 @@ function Game.main_menu(arg_280_0, arg_280_1) config = { scale = 0.3, text = MULTIPLAYER_VERSION, - colour = G.C.UI.TEXT_LIGHT - } - } - } + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, }, config = { align = "tri", bond = "Weak", offset = { x = 0, - y = 0.6 + y = 0.6, }, - major = G.ROOM_ATTACH - } + major = G.ROOM_ATTACH, + }, }) end function create_UIBox_create_lobby_button() local var_495_0 = 0.75 - return (create_UIBox_generic_options({ - back_func = "play_options", - contents = { - { - n = G.UIT.R, - config = { - padding = 0, - align = "cm" - }, - nodes = { - create_tabs({ - snap_to_nav = true, - colour = G.C.BOOSTER, - tabs = { - { - label = "Attrition (1v1)", - chosen = true, - tab_definition_function = function() - return { - n = G.UIT.ROOT, - config = { - emboss = 0.05, - minh = 6, - r = 0.1, - minw = 10, - align = "tm", - padding = 0.2, - colour = G.C.BLACK - }, - nodes = { - { - n = G.UIT.R, - config = { - align = "tm", - padding = 0.05, - minw = 4, - minh = 1.5 + return ( + create_UIBox_generic_options({ + back_func = "play_options", + contents = { + { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", + }, + nodes = { + create_tabs({ + snap_to_nav = true, + colour = G.C.BOOSTER, + tabs = { + { + label = "Attrition (1v1)", + chosen = true, + tab_definition_function = function() + return { + n = G.UIT.ROOT, + config = { + emboss = 0.05, + minh = 6, + r = 0.1, + minw = 10, + align = "tm", + padding = 0.2, + colour = G.C.BLACK, + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "tm", + padding = 0.05, + minw = 4, + minh = 1.5, + }, + nodes = { + { + n = G.UIT.T, + config = { + text = Utils.wrapText( + "Both players start with 4 lives, every boss round is a competition between players where the player with the lower score loses a life.", + 50 + ), + shadow = true, + scale = var_495_0 * 0.6, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, }, - nodes = { - { - n = G.UIT.T, - config = { - text = Utils.wrapText( - "Both players start with 4 lives, every boss round is a competition between players where the player with the lower score loses a life.", - 50), - shadow = true, - scale = var_495_0 * 0.6, - colour = G.C.UI.TEXT_LIGHT - } - } - } + create_toggle({ + label = "Lose lives on round loss", + ref_table = Lobby.config, + ref_value = "death_on_round_loss", + }), + create_toggle({ + label = "Different seeds", + ref_table = Lobby.config, + ref_value = "different_seeds", + }), + UIBox_button({ + label = { "Start Lobby" }, + colour = G.C.RED, + button = "start_lobby", + minw = 5, + }), }, - create_toggle({ - label = "Lose lives on round loss", - ref_table = Lobby.config, - ref_value = - 'death_on_round_loss' - }), - create_toggle({ label = "Different seeds", ref_table = Lobby.config, ref_value = 'different_seeds' }), - UIBox_button({ - label = { "Start Lobby" }, - colour = G.C.RED, - button = "start_lobby", - minw = 5, - }) } - } - end - }, - { - label = "Draft (1v1)", - tab_definition_function = function() - return { - n = G.UIT.ROOT, - config = { - emboss = 0.05, - minh = 6, - r = 0.1, - minw = 10, - align = "tm", - padding = 0.2, - colour = G.C.BLACK - }, - nodes = { - { - n = G.UIT.R, - config = { - align = "tm", - padding = 0.05, - minw = 4, - minh = 2.5 + end, + }, + { + label = "Draft (1v1)", + tab_definition_function = function() + return { + n = G.UIT.ROOT, + config = { + emboss = 0.05, + minh = 6, + r = 0.1, + minw = 10, + align = "tm", + padding = 0.2, + colour = G.C.BLACK, + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "tm", + padding = 0.05, + minw = 4, + minh = 2.5, + }, + nodes = { + { + n = G.UIT.T, + config = { + text = Utils.wrapText( + "Both players play a set amount of antes simultaneously, then they play an ante where every round the player with the higher scorer wins, player with the most round wins in the final ante is the victor.", + 50 + ), + shadow = true, + scale = var_495_0 * 0.6, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, }, - nodes = { - { - n = G.UIT.T, - config = { - text = Utils.wrapText( - "Both players play a set amount of antes simultaneously, then they play an ante where every round the player with the higher scorer wins, player with the most round wins in the final ante is the victor.", - 50), - shadow = true, - scale = var_495_0 * 0.6, - colour = G.C.UI.TEXT_LIGHT - } - } - } + UIBox_button({ + label = { "Coming Soon!" }, + colour = G.C.RED, + minw = 5, + }), }, - UIBox_button({ - label = { "Coming Soon!" }, - colour = G.C.RED, - minw = 5, - }) } - } - end - }, - { - label = "Heads Up (1v1)", - tab_definition_function = function() - return { - n = G.UIT.ROOT, - config = { - emboss = 0.05, - minh = 6, - r = 0.1, - minw = 10, - align = "tm", - padding = 0.2, - colour = G.C.BLACK - }, - nodes = { - { - n = G.UIT.R, - config = { - align = "tm", - padding = 0.05, - minw = 4, - minh = 1 + end, + }, + { + label = "Heads Up (1v1)", + tab_definition_function = function() + return { + n = G.UIT.ROOT, + config = { + emboss = 0.05, + minh = 6, + r = 0.1, + minw = 10, + align = "tm", + padding = 0.2, + colour = G.C.BLACK, + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "tm", + padding = 0.05, + minw = 4, + minh = 1, + }, + nodes = { + { + n = G.UIT.T, + config = { + text = Utils.wrapText( + "Both players play the first ante, then must keep beating the opponents previous score or lose.", + 50 + ), + shadow = true, + scale = var_495_0 * 0.6, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, }, - nodes = { - { - n = G.UIT.T, - config = { - text = Utils.wrapText( - "Both players play the first ante, then must keep beating the opponents previous score or lose.", - 50), - shadow = true, - scale = var_495_0 * 0.6, - colour = G.C.UI.TEXT_LIGHT - } - } - } + UIBox_button({ + label = { "Coming Soon!" }, + colour = G.C.RED, + minw = 5, + }), }, - UIBox_button({ - label = { "Coming Soon!" }, - colour = G.C.RED, - minw = 5, - }) } - } - end - }, - { - label = "Battle Royale (8p)", - tab_definition_function = function() - return { - n = G.UIT.ROOT, - config = { - emboss = 0.05, - minh = 6, - r = 0.1, - minw = 10, - align = "Tm", - padding = 0.2, - colour = G.C.BLACK - }, - nodes = { - { - n = G.UIT.R, - config = { - align = "tm", - padding = 0.05, - minw = 4, - minh = 1 + end, + }, + { + label = "Battle Royale (8p)", + tab_definition_function = function() + return { + n = G.UIT.ROOT, + config = { + emboss = 0.05, + minh = 6, + r = 0.1, + minw = 10, + align = "Tm", + padding = 0.2, + colour = G.C.BLACK, + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "tm", + padding = 0.05, + minw = 4, + minh = 1, + }, + nodes = { + { + n = G.UIT.T, + config = { + text = Utils.wrapText( + "Draft, except there are up to 8 players and every player only has 1 life.", + 50 + ), + shadow = true, + scale = var_495_0 * 0.6, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, }, - nodes = { - { - n = G.UIT.T, - config = { - text = Utils.wrapText( - "Draft, except there are up to 8 players and every player only has 1 life.", 50), - shadow = true, - scale = var_495_0 * 0.6, - colour = G.C.UI.TEXT_LIGHT - } - } - } + UIBox_button({ + label = { "Coming Soon!" }, + colour = G.C.RED, + minw = 5, + }), }, - UIBox_button({ - label = { "Coming Soon!" }, - colour = G.C.RED, - minw = 5, - }) } - } - end - } - } - }) - } - } - } - })) + end, + }, + }, + }), + }, + }, + }, + }) + ) end function create_UIBox_join_lobby_button() - return (create_UIBox_generic_options({ - back_func = "play_options", - contents = { - { - n = G.UIT.R, - config = { - padding = 0, - align = "cm", - }, - nodes = { - { - n = G.UIT.R, - config = { - padding = 0.5, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, - config = { - scale = 0.6, - shadow = true, - text = 'Lobby Code:', - colour = G.C.UI.TEXT_LIGHT - } - } - } + return ( + create_UIBox_generic_options({ + back_func = "play_options", + contents = { + { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", }, - { - n = G.UIT.R, - config = { - padding = 0.5, - align = "cm" + nodes = { + { + n = G.UIT.R, + config = { + padding = 0.5, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, + config = { + scale = 0.6, + shadow = true, + text = "Lobby Code:", + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + }, + { + n = G.UIT.R, + config = { + padding = 0.5, + align = "cm", + }, + nodes = { + create_text_input({ + w = 4, + h = 1, + max_length = 5, + prompt_text = "Enter Lobby Code", + ref_table = Lobby, + ref_value = "temp_code", + extended_corpus = false, + keyboard_offset = 1, + minw = 5, + callback = function(val) + ActionHandlers.join_lobby(Lobby.temp_code) + end, + }), + }, }, - nodes = { - create_text_input({ - w = 4, - h = 1, - max_length = 5, - prompt_text = "Enter Lobby Code", - ref_table = Lobby, - ref_value = 'temp_code', - extended_corpus = false, - keyboard_offset = 1, - minw = 5, - callback = function(val) - ActionHandlers.join_lobby(Lobby.temp_code) - end, - }) - } + UIBox_button({ + label = { "Paste From Clipboard" }, + colour = G.C.RED, + button = "join_from_clipboard", + minw = 5, + }), }, - UIBox_button({ - label = { "Paste From Clipboard" }, - colour = G.C.RED, - button = "join_from_clipboard", - minw = 5, - }), - } - } - } - })) + }, + }, + }) + ) end function override_main_menu_play_button() - return (create_UIBox_generic_options({ - contents = { - UIBox_button({ - label = { "Singleplayer" }, - colour = G.C.BLUE, - button = "setup_run", - minw = 5, - }), - Lobby.connected and UIBox_button({ - label = { "Create Lobby" }, - colour = G.C.GREEN, - button = "create_lobby", - minw = 5, - }) or nil, - Lobby.connected and UIBox_button({ - label = { "Join Lobby" }, - colour = G.C.RED, - button = "join_lobby", - minw = 5, - }) or nil, - not Lobby.connected and UIBox_button({ - label = { "Reconnect" }, - colour = G.C.RED, - button = "reconnect", - minw = 5, - }) or nil, - } - })) + return ( + create_UIBox_generic_options({ + contents = { + UIBox_button({ + label = { "Singleplayer" }, + colour = G.C.BLUE, + button = "setup_run", + minw = 5, + }), + Lobby.connected and UIBox_button({ + label = { "Create Lobby" }, + colour = G.C.GREEN, + button = "create_lobby", + minw = 5, + }) or nil, + Lobby.connected and UIBox_button({ + label = { "Join Lobby" }, + colour = G.C.RED, + button = "join_lobby", + minw = 5, + }) or nil, + not Lobby.connected and UIBox_button({ + label = { "Reconnect" }, + colour = G.C.RED, + button = "reconnect", + minw = 5, + }) or nil, + }, + }) + ) end function G.FUNCS.play_options(arg_736_0) G.SETTINGS.paused = true G.FUNCS.overlay_menu({ - definition = override_main_menu_play_button() + definition = override_main_menu_play_button(), }) end @@ -367,7 +381,7 @@ function G.FUNCS.create_lobby(arg_736_0) G.SETTINGS.paused = true G.FUNCS.overlay_menu({ - definition = create_UIBox_create_lobby_button() + definition = create_UIBox_create_lobby_button(), }) end @@ -375,7 +389,7 @@ function G.FUNCS.join_lobby(arg_736_0) G.SETTINGS.paused = true G.FUNCS.overlay_menu({ - definition = create_UIBox_join_lobby_button() + definition = create_UIBox_join_lobby_button(), }) end @@ -395,7 +409,7 @@ local create_UIBox_main_menu_buttonsRef = create_UIBox_main_menu_buttons function create_UIBox_main_menu_buttons() local menu = create_UIBox_main_menu_buttonsRef() menu.nodes[1].nodes[1].nodes[1].nodes[1].config.button = "play_options" - return (menu) + return menu end ---------------------------------------------- diff --git a/Multiplayer/Mod_Description.lua b/Multiplayer/Mod_Description.lua index 233e27ad..444035dc 100644 --- a/Multiplayer/Mod_Description.lua +++ b/Multiplayer/Mod_Description.lua @@ -3,54 +3,54 @@ ---------------------------------------------- ------------MOD DESCRIPTION------------------- -local Utils = require "Utils" -local Lobby = require "Lobby" +local Utils = require("Utils") +local Lobby = require("Lobby") Description = {} function Description.load_description_gui() - SMODS.registerUIElement("VirtualizedMultiplayer", { - { - n = G.UIT.R, - config = { - padding = 0.5, - align = "cm" - }, - nodes = { - { - n = G.UIT.T, + SMODS.registerUIElement("VirtualizedMultiplayer", { + { + n = G.UIT.R, + config = { + padding = 0.5, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, config = { scale = 0.6, - text = 'Username:', - colour = G.C.UI.TEXT_LIGHT - } - }, - create_text_input({ - w = 4, - max_length = 25, - prompt_text = "Enter Username", - ref_table = Lobby, - ref_value = 'username', - extended_corpus = true, - keyboard_offset = 1, - callback = function(val) - Utils.save_username(Lobby.username) - end - }), - { - n = G.UIT.T, + text = "Username:", + colour = G.C.UI.TEXT_LIGHT, + }, + }, + create_text_input({ + w = 4, + max_length = 25, + prompt_text = "Enter Username", + ref_table = Lobby, + ref_value = "username", + extended_corpus = true, + keyboard_offset = 1, + callback = function(val) + Utils.save_username(Lobby.username) + end, + }), + { + n = G.UIT.T, config = { scale = 0.3, - text = 'Press enter to save', - colour = G.C.UI.TEXT_LIGHT - } - } - } - } - }) + text = "Press enter to save", + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + }, + }) end return Description ---------------------------------------------- -------------MOD DESCRIPTION END--------------- \ No newline at end of file +------------MOD DESCRIPTION END--------------- diff --git a/Multiplayer/Networking.lua b/Multiplayer/Networking.lua index dbee6118..2f5bce5f 100644 --- a/Multiplayer/Networking.lua +++ b/Multiplayer/Networking.lua @@ -10,8 +10,8 @@ -- the necessary modules again local CONFIG_URL, CONFIG_PORT = ... -require 'love.filesystem' -SOCKET = require 'socket' +require("love.filesystem") +SOCKET = require("socket") -- Defining this again, for debugging this thread local function initializeThreadDebugSocketConnection() @@ -32,12 +32,13 @@ initializeThreadDebugSocketConnection() Networking = {} function Networking.connect() - SEND_THREAD_DEBUG_MESSAGE(string.format("Attempting to connect to multiplayer server... URL: %s, PORT: %d", CONFIG_URL, - CONFIG_PORT)) + SEND_THREAD_DEBUG_MESSAGE( + string.format("Attempting to connect to multiplayer server... URL: %s, PORT: %d", CONFIG_URL, CONFIG_PORT) + ) Networking.Client = SOCKET.tcp() - Networking.Client:setoption('tcp-nodelay', true) + Networking.Client:setoption("tcp-nodelay", true) local connectionResult, errorMessage = Networking.Client:connect(CONFIG_URL, CONFIG_PORT) -- Not sure if I want to make these values public yet if connectionResult ~= 1 then diff --git a/Multiplayer/Utils.lua b/Multiplayer/Utils.lua index 8210b9e2..c6cd60df 100644 --- a/Multiplayer/Utils.lua +++ b/Multiplayer/Utils.lua @@ -4,7 +4,7 @@ ---------------------------------------------- ------------MOD UTILS------------------------- -local ActionHandlers = require "Action_Handlers" +local ActionHandlers = require("Action_Handlers") Utils = {} @@ -24,13 +24,18 @@ function Utils.serialize_table(val, name, skipnewlines, depth) local tmp = string.rep(" ", depth) - if name then tmp = tmp .. name .. " = " end + if name then + tmp = tmp .. name .. " = " + end if type(val) == "table" then tmp = tmp .. "{" .. (not skipnewlines and "\n" or "") for k, v in pairs(val) do - tmp = tmp .. Utils.serialize_table(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and "\n" or "") + tmp = tmp + .. Utils.serialize_table(v, k, skipnewlines, depth + 1) + .. "," + .. (not skipnewlines and "\n" or "") end tmp = tmp .. string.rep(" ", depth) .. "}" @@ -41,7 +46,7 @@ function Utils.serialize_table(val, name, skipnewlines, depth) elseif type(val) == "boolean" then tmp = tmp .. (val and "true" or "false") else - tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\"" + tmp = tmp .. '"[inserializeable datatype:' .. type(val) .. ']"' end return tmp @@ -54,10 +59,10 @@ function Utils.wrapText(text, maxChars) for word in text:gmatch("%S+") do if currentLineLength + #word <= maxChars then - wrappedText = wrappedText .. word .. ' ' + wrappedText = wrappedText .. word .. " " currentLineLength = currentLineLength + #word + 1 else - wrappedText = wrappedText .. '\n' .. word .. ' ' + wrappedText = wrappedText .. "\n" .. word .. " " currentLineLength = #word + 1 end end @@ -73,7 +78,9 @@ end function Utils.get_username() local fileContent = love.filesystem.read(usernameFilePath) - if not fileContent then return end + if not fileContent then + return + end Lobby.username = fileContent end @@ -123,13 +130,13 @@ function Utils.overlay_message(message) scale = 0.6, shadow = true, text = message, - colour = G.C.UI.TEXT_LIGHT - } - } - } - } - } - }) + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + }, + }, + }), }) end diff --git a/Multiplayer/example.Config.lua b/Multiplayer/example.Config.lua index cf757768..e708e76a 100644 --- a/Multiplayer/example.Config.lua +++ b/Multiplayer/example.Config.lua @@ -6,10 +6,10 @@ Config = {} -Config.URL = 'localhost' +Config.URL = "localhost" Config.PORT = 8788 return Config ---------------------------------------------- -------------MOD CONFIG END-------------------- \ No newline at end of file +------------MOD CONFIG END-------------------- From b9e2a809d07eeeb033ae83f1e6bb4a56aef23a06 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 15 Mar 2024 22:04:44 -0700 Subject: [PATCH 0014/1128] Fixed bug where lobby menu buttons were disabled for host (#12) * Added isHost value to action * Fixed Disableable_Button arg names --------- Co-authored-by: TGMM --- .gitignore | 2 +- Multiplayer/Action_Handlers.lua | 2 ++ Multiplayer/Disableable_Button.lua | 2 +- Multiplayer/Lobby.lua | 8 ++++---- Server/src/Lobby.ts | 3 +++ Server/src/actions.ts | 1 + 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 6e146d1a..40cf2355 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ node_modules .env Config.lua Server/dist -Multiplayer/.vscode \ No newline at end of file +.vscode diff --git a/Multiplayer/Action_Handlers.lua b/Multiplayer/Action_Handlers.lua index 8765c909..77112ed5 100644 --- a/Multiplayer/Action_Handlers.lua +++ b/Multiplayer/Action_Handlers.lua @@ -96,6 +96,8 @@ function Game.update(arg_298_0, arg_298_1) sendDebugMessage("Client got " .. parsedAction.action .. " message") + sendDebugMessage(msg) + if parsedAction.action == "connected" then action_connected() elseif parsedAction.action == "joinedLobby" then diff --git a/Multiplayer/Disableable_Button.lua b/Multiplayer/Disableable_Button.lua index cfaa89dd..f656807a 100644 --- a/Multiplayer/Disableable_Button.lua +++ b/Multiplayer/Disableable_Button.lua @@ -6,7 +6,7 @@ function Disableable_Button(args) local enabled_table = args.enabled_ref_table or {} - local enabled = enabled_table[args.disable_ref_value] + local enabled = enabled_table[args.enabled_ref_value] args.colour = args.colour or G.C.RED args.text_colour = args.text_colour or G.C.UI.TEXT_LIGHT args.label = not enabled and args.disabled_text or args.label diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index cf8ae680..9ecac8bd 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -243,8 +243,8 @@ local function create_UIBox_lobby_menu() disabled_text = { "WAITING FOR", "HOST TO START" }, scale = text_scale * 2, col = true, - disable_ref_table = Lobby, - disable_ref_value = "is_host", + enabled_ref_table = Lobby, + enabled_ref_value = "is_host", }), { n = G.UIT.C, @@ -260,8 +260,8 @@ local function create_UIBox_lobby_menu() label = { "LOBBY OPTIONS" }, scale = text_scale * 1.2, col = true, - disable_ref_table = Lobby, - disable_ref_value = "is_host", + enabled_ref_table = Lobby, + enabled_ref_value = "is_host", }), { n = G.UIT.C, diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index 34216644..1a825113 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -73,6 +73,7 @@ class Lobby { const action: ActionLobbyInfo = { action: 'lobbyInfo', host: this.host.username, + isHost: false, } if (this.guest?.username) { @@ -80,6 +81,8 @@ class Lobby { this.guest.send(serializeAction(action)) } + // Should only sent true to the host + action.isHost = true this.host.send(serializeAction(action)) } } diff --git a/Server/src/actions.ts b/Server/src/actions.ts index 8bae7ba9..b1f07cb4 100644 --- a/Server/src/actions.ts +++ b/Server/src/actions.ts @@ -6,6 +6,7 @@ export type ActionLobbyInfo = { action: 'lobbyInfo' host: string guest?: string + isHost: boolean } export type ActionStopGame = { action: 'stopGame' } export type ActionStartGame = { From 8c9b51198be1cda14c6eec93c6bf72b8f33f669b Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 16 Mar 2024 12:11:47 -0700 Subject: [PATCH 0015/1128] Refactored lua files to use globals instead of imports and split Lobby into state management and UI (#14) --- Multiplayer/Action_Handlers.lua | 119 ----- .../{ => Components}/Disableable_Button.lua | 0 Multiplayer/Core.lua | 19 +- Multiplayer/{ => Items}/Blind.lua | 2 +- Multiplayer/{ => Items}/Deck.lua | 0 Multiplayer/Lobby.lua | 414 +----------------- Multiplayer/Networking/Action_Handlers.lua | 114 +++++ .../{Networking.lua => Networking/Socket.lua} | 5 +- Multiplayer/{ => UI}/Game_UI.lua | 35 +- Multiplayer/UI/Lobby_UI.lua | 393 +++++++++++++++++ Multiplayer/{ => UI}/Main_Menu.lua | 52 +-- Multiplayer/{ => UI}/Mod_Description.lua | 6 +- Multiplayer/Utils.lua | 7 +- 13 files changed, 589 insertions(+), 577 deletions(-) delete mode 100644 Multiplayer/Action_Handlers.lua rename Multiplayer/{ => Components}/Disableable_Button.lua (100%) rename Multiplayer/{ => Items}/Blind.lua (98%) rename Multiplayer/{ => Items}/Deck.lua (100%) create mode 100644 Multiplayer/Networking/Action_Handlers.lua rename Multiplayer/{Networking.lua => Networking/Socket.lua} (92%) rename Multiplayer/{ => UI}/Game_UI.lua (96%) create mode 100644 Multiplayer/UI/Lobby_UI.lua rename Multiplayer/{ => UI}/Main_Menu.lua (88%) rename Multiplayer/{ => UI}/Mod_Description.lua (91%) diff --git a/Multiplayer/Action_Handlers.lua b/Multiplayer/Action_Handlers.lua deleted file mode 100644 index 77112ed5..00000000 --- a/Multiplayer/Action_Handlers.lua +++ /dev/null @@ -1,119 +0,0 @@ ---- STEAMODDED HEADER ---- STEAMODDED SECONDARY FILE - ----------------------------------------------- -------------MOD ACTION_HANDLERS-------------------- -local Lobby = require("Lobby") - -ActionHandlers = {} -ActionHandlers.Client = {} - -function ActionHandlers.Client.send(msg) - love.thread.getChannel("uiToNetwork"):push(msg) -end - --- Server to Client -function ActionHandlers.set_username(username) - Lobby.username = username or "Guest" - if Lobby.connected then - ActionHandlers.Client.send("action:username,username:" .. Lobby.username) - end -end - -local function action_connected() - sendDebugMessage("Client connected to multiplayer server") - Lobby.connected = true - Lobby.update_connection_status() - ActionHandlers.Client.send("action:username,username:" .. Lobby.username) -end - -local function action_joinedLobby(code) - sendDebugMessage("Joining lobby " .. code) - Lobby.code = code - ActionHandlers.lobby_info() - Lobby.update_connection_status() -end - -local function action_lobbyInfo(host, guest, is_host) - Lobby.players = {} - Lobby.is_host = is_host == "true" - Lobby.host = { username = host } - if guest ~= nil then - Lobby.guest = { username = guest } - end - Lobby.update_player_usernames() -end - -local function action_error(message) - sendDebugMessage(message) - - Utils.overlay_message(message) -end - -local function action_keep_alive() - ActionHandlers.Client.send("action:keepAliveAck") -end - --- Client to Server -function ActionHandlers.create_lobby() - -- TODO: This is hardcoded to attrition for now, must be changed - ActionHandlers.Client.send("action:createLobby,gameMode:attrition") -end - -function ActionHandlers.join_lobby(code) - ActionHandlers.Client.send("action:joinLobby,code:" .. code) -end - -function ActionHandlers.lobby_info() - ActionHandlers.Client.send("action:lobbyInfo") -end - -function ActionHandlers.leave_lobby() - ActionHandlers.Client.send("action:leaveLobby") -end - --- Utils -function ActionHandlers.connect() - ActionHandlers.Client.send("connect") -end - -local function string_to_table(str) - local tbl = {} - for key, value in string.gmatch(str, "([^,]+):([^,]+)") do - tbl[key] = value - end - return tbl -end - -local game_update_ref = Game.update -function Game.update(arg_298_0, arg_298_1) - game_update_ref(arg_298_0, arg_298_1) - - repeat - local msg = love.thread.getChannel("networkToUi"):pop() - if msg then - local parsedAction = string_to_table(msg) - - sendDebugMessage("Client got " .. parsedAction.action .. " message") - - sendDebugMessage(msg) - - if parsedAction.action == "connected" then - action_connected() - elseif parsedAction.action == "joinedLobby" then - action_joinedLobby(parsedAction.code) - elseif parsedAction.action == "lobbyInfo" then - action_lobbyInfo(parsedAction.host, parsedAction.guest, parsedAction.isHost) - elseif parsedAction.action == "error" then - action_error(parsedAction.message) - elseif parsedAction.action == "keepAlive" then - action_keep_alive() - end - end - until not msg -end - -return ActionHandlers - ----------------------------------------------- -------------MOD ACTION_HANDLERS END---------------- diff --git a/Multiplayer/Disableable_Button.lua b/Multiplayer/Components/Disableable_Button.lua similarity index 100% rename from Multiplayer/Disableable_Button.lua rename to Multiplayer/Components/Disableable_Button.lua diff --git a/Multiplayer/Core.lua b/Multiplayer/Core.lua index 3b2ffb1c..41541421 100644 --- a/Multiplayer/Core.lua +++ b/Multiplayer/Core.lua @@ -28,20 +28,23 @@ local function customLoader(moduleName) end function SMODS.INIT.VirtualizedMultiplayer() + ---@diagnostic disable-next-line: deprecated table.insert(package.loaders, 1, customLoader) - require("Blind") - require("Deck") - require("Main_Menu") + require("Items.Blind") + require("Items.Deck") + require("Lobby") + require("Networking.Action_Handlers") require("Utils").get_username() - require("Action_Handlers") - require("Mod_Description").load_description_gui() - require("Game_UI") + require("UI.Lobby_UI") + require("UI.Main_Menu") + require("UI.Mod_Description").load_description_gui() + require("UI.Game_UI") CONFIG = require("Config") - NETWORKING_THREAD = love.thread.newThread(relativeModPath .. "Networking.lua") + NETWORKING_THREAD = love.thread.newThread(string.format("%sNetworking/Socket.lua", relativeModPath)) NETWORKING_THREAD:start(CONFIG.URL, CONFIG.PORT) - ActionHandlers.connect() + G.MULTIPLAYER.connect() end ---------------------------------------------- diff --git a/Multiplayer/Blind.lua b/Multiplayer/Items/Blind.lua similarity index 98% rename from Multiplayer/Blind.lua rename to Multiplayer/Items/Blind.lua index ed114486..85564b3f 100644 --- a/Multiplayer/Blind.lua +++ b/Multiplayer/Items/Blind.lua @@ -22,7 +22,7 @@ G.P_BLINDS["bl_pvp"] = bl_pvp local get_new_boss_ref = get_new_boss function get_new_boss() - if Lobby.code then + if G.LOBBY.code then return "bl_pvp" else local boss = get_new_boss_ref() diff --git a/Multiplayer/Deck.lua b/Multiplayer/Items/Deck.lua similarity index 100% rename from Multiplayer/Deck.lua rename to Multiplayer/Items/Deck.lua diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 9ecac8bd..3e0b53b1 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -4,9 +4,9 @@ ---------------------------------------------- ------------MOD LOBBY------------------------- -local Disableable_Button = require("Disableable_Button") +G.MULTIPLAYER = {} -Lobby = { +G.LOBBY = { connected = false, temp_code = "", code = nil, @@ -14,417 +14,39 @@ Lobby = { config = {}, username = "Guest", host = {}, - guest = nil, + guest = {}, is_host = false, } -Connection_Status_UI = nil - -local function get_connection_status_ui() - return UIBox({ - definition = { - n = G.UIT.ROOT, - config = { - align = "cm", - colour = G.C.UI.TRANSPARENT_DARK, - }, - nodes = { - { - n = G.UIT.T, - config = { - scale = 0.3, - text = (Lobby.code and "In Lobby") - or (Lobby.connected and "Connected to Service") - or "WARN: Cannot Find Multiplayer Service", - colour = G.C.UI.TEXT_LIGHT, - }, - }, - }, - }, - config = { - align = "tri", - bond = "Weak", - offset = { - x = 0, - y = 0.9, - }, - major = G.ROOM_ATTACH, - }, - }) -end - -function Lobby.update_connection_status() - if Connection_Status_UI then - Connection_Status_UI:remove() +function G.MULTIPLAYER.update_connection_status() + if G.HUD_connection_status then + G.HUD_connection_status:remove() end - Connection_Status_UI = get_connection_status_ui() + G.HUD_connection_status = G.UIDEF.get_connection_status_ui() end local gameMainMenuRef = Game.main_menu -function Game.main_menu(arg_280_0, arg_280_1) - Connection_Status_UI = get_connection_status_ui() - gameMainMenuRef(arg_280_0, arg_280_1) +---@diagnostic disable-next-line: duplicate-set-field +function Game:main_menu(change_context) + G.MULTIPLAYER.update_connection_status() + gameMainMenuRef(self, change_context) end -function G.FUNCS.copy_to_clipboard(arg_736_0) - Utils.copy_to_clipboard(Lobby.code) +function G.FUNCS.copy_to_clipboard(e) + Utils.copy_to_clipboard(G.LOBBY.code) end -function G.FUNCS.reconnect(arg_736_0) - ActionHandlers.connect() +function G.FUNCS.reconnect(e) + G.MULTIPLAYER.connect() G.FUNCS:exit_overlay_menu() end -function create_UIBox_view_code() - local var_495_0 = 0.75 - - return ( - create_UIBox_generic_options({ - contents = { - { - n = G.UIT.R, - config = { - padding = 0, - align = "cm", - }, - nodes = { - { - n = G.UIT.R, - config = { - padding = 0.5, - align = "cm", - }, - nodes = { - { - n = G.UIT.T, - config = { - text = Lobby.code, - shadow = true, - scale = var_495_0 * 0.6, - colour = G.C.UI.TEXT_LIGHT, - }, - }, - }, - }, - { - n = G.UIT.R, - config = { - padding = 0, - align = "cm", - }, - nodes = { - UIBox_button({ - label = { "Copy to Clipboard" }, - colour = G.C.BLUE, - button = "copy_to_clipboard", - minw = 5, - }), - }, - }, - }, - }, - }, - }) - ) -end - -function G.FUNCS.lobby_setup_run(arg_736_0) - G.FUNCS.start_run(arg_736_0, { - stake = 1, - challenge = { - name = "Multiplayer Deck", - id = "c_multiplayer_1", - rules = { - custom = {}, - modifiers = {}, - }, - jokers = {}, - consumeables = {}, - vouchers = {}, - deck = { - type = "Challenge Deck", - }, - restrictions = { - banned_cards = { - { id = "j_diet_cola" }, -- Intention to disable skipping - { id = "j_mr_bones" }, - { id = "v_hieroglyph" }, - { id = "v_petroglyph" }, - }, - banned_tags = {}, - banned_other = {}, - }, - }, - }) -end - -function G.FUNCS.lobby_options(arg_736_0) - G.FUNCS.overlay_menu({ - definition = create_UIBox_generic_options({ - contents = { - { - n = G.UIT.R, - config = { - padding = 0, - align = "cm", - }, - nodes = { - { - n = G.UIT.R, - config = { - padding = 0.5, - align = "cm", - }, - nodes = { - { - n = G.UIT.T, - config = { - text = "Not Implemented Yet", - shadow = true, - scale = 0.6, - colour = G.C.UI.TEXT_LIGHT, - }, - }, - }, - }, - }, - }, - }, - }), - }) -end - -function G.FUNCS.view_code(arg_736_0) - G.FUNCS.overlay_menu({ - definition = create_UIBox_view_code(), - }) -end - -function G.FUNCS.lobby_leave(arg_736_0) - Lobby.code = nil - ActionHandlers.leave_lobby() - Lobby.update_connection_status() -end - -local function create_UIBox_lobby_menu() - local text_scale = 0.45 - - local t = { - n = G.UIT.ROOT, - config = { - align = "cm", - colour = G.C.CLEAR, - }, - nodes = { - { - n = G.UIT.C, - config = { - align = "bm", - }, - nodes = { - { - n = G.UIT.R, - config = { - align = "cm", - padding = 0.2, - r = 0.1, - emboss = 0.1, - colour = G.C.L_BLACK, - mid = true, - }, - nodes = { - Disableable_Button({ - id = "lobby_menu_start", - button = "lobby_setup_run", - colour = G.C.BLUE, - minw = 3.65, - minh = 1.55, - label = { "START" }, - disabled_text = { "WAITING FOR", "HOST TO START" }, - scale = text_scale * 2, - col = true, - enabled_ref_table = Lobby, - enabled_ref_value = "is_host", - }), - { - n = G.UIT.C, - config = { - align = "cm", - }, - nodes = { - Disableable_Button({ - button = "lobby_options", - colour = G.C.ORANGE, - minw = 3.15, - minh = 1.35, - label = { "LOBBY OPTIONS" }, - scale = text_scale * 1.2, - col = true, - enabled_ref_table = Lobby, - enabled_ref_value = "is_host", - }), - { - n = G.UIT.C, - config = { - align = "cm", - minw = 0.2, - }, - nodes = {}, - }, - { - n = G.UIT.C, - config = { - align = "tm", - minw = 2.65, - }, - nodes = { - { - n = G.UIT.R, - config = { - padding = 0.2, - align = "cm", - }, - nodes = { - { - n = G.UIT.T, - config = { - text = "Connected Players:", - shadow = true, - scale = text_scale * 0.8, - colour = G.C.UI.TEXT_LIGHT, - }, - }, - }, - }, - Lobby.host and Lobby.host.username and { - n = G.UIT.R, - config = { - padding = 0, - align = "cm", - }, - nodes = { - { - n = G.UIT.T, - config = { - ref_table = Lobby.host, - ref_value = "username", - shadow = true, - scale = text_scale * 0.8, - colour = G.C.UI.TEXT_LIGHT, - }, - }, - }, - } or nil, - Lobby.guest and Lobby.guest.username and { - n = G.UIT.R, - config = { - padding = 0, - align = "cm", - }, - nodes = { - { - n = G.UIT.T, - config = { - ref_table = Lobby.guest, - ref_value = "username", - shadow = true, - scale = text_scale * 0.8, - colour = G.C.UI.TEXT_LIGHT, - }, - }, - }, - } or nil, - }, - }, - { - n = G.UIT.C, - config = { - align = "cm", - minw = 0.2, - }, - nodes = {}, - }, - UIBox_button({ - button = "view_code", - colour = G.C.PALE_GREEN, - minw = 3.15, - minh = 1.35, - label = { "VIEW CODE" }, - scale = text_scale * 1.2, - col = true, - }), - }, - }, - UIBox_button({ - id = "lobby_menu_leave", - button = "lobby_leave", - colour = G.C.RED, - minw = 3.65, - minh = 1.55, - label = { "LEAVE" }, - scale = text_scale * 1.5, - col = true, - }), - }, - }, - }, - }, - }, - } - return t -end - -local function get_lobby_main_menu_UI() - return UIBox({ - definition = create_UIBox_lobby_menu(), - config = { - align = "bmi", - offset = { - x = 0, - y = 10, - }, - major = G.ROOM_ATTACH, - bond = "Weak", - }, - }) -end - -function display_lobby_main_menu_UI() - G.MAIN_MENU_UI = get_lobby_main_menu_UI() - G.MAIN_MENU_UI.alignment.offset.y = 0 - G.MAIN_MENU_UI:align_to_major() - - G.CONTROLLER:snap_to({ node = G.MAIN_MENU_UI:get_UIE_by_ID("lobby_menu_start") }) -end - -function Lobby.update_player_usernames() - if Lobby.code then +function G.MULTIPLAYER.update_player_usernames() + if G.LOBBY.code then G.MAIN_MENU_UI:remove() - display_lobby_main_menu_UI() + G.FUNCS.display_lobby_main_menu_UI() end end -local setMainMenuUIRef = set_main_menu_UI -function set_main_menu_UI() - if Lobby.code then - display_lobby_main_menu_UI() - else - setMainMenuUIRef() - end -end - -local in_lobby = false -local gameUpdateRef = Game.update -function Game:update(arg_298_1) - if (Lobby.code and not in_lobby) or (not Lobby.code and in_lobby) then - in_lobby = not in_lobby - G.F_NO_SAVING = in_lobby - self.FUNCS.go_to_menu() - end - gameUpdateRef(self, arg_298_1) -end - -return Lobby - ---------------------------------------------- ------------MOD LOBBY END--------------------- diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua new file mode 100644 index 00000000..df728e32 --- /dev/null +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -0,0 +1,114 @@ +--- STEAMODDED HEADER +--- STEAMODDED SECONDARY FILE + +---------------------------------------------- +------------MOD ACTION HANDLERS--------------- + +Client = {} + +function Client.send(msg) + love.thread.getChannel("uiToNetwork"):push(msg) +end + +-- Server to Client +function G.MULTIPLAYER.set_username(username) + G.LOBBY.username = username or "Guest" + if G.LOBBY.connected then + Client.send(string.format("action:username,username:%s", G.LOBBY.username)) + end +end + +local function action_connected() + sendDebugMessage("Client connected to multiplayer server") + G.LOBBY.connected = true + G.MULTIPLAYER.update_connection_status() + Client.send(string.format("action:username,username:%s", G.LOBBY.username)) +end + +local function action_joinedLobby(code) + sendDebugMessage(string.format("Joining lobby %s", code)) + G.LOBBY.code = code + G.MULTIPLAYER.lobby_info() + G.MULTIPLAYER.update_connection_status() +end + +local function action_lobbyInfo(host, guest, is_host) + G.LOBBY.players = {} + G.LOBBY.is_host = is_host == "true" + G.LOBBY.host = { username = host } + if guest ~= nil then + G.LOBBY.guest = { username = guest } + end + G.MULTIPLAYER.update_player_usernames() +end + +local function action_error(message) + sendDebugMessage(message) + + Utils.overlay_message(message) +end + +local function action_keep_alive() + Client.send("action:keepAliveAck") +end + +-- Client to Server +function G.MULTIPLAYER.create_lobby() + -- TODO: This is hardcoded to attrition for now, must be changed + Client.send("action:createLobby,gameMode:attrition") +end + +function G.MULTIPLAYER.join_lobby(code) + Client.send(string.format("action:joinLobby,code:%s", code)) +end + +function G.MULTIPLAYER.lobby_info() + Client.send("action:lobbyInfo") +end + +function G.MULTIPLAYER.leave_lobby() + Client.send("action:leaveLobby") +end + +-- Utils +function G.MULTIPLAYER.connect() + Client.send("connect") +end + +local function string_to_table(str) + local tbl = {} + for key, value in string.gmatch(str, "([^,]+):([^,]+)") do + tbl[key] = value + end + return tbl +end + +local game_update_ref = Game.update +---@diagnostic disable-next-line: duplicate-set-field +function Game:update(dt) + game_update_ref(self, dt) + + repeat + local msg = love.thread.getChannel("networkToUi"):pop() + if msg then + local parsedAction = string_to_table(msg) + + sendDebugMessage(string.format("Client got %s message", parsedAction.action)) + + if parsedAction.action == "connected" then + action_connected() + elseif parsedAction.action == "joinedLobby" then + action_joinedLobby(parsedAction.code) + elseif parsedAction.action == "lobbyInfo" then + action_lobbyInfo(parsedAction.host, parsedAction.guest, parsedAction.isHost) + elseif parsedAction.action == "error" then + action_error(parsedAction.message) + elseif parsedAction.action == "keepAlive" then + action_keep_alive() + end + end + until not msg +end + +---------------------------------------------- +------------MOD ACTION HANDLERS END----------- diff --git a/Multiplayer/Networking.lua b/Multiplayer/Networking/Socket.lua similarity index 92% rename from Multiplayer/Networking.lua rename to Multiplayer/Networking/Socket.lua index 2f5bce5f..15b2a0dd 100644 --- a/Multiplayer/Networking.lua +++ b/Multiplayer/Networking/Socket.lua @@ -2,7 +2,7 @@ --- STEAMODDED SECONDARY FILE ---------------------------------------------- -------------MOD NETWORKING-------------------- +------------MOD SOCKET------------------------ -- Code for networking stuff that runs in a separate thread @@ -31,6 +31,7 @@ initializeThreadDebugSocketConnection() Networking = {} +---@diagnostic disable-next-line: duplicate-set-field function Networking.connect() SEND_THREAD_DEBUG_MESSAGE( string.format("Attempting to connect to multiplayer server... URL: %s, PORT: %d", CONFIG_URL, CONFIG_PORT) @@ -75,4 +76,4 @@ while true do end ---------------------------------------------- -------------MOD NETWORKING END---------------- +------------MOD SOCKET END-------------------- diff --git a/Multiplayer/Game_UI.lua b/Multiplayer/UI/Game_UI.lua similarity index 96% rename from Multiplayer/Game_UI.lua rename to Multiplayer/UI/Game_UI.lua index 7dbe7955..1d267404 100644 --- a/Multiplayer/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -4,13 +4,10 @@ ---------------------------------------------- ------------MOD GAME UI----------------------- -local Lobby = require("Lobby") - -Game_UI = {} - local create_UIBox_options_ref = create_UIBox_options +---@diagnostic disable-next-line: lowercase-global function create_UIBox_options() - if Lobby.code then + if G.LOBBY.code then local current_seed = nil local main_menu = nil local your_collection = nil @@ -128,8 +125,9 @@ function create_UIBox_options() end local create_UIBox_blind_choice_ref = create_UIBox_blind_choice +---@diagnostic disable-next-line: lowercase-global function create_UIBox_blind_choice(type, run_info) - if Lobby.code then + if G.LOBBY.code then if not G.GAME.blind_on_deck then G.GAME.blind_on_deck = "Small" end @@ -259,6 +257,7 @@ function create_UIBox_blind_choice(type, run_info) local blind_state = G.GAME.round_resets.blind_states[type] local _reward = true if G.GAME.modifiers.no_blind_reward and G.GAME.modifiers.no_blind_reward[type] then + ---@diagnostic disable-next-line: cast-local-type _reward = nil end if blind_state == "Select" then @@ -408,7 +407,7 @@ function create_UIBox_blind_choice(type, run_info) { n = G.UIT.O, config = { object = blind_choice.animation } }, }, }, - text_table[1] and { + text_table and text_table[1] and { n = G.UIT.R, config = { align = "cm", @@ -540,6 +539,7 @@ function create_UIBox_blind_choice(type, run_info) n = G.UIT.T, config = { text = string.rep( + ---@diagnostic disable-next-line: param-type-mismatch localize("$"), blind_choice.config.dollars ) .. "+", @@ -569,22 +569,22 @@ function create_UIBox_blind_choice(type, run_info) end end -function Game_UI.update_enemy() - if Lobby.code then +local function update_blind_HUD() + if G.LOBBY.code then G.HUD_blind.alignment.offset.y = -10 G.E_MANAGER:add_event(Event({ trigger = "after", delay = 0.3, blockable = false, func = function() - G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_table = Lobby.enemy + G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_table = G.LOBBY.enemy G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_value = "score" G.HUD_blind:get_UIE_by_ID("HUD_blind").children[2].children[2].children[2].children[1].children[1].config.text = "Current enemy score" G.HUD_blind:get_UIE_by_ID("HUD_blind").children[2].children[2].children[2].children[3].children[1].config.text = "Enemy hands left: " G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object.config.string = - { { ref_table = Lobby.enemy, ref_value = "hands" } } + { { ref_table = G.LOBBY.enemy, ref_value = "hands" } } G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object:update_text() G.HUD_blind.alignment.offset.y = 0 return true @@ -593,8 +593,8 @@ function Game_UI.update_enemy() end end -function Game_UI.reset_blind_HUD() - if Lobby.code then +local function reset_blind_HUD() + if G.LOBBY.code then G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object.config.string = { { ref_table = G.GAME.blind, ref_value = "loc_name" } } G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:update_text() @@ -612,7 +612,7 @@ end local update_draw_to_hand_ref = Game.update_draw_to_hand function Game:update_draw_to_hand(dt) - if Lobby.code then + if G.LOBBY.code then if not G.STATE_COMPLETE and G.GAME.current_round.hands_played == 0 @@ -626,14 +626,14 @@ function Game:update_draw_to_hand(dt) blockable = false, func = function() G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:pop_out(0) - Game_UI.update_enemy() + update_blind_HUD() G.E_MANAGER:add_event(Event({ trigger = "after", delay = 0.45, blockable = false, func = function() G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object.config.string = - { { ref_table = Lobby.enemy, ref_value = "username" } } + { { ref_table = G.LOBBY.is_host and G.LOBBY.guest or G.LOBBY.host, ref_value = "username" } } G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:update_text() G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:pop_in(0) return true @@ -651,9 +651,8 @@ end local blind_defeat_ref = Blind.defeat function Blind:defeat(silent) blind_defeat_ref(self, silent) - Game_UI.reset_blind_HUD() + reset_blind_HUD() end -return Game_UI ---------------------------------------------- ------------MOD GAME UI END------------------- diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua new file mode 100644 index 00000000..c5b8c8c7 --- /dev/null +++ b/Multiplayer/UI/Lobby_UI.lua @@ -0,0 +1,393 @@ +--- STEAMODDED HEADER +--- STEAMODDED SECONDARY FILE + +---------------------------------------------- +------------MOD LOBBY UI---------------------- + +local Disableable_Button = require("Components.Disableable_Button") + +G.HUD_connection_status = nil + +function G.UIDEF.get_connection_status_ui() + return UIBox({ + definition = { + n = G.UIT.ROOT, + config = { + align = "cm", + colour = G.C.UI.TRANSPARENT_DARK, + }, + nodes = { + { + n = G.UIT.T, + config = { + scale = 0.3, + text = (G.LOBBY.code and "In Lobby") + or (G.LOBBY.connected and "Connected to Service") + or "WARN: Cannot Find Multiplayer Service", + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + }, + config = { + align = "tri", + bond = "Weak", + offset = { + x = 0, + y = 0.9, + }, + major = G.ROOM_ATTACH, + }, + }) +end + +function G.UIDEF.create_UIBox_view_code() + local var_495_0 = 0.75 + + return ( + create_UIBox_generic_options({ + contents = { + { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", + }, + nodes = { + { + n = G.UIT.R, + config = { + padding = 0.5, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, + config = { + text = G.LOBBY.code, + shadow = true, + scale = var_495_0 * 0.6, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + }, + { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", + }, + nodes = { + UIBox_button({ + label = { "Copy to Clipboard" }, + colour = G.C.BLUE, + button = "copy_to_clipboard", + minw = 5, + }), + }, + }, + }, + }, + }, + }) + ) +end + +function G.UIDEF.create_UIBox_lobby_menu() + local text_scale = 0.45 + + local t = { + n = G.UIT.ROOT, + config = { + align = "cm", + colour = G.C.CLEAR, + }, + nodes = { + { + n = G.UIT.C, + config = { + align = "bm", + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm", + padding = 0.2, + r = 0.1, + emboss = 0.1, + colour = G.C.L_BLACK, + mid = true, + }, + nodes = { + Disableable_Button({ + id = "lobby_menu_start", + button = "lobby_setup_run", + colour = G.C.BLUE, + minw = 3.65, + minh = 1.55, + label = { "START" }, + disabled_text = { "WAITING FOR", "HOST TO START" }, + scale = text_scale * 2, + col = true, + enabled_ref_table = G.LOBBY, + enabled_ref_value = "is_host", + }), + { + n = G.UIT.C, + config = { + align = "cm", + }, + nodes = { + Disableable_Button({ + button = "lobby_options", + colour = G.C.ORANGE, + minw = 3.15, + minh = 1.35, + label = { "LOBBY OPTIONS" }, + scale = text_scale * 1.2, + col = true, + enabled_ref_table = G.LOBBY, + enabled_ref_value = "is_host", + }), + { + n = G.UIT.C, + config = { + align = "cm", + minw = 0.2, + }, + nodes = {}, + }, + { + n = G.UIT.C, + config = { + align = "tm", + minw = 2.65, + }, + nodes = { + { + n = G.UIT.R, + config = { + padding = 0.2, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, + config = { + text = "Connected Players:", + shadow = true, + scale = text_scale * 0.8, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + }, + G.LOBBY.host.username and { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, + config = { + ref_table = G.LOBBY.host, + ref_value = "username", + shadow = true, + scale = text_scale * 0.8, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + } or nil, + G.LOBBY.guest.username and { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, + config = { + ref_table = G.LOBBY.guest, + ref_value = "username", + shadow = true, + scale = text_scale * 0.8, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + } or nil, + }, + }, + { + n = G.UIT.C, + config = { + align = "cm", + minw = 0.2, + }, + nodes = {}, + }, + UIBox_button({ + button = "view_code", + colour = G.C.PALE_GREEN, + minw = 3.15, + minh = 1.35, + label = { "VIEW CODE" }, + scale = text_scale * 1.2, + col = true, + }), + }, + }, + UIBox_button({ + id = "lobby_menu_leave", + button = "lobby_leave", + colour = G.C.RED, + minw = 3.65, + minh = 1.55, + label = { "LEAVE" }, + scale = text_scale * 1.5, + col = true, + }), + }, + }, + }, + }, + }, + } + return t +end + +function G.UIDEF.create_UIBox_lobby_options() + return create_UIBox_generic_options({ + contents = { + { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", + }, + nodes = { + { + n = G.UIT.R, + config = { + padding = 0.5, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, + config = { + text = "Not Implemented Yet", + shadow = true, + scale = 0.6, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + }, + }, + }, + }, + }) +end + +function G.FUNCS.get_lobby_main_menu_UI(e) + return UIBox({ + definition = G.UIDEF.create_UIBox_lobby_menu(), + config = { + align = "bmi", + offset = { + x = 0, + y = 10, + }, + major = G.ROOM_ATTACH, + bond = "Weak", + }, + }) +end + +function G.FUNCS.lobby_setup_run(e) + G.FUNCS.start_run(e, { + stake = 1, + challenge = { + name = "Multiplayer Deck", + id = "c_multiplayer_1", + rules = { + custom = {}, + modifiers = {}, + }, + jokers = {}, + consumeables = {}, + vouchers = {}, + deck = { + type = "Challenge Deck", + }, + restrictions = { + banned_cards = { + { id = "j_diet_cola" }, -- Intention to disable skipping + { id = "j_mr_bones" }, + { id = "v_hieroglyph" }, + { id = "v_petroglyph" }, + }, + banned_tags = {}, + banned_other = {}, + }, + }, + }) +end + +function G.FUNCS.lobby_options(e) + G.FUNCS.overlay_menu({ + definition = G.UIDEF.create_UIBox_lobby_options(), + }) +end + +function G.FUNCS.view_code(e) + G.FUNCS.overlay_menu({ + definition = G.UIDEF.create_UIBox_view_code(), + }) +end + +function G.FUNCS.lobby_leave(e) + G.LOBBY.code = nil + G.MULTIPLAYER.leave_lobby() + G.MULTIPLAYER.update_connection_status() +end + +function G.FUNCS.display_lobby_main_menu_UI(e) + G.MAIN_MENU_UI = G.FUNCS.get_lobby_main_menu_UI(e) + G.MAIN_MENU_UI.alignment.offset.y = 0 + G.MAIN_MENU_UI:align_to_major() + + G.CONTROLLER:snap_to({ node = G.MAIN_MENU_UI:get_UIE_by_ID("lobby_menu_start") }) +end + +local set_main_menu_UI_ref = set_main_menu_UI +---@diagnostic disable-next-line: lowercase-global +function set_main_menu_UI() + if G.LOBBY.code then + G.FUNCS.display_lobby_main_menu_UI() + else + set_main_menu_UI_ref() + end +end + +local in_lobby = false +local gameUpdateRef = Game.update +---@diagnostic disable-next-line: duplicate-set-field +function Game:update(dt) + if (G.LOBBY.code and not in_lobby) or (not G.LOBBY.code and in_lobby) then + in_lobby = not in_lobby + G.F_NO_SAVING = in_lobby + self.FUNCS.go_to_menu() + end + gameUpdateRef(self, dt) +end + +---------------------------------------------- +------------MOD LOBBY UI END------------------ \ No newline at end of file diff --git a/Multiplayer/Main_Menu.lua b/Multiplayer/UI/Main_Menu.lua similarity index 88% rename from Multiplayer/Main_Menu.lua rename to Multiplayer/UI/Main_Menu.lua index a76c8734..7c1136e9 100644 --- a/Multiplayer/Main_Menu.lua +++ b/Multiplayer/UI/Main_Menu.lua @@ -5,14 +5,13 @@ ------------MOD MAIN MENU--------------------- local Utils = require("Utils") -local Lobby = require("Lobby") -local ActionHandlers = require("Action_Handlers") MULTIPLAYER_VERSION = "0.1.0-MULTIPLAYER" -local gameMainMenuRef = Game.main_menu -function Game.main_menu(arg_280_0, arg_280_1) - gameMainMenuRef(arg_280_0, arg_280_1) +local game_main_menu_ref = Game.main_menu +---@diagnostic disable-next-line: duplicate-set-field +function Game:main_menu(change_context) + game_main_menu_ref(self, change_context) UIBox({ definition = { n = G.UIT.ROOT, @@ -43,7 +42,7 @@ function Game.main_menu(arg_280_0, arg_280_1) }) end -function create_UIBox_create_lobby_button() +function G.UIDEF.create_UIBox_create_lobby_button() local var_495_0 = 0.75 return ( @@ -102,12 +101,12 @@ function create_UIBox_create_lobby_button() }, create_toggle({ label = "Lose lives on round loss", - ref_table = Lobby.config, + ref_table = G.LOBBY.config, ref_value = "death_on_round_loss", }), create_toggle({ label = "Different seeds", - ref_table = Lobby.config, + ref_table = G.LOBBY.config, ref_value = "different_seeds", }), UIBox_button({ @@ -270,7 +269,7 @@ function create_UIBox_create_lobby_button() ) end -function create_UIBox_join_lobby_button() +function G.UIDEF.create_UIBox_join_lobby_button() return ( create_UIBox_generic_options({ back_func = "play_options", @@ -312,13 +311,13 @@ function create_UIBox_join_lobby_button() h = 1, max_length = 5, prompt_text = "Enter Lobby Code", - ref_table = Lobby, + ref_table = G.LOBBY, ref_value = "temp_code", extended_corpus = false, keyboard_offset = 1, minw = 5, callback = function(val) - ActionHandlers.join_lobby(Lobby.temp_code) + G.MULTIPLAYER.join_lobby(G.LOBBY.temp_code) end, }), }, @@ -336,7 +335,7 @@ function create_UIBox_join_lobby_button() ) end -function override_main_menu_play_button() +function G.UIDEF.override_main_menu_play_button() return ( create_UIBox_generic_options({ contents = { @@ -346,19 +345,19 @@ function override_main_menu_play_button() button = "setup_run", minw = 5, }), - Lobby.connected and UIBox_button({ + G.LOBBY.connected and UIBox_button({ label = { "Create Lobby" }, colour = G.C.GREEN, button = "create_lobby", minw = 5, }) or nil, - Lobby.connected and UIBox_button({ + G.LOBBY.connected and UIBox_button({ label = { "Join Lobby" }, colour = G.C.RED, button = "join_lobby", minw = 5, }) or nil, - not Lobby.connected and UIBox_button({ + not G.LOBBY.connected and UIBox_button({ label = { "Reconnect" }, colour = G.C.RED, button = "reconnect", @@ -369,43 +368,44 @@ function override_main_menu_play_button() ) end -function G.FUNCS.play_options(arg_736_0) +function G.FUNCS.play_options(e) G.SETTINGS.paused = true G.FUNCS.overlay_menu({ - definition = override_main_menu_play_button(), + definition = G.UIDEF.override_main_menu_play_button(), }) end -function G.FUNCS.create_lobby(arg_736_0) +function G.FUNCS.create_lobby(e) G.SETTINGS.paused = true G.FUNCS.overlay_menu({ - definition = create_UIBox_create_lobby_button(), + definition = G.UIDEF.create_UIBox_create_lobby_button(), }) end -function G.FUNCS.join_lobby(arg_736_0) +function G.FUNCS.join_lobby(e) G.SETTINGS.paused = true G.FUNCS.overlay_menu({ - definition = create_UIBox_join_lobby_button(), + definition = G.UIDEF.create_UIBox_join_lobby_button(), }) end -function G.FUNCS.join_from_clipboard(arg_736_0) - Lobby.temp_code = Utils.get_from_clipboard() - ActionHandlers.join_lobby(Lobby.temp_code) +function G.FUNCS.join_from_clipboard(e) + G.LOBBY.temp_code = Utils.get_from_clipboard() + G.MULTIPLAYER.join_lobby(G.LOBBY.temp_code) end -function G.FUNCS.start_lobby(arg_736_0) +function G.FUNCS.start_lobby(e) G.SETTINGS.paused = false - ActionHandlers.create_lobby() + G.MULTIPLAYER.create_lobby() end -- Modify play button to take you to mode select first local create_UIBox_main_menu_buttonsRef = create_UIBox_main_menu_buttons +---@diagnostic disable-next-line: lowercase-global function create_UIBox_main_menu_buttons() local menu = create_UIBox_main_menu_buttonsRef() menu.nodes[1].nodes[1].nodes[1].nodes[1].config.button = "play_options" diff --git a/Multiplayer/Mod_Description.lua b/Multiplayer/UI/Mod_Description.lua similarity index 91% rename from Multiplayer/Mod_Description.lua rename to Multiplayer/UI/Mod_Description.lua index 444035dc..31e41e31 100644 --- a/Multiplayer/Mod_Description.lua +++ b/Multiplayer/UI/Mod_Description.lua @@ -3,8 +3,8 @@ ---------------------------------------------- ------------MOD DESCRIPTION------------------- + local Utils = require("Utils") -local Lobby = require("Lobby") Description = {} @@ -29,12 +29,12 @@ function Description.load_description_gui() w = 4, max_length = 25, prompt_text = "Enter Username", - ref_table = Lobby, + ref_table = G.LOBBY, ref_value = "username", extended_corpus = true, keyboard_offset = 1, callback = function(val) - Utils.save_username(Lobby.username) + Utils.save_username(G.LOBBY.username) end, }), { diff --git a/Multiplayer/Utils.lua b/Multiplayer/Utils.lua index c6cd60df..44e799a0 100644 --- a/Multiplayer/Utils.lua +++ b/Multiplayer/Utils.lua @@ -4,11 +4,10 @@ ---------------------------------------------- ------------MOD UTILS------------------------- -local ActionHandlers = require("Action_Handlers") - Utils = {} local localize_ref = localize +---@diagnostic disable-next-line: lowercase-global function localize(args, misc_cat) if args == nil then sendDebugMessage("Caught nil localize args, misc_cat: " .. misc_cat) @@ -72,7 +71,7 @@ end local usernameFilePath = "Mods/Multiplayer/Saved/username.txt" function Utils.save_username(text) - ActionHandlers.set_username(text) + G.MULTIPLAYER.set_username(text) love.filesystem.write(usernameFilePath, text) end @@ -81,7 +80,7 @@ function Utils.get_username() if not fileContent then return end - Lobby.username = fileContent + G.LOBBY.username = fileContent end function Utils.string_split(inputstr, sep) From c238ea62363f47bec03d8cca6f924800ff15e87d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Sat, 16 Mar 2024 12:56:43 -0700 Subject: [PATCH 0016/1128] Moved networking logic to coroutines (#13) * Moved networking logic to coroutines * Added documentation for disconnected action * Made coroutines more responsive --- Multiplayer/Networking/Action_Handlers.lua | 11 ++ Multiplayer/Networking/Socket.lua | 159 +++++++++++++++++---- Server/README.md | 5 + 3 files changed, 151 insertions(+), 24 deletions(-) diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index df728e32..04a4e23e 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -52,6 +52,15 @@ local function action_keep_alive() Client.send("action:keepAliveAck") end +local function action_disconnected() + G.LOBBY.connected = false + if G.LOBBY.code then + G.LOBBY.code = nil + G.FUNCS.go_to_menu() + end + G.MULTIPLAYER.update_connection_status() +end + -- Client to Server function G.MULTIPLAYER.create_lobby() -- TODO: This is hardcoded to attrition for now, must be changed @@ -97,6 +106,8 @@ function Game:update(dt) if parsedAction.action == "connected" then action_connected() + elseif parsedAction.action == "disconnected" then + action_disconnected() elseif parsedAction.action == "joinedLobby" then action_joinedLobby(parsedAction.code) elseif parsedAction.action == "lobbyInfo" then diff --git a/Multiplayer/Networking/Socket.lua b/Multiplayer/Networking/Socket.lua index 15b2a0dd..4df93fc8 100644 --- a/Multiplayer/Networking/Socket.lua +++ b/Multiplayer/Networking/Socket.lua @@ -11,11 +11,11 @@ local CONFIG_URL, CONFIG_PORT = ... require("love.filesystem") -SOCKET = require("socket") +local socket = require("socket") -- Defining this again, for debugging this thread local function initializeThreadDebugSocketConnection() - CLIENT = SOCKET.connect("localhost", 12346) + CLIENT = socket.connect("localhost", 12346) if not CLIENT then print("Failed to connect to the debug server") end @@ -27,52 +27,163 @@ function SEND_THREAD_DEBUG_MESSAGE(message) end end +-- TODO: Disable this if not in debug mode initializeThreadDebugSocketConnection() Networking = {} +local isSocketClosed = true +local networkToUiChannel = love.thread.getChannel("networkToUi") +local uiToNetworkChannel = love.thread.getChannel("uiToNetwork") ----@diagnostic disable-next-line: duplicate-set-field function Networking.connect() + -- TODO: Check first if Networking.Client is not null + -- and if it is, skip this function + SEND_THREAD_DEBUG_MESSAGE( string.format("Attempting to connect to multiplayer server... URL: %s, PORT: %d", CONFIG_URL, CONFIG_PORT) ) - Networking.Client = SOCKET.tcp() + Networking.Client = socket.tcp() + -- Allow for 10 seconds to reconnect + Networking.Client:settimeout(10) Networking.Client:setoption("tcp-nodelay", true) local connectionResult, errorMessage = Networking.Client:connect(CONFIG_URL, CONFIG_PORT) -- Not sure if I want to make these values public yet if connectionResult ~= 1 then SEND_THREAD_DEBUG_MESSAGE(string.format("%s", errorMessage)) + networkToUiChannel:push("action:error,message:Failed to connect to multiplayer server") + else + isSocketClosed = false end Networking.Client:settimeout(0) end --- TODO: Put this in a coroutine -while true do - -- Check for messages from the main thread - repeat - local msg = love.thread.getChannel("uiToNetwork"):pop() - if msg then - if msg:find("^action") ~= nil then - Networking.Client:send(msg .. "\n") - elseif msg == "connect" then - Networking.connect() +-- Check for messages from the main thread +local mainThreadMessageQueue = function() + -- Executes a max of requestsPerCycle action requests + -- from the main thread and then yields + local requestsPerCycle = 25 + while true do + for _ = 1, requestsPerCycle do + local msg = uiToNetworkChannel:pop() + if msg then + if msg:find("^action") ~= nil then + Networking.Client:send(msg .. "\n") + elseif msg == "connect" then + Networking.connect() + end + else + -- If there are no more messages, yield + coroutine.yield() end end - until not msg - - -- Do networking stuff - if Networking.Client then - repeat - local data, error, partial = Networking.Client:receive() - if data then - -- For now, we just send the string as is to the main thread - love.thread.getChannel("networkToUi"):push(data) + + coroutine.yield() + end +end +local mainThreadCoroutine = coroutine.create(mainThreadMessageQueue) + +local timer = function(time) + local init = os.time() + local diff = os.difftime(os.time(), init) + while diff < time do + coroutine.yield(diff) + diff = os.difftime(os.time(), init) + end +end +local timerCoroutine = coroutine.create(timer) + +-- All values are in seconds +local keepAliveInitialTimeout = 7 +local keepAliveRetryTimeout = 3 +local keepAliveRetryCount = 3 + +local isRetry = false +local retryCount = 0 + +-- Check for network packets +local networkPacketQueue = function() + local packetsPerCycle = 25 + while true do + if Networking.Client then + -- Tries to fetch a packet a max of packetsPerCycle times + -- and then yields + for _ = 1, packetsPerCycle do + local data, error, partial = Networking.Client:receive() + if data then + -- Packet arrived, reset retries + isRetry = false + retryCount = 0 + -- Also reset timer + timerCoroutine = coroutine.create(timer) + + -- For now, we just send the string as is to the main thread + networkToUiChannel:push(data) + elseif error == "close" then + -- Handle connection closed gracefully + isSocketClosed = true + retryCount = 0 + isRetry = false + + timerCoroutine = coroutine.create(timer) + networkToUiChannel:push("action:disconnected") + else + -- If there are no more packets, yield + coroutine.yield() + end end - until not data + + coroutine.yield() + end + + coroutine.yield() + end +end +local networkCoroutine = coroutine.create(networkPacketQueue) + +-- Checks for network packets, +-- then sends them to the main thread +-- then advances timers +-- and then sleeps +while true do + coroutine.resume(mainThreadCoroutine) + coroutine.resume(networkCoroutine) + + -- Run Timer + if isSocketClosed ~= true and coroutine.status(timerCoroutine) ~= "dead" then + coroutine.resume(timerCoroutine, keepAliveInitialTimeout) + else + -- Timer triggered + isRetry = true + + if retryCount > keepAliveRetryCount then + Networking.Client:close() + + -- Connection closed, restart everything + isSocketClosed = true + retryCount = 0 + isRetry = false + + timerCoroutine = coroutine.create(timer) + + networkToUiChannel:push("action:disconnected") + end + + if isRetry then + retryCount = retryCount + 1 + -- Send keepAlive without cutting the line + uiToNetworkChannel:push("action:keepAlive") + + -- Restart the timer + timerCoroutine = coroutine.create(timer) + coroutine.resume(timerCoroutine, keepAliveRetryTimeout) + end end + + -- Sleeps for 200 milliseconds + socket.sleep(0.2) end ---------------------------------------------- diff --git a/Server/README.md b/Server/README.md index 1131ed4c..e165c7a2 100644 --- a/Server/README.md +++ b/Server/README.md @@ -18,6 +18,11 @@ connected --- +disconnected +- Only sent from the network thread to the UI and never from the server. Indicates that the socket has gracefully closed or declared dead through the keepAlive mechanism. + +--- + error: message - An error, this should only be used when needed since it is very intrusive From 4e53e4f6d69bf44f3ef3962fa876bb65ef447838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Sat, 16 Mar 2024 18:29:23 -0700 Subject: [PATCH 0017/1128] Fixed a bug where player still shows up on lobby even when they leave (#15) --- Multiplayer/Networking/Action_Handlers.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index 04a4e23e..bb7f62e5 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -38,6 +38,8 @@ local function action_lobbyInfo(host, guest, is_host) G.LOBBY.host = { username = host } if guest ~= nil then G.LOBBY.guest = { username = guest } + else + G.LOBBY.guest = {} end G.MULTIPLAYER.update_player_usernames() end From 9cae6f94409fa7cf73985c9794657dd55fd8a103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Sun, 17 Mar 2024 16:04:32 -0700 Subject: [PATCH 0018/1128] Added ready up and implemented some actions (#16) * Added ability for players to start game * Added pseudo-random seed generator * Added ready up button * Added actions for player readiness * Changed player ready action names to match existing one * Implemented playHand and enemyInfo actions --- Multiplayer/Lobby.lua | 21 +++- Multiplayer/Networking/Action_Handlers.lua | 63 +++++++++- Multiplayer/UI/Game_UI.lua | 132 ++++++++++++++++----- Multiplayer/UI/Lobby_UI.lua | 17 ++- Server/README.md | 7 +- Server/src/Client.ts | 16 ++- Server/src/Lobby.ts | 13 +- Server/src/actionHandlers.ts | 88 +++++++++++++- Server/src/actions.ts | 2 + Server/src/main.ts | 62 ++++++++-- Server/src/utils.ts | 11 ++ 11 files changed, 374 insertions(+), 58 deletions(-) create mode 100644 Server/src/utils.ts diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 3e0b53b1..c7d379cc 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -18,7 +18,23 @@ G.LOBBY = { is_host = false, } +G.MULTIPLAYER_GAME = { + ready_blind = false, + ready_blind_text = "Ready", +} + +PREV_ACHIEVEMENT_VALUE = true function G.MULTIPLAYER.update_connection_status() + -- Save the previous value of the achievement flag + PREV_ACHIEVEMENT_VALUE = G.F_NO_ACHIEVEMENTS + if G.LOBBY.connected then + -- Disable achievements when connected to server + G.F_NO_ACHIEVEMENTS = true + else + -- Restore them when disconnected + G.F_NO_ACHIEVEMENTS = PREV_ACHIEVEMENT_VALUE + end + if G.HUD_connection_status then G.HUD_connection_status:remove() end @@ -43,7 +59,10 @@ end function G.MULTIPLAYER.update_player_usernames() if G.LOBBY.code then - G.MAIN_MENU_UI:remove() + if G.MAIN_MENU_UI then + G.MAIN_MENU_UI:remove() + end + G.FUNCS.display_lobby_main_menu_UI() end end diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index bb7f62e5..3940b8be 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -41,6 +41,9 @@ local function action_lobbyInfo(host, guest, is_host) else G.LOBBY.guest = {} end + -- TODO: This should check for player count instead + -- once we enable more than 2 players + G.LOBBY.ready_to_start = G.LOBBY.is_host and guest ~= nil G.MULTIPLAYER.update_player_usernames() end @@ -63,7 +66,40 @@ local function action_disconnected() G.MULTIPLAYER.update_connection_status() end --- Client to Server +---@param deck string +---@param seed string +---@param stake_str string +local function action_start_game(deck, seed, stake_str) + local stake = tonumber(stake_str) + G.FUNCS.lobby_start_run(nil, { deck = deck, seed = seed, stake = stake }) +end + +local function action_start_blind() + G.MULTIPLAYER_GAME.ready_blind = false + -- TODO: This should check that player is in a + -- multiplayer game + G.FUNCS.toggle_shop() +end + +---@param score_str string +---@param hands_left_str string +local function action_enemy_info(score_str, hands_left_str) + local score = tonumber(score_str) + local hands_left = tonumber(hands_left_str) + + if score == nil or hands_left == nil then + sendDebugMessage("Invalid score or hands_left") + return + end + + -- TODO: This is not working right now + if G.GAME.blind.boss then + -- Set blind chips to enemy score + G.GAME.blind.chips = score + end +end + +-- #region Client to Server function G.MULTIPLAYER.create_lobby() -- TODO: This is hardcoded to attrition for now, must be changed Client.send("action:createLobby,gameMode:attrition") @@ -81,6 +117,25 @@ function G.MULTIPLAYER.leave_lobby() Client.send("action:leaveLobby") end +function G.MULTIPLAYER.start_game() + Client.send("action:startGame") +end + +function G.MULTIPLAYER.ready_blind() + Client.send("action:readyBlind") +end + +function G.MULTIPLAYER.unready_blind() + Client.send("action:unreadyBlind") +end + +---@param score number +---@param hands_left number +function G.MULTIPLAYER.play_hand(score, hands_left) + Client.send(string.format("action:playHand,score:%d,handsLeft:%d", score, hands_left)) +end +-- #endregion Client to Server + -- Utils function G.MULTIPLAYER.connect() Client.send("connect") @@ -114,6 +169,12 @@ function Game:update(dt) action_joinedLobby(parsedAction.code) elseif parsedAction.action == "lobbyInfo" then action_lobbyInfo(parsedAction.host, parsedAction.guest, parsedAction.isHost) + elseif parsedAction.action == "startGame" then + action_start_game(parsedAction.deck, parsedAction.seed, parsedAction.stake) + elseif parsedAction.action == "startBlind" then + action_start_blind() + elseif parsedAction.action == "enemyInfo" then + action_enemy_info(parsedAction.score, parsedAction.handsLeft) elseif parsedAction.action == "error" then action_error(parsedAction.message) elseif parsedAction.action == "keepAlive" then diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 1d267404..6268d4a3 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -522,34 +522,38 @@ function create_UIBox_blind_choice(type, run_info) }, }, }, - _reward and { - n = G.UIT.R, - config = { align = "cm" }, - nodes = { - { - n = G.UIT.T, - config = { - text = localize("ph_blind_reward"), - scale = 0.35, - colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.WHITE, - shadow = not disabled, - }, - }, - { - n = G.UIT.T, - config = { - text = string.rep( - ---@diagnostic disable-next-line: param-type-mismatch - localize("$"), - blind_choice.config.dollars - ) .. "+", - scale = 0.35, - colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.MONEY, - shadow = not disabled, + _reward + and { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.T, + config = { + text = localize("ph_blind_reward"), + scale = 0.35, + colour = disabled and G.C.UI.TEXT_INACTIVE + or G.C.WHITE, + shadow = not disabled, + }, + }, + { + n = G.UIT.T, + config = { + text = string.rep( + ---@diagnostic disable-next-line: param-type-mismatch + localize("$"), + blind_choice.config.dollars + ) .. "+", + scale = 0.35, + colour = disabled and G.C.UI.TEXT_INACTIVE + or G.C.MONEY, + shadow = not disabled, + }, + }, }, - }, - }, - } or nil, + } + or nil, }, }, }, @@ -610,6 +614,25 @@ local function reset_blind_HUD() end end +function G.FUNCS.mp_toggle_ready(e) + G.MULTIPLAYER_GAME.ready_blind = not G.MULTIPLAYER_GAME.ready_blind + G.MULTIPLAYER_GAME.ready_blind_text = G.MULTIPLAYER_GAME.ready_blind and "Unready" or "Ready" + + if G.MULTIPLAYER_GAME.ready_blind then + G.MULTIPLAYER.ready_blind() + else + G.MULTIPLAYER.unready_blind() + end +end + +function G.FUNCS.mp_cfg_ready_blind_button(e) + -- Override next round button + e.config.ref_table = G.FUNCS + e.config.button = "mp_toggle_ready" + e.config.colour = G.MULTIPLAYER_GAME.ready_blind and G.C.GREEN or G.C.RED + e.config.one_press = false +end + local update_draw_to_hand_ref = Game.update_draw_to_hand function Game:update_draw_to_hand(dt) if G.LOBBY.code then @@ -632,8 +655,12 @@ function Game:update_draw_to_hand(dt) delay = 0.45, blockable = false, func = function() - G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object.config.string = - { { ref_table = G.LOBBY.is_host and G.LOBBY.guest or G.LOBBY.host, ref_value = "username" } } + G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object.config.string = { + { + ref_table = G.LOBBY.is_host and G.LOBBY.guest or G.LOBBY.host, + ref_value = "username", + }, + } G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:update_text() G.HUD_blind:get_UIE_by_ID("HUD_blind_name").config.object:pop_in(0) return true @@ -654,5 +681,52 @@ function Blind:defeat(silent) reset_blind_HUD() end +local ui_def_shop_ref = G.UIDEF.shop +---@diagnostic disable-next-line: duplicate-set-field +function G.UIDEF.shop() + -- Only modify the shop if not in a singleplayer game + if not G.LOBBY.connected or not G.LOBBY.code then + return ui_def_shop_ref() + end + + local t = ui_def_shop_ref() + + local inner_table = t.nodes[1].nodes[1].nodes[1].nodes + + local next_round_button = inner_table[1].nodes[1].nodes[1].nodes[1] + next_round_button.config.func = "mp_cfg_ready_blind_button" + + -- Text inside the button + next_round_button.nodes[1].nodes = { + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.T, + config = { + ref_table = G.MULTIPLAYER_GAME, + ref_value = "ready_blind_text", + scale = 0.65, + colour = G.C.WHITE, + shadow = true, + }, + }, + }, + }, + } + + return t +end + +local update_hand_played_ref = Game.update_hand_played +function Game:update_hand_played(dt) + if not G.STATE_COMPLETE then + G.MULTIPLAYER.play_hand(G.GAME.chips, G.GAME.current_round.hands_left) + end + + update_hand_played_ref(self, dt) +end + ---------------------------------------------- ------------MOD GAME UI END------------------- diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua index c5b8c8c7..683d3cde 100644 --- a/Multiplayer/UI/Lobby_UI.lua +++ b/Multiplayer/UI/Lobby_UI.lua @@ -123,16 +123,17 @@ function G.UIDEF.create_UIBox_lobby_menu() nodes = { Disableable_Button({ id = "lobby_menu_start", - button = "lobby_setup_run", + button = "lobby_start_game", colour = G.C.BLUE, minw = 3.65, minh = 1.55, label = { "START" }, - disabled_text = { "WAITING FOR", "HOST TO START" }, + disabled_text = G.LOBBY.is_host and { "WAITING FOR", "PLAYERS" } + or { "WAITING FOR", "HOST TO START" }, scale = text_scale * 2, col = true, enabled_ref_table = G.LOBBY, - enabled_ref_value = "is_host", + enabled_ref_value = "ready_to_start", }), { n = G.UIT.C, @@ -311,9 +312,11 @@ function G.FUNCS.get_lobby_main_menu_UI(e) }) end -function G.FUNCS.lobby_setup_run(e) +---@type fun(e: table | nil, args: { deck: string, stake: number | nil, seed: string | nil }) +function G.FUNCS.lobby_start_run(e, args) G.FUNCS.start_run(e, { stake = 1, + seed = args.seed, challenge = { name = "Multiplayer Deck", id = "c_multiplayer_1", @@ -341,6 +344,10 @@ function G.FUNCS.lobby_setup_run(e) }) end +function G.FUNCS.lobby_start_game(e) + G.MULTIPLAYER.start_game() +end + function G.FUNCS.lobby_options(e) G.FUNCS.overlay_menu({ definition = G.UIDEF.create_UIBox_lobby_options(), @@ -390,4 +397,4 @@ function Game:update(dt) end ---------------------------------------------- -------------MOD LOBBY UI END------------------ \ No newline at end of file +------------MOD LOBBY UI END------------------ diff --git a/Server/README.md b/Server/README.md index e165c7a2..b20e0b25 100644 --- a/Server/README.md +++ b/Server/README.md @@ -142,6 +142,11 @@ readyBlind --- +unreadyBlind +- Declare not ready to start next blind. + +--- + playHand: score, handsLeft - Client has played a hand. - score: The total score of all hands played in the blind so far, must be a number @@ -162,8 +167,6 @@ playerInfo enemyInfo - Request an enemyInfo update. ---- - ### Utility keepAlive diff --git a/Server/src/Client.ts b/Server/src/Client.ts index 76161342..14c9e60e 100644 --- a/Server/src/Client.ts +++ b/Server/src/Client.ts @@ -9,24 +9,30 @@ type SendFn = (data: string) => void type Address = net.AddressInfo | {} class Client { + // Connection info id: string // Could be useful later on to detect reconnects address: Address - username: string - lobby: Lobby | null send: SendFn + // Game info + username = 'Guest' + lobby: Lobby | null = null + /** Whether player is ready for next blind */ + isReady = false + // TODO: Set lives based on game mode + lives = 4 + score = 0 + constructor(address: Address, send: SendFn) { this.id = uuidv4() - this.lobby = null - this.username = 'Guest' this.address = address this.send = send } setUsername = (username: string) => { this.username = username - this.lobby?.broadcast() + this.lobby?.broadcastLobbyInfo() } setLobby = (lobby: Lobby | null) => { diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index 1a825113..c89d43d8 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -1,5 +1,5 @@ import type Client from './Client.js' -import type { ActionLobbyInfo } from './actions.js' +import type { Action, ActionLobbyInfo } from './actions.js' import { serializeAction } from './main.js' const Lobbies = new Map() @@ -45,7 +45,7 @@ class Lobby { if (this.host === null) { Lobbies.delete(this.code) } else { - this.broadcast() + this.broadcastLobbyInfo() } } @@ -62,10 +62,15 @@ class Lobby { this.guest = client client.setLobby(this) client.send(serializeAction({ action: 'joinedLobby', code: this.code })) - this.broadcast() + this.broadcastLobbyInfo() } - broadcast = () => { + broadcast = (action: Action) => { + this.host?.send(serializeAction(action)) + this.guest?.send(serializeAction(action)) + } + + broadcastLobbyInfo = () => { if (!this.host) { return } diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 29e20fb4..67ba4649 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -5,9 +5,11 @@ import type { ActionHandlerArgs, ActionHandlers, ActionJoinLobby, + ActionPlayHand, ActionUsername, } from './actions.js' import { serializeAction } from './main.js' +import { generateSeed } from './utils.js' const usernameAction = ( { username }: ActionHandlerArgs, @@ -46,7 +48,7 @@ const leaveLobbyAction = (client: Client) => { } const lobbyInfoAction = (client: Client) => { - client.lobby?.broadcast() + client.lobby?.broadcastLobbyInfo() } const keepAliveAction = (client: Client) => { @@ -54,12 +56,92 @@ const keepAliveAction = (client: Client) => { client.send(serializeAction({ action: 'keepAliveAck' })) } +const startGameAction = (client: Client) => { + // Only allow the host to start the game + if (!client.lobby || client.lobby.host?.id !== client.id) { + return + } + + // Hardcoded for testing + client.lobby.broadcast({ + action: 'startGame', + deck: 'c_multiplayer_1', + seed: generateSeed(), + }) +} + +const readyBlindAction = (client: Client) => { + client.isReady = true + + // TODO: Refactor for more than two players + if (client.lobby?.host?.isReady && client.lobby.guest?.isReady) { + // Reset ready status for next blind + client.lobby.host.isReady = false + client.lobby.guest.isReady = false + + client.lobby.broadcast({ action: 'startBlind' }) + } +} + +const unreadyBlindAction = (client: Client) => { + client.isReady = false +} + +const playHandAction = ( + { handsLeft, score }: ActionHandlerArgs, + client: Client, +) => { + if (!client.lobby) { + return + } + + // Should this be additive or just + // the latest score? + client.score = score + + const lobby = client.lobby + // Update the other party about the + // enemy's score and hands left + // TODO: Refactor for more than two players + if (lobby.host?.id === client.id) { + lobby.guest?.send( + serializeAction({ + action: 'enemyInfo', + handsLeft, + score, + }), + ) + } else if (lobby.guest?.id === client.id) { + lobby.host?.send( + serializeAction({ + action: 'enemyInfo', + handsLeft, + score, + }), + ) + } + + // TODO: This should check if this is the boss blind + if (lobby.host?.lives === 0 && lobby.guest?.lives === 0) { + const winner = + lobby.host.score > lobby.guest.score ? lobby.host : lobby.guest + const loser = winner.id === lobby.host.id ? lobby.guest : lobby.host + + winner.send(serializeAction({ action: 'endPvP', lost: false })) + loser.send(serializeAction({ action: 'endPvP', lost: true })) + } +} + // Declared partial for now untill all action handlers are defined -export const actionHandlers: Partial = { +export const actionHandlers = { username: usernameAction, createLobby: createLobbyAction, joinLobby: joinLobbyAction, lobbyInfo: lobbyInfoAction, leaveLobby: leaveLobbyAction, keepAlive: keepAliveAction, -} + startGame: startGameAction, + readyBlind: readyBlindAction, + unreadyBlind: unreadyBlindAction, + playHand: playHandAction, +} satisfies Partial diff --git a/Server/src/actions.ts b/Server/src/actions.ts index b1f07cb4..9beb546b 100644 --- a/Server/src/actions.ts +++ b/Server/src/actions.ts @@ -56,6 +56,7 @@ export type ActionLobbyInfoRequest = { action: 'lobbyInfo' } export type ActionStopGameRequest = { action: 'stopGame' } export type ActionStartGameRequest = { action: 'startGame' } export type ActionReadyBlind = { action: 'readyBlind' } +export type ActionUnreadyBlind = { action: 'unreadyBlind' } export type ActionPlayHand = { action: 'playHand' score: number @@ -78,6 +79,7 @@ export type ActionClientToServer = | ActionGameInfoRequest | ActionPlayerInfoRequest | ActionEnemyInfoRequest + | ActionUnreadyBlind // Utility actions export type ActionKeepAlive = { action: 'keepAlive' } diff --git a/Server/src/main.ts b/Server/src/main.ts index 23e9fdf2..95231d8e 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -1,7 +1,16 @@ import net from 'node:net' import Client from './Client.js' import { actionHandlers } from './actionHandlers.js' -import type { Action, ActionClientToServer, ActionUtility } from './actions.js' +import type { + Action, + ActionClientToServer, + ActionCreateLobby, + ActionHandlerArgs, + ActionJoinLobby, + ActionPlayHand, + ActionUsername, + ActionUtility, +} from './actions.js' const PORT = 8080 @@ -89,13 +98,50 @@ const server = net.createServer((socket) => { const { action, ...actionArgs } = message console.log(`Received action ${action} from ${client.id}`) - // This only works for now, once we add more arguments - // we'll need to refactor this - // Maybe add a context type that includes everything - // connection related? - Object.keys(actionArgs).length > 0 - ? actionHandlers[action]?.(actionArgs, client) - : actionHandlers[action]?.(client) + switch (action) { + case 'username': + actionHandlers.username( + actionArgs as ActionHandlerArgs, + client, + ) + break + case 'createLobby': + actionHandlers.createLobby( + actionArgs as ActionHandlerArgs, + client, + ) + break + case 'joinLobby': + actionHandlers.joinLobby( + actionArgs as ActionHandlerArgs, + client, + ) + break + case 'lobbyInfo': + actionHandlers.lobbyInfo(client) + break + case 'leaveLobby': + actionHandlers.leaveLobby(client) + break + case 'startGame': + actionHandlers.startGame(client) + break + case 'readyBlind': + actionHandlers.readyBlind(client) + break + case 'unreadyBlind': + actionHandlers.unreadyBlind(client) + break + case 'keepAlive': + actionHandlers.keepAlive(client) + break + case 'playHand': + actionHandlers.playHand( + actionArgs as ActionHandlerArgs, + client, + ) + break + } } catch (error) { const failedToParseError = 'Failed to parse message' console.error(failedToParseError, error) diff --git a/Server/src/utils.ts b/Server/src/utils.ts new file mode 100644 index 00000000..99acf425 --- /dev/null +++ b/Server/src/utils.ts @@ -0,0 +1,11 @@ +export function generateSeed(length = 5) { + let result = '' + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + const charactersLength = characters.length + let counter = 0 + while (counter < length) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)) + counter += 1 + } + return result +} From 7ae88520668ef766c20939365d3e119abc0dc158 Mon Sep 17 00:00:00 2001 From: TGMM Date: Sun, 17 Mar 2024 23:02:09 -0700 Subject: [PATCH 0019/1128] Fixed score and implemented stopGame --- Multiplayer/Lobby.lua | 6 +++- Multiplayer/Networking/Action_Handlers.lua | 18 +++++++--- Multiplayer/UI/Game_UI.lua | 42 ++++++++++++++++++++-- Server/src/Client.ts | 1 + Server/src/Lobby.ts | 6 ++-- Server/src/actionHandlers.ts | 11 +++++- 6 files changed, 72 insertions(+), 12 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index c7d379cc..8be1eb82 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -16,6 +16,10 @@ G.LOBBY = { host = {}, guest = {}, is_host = false, + enemy = { + score = 0, + hands = 5, + }, } G.MULTIPLAYER_GAME = { @@ -27,7 +31,7 @@ PREV_ACHIEVEMENT_VALUE = true function G.MULTIPLAYER.update_connection_status() -- Save the previous value of the achievement flag PREV_ACHIEVEMENT_VALUE = G.F_NO_ACHIEVEMENTS - if G.LOBBY.connected then + if G.LOBBY.connected and G.LOBBY.code then -- Disable achievements when connected to server G.F_NO_ACHIEVEMENTS = true else diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index 3940b8be..cdec0424 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -44,7 +44,10 @@ local function action_lobbyInfo(host, guest, is_host) -- TODO: This should check for player count instead -- once we enable more than 2 players G.LOBBY.ready_to_start = G.LOBBY.is_host and guest ~= nil - G.MULTIPLAYER.update_player_usernames() + + if G.STAGE == G.STAGES.MAIN_MENU then + G.MULTIPLAYER.update_player_usernames() + end end local function action_error(message) @@ -92,10 +95,13 @@ local function action_enemy_info(score_str, hands_left_str) return end - -- TODO: This is not working right now - if G.GAME.blind.boss then - -- Set blind chips to enemy score - G.GAME.blind.chips = score + G.LOBBY.enemy.score = score + G.LOBBY.enemy.hands = hands_left +end + +local function action_stop_game() + if G.STAGE ~= G.STAGES.MAIN_MENU then + G.MULTIPLAYER.update_connection_status() end end @@ -175,6 +181,8 @@ function Game:update(dt) action_start_blind() elseif parsedAction.action == "enemyInfo" then action_enemy_info(parsedAction.score, parsedAction.handsLeft) + elseif parsedAction.action == "stopGame" then + action_stop_game() elseif parsedAction.action == "error" then action_error(parsedAction.message) elseif parsedAction.action == "keepAlive" then diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 6268d4a3..473b4d52 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -720,12 +720,48 @@ function G.UIDEF.shop() end local update_hand_played_ref = Game.update_hand_played +---@diagnostic disable-next-line: duplicate-set-field function Game:update_hand_played(dt) - if not G.STATE_COMPLETE then - G.MULTIPLAYER.play_hand(G.GAME.chips, G.GAME.current_round.hands_left) + -- Ignore for singleplayer + if not G.LOBBY.connected or not G.LOBBY.code then + update_hand_played_ref(dt) + return + end + + if self.buttons then + self.buttons:remove() + self.buttons = nil + end + if self.shop then + self.shop:remove() + self.shop = nil end - update_hand_played_ref(self, dt) + if not G.STATE_COMPLETE then + G.STATE_COMPLETE = true + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function() + G.MULTIPLAYER.play_hand(G.GAME.chips, G.GAME.current_round.hands_left) + + if G.GAME.blind.boss then + -- Set blind chips to enemy score + G.GAME.blind.chips = G.LOBBY.enemy.score + -- For now, never advance to next round + G.STATE = G.STATES.DRAW_TO_HAND + else + if G.GAME.chips - G.GAME.blind.chips >= 0 or G.GAME.current_round.hands_left < 1 then + G.STATE = G.STATES.NEW_ROUND + else + G.STATE = G.STATES.DRAW_TO_HAND + end + end + + G.STATE_COMPLETE = false + return true + end, + })) + end end ---------------------------------------------- diff --git a/Server/src/Client.ts b/Server/src/Client.ts index 14c9e60e..e1b6ec64 100644 --- a/Server/src/Client.ts +++ b/Server/src/Client.ts @@ -23,6 +23,7 @@ class Client { // TODO: Set lives based on game mode lives = 4 score = 0 + handsLeft = 4 constructor(address: Address, send: SendFn) { this.id = uuidv4() diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index c89d43d8..c9e69e33 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -37,14 +37,16 @@ class Lobby { if (this.host?.id === client.id) { this.host = this.guest this.guest = null - } - if (this.guest?.id === client.id) { + } else if (this.guest?.id === client.id) { this.guest = null } client.setLobby(null) if (this.host === null) { Lobbies.delete(this.code) } else { + // TODO: Refactor for more than 2 players + // Stop game if someone leaves + client.lobby?.broadcast({ action: 'stopGame' }) this.broadcastLobbyInfo() } } diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 67ba4649..2facc3b0 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -66,7 +66,7 @@ const startGameAction = (client: Client) => { client.lobby.broadcast({ action: 'startGame', deck: 'c_multiplayer_1', - seed: generateSeed(), + seed: 'JA9C3', }) } @@ -79,6 +79,14 @@ const readyBlindAction = (client: Client) => { client.lobby.host.isReady = false client.lobby.guest.isReady = false + // Reset scores for next blind + client.lobby.host.score = 0 + client.lobby.guest.score = 0 + + // Reset hands left for next blind + client.lobby.host.handsLeft = 4 + client.lobby.guest.handsLeft = 4 + client.lobby.broadcast({ action: 'startBlind' }) } } @@ -98,6 +106,7 @@ const playHandAction = ( // Should this be additive or just // the latest score? client.score = score + client.handsLeft = handsLeft const lobby = client.lobby // Update the other party about the From a24ee990c05d72499de3652a7b4530941fa7424d Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 19 Mar 2024 11:45:16 -0700 Subject: [PATCH 0020/1128] Update README.md --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6fb181c5..89f300bc 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ # A Balatro Multiplayer Mod -This is an WIP Balatro multiplayer mod developed by virtualized. +This is an **WIP** Balatro multiplayer mod developed by virtualized and TGMM. +If you want to get in touch for any reason: **Discord:** virtualized -**Twitter:** @v_rtualized - This project will remain free and open source. It will also be continuously maintained, at least within the near future. If you make a video or stream Balatro using this mod then feel free to send me a DM on either platform above with a link, I would love to take a look :) ## Goals for First Release -- Some form of public server available, or a user friendly method to create a server - - This was originally to us Steam servers but that doesn't seem to be an options unfortunately +- A public server everyone will connect to by default + - We may support private servers but probably won't be making an non-programmer friendly way of making one (eg. Basic knowledge of Docker and port forwarding) - At least 2 out of 4 planned game modes implemented ## Planned Gamemodes @@ -81,4 +80,4 @@ ports: - "your_port_here:8080" ``` -- All clients that you want to connect to the server needs to modify their `Multiplayer/Config.lua` to have your ip as the value for `Config.URL` and your port (if you changed it) for the value of `Config.PORT` \ No newline at end of file +- All clients that you want to connect to the server needs to modify their `Multiplayer/Config.lua` to have your ip as the value for `Config.URL` and your port (if you changed it) for the value of `Config.PORT` From 128083bc490ab8512272e5310fa53018bd7ff15a Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 19 Mar 2024 11:49:57 -0700 Subject: [PATCH 0021/1128] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 89f300bc..4dfcdf9c 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ This is an **WIP** Balatro multiplayer mod developed by virtualized and TGMM. -If you want to get in touch for any reason: -**Discord:** virtualized +If you want to get in touch for any reason add `virtualized` on discord or send an email to `v@virtualized.dev`. This project will remain free and open source. It will also be continuously maintained, at least within the near future. @@ -48,7 +47,7 @@ If you make a video or stream Balatro using this mod then feel free to send me a ### 3. Set `Config.lua` -- If there is no `Config.lua` file in the Multiplayer folder, then there is no public server, you need to either [Create a Dedicated Server](#creating-a-dedicated-server) or have a friend that does. +- If there is no `Config.lua` file in the Multiplayer folder, then there is no public server yet, you need to either [Create a Dedicated Server](#creating-a-dedicated-server) or have a friend that does. - The `example.Config.lua` file does nothing and is just there for you to use as a Config template - ie. when you have a dedicated server to connect to, rename this file to `Config.lua` and change the values From ece124d3a02a62d7f243afbdbff8ad5e3c1b7a92 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 19 Mar 2024 19:36:52 +0000 Subject: [PATCH 0022/1128] Moved default achievement value saving to after initalization of steammodded --- Multiplayer/Lobby.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index c7d379cc..9abbca17 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -23,16 +23,20 @@ G.MULTIPLAYER_GAME = { ready_blind_text = "Ready", } -PREV_ACHIEVEMENT_VALUE = true +START_NO_ACHIEVEMENT_VALUE = true +local init_steamodded_ref = initSteamodded +function initSteamodded() + init_steamodded_ref() + START_NO_ACHIEVEMENT_VALUE = G.F_NO_ACHIEVEMENTS +end + function G.MULTIPLAYER.update_connection_status() - -- Save the previous value of the achievement flag - PREV_ACHIEVEMENT_VALUE = G.F_NO_ACHIEVEMENTS if G.LOBBY.connected then -- Disable achievements when connected to server G.F_NO_ACHIEVEMENTS = true else -- Restore them when disconnected - G.F_NO_ACHIEVEMENTS = PREV_ACHIEVEMENT_VALUE + G.F_NO_ACHIEVEMENTS = START_NO_ACHIEVEMENT_VALUE end if G.HUD_connection_status then From 974ad257a519f4af26cad267d7c63b50c51732be Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 19 Mar 2024 12:40:34 -0700 Subject: [PATCH 0023/1128] Fixed the UI and client side problems with current playable demo (#17) * Fixed shop ready button not resetting * Disabled play hand button when no hands left * Discards hand when 0 hands left and added text to wait for enemy * Sends player to menu on stop game and banned Throwback * Juices enemy score and hands on update --- Multiplayer/Items/Deck.lua | 1 + Multiplayer/Lobby.lua | 2 +- Multiplayer/Networking/Action_Handlers.lua | 5 +++ Multiplayer/UI/Game_UI.lua | 36 +++++++++++++++++++--- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Multiplayer/Items/Deck.lua b/Multiplayer/Items/Deck.lua index 818c83a5..2f831663 100644 --- a/Multiplayer/Items/Deck.lua +++ b/Multiplayer/Items/Deck.lua @@ -21,6 +21,7 @@ local c_multiplayer_1 = { banned_cards = { { id = "j_diet_cola" }, { id = "j_mr_bones" }, + { id = "j_throwback" }, { id = "v_hieroglyph" }, { id = "v_petroglyph" }, }, diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 8be1eb82..895a7e2a 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -18,7 +18,7 @@ G.LOBBY = { is_host = false, enemy = { score = 0, - hands = 5, + hands = 4, }, } diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index cdec0424..f92fe260 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -97,10 +97,15 @@ local function action_enemy_info(score_str, hands_left_str) G.LOBBY.enemy.score = score G.LOBBY.enemy.hands = hands_left + if G.GAME.blind.boss then + G.HUD_blind:get_UIE_by_ID("HUD_blind_count"):juice_up() + G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned"):juice_up() + end end local function action_stop_game() if G.STAGE ~= G.STAGES.MAIN_MENU then + G.FUNCS.go_to_menu() G.MULTIPLAYER.update_connection_status() end end diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 473b4d52..961ff13e 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -679,6 +679,16 @@ local blind_defeat_ref = Blind.defeat function Blind:defeat(silent) blind_defeat_ref(self, silent) reset_blind_HUD() + G.MULTIPLAYER.play_hand(0, G.GAME.round_resets.hands) +end + +local update_shop_ref = Game.update_shop +function Game:update_shop(dt) + if not G.STATE_COMPLETE then + G.MULTIPLAYER_GAME.ready_blind = false + G.MULTIPLAYER_GAME.ready_blind_text = "Ready" + end + update_shop_ref(self, dt) end local ui_def_shop_ref = G.UIDEF.shop @@ -724,7 +734,7 @@ local update_hand_played_ref = Game.update_hand_played function Game:update_hand_played(dt) -- Ignore for singleplayer if not G.LOBBY.connected or not G.LOBBY.code then - update_hand_played_ref(dt) + update_hand_played_ref(self, dt) return end @@ -742,13 +752,21 @@ function Game:update_hand_played(dt) G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function() - G.MULTIPLAYER.play_hand(G.GAME.chips, G.GAME.current_round.hands_left) - if G.GAME.blind.boss then + G.MULTIPLAYER.play_hand(G.GAME.chips, G.GAME.current_round.hands_left) -- Set blind chips to enemy score G.GAME.blind.chips = G.LOBBY.enemy.score -- For now, never advance to next round - G.STATE = G.STATES.DRAW_TO_HAND + if G.GAME.current_round.hands_left < 1 then + if G.hand.cards[1] then + attention_text({ + scale = 0.8, text = 'Waiting for enemy to finish...', hold = 5, align = 'cm', offset = {x = 0,y = -1.5},major = G.play + }) + G.FUNCS.draw_from_hand_to_discard() + end + else + G.STATE = G.STATES.DRAW_TO_HAND + end else if G.GAME.chips - G.GAME.blind.chips >= 0 or G.GAME.current_round.hands_left < 1 then G.STATE = G.STATES.NEW_ROUND @@ -764,5 +782,15 @@ function Game:update_hand_played(dt) end end +local can_play_ref = G.FUNCS.can_play +G.FUNCS.can_play = function(e) + if G.GAME.current_round.hands_left <= 0 then + e.config.colour = G.C.UI.BACKGROUND_INACTIVE + e.config.button = nil + else + can_play_ref(e) + end +end + ---------------------------------------------- ------------MOD GAME UI END------------------- From a1dfb07f75f72a1945001bdb9a0e13ddcaff46a3 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 19 Mar 2024 20:46:05 +0000 Subject: [PATCH 0024/1128] Attempted to add steam rich presence --- Multiplayer/Lobby.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 9abbca17..a6fb3d40 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -4,6 +4,8 @@ ---------------------------------------------- ------------MOD LOBBY------------------------- +local Utils = require "Utils" + G.MULTIPLAYER = {} G.LOBBY = { @@ -39,6 +41,22 @@ function G.MULTIPLAYER.update_connection_status() G.F_NO_ACHIEVEMENTS = START_NO_ACHIEVEMENT_VALUE end + -- Game does not have locatization, and therefore does not support steam_display, status, or text right now, but we can hope + -- steam_player_group and steam_player_group_size is functional + if G.LOBBY.code then + G.STEAM.friends.setRichPresence('steam_display', '#FullStatus') + G.STEAM.friends.setRichPresence('status', '#FullStatus') + G.STEAM.friends.setRichPresence('text', 'In Multiplayer Lobby') + G.STEAM.friends.setRichPresence('steam_player_group', G.LOBBY.code) + G.STEAM.friends.setRichPresence('steam_player_group_size', G.LOBBY.guest.username and '2' or '1') + else + G.STEAM.friends.setRichPresence('steam_display', '#FullStatus') + G.STEAM.friends.setRichPresence('status', '#FullStatus') + G.STEAM.friends.setRichPresence('text', 'Using Multiplayer Mod') + G.STEAM.friends.setRichPresence('steam_player_group', '') + G.STEAM.friends.setRichPresence('steam_player_group_size', '') + end + if G.HUD_connection_status then G.HUD_connection_status:remove() end From 1c6cc8952139ba24b2226ab82f173ab5876b8deb Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 19 Mar 2024 23:58:14 +0000 Subject: [PATCH 0025/1128] Changed achievement default value function to initMods instead of initSteammodded --- Multiplayer/Lobby.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index a6fb3d40..511400b2 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -26,9 +26,9 @@ G.MULTIPLAYER_GAME = { } START_NO_ACHIEVEMENT_VALUE = true -local init_steamodded_ref = initSteamodded -function initSteamodded() - init_steamodded_ref() +local init_mods_ref = initMods +function initMods() + init_mods_ref() START_NO_ACHIEVEMENT_VALUE = G.F_NO_ACHIEVEMENTS end From 17ab5322f709b18340c77bd73e800adecc835b8f Mon Sep 17 00:00:00 2001 From: TGMM Date: Tue, 19 Mar 2024 22:16:19 -0700 Subject: [PATCH 0026/1128] Fixed bug where round wouldn't end --- Multiplayer/Lobby.lua | 1 + Multiplayer/Networking/Action_Handlers.lua | 19 +++++++ Multiplayer/UI/Game_UI.lua | 45 ++++++++-------- Server/src/Client.ts | 7 +-- Server/src/Lobby.ts | 32 +++++------ Server/src/actionHandlers.ts | 62 ++++++++++++---------- Server/src/actions.ts | 2 + Server/src/main.ts | 51 +++++++++++------- 8 files changed, 129 insertions(+), 90 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 895a7e2a..9dea6848 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -25,6 +25,7 @@ G.LOBBY = { G.MULTIPLAYER_GAME = { ready_blind = false, ready_blind_text = "Ready", + processed_round_done = false, } PREV_ACHIEVEMENT_VALUE = true diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index f92fe260..a45884e4 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -110,6 +110,21 @@ local function action_stop_game() end end +local function action_end_pvp() + -- TODO: Some logic here to say that you won + -- or lost the round + + G.STATE = G.STATES.NEW_ROUND + G.STATE_COMPLETE = false +end + +---@param lives number +local function action_player_info(lives) + if lives == 0 then + action_stop_game() + end +end + -- #region Client to Server function G.MULTIPLAYER.create_lobby() -- TODO: This is hardcoded to attrition for now, must be changed @@ -188,6 +203,10 @@ function Game:update(dt) action_enemy_info(parsedAction.score, parsedAction.handsLeft) elseif parsedAction.action == "stopGame" then action_stop_game() + elseif parsedAction.action == "endPvP" then + action_end_pvp() + elseif parsedAction.action == "playerInfo" then + action_player_info(parsedAction.lives) elseif parsedAction.action == "error" then action_error(parsedAction.message) elseif parsedAction.action == "keepAlive" then diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 961ff13e..9281b932 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -732,8 +732,8 @@ end local update_hand_played_ref = Game.update_hand_played ---@diagnostic disable-next-line: duplicate-set-field function Game:update_hand_played(dt) - -- Ignore for singleplayer - if not G.LOBBY.connected or not G.LOBBY.code then + -- Ignore for singleplayer or regular blinds + if not G.LOBBY.connected or not G.LOBBY.code or not G.GAME.blind.boss then update_hand_played_ref(self, dt) return end @@ -752,30 +752,29 @@ function Game:update_hand_played(dt) G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function() - if G.GAME.blind.boss then - G.MULTIPLAYER.play_hand(G.GAME.chips, G.GAME.current_round.hands_left) - -- Set blind chips to enemy score - G.GAME.blind.chips = G.LOBBY.enemy.score - -- For now, never advance to next round - if G.GAME.current_round.hands_left < 1 then - if G.hand.cards[1] then - attention_text({ - scale = 0.8, text = 'Waiting for enemy to finish...', hold = 5, align = 'cm', offset = {x = 0,y = -1.5},major = G.play - }) - G.FUNCS.draw_from_hand_to_discard() - end - else - G.STATE = G.STATES.DRAW_TO_HAND + G.MULTIPLAYER.play_hand(G.GAME.chips, G.GAME.current_round.hands_left) + -- Set blind chips to enemy score + G.GAME.blind.chips = G.LOBBY.enemy.score + -- For now, never advance to next round + if G.GAME.current_round.hands_left < 1 then + if G.hand.cards[1] then + attention_text({ + scale = 0.8, + text = "Waiting for enemy to finish...", + hold = 5, + align = "cm", + offset = { x = 0, y = -1.5 }, + major = G.play, + }) + G.FUNCS.draw_from_hand_to_discard() end + + G.MULTIPLAYER_GAME.processed_round_done = true else - if G.GAME.chips - G.GAME.blind.chips >= 0 or G.GAME.current_round.hands_left < 1 then - G.STATE = G.STATES.NEW_ROUND - else - G.STATE = G.STATES.DRAW_TO_HAND - end + G.STATE_COMPLETE = false + G.STATE = G.STATES.DRAW_TO_HAND end - G.STATE_COMPLETE = false return true end, })) @@ -784,7 +783,7 @@ end local can_play_ref = G.FUNCS.can_play G.FUNCS.can_play = function(e) - if G.GAME.current_round.hands_left <= 0 then + if G.GAME.current_round.hands_left <= 0 then e.config.colour = G.C.UI.BACKGROUND_INACTIVE e.config.button = nil else diff --git a/Server/src/Client.ts b/Server/src/Client.ts index e1b6ec64..ab55ac58 100644 --- a/Server/src/Client.ts +++ b/Server/src/Client.ts @@ -1,8 +1,9 @@ import { v4 as uuidv4 } from 'uuid' import type Lobby from './Lobby.js' import type net from 'node:net' +import type { ActionServerToClient } from './actions.js' -type SendFn = (data: string) => void +type SendFn = (action: ActionServerToClient) => void /* biome-ignore lint/complexity/noBannedTypes: This is how the net module does it */ @@ -13,7 +14,7 @@ class Client { id: string // Could be useful later on to detect reconnects address: Address - send: SendFn + sendAction: SendFn // Game info username = 'Guest' @@ -28,7 +29,7 @@ class Client { constructor(address: Address, send: SendFn) { this.id = uuidv4() this.address = address - this.send = send + this.sendAction = send } setUsername = (username: string) => { diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index c9e69e33..58b83456 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -1,5 +1,9 @@ import type Client from './Client.js' -import type { Action, ActionLobbyInfo } from './actions.js' +import type { + Action, + ActionLobbyInfo, + ActionServerToClient, +} from './actions.js' import { serializeAction } from './main.js' const Lobbies = new Map() @@ -26,7 +30,7 @@ class Lobby { this.host = host this.guest = null host.setLobby(this) - host.send(serializeAction({ action: 'joinedLobby', code: this.code })) + host.sendAction({ action: 'joinedLobby', code: this.code }) } static get = (code: string) => { @@ -46,30 +50,28 @@ class Lobby { } else { // TODO: Refactor for more than 2 players // Stop game if someone leaves - client.lobby?.broadcast({ action: 'stopGame' }) + client.lobby?.broadcastAction({ action: 'stopGame' }) this.broadcastLobbyInfo() } } join = (client: Client) => { if (this.guest) { - client.send( - serializeAction({ - action: 'error', - message: 'Lobby is full or does not exist.', - }), - ) + client.sendAction({ + action: 'error', + message: 'Lobby is full or does not exist.', + }) return } this.guest = client client.setLobby(this) - client.send(serializeAction({ action: 'joinedLobby', code: this.code })) + client.sendAction({ action: 'joinedLobby', code: this.code }) this.broadcastLobbyInfo() } - broadcast = (action: Action) => { - this.host?.send(serializeAction(action)) - this.guest?.send(serializeAction(action)) + broadcastAction = (action: ActionServerToClient) => { + this.host?.sendAction(action) + this.guest?.sendAction(action) } broadcastLobbyInfo = () => { @@ -85,12 +87,12 @@ class Lobby { if (this.guest?.username) { action.guest = this.guest.username - this.guest.send(serializeAction(action)) + this.guest.sendAction(action) } // Should only sent true to the host action.isHost = true - this.host.send(serializeAction(action)) + this.host.sendAction(action) } } diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 2facc3b0..0486df02 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -8,7 +8,6 @@ import type { ActionPlayHand, ActionUsername, } from './actions.js' -import { serializeAction } from './main.js' import { generateSeed } from './utils.js' const usernameAction = ( @@ -32,12 +31,10 @@ const joinLobbyAction = ( ) => { const newLobby = Lobby.get(code) if (!newLobby) { - client.send( - serializeAction({ - action: 'error', - message: 'Lobby does not exist.', - }), - ) + client.sendAction({ + action: 'error', + message: 'Lobby does not exist.', + }) return } newLobby.join(client) @@ -53,7 +50,7 @@ const lobbyInfoAction = (client: Client) => { const keepAliveAction = (client: Client) => { // Send an ack back to the received keepAlive - client.send(serializeAction({ action: 'keepAliveAck' })) + client.sendAction({ action: 'keepAliveAck' }) } const startGameAction = (client: Client) => { @@ -63,7 +60,7 @@ const startGameAction = (client: Client) => { } // Hardcoded for testing - client.lobby.broadcast({ + client.lobby.broadcastAction({ action: 'startGame', deck: 'c_multiplayer_1', seed: 'JA9C3', @@ -87,7 +84,7 @@ const readyBlindAction = (client: Client) => { client.lobby.host.handsLeft = 4 client.lobby.guest.handsLeft = 4 - client.lobby.broadcast({ action: 'startBlind' }) + client.lobby.broadcastAction({ action: 'startBlind' }) } } @@ -106,38 +103,45 @@ const playHandAction = ( // Should this be additive or just // the latest score? client.score = score - client.handsLeft = handsLeft + client.handsLeft = + typeof handsLeft === 'number' ? handsLeft : Number(handsLeft) const lobby = client.lobby // Update the other party about the // enemy's score and hands left // TODO: Refactor for more than two players if (lobby.host?.id === client.id) { - lobby.guest?.send( - serializeAction({ - action: 'enemyInfo', - handsLeft, - score, - }), - ) + lobby.guest?.sendAction({ + action: 'enemyInfo', + handsLeft, + score, + }) } else if (lobby.guest?.id === client.id) { - lobby.host?.send( - serializeAction({ - action: 'enemyInfo', - handsLeft, - score, - }), - ) + lobby.host?.sendAction({ + action: 'enemyInfo', + handsLeft, + score, + }) } - // TODO: This should check if this is the boss blind - if (lobby.host?.lives === 0 && lobby.guest?.lives === 0) { + console.log( + `Host hands: ${lobby.host?.handsLeft}, Guest hands: ${lobby.guest?.handsLeft}`, + ) + // This info is only sent on a boss blind, so it shouldn't + // affect other blinds + if (lobby.host?.handsLeft === 0 && lobby.guest?.handsLeft === 0) { const winner = lobby.host.score > lobby.guest.score ? lobby.host : lobby.guest const loser = winner.id === lobby.host.id ? lobby.guest : lobby.host - winner.send(serializeAction({ action: 'endPvP', lost: false })) - loser.send(serializeAction({ action: 'endPvP', lost: true })) + console.log('Winner:', winner.username) + console.log('Loser:', loser.username) + + loser.lives -= 1 + loser.sendAction({ action: 'playerInfo', lives: loser.lives }) + + winner.sendAction({ action: 'endPvP', lost: false }) + loser.sendAction({ action: 'endPvP', lost: true }) } } diff --git a/Server/src/actions.ts b/Server/src/actions.ts index 9beb546b..ab9777c8 100644 --- a/Server/src/actions.ts +++ b/Server/src/actions.ts @@ -46,6 +46,8 @@ export type ActionServerToClient = | ActionPlayerInfo | ActionEnemyInfo | ActionEndPvP + | ActionKeepAlive + | ActionKeepAliveAck // Client to Server export type ActionUsername = { action: 'username'; username: string } diff --git a/Server/src/main.ts b/Server/src/main.ts index 95231d8e..0ee1f862 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -8,11 +8,12 @@ import type { ActionHandlerArgs, ActionJoinLobby, ActionPlayHand, + ActionServerToClient, ActionUsername, ActionUtility, } from './actions.js' -const PORT = 8080 +const PORT = 15158 /** The amount of milliseconds we wait before sending the initial keepalive packet */ const KEEP_ALIVE_INITIAL_TIMEOUT = 5000 @@ -23,11 +24,10 @@ const KEEP_ALIVE_RETRY_COUNT = 3 // biome-ignore lint/suspicious/noExplicitAny: Object is parsed from string const stringToJson = (str: string): any => { - // biome-ignore lint/suspicious/noExplicitAny: Object is parsed from string - const obj: any = {} + const obj: Record = {} for (const part of str.split(',')) { const [key, value] = part.split(':') - obj[key] = value + obj[key] = Number.isNaN(value) ? value : +value } return obj } @@ -41,12 +41,21 @@ export const serializeAction = (action: Action): string => { return parts.join(',') } -const sendToSocket = (socket: net.Socket) => (data: string) => { - if (!socket) { - return +const sendActionToSocket = + (socket: net.Socket) => (action: ActionServerToClient) => { + if (!socket) { + return + } + + const data = serializeAction(action) + + const { action: actionName, ...actionArgs } = action + console.log( + `Sent action ${actionName} to client: ${JSON.stringify(actionArgs)}`, + ) + + socket.write(`${data}\n`) } - socket.write(`${data}\n`) -} const server = net.createServer((socket) => { socket.allowHalfOpen = false @@ -54,8 +63,8 @@ const server = net.createServer((socket) => { // improve latency between responses socket.setNoDelay() - const client = new Client(socket.address(), sendToSocket(socket)) - client.send(serializeAction({ action: 'connected' })) + const client = new Client(socket.address(), sendActionToSocket(socket)) + client.sendAction({ action: 'connected' }) let isRetry = false let retryCount = 0 @@ -66,7 +75,7 @@ const server = net.createServer((socket) => { return } - client.send(serializeAction({ action: 'keepAlive' })) + client.sendAction({ action: 'keepAlive' }) retryCount++ if (retryCount >= KEEP_ALIVE_RETRY_COUNT) { @@ -78,7 +87,7 @@ const server = net.createServer((socket) => { // Once the client connects, we start a timer const keepAlive: ReturnType = setTimeout(() => { - client.send(serializeAction({ action: 'keepAlive' })) + client.sendAction({ action: 'keepAlive' }) isRetry = true retryTimer.refresh() }, KEEP_ALIVE_INITIAL_TIMEOUT) @@ -96,7 +105,11 @@ const server = net.createServer((socket) => { try { const message: ActionClientToServer | ActionUtility = stringToJson(msg) const { action, ...actionArgs } = message - console.log(`Received action ${action} from ${client.id}`) + console.log( + `Received action ${action} from ${client.id}: ${JSON.stringify( + actionArgs, + )}`, + ) switch (action) { case 'username': @@ -145,12 +158,10 @@ const server = net.createServer((socket) => { } catch (error) { const failedToParseError = 'Failed to parse message' console.error(failedToParseError, error) - client.send( - serializeAction({ - action: 'error', - message: failedToParseError, - }), - ) + client.sendAction({ + action: 'error', + message: failedToParseError, + }) } } }) From cd0496bd681e76db2ab7f1a2cb9ee6e27e49e13e Mon Sep 17 00:00:00 2001 From: TGMM Date: Thu, 21 Mar 2024 21:07:27 -0700 Subject: [PATCH 0027/1128] Added gameStop action and lives counter --- Multiplayer/Lobby.lua | 1 + Multiplayer/Networking/Action_Handlers.lua | 8 ++-- Multiplayer/UI/Game_UI.lua | 49 +++++++++++++++++++++- Multiplayer/UI/Lobby_UI.lua | 5 +++ Server/src/Lobby.ts | 4 +- Server/src/actionHandlers.ts | 34 +++++++++++---- Server/src/main.ts | 6 ++- 7 files changed, 92 insertions(+), 15 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 9dea6848..b5b0ed31 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -26,6 +26,7 @@ G.MULTIPLAYER_GAME = { ready_blind = false, ready_blind_text = "Ready", processed_round_done = false, + lives = 3, } PREV_ACHIEVEMENT_VALUE = true diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index a45884e4..cbcbfc5e 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -120,9 +120,7 @@ end ---@param lives number local function action_player_info(lives) - if lives == 0 then - action_stop_game() - end + G.MULTIPLAYER_GAME.lives = lives end -- #region Client to Server @@ -155,6 +153,10 @@ function G.MULTIPLAYER.unready_blind() Client.send("action:unreadyBlind") end +function G.MULTIPLAYER.stop_game() + Client.send("action:stopGame") +end + ---@param score number ---@param hands_left number function G.MULTIPLAYER.play_hand(score, hands_left) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 9281b932..a444b072 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -22,7 +22,11 @@ function create_UIBox_options() })) if G.STAGE == G.STAGES.RUN then - main_menu = UIBox_button({ label = { "Return to Lobby" }, button = "go_to_menu", minw = 5 }) + main_menu = UIBox_button({ + label = { "Return to Lobby" }, + button = "return_to_lobby", + minw = 5, + }) your_collection = UIBox_button({ label = { localize("b_collection") }, button = "your_collection", @@ -791,5 +795,48 @@ G.FUNCS.can_play = function(e) end end +local update_new_round_ref = Game.update_new_round +function Game:update_new_round(dt) + -- Prevent player from losing + G.GAME.blind.chips = 0 + -- Prevent player from winning + G.GAME.win_ante = 999 + + update_new_round_ref(self, dt) + + -- Reset ante number + G.GAME.win_ante = 8 +end + +local start_run_ref = Game.start_run +function Game:start_run(args) + -- if not G.LOBBY.connected or not G.LOBBY.code then + -- update_run_ref(self, args) + -- return + -- end + + start_run_ref(self, args) + + local scale = 0.4 + local hud_ante = G.HUD:get_UIE_by_ID("hud_ante") + hud_ante.children[1].children[1].config.text = "Lives" + + -- Set lives number + hud_ante.children[2].children[1].config.object = DynaText({ + string = { { ref_table = G.MULTIPLAYER_GAME, ref_value = "lives" } }, + colours = { G.C.IMPORTANT }, + shadow = true, + font = G.LANGUAGES["en-us"].font, + scale = 2 * scale, + }) + + -- Remove unnecessary HUD elements + hud_ante.children[2].children[2] = nil + hud_ante.children[2].children[3] = nil + hud_ante.children[2].children[4] = nil + + self.HUD:recalculate() +end + ---------------------------------------------- ------------MOD GAME UI END------------------- diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua index 683d3cde..18d1645d 100644 --- a/Multiplayer/UI/Lobby_UI.lua +++ b/Multiplayer/UI/Lobby_UI.lua @@ -374,6 +374,11 @@ function G.FUNCS.display_lobby_main_menu_UI(e) G.CONTROLLER:snap_to({ node = G.MAIN_MENU_UI:get_UIE_by_ID("lobby_menu_start") }) end +function G.FUNCS.return_to_lobby() + G.FUNCS.go_to_menu() + G.MULTIPLAYER.stop_game() +end + local set_main_menu_UI_ref = set_main_menu_UI ---@diagnostic disable-next-line: lowercase-global function set_main_menu_UI() diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index 58b83456..414d981d 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -44,13 +44,15 @@ class Lobby { } else if (this.guest?.id === client.id) { this.guest = null } + + const lobby = client.lobby client.setLobby(null) if (this.host === null) { Lobbies.delete(this.code) } else { // TODO: Refactor for more than 2 players // Stop game if someone leaves - client.lobby?.broadcastAction({ action: 'stopGame' }) + lobby?.broadcastAction({ action: 'stopGame' }) this.broadcastLobbyInfo() } } diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 0486df02..d58e42e8 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -130,21 +130,36 @@ const playHandAction = ( // This info is only sent on a boss blind, so it shouldn't // affect other blinds if (lobby.host?.handsLeft === 0 && lobby.guest?.handsLeft === 0) { - const winner = - lobby.host.score > lobby.guest.score ? lobby.host : lobby.guest - const loser = winner.id === lobby.host.id ? lobby.guest : lobby.host + // If no lives are left, we end the game + if (lobby.host.lives === 0 || lobby.guest.lives === 0) { + const gameWinner = + lobby.host.lives > lobby.guest.lives ? lobby.host : lobby.guest + const gameLoser = + gameWinner.id === lobby.host.id ? lobby.guest : lobby.host + + stopGameAction(client) + + // TODO: Announce who won + return + } - console.log('Winner:', winner.username) - console.log('Loser:', loser.username) + const roundWinner = + lobby.host.score > lobby.guest.score ? lobby.host : lobby.guest + const roundLoser = + roundWinner.id === lobby.host.id ? lobby.guest : lobby.host - loser.lives -= 1 - loser.sendAction({ action: 'playerInfo', lives: loser.lives }) + roundLoser.lives -= 1 + roundLoser.sendAction({ action: 'playerInfo', lives: roundLoser.lives }) - winner.sendAction({ action: 'endPvP', lost: false }) - loser.sendAction({ action: 'endPvP', lost: true }) + roundWinner.sendAction({ action: 'endPvP', lost: false }) + roundLoser.sendAction({ action: 'endPvP', lost: true }) } } +const stopGameAction = (client: Client) => { + client.lobby?.broadcastAction({ action: 'stopGame' }) +} + // Declared partial for now untill all action handlers are defined export const actionHandlers = { username: usernameAction, @@ -157,4 +172,5 @@ export const actionHandlers = { readyBlind: readyBlindAction, unreadyBlind: unreadyBlindAction, playHand: playHandAction, + stopGame: stopGameAction, } satisfies Partial diff --git a/Server/src/main.ts b/Server/src/main.ts index 0ee1f862..4d9eb549 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -27,7 +27,8 @@ const stringToJson = (str: string): any => { const obj: Record = {} for (const part of str.split(',')) { const [key, value] = part.split(':') - obj[key] = Number.isNaN(value) ? value : +value + const numericValue = Number.parseFloat(value) + obj[key] = Number.isNaN(numericValue) ? value : numericValue } return obj } @@ -154,6 +155,9 @@ const server = net.createServer((socket) => { client, ) break + case 'stopGame': + actionHandlers.stopGame(client) + break } } catch (error) { const failedToParseError = 'Failed to parse message' From fd2e6daa0b6cf92486387aff86b0c468fb706b80 Mon Sep 17 00:00:00 2001 From: TGMM Date: Thu, 21 Mar 2024 21:10:55 -0700 Subject: [PATCH 0028/1128] Restored debug logic --- Multiplayer/UI/Game_UI.lua | 8 ++++---- Server/src/actionHandlers.ts | 2 +- Server/src/main.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index a444b072..bbf3fda2 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -810,10 +810,10 @@ end local start_run_ref = Game.start_run function Game:start_run(args) - -- if not G.LOBBY.connected or not G.LOBBY.code then - -- update_run_ref(self, args) - -- return - -- end + if not G.LOBBY.connected or not G.LOBBY.code then + update_run_ref(self, args) + return + end start_run_ref(self, args) diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index d58e42e8..c6ebd7f0 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -63,7 +63,7 @@ const startGameAction = (client: Client) => { client.lobby.broadcastAction({ action: 'startGame', deck: 'c_multiplayer_1', - seed: 'JA9C3', + seed: generateSeed(), }) } diff --git a/Server/src/main.ts b/Server/src/main.ts index 4d9eb549..e7d20b23 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -13,7 +13,7 @@ import type { ActionUtility, } from './actions.js' -const PORT = 15158 +const PORT = 8080 /** The amount of milliseconds we wait before sending the initial keepalive packet */ const KEEP_ALIVE_INITIAL_TIMEOUT = 5000 From 382e877b07a52d3e8af99c9094b801d834aaa3aa Mon Sep 17 00:00:00 2001 From: TGMM Date: Thu, 21 Mar 2024 22:10:41 -0700 Subject: [PATCH 0029/1128] Fixed lives count --- Server/src/actionHandlers.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index c6ebd7f0..b63c83c6 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -130,6 +130,14 @@ const playHandAction = ( // This info is only sent on a boss blind, so it shouldn't // affect other blinds if (lobby.host?.handsLeft === 0 && lobby.guest?.handsLeft === 0) { + const roundWinner = + lobby.host.score > lobby.guest.score ? lobby.host : lobby.guest + const roundLoser = + roundWinner.id === lobby.host.id ? lobby.guest : lobby.host + + roundLoser.lives -= 1 + roundLoser.sendAction({ action: 'playerInfo', lives: roundLoser.lives }) + // If no lives are left, we end the game if (lobby.host.lives === 0 || lobby.guest.lives === 0) { const gameWinner = @@ -143,14 +151,6 @@ const playHandAction = ( return } - const roundWinner = - lobby.host.score > lobby.guest.score ? lobby.host : lobby.guest - const roundLoser = - roundWinner.id === lobby.host.id ? lobby.guest : lobby.host - - roundLoser.lives -= 1 - roundLoser.sendAction({ action: 'playerInfo', lives: roundLoser.lives }) - roundWinner.sendAction({ action: 'endPvP', lost: false }) roundLoser.sendAction({ action: 'endPvP', lost: true }) } From 1ed248844bb8cbc906ce71374f89c11d85a79b63 Mon Sep 17 00:00:00 2001 From: TGMM Date: Fri, 22 Mar 2024 15:34:47 -0700 Subject: [PATCH 0030/1128] Fixed ref on start_run function --- Multiplayer/UI/Game_UI.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index bbf3fda2..ad7e1b66 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -811,7 +811,7 @@ end local start_run_ref = Game.start_run function Game:start_run(args) if not G.LOBBY.connected or not G.LOBBY.code then - update_run_ref(self, args) + start_run_ref(self, args) return end From 22d26104905f644eacbcac5fb1c23baf16b94a51 Mon Sep 17 00:00:00 2001 From: TGMM Date: Fri, 22 Mar 2024 16:55:08 -0700 Subject: [PATCH 0031/1128] Added gamemode and fixed a bug with player lives sync --- Server/src/Lobby.ts | 17 +++++++++++++++-- Server/src/actionHandlers.ts | 22 ++++++++++++++++++---- Server/src/actions.ts | 5 ++++- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index 414d981d..03c4a06e 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -1,8 +1,8 @@ import type Client from './Client.js' import type { - Action, ActionLobbyInfo, ActionServerToClient, + GameMode, } from './actions.js' import { serializeAction } from './main.js' @@ -21,14 +21,19 @@ class Lobby { code: string host: Client | null guest: Client | null + gameMode: GameMode - constructor(host: Client) { + // Attrition is the default game mode + constructor(host: Client, gameMode: GameMode = 'attrition') { do { this.code = generateUniqueLobbyCode() } while (Lobbies.get(this.code)) Lobbies.set(this.code, this) + this.host = host this.guest = null + this.gameMode = gameMode + host.setLobby(this) host.sendAction({ action: 'joinedLobby', code: this.code }) } @@ -96,6 +101,14 @@ class Lobby { action.isHost = true this.host.sendAction(action) } + + setPlayersLives = (lives: number) => { + // TODO: Refactor for more than 2 players + if (this.host) this.host.lives = lives + if (this.guest) this.guest.lives = lives + + this.broadcastAction({ action: 'playerInfo', lives }) + } } export default Lobby diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index b63c83c6..e2d21fc2 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -22,7 +22,7 @@ const createLobbyAction = ( client: Client, ) => { /** Also sets the client lobby to this newly created one */ - new Lobby(client) + new Lobby(client, gameMode) } const joinLobbyAction = ( @@ -54,13 +54,27 @@ const keepAliveAction = (client: Client) => { } const startGameAction = (client: Client) => { + const lobby = client.lobby // Only allow the host to start the game - if (!client.lobby || client.lobby.host?.id !== client.id) { + if (!lobby || lobby.host?.id !== client.id) { return } - // Hardcoded for testing - client.lobby.broadcastAction({ + let lives = 4 + // TODO: Put this in a gamemode map that + // has more info than just lives + switch (lobby.gameMode) { + case 'attrition': + lives = 4 + break + case 'draft': + lives = 2 + break + } + + // Reset players' lives + lobby.setPlayersLives(lives) + lobby.broadcastAction({ action: 'startGame', deck: 'c_multiplayer_1', seed: generateSeed(), diff --git a/Server/src/actions.ts b/Server/src/actions.ts index ab9777c8..22789754 100644 --- a/Server/src/actions.ts +++ b/Server/src/actions.ts @@ -51,7 +51,7 @@ export type ActionServerToClient = // Client to Server export type ActionUsername = { action: 'username'; username: string } -export type ActionCreateLobby = { action: 'createLobby'; gameMode: string } +export type ActionCreateLobby = { action: 'createLobby'; gameMode: GameMode } export type ActionJoinLobby = { action: 'joinLobby'; code: string } export type ActionLeaveLobby = { action: 'leaveLobby' } export type ActionLobbyInfoRequest = { action: 'lobbyInfo' } @@ -108,3 +108,6 @@ export type ActionHandlers = { } export type ActionHandlerArgs = Omit + +// Other types +export type GameMode = 'attrition' | 'draft' From 9161f12f2ab96ba0030b4f697af41fef1f628e35 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 04:13:29 +0000 Subject: [PATCH 0032/1128] make pull request From 9d25dee0a78909f852dea505bad45950e26f25bb Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 04:13:42 +0000 Subject: [PATCH 0033/1128] make pull request From 676d1233ecad33bd5a12f5c018ba0892357ae55e Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 04:46:10 +0000 Subject: [PATCH 0034/1128] make pull request From da86e938899db742649658b74fd9a41a20167eb9 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 04:49:30 +0000 Subject: [PATCH 0035/1128] make pull request From 181e3cc18ff6abf0abfff18e3df2d88ad9cc5886 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 04:52:11 +0000 Subject: [PATCH 0036/1128] make pull request From 70b36ed45941e632ec9ce981edca63635184591d Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 05:45:41 +0000 Subject: [PATCH 0037/1128] make pull request From 6793869633666f746cacd80fa3d7ff3c4e80cbd6 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 08:28:54 +0000 Subject: [PATCH 0038/1128] Fixed not being able to lose a round on singleplayer --- Multiplayer/UI/Game_UI.lua | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index ad7e1b66..de7d6bfb 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -797,15 +797,19 @@ end local update_new_round_ref = Game.update_new_round function Game:update_new_round(dt) - -- Prevent player from losing - G.GAME.blind.chips = 0 - -- Prevent player from winning - G.GAME.win_ante = 999 + if G.LOBBY.code then + -- Prevent player from losing + G.GAME.blind.chips = 0 + -- Prevent player from winning + G.GAME.win_ante = 999 - update_new_round_ref(self, dt) + update_new_round_ref(self, dt) - -- Reset ante number - G.GAME.win_ante = 8 + -- Reset ante number + G.GAME.win_ante = 8 + return + end + update_new_round_ref(self, dt) end local start_run_ref = Game.start_run From 40e42aaf705af0558517a14ca83fbc0666775f3f Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 11:40:53 +0000 Subject: [PATCH 0039/1128] Implemented cashout round loss indicator --- Multiplayer/UI/Game_UI.lua | 91 +++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index ad7e1b66..e5f95e45 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -798,7 +798,10 @@ end local update_new_round_ref = Game.update_new_round function Game:update_new_round(dt) -- Prevent player from losing - G.GAME.blind.chips = 0 + if G.GAME.chips - G.GAME.blind.chips < 0 then + G.GAME.blind.chips = -1 + end + -- Prevent player from winning G.GAME.win_ante = 999 @@ -838,5 +841,91 @@ function Game:start_run(args) self.HUD:recalculate() end + +local add_round_eval_row_ref = add_round_eval_row +function add_round_eval_row(config) + if G.LOBBY.code and config.name == 'blind1' and G.GAME.blind.chips == -1 then + local config = config or {} + local width = G.round_eval.T.w - 0.51 + local num_dollars = config.dollars or 1 + local scale = 0.9 + delay(0.4) + G.E_MANAGER:add_event(Event({ + trigger = 'before',delay = 0.5, + func = function() + --Add the far left text and context first: + local left_text = {} + local blind_sprite = AnimatedSprite(0, 0, 1.2,1.2, G.ANIMATION_ATLAS['blind_chips'], copy_table(G.GAME.blind.pos)) + blind_sprite:define_draw_steps({ + {shader = 'dissolve', shadow_height = 0.05}, + {shader = 'dissolve'} + }) + table.insert(left_text, {n=G.UIT.O, config={w=1.2,h=1.2 , object = blind_sprite, hover = true, can_collide = false}}) + + table.insert(left_text, + {n=G.UIT.C, config={padding = 0.05, align = 'cm'}, nodes={ + {n=G.UIT.R, config={align = 'cm'}, nodes={ + {n=G.UIT.O, config={object = DynaText({string = {G.GAME.blind.boss and ' Lost a Life ' or ' Failed '}, colours = {G.C.FILTER}, shadow = true, pop_in = 0, scale = 0.5*scale, silent = true})}} + }} + }}) + local full_row = {n=G.UIT.R, config={align = "cm", minw = 5}, nodes={ + {n=G.UIT.C, config={padding = 0.05, minw = width*0.55, minh = 0.61, align = "cl"}, nodes=left_text}, + {n=G.UIT.C, config={padding = 0.05,minw = width*0.45, align = "cr"}, nodes={{n=G.UIT.C, config={align = "cm", id = 'dollar_'..config.name},nodes={}}}} + }} + + G.GAME.blind:juice_up() + G.round_eval:add_child(full_row,G.round_eval:get_UIE_by_ID('base_round_eval')) + play_sound('negative',( 1.5*config.pitch) or 1, 0.2) + play_sound('whoosh2', 0.9, 0.7) + if config.card then config.card:juice_up(0.7, 0.46) end + return true + end + })) + local dollar_row = 0 + if num_dollars > 60 then + G.E_MANAGER:add_event(Event({ + trigger = 'before',delay = 0.38, + func = function() + G.round_eval:add_child( + {n=G.UIT.R, config={align = "cm", id = 'dollar_row_'..(dollar_row+1)..'_'..config.name}, nodes={ + {n=G.UIT.O, config={object = DynaText({string = {localize('$')..num_dollars}, colours = {G.C.MONEY}, shadow = true, pop_in = 0, scale = 0.65, float = true})}} + }}, + G.round_eval:get_UIE_by_ID('dollar_'..config.name)) + + play_sound('coin3', 0.9+0.2*math.random(), 0.7) + play_sound('coin6', 1.3, 0.8) + return true + end + })) + else + for i = 1, num_dollars or 1 do + G.E_MANAGER:add_event(Event({ + trigger = 'before',delay = 0.18 - ((num_dollars > 20 and 0.13) or (num_dollars > 9 and 0.1) or 0), + func = function() + if i%30 == 1 then + G.round_eval:add_child( + {n=G.UIT.R, config={align = "cm", id = 'dollar_row_'..(dollar_row+1)..'_'..config.name}, nodes={}}, + G.round_eval:get_UIE_by_ID('dollar_'..config.name)) + dollar_row = dollar_row+1 + end + + local r = {n=G.UIT.T, config={text = localize('$'), colour = G.C.MONEY, scale = ((num_dollars > 20 and 0.28) or (num_dollars > 9 and 0.43) or 0.58), shadow = true, hover = true, can_collide = false, juice = true}} + play_sound('coin3', 0.9+0.2*math.random(), 0.7 - (num_dollars > 20 and 0.2 or 0)) + + if config.name == 'blind1' then + G.GAME.current_round.dollars_to_be_earned = G.GAME.current_round.dollars_to_be_earned:sub(2) + end + + G.round_eval:add_child(r,G.round_eval:get_UIE_by_ID('dollar_row_'..(dollar_row)..'_'..config.name)) + G.VIBRATION = G.VIBRATION + 0.4 + return true + end + })) + end + end + else + add_round_eval_row_ref(config) + end +end ---------------------------------------------- ------------MOD GAME UI END------------------- From 616cc40766a0a74bc3dce3abfd1060352947ab28 Mon Sep 17 00:00:00 2001 From: TGMM Date: Sat, 23 Mar 2024 13:08:04 -0700 Subject: [PATCH 0040/1128] Formatted Game_UI with StyLua --- Multiplayer/UI/Game_UI.lua | 214 +++++++++++++++++++++++++------------ 1 file changed, 144 insertions(+), 70 deletions(-) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index e5f95e45..9ed870f4 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -801,7 +801,7 @@ function Game:update_new_round(dt) if G.GAME.chips - G.GAME.blind.chips < 0 then G.GAME.blind.chips = -1 end - + -- Prevent player from winning G.GAME.win_ante = 999 @@ -841,87 +841,161 @@ function Game:start_run(args) self.HUD:recalculate() end - local add_round_eval_row_ref = add_round_eval_row function add_round_eval_row(config) - if G.LOBBY.code and config.name == 'blind1' and G.GAME.blind.chips == -1 then - local config = config or {} - local width = G.round_eval.T.w - 0.51 - local num_dollars = config.dollars or 1 + if G.LOBBY.code and config.name == "blind1" and G.GAME.blind.chips == -1 then + local config = config or {} + local width = G.round_eval.T.w - 0.51 + local num_dollars = config.dollars or 1 local scale = 0.9 delay(0.4) G.E_MANAGER:add_event(Event({ - trigger = 'before',delay = 0.5, + trigger = "before", + delay = 0.5, func = function() - --Add the far left text and context first: - local left_text = {} - local blind_sprite = AnimatedSprite(0, 0, 1.2,1.2, G.ANIMATION_ATLAS['blind_chips'], copy_table(G.GAME.blind.pos)) - blind_sprite:define_draw_steps({ - {shader = 'dissolve', shadow_height = 0.05}, - {shader = 'dissolve'} - }) - table.insert(left_text, {n=G.UIT.O, config={w=1.2,h=1.2 , object = blind_sprite, hover = true, can_collide = false}}) - - table.insert(left_text, - {n=G.UIT.C, config={padding = 0.05, align = 'cm'}, nodes={ - {n=G.UIT.R, config={align = 'cm'}, nodes={ - {n=G.UIT.O, config={object = DynaText({string = {G.GAME.blind.boss and ' Lost a Life ' or ' Failed '}, colours = {G.C.FILTER}, shadow = true, pop_in = 0, scale = 0.5*scale, silent = true})}} - }} - }}) - local full_row = {n=G.UIT.R, config={align = "cm", minw = 5}, nodes={ - {n=G.UIT.C, config={padding = 0.05, minw = width*0.55, minh = 0.61, align = "cl"}, nodes=left_text}, - {n=G.UIT.C, config={padding = 0.05,minw = width*0.45, align = "cr"}, nodes={{n=G.UIT.C, config={align = "cm", id = 'dollar_'..config.name},nodes={}}}} - }} - - G.GAME.blind:juice_up() - G.round_eval:add_child(full_row,G.round_eval:get_UIE_by_ID('base_round_eval')) - play_sound('negative',( 1.5*config.pitch) or 1, 0.2) - play_sound('whoosh2', 0.9, 0.7) - if config.card then config.card:juice_up(0.7, 0.46) end - return true - end + --Add the far left text and context first: + local left_text = {} + local blind_sprite = + AnimatedSprite(0, 0, 1.2, 1.2, G.ANIMATION_ATLAS["blind_chips"], copy_table(G.GAME.blind.pos)) + blind_sprite:define_draw_steps({ + { shader = "dissolve", shadow_height = 0.05 }, + { shader = "dissolve" }, + }) + table.insert(left_text, { + n = G.UIT.O, + config = { w = 1.2, h = 1.2, object = blind_sprite, hover = true, can_collide = false }, + }) + + table.insert(left_text, { + n = G.UIT.C, + config = { padding = 0.05, align = "cm" }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.O, + config = { + object = DynaText({ + string = { G.GAME.blind.boss and " Lost a Life " or " Failed " }, + colours = { G.C.FILTER }, + shadow = true, + pop_in = 0, + scale = 0.5 * scale, + silent = true, + }), + }, + }, + }, + }, + }, + }) + local full_row = { + n = G.UIT.R, + config = { align = "cm", minw = 5 }, + nodes = { + { + n = G.UIT.C, + config = { padding = 0.05, minw = width * 0.55, minh = 0.61, align = "cl" }, + nodes = left_text, + }, + { + n = G.UIT.C, + config = { padding = 0.05, minw = width * 0.45, align = "cr" }, + nodes = { + { n = G.UIT.C, config = { align = "cm", id = "dollar_" .. config.name }, nodes = {} }, + }, + }, + }, + } + + G.GAME.blind:juice_up() + G.round_eval:add_child(full_row, G.round_eval:get_UIE_by_ID("base_round_eval")) + play_sound("negative", (1.5 * config.pitch) or 1, 0.2) + play_sound("whoosh2", 0.9, 0.7) + if config.card then + config.card:juice_up(0.7, 0.46) + end + return true + end, })) local dollar_row = 0 if num_dollars > 60 then + G.E_MANAGER:add_event(Event({ + trigger = "before", + delay = 0.38, + func = function() + G.round_eval:add_child({ + n = G.UIT.R, + config = { align = "cm", id = "dollar_row_" .. (dollar_row + 1) .. "_" .. config.name }, + nodes = { + { + n = G.UIT.O, + config = { + object = DynaText({ + string = { localize("$") .. num_dollars }, + colours = { G.C.MONEY }, + shadow = true, + pop_in = 0, + scale = 0.65, + float = true, + }), + }, + }, + }, + }, G.round_eval:get_UIE_by_ID("dollar_" .. config.name)) + + play_sound("coin3", 0.9 + 0.2 * math.random(), 0.7) + play_sound("coin6", 1.3, 0.8) + return true + end, + })) + else + for i = 1, num_dollars or 1 do G.E_MANAGER:add_event(Event({ - trigger = 'before',delay = 0.38, - func = function() - G.round_eval:add_child( - {n=G.UIT.R, config={align = "cm", id = 'dollar_row_'..(dollar_row+1)..'_'..config.name}, nodes={ - {n=G.UIT.O, config={object = DynaText({string = {localize('$')..num_dollars}, colours = {G.C.MONEY}, shadow = true, pop_in = 0, scale = 0.65, float = true})}} - }}, - G.round_eval:get_UIE_by_ID('dollar_'..config.name)) - - play_sound('coin3', 0.9+0.2*math.random(), 0.7) - play_sound('coin6', 1.3, 0.8) - return true + trigger = "before", + delay = 0.18 - ((num_dollars > 20 and 0.13) or (num_dollars > 9 and 0.1) or 0), + func = function() + if i % 30 == 1 then + G.round_eval:add_child({ + n = G.UIT.R, + config = { + align = "cm", + id = "dollar_row_" .. (dollar_row + 1) .. "_" .. config.name, + }, + nodes = {}, + }, G.round_eval:get_UIE_by_ID("dollar_" .. config.name)) + dollar_row = dollar_row + 1 end + + local r = { + n = G.UIT.T, + config = { + text = localize("$"), + colour = G.C.MONEY, + scale = ((num_dollars > 20 and 0.28) or (num_dollars > 9 and 0.43) or 0.58), + shadow = true, + hover = true, + can_collide = false, + juice = true, + }, + } + play_sound("coin3", 0.9 + 0.2 * math.random(), 0.7 - (num_dollars > 20 and 0.2 or 0)) + + if config.name == "blind1" then + G.GAME.current_round.dollars_to_be_earned = G.GAME.current_round.dollars_to_be_earned:sub(2) + end + + G.round_eval:add_child( + r, + G.round_eval:get_UIE_by_ID("dollar_row_" .. dollar_row .. "_" .. config.name) + ) + G.VIBRATION = G.VIBRATION + 0.4 + return true + end, })) - else - for i = 1, num_dollars or 1 do - G.E_MANAGER:add_event(Event({ - trigger = 'before',delay = 0.18 - ((num_dollars > 20 and 0.13) or (num_dollars > 9 and 0.1) or 0), - func = function() - if i%30 == 1 then - G.round_eval:add_child( - {n=G.UIT.R, config={align = "cm", id = 'dollar_row_'..(dollar_row+1)..'_'..config.name}, nodes={}}, - G.round_eval:get_UIE_by_ID('dollar_'..config.name)) - dollar_row = dollar_row+1 - end - - local r = {n=G.UIT.T, config={text = localize('$'), colour = G.C.MONEY, scale = ((num_dollars > 20 and 0.28) or (num_dollars > 9 and 0.43) or 0.58), shadow = true, hover = true, can_collide = false, juice = true}} - play_sound('coin3', 0.9+0.2*math.random(), 0.7 - (num_dollars > 20 and 0.2 or 0)) - - if config.name == 'blind1' then - G.GAME.current_round.dollars_to_be_earned = G.GAME.current_round.dollars_to_be_earned:sub(2) - end - - G.round_eval:add_child(r,G.round_eval:get_UIE_by_ID('dollar_row_'..(dollar_row)..'_'..config.name)) - G.VIBRATION = G.VIBRATION + 0.4 - return true - end - })) - end + end end else add_round_eval_row_ref(config) From c18574256002527c740b5d9c6827c66a71be4079 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 22:11:08 +0000 Subject: [PATCH 0041/1128] Implemented win/lose action --- Multiplayer/Networking/Action_Handlers.lua | 19 +- Multiplayer/UI/Game_UI.lua | 282 +++++++++++++++++++++ Server/src/actionHandlers.ts | 56 ++-- 3 files changed, 329 insertions(+), 28 deletions(-) diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index cbcbfc5e..08919814 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -111,11 +111,8 @@ local function action_stop_game() end local function action_end_pvp() - -- TODO: Some logic here to say that you won - -- or lost the round - - G.STATE = G.STATES.NEW_ROUND G.STATE_COMPLETE = false + G.STATE = G.STATES.NEW_ROUND end ---@param lives number @@ -123,6 +120,16 @@ local function action_player_info(lives) G.MULTIPLAYER_GAME.lives = lives end +local function action_win_game() + win_game() + G.GAME.won = true +end + +local function action_lose_game() + G.STATE_COMPLETE = false + G.STATE = G.STATES.GAME_OVER +end + -- #region Client to Server function G.MULTIPLAYER.create_lobby() -- TODO: This is hardcoded to attrition for now, must be changed @@ -209,6 +216,10 @@ function Game:update(dt) action_end_pvp() elseif parsedAction.action == "playerInfo" then action_player_info(parsedAction.lives) + elseif parsedAction.action == "winGame" then + action_win_game() + elseif parsedAction.action == "loseGame" then + action_lose_game() elseif parsedAction.action == "error" then action_error(parsedAction.message) elseif parsedAction.action == "keepAlive" then diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index ad7e1b66..b7cbe04e 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -808,6 +808,172 @@ function Game:update_new_round(dt) G.GAME.win_ante = 8 end +local end_round_ref = end_round +function end_round() + if not G.LOBBY.code then return end_round_ref() end + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.2, + func = function() + G.RESET_BLIND_STATES = true + G.RESET_JIGGLES = true + for i = 1, #G.jokers.cards do + local eval = nil + eval = G.jokers.cards[i]:calculate_joker({end_of_round = true, game_over = game_over}) + if eval then + card_eval_status_text(G.jokers.cards[i], 'jokers', nil, nil, nil, eval) + end + end + G.GAME.unused_discards = (G.GAME.unused_discards or 0) + G.GAME.current_round.discards_left + if G.GAME.blind and G.GAME.blind.config.blind then + discover_card(G.GAME.blind.config.blind) + end + + if G.GAME.blind:get_type() == 'Boss' then + local _handname, _played, _order = 'High Card', -1, 100 + for k, v in pairs(G.GAME.hands) do + if v.played > _played or (v.played == _played and _order > v.order) then + _played = v.played + _handname = k + end + end + G.GAME.current_round.most_played_poker_hand = _handname + end + + if G.GAME.blind:get_type() == 'Boss' and not G.GAME.seeded and not G.GAME.challenge then + G.GAME.current_boss_streak = G.GAME.current_boss_streak + 1 + check_and_set_high_score('boss_streak', G.GAME.current_boss_streak) + end + + if G.GAME.current_round.hands_played == 1 then + inc_career_stat('c_single_hand_round_streak', 1) + else + if not G.GAME.seeded and not G.GAME.challenge then + G.PROFILES[G.SETTINGS.profile].career_stats.c_single_hand_round_streak = 0 + G:save_settings() + end + end + + check_for_unlock({type = 'round_win'}) + set_joker_usage() + for i=1, #G.hand.cards do + --Check for hand doubling + local reps = {1} + local j = 1 + while j <= #reps do + local percent = (i-0.999)/(#G.hand.cards-0.998) + (j-1)*0.1 + if reps[j] ~= 1 then card_eval_status_text((reps[j].jokers or reps[j].seals).card, 'jokers', nil, nil, nil, (reps[j].jokers or reps[j].seals)) end + + --calculate the hand effects + local effects = {G.hand.cards[i]:get_end_of_round_effect()} + for k=1, #G.jokers.cards do + --calculate the joker individual card effects + local eval = G.jokers.cards[k]:calculate_joker({cardarea = G.hand, other_card = G.hand.cards[i], individual = true, end_of_round = true}) + if eval then + table.insert(effects, eval) + end + end + + if reps[j] == 1 then + --Check for hand doubling + --From Red seal + local eval = eval_card(G.hand.cards[i], {end_of_round = true,cardarea = G.hand, repetition = true, repetition_only = true}) + if next(eval) and (next(effects[1]) or #effects > 1) then + for h = 1, eval.seals.repetitions do + reps[#reps+1] = eval + end + end + + --from Jokers + for j=1, #G.jokers.cards do + --calculate the joker effects + local eval = eval_card(G.jokers.cards[j], {cardarea = G.hand, other_card = G.hand.cards[i], repetition = true, end_of_round = true, card_effects = effects}) + if next(eval) then + for h = 1, eval.jokers.repetitions do + reps[#reps+1] = eval + end + end + end + end + + for ii = 1, #effects do + --if this effect came from a joker + if effects[ii].card then + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = (function() effects[ii].card:juice_up(0.7);return true end) + })) + end + + --If dollars + if effects[ii].h_dollars then + ease_dollars(effects[ii].h_dollars) + card_eval_status_text(G.hand.cards[i], 'dollars', effects[ii].h_dollars, percent) + end + + --Any extras + if effects[ii].extra then + card_eval_status_text(G.hand.cards[i], 'extra', nil, percent, nil, effects[ii].extra) + end + end + j = j + 1 + end + end + delay(0.3) + + + G.FUNCS.draw_from_hand_to_discard() + if G.GAME.blind:get_type() == 'Boss' then + G.GAME.voucher_restock = nil + if G.GAME.modifiers.set_eternal_ante and (G.GAME.round_resets.ante == G.GAME.modifiers.set_eternal_ante) then + for k, v in ipairs(G.jokers.cards) do + v:set_eternal(true) + end + end + if G.GAME.modifiers.set_joker_slots_ante and (G.GAME.round_resets.ante == G.GAME.modifiers.set_joker_slots_ante) then + G.jokers.config.card_limit = 0 + end + delay(0.4); ease_ante(1); delay(0.4); check_for_unlock({type = 'ante_up', ante = G.GAME.round_resets.ante + 1}) + end + G.FUNCS.draw_from_discard_to_deck() + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.3, + func = function() + G.STATE = G.STATES.ROUND_EVAL + G.STATE_COMPLETE = false + + if G.GAME.round_resets.blind == G.P_BLINDS.bl_small then + G.GAME.round_resets.blind_states.Small = 'Defeated' + elseif G.GAME.round_resets.blind == G.P_BLINDS.bl_big then + G.GAME.round_resets.blind_states.Big = 'Defeated' + else + G.GAME.current_round.voucher = get_next_voucher_key() + G.GAME.round_resets.blind_states.Boss = 'Defeated' + for k, v in ipairs(G.playing_cards) do + v.ability.played_this_ante = nil + end + end + + if G.GAME.round_resets.temp_handsize then G.hand:change_size(-G.GAME.round_resets.temp_handsize); G.GAME.round_resets.temp_handsize = nil end + if G.GAME.round_resets.temp_reroll_cost then G.GAME.round_resets.temp_reroll_cost = nil; calculate_reroll_cost(true) end + + reset_idol_card() + reset_mail_rank() + reset_ancient_card() + reset_castle_card() + for k, v in ipairs(G.playing_cards) do + v.ability.discarded = nil + v.ability.forced_selection = nil + end + return true + end + })) + return true + end + })) +end + local start_run_ref = Game.start_run function Game:start_run(args) if not G.LOBBY.connected or not G.LOBBY.code then @@ -838,5 +1004,121 @@ function Game:start_run(args) self.HUD:recalculate() end +local create_UIBox_game_over_ref = create_UIBox_game_over +function create_UIBox_game_over() + if G.LOBBY.code then + local eased_red = copy_table(G.GAME.round_resets.ante <= G.GAME.win_ante and G.C.RED or G.C.BLUE) + eased_red[4] = 0 + ease_value(eased_red, 4, 0.8, nil, nil, true) + local t = create_UIBox_generic_options({ bg_colour = eased_red ,no_back = true, padding = 0, contents = { + {n=G.UIT.R, config={align = "cm"}, nodes={ + {n=G.UIT.O, config={object = DynaText({string = {localize('ph_game_over')}, colours = {G.C.RED},shadow = true, float = true, scale = 1.5, pop_in = 0.4, maxw = 6.5})}}, + }}, + {n=G.UIT.R, config={align = "cm", padding = 0.15}, nodes={ + {n=G.UIT.C, config={align = "cm"}, nodes={ + {n=G.UIT.R, config={align = "cm", padding = 0.05, colour = G.C.BLACK, emboss = 0.05, r = 0.1}, nodes={ + {n=G.UIT.R, config={align = "cm", padding = 0.08}, nodes={ + create_UIBox_round_scores_row('hand'), + create_UIBox_round_scores_row('poker_hand'), + }}, + {n=G.UIT.R, config={align = "cm"}, nodes={ + {n=G.UIT.C, config={align = "cm", padding = 0.08}, nodes={ + create_UIBox_round_scores_row('cards_played', G.C.BLUE), + create_UIBox_round_scores_row('cards_discarded', G.C.RED), + create_UIBox_round_scores_row('cards_purchased', G.C.MONEY), + create_UIBox_round_scores_row('times_rerolled', G.C.GREEN), + create_UIBox_round_scores_row('new_collection', G.C.WHITE), + create_UIBox_round_scores_row('seed', G.C.WHITE), + UIBox_button({button = 'copy_seed', label = {localize('b_copy')}, colour = G.C.BLUE, scale = 0.3, minw = 2.3, minh = 0.4, focus_args = {nav = 'wide'}}), + }}, + {n=G.UIT.C, config={align = "tr", padding = 0.08}, nodes={ + create_UIBox_round_scores_row('furthest_ante', G.C.FILTER), + create_UIBox_round_scores_row('furthest_round', G.C.FILTER), + create_UIBox_round_scores_row('defeated_by'), + }} + }} + }}, + {n=G.UIT.R, config={align = "cm", padding = 0.1}, nodes={ + {n=G.UIT.R, config={id = 'from_game_over', align = "cm", minw = 5, padding = 0.1, r = 0.1, hover = true, colour = G.C.RED, button = "return_to_lobby", shadow = true, focus_args = {nav = 'wide', snap_to = true}}, nodes={ + {n=G.UIT.R, config={align = "cm", padding = 0, no_fill = true, maxw = 4.8}, nodes={ + {n=G.UIT.T, config={text = 'Retun to Lobby', scale = 0.5, colour = G.C.UI.TEXT_LIGHT}} + }} + }}, + {n=G.UIT.R, config={align = "cm", minw = 5, padding = 0.1, r = 0.1, hover = true, colour = G.C.RED, button = "lobby_leave", shadow = true, focus_args = {nav = 'wide'}}, nodes={ + {n=G.UIT.R, config={align = "cm", padding = 0, no_fill = true, maxw = 4.8}, nodes={ + {n=G.UIT.T, config={text = 'Leave Lobby', scale = 0.5, colour = G.C.UI.TEXT_LIGHT}} + }} + }} + }} + }}, + }} + }}) + t.nodes[1] = {n=G.UIT.R, config={align = "cm", padding = 0.1}, nodes={ + {n=G.UIT.C, config={align = "cm", padding = 2}, nodes={ + {n=G.UIT.R, config={align = "cm"}, nodes={ + {n=G.UIT.O, config={padding = 0, id = 'jimbo_spot', object = Moveable(0,0,G.CARD_W*1.1, G.CARD_H*1.1)}}, + }}, + }}, + {n=G.UIT.C, config={align = "cm", padding = 0.1}, nodes={t.nodes[1]}}} + } + + --t.nodes[1].config.mid = true + return t + end + return create_UIBox_game_over_ref() +end + +local create_UIBox_win_ref = create_UIBox_win +function create_UIBox_win() + if G.LOBBY.code then + local eased_green = copy_table(G.C.GREEN) + eased_green[4] = 0 + ease_value(eased_green, 4, 0.5, nil, nil, true) + local t = create_UIBox_generic_options({ padding = 0, bg_colour = eased_green , colour = G.C.BLACK, outline_colour = G.C.EDITION, no_back = true, no_esc = true, contents = { + {n=G.UIT.R, config={align = "cm"}, nodes={ + {n=G.UIT.O, config={object = DynaText({string = {localize('ph_you_win')}, colours = {G.C.EDITION},shadow = true, float = true, spacing = 10, rotate = true, scale = 1.5, pop_in = 0.4, maxw = 6.5})}}, + }}, + {n=G.UIT.R, config={align = "cm", padding = 0.15}, nodes={ + {n=G.UIT.C, config={align = "cm"}, nodes={ + {n=G.UIT.R, config={align = "cm", padding = 0.08}, nodes={ + create_UIBox_round_scores_row('hand'), + create_UIBox_round_scores_row('poker_hand'), + }}, + {n=G.UIT.R, config={align = "cm"}, nodes={ + {n=G.UIT.C, config={align = "cm", padding = 0.08}, nodes={ + create_UIBox_round_scores_row('cards_played', G.C.BLUE), + create_UIBox_round_scores_row('cards_discarded', G.C.RED), + create_UIBox_round_scores_row('cards_purchased', G.C.MONEY), + create_UIBox_round_scores_row('times_rerolled', G.C.GREEN), + create_UIBox_round_scores_row('new_collection', G.C.WHITE), + create_UIBox_round_scores_row('seed', G.C.WHITE), + UIBox_button({button = 'copy_seed', label = {localize('b_copy')}, colour = G.C.BLUE, scale = 0.3, minw = 2.3, minh = 0.4,}), + }}, + {n=G.UIT.C, config={align = "tr", padding = 0.08}, nodes={ + create_UIBox_round_scores_row('furthest_ante', G.C.FILTER), + create_UIBox_round_scores_row('furthest_round', G.C.FILTER), + {n=G.UIT.R, config={align = "cm", minh = 0.4, minw = 0.1}, nodes={}}, + UIBox_button({id = 'from_game_won', button = 'return_to_lobby', label = {'Return to','Lobby'}, minw = 2.5, maxw = 2.5, minh = 1, focus_args = {nav = 'wide', snap_to = true}}) or nil, + {n=G.UIT.R, config={align = "cm", minh = 0.2, minw = 0.1}, nodes={}} or nil, + UIBox_button({button = 'lobby_leave', label = {'Leave','Lobby'}, minw = 2.5, maxw = 2.5, minh = 1, focus_args = {nav = 'wide'}}) or nil, + }} + }} + }} + }} + }}) + t.nodes[1] = {n=G.UIT.R, config={align = "cm", padding = 0.1}, nodes={ + {n=G.UIT.C, config={align = "cm", padding = 2}, nodes={ + {n=G.UIT.O, config={padding = 0, id = 'jimbo_spot', object = Moveable(0,0,G.CARD_W*1.1, G.CARD_H*1.1)}}, + }}, + {n=G.UIT.C, config={align = "cm", padding = 0.1}, nodes={t.nodes[1]} + }} + } + --t.nodes[1].config.mid = true + t.config.id = 'you_win_UI' + return t + end + return create_UIBox_win_ref() +end + ---------------------------------------------- ------------MOD GAME UI END------------------- diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index e2d21fc2..08fda204 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -141,32 +141,40 @@ const playHandAction = ( console.log( `Host hands: ${lobby.host?.handsLeft}, Guest hands: ${lobby.guest?.handsLeft}`, ) + + if (!lobby.host || !lobby.guest) { + stopGameAction(client) + return + } // This info is only sent on a boss blind, so it shouldn't // affect other blinds - if (lobby.host?.handsLeft === 0 && lobby.guest?.handsLeft === 0) { - const roundWinner = - lobby.host.score > lobby.guest.score ? lobby.host : lobby.guest - const roundLoser = - roundWinner.id === lobby.host.id ? lobby.guest : lobby.host - - roundLoser.lives -= 1 - roundLoser.sendAction({ action: 'playerInfo', lives: roundLoser.lives }) - - // If no lives are left, we end the game - if (lobby.host.lives === 0 || lobby.guest.lives === 0) { - const gameWinner = - lobby.host.lives > lobby.guest.lives ? lobby.host : lobby.guest - const gameLoser = - gameWinner.id === lobby.host.id ? lobby.guest : lobby.host - - stopGameAction(client) - - // TODO: Announce who won - return - } - - roundWinner.sendAction({ action: 'endPvP', lost: false }) - roundLoser.sendAction({ action: 'endPvP', lost: true }) + if ((lobby.guest.handsLeft === 0 && lobby.host.score > lobby.guest.score) || + (lobby.host.handsLeft === 0 && lobby.guest.score > lobby.host.score) || + (lobby.host.handsLeft === 0 && lobby.guest.handsLeft === 0)) { + const roundWinner = + lobby.host.score > lobby.guest.score ? lobby.host : lobby.guest + const roundLoser = + roundWinner.id === lobby.host.id ? lobby.guest : lobby.host + + if (lobby.host.score !== lobby.guest.score) { + roundLoser.lives -= 1 + roundLoser.sendAction({ action: 'playerInfo', lives: roundLoser.lives }) + + // If no lives are left, we end the game + if (lobby.host.lives === 0 || lobby.guest.lives === 0) { + const gameWinner = + lobby.host.lives > lobby.guest.lives ? lobby.host : lobby.guest + const gameLoser = + gameWinner.id === lobby.host.id ? lobby.guest : lobby.host + + gameWinner?.sendAction({ action: 'winGame' }) + gameLoser?.sendAction({ action: 'loseGame' }) + return + } + } + + roundWinner.sendAction({ action: 'endPvP', lost: false }) + roundLoser.sendAction({ action: 'endPvP', lost: true }) } } From a494b868cdd10413bf6075248bf48031924de7d7 Mon Sep 17 00:00:00 2001 From: TGMM Date: Sat, 23 Mar 2024 15:27:56 -0700 Subject: [PATCH 0042/1128] Ran formatter on files --- Multiplayer/Networking/Action_Handlers.lua | 2 +- Multiplayer/UI/Game_UI.lua | 741 ++++++++++++++------- Server/src/actionHandlers.ts | 52 +- 3 files changed, 535 insertions(+), 260 deletions(-) diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index 08919814..4b58b948 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -122,7 +122,7 @@ end local function action_win_game() win_game() - G.GAME.won = true + G.GAME.won = true end local function action_lose_game() diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index ccfdf973..39e4638d 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -813,167 +813,209 @@ end local end_round_ref = end_round function end_round() - if not G.LOBBY.code then return end_round_ref() end + if not G.LOBBY.code then + return end_round_ref() + end G.E_MANAGER:add_event(Event({ - trigger = 'after', + trigger = "after", delay = 0.2, func = function() G.RESET_BLIND_STATES = true G.RESET_JIGGLES = true - for i = 1, #G.jokers.cards do - local eval = nil - eval = G.jokers.cards[i]:calculate_joker({end_of_round = true, game_over = game_over}) - if eval then - card_eval_status_text(G.jokers.cards[i], 'jokers', nil, nil, nil, eval) - end + for i = 1, #G.jokers.cards do + local eval = nil + eval = G.jokers.cards[i]:calculate_joker({ end_of_round = true, game_over = game_over }) + if eval then + card_eval_status_text(G.jokers.cards[i], "jokers", nil, nil, nil, eval) + end + end + G.GAME.unused_discards = (G.GAME.unused_discards or 0) + G.GAME.current_round.discards_left + if G.GAME.blind and G.GAME.blind.config.blind then + discover_card(G.GAME.blind.config.blind) + end + + if G.GAME.blind:get_type() == "Boss" then + local _handname, _played, _order = "High Card", -1, 100 + for k, v in pairs(G.GAME.hands) do + if v.played > _played or (v.played == _played and _order > v.order) then + _played = v.played + _handname = k end - G.GAME.unused_discards = (G.GAME.unused_discards or 0) + G.GAME.current_round.discards_left - if G.GAME.blind and G.GAME.blind.config.blind then - discover_card(G.GAME.blind.config.blind) + end + G.GAME.current_round.most_played_poker_hand = _handname + end + + if G.GAME.blind:get_type() == "Boss" and not G.GAME.seeded and not G.GAME.challenge then + G.GAME.current_boss_streak = G.GAME.current_boss_streak + 1 + check_and_set_high_score("boss_streak", G.GAME.current_boss_streak) + end + + if G.GAME.current_round.hands_played == 1 then + inc_career_stat("c_single_hand_round_streak", 1) + else + if not G.GAME.seeded and not G.GAME.challenge then + G.PROFILES[G.SETTINGS.profile].career_stats.c_single_hand_round_streak = 0 + G:save_settings() + end + end + + check_for_unlock({ type = "round_win" }) + set_joker_usage() + for i = 1, #G.hand.cards do + --Check for hand doubling + local reps = { 1 } + local j = 1 + while j <= #reps do + local percent = (i - 0.999) / (#G.hand.cards - 0.998) + (j - 1) * 0.1 + if reps[j] ~= 1 then + card_eval_status_text( + (reps[j].jokers or reps[j].seals).card, + "jokers", + nil, + nil, + nil, + (reps[j].jokers or reps[j].seals) + ) end - if G.GAME.blind:get_type() == 'Boss' then - local _handname, _played, _order = 'High Card', -1, 100 - for k, v in pairs(G.GAME.hands) do - if v.played > _played or (v.played == _played and _order > v.order) then - _played = v.played - _handname = k - end + --calculate the hand effects + local effects = { G.hand.cards[i]:get_end_of_round_effect() } + for k = 1, #G.jokers.cards do + --calculate the joker individual card effects + local eval = G.jokers.cards[k]:calculate_joker({ + cardarea = G.hand, + other_card = G.hand.cards[i], + individual = true, + end_of_round = true, + }) + if eval then + table.insert(effects, eval) + end + end + + if reps[j] == 1 then + --Check for hand doubling + --From Red seal + local eval = eval_card( + G.hand.cards[i], + { end_of_round = true, cardarea = G.hand, repetition = true, repetition_only = true } + ) + if next(eval) and (next(effects[1]) or #effects > 1) then + for h = 1, eval.seals.repetitions do + reps[#reps + 1] = eval + end + end + + --from Jokers + for j = 1, #G.jokers.cards do + --calculate the joker effects + local eval = eval_card(G.jokers.cards[j], { + cardarea = G.hand, + other_card = G.hand.cards[i], + repetition = true, + end_of_round = true, + card_effects = effects, + }) + if next(eval) then + for h = 1, eval.jokers.repetitions do + reps[#reps + 1] = eval + end end - G.GAME.current_round.most_played_poker_hand = _handname + end end - if G.GAME.blind:get_type() == 'Boss' and not G.GAME.seeded and not G.GAME.challenge then - G.GAME.current_boss_streak = G.GAME.current_boss_streak + 1 - check_and_set_high_score('boss_streak', G.GAME.current_boss_streak) + for ii = 1, #effects do + --if this effect came from a joker + if effects[ii].card then + G.E_MANAGER:add_event(Event({ + trigger = "immediate", + func = function() + effects[ii].card:juice_up(0.7) + return true + end, + })) + end + + --If dollars + if effects[ii].h_dollars then + ease_dollars(effects[ii].h_dollars) + card_eval_status_text(G.hand.cards[i], "dollars", effects[ii].h_dollars, percent) + end + + --Any extras + if effects[ii].extra then + card_eval_status_text(G.hand.cards[i], "extra", nil, percent, nil, effects[ii].extra) + end end - - if G.GAME.current_round.hands_played == 1 then - inc_career_stat('c_single_hand_round_streak', 1) - else - if not G.GAME.seeded and not G.GAME.challenge then - G.PROFILES[G.SETTINGS.profile].career_stats.c_single_hand_round_streak = 0 - G:save_settings() - end + j = j + 1 + end + end + delay(0.3) + + G.FUNCS.draw_from_hand_to_discard() + if G.GAME.blind:get_type() == "Boss" then + G.GAME.voucher_restock = nil + if + G.GAME.modifiers.set_eternal_ante + and (G.GAME.round_resets.ante == G.GAME.modifiers.set_eternal_ante) + then + for k, v in ipairs(G.jokers.cards) do + v:set_eternal(true) end + end + if + G.GAME.modifiers.set_joker_slots_ante + and (G.GAME.round_resets.ante == G.GAME.modifiers.set_joker_slots_ante) + then + G.jokers.config.card_limit = 0 + end + delay(0.4) + ease_ante(1) + delay(0.4) + check_for_unlock({ type = "ante_up", ante = G.GAME.round_resets.ante + 1 }) + end + G.FUNCS.draw_from_discard_to_deck() + G.E_MANAGER:add_event(Event({ + trigger = "after", + delay = 0.3, + func = function() + G.STATE = G.STATES.ROUND_EVAL + G.STATE_COMPLETE = false - check_for_unlock({type = 'round_win'}) - set_joker_usage() - for i=1, #G.hand.cards do - --Check for hand doubling - local reps = {1} - local j = 1 - while j <= #reps do - local percent = (i-0.999)/(#G.hand.cards-0.998) + (j-1)*0.1 - if reps[j] ~= 1 then card_eval_status_text((reps[j].jokers or reps[j].seals).card, 'jokers', nil, nil, nil, (reps[j].jokers or reps[j].seals)) end - - --calculate the hand effects - local effects = {G.hand.cards[i]:get_end_of_round_effect()} - for k=1, #G.jokers.cards do - --calculate the joker individual card effects - local eval = G.jokers.cards[k]:calculate_joker({cardarea = G.hand, other_card = G.hand.cards[i], individual = true, end_of_round = true}) - if eval then - table.insert(effects, eval) - end - end - - if reps[j] == 1 then - --Check for hand doubling - --From Red seal - local eval = eval_card(G.hand.cards[i], {end_of_round = true,cardarea = G.hand, repetition = true, repetition_only = true}) - if next(eval) and (next(effects[1]) or #effects > 1) then - for h = 1, eval.seals.repetitions do - reps[#reps+1] = eval - end - end - - --from Jokers - for j=1, #G.jokers.cards do - --calculate the joker effects - local eval = eval_card(G.jokers.cards[j], {cardarea = G.hand, other_card = G.hand.cards[i], repetition = true, end_of_round = true, card_effects = effects}) - if next(eval) then - for h = 1, eval.jokers.repetitions do - reps[#reps+1] = eval - end - end - end - end - - for ii = 1, #effects do - --if this effect came from a joker - if effects[ii].card then - G.E_MANAGER:add_event(Event({ - trigger = 'immediate', - func = (function() effects[ii].card:juice_up(0.7);return true end) - })) - end - - --If dollars - if effects[ii].h_dollars then - ease_dollars(effects[ii].h_dollars) - card_eval_status_text(G.hand.cards[i], 'dollars', effects[ii].h_dollars, percent) - end - - --Any extras - if effects[ii].extra then - card_eval_status_text(G.hand.cards[i], 'extra', nil, percent, nil, effects[ii].extra) - end - end - j = j + 1 - end + if G.GAME.round_resets.blind == G.P_BLINDS.bl_small then + G.GAME.round_resets.blind_states.Small = "Defeated" + elseif G.GAME.round_resets.blind == G.P_BLINDS.bl_big then + G.GAME.round_resets.blind_states.Big = "Defeated" + else + G.GAME.current_round.voucher = get_next_voucher_key() + G.GAME.round_resets.blind_states.Boss = "Defeated" + for k, v in ipairs(G.playing_cards) do + v.ability.played_this_ante = nil + end end - delay(0.3) + if G.GAME.round_resets.temp_handsize then + G.hand:change_size(-G.GAME.round_resets.temp_handsize) + G.GAME.round_resets.temp_handsize = nil + end + if G.GAME.round_resets.temp_reroll_cost then + G.GAME.round_resets.temp_reroll_cost = nil + calculate_reroll_cost(true) + end - G.FUNCS.draw_from_hand_to_discard() - if G.GAME.blind:get_type() == 'Boss' then - G.GAME.voucher_restock = nil - if G.GAME.modifiers.set_eternal_ante and (G.GAME.round_resets.ante == G.GAME.modifiers.set_eternal_ante) then - for k, v in ipairs(G.jokers.cards) do - v:set_eternal(true) - end - end - if G.GAME.modifiers.set_joker_slots_ante and (G.GAME.round_resets.ante == G.GAME.modifiers.set_joker_slots_ante) then - G.jokers.config.card_limit = 0 - end - delay(0.4); ease_ante(1); delay(0.4); check_for_unlock({type = 'ante_up', ante = G.GAME.round_resets.ante + 1}) + reset_idol_card() + reset_mail_rank() + reset_ancient_card() + reset_castle_card() + for k, v in ipairs(G.playing_cards) do + v.ability.discarded = nil + v.ability.forced_selection = nil end - G.FUNCS.draw_from_discard_to_deck() - G.E_MANAGER:add_event(Event({ - trigger = 'after', - delay = 0.3, - func = function() - G.STATE = G.STATES.ROUND_EVAL - G.STATE_COMPLETE = false - - if G.GAME.round_resets.blind == G.P_BLINDS.bl_small then - G.GAME.round_resets.blind_states.Small = 'Defeated' - elseif G.GAME.round_resets.blind == G.P_BLINDS.bl_big then - G.GAME.round_resets.blind_states.Big = 'Defeated' - else - G.GAME.current_round.voucher = get_next_voucher_key() - G.GAME.round_resets.blind_states.Boss = 'Defeated' - for k, v in ipairs(G.playing_cards) do - v.ability.played_this_ante = nil - end - end - - if G.GAME.round_resets.temp_handsize then G.hand:change_size(-G.GAME.round_resets.temp_handsize); G.GAME.round_resets.temp_handsize = nil end - if G.GAME.round_resets.temp_reroll_cost then G.GAME.round_resets.temp_reroll_cost = nil; calculate_reroll_cost(true) end - - reset_idol_card() - reset_mail_rank() - reset_ancient_card() - reset_castle_card() - for k, v in ipairs(G.playing_cards) do - v.ability.discarded = nil - v.ability.forced_selection = nil - end - return true - end - })) + return true + end, + })) return true - end + end, })) end @@ -1007,64 +1049,201 @@ function Game:start_run(args) self.HUD:recalculate() end - local create_UIBox_game_over_ref = create_UIBox_game_over function create_UIBox_game_over() if G.LOBBY.code then local eased_red = copy_table(G.GAME.round_resets.ante <= G.GAME.win_ante and G.C.RED or G.C.BLUE) eased_red[4] = 0 ease_value(eased_red, 4, 0.8, nil, nil, true) - local t = create_UIBox_generic_options({ bg_colour = eased_red ,no_back = true, padding = 0, contents = { - {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.O, config={object = DynaText({string = {localize('ph_game_over')}, colours = {G.C.RED},shadow = true, float = true, scale = 1.5, pop_in = 0.4, maxw = 6.5})}}, - }}, - {n=G.UIT.R, config={align = "cm", padding = 0.15}, nodes={ - {n=G.UIT.C, config={align = "cm"}, nodes={ - {n=G.UIT.R, config={align = "cm", padding = 0.05, colour = G.C.BLACK, emboss = 0.05, r = 0.1}, nodes={ - {n=G.UIT.R, config={align = "cm", padding = 0.08}, nodes={ - create_UIBox_round_scores_row('hand'), - create_UIBox_round_scores_row('poker_hand'), - }}, - {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.C, config={align = "cm", padding = 0.08}, nodes={ - create_UIBox_round_scores_row('cards_played', G.C.BLUE), - create_UIBox_round_scores_row('cards_discarded', G.C.RED), - create_UIBox_round_scores_row('cards_purchased', G.C.MONEY), - create_UIBox_round_scores_row('times_rerolled', G.C.GREEN), - create_UIBox_round_scores_row('new_collection', G.C.WHITE), - create_UIBox_round_scores_row('seed', G.C.WHITE), - UIBox_button({button = 'copy_seed', label = {localize('b_copy')}, colour = G.C.BLUE, scale = 0.3, minw = 2.3, minh = 0.4, focus_args = {nav = 'wide'}}), - }}, - {n=G.UIT.C, config={align = "tr", padding = 0.08}, nodes={ - create_UIBox_round_scores_row('furthest_ante', G.C.FILTER), - create_UIBox_round_scores_row('furthest_round', G.C.FILTER), - create_UIBox_round_scores_row('defeated_by'), - }} - }} - }}, - {n=G.UIT.R, config={align = "cm", padding = 0.1}, nodes={ - {n=G.UIT.R, config={id = 'from_game_over', align = "cm", minw = 5, padding = 0.1, r = 0.1, hover = true, colour = G.C.RED, button = "return_to_lobby", shadow = true, focus_args = {nav = 'wide', snap_to = true}}, nodes={ - {n=G.UIT.R, config={align = "cm", padding = 0, no_fill = true, maxw = 4.8}, nodes={ - {n=G.UIT.T, config={text = 'Retun to Lobby', scale = 0.5, colour = G.C.UI.TEXT_LIGHT}} - }} - }}, - {n=G.UIT.R, config={align = "cm", minw = 5, padding = 0.1, r = 0.1, hover = true, colour = G.C.RED, button = "lobby_leave", shadow = true, focus_args = {nav = 'wide'}}, nodes={ - {n=G.UIT.R, config={align = "cm", padding = 0, no_fill = true, maxw = 4.8}, nodes={ - {n=G.UIT.T, config={text = 'Leave Lobby', scale = 0.5, colour = G.C.UI.TEXT_LIGHT}} - }} - }} - }} - }}, - }} - }}) - t.nodes[1] = {n=G.UIT.R, config={align = "cm", padding = 0.1}, nodes={ - {n=G.UIT.C, config={align = "cm", padding = 2}, nodes={ - {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.O, config={padding = 0, id = 'jimbo_spot', object = Moveable(0,0,G.CARD_W*1.1, G.CARD_H*1.1)}}, - }}, - }}, - {n=G.UIT.C, config={align = "cm", padding = 0.1}, nodes={t.nodes[1]}}} - } + local t = create_UIBox_generic_options({ + bg_colour = eased_red, + no_back = true, + padding = 0, + contents = { + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.O, + config = { + object = DynaText({ + string = { localize("ph_game_over") }, + colours = { G.C.RED }, + shadow = true, + float = true, + scale = 1.5, + pop_in = 0.4, + maxw = 6.5, + }), + }, + }, + }, + }, + { + n = G.UIT.R, + config = { align = "cm", padding = 0.15 }, + nodes = { + { + n = G.UIT.C, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm", + padding = 0.05, + colour = G.C.BLACK, + emboss = 0.05, + r = 0.1, + }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm", padding = 0.08 }, + nodes = { + create_UIBox_round_scores_row("hand"), + create_UIBox_round_scores_row("poker_hand"), + }, + }, + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.C, + config = { align = "cm", padding = 0.08 }, + nodes = { + create_UIBox_round_scores_row("cards_played", G.C.BLUE), + create_UIBox_round_scores_row("cards_discarded", G.C.RED), + create_UIBox_round_scores_row("cards_purchased", G.C.MONEY), + create_UIBox_round_scores_row("times_rerolled", G.C.GREEN), + create_UIBox_round_scores_row("new_collection", G.C.WHITE), + create_UIBox_round_scores_row("seed", G.C.WHITE), + UIBox_button({ + button = "copy_seed", + label = { localize("b_copy") }, + colour = G.C.BLUE, + scale = 0.3, + minw = 2.3, + minh = 0.4, + focus_args = { nav = "wide" }, + }), + }, + }, + { + n = G.UIT.C, + config = { align = "tr", padding = 0.08 }, + nodes = { + create_UIBox_round_scores_row("furthest_ante", G.C.FILTER), + create_UIBox_round_scores_row("furthest_round", G.C.FILTER), + create_UIBox_round_scores_row("defeated_by"), + }, + }, + }, + }, + }, + }, + { + n = G.UIT.R, + config = { align = "cm", padding = 0.1 }, + nodes = { + { + n = G.UIT.R, + config = { + id = "from_game_over", + align = "cm", + minw = 5, + padding = 0.1, + r = 0.1, + hover = true, + colour = G.C.RED, + button = "return_to_lobby", + shadow = true, + focus_args = { nav = "wide", snap_to = true }, + }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm", padding = 0, no_fill = true, maxw = 4.8 }, + nodes = { + { + n = G.UIT.T, + config = { + text = "Retun to Lobby", + scale = 0.5, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + }, + }, + }, + { + n = G.UIT.R, + config = { + align = "cm", + minw = 5, + padding = 0.1, + r = 0.1, + hover = true, + colour = G.C.RED, + button = "lobby_leave", + shadow = true, + focus_args = { nav = "wide" }, + }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm", padding = 0, no_fill = true, maxw = 4.8 }, + nodes = { + { + n = G.UIT.T, + config = { + text = "Leave Lobby", + scale = 0.5, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }) + t.nodes[1] = { + n = G.UIT.R, + config = { align = "cm", padding = 0.1 }, + nodes = { + { + n = G.UIT.C, + config = { align = "cm", padding = 2 }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.O, + config = { + padding = 0, + id = "jimbo_spot", + object = Moveable(0, 0, G.CARD_W * 1.1, G.CARD_H * 1.1), + }, + }, + }, + }, + }, + }, + { n = G.UIT.C, config = { align = "cm", padding = 0.1 }, nodes = { t.nodes[1] } }, + }, + } --t.nodes[1].config.mid = true return t @@ -1078,53 +1257,147 @@ function create_UIBox_win() local eased_green = copy_table(G.C.GREEN) eased_green[4] = 0 ease_value(eased_green, 4, 0.5, nil, nil, true) - local t = create_UIBox_generic_options({ padding = 0, bg_colour = eased_green , colour = G.C.BLACK, outline_colour = G.C.EDITION, no_back = true, no_esc = true, contents = { - {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.O, config={object = DynaText({string = {localize('ph_you_win')}, colours = {G.C.EDITION},shadow = true, float = true, spacing = 10, rotate = true, scale = 1.5, pop_in = 0.4, maxw = 6.5})}}, - }}, - {n=G.UIT.R, config={align = "cm", padding = 0.15}, nodes={ - {n=G.UIT.C, config={align = "cm"}, nodes={ - {n=G.UIT.R, config={align = "cm", padding = 0.08}, nodes={ - create_UIBox_round_scores_row('hand'), - create_UIBox_round_scores_row('poker_hand'), - }}, - {n=G.UIT.R, config={align = "cm"}, nodes={ - {n=G.UIT.C, config={align = "cm", padding = 0.08}, nodes={ - create_UIBox_round_scores_row('cards_played', G.C.BLUE), - create_UIBox_round_scores_row('cards_discarded', G.C.RED), - create_UIBox_round_scores_row('cards_purchased', G.C.MONEY), - create_UIBox_round_scores_row('times_rerolled', G.C.GREEN), - create_UIBox_round_scores_row('new_collection', G.C.WHITE), - create_UIBox_round_scores_row('seed', G.C.WHITE), - UIBox_button({button = 'copy_seed', label = {localize('b_copy')}, colour = G.C.BLUE, scale = 0.3, minw = 2.3, minh = 0.4,}), - }}, - {n=G.UIT.C, config={align = "tr", padding = 0.08}, nodes={ - create_UIBox_round_scores_row('furthest_ante', G.C.FILTER), - create_UIBox_round_scores_row('furthest_round', G.C.FILTER), - {n=G.UIT.R, config={align = "cm", minh = 0.4, minw = 0.1}, nodes={}}, - UIBox_button({id = 'from_game_won', button = 'return_to_lobby', label = {'Return to','Lobby'}, minw = 2.5, maxw = 2.5, minh = 1, focus_args = {nav = 'wide', snap_to = true}}) or nil, - {n=G.UIT.R, config={align = "cm", minh = 0.2, minw = 0.1}, nodes={}} or nil, - UIBox_button({button = 'lobby_leave', label = {'Leave','Lobby'}, minw = 2.5, maxw = 2.5, minh = 1, focus_args = {nav = 'wide'}}) or nil, - }} - }} - }} - }} - }}) - t.nodes[1] = {n=G.UIT.R, config={align = "cm", padding = 0.1}, nodes={ - {n=G.UIT.C, config={align = "cm", padding = 2}, nodes={ - {n=G.UIT.O, config={padding = 0, id = 'jimbo_spot', object = Moveable(0,0,G.CARD_W*1.1, G.CARD_H*1.1)}}, - }}, - {n=G.UIT.C, config={align = "cm", padding = 0.1}, nodes={t.nodes[1]} - }} + local t = create_UIBox_generic_options({ + padding = 0, + bg_colour = eased_green, + colour = G.C.BLACK, + outline_colour = G.C.EDITION, + no_back = true, + no_esc = true, + contents = { + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.O, + config = { + object = DynaText({ + string = { localize("ph_you_win") }, + colours = { G.C.EDITION }, + shadow = true, + float = true, + spacing = 10, + rotate = true, + scale = 1.5, + pop_in = 0.4, + maxw = 6.5, + }), + }, + }, + }, + }, + { + n = G.UIT.R, + config = { align = "cm", padding = 0.15 }, + nodes = { + { + n = G.UIT.C, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm", padding = 0.08 }, + nodes = { + create_UIBox_round_scores_row("hand"), + create_UIBox_round_scores_row("poker_hand"), + }, + }, + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.C, + config = { align = "cm", padding = 0.08 }, + nodes = { + create_UIBox_round_scores_row("cards_played", G.C.BLUE), + create_UIBox_round_scores_row("cards_discarded", G.C.RED), + create_UIBox_round_scores_row("cards_purchased", G.C.MONEY), + create_UIBox_round_scores_row("times_rerolled", G.C.GREEN), + create_UIBox_round_scores_row("new_collection", G.C.WHITE), + create_UIBox_round_scores_row("seed", G.C.WHITE), + UIBox_button({ + button = "copy_seed", + label = { localize("b_copy") }, + colour = G.C.BLUE, + scale = 0.3, + minw = 2.3, + minh = 0.4, + }), + }, + }, + { + n = G.UIT.C, + config = { align = "tr", padding = 0.08 }, + nodes = { + create_UIBox_round_scores_row("furthest_ante", G.C.FILTER), + create_UIBox_round_scores_row("furthest_round", G.C.FILTER), + { + n = G.UIT.R, + config = { align = "cm", minh = 0.4, minw = 0.1 }, + nodes = {}, + }, + UIBox_button({ + id = "from_game_won", + button = "return_to_lobby", + label = { "Return to", "Lobby" }, + minw = 2.5, + maxw = 2.5, + minh = 1, + focus_args = { nav = "wide", snap_to = true }, + }) or nil, + { + n = G.UIT.R, + config = { align = "cm", minh = 0.2, minw = 0.1 }, + nodes = {}, + } or nil, + UIBox_button({ + button = "lobby_leave", + label = { "Leave", "Lobby" }, + minw = 2.5, + maxw = 2.5, + minh = 1, + focus_args = { nav = "wide" }, + }) or nil, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }) + t.nodes[1] = { + n = G.UIT.R, + config = { align = "cm", padding = 0.1 }, + nodes = { + { + n = G.UIT.C, + config = { align = "cm", padding = 2 }, + nodes = { + { + n = G.UIT.O, + config = { + padding = 0, + id = "jimbo_spot", + object = Moveable(0, 0, G.CARD_W * 1.1, G.CARD_H * 1.1), + }, + }, + }, + }, + { n = G.UIT.C, config = { align = "cm", padding = 0.1 }, nodes = { t.nodes[1] } }, + }, } --t.nodes[1].config.mid = true - t.config.id = 'you_win_UI' + t.config.id = "you_win_UI" return t end return create_UIBox_win_ref() end - local add_round_eval_row_ref = add_round_eval_row function add_round_eval_row(config) if G.LOBBY.code and config.name == "blind1" and G.GAME.blind.chips == -1 then diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 08fda204..9655b7f7 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -148,33 +148,35 @@ const playHandAction = ( } // This info is only sent on a boss blind, so it shouldn't // affect other blinds - if ((lobby.guest.handsLeft === 0 && lobby.host.score > lobby.guest.score) || - (lobby.host.handsLeft === 0 && lobby.guest.score > lobby.host.score) || - (lobby.host.handsLeft === 0 && lobby.guest.handsLeft === 0)) { - const roundWinner = - lobby.host.score > lobby.guest.score ? lobby.host : lobby.guest - const roundLoser = - roundWinner.id === lobby.host.id ? lobby.guest : lobby.host - - if (lobby.host.score !== lobby.guest.score) { - roundLoser.lives -= 1 - roundLoser.sendAction({ action: 'playerInfo', lives: roundLoser.lives }) - - // If no lives are left, we end the game - if (lobby.host.lives === 0 || lobby.guest.lives === 0) { - const gameWinner = - lobby.host.lives > lobby.guest.lives ? lobby.host : lobby.guest - const gameLoser = - gameWinner.id === lobby.host.id ? lobby.guest : lobby.host - - gameWinner?.sendAction({ action: 'winGame' }) - gameLoser?.sendAction({ action: 'loseGame' }) - return - } + if ( + (lobby.guest.handsLeft === 0 && lobby.host.score > lobby.guest.score) || + (lobby.host.handsLeft === 0 && lobby.guest.score > lobby.host.score) || + (lobby.host.handsLeft === 0 && lobby.guest.handsLeft === 0) + ) { + const roundWinner = + lobby.host.score > lobby.guest.score ? lobby.host : lobby.guest + const roundLoser = + roundWinner.id === lobby.host.id ? lobby.guest : lobby.host + + if (lobby.host.score !== lobby.guest.score) { + roundLoser.lives -= 1 + roundLoser.sendAction({ action: 'playerInfo', lives: roundLoser.lives }) + + // If no lives are left, we end the game + if (lobby.host.lives === 0 || lobby.guest.lives === 0) { + const gameWinner = + lobby.host.lives > lobby.guest.lives ? lobby.host : lobby.guest + const gameLoser = + gameWinner.id === lobby.host.id ? lobby.guest : lobby.host + + gameWinner?.sendAction({ action: 'winGame' }) + gameLoser?.sendAction({ action: 'loseGame' }) + return } + } - roundWinner.sendAction({ action: 'endPvP', lost: false }) - roundLoser.sendAction({ action: 'endPvP', lost: true }) + roundWinner.sendAction({ action: 'endPvP', lost: false }) + roundLoser.sendAction({ action: 'endPvP', lost: true }) } } From 524db22c48f0948f0ffc046f0263cf14c45981f6 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 22:36:54 +0000 Subject: [PATCH 0043/1128] Set dollars to 0 when blind chips aren't met --- Multiplayer/UI/Game_UI.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 9ed870f4..7b082204 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -800,6 +800,7 @@ function Game:update_new_round(dt) -- Prevent player from losing if G.GAME.chips - G.GAME.blind.chips < 0 then G.GAME.blind.chips = -1 + G.GAME.blind.dollars = 0 end -- Prevent player from winning From 9174d1f298cba763e8aadcf17966116722025dec Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 22:40:13 +0000 Subject: [PATCH 0044/1128] Removed comments from souce code --- Multiplayer/UI/Game_UI.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 39e4638d..ff8811dc 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1245,7 +1245,6 @@ function create_UIBox_game_over() }, } - --t.nodes[1].config.mid = true return t end return create_UIBox_game_over_ref() @@ -1391,7 +1390,6 @@ function create_UIBox_win() { n = G.UIT.C, config = { align = "cm", padding = 0.1 }, nodes = { t.nodes[1] } }, }, } - --t.nodes[1].config.mid = true t.config.id = "you_win_UI" return t end From 62724a82bf4a3b9c0eec6fc3219a5b93791d37a2 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 23:19:17 +0000 Subject: [PATCH 0045/1128] Implemented ease_lives and stopped ease_ante from displaying --- Multiplayer/Lobby.lua | 2 +- Multiplayer/Networking/Action_Handlers.lua | 3 ++ Multiplayer/UI/Game_UI.lua | 42 ++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index b5b0ed31..71591455 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -26,7 +26,7 @@ G.MULTIPLAYER_GAME = { ready_blind = false, ready_blind_text = "Ready", processed_round_done = false, - lives = 3, + lives = 0, } PREV_ACHIEVEMENT_VALUE = true diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index 4b58b948..a650454f 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -117,6 +117,9 @@ end ---@param lives number local function action_player_info(lives) + if (G.MULTIPLAYER_GAME.lives ~= lives) then + ease_lives(lives - G.MULTIPLAYER_GAME.lives) + end G.MULTIPLAYER_GAME.lives = lives end diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 23aaa1fc..2eef3fcb 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1560,5 +1560,47 @@ function add_round_eval_row(config) add_round_eval_row_ref(config) end end + +local ease_ante_ref = ease_ante +function ease_ante(mod) + if not G.LOBBY.code then return ease_ante_ref(mod) end + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + G.GAME.round_resets.ante = G.GAME.round_resets.ante + mod + check_and_set_high_score('furthest_ante', G.GAME.round_resets.ante) + return true + end + })) +end + +function ease_lives(mod) + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + local lives_UI = G.hand_text_area.ante + mod = mod or 0 + local text = '+' + local col = G.C.IMPORTANT + if mod < 0 then + text = '-' + col = G.C.RED + end + lives_UI.config.object:update() + G.HUD:recalculate() + attention_text({ + text = text..tostring(math.abs(mod)), + scale = 1, + hold = 0.7, + cover = lives_UI.parent, + cover_colour = col, + align = 'cm', + }) + play_sound('highlight2', 0.685, 0.2) + play_sound('generic1') + return true + end + })) +end ---------------------------------------------- ------------MOD GAME UI END------------------- From 19ed31cd6250ee42b8bfb74067b9a81bdaf29734 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 23:53:49 +0000 Subject: [PATCH 0046/1128] Fixed G.screenwipe is nil error --- Multiplayer/Lobby.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index b5b0ed31..cf7e7a41 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -29,6 +29,10 @@ G.MULTIPLAYER_GAME = { lives = 3, } +G.MUTEX = { + wipe = false +} + PREV_ACHIEVEMENT_VALUE = true function G.MULTIPLAYER.update_connection_status() -- Save the previous value of the achievement flag @@ -73,5 +77,12 @@ function G.MULTIPLAYER.update_player_usernames() end end + +local wipe_off_ref = G.FUNCS.wipe_off +G.FUNCS.wipe_off = function() + if not G.screenwipe then return end + wipe_off_ref() +end + ---------------------------------------------- ------------MOD LOBBY END--------------------- From 993de354d46d2552df120f49b2048f1be7af4368 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 23 Mar 2024 23:54:21 +0000 Subject: [PATCH 0047/1128] Removed mutexes --- Multiplayer/Lobby.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index cf7e7a41..def4d950 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -29,10 +29,6 @@ G.MULTIPLAYER_GAME = { lives = 3, } -G.MUTEX = { - wipe = false -} - PREV_ACHIEVEMENT_VALUE = true function G.MULTIPLAYER.update_connection_status() -- Save the previous value of the achievement flag From e1720f8bf6c13ac3ee96040dcec22b8bdb9362bd Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sun, 24 Mar 2024 00:49:28 +0000 Subject: [PATCH 0048/1128] Fixed timeout crash --- Multiplayer/Networking/Action_Handlers.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index 4b58b948..cc98cd20 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -64,7 +64,6 @@ local function action_disconnected() G.LOBBY.connected = false if G.LOBBY.code then G.LOBBY.code = nil - G.FUNCS.go_to_menu() end G.MULTIPLAYER.update_connection_status() end From a5e95428e5f7ea0b19d30299f28c8f790788a8b4 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sun, 24 Mar 2024 02:47:25 -0700 Subject: [PATCH 0049/1128] Save username even if someone doesn't press enter (#21) * make pull request * Added ability to save even if user presses ESC * Added ability to save when pressing the back button --------- Co-authored-by: TGMM --- Multiplayer/Networking/Socket.lua | 4 +- Multiplayer/UI/Game_UI.lua | 80 +++++++++++++++++++----------- Multiplayer/UI/Mod_Description.lua | 1 + 3 files changed, 54 insertions(+), 31 deletions(-) diff --git a/Multiplayer/Networking/Socket.lua b/Multiplayer/Networking/Socket.lua index 4df93fc8..e571f957 100644 --- a/Multiplayer/Networking/Socket.lua +++ b/Multiplayer/Networking/Socket.lua @@ -152,9 +152,9 @@ while true do coroutine.resume(networkCoroutine) -- Run Timer - if isSocketClosed ~= true and coroutine.status(timerCoroutine) ~= "dead" then + if not isSocketClosed and coroutine.status(timerCoroutine) ~= "dead" then coroutine.resume(timerCoroutine, keepAliveInitialTimeout) - else + elseif not isSocketClosed then -- Timer triggered isRetry = true diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 196f3ce0..9e907b15 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1564,44 +1564,66 @@ end local ease_ante_ref = ease_ante function ease_ante(mod) - if not G.LOBBY.code then return ease_ante_ref(mod) end + if not G.LOBBY.code then + return ease_ante_ref(mod) + end G.E_MANAGER:add_event(Event({ - trigger = 'immediate', + trigger = "immediate", func = function() - G.GAME.round_resets.ante = G.GAME.round_resets.ante + mod - check_and_set_high_score('furthest_ante', G.GAME.round_resets.ante) - return true - end + G.GAME.round_resets.ante = G.GAME.round_resets.ante + mod + check_and_set_high_score("furthest_ante", G.GAME.round_resets.ante) + return true + end, })) end function ease_lives(mod) G.E_MANAGER:add_event(Event({ - trigger = 'immediate', + trigger = "immediate", func = function() - local lives_UI = G.hand_text_area.ante - mod = mod or 0 - local text = '+' - local col = G.C.IMPORTANT - if mod < 0 then - text = '-' - col = G.C.RED - end - lives_UI.config.object:update() - G.HUD:recalculate() - attention_text({ - text = text..tostring(math.abs(mod)), - scale = 1, - hold = 0.7, - cover = lives_UI.parent, - cover_colour = col, - align = 'cm', - }) - play_sound('highlight2', 0.685, 0.2) - play_sound('generic1') - return true - end + local lives_UI = G.hand_text_area.ante + mod = mod or 0 + local text = "+" + local col = G.C.IMPORTANT + if mod < 0 then + text = "-" + col = G.C.RED + end + lives_UI.config.object:update() + G.HUD:recalculate() + attention_text({ + text = text .. tostring(math.abs(mod)), + scale = 1, + hold = 0.7, + cover = lives_UI.parent, + cover_colour = col, + align = "cm", + }) + play_sound("highlight2", 0.685, 0.2) + play_sound("generic1") + return true + end, })) end + +local exit_overlay_menu_ref = G.FUNCS.exit_overlay_menu +---@diagnostic disable-next-line: duplicate-set-field +function G.FUNCS:exit_overlay_menu() + -- Saves username if user presses ESC instead of Enter + if G.OVERLAY_MENU:get_UIE_by_ID("username_input_box") ~= nil then + Utils.save_username(G.LOBBY.username) + end + + exit_overlay_menu_ref(self) +end + +local mods_button_ref = G.FUNCS.mods_button +function G.FUNCS.mods_button(arg_736_0) + if G.OVERLAY_MENU and G.OVERLAY_MENU:get_UIE_by_ID("username_input_box") ~= nil then + Utils.save_username(G.LOBBY.username) + end + + mods_button_ref(arg_736_0) +end ---------------------------------------------- ------------MOD GAME UI END------------------- diff --git a/Multiplayer/UI/Mod_Description.lua b/Multiplayer/UI/Mod_Description.lua index 31e41e31..e893ac1b 100644 --- a/Multiplayer/UI/Mod_Description.lua +++ b/Multiplayer/UI/Mod_Description.lua @@ -15,6 +15,7 @@ function Description.load_description_gui() config = { padding = 0.5, align = "cm", + id = "username_input_box", }, nodes = { { From 53f4016f2904fa7ce42b014d96d4278ef25f710a Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 26 Mar 2024 19:41:15 +0000 Subject: [PATCH 0050/1128] Removed options in create lobby window --- Multiplayer/UI/Main_Menu.lua | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Multiplayer/UI/Main_Menu.lua b/Multiplayer/UI/Main_Menu.lua index 7c1136e9..538c7348 100644 --- a/Multiplayer/UI/Main_Menu.lua +++ b/Multiplayer/UI/Main_Menu.lua @@ -99,16 +99,6 @@ function G.UIDEF.create_UIBox_create_lobby_button() }, }, }, - create_toggle({ - label = "Lose lives on round loss", - ref_table = G.LOBBY.config, - ref_value = "death_on_round_loss", - }), - create_toggle({ - label = "Different seeds", - ref_table = G.LOBBY.config, - ref_value = "different_seeds", - }), UIBox_button({ label = { "Start Lobby" }, colour = G.C.RED, From 337a97ade02a6e87f8faf7a1d7faf9b15f09fdf4 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 26 Mar 2024 21:13:21 +0000 Subject: [PATCH 0051/1128] Created lobby options UI --- Multiplayer/Components/Disableable_Button.lua | 1 + Multiplayer/Components/Disableable_Toggle.lua | 110 +++++++++++++++++ Multiplayer/Lobby.lua | 7 +- Multiplayer/UI/Lobby_UI.lua | 116 ++++++++++++++++-- 4 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 Multiplayer/Components/Disableable_Toggle.lua diff --git a/Multiplayer/Components/Disableable_Button.lua b/Multiplayer/Components/Disableable_Button.lua index f656807a..8b331694 100644 --- a/Multiplayer/Components/Disableable_Button.lua +++ b/Multiplayer/Components/Disableable_Button.lua @@ -9,6 +9,7 @@ function Disableable_Button(args) local enabled = enabled_table[args.enabled_ref_value] args.colour = args.colour or G.C.RED args.text_colour = args.text_colour or G.C.UI.TEXT_LIGHT + args.disabled_text = args.disabled_text or args.label args.label = not enabled and args.disabled_text or args.label local button_component = UIBox_button(args) diff --git a/Multiplayer/Components/Disableable_Toggle.lua b/Multiplayer/Components/Disableable_Toggle.lua new file mode 100644 index 00000000..f8703e8a --- /dev/null +++ b/Multiplayer/Components/Disableable_Toggle.lua @@ -0,0 +1,110 @@ +--- STEAMODDED HEADER +--- STEAMODDED SECONDARY FILE + +---------------------------------------------- +------------MOD DISABLEABLE TOGGLE------------ + +function Disableable_Toggle(args) + local enabled_table = args.enabled_ref_table or {} + local enabled = enabled_table[args.enabled_ref_value] + + local toggle_component = create_toggle(args) + toggle_component.nodes[2].nodes[1].nodes[1].config.button = enabled and 'toggle_button' or nil + toggle_component.nodes[2].nodes[1].nodes[1].config.button_dist = enabled and 0.2 or nil + toggle_component.nodes[2].nodes[1].nodes[1].config.hover = enabled and true or false + toggle_component.nodes[2].nodes[1].nodes[1].config.toggle_callback = enabled and args.callback or nil + toggle_component.nodes[2].nodes[1].nodes[1].config.func = enabled and 'toggle' or nil + return toggle_component +end + +return Disableable_Toggle + +--[[ create_toggle returns this + { + n=args.col and G.UIT.C or G.UIT.R, + config={ + align = "cm", + padding = 0.1, + r = 0.1, + colour = G.C.CLEAR, + focus_args = {funnel_from = true} + }, + nodes={ + { + n=G.UIT.C, + config={ + align = "cr", + minw = args.w + }, + nodes={ + { + n=G.UIT.T, + config={ + text = args.label, + scale = args.label_scale, + colour = G.C.UI.TEXT_LIGHT + } + }, + { + n=G.UIT.B, + config={ + w = 0.1, + h = 0.1 + } + }, + } + }, + { + n=G.UIT.C, + config={ + align = "cl", + minw = 0.3*args.w + }, + nodes={ + { + n=G.UIT.C, + config={ + align = "cm", + r = 0.1, + colour = G.C.BLACK + }, + nodes={ + { + n=G.UIT.C, + config={ + align = "cm", + r = 0.1, + padding = 0.03, + minw = 0.4*args.scale, + minh = 0.4*args.scale, + outline_colour = G.C.WHITE, + outline = 1.2*args.scale, + line_emboss = 0.5*args.scale, + ref_table = args, + colour = args.inactive_colour, + button = 'toggle_button', + button_dist = 0.2, + hover = true, + toggle_callback = args.callback, + func = 'toggle', + focus_args = {funnel_to = true} + }, + nodes={ + { + n=G.UIT.O, + config={ + object = check + } + }, + } + }, + } + } + } + }, + } + } +]] + +---------------------------------------------- +------------MOD DISABLEABLE TOGGLE END-------- \ No newline at end of file diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index f922f317..53aa9a10 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -11,7 +11,12 @@ G.LOBBY = { temp_code = "", code = nil, type = "", - config = {}, + config = { + no_gold_on_round_loss = true, + death_on_round_loss = false, + different_seeds = false, + bsh = false + }, username = "Guest", host = {}, guest = {}, diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua index 18d1645d..62a4303f 100644 --- a/Multiplayer/UI/Lobby_UI.lua +++ b/Multiplayer/UI/Lobby_UI.lua @@ -5,6 +5,7 @@ ------------MOD LOBBY UI---------------------- local Disableable_Button = require("Components.Disableable_Button") +local Disableable_Toggle = require("Components.Disableable_Toggle") G.HUD_connection_status = nil @@ -141,7 +142,7 @@ function G.UIDEF.create_UIBox_lobby_menu() align = "cm", }, nodes = { - Disableable_Button({ + UIBox_button({ button = "lobby_options", colour = G.C.ORANGE, minw = 3.15, @@ -149,8 +150,6 @@ function G.UIDEF.create_UIBox_lobby_menu() label = { "LOBBY OPTIONS" }, scale = text_scale * 1.2, col = true, - enabled_ref_table = G.LOBBY, - enabled_ref_value = "is_host", }), { n = G.UIT.C, @@ -269,26 +268,112 @@ function G.UIDEF.create_UIBox_lobby_options() { n = G.UIT.R, config = { - padding = 0, + emboss = 0.05, + minh = 6, + r = 0.1, + minw = 10, + padding = 0.2, + colour = G.C.BLACK, align = "cm", }, nodes = { { n = G.UIT.R, config = { - padding = 0.5, + padding = 0.3, align = "cm", }, nodes = { + not G.LOBBY.is_host and { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, + config = { + scale = 0.6, + shadow = true, + text = 'Only the Lobby Host can change these options', + colour = G.C.UI.TEXT_LIGHT, + } + } + } + } or nil, + { + n = G.UIT.R, + config = { + padding = 0, + align = "cr", + }, + nodes = { + create_toggle({ + enabled_ref_table = G.LOBBY, + enabled_ref_value = 'is_host', + label = "Don't get blind gold on round loss", + ref_table = G.LOBBY.config, + ref_value = "no_gold_on_round_loss", + }), + } + }, + { + n = G.UIT.R, + config = { + padding = 0, + align = "cr", + }, + nodes = { + create_toggle({ + enabled_ref_table = G.LOBBY, + enabled_ref_value = 'is_host', + label = "Lose a life on non-PvP round loss", + ref_table = G.LOBBY.config, + ref_value = "death_on_round_loss", + }), + } + }, { - n = G.UIT.T, + n = G.UIT.R, config = { - text = "Not Implemented Yet", - shadow = true, - scale = 0.6, - colour = G.C.UI.TEXT_LIGHT, + padding = 0, + align = "cr", }, + nodes = { + create_toggle({ + enabled_ref_table = G.LOBBY, + enabled_ref_value = 'is_host', + label = "Players have different seeds", + ref_table = G.LOBBY.config, + ref_value = "different_seeds", + }), + } }, + { + n = G.UIT.R, + config = { + padding = 0, + align = "cr", + }, + nodes = { + create_toggle({ + enabled_ref_table = G.LOBBY, + enabled_ref_value = 'is_host', + label = "PvP based on best single hand instead of best round", + ref_table = G.LOBBY.config, + ref_value = "bsh", + }) + } + }, + Disableable_Button({ + enabled_ref_table = G.LOBBY, + enabled_ref_value = 'is_host', + button = "reset_lobby_options", + label = { localize("k_reset") }, + colour = G.C.RED, + minw = 5, + }), }, }, }, @@ -297,6 +382,17 @@ function G.UIDEF.create_UIBox_lobby_options() }) end +function G.FUNCS.reset_lobby_options(e) + G.LOBBY.config = { + no_gold_on_round_loss = true, + death_on_round_loss = false, + different_seeds = false, + bsh = false + } + G.FUNCS:exit_overlay_menu() + G.FUNCS.lobby_options(e) +end + function G.FUNCS.get_lobby_main_menu_UI(e) return UIBox({ definition = G.UIDEF.create_UIBox_lobby_menu(), From 8faeb4a014257afcc83ac8a1287a33edc5bd54db Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Wed, 3 Apr 2024 18:20:37 +0000 Subject: [PATCH 0052/1128] Implemented server side lobby options --- Multiplayer/Components/Disableable_Toggle.lua | 2 +- Multiplayer/Networking/Action_Handlers.lua | 28 +++++++++++++++++++ Multiplayer/UI/Lobby_UI.lua | 24 ++++++++++++---- Server/README.md | 8 ++++++ Server/src/Lobby.ts | 8 ++++++ Server/src/actionHandlers.ts | 5 ++++ Server/src/actions.ts | 4 +++ Server/src/main.ts | 3 ++ 8 files changed, 75 insertions(+), 7 deletions(-) diff --git a/Multiplayer/Components/Disableable_Toggle.lua b/Multiplayer/Components/Disableable_Toggle.lua index f8703e8a..6664263d 100644 --- a/Multiplayer/Components/Disableable_Toggle.lua +++ b/Multiplayer/Components/Disableable_Toggle.lua @@ -9,11 +9,11 @@ function Disableable_Toggle(args) local enabled = enabled_table[args.enabled_ref_value] local toggle_component = create_toggle(args) + toggle_component.nodes[2].nodes[1].nodes[1].config.id = args.id toggle_component.nodes[2].nodes[1].nodes[1].config.button = enabled and 'toggle_button' or nil toggle_component.nodes[2].nodes[1].nodes[1].config.button_dist = enabled and 0.2 or nil toggle_component.nodes[2].nodes[1].nodes[1].config.hover = enabled and true or false toggle_component.nodes[2].nodes[1].nodes[1].config.toggle_callback = enabled and args.callback or nil - toggle_component.nodes[2].nodes[1].nodes[1].config.func = enabled and 'toggle' or nil return toggle_component end diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index fe2a6b86..fe0477dc 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -132,6 +132,24 @@ local function action_lose_game() G.STATE = G.STATES.GAME_OVER end +local function action_lobby_options(options) + for k, v in pairs(options) do + local parsed_v = v + if v == "true" then + parsed_v = true + elseif v == "false" then + parsed_v = false + end + G.LOBBY.config[k] = parsed_v + if G.OVERLAY_MENU then + local config_uie = G.OVERLAY_MENU:get_UIE_by_ID(k .. '_toggle') + if config_uie then + G.FUNCS.toggle(config_uie) + end + end + end +end + -- #region Client to Server function G.MULTIPLAYER.create_lobby() -- TODO: This is hardcoded to attrition for now, must be changed @@ -171,6 +189,14 @@ end function G.MULTIPLAYER.play_hand(score, hands_left) Client.send(string.format("action:playHand,score:%d,handsLeft:%d", score, hands_left)) end + +function G.MULTIPLAYER.lobby_options() + local msg = "action:lobbyOptions" + for k, v in pairs(G.LOBBY.config) do + msg = msg .. string.format(",%s:%s", k, tostring(v)) + end + Client.send(msg) +end -- #endregion Client to Server -- Utils @@ -222,6 +248,8 @@ function Game:update(dt) action_win_game() elseif parsedAction.action == "loseGame" then action_lose_game() + elseif parsedAction.action == "lobbyOptions" then + action_lobby_options(parsedAction) elseif parsedAction.action == "error" then action_error(parsedAction.message) elseif parsedAction.action == "keepAlive" then diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua index 62a4303f..18d3e5fe 100644 --- a/Multiplayer/UI/Lobby_UI.lua +++ b/Multiplayer/UI/Lobby_UI.lua @@ -7,6 +7,10 @@ local Disableable_Button = require("Components.Disableable_Button") local Disableable_Toggle = require("Components.Disableable_Toggle") +local function toggle_lobby_options(value) + G.MULTIPLAYER.lobby_options() +end + G.HUD_connection_status = nil function G.UIDEF.get_connection_status_ui() @@ -309,12 +313,14 @@ function G.UIDEF.create_UIBox_lobby_options() align = "cr", }, nodes = { - create_toggle({ + Disableable_Toggle({ + id = "no_gold_on_round_loss_toggle", enabled_ref_table = G.LOBBY, enabled_ref_value = 'is_host', label = "Don't get blind gold on round loss", ref_table = G.LOBBY.config, ref_value = "no_gold_on_round_loss", + callback = toggle_lobby_options }), } }, @@ -325,12 +331,14 @@ function G.UIDEF.create_UIBox_lobby_options() align = "cr", }, nodes = { - create_toggle({ + Disableable_Toggle({ + id = "death_on_round_loss_toggle", enabled_ref_table = G.LOBBY, enabled_ref_value = 'is_host', label = "Lose a life on non-PvP round loss", ref_table = G.LOBBY.config, ref_value = "death_on_round_loss", + callback = toggle_lobby_options }), } }, @@ -341,12 +349,14 @@ function G.UIDEF.create_UIBox_lobby_options() align = "cr", }, nodes = { - create_toggle({ + Disableable_Toggle({ + id = "different_seeds_toggle", enabled_ref_table = G.LOBBY, enabled_ref_value = 'is_host', label = "Players have different seeds", ref_table = G.LOBBY.config, ref_value = "different_seeds", + callback = toggle_lobby_options }), } }, @@ -357,12 +367,14 @@ function G.UIDEF.create_UIBox_lobby_options() align = "cr", }, nodes = { - create_toggle({ + Disableable_Toggle({ + id = "bsh_toggle", enabled_ref_table = G.LOBBY, enabled_ref_value = 'is_host', label = "PvP based on best single hand instead of best round", ref_table = G.LOBBY.config, ref_value = "bsh", + callback = toggle_lobby_options }) } }, @@ -389,8 +401,8 @@ function G.FUNCS.reset_lobby_options(e) different_seeds = false, bsh = false } - G.FUNCS:exit_overlay_menu() - G.FUNCS.lobby_options(e) + G.FUNCS.exit_overlay_menu() + G.MULTIPLAYER.lobby_options() end function G.FUNCS.get_lobby_main_menu_UI(e) diff --git a/Server/README.md b/Server/README.md index b20e0b25..45e5c655 100644 --- a/Server/README.md +++ b/Server/README.md @@ -97,6 +97,9 @@ endPvP: lost --- +lobbyOptions: {any number of options recognized by the client} +- Updates guest clients when host changes options, should be sent when client connects and when options change + ### Client to Server username: username @@ -167,6 +170,11 @@ playerInfo enemyInfo - Request an enemyInfo update. +--- + +lobbyOptions: {any number of options recognized by the client} +- Updates the server-side log of lobby options, should be sent on lobby start and when options are changed + ### Utility keepAlive diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index 03c4a06e..31f68bf4 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -22,6 +22,7 @@ class Lobby { host: Client | null guest: Client | null gameMode: GameMode + options: { [key: string]: string } // Attrition is the default game mode constructor(host: Client, gameMode: GameMode = 'attrition') { @@ -33,6 +34,7 @@ class Lobby { this.host = host this.guest = null this.gameMode = gameMode + this.options = {} host.setLobby(this) host.sendAction({ action: 'joinedLobby', code: this.code }) @@ -73,6 +75,7 @@ class Lobby { this.guest = client client.setLobby(this) client.sendAction({ action: 'joinedLobby', code: this.code }) + client.sendAction({ action: 'lobbyOptions', ...this.options }) this.broadcastLobbyInfo() } @@ -109,6 +112,11 @@ class Lobby { this.broadcastAction({ action: 'playerInfo', lives }) } + + setOptions = (options: { [key: string]: string }) => { + this.options = options + this.guest?.sendAction({ action: 'lobbyOptions', ...options }) + } } export default Lobby diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 9655b7f7..500941e4 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -184,6 +184,10 @@ const stopGameAction = (client: Client) => { client.lobby?.broadcastAction({ action: 'stopGame' }) } +const lobbyOptionsAction = (options: any, client: Client) => { + client.lobby?.setOptions(options) +} + // Declared partial for now untill all action handlers are defined export const actionHandlers = { username: usernameAction, @@ -197,4 +201,5 @@ export const actionHandlers = { unreadyBlind: unreadyBlindAction, playHand: playHandAction, stopGame: stopGameAction, + lobbyOptions: lobbyOptionsAction, } satisfies Partial diff --git a/Server/src/actions.ts b/Server/src/actions.ts index 22789754..bd2f22d1 100644 --- a/Server/src/actions.ts +++ b/Server/src/actions.ts @@ -32,6 +32,8 @@ export type ActionEnemyInfo = { } export type ActionEndPvP = { action: 'endPvP'; lost: boolean } +export type ActionLobbyOptions = { action: 'lobbyOptions' } + export type ActionServerToClient = | ActionConnected | ActionError @@ -46,6 +48,7 @@ export type ActionServerToClient = | ActionPlayerInfo | ActionEnemyInfo | ActionEndPvP + | ActionLobbyOptions | ActionKeepAlive | ActionKeepAliveAck @@ -82,6 +85,7 @@ export type ActionClientToServer = | ActionPlayerInfoRequest | ActionEnemyInfoRequest | ActionUnreadyBlind + | ActionLobbyOptions // Utility actions export type ActionKeepAlive = { action: 'keepAlive' } diff --git a/Server/src/main.ts b/Server/src/main.ts index e7d20b23..ee6a0569 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -158,6 +158,9 @@ const server = net.createServer((socket) => { case 'stopGame': actionHandlers.stopGame(client) break + case 'lobbyOptions': + actionHandlers.lobbyOptions(actionArgs, client) + break } } catch (error) { const failedToParseError = 'Failed to parse message' From 56bfbbc9c7437a4eec7afa8b938a5df1d8c711e2 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Wed, 3 Apr 2024 19:28:20 +0000 Subject: [PATCH 0053/1128] Implemented lobby option functionality --- Multiplayer/Lobby.lua | 3 +-- Multiplayer/Networking/Action_Handlers.lua | 7 ++++++ Multiplayer/UI/Game_UI.lua | 5 ++-- Multiplayer/UI/Lobby_UI.lua | 21 +--------------- Server/README.md | 5 ++++ Server/src/Lobby.ts | 10 ++++++-- Server/src/actionHandlers.ts | 29 +++++++++++++++++++--- Server/src/actions.ts | 2 ++ Server/src/main.ts | 3 +++ 9 files changed, 56 insertions(+), 29 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 53aa9a10..ba8cb8d5 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -14,8 +14,7 @@ G.LOBBY = { config = { no_gold_on_round_loss = true, death_on_round_loss = false, - different_seeds = false, - bsh = false + different_seeds = false }, username = "Guest", host = {}, diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index fe0477dc..c530752b 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -184,6 +184,13 @@ function G.MULTIPLAYER.stop_game() Client.send("action:stopGame") end +function G.MULTIPLAYER.fail_round() + if G.LOBBY.config.no_gold_on_round_loss then + G.GAME.blind.dollars = 0 + end + Client.send("action:failRound") +end + ---@param score number ---@param hands_left number function G.MULTIPLAYER.play_hand(score, hands_left) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 9e907b15..3c7eb22a 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -801,7 +801,7 @@ function Game:update_new_round(dt) -- Prevent player from losing if G.GAME.chips - G.GAME.blind.chips < 0 then G.GAME.blind.chips = -1 - G.GAME.blind.dollars = 0 + G.MULTIPLAYER.fail_round() end -- Prevent player from winning @@ -1438,7 +1438,7 @@ function add_round_eval_row(config) n = G.UIT.O, config = { object = DynaText({ - string = { G.GAME.blind.boss and " Lost a Life " or " Failed " }, + string = { (G.GAME.blind.boss or G.LOBBY.config.death_on_round_loss) and " Lost a Life " or " Failed " }, colours = { G.C.FILTER }, shadow = true, pop_in = 0, @@ -1581,6 +1581,7 @@ function ease_lives(mod) G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function() + if not G.hand_text_area then return end local lives_UI = G.hand_text_area.ante mod = mod or 0 local text = "+" diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua index 18d3e5fe..c8d254e5 100644 --- a/Multiplayer/UI/Lobby_UI.lua +++ b/Multiplayer/UI/Lobby_UI.lua @@ -360,24 +360,6 @@ function G.UIDEF.create_UIBox_lobby_options() }), } }, - { - n = G.UIT.R, - config = { - padding = 0, - align = "cr", - }, - nodes = { - Disableable_Toggle({ - id = "bsh_toggle", - enabled_ref_table = G.LOBBY, - enabled_ref_value = 'is_host', - label = "PvP based on best single hand instead of best round", - ref_table = G.LOBBY.config, - ref_value = "bsh", - callback = toggle_lobby_options - }) - } - }, Disableable_Button({ enabled_ref_table = G.LOBBY, enabled_ref_value = 'is_host', @@ -398,8 +380,7 @@ function G.FUNCS.reset_lobby_options(e) G.LOBBY.config = { no_gold_on_round_loss = true, death_on_round_loss = false, - different_seeds = false, - bsh = false + different_seeds = false } G.FUNCS.exit_overlay_menu() G.MULTIPLAYER.lobby_options() diff --git a/Server/README.md b/Server/README.md index 45e5c655..1b28c736 100644 --- a/Server/README.md +++ b/Server/README.md @@ -175,6 +175,11 @@ enemyInfo lobbyOptions: {any number of options recognized by the client} - Updates the server-side log of lobby options, should be sent on lobby start and when options are changed +--- + +failRound +- Declares the client lost a round + ### Utility keepAlive diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index 31f68bf4..8fb9e684 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -22,7 +22,7 @@ class Lobby { host: Client | null guest: Client | null gameMode: GameMode - options: { [key: string]: string } + options: { [key: string]: any } // Attrition is the default game mode constructor(host: Client, gameMode: GameMode = 'attrition') { @@ -114,7 +114,13 @@ class Lobby { } setOptions = (options: { [key: string]: string }) => { - this.options = options + for (const key of Object.keys(options)) { + if (options[key] == "true" || options[key] == "false") { + this.options[key] = options[key] == "true" + } else{ + this.options[key] = options[key] + } + } this.guest?.sendAction({ action: 'lobbyOptions', ...options }) } } diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 500941e4..446f552b 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -77,7 +77,7 @@ const startGameAction = (client: Client) => { lobby.broadcastAction({ action: 'startGame', deck: 'c_multiplayer_1', - seed: generateSeed(), + seed: lobby.options.different_seeds? undefined : generateSeed(), }) } @@ -114,8 +114,6 @@ const playHandAction = ( return } - // Should this be additive or just - // the latest score? client.score = score client.handsLeft = typeof handsLeft === 'number' ? handsLeft : Number(handsLeft) @@ -188,6 +186,30 @@ const lobbyOptionsAction = (options: any, client: Client) => { client.lobby?.setOptions(options) } +const failRoundAction = (client: Client) => { + const lobby = client.lobby + + if (!lobby) return + + client.lives -= 1 + client.sendAction({ action: 'playerInfo', lives: client.lives }) + + if (client.lives === 0) { + let gameLoser = null + let gameWinner = null + if (client.id === lobby.host?.id) { + gameLoser = lobby.host + gameWinner = lobby.guest + } else { + gameLoser = lobby.guest + gameWinner = lobby.host + } + + gameWinner?.sendAction({ action: 'winGame' }) + gameLoser?.sendAction({ action: 'loseGame' }) + } +} + // Declared partial for now untill all action handlers are defined export const actionHandlers = { username: usernameAction, @@ -202,4 +224,5 @@ export const actionHandlers = { playHand: playHandAction, stopGame: stopGameAction, lobbyOptions: lobbyOptionsAction, + failRound: failRoundAction, } satisfies Partial diff --git a/Server/src/actions.ts b/Server/src/actions.ts index bd2f22d1..92f43a96 100644 --- a/Server/src/actions.ts +++ b/Server/src/actions.ts @@ -70,6 +70,7 @@ export type ActionPlayHand = { export type ActionGameInfoRequest = { action: 'gameInfo' } export type ActionPlayerInfoRequest = { action: 'playerInfo' } export type ActionEnemyInfoRequest = { action: 'enemyInfo' } +export type ActionFailRound = { action: 'failRound' } export type ActionClientToServer = | ActionUsername @@ -86,6 +87,7 @@ export type ActionClientToServer = | ActionEnemyInfoRequest | ActionUnreadyBlind | ActionLobbyOptions + | ActionFailRound // Utility actions export type ActionKeepAlive = { action: 'keepAlive' } diff --git a/Server/src/main.ts b/Server/src/main.ts index ee6a0569..01fc7def 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -161,6 +161,9 @@ const server = net.createServer((socket) => { case 'lobbyOptions': actionHandlers.lobbyOptions(actionArgs, client) break + case 'failRound': + actionHandlers.failRound(client) + break } } catch (error) { const failedToParseError = 'Failed to parse message' From 2d04041431ed2e49509f9edec93a2dacfbca9eb1 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Thu, 4 Apr 2024 16:58:24 +0000 Subject: [PATCH 0054/1128] Implemented setting blind based on server response --- Multiplayer/Items/Blind.lua | 18 ++++---------- Multiplayer/Networking/Action_Handlers.lua | 21 +++++++++++++--- Multiplayer/UI/Game_UI.lua | 29 +++++++++++++++++++--- Multiplayer/UI/Main_Menu.lua | 9 ++++--- Server/src/Lobby.ts | 4 +++ Server/src/actionHandlers.ts | 5 ++++ Server/src/main.ts | 3 +++ 7 files changed, 66 insertions(+), 23 deletions(-) diff --git a/Multiplayer/Items/Blind.lua b/Multiplayer/Items/Blind.lua index 85564b3f..338c009f 100644 --- a/Multiplayer/Items/Blind.lua +++ b/Multiplayer/Items/Blind.lua @@ -20,19 +20,6 @@ local bl_pvp = { G.P_BLINDS["bl_pvp"] = bl_pvp -local get_new_boss_ref = get_new_boss -function get_new_boss() - if G.LOBBY.code then - return "bl_pvp" - else - local boss = get_new_boss_ref() - while boss == "bl_pvp" do - boss = get_new_boss_ref() - end - return boss - end -end - local localize_ref = localize function localize(args, misc_cat) if type(args) == "table" and args.key == "bl_pvp" and args.set == "Blind" then @@ -64,5 +51,10 @@ function set_discover_tallies() return res end +function is_pvp_boss() + if not G.GAME or not G.GAME.blind then return false end + return G.GAME.blind.name == "Your Nemesis" +end + ---------------------------------------------- ------------MOD BLIND END--------------------- diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index fe2a6b86..e3111cf9 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -96,7 +96,7 @@ local function action_enemy_info(score_str, hands_left_str) G.LOBBY.enemy.score = score G.LOBBY.enemy.hands = hands_left - if G.GAME.blind.boss then + if is_pvp_boss() then G.HUD_blind:get_UIE_by_ID("HUD_blind_count"):juice_up() G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned"):juice_up() end @@ -132,10 +132,17 @@ local function action_lose_game() G.STATE = G.STATES.GAME_OVER end +local function action_game_info(small, big, boss) + G.GAME.round_resets.blind_choices = { + Small = small or 'bl_small', + Big = big or 'bl_big', + Boss = boss + } +end + -- #region Client to Server -function G.MULTIPLAYER.create_lobby() - -- TODO: This is hardcoded to attrition for now, must be changed - Client.send("action:createLobby,gameMode:attrition") +function G.MULTIPLAYER.create_lobby(gamemode) + Client.send(string.format("action:createLobby,gameMode:%s", gamemode)) end function G.MULTIPLAYER.join_lobby(code) @@ -166,6 +173,10 @@ function G.MULTIPLAYER.stop_game() Client.send("action:stopGame") end +function G.MULTIPLAYER.game_info() + Client.send("action:gameInfo") +end + ---@param score number ---@param hands_left number function G.MULTIPLAYER.play_hand(score, hands_left) @@ -222,6 +233,8 @@ function Game:update(dt) action_win_game() elseif parsedAction.action == "loseGame" then action_lose_game() + elseif parsedAction.action == "gameInfo" then + action_game_info(parsedAction.small, parsedAction.big, parsedAction.boss) elseif parsedAction.action == "error" then action_error(parsedAction.message) elseif parsedAction.action == "keepAlive" then diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 9e907b15..c42aa38d 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -646,7 +646,7 @@ function Game:update_draw_to_hand(dt) and G.GAME.current_round.discards_used == 0 and G.GAME.facing_blind then - if G.GAME.blind.name == "Your Nemesis" then + if is_pvp_boss() then G.E_MANAGER:add_event(Event({ trigger = "after", delay = 1, @@ -737,7 +737,7 @@ local update_hand_played_ref = Game.update_hand_played ---@diagnostic disable-next-line: duplicate-set-field function Game:update_hand_played(dt) -- Ignore for singleplayer or regular blinds - if not G.LOBBY.connected or not G.LOBBY.code or not G.GAME.blind.boss then + if not G.LOBBY.connected or not G.LOBBY.code or not is_pvp_boss() then update_hand_played_ref(self, dt) return end @@ -1438,7 +1438,7 @@ function add_round_eval_row(config) n = G.UIT.O, config = { object = DynaText({ - string = { G.GAME.blind.boss and " Lost a Life " or " Failed " }, + string = { is_pvp_boss() and " Lost a Life " or " Failed " }, colours = { G.C.FILTER }, shadow = true, pop_in = 0, @@ -1581,6 +1581,7 @@ function ease_lives(mod) G.E_MANAGER:add_event(Event({ trigger = "immediate", func = function() + if not G.hand_text_area then return end local lives_UI = G.hand_text_area.ante mod = mod or 0 local text = "+" @@ -1625,5 +1626,27 @@ function G.FUNCS.mods_button(arg_736_0) mods_button_ref(arg_736_0) end + +local get_new_boss_ref = get_new_boss +local function get_regular_boss() + local boss = get_new_boss_ref() + while boss == "bl_pvp" do + boss = get_new_boss_ref() + end + return boss +end + +--[[ + We repurpose get_new_boss here because it is called to determine the boss blind, + and we only ever need to do that when we are also determining the small and big blinds for a given ante. + + Note: this is also called when a boss is rerolled, but that should be disabled and is also inconsequential +]] +function get_new_boss() + if G.LOBBY.code then + G.MULTIPLAYER.game_info() + end + return get_regular_boss() +end ---------------------------------------------- ------------MOD GAME UI END------------------- diff --git a/Multiplayer/UI/Main_Menu.lua b/Multiplayer/UI/Main_Menu.lua index 7c1136e9..3069efd2 100644 --- a/Multiplayer/UI/Main_Menu.lua +++ b/Multiplayer/UI/Main_Menu.lua @@ -110,6 +110,7 @@ function G.UIDEF.create_UIBox_create_lobby_button() ref_value = "different_seeds", }), UIBox_button({ + id = "start_attrition", label = { "Start Lobby" }, colour = G.C.RED, button = "start_lobby", @@ -158,8 +159,10 @@ function G.UIDEF.create_UIBox_create_lobby_button() }, }, UIBox_button({ - label = { "Coming Soon!" }, + id = "start_draft", + label = { "Start Lobby" }, colour = G.C.RED, + button = "start_lobby", minw = 5, }), }, @@ -399,8 +402,8 @@ end function G.FUNCS.start_lobby(e) G.SETTINGS.paused = false - - G.MULTIPLAYER.create_lobby() + local gamemode = e.config.id == "start_attrition" and "attrition" or "draft" + G.MULTIPLAYER.create_lobby(gamemode) end -- Modify play button to take you to mode select first diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index 03c4a06e..974ff618 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -109,6 +109,10 @@ class Lobby { this.broadcastAction({ action: 'playerInfo', lives }) } + + getGameInfo = () => { + return { small: 'bl_pvp', big: 'bl_pvp', boss: 'bl_pvp' } + } } export default Lobby diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 9655b7f7..d6f6f148 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -184,6 +184,10 @@ const stopGameAction = (client: Client) => { client.lobby?.broadcastAction({ action: 'stopGame' }) } +const gameInfoAction = (client: Client) => { + client.sendAction({ action: 'gameInfo', ...client.lobby?.getGameInfo() }) +} + // Declared partial for now untill all action handlers are defined export const actionHandlers = { username: usernameAction, @@ -197,4 +201,5 @@ export const actionHandlers = { unreadyBlind: unreadyBlindAction, playHand: playHandAction, stopGame: stopGameAction, + gameInfo: gameInfoAction, } satisfies Partial diff --git a/Server/src/main.ts b/Server/src/main.ts index e7d20b23..c5c261d0 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -158,6 +158,9 @@ const server = net.createServer((socket) => { case 'stopGame': actionHandlers.stopGame(client) break + case 'gameInfo': + actionHandlers.gameInfo(client) + break } } catch (error) { const failedToParseError = 'Failed to parse message' From d2aac5211d7977872f241ab431d3405408d83cca Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Thu, 4 Apr 2024 18:08:57 +0000 Subject: [PATCH 0055/1128] Implemented setting blinds per gamemode --- Multiplayer/Networking/Action_Handlers.lua | 10 +++++-- Multiplayer/UI/Game_UI.lua | 1 + Server/README.md | 6 ++++ Server/src/Client.ts | 2 +- Server/src/GameMode.ts | 33 ++++++++++++++++++++++ Server/src/Lobby.ts | 9 ++++-- Server/src/actionHandlers.ts | 27 +++++++++--------- Server/src/actions.ts | 5 ++++ Server/src/main.ts | 4 +++ 9 files changed, 77 insertions(+), 20 deletions(-) create mode 100644 Server/src/GameMode.ts diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index fc7e98ce..76098a0b 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -135,9 +135,9 @@ end local function action_game_info(small, big, boss) G.GAME.round_resets.blind_choices = { - Small = small or 'bl_small', - Big = big or 'bl_big', - Boss = boss + Small = small or G.GAME.round_resets.blind_choices.Small, + Big = big or G.GAME.round_resets.blind_choices.Big, + Boss = boss or G.GAME.round_resets.blind_choices.Boss } end @@ -216,6 +216,10 @@ function G.MULTIPLAYER.lobby_options() end Client.send(msg) end + +function G.MULTIPLAYER.set_ante(ante) + Client.send(string.format("action:setAnte,ante:%d", ante)) +end -- #endregion Client to Server -- Utils diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 63c352e3..5855ef16 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1564,6 +1564,7 @@ end local ease_ante_ref = ease_ante function ease_ante(mod) + G.MULTIPLAYER.set_ante(G.GAME.round_resets.ante + mod) if not G.LOBBY.code then return ease_ante_ref(mod) end diff --git a/Server/README.md b/Server/README.md index 1b28c736..b001bb6f 100644 --- a/Server/README.md +++ b/Server/README.md @@ -180,6 +180,12 @@ lobbyOptions: {any number of options recognized by the client} failRound - Declares the client lost a round +--- + +setAnte: ante +- Declares the current ante that the client is on to the server, this needs to be handled by the server on a per-client basis since clients can be on different antes at the same time +- ante: The ante to set the client to on the server side + ### Utility keepAlive diff --git a/Server/src/Client.ts b/Server/src/Client.ts index ab55ac58..47a2bab4 100644 --- a/Server/src/Client.ts +++ b/Server/src/Client.ts @@ -21,10 +21,10 @@ class Client { lobby: Lobby | null = null /** Whether player is ready for next blind */ isReady = false - // TODO: Set lives based on game mode lives = 4 score = 0 handsLeft = 4 + ante = 1 constructor(address: Address, send: SendFn) { this.id = uuidv4() diff --git a/Server/src/GameMode.ts b/Server/src/GameMode.ts new file mode 100644 index 00000000..bef85009 --- /dev/null +++ b/Server/src/GameMode.ts @@ -0,0 +1,33 @@ +import { GameMode } from "./actions.js" + +type GameModeData = { + startingLives: number + + getBlindFromAnte: (ante: number) => { + small?: string + big?: string + boss?: string + } + + // TODO: Validate lobby options when they differ per gamemode +} + +const GameModes: { + [key in GameMode]: GameModeData +} = { + 'attrition': { + startingLives: 4, + getBlindFromAnte: (ante) => { + return { boss: 'bl_pvp' } + } + }, + 'draft': { + startingLives: 2, + getBlindFromAnte: (ante) => { + if (ante < 2) return { } + else return { small: 'bl_pvp', big: 'bl_pvp', boss: 'bl_pvp' } + } + } +} + +export default GameModes \ No newline at end of file diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index bb37eb4c..991630ed 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -1,4 +1,5 @@ import type Client from './Client.js' +import GameModes from './GameMode.js' import type { ActionLobbyInfo, ActionServerToClient, @@ -113,8 +114,12 @@ class Lobby { this.broadcastAction({ action: 'playerInfo', lives }) } - getGameInfo = () => { - return { small: 'bl_pvp', big: 'bl_pvp', boss: 'bl_pvp' } + sendGameInfo = (client: Client) => { + if (this.host !== client && this.guest !== client) { + return client.sendAction({ action: 'error', message: 'Client not in Lobby' }) + } + + client.sendAction({ action: 'gameInfo', ...GameModes[this.gameMode].getBlindFromAnte(client.ante) }) } setOptions = (options: { [key: string]: string }) => { diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 3481e0b3..18bf2421 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -1,4 +1,5 @@ import type Client from './Client.js' +import GameModes from './GameMode.js' import Lobby from './Lobby.js' import type { ActionCreateLobby, @@ -6,6 +7,7 @@ import type { ActionHandlers, ActionJoinLobby, ActionPlayHand, + ActionSetAnte, ActionUsername, } from './actions.js' import { generateSeed } from './utils.js' @@ -60,17 +62,7 @@ const startGameAction = (client: Client) => { return } - let lives = 4 - // TODO: Put this in a gamemode map that - // has more info than just lives - switch (lobby.gameMode) { - case 'attrition': - lives = 4 - break - case 'draft': - lives = 2 - break - } + let lives = GameModes[lobby.gameMode].startingLives // Reset players' lives lobby.setPlayersLives(lives) @@ -183,7 +175,7 @@ const stopGameAction = (client: Client) => { } const gameInfoAction = (client: Client) => { - client.sendAction({ action: 'gameInfo', ...client.lobby?.getGameInfo() }) + client.lobby?.sendGameInfo(client) } const lobbyOptionsAction = (options: any, client: Client) => { @@ -195,8 +187,10 @@ const failRoundAction = (client: Client) => { if (!lobby) return - client.lives -= 1 - client.sendAction({ action: 'playerInfo', lives: client.lives }) + if (lobby.options.death_on_round_loss) { + client.lives -= 1 + client.sendAction({ action: 'playerInfo', lives: client.lives }) + } if (client.lives === 0) { let gameLoser = null @@ -214,6 +208,10 @@ const failRoundAction = (client: Client) => { } } +const setAnteAction = ({ ante }: ActionHandlerArgs, client: Client) => { + client.ante = ante +} + // Declared partial for now untill all action handlers are defined export const actionHandlers = { username: usernameAction, @@ -230,4 +228,5 @@ export const actionHandlers = { gameInfo: gameInfoAction, lobbyOptions: lobbyOptionsAction, failRound: failRoundAction, + setAnte: setAnteAction, } satisfies Partial diff --git a/Server/src/actions.ts b/Server/src/actions.ts index 92f43a96..0ad83b56 100644 --- a/Server/src/actions.ts +++ b/Server/src/actions.ts @@ -71,6 +71,10 @@ export type ActionGameInfoRequest = { action: 'gameInfo' } export type ActionPlayerInfoRequest = { action: 'playerInfo' } export type ActionEnemyInfoRequest = { action: 'enemyInfo' } export type ActionFailRound = { action: 'failRound' } +export type ActionSetAnte = { + action: 'setAnte' + ante: number +} export type ActionClientToServer = | ActionUsername @@ -88,6 +92,7 @@ export type ActionClientToServer = | ActionUnreadyBlind | ActionLobbyOptions | ActionFailRound + | ActionSetAnte // Utility actions export type ActionKeepAlive = { action: 'keepAlive' } diff --git a/Server/src/main.ts b/Server/src/main.ts index d54efb3c..40994f72 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -9,6 +9,7 @@ import type { ActionJoinLobby, ActionPlayHand, ActionServerToClient, + ActionSetAnte, ActionUsername, ActionUtility, } from './actions.js' @@ -167,6 +168,9 @@ const server = net.createServer((socket) => { case 'failRound': actionHandlers.failRound(client) break + case 'setAnte': + actionHandlers.setAnte(actionArgs as ActionHandlerArgs, client) + break } } catch (error) { const failedToParseError = 'Failed to parse message' From 80bb8d3ac867e6ecd1c69917c33b1a10255944f7 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Thu, 4 Apr 2024 20:18:19 +0000 Subject: [PATCH 0056/1128] Fixed timing and waiting for game_info calls --- Multiplayer/Lobby.lua | 2 ++ Multiplayer/Networking/Action_Handlers.lua | 8 ++++--- Multiplayer/UI/Game_UI.lua | 28 +++++++++++----------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index ba8cb8d5..f1e7fe76 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -31,6 +31,8 @@ G.MULTIPLAYER_GAME = { ready_blind_text = "Ready", processed_round_done = false, lives = 0, + loaded_ante = 0, + loading_blinds = false } PREV_ACHIEVEMENT_VALUE = true diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index 76098a0b..66b24f41 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -135,10 +135,12 @@ end local function action_game_info(small, big, boss) G.GAME.round_resets.blind_choices = { - Small = small or G.GAME.round_resets.blind_choices.Small, - Big = big or G.GAME.round_resets.blind_choices.Big, - Boss = boss or G.GAME.round_resets.blind_choices.Boss + Small = small or 'bl_small', + Big = big or 'bl_big', + Boss = boss or get_new_boss() } + G.MULTIPLAYER_GAME.loaded_ante = G.GAME.round_resets.ante + G.MULTIPLAYER.loading_blinds = false end local function action_lobby_options(options) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 5855ef16..4d83488b 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1608,6 +1608,16 @@ function ease_lives(mod) })) end +local update_blind_select_ref = Game.update_blind_select +function Game:update_blind_select(dt) + if G.MULTIPLAYER_GAME.loaded_ante == G.GAME.round_resets.ante then + update_blind_select_ref(self, dt) + elseif not G.MULTIPLAYER.loading_blinds then + G.MULTIPLAYER.loading_blinds = true + G.MULTIPLAYER.game_info() + end +end + local exit_overlay_menu_ref = G.FUNCS.exit_overlay_menu ---@diagnostic disable-next-line: duplicate-set-field function G.FUNCS:exit_overlay_menu() @@ -1629,25 +1639,15 @@ function G.FUNCS.mods_button(arg_736_0) end local get_new_boss_ref = get_new_boss -local function get_regular_boss() +function get_new_boss() + if G.LOBBY.code and G.GAME.round_resets.blind_choices.Boss then + return G.GAME.round_resets.blind_choices.Boss + end local boss = get_new_boss_ref() while boss == "bl_pvp" do boss = get_new_boss_ref() end return boss end - ---[[ - We repurpose get_new_boss here because it is called to determine the boss blind, - and we only ever need to do that when we are also determining the small and big blinds for a given ante. - - Note: this is also called when a boss is rerolled, but that should be disabled and is also inconsequential -]] -function get_new_boss() - if G.LOBBY.code then - G.MULTIPLAYER.game_info() - end - return get_regular_boss() -end ---------------------------------------------- ------------MOD GAME UI END------------------- From 56d886d823526df390f7e242ea798a1fb00dce6e Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Thu, 4 Apr 2024 20:48:55 +0000 Subject: [PATCH 0057/1128] Fixed blinds not proceeding when small and big are changed --- Multiplayer/UI/Game_UI.lua | 12 +++++------- Server/src/GameMode.ts | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 4d83488b..b4947dd0 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -170,11 +170,7 @@ function create_UIBox_blind_choice(type, run_info) pseudorandom_element(_poker_hands, pseudoseed("orbital")) end - if type == "Small" then - extras = nil - elseif type == "Big" then - extras = nil - elseif not run_info then + if G.GAME.round_resets.blind_choices[type] == 'bl_pvp' then local dt1 = DynaText({ string = { { string = "LIFE", colour = G.C.FILTER } }, colours = { G.C.BLACK }, @@ -237,6 +233,8 @@ function create_UIBox_blind_choice(type, run_info) }, }, } + else + extras = nil end G.GAME.round_resets.blind_ante = G.GAME.round_resets.blind_ante or G.GAME.round_resets.ante @@ -987,9 +985,9 @@ function end_round() G.STATE = G.STATES.ROUND_EVAL G.STATE_COMPLETE = false - if G.GAME.round_resets.blind == G.P_BLINDS.bl_small then + if G.GAME.round_resets.blind_states.Small ~= "Defeated" and G.GAME.round_resets.blind_states.Small ~= "Skipped" then G.GAME.round_resets.blind_states.Small = "Defeated" - elseif G.GAME.round_resets.blind == G.P_BLINDS.bl_big then + elseif G.GAME.round_resets.blind_states.Big ~= "Defeated" and G.GAME.round_resets.blind_states.Big ~= "Skipped" then G.GAME.round_resets.blind_states.Big = "Defeated" else G.GAME.current_round.voucher = get_next_voucher_key() diff --git a/Server/src/GameMode.ts b/Server/src/GameMode.ts index bef85009..8f8ff414 100644 --- a/Server/src/GameMode.ts +++ b/Server/src/GameMode.ts @@ -24,7 +24,7 @@ const GameModes: { 'draft': { startingLives: 2, getBlindFromAnte: (ante) => { - if (ante < 2) return { } + if (ante < 4) return { } else return { small: 'bl_pvp', big: 'bl_pvp', boss: 'bl_pvp' } } } From b29f0ecbc93d1bdaa55c45231207a481a8eb5e37 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 5 Apr 2024 06:57:05 +0000 Subject: [PATCH 0058/1128] Fixed boss not changing and singleplayer waiting for server responses --- Multiplayer/Networking/Action_Handlers.lua | 2 +- Multiplayer/UI/Game_UI.lua | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index 66b24f41..d112aaae 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -137,7 +137,7 @@ local function action_game_info(small, big, boss) G.GAME.round_resets.blind_choices = { Small = small or 'bl_small', Big = big or 'bl_big', - Boss = boss or get_new_boss() + Boss = boss or get_new_boss(true) } G.MULTIPLAYER_GAME.loaded_ante = G.GAME.round_resets.ante G.MULTIPLAYER.loading_blinds = false diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index b4947dd0..78ef6681 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1608,7 +1608,7 @@ end local update_blind_select_ref = Game.update_blind_select function Game:update_blind_select(dt) - if G.MULTIPLAYER_GAME.loaded_ante == G.GAME.round_resets.ante then + if G.MULTIPLAYER_GAME.loaded_ante == G.GAME.round_resets.ante or not G.LOBBY.code then update_blind_select_ref(self, dt) elseif not G.MULTIPLAYER.loading_blinds then G.MULTIPLAYER.loading_blinds = true @@ -1637,8 +1637,8 @@ function G.FUNCS.mods_button(arg_736_0) end local get_new_boss_ref = get_new_boss -function get_new_boss() - if G.LOBBY.code and G.GAME.round_resets.blind_choices.Boss then +function get_new_boss(force_change) + if G.LOBBY.code and G.GAME.round_resets.blind_choices.Boss and not force_change then return G.GAME.round_resets.blind_choices.Boss end local boss = get_new_boss_ref() From 998841da5ce12075514e15af75668ff70ceb7b49 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 5 Apr 2024 07:22:51 +0000 Subject: [PATCH 0059/1128] Fixed screenwipe crashes --- Multiplayer/Lobby.lua | 82 +++++++++++++++++++++++++++++++++++-- Multiplayer/UI/Lobby_UI.lua | 1 - 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index f1e7fe76..51915d04 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -79,11 +79,87 @@ function G.MULTIPLAYER.update_player_usernames() end end +--[[ + There doesn't seem to be any other way to fix the wipe crashes than copying and manipulating the whole function +]] -local wipe_off_ref = G.FUNCS.wipe_off G.FUNCS.wipe_off = function() - if not G.screenwipe then return end - wipe_off_ref() + G.E_MANAGER:add_event(Event({ + no_delete = true, + func = function() + delay(0.3) + if not G.screenwipe then return true end + G.screenwipe.children.particles.max = 0 + G.E_MANAGER:add_event(Event({ + trigger = 'ease', + no_delete = true, + blockable = false, + blocking = false, + timer = 'REAL', + ref_table = G.screenwipe.colours.black, + ref_value = 4, + ease_to = 0, + delay = 0.3, + func = (function(t) return t end) + })) + G.E_MANAGER:add_event(Event({ + trigger = 'ease', + no_delete = true, + blockable = false, + blocking = false, + timer = 'REAL', + ref_table = G.screenwipe.colours.white, + ref_value = 4, + ease_to = 0, + delay = 0.3, + func = (function(t) return t end) + })) + return true + end + })) + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 0.55, + no_delete = true, + blocking = false, + timer = 'REAL', + func = function() + if not G.screenwipe then return true end + if G.screenwipecard then G.screenwipecard:start_dissolve({G.C.BLACK, G.C.ORANGE,G.C.GOLD, G.C.RED}) end + if G.screenwipe:get_UIE_by_ID('text') then + for k, v in ipairs(G.screenwipe:get_UIE_by_ID('text').children) do + v.children[1].config.object:pop_out(4) + end + end + return true + end + })) + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 1.1, + no_delete = true, + blocking = false, + timer = 'REAL', + func = function() + if not G.screenwipe then return true end + G.screenwipe.children.particles:remove() + G.screenwipe:remove() + G.screenwipe.children.particles = nil + G.screenwipe = nil + G.screenwipecard = nil + return true + end + })) + G.E_MANAGER:add_event(Event({ + trigger = 'after', + delay = 1.2, + no_delete = true, + blocking = true, + timer = 'REAL', + func = function() + return true + end + })) end ---------------------------------------------- diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua index c8d254e5..ec4cd6ea 100644 --- a/Multiplayer/UI/Lobby_UI.lua +++ b/Multiplayer/UI/Lobby_UI.lua @@ -464,7 +464,6 @@ function G.FUNCS.display_lobby_main_menu_UI(e) end function G.FUNCS.return_to_lobby() - G.FUNCS.go_to_menu() G.MULTIPLAYER.stop_game() end From 78364da96cd92b876089165671bcb335be1775c8 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 5 Apr 2024 07:50:53 +0000 Subject: [PATCH 0060/1128] Fixed serverside ante not reseting between games --- Multiplayer/Networking/Action_Handlers.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index d112aaae..23e48e06 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -73,6 +73,7 @@ end ---@param stake_str string local function action_start_game(deck, seed, stake_str) local stake = tonumber(stake_str) + G.MULTIPLAYER.set_ante(0) G.FUNCS.lobby_start_run(nil, { deck = deck, seed = seed, stake = stake }) end From dcbc41525c854085f3922161846cb1fe618a2ae7 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 5 Apr 2024 07:56:51 +0000 Subject: [PATCH 0061/1128] Bumped to 0.1.1 --- Multiplayer/UI/Main_Menu.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/UI/Main_Menu.lua b/Multiplayer/UI/Main_Menu.lua index 308f1bab..159d512b 100644 --- a/Multiplayer/UI/Main_Menu.lua +++ b/Multiplayer/UI/Main_Menu.lua @@ -6,7 +6,7 @@ local Utils = require("Utils") -MULTIPLAYER_VERSION = "0.1.0-MULTIPLAYER" +MULTIPLAYER_VERSION = "0.1.1-MULTIPLAYER" local game_main_menu_ref = Game.main_menu ---@diagnostic disable-next-line: duplicate-set-field From 62c64c8c2950d7daa54a6bf471cff19e8336cd3c Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 9 Apr 2024 04:09:51 +0000 Subject: [PATCH 0062/1128] Fixed typo --- Multiplayer/UI/Game_UI.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 78ef6681..273a6084 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1173,7 +1173,7 @@ function create_UIBox_game_over() { n = G.UIT.T, config = { - text = "Retun to Lobby", + text = "Return to Lobby", scale = 0.5, colour = G.C.UI.TEXT_LIGHT, }, From 5709119d0aacff3bf8262f990e8085df6ce43b94 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 9 Apr 2024 04:14:51 +0000 Subject: [PATCH 0063/1128] Fixed booster pack crash --- Multiplayer/UI/Game_UI.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 273a6084..550efee1 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1647,5 +1647,16 @@ function get_new_boss(force_change) end return boss end + +G.FUNCS.can_skip_booster = function(e) + if G.pack_cards and G.pack_cards.cards and G.pack_cards.cards[1] and + (G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.STANDARD_PACK or G.STATE == G.STATES.BUFFOON_PACK or (G.hand and G.hand.cards[1])) then + e.config.colour = G.C.GREY + e.config.button = 'skip_booster' + else + e.config.colour = G.C.UI.BACKGROUND_INACTIVE + e.config.button = nil + end +end ---------------------------------------------- ------------MOD GAME UI END------------------- From 1ec371033be9dd7ad6fa79236dfdd064a2b7d326 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 9 Apr 2024 04:17:31 +0000 Subject: [PATCH 0064/1128] Fixed double life loss on pvp blnds when life loss option on --- Server/src/actionHandlers.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 18bf2421..6ee45ba8 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -149,7 +149,9 @@ const playHandAction = ( roundWinner.id === lobby.host.id ? lobby.guest : lobby.host if (lobby.host.score !== lobby.guest.score) { - roundLoser.lives -= 1 + if (!lobby.options.death_on_round_loss) { + roundLoser.lives -= 1 + } roundLoser.sendAction({ action: 'playerInfo', lives: roundLoser.lives }) // If no lives are left, we end the game From f44b5f4de8caddc0e19f362e4ad418ec16553626 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 9 Apr 2024 04:22:04 +0000 Subject: [PATCH 0065/1128] Prevented connection status from appearing outside of main menu --- Multiplayer/Lobby.lua | 6 ++++-- Multiplayer/UI/Main_Menu.lua | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 51915d04..a07f7ee2 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -49,8 +49,10 @@ function G.MULTIPLAYER.update_connection_status() if G.HUD_connection_status then G.HUD_connection_status:remove() - end - G.HUD_connection_status = G.UIDEF.get_connection_status_ui() + end + if G.STAGE == G.STAGES.MAIN_MENU then + G.HUD_connection_status = G.UIDEF.get_connection_status_ui() + end end local gameMainMenuRef = Game.main_menu diff --git a/Multiplayer/UI/Main_Menu.lua b/Multiplayer/UI/Main_Menu.lua index 159d512b..ca236c01 100644 --- a/Multiplayer/UI/Main_Menu.lua +++ b/Multiplayer/UI/Main_Menu.lua @@ -6,7 +6,7 @@ local Utils = require("Utils") -MULTIPLAYER_VERSION = "0.1.1-MULTIPLAYER" +MULTIPLAYER_VERSION = "0.1.2-MULTIPLAYER" local game_main_menu_ref = Game.main_menu ---@diagnostic disable-next-line: duplicate-set-field From 6512e649f6031d90397a1d7fb95423809a68c01e Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Tue, 9 Apr 2024 04:35:00 +0000 Subject: [PATCH 0066/1128] Forced hand eval on last hand in pvp --- Multiplayer/UI/Game_UI.lua | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 550efee1..06ffc090 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -731,6 +731,72 @@ function G.UIDEF.shop() return t end +local function eval_hand_and_jokers() + for i=1, #G.hand.cards do + --Check for hand doubling + local reps = {1} + local j = 1 + while j <= #reps do + local percent = (i-0.999)/(#G.hand.cards-0.998) + (j-1)*0.1 + if reps[j] ~= 1 then card_eval_status_text((reps[j].jokers or reps[j].seals).card, 'jokers', nil, nil, nil, (reps[j].jokers or reps[j].seals)) end + + --calculate the hand effects + local effects = {G.hand.cards[i]:get_end_of_round_effect()} + for k=1, #G.jokers.cards do + --calculate the joker individual card effects + local eval = G.jokers.cards[k]:calculate_joker({cardarea = G.hand, other_card = G.hand.cards[i], individual = true, end_of_round = true}) + if eval then + table.insert(effects, eval) + end + end + + if reps[j] == 1 then + --Check for hand doubling + --From Red seal + local eval = eval_card(G.hand.cards[i], {end_of_round = true,cardarea = G.hand, repetition = true, repetition_only = true}) + if next(eval) and (next(effects[1]) or #effects > 1) then + for h = 1, eval.seals.repetitions do + reps[#reps+1] = eval + end + end + + --from Jokers + for j=1, #G.jokers.cards do + --calculate the joker effects + local eval = eval_card(G.jokers.cards[j], {cardarea = G.hand, other_card = G.hand.cards[i], repetition = true, end_of_round = true, card_effects = effects}) + if next(eval) then + for h = 1, eval.jokers.repetitions do + reps[#reps+1] = eval + end + end + end + end + + for ii = 1, #effects do + --if this effect came from a joker + if effects[ii].card then + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = (function() effects[ii].card:juice_up(0.7);return true end) + })) + end + + --If dollars + if effects[ii].h_dollars then + ease_dollars(effects[ii].h_dollars) + card_eval_status_text(G.hand.cards[i], 'dollars', effects[ii].h_dollars, percent) + end + + --Any extras + if effects[ii].extra then + card_eval_status_text(G.hand.cards[i], 'extra', nil, percent, nil, effects[ii].extra) + end + end + j = j + 1 + end + end +end + local update_hand_played_ref = Game.update_hand_played ---@diagnostic disable-next-line: duplicate-set-field function Game:update_hand_played(dt) @@ -760,6 +826,7 @@ function Game:update_hand_played(dt) -- For now, never advance to next round if G.GAME.current_round.hands_left < 1 then if G.hand.cards[1] then + eval_hand_and_jokers() attention_text({ scale = 0.8, text = "Waiting for enemy to finish...", From 14be97a82979f5fa90905b8368ca2761daf1758e Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Thu, 11 Apr 2024 20:54:00 +0000 Subject: [PATCH 0067/1128] Rewrote get_new_boss to stop crashes --- Multiplayer/UI/Game_UI.lua | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 06ffc090..18e5d4f2 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1703,6 +1703,48 @@ function G.FUNCS.mods_button(arg_736_0) mods_button_ref(arg_736_0) end +-- Rewritten from the original function to fix typing issues causing crashes +function get_new_boss() + G.GAME.perscribed_bosses = G.GAME.perscribed_bosses or {} + if G.GAME.perscribed_bosses and G.GAME.perscribed_bosses[G.GAME.round_resets.ante] then + local ret_boss = G.GAME.perscribed_bosses[G.GAME.round_resets.ante] + G.GAME.perscribed_bosses[G.GAME.round_resets.ante] = nil + G.GAME.bosses_used[ret_boss] = G.GAME.bosses_used[ret_boss] + 1 + return ret_boss + end + if G.FORCE_BOSS then return G.FORCE_BOSS end + + local eligible_bosses = {} + for k, v in pairs(G.P_BLINDS) do + if v.boss then + local condition = v.boss.showdown and (G.GAME.round_resets.ante)%G.GAME.win_ante == 0 and G.GAME.round_resets.ante >= 2 + or not v.boss.showdown and (v.boss.min <= math.max(1, G.GAME.round_resets.ante) and ((math.max(1, G.GAME.round_resets.ante))%G.GAME.win_ante ~= 0 or G.GAME.round_resets.ante < 2)) + if condition then + eligible_bosses[k] = true + end + end + end + + local min_use = 100 + local bosses_with_min_use = {} + for k, _ in pairs(eligible_bosses) do + local uses = G.GAME.bosses_used[k] or 0 + if uses <= min_use then + if uses < min_use then + bosses_with_min_use = {} + min_use = uses + end + bosses_with_min_use[k] = true + end + end + + local _, boss = pseudorandom_element(bosses_with_min_use, pseudoseed('boss')) + G.GAME.bosses_used[boss] = (G.GAME.bosses_used[boss] or 0) + 1 + + return boss +end + + local get_new_boss_ref = get_new_boss function get_new_boss(force_change) if G.LOBBY.code and G.GAME.round_resets.blind_choices.Boss and not force_change then From eab329cecb38dd7b45b09d052fa8b84b6e8236c9 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Thu, 11 Apr 2024 23:59:01 +0000 Subject: [PATCH 0068/1128] Fixed some multiplayer game states not resetting between games --- Multiplayer/Lobby.lua | 27 ++++++++++++++++++---- Multiplayer/Networking/Action_Handlers.lua | 9 ++++---- Multiplayer/UI/Game_UI.lua | 17 +++++++++++--- Multiplayer/UI/Lobby_UI.lua | 1 + 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index a07f7ee2..56a32ed1 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -20,10 +20,6 @@ G.LOBBY = { host = {}, guest = {}, is_host = false, - enemy = { - score = 0, - hands = 4, - }, } G.MULTIPLAYER_GAME = { @@ -32,9 +28,30 @@ G.MULTIPLAYER_GAME = { processed_round_done = false, lives = 0, loaded_ante = 0, - loading_blinds = false + loading_blinds = false, + end_pvp = false, + enemy = { + score = 0, + hands = 4, + }, } +function reset_game_states() + G.MULTIPLAYER_GAME = { + ready_blind = false, + ready_blind_text = "Ready", + processed_round_done = false, + lives = 0, + loaded_ante = 0, + loading_blinds = false, + end_pvp = false, + enemy = { + score = 0, + hands = 4, + }, + } +end + PREV_ACHIEVEMENT_VALUE = true function G.MULTIPLAYER.update_connection_status() -- Save the previous value of the achievement flag diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index 23e48e06..e97f8051 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -72,6 +72,7 @@ end ---@param seed string ---@param stake_str string local function action_start_game(deck, seed, stake_str) + reset_game_states() local stake = tonumber(stake_str) G.MULTIPLAYER.set_ante(0) G.FUNCS.lobby_start_run(nil, { deck = deck, seed = seed, stake = stake }) @@ -95,8 +96,8 @@ local function action_enemy_info(score_str, hands_left_str) return end - G.LOBBY.enemy.score = score - G.LOBBY.enemy.hands = hands_left + G.MULTIPLAYER_GAME.enemy.score = score + G.MULTIPLAYER_GAME.enemy.hands = hands_left if is_pvp_boss() then G.HUD_blind:get_UIE_by_ID("HUD_blind_count"):juice_up() G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned"):juice_up() @@ -107,12 +108,12 @@ local function action_stop_game() if G.STAGE ~= G.STAGES.MAIN_MENU then G.FUNCS.go_to_menu() G.MULTIPLAYER.update_connection_status() + reset_game_states() end end local function action_end_pvp() - G.STATE_COMPLETE = false - G.STATE = G.STATES.NEW_ROUND + G.MULTIPLAYER_GAME.end_pvp = true end ---@param lives number diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 18e5d4f2..15a6a7e8 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -583,14 +583,14 @@ local function update_blind_HUD() delay = 0.3, blockable = false, func = function() - G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_table = G.LOBBY.enemy + G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_table = G.MULTIPLAYER_GAME.enemy G.HUD_blind:get_UIE_by_ID("HUD_blind_count").config.ref_value = "score" G.HUD_blind:get_UIE_by_ID("HUD_blind").children[2].children[2].children[2].children[1].children[1].config.text = "Current enemy score" G.HUD_blind:get_UIE_by_ID("HUD_blind").children[2].children[2].children[2].children[3].children[1].config.text = "Enemy hands left: " G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object.config.string = - { { ref_table = G.LOBBY.enemy, ref_value = "hands" } } + { { ref_table = G.MULTIPLAYER_GAME.enemy, ref_value = "hands" } } G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object:update_text() G.HUD_blind.alignment.offset.y = 0 return true @@ -822,7 +822,7 @@ function Game:update_hand_played(dt) func = function() G.MULTIPLAYER.play_hand(G.GAME.chips, G.GAME.current_round.hands_left) -- Set blind chips to enemy score - G.GAME.blind.chips = G.LOBBY.enemy.score + G.GAME.blind.chips = G.MULTIPLAYER_GAME.enemy.score -- For now, never advance to next round if G.GAME.current_round.hands_left < 1 then if G.hand.cards[1] then @@ -1767,5 +1767,16 @@ G.FUNCS.can_skip_booster = function(e) e.config.button = nil end end + +local update_selecting_hand_ref = Game.update_selecting_hand +function Game:update_selecting_hand(dt) + update_selecting_hand_ref(self, dt) + if G.MULTIPLAYER_GAME.end_pvp then + G.STATE_COMPLETE = false + G.STATE = G.STATES.NEW_ROUND + G.MULTIPLAYER_GAME.end_pvp = false + return + end +end ---------------------------------------------- ------------MOD GAME UI END------------------- diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua index ec4cd6ea..4cf9de03 100644 --- a/Multiplayer/UI/Lobby_UI.lua +++ b/Multiplayer/UI/Lobby_UI.lua @@ -485,6 +485,7 @@ function Game:update(dt) in_lobby = not in_lobby G.F_NO_SAVING = in_lobby self.FUNCS.go_to_menu() + reset_game_states() end gameUpdateRef(self, dt) end From e750491b57cfb8531c9d1d03987e147c54d47e14 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 00:52:50 +0000 Subject: [PATCH 0069/1128] Fixed cards being put in hand after pvp game ends --- Multiplayer/UI/Game_UI.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 15a6a7e8..ecab8338 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -848,6 +848,12 @@ function Game:update_hand_played(dt) end, })) end + + if G.MULTIPLAYER_GAME.end_pvp then + G.STATE_COMPLETE = false + G.STATE = G.STATES.NEW_ROUND + G.MULTIPLAYER_GAME.end_pvp = false + end end local can_play_ref = G.FUNCS.can_play From d0715f94f0ec2e3efa1ecc6333aa5893dc8113e1 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 01:14:47 +0000 Subject: [PATCH 0070/1128] Fixed double end-blind-eval screen --- Multiplayer/UI/Game_UI.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index ecab8338..c877e93e 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -839,7 +839,7 @@ function Game:update_hand_played(dt) end G.MULTIPLAYER_GAME.processed_round_done = true - else + elseif not G.MULTIPLAYER_GAME.end_pvp then G.STATE_COMPLETE = false G.STATE = G.STATES.DRAW_TO_HAND end From 141b9b4fedb3d98c2e33807cdf948a0dd7af6063 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 01:16:28 +0000 Subject: [PATCH 0071/1128] Fixed lives visually being reset on the client --- Server/src/actionHandlers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 6ee45ba8..10c4c273 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -64,13 +64,13 @@ const startGameAction = (client: Client) => { let lives = GameModes[lobby.gameMode].startingLives - // Reset players' lives - lobby.setPlayersLives(lives) lobby.broadcastAction({ action: 'startGame', deck: 'c_multiplayer_1', seed: lobby.options.different_seeds? undefined : generateSeed(), }) + // Reset players' lives + lobby.setPlayersLives(lives) } const readyBlindAction = (client: Client) => { From 3d747048b6853b33418da4aa651f9b440f2a73a6 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 01:34:56 +0000 Subject: [PATCH 0072/1128] Prevents player from opening booster while readied --- Multiplayer/UI/Game_UI.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index c877e93e..3720f82d 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -622,6 +622,7 @@ function G.FUNCS.mp_toggle_ready(e) if G.MULTIPLAYER_GAME.ready_blind then G.MULTIPLAYER.ready_blind() + stop_use() else G.MULTIPLAYER.unready_blind() end @@ -1784,5 +1785,15 @@ function Game:update_selecting_hand(dt) return end end + +local can_open_ref = G.FUNCS.can_open +G.FUNCS.can_open = function(e) + if G.MULTIPLAYER_GAME.ready_blind then + e.config.colour = G.C.UI.BACKGROUND_INACTIVE + e.config.button = nil + return + end + can_open_ref(e) +end ---------------------------------------------- ------------MOD GAME UI END------------------- From 98187b9da4a3937aa27d0f6a1bd09c1c3b1b514a Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 01:41:53 +0000 Subject: [PATCH 0073/1128] Fixed pvp disabled bug --- Multiplayer/UI/Game_UI.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 3720f82d..cc07f826 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1795,5 +1795,13 @@ G.FUNCS.can_open = function(e) end can_open_ref(e) end + +local blind_disable_ref = Blind.disable +function Blind:disable() + if is_pvp_boss() then + return + end + blind_disable_ref(self) +end ---------------------------------------------- ------------MOD GAME UI END------------------- From 51a1ee8de04618f05955b698c526f28297a5f51d Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 04:15:39 +0000 Subject: [PATCH 0074/1128] Fixed j_throwback showing up in multiplayer --- Multiplayer/Items/Deck.lua | 15 ++++++++------- Multiplayer/UI/Game_UI.lua | 2 ++ Multiplayer/UI/Lobby_UI.lua | 25 +------------------------ Multiplayer/UI/Main_Menu.lua | 18 ------------------ 4 files changed, 11 insertions(+), 49 deletions(-) diff --git a/Multiplayer/Items/Deck.lua b/Multiplayer/Items/Deck.lua index 2f831663..0493025b 100644 --- a/Multiplayer/Items/Deck.lua +++ b/Multiplayer/Items/Deck.lua @@ -30,7 +30,8 @@ local c_multiplayer_1 = { }, } -G.CHALLENGES[21] = c_multiplayer_1 +local c_multiplayer_1_index = #G.CHALLENGES+1 +G.CHALLENGES[c_multiplayer_1_index] = c_multiplayer_1 local localize_ref = localize function localize(args, misc_cat) @@ -42,24 +43,24 @@ end local set_discover_tallies_ref = set_discover_tallies function set_discover_tallies() - G.CHALLENGES[21] = nil + G.CHALLENGES[c_multiplayer_1_index] = nil local res = set_discover_tallies_ref() - G.CHALLENGES[21] = c_multiplayer_1 + G.CHALLENGES[c_multiplayer_1_index] = c_multiplayer_1 return res end local challenge_list_ref = G.FUNCS.challenge_list G.FUNCS.challenge_list = function(e) - G.CHALLENGES[21] = nil + G.CHALLENGES[c_multiplayer_1_index] = nil challenge_list_ref(e) - G.CHALLENGES[21] = c_multiplayer_1 + G.CHALLENGES[c_multiplayer_1_index] = c_multiplayer_1 end local challenges_ref = G.UIDEF.challenges function G.UIDEF.challenges(from_game_over) - G.CHALLENGES[21] = nil + G.CHALLENGES[c_multiplayer_1_index] = nil local res = challenges_ref(from_game_over) - G.CHALLENGES[21] = c_multiplayer_1 + G.CHALLENGES[c_multiplayer_1_index] = c_multiplayer_1 return res end diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index cc07f826..1e1e98d7 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -4,6 +4,8 @@ ---------------------------------------------- ------------MOD GAME UI----------------------- +local Utils = require("Utils") + local create_UIBox_options_ref = create_UIBox_options ---@diagnostic disable-next-line: lowercase-global function create_UIBox_options() diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua index 4cf9de03..17fb14e3 100644 --- a/Multiplayer/UI/Lobby_UI.lua +++ b/Multiplayer/UI/Lobby_UI.lua @@ -406,30 +406,7 @@ function G.FUNCS.lobby_start_run(e, args) G.FUNCS.start_run(e, { stake = 1, seed = args.seed, - challenge = { - name = "Multiplayer Deck", - id = "c_multiplayer_1", - rules = { - custom = {}, - modifiers = {}, - }, - jokers = {}, - consumeables = {}, - vouchers = {}, - deck = { - type = "Challenge Deck", - }, - restrictions = { - banned_cards = { - { id = "j_diet_cola" }, -- Intention to disable skipping - { id = "j_mr_bones" }, - { id = "v_hieroglyph" }, - { id = "v_petroglyph" }, - }, - banned_tags = {}, - banned_other = {}, - }, - }, + challenge = G.CHALLENGES[get_challenge_int_from_id('c_multiplayer_1')], }) end diff --git a/Multiplayer/UI/Main_Menu.lua b/Multiplayer/UI/Main_Menu.lua index ca236c01..90004fe5 100644 --- a/Multiplayer/UI/Main_Menu.lua +++ b/Multiplayer/UI/Main_Menu.lua @@ -274,24 +274,6 @@ function G.UIDEF.create_UIBox_join_lobby_button() align = "cm", }, nodes = { - { - n = G.UIT.R, - config = { - padding = 0.5, - align = "cm", - }, - nodes = { - { - n = G.UIT.T, - config = { - scale = 0.6, - shadow = true, - text = "Lobby Code:", - colour = G.C.UI.TEXT_LIGHT, - }, - }, - }, - }, { n = G.UIT.R, config = { From c1ea1fb7807bf58b6020bd78079baa7f24692f98 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 05:54:58 +0000 Subject: [PATCH 0075/1128] Added comeback money based on lives lost --- Multiplayer/Lobby.lua | 4 +- Multiplayer/Networking/Action_Handlers.lua | 2 + Multiplayer/UI/Game_UI.lua | 151 ++++++++++++++++----- 3 files changed, 123 insertions(+), 34 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 51915d04..9dca41b9 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -32,7 +32,9 @@ G.MULTIPLAYER_GAME = { processed_round_done = false, lives = 0, loaded_ante = 0, - loading_blinds = false + loading_blinds = false, + comeback_bonus_given = true, + comeback_bonus = 0 } PREV_ACHIEVEMENT_VALUE = true diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index 23e48e06..fb7e20b1 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -117,6 +117,8 @@ end ---@param lives number local function action_player_info(lives) + G.MULTIPLAYER_GAME.comeback_bonus_given = false + G.MULTIPLAYER_GAME.comeback_bonus = G.MULTIPLAYER_GAME.comeback_bonus + (1 * (lives - G.MULTIPLAYER_GAME.lives)) if (G.MULTIPLAYER_GAME.lives ~= lives) then ease_lives(lives - G.MULTIPLAYER_GAME.lives) end diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index 78ef6681..04430b0f 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1401,7 +1401,7 @@ end local add_round_eval_row_ref = add_round_eval_row function add_round_eval_row(config) - if G.LOBBY.code and config.name == "blind1" and G.GAME.blind.chips == -1 then + if G.LOBBY.code and (config.name == "blind1" or config.name == "comeback") then local config = config or {} local width = G.round_eval.T.w - 0.51 local num_dollars = config.dollars or 1 @@ -1413,42 +1413,47 @@ function add_round_eval_row(config) func = function() --Add the far left text and context first: local left_text = {} - local blind_sprite = - AnimatedSprite(0, 0, 1.2, 1.2, G.ANIMATION_ATLAS["blind_chips"], copy_table(G.GAME.blind.pos)) - blind_sprite:define_draw_steps({ - { shader = "dissolve", shadow_height = 0.05 }, - { shader = "dissolve" }, - }) - table.insert(left_text, { - n = G.UIT.O, - config = { w = 1.2, h = 1.2, object = blind_sprite, hover = true, can_collide = false }, - }) - - table.insert(left_text, { - n = G.UIT.C, - config = { padding = 0.05, align = "cm" }, - nodes = { - { - n = G.UIT.R, - config = { align = "cm" }, - nodes = { - { - n = G.UIT.O, - config = { - object = DynaText({ - string = { (is_pvp_boss() or G.LOBBY.config.death_on_round_loss) and " Lost a Life " or " Failed " }, - colours = { G.C.FILTER }, - shadow = true, - pop_in = 0, - scale = 0.5 * scale, - silent = true, - }), + if config.name == "blind1" then + local blind_sprite = + AnimatedSprite(0, 0, 1.2, 1.2, G.ANIMATION_ATLAS["blind_chips"], copy_table(G.GAME.blind.pos)) + blind_sprite:define_draw_steps({ + { shader = "dissolve", shadow_height = 0.05 }, + { shader = "dissolve" }, + }) + table.insert(left_text, { + n = G.UIT.O, + config = { w = 1.2, h = 1.2, object = blind_sprite, hover = true, can_collide = false }, + }) + + table.insert(left_text, { + n = G.UIT.C, + config = { padding = 0.05, align = "cm" }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.O, + config = { + object = DynaText({ + string = { G.GAME.blind.chips == -1 and ((is_pvp_boss() or G.LOBBY.config.death_on_round_loss) and " Lost a Life " or " Failed ") or "Defeated the Enemy" }, + colours = { G.C.FILTER }, + shadow = true, + pop_in = 0, + scale = 0.5 * scale, + silent = true, + }), + }, }, }, }, }, - }, - }) + }) + elseif config.name == 'comeback' then + table.insert(left_text, {n=G.UIT.T, config={text = G.MULTIPLAYER_GAME.comeback_bonus, scale = 0.8*scale, colour = G.C.BLACK, shadow = true, juice = true}}) + table.insert(left_text, {n=G.UIT.O, config={object = DynaText({string = {" Total Lives Lost ($4 each)"}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4*scale, silent = true})}}) + end local full_row = { n = G.UIT.R, config = { align = "cm", minw = 5 }, @@ -1560,6 +1565,86 @@ function add_round_eval_row(config) end end +G.FUNCS.evaluate_round = function() + local pitch = 0.95 + local dollars = 0 + + if G.GAME.chips - G.GAME.blind.chips >= 0 then + add_round_eval_row({dollars = G.GAME.blind.dollars, name='blind1', pitch = pitch}) + pitch = pitch + 0.06 + dollars = dollars + G.GAME.blind.dollars + else + add_round_eval_row({dollars = 0, name='blind1', pitch = pitch, saved = true}) + pitch = pitch + 0.06 + end + + G.E_MANAGER:add_event(Event({ + trigger = 'before', + delay = 1.3*math.min(G.GAME.blind.dollars+2, 7)/2*0.15 + 0.5, + func = function() + G.GAME.blind:defeat() + return true + end + })) + delay(0.2) + G.E_MANAGER:add_event(Event({ + func = function() + ease_background_colour_blind(G.STATES.ROUND_EVAL, '') + return true + end + })) + G.GAME.selected_back:trigger_effect({context = 'eval'}) + + if G.GAME.current_round.hands_left > 0 and not G.GAME.modifiers.no_extra_hand_money then + add_round_eval_row({dollars = G.GAME.current_round.hands_left*(G.GAME.modifiers.money_per_hand or 1), disp = G.GAME.current_round.hands_left, bonus = true, name='hands', pitch = pitch}) + pitch = pitch + 0.06 + dollars = dollars + G.GAME.current_round.hands_left*(G.GAME.modifiers.money_per_hand or 1) + end + if G.GAME.current_round.discards_left > 0 and G.GAME.modifiers.money_per_discard then + add_round_eval_row({dollars = G.GAME.current_round.discards_left*(G.GAME.modifiers.money_per_discard), disp = G.GAME.current_round.discards_left, bonus = true, name='discards', pitch = pitch}) + pitch = pitch + 0.06 + dollars = dollars + G.GAME.current_round.discards_left*(G.GAME.modifiers.money_per_discard) + end + for i = 1, #G.jokers.cards do + local ret = G.jokers.cards[i]:calculate_dollar_bonus() + if ret then + add_round_eval_row({dollars = ret, bonus = true, name='joker'..i, pitch = pitch, card = G.jokers.cards[i]}) + pitch = pitch + 0.06 + dollars = dollars + ret + end + end + for i = 1, #G.GAME.tags do + local ret = G.GAME.tags[i]:apply_to_run({type = 'eval'}) + if ret then + add_round_eval_row({dollars = ret.dollars, bonus = true, name='tag'..i, pitch = pitch, condition = ret.condition, pos = ret.pos, tag = ret.tag}) + pitch = pitch + 0.06 + dollars = dollars + ret.dollars + end + end + if G.GAME.dollars >= 5 and not G.GAME.modifiers.no_interest then + add_round_eval_row({bonus = true, name='interest', pitch = pitch, dollars = G.GAME.interest_amount*math.min(math.floor(G.GAME.dollars/5), G.GAME.interest_cap/5)}) + pitch = pitch + 0.06 + if not G.GAME.seeded and not G.GAME.challenge then + if G.GAME.interest_amount*math.min(math.floor(G.GAME.dollars/5), G.GAME.interest_cap/5) == G.GAME.interest_amount*G.GAME.interest_cap/5 then + G.PROFILES[G.SETTINGS.profile].career_stats.c_round_interest_cap_streak = G.PROFILES[G.SETTINGS.profile].career_stats.c_round_interest_cap_streak + 1 + else + G.PROFILES[G.SETTINGS.profile].career_stats.c_round_interest_cap_streak = 0 + end + end + check_for_unlock({type = 'interest_streak'}) + dollars = dollars + G.GAME.interest_amount*math.min(math.floor(G.GAME.dollars/5), G.GAME.interest_cap/5) + end + if not G.MULTIPLAYER_GAME.comeback_bonus_given then + G.MULTIPLAYER_GAME.comeback_bonus_given = true + add_round_eval_row({bonus = true, name='comeback', pitch = pitch, dollars = 4*G.MULTIPLAYER_GAME.comeback_bonus}) + dollars = dollars + 4*G.MULTIPLAYER_GAME.comeback_bonus + end + + pitch = pitch + 0.06 + + add_round_eval_row({name = 'bottom', dollars = dollars}) +end + local ease_ante_ref = ease_ante function ease_ante(mod) G.MULTIPLAYER.set_ante(G.GAME.round_resets.ante + mod) From eb4106d47f8dd2cf6b6dba606a920cdf97dd756d Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 06:44:29 +0000 Subject: [PATCH 0076/1128] Implemented comeback money mechanic --- Multiplayer/Networking/Action_Handlers.lua | 6 ++- Multiplayer/UI/Game_UI.lua | 53 ++++++++++++---------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index a6ad9e86..835d827d 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -118,9 +118,11 @@ end ---@param lives number local function action_player_info(lives) - G.MULTIPLAYER_GAME.comeback_bonus_given = false - G.MULTIPLAYER_GAME.comeback_bonus = G.MULTIPLAYER_GAME.comeback_bonus + (1 * (lives - G.MULTIPLAYER_GAME.lives)) if (G.MULTIPLAYER_GAME.lives ~= lives) then + if G.MULTIPLAYER_GAME.lives ~= 0 then + G.MULTIPLAYER_GAME.comeback_bonus_given = false + G.MULTIPLAYER_GAME.comeback_bonus = G.MULTIPLAYER_GAME.comeback_bonus + 1 + end ease_lives(lives - G.MULTIPLAYER_GAME.lives) end G.MULTIPLAYER_GAME.lives = lives diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index cc5b1a43..d8cf4666 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1477,7 +1477,7 @@ end local add_round_eval_row_ref = add_round_eval_row function add_round_eval_row(config) - if G.LOBBY.code and (config.name == "blind1" or config.name == "comeback") then + if G.LOBBY.code and ((config.name == "blind1" and (is_pvp_boss() or (G.LOBBY.config.death_on_round_loss and G.GAME.blind.chips == -1))) or config.name == "comeback") then local config = config or {} local width = G.round_eval.T.w - 0.51 local num_dollars = config.dollars or 1 @@ -1501,33 +1501,34 @@ function add_round_eval_row(config) config = { w = 1.2, h = 1.2, object = blind_sprite, hover = true, can_collide = false }, }) - table.insert(left_text, { - n = G.UIT.C, - config = { padding = 0.05, align = "cm" }, - nodes = { - { - n = G.UIT.R, - config = { align = "cm" }, - nodes = { - { - n = G.UIT.O, - config = { - object = DynaText({ - string = { G.GAME.blind.chips == -1 and ((is_pvp_boss() or G.LOBBY.config.death_on_round_loss) and " Lost a Life " or " Failed ") or "Defeated the Enemy" }, - colours = { G.C.FILTER }, - shadow = true, - pop_in = 0, - scale = 0.5 * scale, - silent = true, - }), + table.insert(left_text, + { + n = G.UIT.C, + config = { padding = 0.05, align = "cm" }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.O, + config = { + object = DynaText({ + string = { G.GAME.blind.chips == -1 and ((is_pvp_boss() or G.LOBBY.config.death_on_round_loss) and " Lost a Life " or " Failed ") or "Defeated the Enemy" }, + colours = { G.C.FILTER }, + shadow = true, + pop_in = 0, + scale = 0.5 * scale, + silent = true, + }), + }, }, }, }, }, - }, - }) + }) elseif config.name == 'comeback' then - table.insert(left_text, {n=G.UIT.T, config={text = G.MULTIPLAYER_GAME.comeback_bonus, scale = 0.8*scale, colour = G.C.BLACK, shadow = true, juice = true}}) + table.insert(left_text, {n=G.UIT.T, config={text = G.MULTIPLAYER_GAME.comeback_bonus, scale = 0.8*scale, colour = G.C.PURPLE, shadow = true, juice = true}}) table.insert(left_text, {n=G.UIT.O, config={object = DynaText({string = {" Total Lives Lost ($4 each)"}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4*scale, silent = true})}}) end local full_row = { @@ -1549,8 +1550,10 @@ function add_round_eval_row(config) }, } - G.GAME.blind:juice_up() - G.round_eval:add_child(full_row, G.round_eval:get_UIE_by_ID("base_round_eval")) + if config.name == 'blind1' then + G.GAME.blind:juice_up() + end + G.round_eval:add_child(full_row, G.round_eval:get_UIE_by_ID(config.bonus and 'bonus_round_eval' or 'base_round_eval')) play_sound("negative", (1.5 * config.pitch) or 1, 0.2) play_sound("whoosh2", 0.9, 0.7) if config.card then From a4d60747f229acae1701e7f7942cdfa808a2bafb Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 06:55:46 +0000 Subject: [PATCH 0077/1128] Added comeback mechanic as a lobby option --- Multiplayer/Lobby.lua | 3 ++- Multiplayer/Networking/Action_Handlers.lua | 2 +- Multiplayer/UI/Game_UI.lua | 2 +- Multiplayer/UI/Lobby_UI.lua | 18 ++++++++++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index addedf1a..f4637518 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -12,7 +12,8 @@ G.LOBBY = { code = nil, type = "", config = { - no_gold_on_round_loss = true, + gold_on_life_loss = true, + no_gold_on_round_loss = false, death_on_round_loss = false, different_seeds = false }, diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index 835d827d..b2fa13ee 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -119,7 +119,7 @@ end ---@param lives number local function action_player_info(lives) if (G.MULTIPLAYER_GAME.lives ~= lives) then - if G.MULTIPLAYER_GAME.lives ~= 0 then + if G.MULTIPLAYER_GAME.lives ~= 0 and G.LOBBY.config.gold_on_life_loss then G.MULTIPLAYER_GAME.comeback_bonus_given = false G.MULTIPLAYER_GAME.comeback_bonus = G.MULTIPLAYER_GAME.comeback_bonus + 1 end diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index d8cf4666..a5784a34 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -1477,7 +1477,7 @@ end local add_round_eval_row_ref = add_round_eval_row function add_round_eval_row(config) - if G.LOBBY.code and ((config.name == "blind1" and (is_pvp_boss() or (G.LOBBY.config.death_on_round_loss and G.GAME.blind.chips == -1))) or config.name == "comeback") then + if G.LOBBY.code and ((config.name == "blind1" and (is_pvp_boss() or G.GAME.blind.chips == -1 or (G.LOBBY.config.death_on_round_loss and G.GAME.blind.chips == -1))) or config.name == "comeback") then local config = config or {} local width = G.round_eval.T.w - 0.51 local num_dollars = config.dollars or 1 diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua index 17fb14e3..4e8372a0 100644 --- a/Multiplayer/UI/Lobby_UI.lua +++ b/Multiplayer/UI/Lobby_UI.lua @@ -306,6 +306,24 @@ function G.UIDEF.create_UIBox_lobby_options() } } } or nil, + { + n = G.UIT.R, + config = { + padding = 0, + align = "cr", + }, + nodes = { + Disableable_Toggle({ + id = "gold_on_life_loss_toggle", + enabled_ref_table = G.LOBBY, + enabled_ref_value = 'is_host', + label = "Give comeback gold on life loss", + ref_table = G.LOBBY.config, + ref_value = "gold_on_life_loss", + callback = toggle_lobby_options + }), + } + }, { n = G.UIT.R, config = { From 6a5ba84ae3b18132fcfa4352169fb4f68f97aca5 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 19:41:08 +0000 Subject: [PATCH 0078/1128] Fixed gamemode descriptions --- Multiplayer/UI/Main_Menu.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Multiplayer/UI/Main_Menu.lua b/Multiplayer/UI/Main_Menu.lua index 90004fe5..fbcd076d 100644 --- a/Multiplayer/UI/Main_Menu.lua +++ b/Multiplayer/UI/Main_Menu.lua @@ -89,7 +89,7 @@ function G.UIDEF.create_UIBox_create_lobby_button() n = G.UIT.T, config = { text = Utils.wrapText( - "Both players start with 4 lives, every boss round is a competition between players where the player with the lower score loses a life.", + "Every boss round is a competition between players where the player with the lower score loses a life.", 50 ), shadow = true, @@ -138,7 +138,7 @@ function G.UIDEF.create_UIBox_create_lobby_button() n = G.UIT.T, config = { text = Utils.wrapText( - "Both players play a set amount of antes simultaneously, then they play an ante where every round the player with the higher scorer wins, player with the most round wins in the final ante is the victor.", + "Both players play 3 normal antes, then they play an ante where every round the player with the higher scorer wins.", 50 ), shadow = true, @@ -234,7 +234,7 @@ function G.UIDEF.create_UIBox_create_lobby_button() n = G.UIT.T, config = { text = Utils.wrapText( - "Draft, except there are up to 8 players and every player only has 1 life.", + "Attrition, except there are up to 8 players and every player only has 1 life.", 50 ), shadow = true, From 44a1b1c57c75593caa9377256aa90e46a6c0b167 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 20:58:00 +0000 Subject: [PATCH 0079/1128] Added starting lives lobby option --- .../Components/Disableable_Option_Cycle.lua | 176 ++++++++++++ Multiplayer/Lobby.lua | 7 +- Multiplayer/Networking/Action_Handlers.lua | 3 + Multiplayer/UI/Lobby_UI.lua | 255 ++++++++++-------- Multiplayer/UI/Main_Menu.lua | 5 +- 5 files changed, 334 insertions(+), 112 deletions(-) create mode 100644 Multiplayer/Components/Disableable_Option_Cycle.lua diff --git a/Multiplayer/Components/Disableable_Option_Cycle.lua b/Multiplayer/Components/Disableable_Option_Cycle.lua new file mode 100644 index 00000000..a612cbbf --- /dev/null +++ b/Multiplayer/Components/Disableable_Option_Cycle.lua @@ -0,0 +1,176 @@ +--- STEAMODDED HEADER +--- STEAMODDED SECONDARY FILE + +---------------------------------------------- +------------MOD DISABLEABLE TOGGLE------------ + +function Disableable_Option_Cycle(args) + local enabled_table = args.enabled_ref_table or {} + local enabled = enabled_table[args.enabled_ref_value] + + if not enabled then + args.options = {args.options[args.current_option]} + args.current_option = 1 + end + + local option_component = create_option_cycle(args) + return option_component +end + +return Disableable_Option_Cycle + +--[[ create_option_cycle returns this + { + n = G.UIT.C, + config = { + align = "cm", + padding = 0.1, + r = 0.1, + colour = G.C.CLEAR, + id = args.id and (not args.label and args.id or nil) or nil, + focus_args = args.focus_args + }, + nodes={ + { + n = G.UIT.C, + config = { + align = "cm", + r = 0.1, + minw = 0.6*args.scale, + hover = not disabled, + colour = not disabled and args.colour or G.C.BLACK, + shadow = not disabled, + button = not disabled and 'option_cycle' or nil, + ref_table = args, + ref_value = 'l', + focus_args = {type = 'none'} + }, + nodes = { + { + n = G.UIT.T, + config = { + ref_table = args, + ref_value = 'l', + scale = args.text_scale, + colour = not disabled and G.C.UI.TEXT_LIGHT or G.C.UI.TEXT_INACTIVE + } + } + } + }, + args.mid and { + n = G.UIT.C, + config = { + id = 'cycle_main' + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm", + minh = 0.05 + }, + nodes = { + args.mid + } + }, + not disabled and choice_pips or nil + } + } + or { + n=G.UIT.C, + config = { + id = 'cycle_main', + align = "cm", + minw = args.w, + minh = args.h, + r = 0.1, + padding = 0.05, + colour = args.colour, + emboss = 0.1, + hover = true, + can_collide = true, + on_demand_tooltip = args.on_demand_tooltip + }, + nodes={ + { + n=G.UIT.R, + config={ + align = "cm" + }, + nodes={ + { + n=G.UIT.R, + config={ + align = "cm" + }, + nodes={ + { + n=G.UIT.O, + config={ + object = DynaText({ + string = {{ + ref_table = args, + ref_value = "current_option_val" + }}, + colours = {G.C.UI.TEXT_LIGHT}, + pop_in = 0, + pop_in_rate = 8, + reset_pop_in = true, + shadow = true, + float = true, + silent = true, + bump = true, + scale = args.text_scale, + non_recalc = true + }) + } + }, + } + }, + { + n=G.UIT.R, + config={ + align = "cm", + minh = 0.05 + }, + nodes={} + }, + not disabled and choice_pips or nil + } + } + } + }, + { + n=G.UIT.C, + config={ + align = "cm", + r = 0.1, + minw = 0.6*args.scale, + hover = not disabled, + colour = not disabled and args.colour or G.C.BLACK, + shadow = not disabled, + button = not disabled and 'option_cycle' or nil, + ref_table = args, + ref_value = 'r', + focus_args = { + type = 'none' + } + }, + nodes={ + { + n=G.UIT.T, + config={ + ref_table = args, + ref_value = 'r', + scale = args.text_scale, + colour = not disabled and G.C.UI.TEXT_LIGHT or G.C.UI.TEXT_INACTIVE + } + } + } + }, + } + } +]] + +---------------------------------------------- +------------MOD DISABLEABLE TOGGLE END-------- \ No newline at end of file diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index f4637518..74b77abf 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -15,7 +15,8 @@ G.LOBBY = { gold_on_life_loss = true, no_gold_on_round_loss = false, death_on_round_loss = false, - different_seeds = false + different_seeds = false, + starting_lives = 4, }, username = "Guest", host = {}, @@ -57,6 +58,10 @@ function reset_game_states() } end +function reset_gamemode_modifiers() + G.LOBBY.config.starting_lives = G.LOBBY.type == "draft" and 2 or 4 +end + PREV_ACHIEVEMENT_VALUE = true function G.MULTIPLAYER.update_connection_status() -- Save the previous value of the achievement flag diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index b2fa13ee..20d22945 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -157,6 +157,9 @@ local function action_lobby_options(options) elseif v == "false" then parsed_v = false end + if k == "starting_lives" then + parsed_v = tonumber(v) + end G.LOBBY.config[k] = parsed_v if G.OVERLAY_MENU then local config_uie = G.OVERLAY_MENU:get_UIE_by_ID(k .. '_toggle') diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua index 4e8372a0..4a4556f2 100644 --- a/Multiplayer/UI/Lobby_UI.lua +++ b/Multiplayer/UI/Lobby_UI.lua @@ -6,6 +6,7 @@ local Disableable_Button = require("Components.Disableable_Button") local Disableable_Toggle = require("Components.Disableable_Toggle") +local Disableable_Option_Cycle = require("Components.Disableable_Option_Cycle") local function toggle_lobby_options(value) G.MULTIPLAYER.lobby_options() @@ -272,136 +273,172 @@ function G.UIDEF.create_UIBox_lobby_options() { n = G.UIT.R, config = { - emboss = 0.05, - minh = 6, - r = 0.1, - minw = 10, - padding = 0.2, - colour = G.C.BLACK, + padding = 0, align = "cm", }, nodes = { - { + not G.LOBBY.is_host and { n = G.UIT.R, config = { padding = 0.3, align = "cm", }, nodes = { - not G.LOBBY.is_host and { - n = G.UIT.R, - config = { - padding = 0, - align = "cm", - }, - nodes = { - { - n = G.UIT.T, - config = { - scale = 0.6, - shadow = true, - text = 'Only the Lobby Host can change these options', - colour = G.C.UI.TEXT_LIGHT, - } - } - } - } or nil, - { - n = G.UIT.R, - config = { - padding = 0, - align = "cr", - }, - nodes = { - Disableable_Toggle({ - id = "gold_on_life_loss_toggle", - enabled_ref_table = G.LOBBY, - enabled_ref_value = 'is_host', - label = "Give comeback gold on life loss", - ref_table = G.LOBBY.config, - ref_value = "gold_on_life_loss", - callback = toggle_lobby_options - }), - } - }, { - n = G.UIT.R, + n = G.UIT.T, config = { - padding = 0, - align = "cr", - }, - nodes = { - Disableable_Toggle({ - id = "no_gold_on_round_loss_toggle", - enabled_ref_table = G.LOBBY, - enabled_ref_value = 'is_host', - label = "Don't get blind gold on round loss", - ref_table = G.LOBBY.config, - ref_value = "no_gold_on_round_loss", - callback = toggle_lobby_options - }), + scale = 0.6, + shadow = true, + text = 'Only the Lobby Host can change these options', + colour = G.C.UI.TEXT_LIGHT, } - }, + } + } + } or nil, + create_tabs({ + snap_to_nav = true, + colour = G.C.BOOSTER, + tabs = { { - n = G.UIT.R, - config = { - padding = 0, - align = "cr", - }, - nodes = { - Disableable_Toggle({ - id = "death_on_round_loss_toggle", - enabled_ref_table = G.LOBBY, - enabled_ref_value = 'is_host', - label = "Lose a life on non-PvP round loss", - ref_table = G.LOBBY.config, - ref_value = "death_on_round_loss", - callback = toggle_lobby_options - }), - } + label = "Lobby Options", + chosen = true, + tab_definition_function = function() + return { + n = G.UIT.ROOT, + config = { + emboss = 0.05, + minh = 6, + r = 0.1, + minw = 10, + align = "tm", + padding = 0.2, + colour = G.C.BLACK, + }, + nodes = { + { + n = G.UIT.R, + config = { + padding = 0, + align = "cr", + }, + nodes = { + Disableable_Toggle({ + id = "gold_on_life_loss_toggle", + enabled_ref_table = G.LOBBY, + enabled_ref_value = 'is_host', + label = "Give comeback gold on life loss", + ref_table = G.LOBBY.config, + ref_value = "gold_on_life_loss", + callback = toggle_lobby_options + }), + } + }, + { + n = G.UIT.R, + config = { + padding = 0, + align = "cr", + }, + nodes = { + Disableable_Toggle({ + id = "no_gold_on_round_loss_toggle", + enabled_ref_table = G.LOBBY, + enabled_ref_value = 'is_host', + label = "Don't get blind gold on round loss", + ref_table = G.LOBBY.config, + ref_value = "no_gold_on_round_loss", + callback = toggle_lobby_options + }), + } + }, + { + n = G.UIT.R, + config = { + padding = 0, + align = "cr", + }, + nodes = { + Disableable_Toggle({ + id = "death_on_round_loss_toggle", + enabled_ref_table = G.LOBBY, + enabled_ref_value = 'is_host', + label = "Lose a life on non-PvP round loss", + ref_table = G.LOBBY.config, + ref_value = "death_on_round_loss", + callback = toggle_lobby_options + }), + } + }, + { + n = G.UIT.R, + config = { + padding = 0, + align = "cr", + }, + nodes = { + Disableable_Toggle({ + id = "different_seeds_toggle", + enabled_ref_table = G.LOBBY, + enabled_ref_value = 'is_host', + label = "Players have different seeds", + ref_table = G.LOBBY.config, + ref_value = "different_seeds", + callback = toggle_lobby_options + }), + } + }, + }, + } + end }, { - n = G.UIT.R, - config = { - padding = 0, - align = "cr", - }, - nodes = { - Disableable_Toggle({ - id = "different_seeds_toggle", - enabled_ref_table = G.LOBBY, - enabled_ref_value = 'is_host', - label = "Players have different seeds", - ref_table = G.LOBBY.config, - ref_value = "different_seeds", - callback = toggle_lobby_options - }), - } - }, - Disableable_Button({ - enabled_ref_table = G.LOBBY, - enabled_ref_value = 'is_host', - button = "reset_lobby_options", - label = { localize("k_reset") }, - colour = G.C.RED, - minw = 5, - }), - }, - }, + label = "Gamemode Modifiers", + tab_definition_function = function() + return { + n = G.UIT.ROOT, + config = { + emboss = 0.05, + minh = 6, + r = 0.1, + minw = 10, + align = "tm", + padding = 0.2, + colour = G.C.BLACK, + }, + nodes = { + { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", + }, + nodes = { + Disableable_Option_Cycle({ + id = "starting_lives_option", + enabled_ref_table = G.LOBBY, + enabled_ref_value = 'is_host', + label = "Lives", + options = {1, 2, 4, 6, 8}, + current_option = G.LOBBY.config.starting_lives < 4 and G.LOBBY.config.starting_lives or G.LOBBY.config.starting_lives == 4 and 3 or G.LOBBY.config.starting_lives == 6 and 4 or 5, + opt_callback = 'change_starting_lives' + }), + } + }, + }, + } + end + } + } + }) }, }, }, }) end -function G.FUNCS.reset_lobby_options(e) - G.LOBBY.config = { - no_gold_on_round_loss = true, - death_on_round_loss = false, - different_seeds = false - } - G.FUNCS.exit_overlay_menu() - G.MULTIPLAYER.lobby_options() +G.FUNCS.change_starting_lives = function(args) + G.LOBBY.config.starting_lives = args.to_val + toggle_lobby_options() end function G.FUNCS.get_lobby_main_menu_UI(e) diff --git a/Multiplayer/UI/Main_Menu.lua b/Multiplayer/UI/Main_Menu.lua index fbcd076d..51289e5b 100644 --- a/Multiplayer/UI/Main_Menu.lua +++ b/Multiplayer/UI/Main_Menu.lua @@ -374,8 +374,9 @@ end function G.FUNCS.start_lobby(e) G.SETTINGS.paused = false - local gamemode = e.config.id == "start_attrition" and "attrition" or "draft" - G.MULTIPLAYER.create_lobby(gamemode) + G.LOBBY.type = e.config.id == "start_attrition" and "attrition" or "draft" + reset_gamemode_modifiers() + G.MULTIPLAYER.create_lobby(G.LOBBY.type) end -- Modify play button to take you to mode select first From 30026566d1f69051836693cc1a24bc6a7b4882e3 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 21:08:00 +0000 Subject: [PATCH 0080/1128] Implemented starting_lives on the server --- Server/src/actionHandlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 10c4c273..97f6b145 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -62,7 +62,7 @@ const startGameAction = (client: Client) => { return } - let lives = GameModes[lobby.gameMode].startingLives + let lives = lobby.options.starting_lives? parseInt(lobby.options.starting_lives) : GameModes[lobby.gameMode].startingLives lobby.broadcastAction({ action: 'startGame', From d0244d0a7a36aee0a2828ae513f40432a61dd067 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 21:16:10 +0000 Subject: [PATCH 0081/1128] Added draft starting antes --- Multiplayer/Lobby.lua | 2 ++ Multiplayer/UI/Lobby_UI.lua | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/Multiplayer/Lobby.lua b/Multiplayer/Lobby.lua index 74b77abf..a1d48e33 100644 --- a/Multiplayer/Lobby.lua +++ b/Multiplayer/Lobby.lua @@ -17,6 +17,7 @@ G.LOBBY = { death_on_round_loss = false, different_seeds = false, starting_lives = 4, + draft_starting_antes = 3 }, username = "Guest", host = {}, @@ -60,6 +61,7 @@ end function reset_gamemode_modifiers() G.LOBBY.config.starting_lives = G.LOBBY.type == "draft" and 2 or 4 + G.LOBBY.config.draft_starting_antes = 3 end PREV_ACHIEVEMENT_VALUE = true diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua index 4a4556f2..8efa5078 100644 --- a/Multiplayer/UI/Lobby_UI.lua +++ b/Multiplayer/UI/Lobby_UI.lua @@ -422,6 +422,15 @@ function G.UIDEF.create_UIBox_lobby_options() current_option = G.LOBBY.config.starting_lives < 4 and G.LOBBY.config.starting_lives or G.LOBBY.config.starting_lives == 4 and 3 or G.LOBBY.config.starting_lives == 6 and 4 or 5, opt_callback = 'change_starting_lives' }), + G.LOBBY.type == 'draft' and Disableable_Option_Cycle({ + id = "draft_starting_antes_option", + enabled_ref_table = G.LOBBY, + enabled_ref_value = 'is_host', + label = "Starting Antes", + options = {2, 3, 4, 5, 6, 7}, + current_option = G.LOBBY.config.draft_starting_antes - 1, + opt_callback = 'change_draft_starting_antes' + }) or nil, } }, }, @@ -441,6 +450,11 @@ G.FUNCS.change_starting_lives = function(args) toggle_lobby_options() end +G.FUNCS.change_draft_starting_antes = function(args) + G.LOBBY.config.draft_starting_antes = args.to_val + toggle_lobby_options() +end + function G.FUNCS.get_lobby_main_menu_UI(e) return UIBox({ definition = G.UIDEF.create_UIBox_lobby_menu(), From d0a3f98792a189eb5352e04f9d62275ceb4c06ca Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 21:31:12 +0000 Subject: [PATCH 0082/1128] Implemented draft starting antes modifier --- Multiplayer/Networking/Action_Handlers.lua | 8 +++++--- Multiplayer/UI/Main_Menu.lua | 2 -- Server/src/GameMode.ts | 9 +++++---- Server/src/Lobby.ts | 6 +++--- Server/src/actions.ts | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index 20d22945..baa6ff6e 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -25,9 +25,11 @@ local function action_connected() Client.send(string.format("action:username,username:%s", G.LOBBY.username)) end -local function action_joinedLobby(code) +local function action_joinedLobby(code, type) sendDebugMessage(string.format("Joining lobby %s", code)) G.LOBBY.code = code + G.LOBBY.type = type + reset_gamemode_modifiers() G.MULTIPLAYER.lobby_info() G.MULTIPLAYER.update_connection_status() end @@ -157,7 +159,7 @@ local function action_lobby_options(options) elseif v == "false" then parsed_v = false end - if k == "starting_lives" then + if k == "starting_lives" or k == "draft_starting_antes" then parsed_v = tonumber(v) end G.LOBBY.config[k] = parsed_v @@ -263,7 +265,7 @@ function Game:update(dt) elseif parsedAction.action == "disconnected" then action_disconnected() elseif parsedAction.action == "joinedLobby" then - action_joinedLobby(parsedAction.code) + action_joinedLobby(parsedAction.code, parsedAction.type) elseif parsedAction.action == "lobbyInfo" then action_lobbyInfo(parsedAction.host, parsedAction.guest, parsedAction.isHost) elseif parsedAction.action == "startGame" then diff --git a/Multiplayer/UI/Main_Menu.lua b/Multiplayer/UI/Main_Menu.lua index 51289e5b..776239f2 100644 --- a/Multiplayer/UI/Main_Menu.lua +++ b/Multiplayer/UI/Main_Menu.lua @@ -374,8 +374,6 @@ end function G.FUNCS.start_lobby(e) G.SETTINGS.paused = false - G.LOBBY.type = e.config.id == "start_attrition" and "attrition" or "draft" - reset_gamemode_modifiers() G.MULTIPLAYER.create_lobby(G.LOBBY.type) end diff --git a/Server/src/GameMode.ts b/Server/src/GameMode.ts index 8f8ff414..5ba14be6 100644 --- a/Server/src/GameMode.ts +++ b/Server/src/GameMode.ts @@ -3,7 +3,7 @@ import { GameMode } from "./actions.js" type GameModeData = { startingLives: number - getBlindFromAnte: (ante: number) => { + getBlindFromAnte: (ante: number, options: any) => { small?: string big?: string boss?: string @@ -17,14 +17,15 @@ const GameModes: { } = { 'attrition': { startingLives: 4, - getBlindFromAnte: (ante) => { + getBlindFromAnte: (ante, options) => { return { boss: 'bl_pvp' } } }, 'draft': { startingLives: 2, - getBlindFromAnte: (ante) => { - if (ante < 4) return { } + getBlindFromAnte: (ante, options) => { + const starting_antes = options?.draft_starting_antes ? parseInt(options.draft_starting_antes) : 3 + if (ante <= starting_antes) return { } else return { small: 'bl_pvp', big: 'bl_pvp', boss: 'bl_pvp' } } } diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index 991630ed..9f99a155 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -38,7 +38,7 @@ class Lobby { this.options = {} host.setLobby(this) - host.sendAction({ action: 'joinedLobby', code: this.code }) + host.sendAction({ action: 'joinedLobby', code: this.code, type: this.gameMode }) } static get = (code: string) => { @@ -75,7 +75,7 @@ class Lobby { } this.guest = client client.setLobby(this) - client.sendAction({ action: 'joinedLobby', code: this.code }) + client.sendAction({ action: 'joinedLobby', code: this.code, type: this.gameMode }) client.sendAction({ action: 'lobbyOptions', ...this.options }) this.broadcastLobbyInfo() } @@ -119,7 +119,7 @@ class Lobby { return client.sendAction({ action: 'error', message: 'Client not in Lobby' }) } - client.sendAction({ action: 'gameInfo', ...GameModes[this.gameMode].getBlindFromAnte(client.ante) }) + client.sendAction({ action: 'gameInfo', ...GameModes[this.gameMode].getBlindFromAnte(client.ante, this.options) }) } setOptions = (options: { [key: string]: string }) => { diff --git a/Server/src/actions.ts b/Server/src/actions.ts index 0ad83b56..55d2bd0c 100644 --- a/Server/src/actions.ts +++ b/Server/src/actions.ts @@ -1,7 +1,7 @@ // Server to Client export type ActionConnected = { action: 'connected' } export type ActionError = { action: 'error'; message: string } -export type ActionJoinedLobby = { action: 'joinedLobby'; code: string } +export type ActionJoinedLobby = { action: 'joinedLobby'; code: string; type: GameMode } export type ActionLobbyInfo = { action: 'lobbyInfo' host: string From 5ee2151b42dbb086af40b9c9184746ebb23e6e89 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 21:37:05 +0000 Subject: [PATCH 0083/1128] Fixed lobby not being set --- Multiplayer/UI/Main_Menu.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/UI/Main_Menu.lua b/Multiplayer/UI/Main_Menu.lua index 776239f2..5fe97d83 100644 --- a/Multiplayer/UI/Main_Menu.lua +++ b/Multiplayer/UI/Main_Menu.lua @@ -374,7 +374,7 @@ end function G.FUNCS.start_lobby(e) G.SETTINGS.paused = false - G.MULTIPLAYER.create_lobby(G.LOBBY.type) + G.MULTIPLAYER.create_lobby(e.config.id == "start_attrition" and "attrition" or "draft") end -- Modify play button to take you to mode select first From 3aef7c7a5e1be19086d6524ca5a78f46c28ae26a Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 21:46:04 +0000 Subject: [PATCH 0084/1128] Added description for Vanilla+ gamemode --- Multiplayer/UI/Main_Menu.lua | 55 +++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/Multiplayer/UI/Main_Menu.lua b/Multiplayer/UI/Main_Menu.lua index 5fe97d83..4ccf63e6 100644 --- a/Multiplayer/UI/Main_Menu.lua +++ b/Multiplayer/UI/Main_Menu.lua @@ -61,7 +61,7 @@ function G.UIDEF.create_UIBox_create_lobby_button() colour = G.C.BOOSTER, tabs = { { - label = "Attrition (1v1)", + label = "Attrition", chosen = true, tab_definition_function = function() return { @@ -111,7 +111,7 @@ function G.UIDEF.create_UIBox_create_lobby_button() end, }, { - label = "Draft (1v1)", + label = "Draft", tab_definition_function = function() return { n = G.UIT.ROOT, @@ -160,7 +160,54 @@ function G.UIDEF.create_UIBox_create_lobby_button() end, }, { - label = "Heads Up (1v1)", + label = "Vanilla+", + tab_definition_function = function() + return { + n = G.UIT.ROOT, + config = { + emboss = 0.05, + minh = 6, + r = 0.1, + minw = 10, + align = "tm", + padding = 0.2, + colour = G.C.BLACK, + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "tm", + padding = 0.05, + minw = 4, + minh = 1, + }, + nodes = { + { + n = G.UIT.T, + config = { + text = Utils.wrapText( + "The first person to fail a round loses, no PvP blinds.", + 50 + ), + shadow = true, + scale = var_495_0 * 0.6, + colour = G.C.UI.TEXT_LIGHT, + }, + }, + }, + }, + UIBox_button({ + label = { "Coming Soon!" }, + colour = G.C.RED, + minw = 5, + }), + }, + } + end, + }, + { + label = "Heads Up", tab_definition_function = function() return { n = G.UIT.ROOT, @@ -207,7 +254,7 @@ function G.UIDEF.create_UIBox_create_lobby_button() end, }, { - label = "Battle Royale (8p)", + label = "Battle Royale", tab_definition_function = function() return { n = G.UIT.ROOT, From 9a332c3c6b415a652bbc92382e334cdb8755981a Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 21:49:12 +0000 Subject: [PATCH 0085/1128] Changed mod description --- Multiplayer/Core.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplayer/Core.lua b/Multiplayer/Core.lua index 41541421..eb0007f3 100644 --- a/Multiplayer/Core.lua +++ b/Multiplayer/Core.lua @@ -2,7 +2,7 @@ --- MOD_NAME: Multiplayer --- MOD_ID: VirtualizedMultiplayer --- MOD_AUTHOR: [virtualized, TGMM] ---- MOD_DESCRIPTION: Allows players to compete with their friends! Contact @virtualized on discord for mod assistance. +--- MOD_DESCRIPTION: Allows players to compete with their friends! ---------------------------------------------- ------------MOD CORE-------------------------- From b7ed2ca730973a920f831480c375f54fcfb469c5 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 22:04:26 +0000 Subject: [PATCH 0086/1128] Added discord invite link to mod settings --- Multiplayer/UI/Mod_Description.lua | 107 +++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 21 deletions(-) diff --git a/Multiplayer/UI/Mod_Description.lua b/Multiplayer/UI/Mod_Description.lua index e893ac1b..78d41567 100644 --- a/Multiplayer/UI/Mod_Description.lua +++ b/Multiplayer/UI/Mod_Description.lua @@ -15,42 +15,107 @@ function Description.load_description_gui() config = { padding = 0.5, align = "cm", - id = "username_input_box", }, nodes = { { - n = G.UIT.T, + n = G.UIT.R, config = { - scale = 0.6, - text = "Username:", - colour = G.C.UI.TEXT_LIGHT, + padding = 0.5, + align = "cm", + id = "username_input_box", }, + nodes = { + { + n = G.UIT.T, + config = { + scale = 0.6, + text = "Username:", + colour = G.C.UI.TEXT_LIGHT, + }, + }, + create_text_input({ + w = 4, + max_length = 25, + prompt_text = "Enter Username", + ref_table = G.LOBBY, + ref_value = "username", + extended_corpus = true, + keyboard_offset = 1, + callback = function(val) + Utils.save_username(G.LOBBY.username) + end, + }), + { + n = G.UIT.T, + config = { + scale = 0.3, + text = "Press enter to save", + colour = G.C.UI.TEXT_LIGHT, + }, + }, + } }, - create_text_input({ - w = 4, - max_length = 25, - prompt_text = "Enter Username", - ref_table = G.LOBBY, - ref_value = "username", - extended_corpus = true, - keyboard_offset = 1, - callback = function(val) - Utils.save_username(G.LOBBY.username) - end, - }), { - n = G.UIT.T, + n = G.UIT.R, config = { - scale = 0.3, - text = "Press enter to save", - colour = G.C.UI.TEXT_LIGHT, + padding = 0, + align = "cm" }, + nodes = { + { + n = G.UIT.T, + config = { + text = "Join the ", + shadow = true, + scale = 0.6, + colour = G.C.UI.TEXT_LIGHT + } + } + }, + }, + { + n = G.UIT.R, + config = { + padding = 0.2, + align = "cm", + }, + nodes = { + UIBox_button({ + minw = 6, + button = "multiplayer_discord", + label = { + "Balatro Multiplayer Discord Server" + } + }) + } }, + { + n = G.UIT.R, + config = { + padding = 0.2, + align = "cm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = "You can report any bugs and find people to play with there!", + shadow = true, + scale = 0.375, + colour = G.C.UI.TEXT_LIGHT + } + } + } + } }, }, }) end +function G.FUNCS.multiplayer_discord(e) + love.system.openURL("https://discord.gg/gEemz4ptuF") +end + return Description ---------------------------------------------- From b40600fa2521314c307f08533e35e83a9185074e Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Fri, 12 Apr 2024 22:11:27 +0000 Subject: [PATCH 0087/1128] Added message letting users know they can change their username --- Multiplayer/UI/Lobby_UI.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Multiplayer/UI/Lobby_UI.lua b/Multiplayer/UI/Lobby_UI.lua index 4e8372a0..4c3a7590 100644 --- a/Multiplayer/UI/Lobby_UI.lua +++ b/Multiplayer/UI/Lobby_UI.lua @@ -115,6 +115,24 @@ function G.UIDEF.create_UIBox_lobby_menu() align = "bm", }, nodes = { + G.LOBBY.username == "Guest" and { + n = G.UIT.R, + config = { + padding = 0.1, + align = "cm", + }, + nodes = { + { + n = G.UIT.T, + config = { + scale = 0.3, + shadow = true, + text = 'Set your username in the main menu! (Mods > Multiplayer)', + colour = G.C.UI.TEXT_LIGHT, + }, + }, + } + } or nil, { n = G.UIT.R, config = { From 19114bc59f9e1d8456121ea3d3ae9354e8909075 Mon Sep 17 00:00:00 2001 From: TGMM Date: Fri, 12 Apr 2024 15:39:24 -0700 Subject: [PATCH 0088/1128] Implemented version action --- Multiplayer/Networking/Action_Handlers.lua | 21 ++- Server/package.json | 3 +- Server/src/Client.ts | 7 +- Server/src/GameMode.ts | 54 +++--- Server/src/Lobby.ts | 160 ++++++++-------- Server/src/actionHandlers.ts | 201 ++++++++++++--------- Server/src/actions.ts | 30 +-- Server/src/main.ts | 21 ++- 8 files changed, 282 insertions(+), 215 deletions(-) diff --git a/Multiplayer/Networking/Action_Handlers.lua b/Multiplayer/Networking/Action_Handlers.lua index baa6ff6e..48076b00 100644 --- a/Multiplayer/Networking/Action_Handlers.lua +++ b/Multiplayer/Networking/Action_Handlers.lua @@ -120,7 +120,7 @@ end ---@param lives number local function action_player_info(lives) - if (G.MULTIPLAYER_GAME.lives ~= lives) then + if G.MULTIPLAYER_GAME.lives ~= lives then if G.MULTIPLAYER_GAME.lives ~= 0 and G.LOBBY.config.gold_on_life_loss then G.MULTIPLAYER_GAME.comeback_bonus_given = false G.MULTIPLAYER_GAME.comeback_bonus = G.MULTIPLAYER_GAME.comeback_bonus + 1 @@ -140,12 +140,11 @@ local function action_lose_game() G.STATE = G.STATES.GAME_OVER end - local function action_game_info(small, big, boss) G.GAME.round_resets.blind_choices = { - Small = small or 'bl_small', - Big = big or 'bl_big', - Boss = boss or get_new_boss(true) + Small = small or "bl_small", + Big = big or "bl_big", + Boss = boss or get_new_boss(true), } G.MULTIPLAYER_GAME.loaded_ante = G.GAME.round_resets.ante G.MULTIPLAYER.loading_blinds = false @@ -164,7 +163,7 @@ local function action_lobby_options(options) end G.LOBBY.config[k] = parsed_v if G.OVERLAY_MENU then - local config_uie = G.OVERLAY_MENU:get_UIE_by_ID(k .. '_toggle') + local config_uie = G.OVERLAY_MENU:get_UIE_by_ID(k .. "_toggle") if config_uie then G.FUNCS.toggle(config_uie) end @@ -172,6 +171,10 @@ local function action_lobby_options(options) end end +local function action_version() + G.MULTIPLAYER.version() +end + -- #region Client to Server function G.MULTIPLAYER.create_lobby(gamemode) Client.send(string.format("action:createLobby,gameMode:%s", gamemode)) @@ -216,6 +219,10 @@ function G.MULTIPLAYER.fail_round() Client.send("action:failRound") end +function G.MULTIPLAYER.version() + Client.send(string.format("action:version,version:%s", MULTIPLAYER_VERSION)) +end + ---@param score number ---@param hands_left number function G.MULTIPLAYER.play_hand(score, hands_left) @@ -262,6 +269,8 @@ function Game:update(dt) if parsedAction.action == "connected" then action_connected() + elseif parsedAction.action == "version" then + action_version() elseif parsedAction.action == "disconnected" then action_disconnected() elseif parsedAction.action == "joinedLobby" then diff --git a/Server/package.json b/Server/package.json index 5f456724..66dc5c29 100644 --- a/Server/package.json +++ b/Server/package.json @@ -1,4 +1,5 @@ { + "version": "0.1.2-MULTIPLAYER", "scripts": { "build": "tsc", "dev": "tsx watch src/main.ts", @@ -16,4 +17,4 @@ "typescript": "^5.4.2" }, "type": "module" -} +} \ No newline at end of file diff --git a/Server/src/Client.ts b/Server/src/Client.ts index 47a2bab4..bdc6322f 100644 --- a/Server/src/Client.ts +++ b/Server/src/Client.ts @@ -1,9 +1,10 @@ +import type net from 'node:net' import { v4 as uuidv4 } from 'uuid' import type Lobby from './Lobby.js' -import type net from 'node:net' import type { ActionServerToClient } from './actions.js' type SendFn = (action: ActionServerToClient) => void +type CloseConnFn = () => void /* biome-ignore lint/complexity/noBannedTypes: This is how the net module does it */ @@ -15,6 +16,7 @@ class Client { // Could be useful later on to detect reconnects address: Address sendAction: SendFn + closeConnection: CloseConnFn // Game info username = 'Guest' @@ -26,10 +28,11 @@ class Client { handsLeft = 4 ante = 1 - constructor(address: Address, send: SendFn) { + constructor(address: Address, send: SendFn, closeConnection: CloseConnFn) { this.id = uuidv4() this.address = address this.sendAction = send + this.closeConnection = closeConnection } setUsername = (username: string) => { diff --git a/Server/src/GameMode.ts b/Server/src/GameMode.ts index 5ba14be6..b6401431 100644 --- a/Server/src/GameMode.ts +++ b/Server/src/GameMode.ts @@ -1,34 +1,36 @@ -import { GameMode } from "./actions.js" +import type { GameMode } from "./actions.js"; type GameModeData = { - startingLives: number + startingLives: number; - getBlindFromAnte: (ante: number, options: any) => { - small?: string - big?: string - boss?: string - } + getBlindFromAnte: ( + ante: number, + options: unknown, + ) => { + small?: string; + big?: string; + boss?: string; + }; - // TODO: Validate lobby options when they differ per gamemode -} + // TODO: Validate lobby options when they differ per gamemode +}; const GameModes: { - [key in GameMode]: GameModeData + [key in GameMode]: GameModeData; } = { - 'attrition': { - startingLives: 4, - getBlindFromAnte: (ante, options) => { - return { boss: 'bl_pvp' } - } - }, - 'draft': { - startingLives: 2, - getBlindFromAnte: (ante, options) => { - const starting_antes = options?.draft_starting_antes ? parseInt(options.draft_starting_antes) : 3 - if (ante <= starting_antes) return { } - else return { small: 'bl_pvp', big: 'bl_pvp', boss: 'bl_pvp' } - } - } -} + attrition: { + startingLives: 4, + getBlindFromAnte: (ante) => { + return { boss: "bl_pvp" }; + }, + }, + draft: { + startingLives: 2, + getBlindFromAnte: (ante) => { + if (ante < 4) return {}; + return { small: "bl_pvp", big: "bl_pvp", boss: "bl_pvp" }; + }, + }, +}; -export default GameModes \ No newline at end of file +export default GameModes; diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index 9f99a155..b055d643 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -1,137 +1,151 @@ -import type Client from './Client.js' -import GameModes from './GameMode.js' +import type Client from "./Client.js"; +import GameModes from "./GameMode.js"; import type { ActionLobbyInfo, ActionServerToClient, GameMode, -} from './actions.js' -import { serializeAction } from './main.js' +} from "./actions.js"; +import { serializeAction } from "./main.js"; -const Lobbies = new Map() +const Lobbies = new Map(); const generateUniqueLobbyCode = (): string => { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - let result = '' + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + let result = ""; for (let i = 0; i < 5; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)) + result += chars.charAt(Math.floor(Math.random() * chars.length)); } - return Lobbies.get(result) ? generateUniqueLobbyCode() : result -} + return Lobbies.get(result) ? generateUniqueLobbyCode() : result; +}; class Lobby { - code: string - host: Client | null - guest: Client | null - gameMode: GameMode - options: { [key: string]: any } + code: string; + host: Client | null; + guest: Client | null; + gameMode: GameMode; + options: { [key: string]: unknown }; // Attrition is the default game mode - constructor(host: Client, gameMode: GameMode = 'attrition') { + constructor(host: Client, gameMode: GameMode = "attrition") { do { - this.code = generateUniqueLobbyCode() - } while (Lobbies.get(this.code)) - Lobbies.set(this.code, this) - - this.host = host - this.guest = null - this.gameMode = gameMode - this.options = {} - - host.setLobby(this) - host.sendAction({ action: 'joinedLobby', code: this.code, type: this.gameMode }) + this.code = generateUniqueLobbyCode(); + } while (Lobbies.get(this.code)); + Lobbies.set(this.code, this); + + this.host = host; + this.guest = null; + this.gameMode = gameMode; + this.options = {}; + + host.setLobby(this); + host.sendAction({ + action: "joinedLobby", + code: this.code, + type: this.gameMode, + }); } static get = (code: string) => { - return Lobbies.get(code) - } + return Lobbies.get(code); + }; leave = (client: Client) => { if (this.host?.id === client.id) { - this.host = this.guest - this.guest = null + this.host = this.guest; + this.guest = null; } else if (this.guest?.id === client.id) { - this.guest = null + this.guest = null; } - const lobby = client.lobby - client.setLobby(null) + const lobby = client.lobby; + client.setLobby(null); if (this.host === null) { - Lobbies.delete(this.code) + Lobbies.delete(this.code); } else { // TODO: Refactor for more than 2 players // Stop game if someone leaves - lobby?.broadcastAction({ action: 'stopGame' }) - this.broadcastLobbyInfo() + lobby?.broadcastAction({ action: "stopGame" }); + this.broadcastLobbyInfo(); } - } + }; join = (client: Client) => { if (this.guest) { client.sendAction({ - action: 'error', - message: 'Lobby is full or does not exist.', - }) - return + action: "error", + message: "Lobby is full or does not exist.", + }); + return; } - this.guest = client - client.setLobby(this) - client.sendAction({ action: 'joinedLobby', code: this.code, type: this.gameMode }) - client.sendAction({ action: 'lobbyOptions', ...this.options }) - this.broadcastLobbyInfo() - } + this.guest = client; + client.setLobby(this); + client.sendAction({ + action: "joinedLobby", + code: this.code, + type: this.gameMode, + }); + client.sendAction({ action: "lobbyOptions", ...this.options }); + this.broadcastLobbyInfo(); + }; broadcastAction = (action: ActionServerToClient) => { - this.host?.sendAction(action) - this.guest?.sendAction(action) - } + this.host?.sendAction(action); + this.guest?.sendAction(action); + }; broadcastLobbyInfo = () => { if (!this.host) { - return + return; } const action: ActionLobbyInfo = { - action: 'lobbyInfo', + action: "lobbyInfo", host: this.host.username, isHost: false, - } + }; if (this.guest?.username) { - action.guest = this.guest.username - this.guest.sendAction(action) + action.guest = this.guest.username; + this.guest.sendAction(action); } // Should only sent true to the host - action.isHost = true - this.host.sendAction(action) - } + action.isHost = true; + this.host.sendAction(action); + }; setPlayersLives = (lives: number) => { // TODO: Refactor for more than 2 players - if (this.host) this.host.lives = lives - if (this.guest) this.guest.lives = lives + if (this.host) this.host.lives = lives; + if (this.guest) this.guest.lives = lives; - this.broadcastAction({ action: 'playerInfo', lives }) - } + this.broadcastAction({ action: "playerInfo", lives }); + }; sendGameInfo = (client: Client) => { if (this.host !== client && this.guest !== client) { - return client.sendAction({ action: 'error', message: 'Client not in Lobby' }) + return client.sendAction({ + action: "error", + message: "Client not in Lobby", + }); } - client.sendAction({ action: 'gameInfo', ...GameModes[this.gameMode].getBlindFromAnte(client.ante, this.options) }) - } + client.sendAction({ + action: "gameInfo", + ...GameModes[this.gameMode].getBlindFromAnte(client.ante, this.options), + }); + }; setOptions = (options: { [key: string]: string }) => { for (const key of Object.keys(options)) { - if (options[key] == "true" || options[key] == "false") { - this.options[key] = options[key] == "true" - } else{ - this.options[key] = options[key] + if (options[key] === "true" || options[key] === "false") { + this.options[key] = options[key] === "true"; + } else { + this.options[key] = options[key]; } } - this.guest?.sendAction({ action: 'lobbyOptions', ...options }) - } + this.guest?.sendAction({ action: "lobbyOptions", ...options }); + }; } -export default Lobby +export default Lobby; diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 97f6b145..2cdcde0f 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -1,6 +1,6 @@ -import type Client from './Client.js' -import GameModes from './GameMode.js' -import Lobby from './Lobby.js' +import type Client from "./Client.js"; +import GameModes from "./GameMode.js"; +import Lobby from "./Lobby.js"; import type { ActionCreateLobby, ActionHandlerArgs, @@ -9,132 +9,136 @@ import type { ActionPlayHand, ActionSetAnte, ActionUsername, -} from './actions.js' -import { generateSeed } from './utils.js' + ActionVersion, +} from "./actions.js"; +import { generateSeed } from "./utils.js"; +import { version as serverVersion } from "../package.json"; const usernameAction = ( { username }: ActionHandlerArgs, client: Client, ) => { - client.setUsername(username) -} + client.setUsername(username); +}; const createLobbyAction = ( { gameMode }: ActionHandlerArgs, client: Client, ) => { /** Also sets the client lobby to this newly created one */ - new Lobby(client, gameMode) -} + new Lobby(client, gameMode); +}; const joinLobbyAction = ( { code }: ActionHandlerArgs, client: Client, ) => { - const newLobby = Lobby.get(code) + const newLobby = Lobby.get(code); if (!newLobby) { client.sendAction({ - action: 'error', - message: 'Lobby does not exist.', - }) - return + action: "error", + message: "Lobby does not exist.", + }); + return; } - newLobby.join(client) -} + newLobby.join(client); +}; const leaveLobbyAction = (client: Client) => { - client.lobby?.leave(client) -} + client.lobby?.leave(client); +}; const lobbyInfoAction = (client: Client) => { - client.lobby?.broadcastLobbyInfo() -} + client.lobby?.broadcastLobbyInfo(); +}; const keepAliveAction = (client: Client) => { // Send an ack back to the received keepAlive - client.sendAction({ action: 'keepAliveAck' }) -} + client.sendAction({ action: "keepAliveAck" }); +}; const startGameAction = (client: Client) => { - const lobby = client.lobby + const lobby = client.lobby; // Only allow the host to start the game if (!lobby || lobby.host?.id !== client.id) { - return + return; } - let lives = lobby.options.starting_lives? parseInt(lobby.options.starting_lives) : GameModes[lobby.gameMode].startingLives + const lives = lobby.options.starting_lives + ? Number.parseInt(lobby.options.starting_lives) + : GameModes[lobby.gameMode].startingLives; lobby.broadcastAction({ - action: 'startGame', - deck: 'c_multiplayer_1', - seed: lobby.options.different_seeds? undefined : generateSeed(), - }) + action: "startGame", + deck: "c_multiplayer_1", + seed: lobby.options.different_seeds ? undefined : generateSeed(), + }); // Reset players' lives - lobby.setPlayersLives(lives) -} + lobby.setPlayersLives(lives); +}; const readyBlindAction = (client: Client) => { - client.isReady = true + client.isReady = true; // TODO: Refactor for more than two players if (client.lobby?.host?.isReady && client.lobby.guest?.isReady) { // Reset ready status for next blind - client.lobby.host.isReady = false - client.lobby.guest.isReady = false + client.lobby.host.isReady = false; + client.lobby.guest.isReady = false; // Reset scores for next blind - client.lobby.host.score = 0 - client.lobby.guest.score = 0 + client.lobby.host.score = 0; + client.lobby.guest.score = 0; // Reset hands left for next blind - client.lobby.host.handsLeft = 4 - client.lobby.guest.handsLeft = 4 + client.lobby.host.handsLeft = 4; + client.lobby.guest.handsLeft = 4; - client.lobby.broadcastAction({ action: 'startBlind' }) + client.lobby.broadcastAction({ action: "startBlind" }); } -} +}; const unreadyBlindAction = (client: Client) => { - client.isReady = false -} + client.isReady = false; +}; const playHandAction = ( { handsLeft, score }: ActionHandlerArgs, client: Client, ) => { if (!client.lobby) { - return + return; } - client.score = score + client.score = score; client.handsLeft = - typeof handsLeft === 'number' ? handsLeft : Number(handsLeft) + typeof handsLeft === "number" ? handsLeft : Number(handsLeft); - const lobby = client.lobby + const lobby = client.lobby; // Update the other party about the // enemy's score and hands left // TODO: Refactor for more than two players if (lobby.host?.id === client.id) { lobby.guest?.sendAction({ - action: 'enemyInfo', + action: "enemyInfo", handsLeft, score, - }) + }); } else if (lobby.guest?.id === client.id) { lobby.host?.sendAction({ - action: 'enemyInfo', + action: "enemyInfo", handsLeft, score, - }) + }); } console.log( `Host hands: ${lobby.host?.handsLeft}, Guest hands: ${lobby.guest?.handsLeft}`, - ) + ); if (!lobby.host || !lobby.guest) { - stopGameAction(client) - return + stopGameAction(client); + return; } // This info is only sent on a boss blind, so it shouldn't // affect other blinds @@ -144,75 +148,91 @@ const playHandAction = ( (lobby.host.handsLeft === 0 && lobby.guest.handsLeft === 0) ) { const roundWinner = - lobby.host.score > lobby.guest.score ? lobby.host : lobby.guest + lobby.host.score > lobby.guest.score ? lobby.host : lobby.guest; const roundLoser = - roundWinner.id === lobby.host.id ? lobby.guest : lobby.host + roundWinner.id === lobby.host.id ? lobby.guest : lobby.host; if (lobby.host.score !== lobby.guest.score) { if (!lobby.options.death_on_round_loss) { - roundLoser.lives -= 1 + roundLoser.lives -= 1; } - roundLoser.sendAction({ action: 'playerInfo', lives: roundLoser.lives }) + roundLoser.sendAction({ action: "playerInfo", lives: roundLoser.lives }); // If no lives are left, we end the game if (lobby.host.lives === 0 || lobby.guest.lives === 0) { const gameWinner = - lobby.host.lives > lobby.guest.lives ? lobby.host : lobby.guest + lobby.host.lives > lobby.guest.lives ? lobby.host : lobby.guest; const gameLoser = - gameWinner.id === lobby.host.id ? lobby.guest : lobby.host + gameWinner.id === lobby.host.id ? lobby.guest : lobby.host; - gameWinner?.sendAction({ action: 'winGame' }) - gameLoser?.sendAction({ action: 'loseGame' }) - return + gameWinner?.sendAction({ action: "winGame" }); + gameLoser?.sendAction({ action: "loseGame" }); + return; } } - roundWinner.sendAction({ action: 'endPvP', lost: false }) - roundLoser.sendAction({ action: 'endPvP', lost: true }) + roundWinner.sendAction({ action: "endPvP", lost: false }); + roundLoser.sendAction({ action: "endPvP", lost: true }); } -} +}; const stopGameAction = (client: Client) => { - client.lobby?.broadcastAction({ action: 'stopGame' }) -} + client.lobby?.broadcastAction({ action: "stopGame" }); +}; const gameInfoAction = (client: Client) => { - client.lobby?.sendGameInfo(client) -} - -const lobbyOptionsAction = (options: any, client: Client) => { - client.lobby?.setOptions(options) -} + client.lobby?.sendGameInfo(client); +}; + +const lobbyOptionsAction = ( + options: Record, + client: Client, +) => { + client.lobby?.setOptions(options); +}; const failRoundAction = (client: Client) => { - const lobby = client.lobby + const lobby = client.lobby; - if (!lobby) return + if (!lobby) return; if (lobby.options.death_on_round_loss) { - client.lives -= 1 - client.sendAction({ action: 'playerInfo', lives: client.lives }) + client.lives -= 1; + client.sendAction({ action: "playerInfo", lives: client.lives }); } if (client.lives === 0) { - let gameLoser = null - let gameWinner = null + let gameLoser = null; + let gameWinner = null; if (client.id === lobby.host?.id) { - gameLoser = lobby.host - gameWinner = lobby.guest + gameLoser = lobby.host; + gameWinner = lobby.guest; } else { - gameLoser = lobby.guest - gameWinner = lobby.host + gameLoser = lobby.guest; + gameWinner = lobby.host; } - gameWinner?.sendAction({ action: 'winGame' }) - gameLoser?.sendAction({ action: 'loseGame' }) + gameWinner?.sendAction({ action: "winGame" }); + gameLoser?.sendAction({ action: "loseGame" }); } -} +}; -const setAnteAction = ({ ante }: ActionHandlerArgs, client: Client) => { - client.ante = ante -} +const setAnteAction = ( + { ante }: ActionHandlerArgs, + client: Client, +) => { + client.ante = ante; +}; + +/** Verifies the client version and allows connection if it matches the server's */ +const versionAction = ( + { version }: ActionHandlerArgs, + client: Client, +) => { + if (version !== serverVersion) { + client.sendAction({ action: "error", message: "Version mismatch" }); + } +}; // Declared partial for now untill all action handlers are defined export const actionHandlers = { @@ -231,4 +251,5 @@ export const actionHandlers = { lobbyOptions: lobbyOptionsAction, failRound: failRoundAction, setAnte: setAnteAction, -} satisfies Partial + version: versionAction, +} satisfies Partial; diff --git a/Server/src/actions.ts b/Server/src/actions.ts index 55d2bd0c..87136923 100644 --- a/Server/src/actions.ts +++ b/Server/src/actions.ts @@ -31,8 +31,8 @@ export type ActionEnemyInfo = { handsLeft: number } export type ActionEndPvP = { action: 'endPvP'; lost: boolean } - export type ActionLobbyOptions = { action: 'lobbyOptions' } +export type ActionRequestVersion = { action: 'version' } export type ActionServerToClient = | ActionConnected @@ -49,8 +49,8 @@ export type ActionServerToClient = | ActionEnemyInfo | ActionEndPvP | ActionLobbyOptions - | ActionKeepAlive - | ActionKeepAliveAck + | ActionRequestVersion + | ActionUtility // Client to Server export type ActionUsername = { action: 'username'; username: string } @@ -71,10 +71,11 @@ export type ActionGameInfoRequest = { action: 'gameInfo' } export type ActionPlayerInfoRequest = { action: 'playerInfo' } export type ActionEnemyInfoRequest = { action: 'enemyInfo' } export type ActionFailRound = { action: 'failRound' } -export type ActionSetAnte = { +export type ActionSetAnte = { action: 'setAnte' - ante: number + ante: number } +export type ActionVersion = { action: 'version'; version: string } export type ActionClientToServer = | ActionUsername @@ -93,6 +94,7 @@ export type ActionClientToServer = | ActionLobbyOptions | ActionFailRound | ActionSetAnte + | ActionVersion // Utility actions export type ActionKeepAlive = { action: 'keepAlive' } @@ -107,15 +109,15 @@ export type ActionHandlers = { [K in HandledActions['action']]: keyof ActionHandlerArgs< Extract > extends never - ? ( - // biome-ignore lint/suspicious/noExplicitAny: Function can receive any arguments - ...args: any[] - ) => void - : ( - action: ActionHandlerArgs>, - // biome-ignore lint/suspicious/noExplicitAny: Function can receive any arguments - ...args: any[] - ) => void + ? ( + // biome-ignore lint/suspicious/noExplicitAny: Function can receive any arguments + ...args: any[] + ) => void + : ( + action: ActionHandlerArgs>, + // biome-ignore lint/suspicious/noExplicitAny: Function can receive any arguments + ...args: any[] + ) => void } export type ActionHandlerArgs = Omit diff --git a/Server/src/main.ts b/Server/src/main.ts index 40994f72..fcc2c50c 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -7,11 +7,13 @@ import type { ActionCreateLobby, ActionHandlerArgs, ActionJoinLobby, + ActionLobbyOptions, ActionPlayHand, ActionServerToClient, ActionSetAnte, ActionUsername, ActionUtility, + ActionVersion, } from './actions.js' const PORT = 8080 @@ -65,8 +67,9 @@ const server = net.createServer((socket) => { // improve latency between responses socket.setNoDelay() - const client = new Client(socket.address(), sendActionToSocket(socket)) + const client = new Client(socket.address(), sendActionToSocket(socket), socket.end) client.sendAction({ action: 'connected' }) + client.sendAction({ action: 'version' }) let isRetry = false let retryCount = 0 @@ -114,6 +117,12 @@ const server = net.createServer((socket) => { ) switch (action) { + case 'version': + actionHandlers.version( + actionArgs as ActionHandlerArgs, + client, + ) + break case 'username': actionHandlers.username( actionArgs as ActionHandlerArgs, @@ -163,13 +172,19 @@ const server = net.createServer((socket) => { actionHandlers.gameInfo(client) break case 'lobbyOptions': - actionHandlers.lobbyOptions(actionArgs, client) + actionHandlers.lobbyOptions( + actionArgs as ActionHandlerArgs, + client, + ) break case 'failRound': actionHandlers.failRound(client) break case 'setAnte': - actionHandlers.setAnte(actionArgs as ActionHandlerArgs, client) + actionHandlers.setAnte( + actionArgs as ActionHandlerArgs, + client, + ) break } } catch (error) { From a11156c844dc2ab036faf97e1216830aed822469 Mon Sep 17 00:00:00 2001 From: TGMM Date: Fri, 12 Apr 2024 15:50:15 -0700 Subject: [PATCH 0089/1128] Fixed number parsing --- Server/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/main.ts b/Server/src/main.ts index fcc2c50c..66484ea2 100644 --- a/Server/src/main.ts +++ b/Server/src/main.ts @@ -30,7 +30,7 @@ const stringToJson = (str: string): any => { const obj: Record = {} for (const part of str.split(',')) { const [key, value] = part.split(':') - const numericValue = Number.parseFloat(value) + const numericValue = Number(value) obj[key] = Number.isNaN(numericValue) ? value : numericValue } return obj From 01932c1fc115c40b9a6cab9364dd2a79c9793704 Mon Sep 17 00:00:00 2001 From: TGMM Date: Fri, 12 Apr 2024 16:17:25 -0700 Subject: [PATCH 0090/1128] Fixed type error --- Server/src/Lobby.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Server/src/Lobby.ts b/Server/src/Lobby.ts index b055d643..87f35fe2 100644 --- a/Server/src/Lobby.ts +++ b/Server/src/Lobby.ts @@ -23,7 +23,8 @@ class Lobby { host: Client | null; guest: Client | null; gameMode: GameMode; - options: { [key: string]: unknown }; + // biome-ignore lint/suspicious/noExplicitAny: + options: { [key: string]: any }; // Attrition is the default game mode constructor(host: Client, gameMode: GameMode = "attrition") { From 4a5c60a68b1f60677de9420a30506fb8126bdef4 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Sat, 13 Apr 2024 01:17:55 +0000 Subject: [PATCH 0091/1128] Fixed end_pvp flag getting carried over to small blind --- Multiplayer/UI/Game_UI.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Multiplayer/UI/Game_UI.lua b/Multiplayer/UI/Game_UI.lua index a5784a34..d1550fd2 100644 --- a/Multiplayer/UI/Game_UI.lua +++ b/Multiplayer/UI/Game_UI.lua @@ -692,6 +692,7 @@ function Game:update_shop(dt) if not G.STATE_COMPLETE then G.MULTIPLAYER_GAME.ready_blind = false G.MULTIPLAYER_GAME.ready_blind_text = "Ready" + G.MULTIPLAYER_GAME.end_pvp = false end update_shop_ref(self, dt) end @@ -840,8 +841,6 @@ function Game:update_hand_played(dt) }) G.FUNCS.draw_from_hand_to_discard() end - - G.MULTIPLAYER_GAME.processed_round_done = true elseif not G.MULTIPLAYER_GAME.end_pvp then G.STATE_COMPLETE = false G.STATE = G.STATES.DRAW_TO_HAND @@ -1872,7 +1871,6 @@ function Game:update_selecting_hand(dt) G.STATE_COMPLETE = false G.STATE = G.STATES.NEW_ROUND G.MULTIPLAYER_GAME.end_pvp = false - return end end From 8e6ba9e0fc979cd7ec7d0e370e6e75b467afa43c Mon Sep 17 00:00:00 2001 From: TGMM Date: Fri, 12 Apr 2024 18:22:43 -0700 Subject: [PATCH 0092/1128] Updated error message --- Server/src/actionHandlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index 2cdcde0f..d61fe80c 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -230,7 +230,7 @@ const versionAction = ( client: Client, ) => { if (version !== serverVersion) { - client.sendAction({ action: "error", message: "Version mismatch" }); + client.sendAction({ action: "error", message: `WARN: Server expecting version ${serverVersion}` }); } }; From dc0ef8b6e10a329a7e32cad7c76360e2f951d479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Fri, 12 Apr 2024 19:35:28 -0700 Subject: [PATCH 0093/1128] Fixed docker bugs (#41) --- Server/Dockerfile | 2 +- Server/src/actionHandlers.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/Dockerfile b/Server/Dockerfile index c1401ed7..ea6867cf 100644 --- a/Server/Dockerfile +++ b/Server/Dockerfile @@ -12,4 +12,4 @@ RUN npm run build EXPOSE 8080 -CMD [ "node", "dist/main.js" ] +CMD [ "node", "dist/src/main.js" ] diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index d61fe80c..c1be0cce 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -12,7 +12,7 @@ import type { ActionVersion, } from "./actions.js"; import { generateSeed } from "./utils.js"; -import { version as serverVersion } from "../package.json"; +import { version as serverVersion } from "../package.json" with { type: "json" }; const usernameAction = ( { username }: ActionHandlerArgs, From a49e2fa120ae45e0ea29e474dda2687d4208f831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Fri, 12 Apr 2024 19:38:33 -0700 Subject: [PATCH 0094/1128] Monkey patch server version (#42) --- Server/src/actionHandlers.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Server/src/actionHandlers.ts b/Server/src/actionHandlers.ts index c1be0cce..c07b1471 100644 --- a/Server/src/actionHandlers.ts +++ b/Server/src/actionHandlers.ts @@ -12,7 +12,6 @@ import type { ActionVersion, } from "./actions.js"; import { generateSeed } from "./utils.js"; -import { version as serverVersion } from "../package.json" with { type: "json" }; const usernameAction = ( { username }: ActionHandlerArgs, @@ -224,6 +223,8 @@ const setAnteAction = ( client.ante = ante; }; +// TODO: Fix this +const serverVersion = "0.1.2-MULTIPLAYER"; /** Verifies the client version and allows connection if it matches the server's */ const versionAction = ( { version }: ActionHandlerArgs, From 9e488fe97cdf170b2e1372b787b19b7ea52136e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eliud=20de=20Le=C3=B3n?= Date: Fri, 12 Apr 2024 20:12:49 -0700 Subject: [PATCH 0095/1128] Reverted docker change (#43) * Monkey patch server version * Revert "Fixed docker bugs (#41)" This reverts commit dc0ef8b6e10a329a7e32cad7c76360e2f951d479. --- Server/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/Dockerfile b/Server/Dockerfile index ea6867cf..c1401ed7 100644 --- a/Server/Dockerfile +++ b/Server/Dockerfile @@ -12,4 +12,4 @@ RUN npm run build EXPOSE 8080 -CMD [ "node", "dist/src/main.js" ] +CMD [ "node", "dist/main.js" ] From 07a6fef258013db31b097eb825dca9480a6ce00d Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Mon, 15 Apr 2024 01:00:04 +0000 Subject: [PATCH 0096/1128] Fixed starting ante not working --- Server/src/GameMode.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Server/src/GameMode.ts b/Server/src/GameMode.ts index b6401431..88da305e 100644 --- a/Server/src/GameMode.ts +++ b/Server/src/GameMode.ts @@ -5,7 +5,7 @@ type GameModeData = { getBlindFromAnte: ( ante: number, - options: unknown, + options: any, ) => { small?: string; big?: string; @@ -20,15 +20,16 @@ const GameModes: { } = { attrition: { startingLives: 4, - getBlindFromAnte: (ante) => { + getBlindFromAnte: (ante, options) => { return { boss: "bl_pvp" }; }, }, draft: { startingLives: 2, - getBlindFromAnte: (ante) => { - if (ante < 4) return {}; - return { small: "bl_pvp", big: "bl_pvp", boss: "bl_pvp" }; + getBlindFromAnte: (ante, options) => { + const starting_antes = options?.draft_starting_antes ? parseInt(options.draft_starting_antes) : 3 + if (ante <= starting_antes) return { } + else return { small: 'bl_pvp', big: 'bl_pvp', boss: 'bl_pvp' } }, }, }; From 959398a8f4bf097eb2caabb8460cabbd00fe4625 Mon Sep 17 00:00:00 2001 From: Connor Mills Date: Mon, 15 Apr 2024 04:03:42 +0000 Subject: [PATCH 0097/1128] Added assets for future use --- Assets/player_blind_gif.gif | Bin 0 -> 25822 bytes Assets/player_blind_heart_gif.gif | Bin 0 -> 26116 bytes Assets/player_blind_heart_row.png | Bin 0 -> 8089 bytes Assets/player_blind_hook_gif.gif | Bin 0 -> 25823 bytes Assets/player_blind_hook_row.png | Bin 0 -> 7555 bytes Assets/player_blind_mark_gif.gif | Bin 0 -> 25784 bytes Assets/player_blind_mark_row.png | Bin 0 -> 7476 bytes Assets/player_blind_row.png | Bin 0 -> 7460 bytes Assets/player_blind_rows.xcf | Bin 0 -> 5472251 bytes Assets/player_blind_tooth_gif.gif | Bin 0 -> 25829 bytes Assets/player_blind_tooth_row.png | Bin 0 -> 7559 bytes 11 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Assets/player_blind_gif.gif create mode 100644 Assets/player_blind_heart_gif.gif create mode 100644 Assets/player_blind_heart_row.png create mode 100644 Assets/player_blind_hook_gif.gif create mode 100644 Assets/player_blind_hook_row.png create mode 100644 Assets/player_blind_mark_gif.gif create mode 100644 Assets/player_blind_mark_row.png create mode 100644 Assets/player_blind_row.png create mode 100644 Assets/player_blind_rows.xcf create mode 100644 Assets/player_blind_tooth_gif.gif create mode 100644 Assets/player_blind_tooth_row.png diff --git a/Assets/player_blind_gif.gif b/Assets/player_blind_gif.gif new file mode 100644 index 0000000000000000000000000000000000000000..f3ef913444562cc13a1e1d194cb1be831dd1769d GIT binary patch literal 25822 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 literal 0 HcmV?d00001 diff --git a/Assets/player_blind_heart_gif.gif b/Assets/player_blind_heart_gif.gif new file mode 100644 index 0000000000000000000000000000000000000000..cba330092fe5e6b570779feb0d3cd2bacd178ab0 GIT binary patch literal 26116 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 WhUBAyT#G5ikiCr)w&cL^`2PSW7xMoA literal 0 HcmV?d00001 diff --git a/Assets/player_blind_hook_gif.gif b/Assets/player_blind_hook_gif.gif new file mode 100644 index 0000000000000000000000000000000000000000..450be24747c6efd68895b759b467f1c0ce8c9e04 GIT binary patch literal 25823 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 d>sRu~)M;4`a}pC5eq?HL-{kF`|(X` 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 literal 0 HcmV?d00001 diff --git a/Assets/player_blind_mark_row.png b/Assets/player_blind_mark_row.png new file mode 100644 index 0000000000000000000000000000000000000000..2117940c97b3811ea05a540f805d45b41775e40a GIT binary patch literal 7476 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 O1*{J8EA{z&*Z%^BREW<2 literal 0 HcmV?d00001 diff --git a/Assets/player_blind_row.png b/Assets/player_blind_row.png new file mode 100644 index 0000000000000000000000000000000000000000..a89ebed8b3f48dfb6353f62d1c60908eb37edcd1 GIT binary patch literal 7460 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

f!+XC>`eI`6VEo( z=|lT4{V1!^O&{7#VHhq-Km=RnnqzCN3u0tI_Kh4JNE+b z@pBEjmCqo4>uy)aSMVzdb-FG*MUh`XdF<;UESGR^z|wVGhE7jx<}zec%q!+H#?vz| zyYl@T#q^{4T_ql4#_X?dg^<}1_oW872j^Mi=>-bwe{|-nqlR<`wKGa7Hi)UsCeFtWs%w%-c*D)17 zeY-xCfwonSN+(a{^z21H zx`z#Ti#@n7hl0QGTt<8!^BMgDmw_D)(KkVojk1ih_idK|=1|)yaI$?91UpQC_uF_3 zV$^Z$>Ua$Ar%rz&xQxv3sK@@*T!y#Fv9a65(#K8@>c-WVxr%c7vOYP6Z-NwdXf$&f zI1|(WXz1 zEuL0-wR2$$}-O7 zx19l)LlNS;A|?47{k;(ED(S#b5T%Y|SI1U(KXv*O!Deu0LO|WsjEA?WV>8Cpm)VMP z`m#Q`AKwcp?8<0nGsrU0N8PRrA@INsl-Z1~`p|AgHp8cH*M~CDwrW>K@HLKYGfNSC zHruhk*^aUwebb~(pWMHAS}_{QCg!u~girs?QhrOMUtlvfdM|ZM#vb==jb(gi<8(jU zQ{NHEM!s8`tuIF3EnV#8Un;eI7$s>PZv;3lBaQBpb?iWoIE;PHH zL;6bAC)^t$#JRD&O!>NGy>DMNN!E9l0T!`~hk0_Pfk!If%aJK;O|#I8Q`O-!ei z>?Rv=55%0_7ed=55##E5y+3zS90$wc)6-wFuT8IXT8S5ymk-tf{em_to*mi6ri#B% zI_1+<&op+~PKihhgv3w+H1eWSe99z~9VKHBs=?=o-#F^554ZMtf1g83?K zyFTPi+cJ0I%fRtSe#f_My`9FYw~KdQ1Yh+wzE7&T3Y|7Wn`%9tvr}l-MftSPaJpx@ z67E@}n4u_2m>o-BE6p}AJLYT!W=H#0DPy?jq9ko#f4^fX(&#?fz<%ualPL zMfk+*xUrOn^o5-s?)?rT*jQesjIXC+cC>JJNnV8?%#Is-T!__rV2io*XM^stQI%HOc?lx7-gctz%=&ENLyKI+5 zIc+FUuvtaFrO{a#_=*GA5$dbi2l{B!^OVbg_G5N}yEfW%pr`eT#6HocT94=K6xvZyKI1c->6xyDd)6rC9*PoX z$I>@Svn|YyIS;YeOY94!jNyKYlC*{0`i_4{qx)nFyTc>ip(vHVNm`Z{;S;mt)>0nQ z7j|*Dw>5}OV|ke}zL|>I(Zby&c@=&zJ8tc9Ay(pzbKnXy@t81~e5UyuA>8%Q#yN;D zAWrnY5ZdmB7z@|y{TY+QqAG_^Pk+h5Hoej{^9jWI^cAo%>xP>j9rk;dRa}=3)7}+hT$fk)px;ZQyqNDLt{kgT*;vkHSMEhu57lciujinJRnYq# z{61-BNOimWQb>@K>jIy!tyhxPc`gAFE_XFzAUf4BBFfjnTDdiD}&J0PUAOk$LL$850+`5Pv0L^OhbAE4Bkn^G+-XF z{S25@EY#a4=?Z3_m2gj8U=}cE6(oHz#P5W_d{^Kdw)Ze&HZl#EF@fr{d=CUXl;q@j zfnz}KMQMX450Inaeu!|NY%BtilN~_a5EiAtFnGp+K8_#L;1GSz4hWek56qaB7tq$w zBU~~Dw5hkt92%}-6yOKib_?#CFQRzl3)UjrAo5ENwCTla$X4iF^<;h6XJBH?zt5wK zjkopn=Y9y5CC+Oc8|G;j*az<;qz`2eHhW6!!vN{(*#-G*)3-AW>Miyfe69Ji*0T%B zx2wJ@tI*X><1%l@=)H~y%RbPj@3SiQAw33G?<8U$7TxXz%qq5vL2sL+YnXl3!aa3? zZNQvWko>nwweJD*U4e_R8MBdnz>Eo$n&mql;GrZZhXU__+>6o{&lX6)TcFI*eX_L( zl%9YwK-~})r6uV8CD{j|Zz4~Runke8obh=S${5)Vt{n@ zjDvi(>BSs)w0}Gp3FRxlY#+5h>m$aZsPD=&bhWEz8y+kp!E*clsA4SA`@!^`M2y6u z+pB|tMgQUN z-t|jeD0ZNUt&rb3wgO|xdb^6PXwqS%+32WeBbxO^-?a5?2+KpXPehCpD+}f8Zdb)t zlzmm%s6BnpRIm~2EJKqXTvFgFu1Ho!<#YurNQhU6T`tBVV8sbxQS!V8R;yNu_vy-1 zZ#O*PugjIdN5uCkh36wM=UAz{JY%^9egbn>iLWR#5=D8AGw6e2C@_P9VE_UKlJuue zpcErLwjwyIvf#)&GGD}z5F)GZ`r*KL|v)(f= zRqRER4lQc=*0U4M`l4^zdUk{@L-bFCj58`7GvxE>YFEWxlzmm%s6DM$Y;?%R#;_QW z4VJY@4{j-N7uO_fqjI{285Hpfu@lBv1EznNZhW9vdq$zc2$hVY-wLb=0fCa*qqp!7jdDm zp({1Ggmw$R7h#mm$nt#(RXSqb29Bapt5UUb6M=W2eZbjeo@3O`+xs?Y;vZrg*2$O6 z^Bz85!^Jj=x>@Z-|D|tFw=Q*|*a7Ayw!r5UPN|UJI=yFNs@RMs9Y&hfs-CTA))#%# z)-x$=8L|OH%s7?OF-1O~u69*yM%h=DjoPz;t=M8&oAltC0+;ccWOGzbH!*`EULkhN z7|Vbe6k$>Fya;B{&EkE!i5V0gFoSL`=6pqbuTprv0&|X)%FCER3;YGu7m|#W*lcZjXYP2<*P+x;)~vUxJ(iFs<_wbdxy?5_DKk zw|p>$5aK>c7j(1_X{K;LfijH7m2;l9QYGsHyqi)iQ=}>6Im;B+xi>_jJfrw1b{p>T zWQ@kFCqrO)up#70EawWg1hws8h19l#gI8}SvtEq*$hx_ zpzC$~b-NzlTR)(LwcH7EVU6qbYzwp)+Np|NY1N|teM+{FTAcN>-5@qT>h&ByYKve~ z!?AR?%=9OY2JBXqQ5|M{LOGcCzVvlcv9&axEm$*Sx%ZpCpg7bl1*+=Ey0YzpR# zf|Np&u3+AAp;(bc#3k@U9)|lkygX_9Hj;eT2iX|_oY2*1^-R7ObfJj(x|RXbX4u#T`b zd$JP65@rVmtrpnXf_7&;jveH#CK&@Kz zU)T-eTikIdYe?OwSG%ZXTlgJo!A_QW?}u`Y@;u-=I@+=SIF5Lg_o^go&027EITKX8 zC-bw{_#ej)JvBj0;U3(VXM8Yc6r>cIbPe;43&q@c76|i~F*#nwEMH)L$QaSz@nV8( zmTzH#CXAWM4o+-gme23S0QF6_b4jreMGR0ej(b+f&wx*GwtQ2AT#ELm)afwGJ2ok# z!@dQ?xi@HxeX7ziF2qma9<5ywSI&9bN|kJ($4cdmG=+N1GKGI*C(1L5k7Bps9^+&* zW-A$jAK4P$WGz*}I-%wrtdQDv%FqiL1+WRjQJ$%@l}iu2&%BMr5rZSPD8L& ztx!YfLuG_SCP7>gxjT*tv_0-RT5dVk#OzfvD~_}=;fC9LJ3&<3n=ot|x>(p9lkty=Vd*ummkob|I^H8wu#^=cQjYzqfv zE!fF2@BNVPC>I4jrlTGE&-YY$e@(L4tOcK#vsJ}=vQvAFqjDV9s}sZ&?!nJ_Rtj@Q zK}w-XH!<(HQ0$UtsxZqN+vH`;@&$H^tQP$pFScq1Y}LUdZP<_twvKOv@{d=eezlqYk5~AY;rSFY*SmA)|&7 zIue&L4*7UH|Ak)6-8J!{ER$pz;6Fv)A-#_i=LN1}9nY#UQLr2mmy_W$mcdGsICiX) zCD}4?4ymy)#zn!qv zEx?h$mSF3&S+X3ow7RoY^yyMDq!0SVHHIb9nNMFYcqQ{@?S^pOb08Rr9WDDdMma8w zu{@x$7i8oo7SU=Tmc@?_=F5{^@i%TKEB1`l9KOh7v5bzrLmp@e?9u6DF+}~)e&&NU zg(ueB(sDG(Pm2{!4mH-2Ey@wZvwP5A_*uzrjb&@S?f5#imS?3@MwAt2l%nPKb+%m3 zP&U>QmY^ssMOU-Q*wfgMEYI(t@}5_0b)6>;!pRy)~_WT7I-`Wh##ChI%hUL(vp z?EgX>W+>yX(4w*fkA<0RByIrj8Eh7j6K2Ec9=71dm>NGLwk*ULUxw0qe$4o?NPe)p zlBW23riCvfr+!Nz@vN+E3V~Xore&nJ& zH#Qp2vu)|Kk4v0bEaUf#adAE!zvF}68HqzL8t3Kh{FjT-<2CWjQ~6Gk9l<@uPeq(q zw+w6!o(St|N;sTk4+{o)$K z66wsRuNSj8=FTDf~_ zEL-bq^o+F*N=mP)z3V5G5#t>t=xn)>p=_)rMx)hJ{xYkt`D(MkF00h~S)3Mf?~#CFQmX~OwG-knKN)w17L^@%G|XcoariB>#&w8AZc z(LHSOUKms3Cs;qv)`b}3>ri^n&lz7A$q#ntmi?A#;p$&!2_ zIWFU&$flq-hK}i@Kl6?_mcFy44Sc8jbUj$V6De;nO9$LhFhKmO3^aP-M0Yp zue9V?>~C>g7-#$TFis@Hi+EJ4fp{jCovqB)>XScueK#wmQMt*bv5bztL>_1f?Dn!) zt677^5cNYD%z`zAC-JDfhY$H_vBH~KOv!#>eyj;cr+M(DqG$4BV57#eMQhWm+Q(YU z^Kw={w%jI0JiKa&VDd1Njl|*i%qCYt7LgN~!f;RD%$a3iE}kz8 zF~%68^qwI!#xRl}?EbbXFothd0hX&92~*?<{2unMV8*m z3SaGZ99y=(Q8hFk2etIfpkg6=PblA5_Ix7}vp!pomtzOXYmqU6-y!s3G{>++TY1LT z_X$gGrM1$Fk+Pr0W_Gp)&Oi$mM-E3Ro~C*Y*e0ej=F8OwwME_3YR2Ssu*um*q-9;~ zNk76-=QujFS%H=8V23jKvE|2y`XcIYloI@8H%oD5)|)J&#vVrp#|&~%&$xKzWE@*} zd-x~AXJv&)l@&;Fwy>Xo9N7=)8>)h%C>}Ve2IP9E9eZo1` z_`O(=C-~d76HiJE4$i~aEqFng+5SKu=lN3Zs3KoUj?;6dah&H($px4l!k9ZP$9w+N z$Ai%@-=04mjTe7G-{cm+p4h%Za#wMj=TUun+OjVA?TYfyHrO*BBSZO?;#p7pc5Tp+ z&n^05nJ{WA};!mbmXtoa^1=53eBKMfl+cC$xa`zi9#$#sb zWJ4Ym)&~a1@Z9sBB@|0$>8-5r)ozzrz4ICxe+6F~R^MZ)FY?Gep?v74?)ElbJ=01~ zjinT0$DbSIuo05SHLt9TU5%D#EBJJ^v%WE}x?|~wY+?O04!5&4=5DlLapZ86;%TbU zV4STnO~&%L+OT)3{{G~|B0HP0R=|67!qAL3jo&|?t~T#WTx za9&=#747-pIL>pz=uw_VU>@;%%<_rj+TQB5->jXuVKNQa6$myn&H3Rt&U3`v#YT=c z0pIF*;yBK8#biXx4q?m}m*YKW?BmJ)(KmjxA|yX7KOtU>4SkbM0bgW$Imy?>ah^N& z>1oUQp`TG6+6H^ZV`M1bQasC&HtIqdeI6_mM(s!*jK}D={2hmw^M;7UJz5+;&CWuz zeE=zt512Oct>W!cam`y`S8*8+%Qb)wHpV}~a&Uh-q&;{xgvJY6dMhh@wcBxQp*+5Z z#@NBvmerT6BkK*>dqVlfvPU1nH%n-jN-2(#KezC^$Rv)W?v`jP_;h7bpQsJ=6TN2K zqy1znbhl34j=Gmd8%GeHrg{x{MW!;2)71yHMcq_F<7_+F<}qt=@&qsAUb2gi*0QfXa@$%}$j?QRcSa=A7p z;8A5|4_a$dWR-gh@kShKD+1KO;Hvd+-QD=@#* z>M$p5`ZOCLi?N;o&&&G?y$w!IKaTTEIC~f;RakxSyUg;5cYFKyT z{_$eOr+f1q8S!vxKAFgHs=jB!{Wtp117MEBx%!>~$L=ho3vYRQ*)!gYWO=Ux|L4*N zImI{nkQRE3s{GzF+Pk0wZ^ZYsjUn|i#;W{Mo4Cz&A3WyXn(V~HA?5641_^9D=Ex*5}GJb4!zmL)pV&iFuiCu4)zz__$p3%yy* zc46M`VtsJ0dNDK~Y*qHSwPLF^-mtb>#XiaN8NUav672`6s}-0p)Ux2qmk#aER?LQd zvTLnYsLO$5eY+nET-jkejmE9uuXJ&;$XBJc{5_-0U7;Op`tZSYYE$@e{fplmiY04n z_v2ZrrZ)cIAN^uIg5Ngse}vxJZ2T#B>Unvv*08=oxe&sO=a`3M+iS0ARDT;gY&n+J zJvnFxmi__QvVKTut%tvutUbAoW38?6;kXu*cu&XG{+wob75?gj+}9v0=xU&o?5cTV!##0a*0b;EOt>_Nn<{_ zs0p01k7sFOUbz?_xMjtoT$TfVOIY4`f{n@c@t$KQKMkGv_B?YO*XQr8PFyqhw2~9{ zJhPWQ$LznJh!TN!4PQ6#+%ooiS$e-8*UO$uUL=1S?ZFK)zn+M+(4$B4d-R-fjk}-& z$FxzF@q5brQBU@8R8H+|LF29uV8lfdJUZ`Ue8nXq7svIIXTe-dPG~Gk)X;onNJ*az z;enF8pE4C2p5z6OU-B?hz9)543l-j8Rga@qLf2yiSYes5{~9HX~38-Mm-XjLl{ z+7YGAx!TOe6v{jt+kOqBSUj*FQ*3p%9DC9|ImnZxe*m_uA4|$o!>hGt=IZaphx6La zGBKu;>tTLukb6X*!AqSE^Mx_9af&`?k;+(jaSUwH-tDff(P2h&9BesN@{}eWW=ZVG zCDSS<7;g}`UApBsmP<^nEV<55W^0R@z}WhDmN8~+i}8WERXoaNIpDX1<&7uUcw!&# z8C>=NbmkksaS&$KvOdm!Z*^jF?alj$#r3jhaQ$~JQKGHnQ254#XKt~749r`26W`08 zsa*trmK*|m>e2_9-?ccuV`2E zJf5X$N(RmKxV25nzOr_qw>BGp;$xxLSwgEVJ#&QP_m*lf9NS*IS)=;f6NnaM-=*%! zL3y_zKLA_SFX|VoH`gC*jA?v0uiY%gA#%0J<_X74qR-%^PKWuzxW70>pR-71EIB~& zo<)1dVAkj`qd9i7oGN)mlMb^acKBk(b2Y&`32xVKIgaHL2P!re`@F5Bn1gFHfeZC1 zSjLzSEyf283X^) zUm3kDI?q7)`mdahd(OaQZ|4c+NR#zx@ONa~#F6?hji&BP9YDs{`TG=S3<8A5XTVlNROw`!cp$i@t@tS$Z4`3#-jzs zQ_LlDFfORs0;~=^l(lb-TzJHID z;^erKlYZN6*hV>H>CXY+$iR3J|JZJU@>VgF;Y0xTF1*33m1-<&uw&fisFdabER?mE zYi;LsTstz8dE3JWOUIGr`ki%;ILt;%)F&{N*!61fAMivEwoA2!r&*>eJja%!WB?9s zG=_{NW(s5L_%?yJL^NmGOb0#9IW&DP0Ntq>oAY8{-7BlgS2Z zXRM;wN<Q)P0!*m=w(BBCs`C%-sj{6T1N&TeusjHM$3ziJmtW65l;2NGUYw2ELKt zLRewnBz|L92$V~1!#F7NPC&9&+yo_oTe2D=m_@b#OKiL%SvLA;wPEWQ8AHBHkCbA=z^PelwA-+aa<0(c$A@S3 z3|e<+tX$!<_)Q>M)an>9@84U6nP&THSz#X$lA-bw(~l!9s4_Pd-VZp?(<>Y zUCynRs6Bd&Ocp}y=Cq#Yc^!(W((q)=bd9Iwa+D0fevQVEfy2CEJQ;V$drP!vy+&`^ zqa3n!zR~8GkF0#fc+6bJr+MEQ=dzI)mVtc`%7D>|9h%kgm^-7KP&4cCWUVe9zcI`| zpB1(QTb?!a++aCAaD&(jj-1l9zzq5r`;Bn|dBJ20wKGmiyrBQmZ0f$m0yKEY-={eH z5Up${Afi_+)o$R$=pH;GdLC&Kd~1b2gG44U-NH8^T&VPEE#K!jKg*@n3SRNF88_p3 zumT@r{716|m<)I*@q0{1e#5w#b_=$oS`)b1rM2|9ckgc-+tbyK`SvTAJ@1ojjB#b1 z?3k*L{KZq1SSZ#~yB<4PiHl9a*hV=^=ij|YdC(ro&|0J2!uI5m6#-0d}t=yaH)6ou?zlotUrpn$nZne@5y#{`+Pc8^O(!}BQd7k7t{wiF* zEQA#t8_Sg*0T)uMBTs~usN~heu(n%RkC+GT6LicS>pTHbWrv$&F6A$}$2 zs!(R{LwgVLTeX87`5an$p35;`kvH+R9j%z>2xo@gL5-ZBb_;kw{UXjxPhK(3e__{$ z;M9#T!jo*FJ?T|(<{8hwg^L{XBQe@Xx05(hKCQ;Vm10&fQ<+EZ z#KuS}$;k?4IrA{>{3jQW#&|A^j9ii33Ne^_k0ZB*G8jAM_u-)y#uhCAkA{2N#H?0P z60fy{6qtLp(~fUlJ66he;YIQ?a9hl3k^PEg)58~;F4Kj3;_g;13r~7yJK$1dtcc@1 zL*%&~`su1C8&hVy;`DWTn-AMs#vTHr7->qUaEO6Sy+!4$3>1Nd=SwirFVYXE#Lw5 zi+CwLdBr&YZC)ROoi^qLbFYQ=q*uk6XUqo{E^^F|#Aq+rPU?&+O4oExW(PB^F+SV* zv>KaLidn-?8*IT zzjmu(iq>x8jKCIju;H!}azmGeu!8qtxzhWA&uew$E{Kt<_#o}=Yqzi-F%ROw9Gk>? z()axsa|M=2EpIyR9otM^Bh1gy!rT~oAL7=MPIlxNXz96^QE%c9J6bW%5gtcej`P!Q z0S~C(E5iOOJ$c1A|5aZfg6B0315dJr+A~@aXPz-1Sh&bBKN6$;OgpJF)+OE4JvkE0 zw8oum=hJAsP$^~;GnIMdEQXO(l9Nr$a^_*$`A=Rw8iTnPJ~HE!ii`_+BgJg=-qy;u zEDh&+%Q=4`9^+)=TLQnL@-3sxEceoIdvDRM@^^u=-tt&{%L4cPop_a81!+hfRjfGYG32<5` zK8AL{WVJlYIC}VjuSw(^Fms%~UauBsJJs%mGT+_o@P*ojeNZeL_lvdJ^kLV;p3qOb z%>?i&{xZuMmfgr3tV<1(k~Qe?6&c%;6xvqo2KkjH9rfD9Q^lVQx@cQj6Sf2L=w{c# ztyp>6Z5mk<_-NF#J@DL?SQR{5BXQ(SF>Y;NQt=-BT4Ganv9C*U;7bc^3ZDOxEN%t9 z!K8$5?c#TdaUz&sjJ*jmclKazGB12@ctpm&AEm|FdSJ0)DOte`h?MTEyeo_p(%0+N;??gL_MJca z>1GF}p<#y*+lBRPvthn{;LnWpFV%@m0^{0kCQxQPp*dr+8|lEJ)UYO5gAQLxDZM8s zwj#4~)a$4hhgQXh47zBE@Eh3@wgd9$X4k@kSb5uR8d(tdXw+N(jj$wmwnpN}gQEX! z$4>Dc{aRv2xbLeR2mZ6bj(}Au$>K`j+fF@OL>RLqm|sdVxeUxr=7sOFj>y;%gFLWj zhhKyt?-3+?al=w(_~Hjxk!y+)=F&0v5Tpg-&TyhvW()!H0sEkL=fXYWu$u$rHJkZ^ zMG3JH8?r!55y`|i;z4{!Zx5UgS%p7xA%6eNve2jbOdEI*;3%ThPIkadwLA#@bk)NS zwv&d8JwlGt*Xz~dY^U1&Gv>RS9lo~Ouy=;-LcXWXh8VgJJXyDEhJM;@CeR0I#DMHZ z-e5&)n2xMLhwp*dJ{Id&tAp4F@*hn)>b3Kuir*M?(YCT5;v3LAEi8%IwQwI+-gcWt zRs%j7^~TC$l_T;dW-{YD_??rqAmOVLj_1G(h`uUH8@=Bc;Wy@oJA3oPh28?L zMaL7Is{F>HtH*-XiS^pmn;S0SN_y9Tg+Tke{T%qtq1%^%kbnDwr@$M4`5lqGc58$? zphWEE@IRRbC>yfB5Wnkcm>c?=lzRWV54i!HotSQQvc+8|j= zVn+gSm|E4{TdSiqsoLM}GS<(b#uV)sOF)x3BF%b5327WMjqYHL(&#;y_})-wx?*Dm z_|NXl4&%s$!k<$dafZ5pI1P=#!iW3|31|*p@;m%JXIlI%kl*2NY2YmXlWDMS1liwl z)rdF`BQC$=spS}P_A_7OL%O6lDp^hz-*)G61Wpj%K{;@bD-S*LK<@avn}2(w_AJ%9 ze%fERh9vZtzYvG+GkRL1MYy9ETOGIfDhYb~dsCj}X*33Yd$EO=Gy2D0qS6M*S`vSS z!joFn-CL`plo)ZWmEGZ;4*hM86-115Gui;@nFg?#!p#M<3st zk95t(3hHZj_SZMYlMD4%x{Wyd+Y}?t{$_=R5BYlpfAhjieuv+Brp1WM@9-A~{u=c^ znFfEcLH2j#$`kxGj1iaL@ibqIIQyBeY#<#*9R3#af1^sWoGiZW&gGc@Rj9ul;nDtr zWa$4si|?jbs<-Q>U%|oe%Ga0g_02BEhCBV;g>r4mntxRlCB)co<)E*MLHb^7;br@~ z2fweS4U)Aaepkb{TGicKtD`g+ajccyVNbaJg2B28MqEL{UncoqHyCjRiM_zOV8n$x z^Xc~C#`jjsbkoKP>T7rQHwwm+3-!0mRPRT73HEmKN{9IEimN}Iuxo&~v*T-G*vH=< z{odp-D_m22GC-q*G7>|#0jZ^_Xs4!fW4Pwr!ju$CEGho#{?!~Wr< zQJW&A+DO(+nOVvNA=;vYUOD$I*92)*0@v<&$*T3qeaUa*8Yu(&%$I{c(~frV)q`z? zKlW>gAJ?Sy_XyMb@Z1;Vj58Dd9+<$D ztzpK=?=ds2VMgM0h!`*aAk*Jm)~P%vbc$R8J8Zy%v6vuGaX8PGd(nSFXDeQ zIfR+1AY*2NeR62wUc@6kN?4FFGnxIOOOG0sr zHewscsa?wH6Qr47+n^1rb=je*Qdv@z3$_jMzOH>|7t6_;l0#SA>wdl}xtlG*T4rb+ zmWKBX`}Zb|+7v0(MzUtg%u*%@(H0%_%DHd3CP=FixOUG=R;^F&PF{*@qzvpcUk>_A zJKDil54IKl*smdeTsyVB>Xz6TVf&yDU4Bh^*r1MkY5P))lycKJPKCX4$*eplm77F^lY+}{A-4H|w2G`%|6m%u)? zi|2b_3&Mz!1DoqZW*iE0?U9GBXcgv)C3L^eh*1V%h|0RRY)UdC98v;DGRU z3G=3XCN{0I|H27F;S?{>(|xi=K2dwYOt!!_C43xvBDi19L@*>Kf;A^98q zJ&1|kT!dON%c5n3W={_UTg0;%K{B3KkX@LjJm=)u*63b#-k6_0s^V1X` zuxYR@!~xCMq0+%&V*Pq;zY?1c4mZUM2N9#oE|$?IK&_XtP73c!8?Y&{RkOa|h)sdj zI_OzGqGaOUB6el!dM(jXC*<1I&M;uWr=d&*+&Y$(+P;igfyTv z3Vu4g3anSgqv*v-oQhdz1Q$-3pQiYLJA-W@4oJNYaSrY!j<47DD{<%G-llkA9O7@; z#WLCii1aenN#Wh;tLjlHaZ|It--tVb!#e0$J|bXZ+9EDx>Uu4aP$yK{)z0u;z@DK@ z1xz}YmD;|HM}fJWmnJ>2p71EftGma@1`_o@5$jMWo`L)xPl{O!eHNvFIFAqVv{@%d zo2MW0MC(Pk6Rn)1&f+~$CVB-w9To+~D`QRcVkHJ8MlIt(?ipx~512C87UJ9qt!2+d zFj(Vn)b=YeJew!s<(4eOL)&?J%(NMEN?aD zw08|^wM03c5NB6Ae;R73J{}`FTsxLkHI@WE_UE6XcEs4cd_HV9>V*f1HN!ndHc+Pj zNdzc_iDw|c$CG01B4k;VFzXywRGIV^jw7l(HBQj(14#!Y`He@F$x*C88(z{nX@Yf9 zBEj@1)>lgRTnFVUDpx?kAXuu>J=Q%jnhZ&<9_A`&kW$>INB6+1LZ!#(G(Rz-#QL7a zRp|yuUxg~U>ew6~5M;28_00&fxME9GHi{&PK!sj}G;>6G0+1z^}F+wf0wN0#t206VCqa16VJ|wQh zvtXDKPexJF8Ya>cVdBX$pZ2UFhU=I7$+TMajPLREnzdFtll#gtW}6&Ae&f+(atP%! zoTRnS1Z$r}faxKunUwCiw#k)Iu456Cgt#&OKE9xj|f_N*a>Ym)rQv|7oG@A34SwN@~byUQ|Wo0AAd zo-AJri4mmV^^U$9L6Q720cMpE6v@@W=JGer46{z z)*uVA-bMGP{6VkX{)QoO=3i0Dzi&ZAiP^?{+n0HOB{AFBPsF;_Nz69+J!TiP)+%K9 zPKB2j9{_qhwNHCZftXJudMxj;4SQpL{80X zn6hiehh1#)oxsFj4!0LqC2faxEr!g{)?91F-vJi3AxL0G!;bi?&O#!^7)9c_F+X^& zEW~qFlC={S&!O#zg&r6b&#Xh0CFwpq-Ijg<_TfIc5A)9n#FOQnv?z&TwFTg>a1Sg6 z?1#EIBw3WSzjg^Jh$fdD+Cw{;r?R|EeS{>5IAeJge*P6%5}|3G?|33htfDroQ*s|@ z6KZVM69v~H$2MITRH-#jVkse{s`W>$f@*&){+_ShH!ELlsM7XXH=xF5eP&2G#*o5+ zb(j(;($J(wmBfxQuEcX=$Pmw!g?O$?vi7^;IkbJR&;ym?nRPg`B;Adt+tTAnkM5JZ zG5?%Ego*hFD~0JgPiNyB$|#kgHlDc_?r9UwV_UQ@Nup+!auJr8cW~;x4~fN&-)U_4 z>)ZTQ;mLo+nq0-~jx}N1u%gKowk90etWSZ9>#$^-t_#kjkM4HO7*f8PZIBn&RvU0+ zvmS4mkA*VJXUW_8WP~)Aoj=S!=67jCE}n7*JyPQ-b!f9B<>#C6^fNr^ISySK19P4p zCtk*Dl=1xo+XY*Et24n{okER#i!rm#P zRo9L(`3@-e(-K)Vp-gYrgfdlDtYSKZs1;(aYWjV_y#U0<&+ zuT(ba5^G!Ta}D^BWsYUPHmUG<<4HV&hDSZ3ByNl`B~~oaf_S2>RCvA%5@wm>h$@S3 zVV;U-e4kpVk}Hlx!F4N`3Es&Z-SawC8|POl`EF^va;bMs<2|r=w~lXC)}Y3$LDz3U zlTA9?-&uns2euEi8f8kC7%FPo`KrC7-R(RAbo*PQy|@`xeBlRIlMUbD_4@K^Ws@$k zwjF1k@^~Z2MU*>6jm_s|;|zc7B*y$>NOFGP?gkurdY&w}l&@SSEAjnm?Rh}SQP=^y z=xm^X9Q#MZJ@i;D^vH46trmKD&S4+4yJ5%eCHdsVlEh`iZB_4F>Wm#Q$RqDohMld& z9j75a-tp>p%=$Qg{*c-VX&h$K`8g_2wQh0*FJ?I3r;ICooX3!SNy-$f% zDwLrfM+%R%=~=(3l}qBZCY`T;)mxL)bb-Nc>W9Ww!@fV+E&pCXAQW?J7UXo8u?pEk@kOr4FFll}HSX!O1%)~-;7daNnT)~4rf;HvdZ zV!0-ruRqhWjN$!8VZ(0vc1)={cEg{st;dflhuTSN#co~G`mYIx%FgOmxxU$^M}L*D zXIcKnVo{(^!}EClt(7nfPr)-5N0CWXLxt9 zzaH+P$2uiuAzPW`w^?F&&S5B&%2pKZ%PzoFUY>VrpnheOYoNt-Prt(HL#<3=MaKML z#l=D|W5veOV@26QtmqWvRrtqFi*@WXbfMCZu5=RL)$78J*y2vr_V{=Qt>1y`g~z8!a}j;ae+`kJt8Z0j+m%At1BIYp^!H`hLHMYGP1?eBdR@v8jP>(O6^{V;wl z1jka6i~DL%G+qTlwf4mGZso%BUy!%$+f#vAXNSu;C4RBD9oOl08vgcS>X7PGj#w}0 zO|H1>=q0^rhft?nIB6?wm~jziI3-6bZ(b$}i_z*#Nj}-pDpql7Q7O?!9b#oY%V>4B zF;0jy*huE7F}_X9C;T+&unIYXMyuj@HjYrMM?poI>_WW4xyuk>9s2Z0txaEtKz;f; zeS9Kq8KHL5H6zpDquI`vGe@DdXj6!}t6e=Rm7jY3sg6o9?j07L$}uVCoHchH!o1@! z=#&d5ZM_Zi48jcOp+pId`Ev!c|ybdlfE+19cj$$6|H()9dRn zW}7}fZ?=pzH>O%A9iHBoJL1AxxsMAQNH52y><+pX`Ve2)7nl5<%3VPSz9iPT@Vy*~U1b%3vco z%kvmTnN2#ZNRA-NsyLpkD&*;rOi{M4w?-%a>@pNrhcG?bYSY)DNT0q=PXt%{Zy(MHhIo#cw!F9SKOMF)iUqA+92ZY_n z4GrlfuO2UBzH~L=?43! zN6=)yCC*S+WFwi#q8ece4b|zm5>{N%0;Su68`dHuy^`5o33oYQ(71XsdNI zr}zpczWjqPm|zycDrO&-zaMO3mccdHOxAISYXHQUIW*ai?u*E>zmIF1rw_862lA>$ zkK8j^ryE>7J%S?pEpdjXA{)s>6w1GhEQNyVbX?;qu2pi6W;u?ChuWG(X4TAQ5ScNxAz$!jC|X1hDx5#$WkCIfD)!jRcY)`B6iDp$gd zryGt0jsiBS!;A60*781E;jJ>BB*tit%kaKsY$KW2r3y28%u=T-<4M|=<0$jQ*nlVP zwAM&YuH?Sw7*Ar~a|31+)~d&w)O&nag&T?a+Vr#uD~!dHfebUY>e;UW6V_o&`5MPo zI55YS6|0%@Ii6h=u2dfNxHG*HU3qqN&9jfWum))LTMAr$$svGj!`J~ zLBmE!Y!rnmzFF^4VHMisT40-=Hg%|zx^_MLH6Y2v_IZwe?L@xDu@!n`8!&AZLY2?) z?5fbH@~B6v>3)n~+sAsczWdp>-xm8LB8*yvSF_Em1+QYouY^%gH_Qsm1{_w0O}Uq{ zPA6x^wA*I`$WkE*aMb!D7d6-OJqKN(?HI~_KXldHKeI>xTp7mYM^ z*j4e(dg5bYv(de`#Di^m+Jx21;`^V}wd>h;0e{xvS@{~rR+y7@V_J_pbD4XQO*fuh z6~2|vdW@T1jqwY_)csuBlWqSkc2gL_y{wl4<@WGXnD=x?x>Lg#-NyHRwKQ27v>@Fw zt}V-_`g-XMuuvV&B^ImK2?rK2tznfC&Mn6Ydl4gsv>~n<@5)D$u83`AUyS2=YQVax zTab5_a4t@jGK|=aWt*_D_ygh0QLKwoT&-=J_4Qa+>D%>P@T>PVj%^!;^*-C}+OaIw zf$<(L?W|u5=Njof&2QXkxQlrIwRk>JQEq#W6sIt^;M`brPnfQ0H$ zE|FNhPAIU5Xbq{9P;NO+$cqRuqz!S^XjeX(bVXz<`(hl|U<0yM-GaQcgmQ5Xl_A7t zB-@09#U2P{jv`&0^lEL}tglD1O5d*Uf?mC^actWVtoPY&*N$X~rt6TdP$YI+yAy8P zzQS^%PdI8rxj3o01?482gBFxq#$U;ss%}BvS;DwD|Jd+hGmdS-!Qu{BX69AKxi}5h+Pzs{ zk7Jd-UEc+}dSBz%w&7Rrv)!&8#}Z4|;at7LdFofe|8I&nOXK~%_!?(PCMG+T-!z>D#;eAx#1Hj4;=CdzmHc4isxYh1 zt4SAQS8pH3sV*MB`nm;qv4m%5+?pwUG2im}x_(WyPOmcfb<1p$crWHI^$MH>5KW6S84s~X`NnW@GGRF*tMI!9-AtCyS@t^^}fcj zZNsGAXPcecu@0L~qV4N+*?#}6&Qle)X7^4w+&bmL>FABrNxYS+GmkJIx&6Z_&OM6v z3*&1mC7C$wRDLsc8rZEGrxHKZ>xkcqSX1(YjjO_ zb-4sV5X1z55JHGV2uY|Db&=CmozT=HHd8}C5~dejNiXvi2)dcg6tuF6WKfe$foLAQ zMKkoKDQ!}_*>pW6P3~r3$YuWpwRjKx8Sy^fbIja+xcR*|B&AZy(&C2OIc9EVZa+NE z_iM+@WBOd@&k}KIe6}RttZ?daSO-;JD!29}j_agL@m`0x7COhN`^7KgP``h7{65$- z=5?90yYOesb5}ZV$0ZzkK-(|H4SycL-pTvO)??qbbC8A~Z!Xxoz#8lyhh3`=R!2YZ z{i@@I`YPho6t2Gkb8n>+k0M^p!#QRh3%sgReiBygUexj#7%2Z5%-eQ`FTT6BCfUZT z%W(&GUx`;axyJCTa7tQNb*!;Vm)~1s(tYD1Hf6uyRFA0hJo5F|_-u|>Ke`lF^?OxM zJ=Yw|#%Er_s{6(-VbOU1?)csKGv#%ewA(Rg%5!@<=M0wd=&tzgPvIEeX&+;r9h@35 zYYNw2i{ae_Mvd4t59j!GEU>Fi0!p~GdtS?D;Gz7xF>l*RzIg1~T4Wo$F2^0%eI<6~ zbQ{C6!YpY$)p5rzU4CzkPxp2^T^j- zy9w+XacmyWvFupjSe+J>Fl_g*me0UO+A|x^?|s|O^JQJr)*#zBb~*0A?kjODC*T;a z6@E!;u#Q1?>GFGPth#Sp#H>eIJ)+L@$k$urvpJ6a=u#Ng?^QkZTyu;YpLq$x?i;^^ zQRDr)<9FlIl-Fg_ZpWr6&+X})OIXIKyW+b)hkG#m`mW+w#!hwW(&pm+O6U3NclHB4 z)3?9SnM;mjU*>D3H&=eLZ(M(^q!;>*siWCG3wJN@Ak0u~Db-&vtz}kpUhoa+@9c}F zctbBD*zIvb7YQjw#Ht;zZM{|;OT@dT?76PIzecll{=4EMg56a0O8WTxj$~O)5AD%L zwnF^+I_DC4bu_j;-gol0(5+T=)kfRm+Nf3K#vQ_OCD{AH&j-%qQ~t`U-aQ-ZpuUau z*q&|~rKY@=)2OZVbJSaV2ut1;fBT(6u%$0?{QAuBXD{=mQ{F~)-fvuet)%Dr#;N1i zJ_~ox@fFN)Ybn)VH?8HHm+xY2;aSD|j@=fXjbE497T3n9 zDc{}k+p%e^gZehsLwO1t_J$AD=8n_VpN>ymr*wUGepAY88M~^j^o#kJk9WvJ$L%c`>V(qhV_Ykke45^k< z{gs(ozDb_EjUL{buF8uQ-`Dij89ba9QR?Y^^Kr7Xr*Z zgzNlw#YdF7sp^&V@%bICvXUO!qm5pL>hbP1WZFWgj<~kR`_9}Jg4K$y+GAT>8=0!y zxZ^jj0((FB7i;qPl)v&4%FDkp6YHS9jrG``ZW)QDyq43bt@Lwb>z>+iC#}lYZ#kZ? zEWJZ0wfqLnIK!v+I1%_3zYqA<%nkcpzqr2A@8-qn>K4D-7ssnR+3()P;p#rW4=*;W z2ifmO7vt)q#_vxq`qfV^@%xiT9?JJeSy?Lg!%&ZVS>066+o9e!!*lf0S5k+&yjv%% zTCcS74xUz8NjHV#w`QV8@KtbQj#2sg*Z-T4=~*qkzll-czj}O$CnHW(oMVzXhV{4_n+`98hfBOFU77zbJi04B;nFhSq>s)++vD%NRd|)T zob7$>p1J>W|BQ8mEUdk`uZC6QS;5gam^Tv`R9~xc^vav*zbBN+o2itZ%O1Hj8tO|b6$yJM3*5gZT88NHk9G}ebtjE86? zZ|th_m$9p3x?Si)ia#LTvErY6hhv4KmvOAh*d4zct492odm1N9ILE18?lO)ots-Q6rA*qer0uU}jPxn8xO(CfR6 zVEsA947Gk22$uIR2oy95dW&CBJ#FYtr2GGb4!al8hxn+e(>cgmp^XT)mN^B-@bBR& zg>=UU{8R#5qvepxsvYZ6hMD=owXSo`i+hwwdxb0ECX#j3sdJBnZv3oZO60y8c{;M) zrf;FwmQE-pq2gVLbys{su4TSSUm{wyRcZ@Ixzau0*Z*7p;z!{c>$r?={TY_x=}$wy zEp)prPWA3V&CBR@dwiL<(nj=LuCl5ewQVeC-*heHTIbbD@!~L_v)g5wx`G1!1YaVu==7?pP_vYiAWMS7)Vrz^{MWKmSp<#yTz|S$~G5c>0s) zjOZ_}fT}&lN894G5-POn2)VTiyFI?lTWMq2@+_xPikO-C^{5Q@AlEzOz+383!m@c7T>E$3gk_ocmCUT0 z{D*n_OyPZ3WOpH3-cle`P%5Y{ew~%K)J(@)%2riDQxBtGF|}>zbPlpsbR+VuWlq8| zT+CZ)3h9my_{DYGT2}2?$1*IAO0v@V;z@KuRp_5yv{gM z-CL-4Tb!Op%)m0L-5y`&t+WwUm#eZWC)IENbS*?%=haH%?YRZjK2#mH#ox~{?@zkB zbGg6ETME5j%M*)BDdK0|Qg_R64~o4*4!ouACS03`eFZt$GM4d`cQguGvXO;{YBi-tsybycMO{yH2hYv>aY>)md z9{hH_7^9uz-LI7{?L(ex$oDxY(Y|w(hlQ-sF0O{}insQqz7g$5r0y)i&e&h;2^V<+ zT4Gy#Y^hZGA_oxKbdQw37N?+Js@I$g=pNBWd!=(fjJDr)_h@@8e2(FAcKSUk!(s;> z7)KB|1Xin=LCz<57<^BE+x^VjLh6a>A2IjCl~jM9*a6-lh0L3@Uh5peluHNs8xAvvJ zk&%dOLRh0$?%QI2r6*kEGH8iy@v)^+>5Du?XwyAX{z|-ueyLt_z5-0YXFB(z@b&uI zC%wD7Jr>Ss@v1RDYBh4BjDy z%$u}eFvNT)zsP7Dtd1|uXdD@P5m}7;7YCEK`HwEvc!Q^3%j;aa*XMbj%2K)R^?p#* z?UoGh!R@?T z%aoi)@A>RVH9S+B#+DXOAxS(@)4SkOX?yQKo{?N_B*h)wJ~nygXN)Lnuy@KA19g+f zf%;Q5cRodPB;>U#1zrff^|W&klrA(p(~+B{x_m-Y&GsTqjmWtzu{r#H?%NlE~Xwo2~J7 zw-B%K4$pf--QuEDmPW1a-WCr+rhZ`l=fcH-wXjyx-P>c;{)Vu*mt5>_>OnrR7}krs zQh3{N2DnhZYG8F$Eff-aqS|SsY)irO@w|iGEha^bll4(kudy&bSHBq7h1W$XtK`6) zkfVDA#I|v60hIj@`=zocQH#(s^n`kezV`kw@074NU}p;2S9P#FtYykQ2EFIABh~Oo zZK|@YJaUsHma6HeajCSu=W5)GYN}jo_Jt8e4fam?e4uW&Yo7j8%{@QOk^Yh8wJQaF z8ol+j`~Fk&7t;jAA_+unQZUzS7dX9(1*Z=h|wu!XLaN!(SyX- zfze5FWOKyCC_ZO#a$RI^Tqj1TWNzkqooS~$M}E%O$b;(bjmoTX?$@>+!HR%r7QPWpV$` zcD+fy%`gs0L~!-Mu|l?Qi&=d&$M_rXJ)2hhn|BD}}d>eVh4L+@YV9!HFp^-)u=u`oVVzZlboUrH&f|zjJrb6tu7EV0l=}I4ctR9Y(_P*^vs=owcJqiLpcr%+ICL z_TGQqJ~omfW8-z?neQ^9sKMST9}bLWd9A2FRdeT(G)F>SyHeoE(pyhE-$LoDuAw70 zOLar-D267Jtgr8CJl{~i!w)g*)>#H-zyzj15l*$HSXsrsF++SF`gbC5b~yVdbro zRR@(4nWUsKE{WPl?#Q^LOkS_>6{*gPkEa;adqARh%o3Nhu1+i%7Edbp_@Y}~=GQ*) zV5rL5mrFW?KYKk~l28(;)VQF?C*``xDaj{6oR0Vz_m&Z!PrAr03Bg%9t~tL{>m$b$ z^*1TiHFXXz&HO2dtR?o+%G`^|UGlIR`EFWtR>cWWM z5|`>2%TS$|M^oK%Ju@)yU1BDT3kc|4C>|z+T3U*|(|P+-reSt)dtjEFYtRlAG}K1D zv^!O(D|tD?6l*0`Ot~z_YwS3e=DdON9)W|QZpO%14faeon3q*xV2o$e-S$*??r3ak zOz65Rjarna|5In{{E1W_F<;Ku?Ml)8o0UC{FdIpp>v9a7h3+!`f<`aZvDg zwB;UY0@tMbt!Hgk{;Bw>(Bt^+6x`Mx>8O+Ohk1+I*sw9^`HJ=7IrmEG%!}oqI>ugi z?nx~Vr=(m%jm5dZ*zBL4w6Hhc`_$QUvYDbjm8>VpOL+M^;LB`YPQ9hv)o+%+&Dl_! z{ZfXui8IHp$GO2UEFr zSH`2I8pRl~X|a}^zG{qGDXU!4*z>5yIkQ%3BfF;O@%(*{5g6N^JL(4ghkGq7n=m7I z-T0NrwB@?Uw#ipSY?r)w+>1#}N76;sP1x4bam|^xS|8cBs24Y+y!j8uRZN@blk|?g zmA7y)$ltt{#JuFX$il_@l~&Kke>h{}YCS#fL*Q;({VmA{Iuwo6s0f$aQC`P*<@@#=XT_ z8RJJT%kdgJ&N4cq!kw`^a&oT9G<&8S%=;~HQ^r8*ZsV5lsM6Tfn9y}s8nr0xO-mbd zX6>x{m@k#?wl?3EqI+s(Pa`b0EB7qN`~&O@H8y7H*VI$2n6-n|f;OQo_ej%6Hc#v& zb+=jhGck9e$MLo*Sie2eQ77-+tVL~XSlf8M;=_2(y;Aa|a8s6p>KHgla@w@ zv3p_#E>9y^Pvdjd<9L5O%C&s)#aP6-j{LCr)u8j`yAS!Bno@l`)K|MlnV_ zW2_}7`Wn|*$|{#M_B^U_&N-Ia$UEwJJb&MxCMGr4*?+j#!aoX4!(|!s6gkLT7kNne z@EF7O^XOeB4k+m&A1Tyt>A2>cWUY_9WYmi_lLr_7;kb%}^n8-uSqbGW#-QUWZ!&RG zxi0dP@qVS%^YI_fILca2k9)aUKGq&T=f16RsgAJ>)rk>0)jikqAI{2Gn;B#I=Y3#& zIw?XHY_O*M)!Vqjw}IVIXy*iu%xq_O8vjdM1( z)J8^D&*S;~-bQiExo%kDY7&>EI z3%Q@-@Tywn5}ZTGji-i%G4C>YP!X1A+CK5(hkN5}xF;|;wX)8%Ys2wyIxKbb~=@>L7j zRUU)Zdj8#&(Blhfy5D8Iqp^lwH*!@=s%2i^58cW*Yo+v;bn9vDjMOmA7< zs{Ko(RU4?Ls-4(t)tB`GyL#R+*ZopD1E_U@I5sq5>3jFNgA1I;=|mwMUJVHxGfOWuSX%K6A7>&r97 zIvD>L!}NghTOSx6hnuTp;~(J^6)*m3F85O$-dHehF{L;TVy3 z1Ls^GPvP}47HW?)BYF7q=t;;&%&6KSvP|ahCf(&y`jd$RiY;VUa`V!Z+paX_H*5mO zrW!`p&3XZUNPmJ~Upl>e!DY{HtseM0dNp#7;`d%6t)4II7aKLO>2~$JW3Kz9?2I2X zo?i1buygbobz6#Kz8ZTKN0Y|*s9D_9l1pIc%Jjj@Zy}C1N?`8lySl&I7RUP^FTwOA z`0@sC!CI$!^~T~kBMC!3WAe(F#O6i4>XjJY6~dQG=}$(l zXgpdA*;O8j)_VSJA(g$Hru@24V4&0*k(1&|3qxj-gDXWTdN0lOKe{#Q|#AE zq*WVa{bJ7z*KAkMJLbAyN@tIZ#nwCx+!cLB-In5*ug2}f(WKciY8E%O;*vC~(&+Q_ z!8>pvjyFo+waQYY-4^E_h3QG~0S;b5+cls?LK(0aM?drrG{wvO<8f%E!F_Dvt#6f7;eLTO~G8Y=rm%u~(T~u~N2!Q7lPh zam1SJOv5w3C>bev6>?|vTXUXecN(?G#G{#KA~}oqVNazqOS@AT(=q3&f}f+YRO%t+ zYQ1Gl#N<(@-b25X+WpAFogt-i<5~5*&Rt4BW2hK|ThfVhn%bWGRSO%>9@evm9CxNP z_J`SHSN!3u=WU1eVvJ7O>YwSHJB<={C%>3$*D8cX8LQ~Z8ZAI{JQ(c=Yo6Wm7IablDI4gPU(D|bMc<}2r6m?iqq z7%NT(E+lbH{c}zd4p&lf597$Li7_%BXG7OLz*}gW3G0kP<_XOEeiJsFS6xW?cj9QC^BvR&)PcL5za_Vo%jOrFqK?! zL$?1=98%;`#9i!6Bc^C97$kYVacA_c3T)EOG-{EF5i;*Nau(ymj!5TscBe28rrw!| zjMK5)xZ8ZbjDZV$k?{eO*O!B(dL z_3WYKohj|fnLT#JXUlrt_D3(qzyiU+=!`s#68089pKHVjF@ue7kxy$(NsBJW_`Xhj zw>)*on;zoLV6`dryhDtSig^!utIRa21O1!TfzcYifi>6j7RyR9FIJP6^t^?g|M|a$ z^o?i2yoLWOHn4sV^;`PJ^S&pFN4{q}|Ikb$$3;!I{XZLrU0zRGAh4!A1B=>Ssa=7~ajH8OTF*e0??Kskt2XD=ST@k`J zMrKPK9v&5A6PSoay;{awCueddCd2rnT`5%GvEXdN{lU{|Iad2Gox6#ASO4d2oijHd z&=*4Oy?!t`nOre6wtG-aj&l;m7Jll+446 zoW+5#pV3*C-6_n=Ci68C17c%9^`NGK6LBrFq4LQyX-8`J$FXOcQX0eJ`Py^ZGtbx> z#-O#Ul|M@-{$*-c=V$cf-V6ThA;+C5d7i|_;8$clZ@ZfpV|2n-e^=-0X_T-t_~Be5 zUWOTLtcUzAV@g_ZImUNoQXfwp@}|vsGgxg(J?{`>g<{@=-YPSV>R^6hQymzs;d@_m zJ#VqBB=fX1c}aisC0LmJwDh;_tJe3c)7;ZnuH*S)Jq1B?sWuz!l*K?zp}*H z2zfe(;@pUs1^sm9)pmX2Vp7_qv;DD)9E|vzZ48XD6YwE({$)b)3vt4oe5WhpUkta; zbi%OWT0TnG29M%xLE6PQ$}?u6#jnKExpt+nXEOFh?18fz(&=2T9dYvPc?MoF%v|yw z)`r|~{xW-|Q`^D3z%J19zJt+fk!nP*IrFhkDwPTH_q-x6;w(zYXLky6a3%sf%AV2Y zL#2yf0gG1{OyfcLA7EFTV=bNK$jr0rO^wKz#=T%}D9%`n{+WlxszgrXs^*IEES1K- zel6}wev9;MC3F7P8)Y+UZX@5gQuYlt!g@r#CKh48 zR4Q}EFE|?!^4Xn2T!}qfWFCCXm%8}1uy}ku#0^blgy! zu>t)f4~v~(#v5mFRddDoib`W&%oYbEFFdgcyfdr>mpO&%jj{r~%@%w@`iIoa(6{jc zKhOC9;d!1bV+Z63@_N3WeH*L+4l-^)oI(uq#id0qKzLtX7A>AYNQk%Aj3WTY@6bnY zj^0mMvOJQ`_Ps4~0^$?4aR%;%00x}#0ewW43vu8tJT-DYz;NVDCrl;m|53U&aJ}jw zE}@0_J&y8(pqp4ep47A}g*}tyAT%p}#-1PC>wE^iU9bR3AL6vP=x+CuHoo_^GGB(h z(y8s>6+pX;2;Z4xC8ZkCYeM7urBaz7QO_&lbVv0epWP|Q!I3dz*Kn$j`BE3ZDi*IW zYKF@rKEI_Jw{(^xZ!^2z)QFsE+(Y4p;tW;ycRehW&m1@GeN}VCaDSz-uWXBd5XVT* zR&sau)+NyOyZpuNTj9&$H!nEnl{S9$v%7s)Pdv%nCH85ozytUs$QDjvbf84u*M?B9 z{l1jYX2|+FcjmGzcE{B5s@xKmzAU!BM}ET9`}jJ1ADCL}y4$=KglV_MZyK2Sa8c)o zP59C8%jFU~-!aU(gpmjNcP!xN5guR(Id6;eGn|E2OMe7M&1+wk-yXXYH;489{nG8g z!&cwjX}02F^Rd{1sKplur8iiCoW28l(GJ-4AZX_PL?l9t%HLqa5D3(k{vaZ zclB*;(%~DxJT_d;8#&0@ZNfXia?yUSTFy_P&ciRa>d%`A1Q~q**g+7muI$(p#@+|< zQ7c26*M3_{Xfu?2T^4mF%lhWc#2%A)RcwRK( zV%x9=zF)c>2-xboJIz*XOCDR}&0AoGeQ#^dRCrkPMaYt|{-_MouNCH%zecTtPbx4k zShK(kig z-D$SsRPxvwul5jnbZi-M?Cpk591$&MtIuVaey#AUd=_dQ+)#mE!M&C2XqLRIZ)iJ? zWgZ)Djwlvp3lDwX0hWvQz|``cE%^1?#f{aqi|ecF{BB;HuHI$*dNVx7^@}f7{|tH) zBBYfJDf&4OoMFJ1ogEvo$6^X$16wNo+f1R6(71|!br0)_$T4IX3ryOtQb=o^{VM+T zlpS&mX??U?&Hr}c+eW0zLYB_IIh6f9RBr#~f6dZ2RK*#qvih^It-wB$Hif2@UDLOu zh#EXMBq1oX#&Zq!`Kl!wx?$DOW0Pbp#OfqDd25BRwI&##_ja`qJM$g8=)BKWDSU zl|rLg=kc%ZVbx|8?eJk`)-ULQF{6Aeq!lH`O!({fFlMYAq?J~oT7TnrR7exPsG^Xi zp8Y*kZcqKM*CzGP>mf~9)?pWn1qHK02Ycn33 zBx@nofUw8BwL;ihlkyoedMXQP2ey@|y|$}T!ZMt!OU6tgaZj)^vykN(yfpK$S&;oB z8v|H@3fY)3D~d5=))iwVUV}YB`YFW!4U6SHGlk%hStp2pWrM(tBgc^0B|qo?$SQ@z z+cRduUr))H3HLBslzZcE{AL4pgs(R!WT|I=50%?r_+R_)XqHI#XLZet7E3&{#BkL%4QM3fO~9`tc76O$;n$QgsnBH4P!=6Wg&I|PJAdD}(Q^@iRK4p2>o~gfUV}Peq=!@0=T6Oq0>DOQIKZ@UYv9aHp zU1|Am!*?9@JGdB%-ytVCwSGsO(|?&?D-Y@F@?t&mx+xs%9iM~IZ#)jnuJJl}>lX5v zF*^faIpeEwymdLhk!zA!H$>i;4m>r5VRxS4MXcYq`r%FdZTCMn{DyByH-$k>rrUfi zUH>il7cwh_-**3_|Mv82^#7y#miiSkRut*GZx6oie{t|NapXY{&2&7c-FHYjewG(2 z6b)Vf?J;TieapVhDa?4VJQcU$*IQZLw~VQ#o-@#_j1$t1Uyr{Sl%D!yr!MI28Gq4z zy;lCDD`n=5m2>*_hWcB$q3ORp;r>&_bAKK_;=Z$>_prCohV(skJX4#{j!7QAT@yDF zca^w2Jn%efZ|PIG`xl<>>){ti7E?+8+&#(S3aNjR@+iYunNL!4y4SxS{DEpK$-F;<-N$A93GV(0kb1 zXhZs*I-aRbXvZWEkJrSF#9bvW4-Y(#+FSY*?*4|S`*e74WHFWWm)&_5S4jPN%A*Wt zWuB*Sci#Pn?mX4AuVms^{dsuaSva(b=~LMA3~j6YR<(xyIA;vT{yCq^-0>f~^Q^y3 zroWqd#)s8A8&4)Po|<}Q@Oc=Q=ZwoKY%|!_yIIXPo`4xr$jo3Yk%2E8X^pqP%M2@I zW-#*Bwcm4Q@HsR1y!+9H-*Cy@LLax^wDR$jo5U zcjpIJ`r+UTapb`ap6QsucHbfC_*q`e;D(OYAq_uosSsucTb_#B@awO*Y%NatwcdJ& z`ks(>yfO}h(j!}sHud(5Lw9AZ{C$LQZ{IKO~^mV!}*%Hk+`eG<>7(nQF}|D!uE7mhT+I!D(PxB_9?EA`Z48E zhO;uq6duM@$1w|cBhNgB=bVK@TbMqDhtXTFN1wvXPV`gTk8^n#Q@;+xm-2v|#=|g< zmCS6kc+!l_#wpydj*MBmjz`7m#VvmKws3Lc;I3a>gNM6nT-=?DW4@~Xs$cJV(`UpE zJacLFGsEAb0Ia?JP(T|S@K%~x`fXiq>BBP!>jXP9;05gRX!rB(DsTYv!tDGLZ2LMa z8(3IC`Va;kb2bhYiC?C0Ee&%!)7-u`GyW&p1vyVc=-LMwkKt>-C@To z+3j}5%4EN9#JXeK=Q^Hjvg5VVAlqFwY7EP!g-M=ldfEN84d2q$wtTXub6Y+SzgfFp z_IPc_w{^~+hIW{2d)j2O=V_}cd}R5`wsPxR_C4)xmUyY_&*{I(%BTM-SvJ1eg_*6- zSZHhEm*>|0T*rtEe0>TrZ+~Dmes|8ityudGCZ869{g15uT#mNAwGFoRF*dTbVP(TN zbXd05SD9T8tKZayekOnM@o?ha?~_a|DbL1Au;(|_qt?Nh`AG zN-JM@pw`v4eP(NCw*B{K=bv2JOm==5?k78+=bnXS+lwO?7Et);igWGa>{&0G?0nWs zu-=oUPyaOJWbL!wQCR!*Iy`*N+1E0oE7QFbc)1&83>8XQdUfcFq*}Pja+g^5esd3*P*;sD*yL})0VP^KcTid*K%<7JoStkp+ zVbL$?@s6o1`&#z=dGoxt^n=Gx&e*rDdRfaQkKeNH1Nxb)`|rZi$1}@p{My!MJhb(O zB^Xt9okmqt+dtu5WfniD(`!o~o0~bC zS^BcVOO5;Y$QQzKfLr^)CuU~xyX{STn+>kJ}eRimu+3?YWhgoZuv3u-bzZbI7)^`6~6IuoPTfRKe_78qBGy9)BvC6Hz zGgQ#Yl5SZ0OM0xv%IdFWFXuf97&C@Hg-gpnM+9@rKOmvW@_!vDU;r`56DM-~T_xjJ zlwp8U>c?pJGv2cBwO7CZ@8;gQ4ln$K0#aCK66Oe?l<@eQyz}$$%X#XPH`eZezxZHQ zFSVGg{S=l(Tw3~Yu`>-hRhGUy;A>rJ@#jdu?QY&JRue}Da~;fZ!|v}V&wT!KSiF01 z%WJ>3^K045cyIUT-Z?4!Zrc4e9vXIkuN@yV8z1D^(B+nX3N!K_F*7E+KZSMrUT*W3 z5_5I($aS6mlAXJHG1WfHx~4OZ@7H)JACwIIh&_T7MAr}TJvz=GYvUa z_PxB_Yh7v8Z@2JUR{v4cw(ln|d_!u~!)-x%=-0Mr ze$NhvWX~TK;oO=};msADbD`e{dwAmi5KhJT;6he?vhGiQU(1}oHd**%7UnFt$-3`~ z@V&)-xZSthgT?H7a>*Vg>Hp=(+5E!~oZk0n0s zx6Q5mZTh(1wzle9dJsd#@DH~CmbI+lhcEfPr7sVxoYl|6ZE=(S^8nD z?QQLkSo=DWg1KTA|4BFjEy;am^^aMY)6*u4zbnGb<%J!&-DB>-X7}B>%->++kB?KB zc&v5G=PF+nx0<6YfAHGEUpI6u3%{jXvih|fHSWC4E&px$xbwEQ^jmtE*^K8OY$tB= z_mTYG`j=N$&h6*nx6Ass{&U9H)_m4NeN8KWNzYiDS^8nD&Gd}fjNMP+5d?sBJfVOS zoe@&VeTD?aEZlPvbwmL=l{$r)%kj(8)!y9hoOIanzE79=8|?n^JcWtJa%S!t1~7k= zOqS1hZS}7kx|Y@7(k&qY?Msb&aGA@L>3eXu>Ej;U+WK$l<)34|UX%kRliK?hwtjVJT!ADcSP#hH2c2k-UFyZ^nXf9~^>hj)AW zTf3Wu&VK~Ey7zzX*}oq;2;*Vzb%4vF1J)A4X+04S99J^mDu(sXF+wdzoNzc{1ZMN2 z@O#XR4w&FDTo25?X9fps;P!r$<xR5xRIfPv-AG zHg%knGxPg@zXt#>5B!I8BGIe+0$ID8oo+T>*yju0%rL=z7$J;@CU$ro7=b*(={?M7 z^9nkyWWIaLdO(g8YB{2X!wD%co9~9-IWsySgTrt=Fe{%K91wzA{ZW>udyW!hP3Q94 ziW2(lq@EllEbIFQGRTob7f=0JZPZ?ttw` z!TWi6?O-N(So&G*g0TlrdG&c7h14>o7NGQ5W$ju2@x#m z%l!e#Yl!cY7V8K#PB;jsdNRLk*T6WFb@nH-l-SWyczFGYS zKiSiB-5<_=VQVY5&e$6qJ$Mo>*o76W-zMC!ga?-NOKS#8`lYo5zR+2$9n>h{Ae`~Z z{4&p?aVF~|P-ZEyqowfh`YyAW-}-KMPKe*yPyw-A$1Gf;hSB_q`e|+94+Ax5?`lK{ zdAN-VqIZ*xHP$$EE+rOlKni0s{C*#?p97kZg0)lFO`!8rBRW_@3f3>vdP0o~YPm)Q zwVbaZC$9yf8# zV93ILTE9*TIwCKf+@Hd}P9qed6Y{TxHi`1}aH8kU?5dMIZ-)~-Z<9Wr=6SCSPp5XG z&%%CUC->XQox19zA@aBD4fBsr`+TVnR(LK)E8d>Nto0-A=edLBZqI!wD?YTfS?+n5 zXV+ejg3}NER_Jc{I54!ps!GuD+6&)@AXQ)iOjrud@EdupLzVWbXOZp{&#mze z@0ZBj4_VmH(AP;mN93jR^i#OBysw2eiPrh`a02Je?5dMDZ-)~&Z<9WrzIm?GAo92C4fAS&&xH{eI-ov;b2)I7{v6I-l8)yN7PLM0rL5`D)@DKH zVV-^Fl@t1{VB2r_I4Hbs!Z;{&j2rX9iPz&#*RN&?Zs79!|`>nO${K=51uZGH397qHl(K+?$1` zQ!&wJVLuU*`|V^*UFAzp{&u|)PRY48UE{u3tG8LNN2KRDgC%Rvxz(x-ZE2Ql9_HC5 zA9?Q&=e%!Nzq%)k_os*HciZ(Eug6yo7HroZ+jYfPs@#(Oq1mq|7kp*$6*%sL9ky%t zY`w#Ftu57B-o$@q1PBm)FkQQl7q5HF?Jza~@-D=^pdds8_Re5B%F!(^>Wh ze4EP8uGhD%>aZ+cwYT&soGf4Yx(iK2OE}1r)!UZc0uK3_H5|jTh(kThDpr_pab=dU z)xVNo%`y({Y!)lLga+Z(Rzo~T-(UCyo==?ux;YEtKx|`>jj-W$+oc6R&N*;r$`lPdt70@ z&6U~UR{u(VHJd!Nv)RhVM22%~LjRaup7qxxhkh<>@vQ%6`Y?_Pn>-s|Gd*RRT^hz| z2=j%~SKzr1GG@EQa(8#vJM8t^IrOPe) zJiM=Uf8gJ~n$FTZ;M-N8S*&S&`>GDhx>tKkpTfz)*G^a=sA%yAd9w7|vRnNjUqb*f zoYljSfa-K+C_werNjbW{92)TUbiO-le?tT<+56GaKRGw(ZRZ}D|o?VP=Z1suO( z%l&R~7aTX|*#~PLA3@tui#1gE&A&$mj#PLPS^FJ27aaI)JLkP4Pi~eq_fG9^ zmbZQRw$jz8AhXglRPeSSgdsi$7Ioy$g&MY_gpy02d%?U*C}H#U(WOvAH+}Jz*$6_2 zDB&=mg`><@{s4b@-#g1}4!$pZJ+@%d<%c2Pm_F>O<&^tiwKJP-PpvHW+-LqhDsY4l zQNf`z(1BGqHacV2bGALQ^JZ0Bn>*7!nAL4x!JUwS>uN-hS?C!ecw5lH^c+~zk+m0M z*p3j63I>1f1M@B+gw3Z%mqG~L^z~c!jEM;0Fd&7a%uoIRUwPj>%WMvwFMLV%iacSW zrwi*Ql`P-y+&>;Gs*v9!0!IfCB^){r?rTd$xaRodnt&{7S59W=053Au5&k%M%s;?;-o5C5D8BLs7bgWe=$@7G zkfDJw3#U^wZ|M}y-)#&X)Te0PGYd8%1X;5&oTG%XKn(KpPP=G3&f6*F5#IRyx&gfW z4s>w98bkDoDZ^R|*#{wXI{h=O8w|~GS~qx82w|$n!WzPMgb> zEnx{E9I(a^{c*~$)annfupK3Yc5wSELk2%2lrVmJ zQlNzHZYd8LDj2hHJZDFq^4ev)F_gfJF3#LxMq`Oaq>zVmv@jM(L4Mx}CD`eeg)G10 z_v;4m@?kAu2_+n`#sHsgqqhueEo2|0(CNg`ux>Ck!)e{%O(BJ;9@QE`8!3e6o_Q6$ z%}8OqayB6aoxgLh+EKf?xF1gCxy=bZep1i7e*Vsyb2(2Ztk7p++5b6CD5ZoHUgo_0 zo2zube$Sblm84yw0WH``l@oH1M@ESf+8R7C?4IZ!uT(9a)+ku1d?!7NmevnSRFKfZ z8qc5_A+&Y&eSii+=*+XqJ&WwDnc!sU^_y{o?}`=$WcKa z&QU=gK5_3Y^XlOZZ`XHe4FONMN2e1;=(Diw{v02aQbGtXa{@mzc{*`ls|S^&{hn1^vGALmQL#v(TgY#2agZXP5Rz$=J7GpFL@3v=}Qlym-8sP zEHYT*5mY0Cw$8p7&_D*Ac~rSak)1IUyo+LZ=1uex-bFDlc^a)FDgg~_M+Vt*OzQ_Z z8py*r8py*ZjXkvQ()s}&aCc89Y|v+6+50&zD5Zo9^7Hv?WNfs=F-N?;~WXYgw! zp^~&SG@t=LnLi>0d2E!Zpsm4U!|wkfdlFq15v=h9su4k3XWtNLAcAgCYr7|rojp^9 zXI?}v;Z-!{i&v3-omgdn0@{cm)qgYQC?F5#C?F4W&i}M~_H9@@=&$(M`!{!<(*Hp? z&;LgFV%PQ6&5P63yZ8;R;iCK3gai1xULpDJ*iATDhcEwCDB;y`f%_K4FK_jn&W|0B z|55Zk!W)6J`Xf4^Zl(0Cyw`JlP|AF9cZLJnns)^k=)1m$ja>)QYmaQ)c_8_qlGoEp zg4#FYgaQ8`_vp|4ipGXcs|0Z+L7w#~+|dVd2ZH(|hIO83zWcy1S+*NtlEZFFZH}-b?K3C6q{k0sw z)+Llc-NLuKg7jFP>^WvAWxm8a!v<|ljt}zi+lCeNz2C#e4g~47GdAw@m3&ai>uFU% z?HyMb2K2$B z!jx};9kO~fR~547_=Cj`d^c;t3(TNG%D?B;bK-v~f7f5_<$rhhrk9X|)osEQNaylu z&v8U4vjc(n5<|2#@@RQ_CEi6m(Oq=e7kzco|7j(kRVact(;Gz< zynR?L0BK+)$6X85ai%*7zB$@+ock*sE$Z>yC$09%rqzd*PG9cQ8inJIQc@1Gp%t&N zop(ctOM1ry&0WphEem}2e&<2EIkxO~9%!e0uk%2@5p9&=99iVyeNjeFz7bhuD*`%U zDWU=Ew^HhUq5AtiXY1D`IUtQGeYFpIe3F$@AdRdpuN7&ihte5A8jsb^hJWgwv~^Q| zU1I+;<1LPcZ1=OYVYJyQi1t1&>Cm&!S+O2suG=@k;z|VbsF0cK1*Uk@7=yXadIGou zn_0HMumyFT>6q)bHlW?exhE5IeL;_3KWVj#7CShsS+sQevX_vCBac#24zj3gB(kJ; zgwWjYOir@LcduIv+Uv1pC-gu&}jsZS9bSihB0 z_dxac9njXVOLD*+Q~GL$^mv|?Q{axQF0U1LsE5+IMH-LQzK8$Be$}RK>aS@hbiGqL z^n11gn(-D#L!}L)4a|h)i6yR-EggC+YsGqqxo%$rh-($hqe5n`7wZ*o8gU3!2y@7S zjyoQx<4ng~H?Mu|Q_g*ynClCAe0xZ%{j~{^v~>FNm+*z-kWx|(vaf6Wv7~pr(A)`4 zPO{E-Ba?wsiux^kq=#yM^gzCLA`rezWjIG2d3aw0(vxpQ9oeb^?K(`TLr+{v-JtqA z3bB4&k^}0P($7#xR!)IJvbwwy3Q>=wvy?Rc;>t!hcK@y$=knAqd+eR=xj*)|_D;L( zBT0Lum46&rNk+$hX~lSGcxg0py1H|5Y`=RKN2~mI|Kf0^--j2Q)r0K!qlbHhE@lZrE>($Qqsjhr~`4`=j8F~q|X~u>)zHBnzju_QVZErYANQwE9g?TF#D2bV0$lpC`+8BBnKHab?lfsUcMvJo@;uBgb zb@5!N4&)=O!d2&Hvf3cDsutrQTL-!Jc`fTj#d@mAUqulxL7tsVqi^q5v& z(C4)g=5XX@b*G;6Usl^@{PZk*N%}$e*$iie+B9QB9N%mHjC;WVSlfKHEw)ar_vg~r zol6|1+Sb}B?Pue(J`~bT8oax>Gt?~XJ!$LIjsxgR^{hP3mSf0$v=$L6TNe`F-cX#C z?(0UitbHMlkK>tEEsb*Ul_&VYDEgC?rmI4X4s#|7GFR3e(Ny9N)3Z7sm)_k6jtaHM(o)!U3e zwVa$pp2Bp@mn_U%slao*l?wU0$9EgUS;LhL--AwJ$93TKD?(R;d6iK&uB9%XZPjTn z25Gr+Dk-ByVRmY3l}Nj_m*ROV1+3`z%Tr7FfcvaWVP(gcOYzcsoK`&og0og`>+{+O z9XZCdx>Ha3FRN`cetH(Zi9K{bogu%Dtc$@PtO(g5IBt)aUt$T0`iS zQah#n={TL8>%B)EgNB)GZEN)o`)ArZ!-e2P)wA-RBL}|3+=?Oh(ON|Kaa~A!K}2y@ zx?<2ptgDT+Y%LIR@mNa!)Ny@p&AocoOr~sG z31rU=7_*R$CoC#t7;$Mgo@S`D3~RL72moE`n;=Q*M)rvXu0hzdmP;(UuCQs8g+O`R z7*6%@`jye>ks;5BK<{51ObGNx7i$n=`n8a#bgvJMrm|G7BjJNt{g}}O`s6KE$h={T zm9W>3MuirKfZT*gOW9E=hzZoh8Bzg}COM)}p-16ReK#**&{7+bry)%&Yp{!zt5X3Y z>TJj9Q~^VmSm!$V7MXh1I@;2Zui}MFEq6*8g!dL=_z!2OSNDvDF8SggxJgJ>^Mp{2=RK0f^(i|XG_HI2_R!|moqEDA%gl5UpaH!f&h{?}_Tha@A3z>TVh_?U_>CTy!(+6yn zs5gZhPgIwGp?4cvVuYT_mA2!mFAakG{&`7ndGeIBAB=>~8k=jZU!K?#fpD*qrq&GL zSw^I4M}Nd~k6Svgnee@R;p7Ru08&NOw7ByR5fA6` zxMDsR82HU&VpfyzR&-n`TwKbIjlpeTH%{c5RuQ8dF)=HGkA;Ty4Z?(kOKrrz(h8}7 zpsZZ2PDbq8j??)JhH=3NIzt$lde%DH(pD`MFMMpdQ_3LJxe!Co8ur#bqhVZ5hy%T; zJyb77#g2Hji~T+$CFoOZfVCzyT#D(y-QI$rtLtC81JLMa>QB%!SWoq`*5|qAG5)lMa6ePq3Ag#lde(-9l1n{l9t$6P z|LD0vWx8{wmB%l$g+R-2J1nHK}YbKV#PS8H57r^VPZK)g8;?81Nno4H1w6{L# zGki^63wkh|rPi~>)l+@DqouXh;%hYS3y`C4W`SC=8OIE=HC45Y+A_!D%)7-n-i@@l zvmgud{x(Ac%dyWZ@m)eNJ z4RwP&z;RZtyhtMgZ^!AR3n9-Q^rF+4o!_W+yc>-LP`psO7e_F%c)^D;0WBu||dm={0RnpX&0c_8RRPE@GGj`L}ve$^3k&jBD^+EiqWvLt0($1k+ zno4G+xwpRP1JWm-hK~F#m7XOcecpvlt+ftkhTGVo*EDu{9@Xz^H`5O*I0DkDy~Lj| z7T^r#I`IPPQ>BUAKrWA~gYS$VSXV34!{Unc$M~{d=GR`?9;)(|_<=Rg?0i7?`|ic@ z@8JS6@7Y}Urg#Eb=<(Z@kt@h`kuMN(G^{zTVhd>{UE~dfHiR{V{~Tw=_3L>|0yAaska&_^CYc{y%UIa+znxrBBs{}+reP^?f?U@dIz z8FZX63foeM+Yp*kndB2n7D8rDF2*sl1IuCU42JZ+5^{!d*b(dfqcXw%LU7j4LUo-n z8D5`OoYL5{7Wau~@V1`E(GoBqK5q1MltEi*1Xv5JOpS=0X|z{SX~Eyti|KwJ8_RKe zK4*Edo|R5*ycpO0i*lsqHuiY#(64F%T5fwR_*r+0yYO~l1XG^+3YKCqPAFLnf$CyeN&vB|-Hb=EC!;-$@z7n{V!0PIpFJ0Mt7neX zLR(|h&#IS=w@};TaMyC2*VUiM-bR5#2`l}9by$jL4%t)bPR)J#POOFTB3_qTPkG{3 zte!UJ=z|$QvNZ+o1#7`+gwjGSrerBzY)PCO?rF{2tJ3uPZ()o{_uKCIaX1MkvL?Ci zyZ0slv%;JRvxsBpz{rt3$#s!I5tcUeJ$V-hdnR3EQiQ*RIE9EEdGp`G*o%1#ZDAe* ziV^Y#9%2b|9acWO{*5W>Tk^9M3cR?XfTlx?60Hw+kcK+K0a05+~k3oQc}l zO4n$xFn$nQNwoB9OrfQ7O}TcV3qiGvg#Rln5B5;kTF}jQ*)ob6OUtOJL+lY zdUE}bpxY4VBWA`JZB1e90`2R$QwlBA;*OT$#UI7Fk^DMJjs3SU4ypTb_wYEJHWYcJ zT=(63d4S^Kl7#lex`;K1d{VB9oRZiD<0If#BCnL|BDW-jDhw`;fVRx|rFjhhEsP;h zjMn7A!D0`L%ZVIQJ1(MYAzMa@-)|lsVnEOp`p7jYFUQ&{rxVK0_@;KOjS8MwvEnL> zDdA4;R;y*K)3y}i#DvCGCi#St#iD4WFU8p`F(%B~9=<`(CjL(?yDL^~iI)jKM0@zX z)>f3cElz3dS&RGV7tP9MoW_sZR%PhvygxMptcAJiS$C)L@hC1NwDWYoj}7J1j??ow z6PWd^bZX2dBuM`c%*a&R%FuT zeII=_B8QgiB73I&-~61V3VD?}|JJpA$C`9v$ylG47d^}XEK%T*q=UH-8?mT0zZ*C& z_n8N2gL;i6uyR{^doDN&u|m#8u(wI)eHA)Ta;Sy;`Q4kv(D{P4rv~PuAwJJ}Cvt7+ zj>u0rDV=cvq^oq(m-c+ug7HBib+;I|h9B}h5HasfL!%S5=)>^r#;hUX)U(=cP4N_}LVl(wr*7|V6MQ8^*Nzf;1S zathbqEa8m`>i2J!kZua^I%O=fPVr=7-l)bDMLDuf+BpzCJR;AO>mtLXUG{OufArNU zb^fh!@Fr;4{d44RA7%Q)*bC`E_r~AAp)|GTtpiKu{{Em*kf9-Idxnf+fxJc?68!^&5&zLcSL^5NjVx`CS9e29Ta<`6U_`^ zXQ{;;w&T`tD86GO=DlfXbcz{$7@mFn^niMl8a!=aV!+0n8Zfn{*f-(#rI_jD0QI3- zQqLxS0GkYUM!D57tVUC^m@!hrKq)<%1+AXVxfs=5+=SR4R*akaZu-umaX)HBm>B1a zw#FIeQ%&W&Bo-#zqvrr&EyrkH(4)o}`91dR+_Mw|zJSrt^ZO`-6*IPou|rwQv5Z#Z zUwlN0e~HWw?XfkEJOj(3d!2_#46DjDADkU&n1@#LILab7W~?VMyW)9w8c$|p-<}vo z&G-S;s?_pE4UEk2aD=v1sctUh!WuhDz8SQ$A zej&V34Ta~r$inC>1AYbjQtJF$?}wl%Av(1&J_Fq2vh@ipfOH^W<2c|M)RQxDORiu?voCH4c4`MAC&(O?lc^VQh~h(ol<9+fxJc2Z6gVPRM3p zOLs(m!jXX|uymCUu1im1=SK(|*TZ@Mcs9W`he=(NAQ&ZZG7|jKG(KrgfhkmttmSP6# z>%AkkXD}88E`fX6F^o+2J)%8rB*45lZ^N^1jdMmKj73^3+;7j(w=;W3`V|B=(|3(;-hR&l9wA#&8YVZ zbt_J(vAsb(jKuo)1FS^p4^-bgqkY|W|b>>=- zF!{%G0^}`H${=IP%h?I=Q$y!m3hxbLA%sH3i{vt17$%E-@c3F*zMi*E`sRM|F^*T8 zdX=f&Q_e#O+lt|+bsN$+>)<AR`!Sw%lR zY3bOXB<^N{xVFlK|9bg7H zAMo$_lF@=bu?YQhaRdD$-fDKD;P~69nHYrM43BuLIfFp>`(qGT-3Iq|pxneDuu^WQ zojpOu254`PGZ9I4=HU2KT24Db*rN|l4w&5e0{6aX=wJ!xck?2Nrvre|np$uO`Nwkx zWD|r>@X%kNKo8FF3Yb)vKRo5uVv+1Vt#qI7-nqhRi^em zITs*&DNI%CHl%Un?|X0#W^#`9{p=FuCFUZr0EK=+`Z|43=^Wc)y#@v#*DKz!zWQIt zKBR7Fm6rM&)|aPDN;}u`tYA>AeXzCjC*li~H^0RgY&j6ik#R!$cC2|?E$Y}=B>rT2 zM*7&XNv*e9Fe2=#kxG5VUbx5B)>y1}tC5WuwU7LfG*%Y$LiUVqL->JlaMnsZ2IcOI z_njkwk)Q=atM#AJ`di|dhsH+Gy7pY+a9TRvYKG){koo$@1L&NS5dVm;IxXlElJ6hZ zI6i776#tXqE^jr^K3{$%%v-Ja@+-V3=+D3WO1dcwFG^VFg-e%wETVjQ6whj`<*B^` z5!thUj^iZCS*MQQ&8x0-lgr2xpXk5q)98<{hzpxnvGWYmeR=%Eh#N_g{5Uy$KRi<_< zIUX10GwfdJHpq^?`$6#sGnu;5Hmv{JIf+_7S6*t{1^tA!b#@>u0!gER%v-A!`t%cNNMNV@yZ7LZEOh0Q+aaU{Y=A3H;D1*$|yW<&(G@kqK-X4 zqywRcTGFSMw$_7tL8uy$&hRK__?V^^Y;84Ai`w(cFGgcwi+rKt*HlVlxW$}Td5PmQXP0J+g0oPLbbIHY&fB1>NyJwHRE+KA!w!PDpcIk zv8oP|O(?mRZH+lY#I?+s69o!x$OY=v85KDS)(IC0{g#sIo3L&vfuMyO|0&0}z0SOE zXb%l7^6mt~>U2b5$0Tvbgj&}+VZV~;3&YC^p{kFp-x_-DtKc2Uxr@T`#yk|V*3-KT zp}MDt`2{S*geMHRlU87S42GB^7Iodosq}urqA@Vkqm-FfhC7cyjgC3znusjdTHP+N zX41S_tVtcWtW|>%!Bz#z1n)8T?H)&!LV%^l&!q4b={(F`T9pl`FQUx_t*re9wiME7 z>ao#FlOeXi=v!P?lM*v{Ef` z$AtFQI!m#V=?lY|)H~olu>;m`4ZZy4%?39NY>2&(wI-V*H|H&EO?x@UpK(H+cFMwM zZRPw0wc(v$=N%fK@hP**P}|Xu`+(Ty8r}(Zru|y$v&D*Q(!5!uMjg#} zy}jDX^YM&AIuk|%5%31EdmN()Kb4x$GFY{a;2fvf`YJhDyK?pQ)gDXMRvVRPuEl0; zAv58Gre1b%fzcc@lsdyfl#=5%VW}F+@w5|0qn@*{R!mv*HNC=0Q)97~j+INS{e;bG z+13Fw99GMmE>K{vhFoARokoyjFP&wOFjpz5z6oKKl1^5@bNb(n0Y~*hgq7ydu#2NA zJFOlhlO$44m`OOemgxgK2Vk&M1s$(CZqiz}u$j!7_US}GeWG`&i zE4QDPl1{ew(*zC0Q(7rsdj-7nE`D|E_C?2fpCg_V(2Q39lGHte4YIXB`;zbTQi3(| zmB8O+tAd1IZdH)HvAyFrd^^q<#D)wW%qc7zzeF?G{WA?YRcI!jGQj)Ga*e*s^nUPL z-i~l&!PlrqR{tEs_*z#>KVJuYGpI)Rphh(<-D!qs>Tq1^lRkxC4aJa`c?xRhG1;GG?0q>`T9y1|62@E2-(=WY=vSdz0wFZwvde0 zB$r}L&q`m8TNsTqEW;B`*d-4$KUn*66mtSX(Mnj7x<|}ATNksh`TnMqnBl%E_IKF| z8R3^(AtP_>>G)mWj=kQp`fWui3(IaV5el~aOhZl;LWw)WUlz5Q=3LwREQ$N~yzf-Anouel=u5Ugo)~)vP@17$vU8m6ELp%}_~OE36W1 zGw?(yfnzHaQpz@78tB8;5F2`1qpXk#rB@muK`fkX-D&c{UW~CjDjdRSoM985XhI@+ znEAn1dL5ITfH&mbnWXM6^UkoyM!ugvDJ5pOuMpkOR*ML~+-ebdV;#q}qmKQ#JVp4L zZwjMhl_SR+SmrYgIaPQAYi_11aYv3vR7#0KTDU}Z`5a~RgVsP=`uY0Mo53Bj;A`B` z(w%0QqYlS4An8;1)o=!RnJ1-CV;*+I5m%;4$yQAYj4{{P32zgsC?)mzHjH88B*PfC zewN9$E?40U-XfJo_$|j8J@~cONRC(oRFs7iuAsIP#@JlBvATAl-v_JHi(CBetu_}A z_xM{fUYy#OeQ$^FX{GOKy)S1I{P@f=w`;Ih@x-1&vaj5xP9e9agH>(bRI<>ARbPb7&kHmFLNzZm<#wJfkCaN29a;X+|BImo{pDoo_*Xm6(vSPif=gEzYn8`e)gr&j zqspzuZk2~e>A9m{rI7o=8TYO|U_7(6VX!()o|t*HlydNzaBhIgB|p~4Q^>rm=$mlu zAJJ!ztoPuDqc0NvN@mO1)$AzSnKXs;ztiod6mnZBSWM95;uw40TOv@!;=`Pb9R7euAWMks&i|Hdpq{pG~g_?Q1I`tKWl+;9J#)0H=p z%HywUkzeIyLuyW!rT4P1v!AkFQpkONj4RcC6Q0?a@nBJyJTa+iDRK@qq4!iSHK6xW z$h=ADn{e&#(r0(A_so)THV@9v{!iOA{GVa&O(DH6{eNK!`TxQ2-*7Mnj{mmQsR+ma zqSSo)k4nFhU;0bCa3}3%oyHcnhtgXmZ&sl_x>r$(b8}am#%ArJhDTP>r z?)VSzzwuWgyk+<*p?)J@DAaF_|9Ae|@C^$64lah`cgPO-*6)b(C@=GCib;lt;)(0&w{G%-YDB~AJJ_9Gu zYmKje0{dRbFzv`2(_!&WVXWLI_x<-d*U#~Z`n>zk8-By*q?^LnwWiyAB3=JE`4=+W z`sdwW_n)7Bg8r|&M_W9yQ>`~3PR=((TXEoA1Or9b8Ur2#)7{8{D`z`(rzf5abC!r z>|*5{kmkVBn7p8+?xZ^+-GRy_uUcLcf1Jf9+1qVd9zItcl=mU^%)_d`>Lz^(cmHSi zxmrz6cKAsy9evV$d`67gCl7P~$JfxSo$K%o`bIjxb|2$g^YP7329=wI(XaZCKj40z zf6s8tOYLvrRjr{mQqeoysd&OYt_ zX~S=LO1de`oHgC%OX>Qjz|%}iT=NJPpMxaGiOQPJ-z-VdhX{L3z<1< z>Ce9Ge>Q|XXs?E@e|q-i`e!3)n6p!uIcxcyd^!HCBahL_>7LRLO+9ng>Um6h%KO<` z>8Uq;xuEZ#9+7rU+6`qhCJLE3Yvmk}=D^aJyr8A-q&p(rfyyPXT3!=>oW&>E+ih7M zo~jPY`;dC(Vbx!ClRkyJf9Rg7)%0YCUslrJcAuRQrxqfvkowQA5u>&yzao5t{*lhF z`0W05_u0)a2bGuU?s*E8L? z8fDqA0?V3R7Cs;>RF1bN_%nr%2@9#8hU?i%K9ie~FX!3y`s(UfC(EqOchIv-V%rnw=-(3F+J@+%u z3YmFk>Ce9E|7r+%FwYvg{+qL})_*mUhIuxHnP-;Y$yeiFb>uNxIo&tRv!~t?TIs1bZNH%Jzd0i9nzS3rrVk65d1mDtkmkVBn7p8+?xZ^+-GRy_uUcLcf1Jf9 z+1qVd9==f>l=mU^%)_d`>Lz^*A5p&AOwW~v55MaE@9v8;V$?dd)PHe}7_~Eb72zB7 zjdXtPzUY3_eR1=vLFHy)^sD}h54fM_-!mNZQu|wYRcq)I%Ci3Xq70MrNk^TmpDj#3 z&vcvc9rM%G&5Nu2US*ZonVY3=(CvH1{C9ma7WnMF4R7{$wCTSe8@_GXn_Z8+p4;zj zU2do2SCLum!P*Cv(i|NXt75*TI3BGyd&(BrI=~P$x1(QH~aPL#Aznb+QyQ%{W=+w=zrl zvp%!ViLc~wve3!52+K;B&adz;lZ{R;rDPVhEOyI3Snu1zrF~9YhuoD1y%TKpJXG4` z^l@RM=QepBes7<HOOLCfVfVQc7k)%R;yOgSEarT-xKbb+GvDxhLD4ehOAPeOlPywS4#n?eVwcoEKJV z3P!z_Su=pu&YAS?NlV9$RGrOEV%)+wD z84Hs&&fPN&W6v0G*mPsmYnibNtIet_@o^=&rDM((%=x>`q<2qwMzc1ZS?^%hWhp1C zd@e`UTpYY}=^b3UTfsSh-q692(@%vZ4%6P$_M}YKxA^)}_VO4rh4meI`&th>U0C5u zaI_*4J z>TO;B1^s4LJAIgi#i}zFCM%x1XPV>hz>0sn&zgW^VULDe-0la{&^&=@&TM&hrx$Yf zgcY^SW-ldH-^|P9?eK)5ZVMcG|9QcuV?)y}ndL64y41MWj4bvZYdBi(@VsV0^ER?; zwBVQYL-2HAY!-I>l74QvzqjP&5%aQ~`rLA_E&JWpdv4*kt@f66pIh%KOuw5A&v=+@ z`Ie6HghwVLGPCF#8sF70+4NzT!z6d-%!tgEFD2%#dAqzF9x>F-tn%m|!+Z@^d;c_r zm8Q1f=Nk8(k>%cF6-O(+4<0YI)xM-3g2xMeRoL@O`nd)F-lCUZ%*%4^a|^z<@ONAB zxuxH>-dk3FZpEiC{cg5A<6*MtTRO&EVaq4Gz8mEY<90o}!l!UIo|#=gOxNfg6R+Q` zmke{ytn}zJ!+Z-?eE&@fD@|?D&o%B*BTK%=N{&{&cx18DwH3dlAC$gf$uH^W7X5n* zU;Z*L%gN6z`r6{(ZPn+Nf7_aGS^c?HpThLJ+4ju6$;NN#`mcQ7(k=6wc~#$;cj1JE zI~Q2sm7TBit>!v;sxzA>Tc3SDa}B=i$=Xlh?g@T(nWbM!_~#M#y8PYA8{2xo2SncQ z9p3M5smn7ag=K#ytNz+2r9=*uMUVgVOowHeS@wC@t^4;@e#zm#=j-w8DLDLO+2?$H z%c5`Tu)ixSUuRLxb@F*<=1%NAd%orx-~EjDygcBUC&BLT&Y4e{onK11r+XfLIj?u} z#%3Sz43Q6fhYx(R=~EaToHEa2X|8=)N@yz!9}nu84!bh5>+`U3pR@JvZT$DFJu^3B z@{?Vkv-T}}zNK>?cQN;zc05`8oLN0t^<}uj-(7z2eYkexb@P88r>lJ6U)~R%nTLHn zeB)m69bWOtqEBJj;K`!D_Ejm7LuKFNS3T2VRc2Ow9)54(zvuAr0D4Idzh%w0bbUCP z;O2#&RB+wSH~1l+ad7KGyX5Eg{9G5m8IxQ2Egk#!EkE82T>B@Sc~!}MX77(#xPNkM zvh}+nd~b1YYxg<#V6FQ;SKd^?x*tDJVdAmZsSnql9g?~x`P5kVWbI$ANi6BL_heO!OQ(dc9nNK`%2Br9rQGLxT`+V+nw;_ z$xfeSVfSJu5A$Z%^#=P+55v{=nNI#Qo$os>WM|zU1mBtZ`eEfSbAKkg-F-j!$lOzW z<_~kHl4+}^T|Kw8@@A3au0T`?>bFZ_V_e~iN|iHe6BtF6?IMWsd3-Q>b_dZ*Jd_$XWV&di+8rG)cN$G z3eW9s_Z}&2akyZsSI()kZ*rz%cjn{`b5ex89A=Q&`SL_Lvu7!N*ROI<^ijXw2+Qk* zosO2q!^+PQNlU&Nyk2f(S9!az!_$1wK{1p6x#|;r(Fsp}=JZJxb{}){Fkf?BZ?FUO zFkEfN=j6@O`3}%RcJBK@@MNj4A6DKg_h+)(zxRX3%00zr-YIt~nYJqI++_2v(MKjb z5A$_Ovvp6VYd_h#Zu|BlClc9@k-xC_v-^gTkJak?KpE}9${oK+VdAk{8Tnj$_7Lit z17VY&T4-)r*mDSMB#?H{(o)clP9) za#Do797aBE;NIkC_AI4``Bm`E4;-HAhOI-Dd9^-^3Z*ck~3%e&cd6-AIt~c11c^Iy?4|DP@>3m;iAv^Q@Ao!Kk z*AFXSlKV5+S>^k|f8?IxGhdNAmCSQj7U#7Ka2mg}i`%Oc``vGxs->9Ikn&ovRo|6U zwQgQ;%GGPNI623(urT==LLqauEevvQb=GpUJP#+U^TXA_lkofe!_`r+G7rP`z$_x( z62aPZ3n|K+kWpBeoc>YByDUzuq3Pt5=r_VW4V_L$iOljYt5YYfxMi4_=k;`2N^W(+ zW-sb>VhUbTX4$##L@j$6xfP0!gM2K^Dy{Wut7whtM3W_1mMEu@Oz}zfwvX1RrL`5*Cree&DN8(Bu0ftGR<#%Ll`QL3C#auSy-_yyA``{SUxP; zk4gL570cfITIM>pZaqGQ&6g#!azQ;B1BK;_MgN*DUCt*hEL*-)P{^EFizS;|yR{rG z-owe-W!9dD-&1C8ux<~-^}sAKW-3>4$!?iNnKLp9%a$`h3VD~MixoDVpTf-LUY*I( z(&>zp$Sm)&c6Hv0TcpXmWIdgkl3Tm5*^7Feo5IXBi_$$OYT3(R1Wnd29uD%bFuB&N zt)dmIb4`|HS-hM_GQ}rZ`6jE_)68{Pt<#RtIxH($b(B7ZlZCA36at7AbC4%XS?xu9 zCCh5o87k?zWIbcK7S^-NZawcCrhN-va1e!gwZJdnNA7X1bE~=znMcRUEQggi+?ijKZSkG>}5x zWnp9eO(&@^%nPKe*yEO}zNj#;?0^v7^!BIQ|{ zSGDwYE};BGaxNK8ccv2;94-CRBHR^cBqqzgU(g$O-LUQlaoUUe_g253otUhAZ?7cB zGZba*d)w!h|8+wz&SU5N^>sT>Q5L)J2i@1&XWU`w>ukm)E5C&XlC@8Ne$0QSkA(u} zR{mUf_DS1!nrn@+{4(A_1KqC_j>%2v~OL;|NpxGPRlOqPGYpf^svVf_!{+86cj zQNeV+Vn6}jUTsw1ZJ#59>xOQeU(b2<>vrOztaslvx-S}F+}Z9?rGFF1;OxuN$7DD6 zUL%78&RZ1ct}j^obqw75(Z~$sw;sE12=QAR24F679kXzW5yo)(B6HEw$j;4K_se1g z($6pg?DtG3);wZ_(<0mzr!6K-&@bqXX1ID0 z&r}D!!`j2rSE~cp0(&3*POesRJU5*+I=jDS|94>o^MBcj5{4(k4wL|gT_J<-3n?7I zIY*>$l7)}4G&`)ldJSM%Z|e;WL}2Kop*Ig0IOn(?VCxYLxxEhX?ye8`b1ttBczd;w z!qw0xIZ}vg23jk47m>orC-J;>=CgUPT(8y)t_4m%dcj<+9jmmM}@y)v+@w^fJ+4lqp8(3?LFHfUTWuvLnN z++H1ccUKMkIhR)ryuDiZ;A-fT93RA$1g#*vi}>K=OP$58U8dK=^%__C*fGwIIfD;i=WaN_>*53l*rgLEI4aVI zV9Ee4Y}X0a^|mI_!T!cWI(qX8D%KEe&7vds*9zX=H3YxU^)&<^uO2739-m3c3DUZP z))U@FPH^}73%m4WzY6CUm6!eL zcmjJW?9?VP12svIr5wZ^WXEPr1^=Ac=V;uj*SNiYpCj*M-{;8hbB!+?$MNd% z1)ck(XYy0x3kTmFpNlV~^#iRYyzN}!PbsoLsz@~N;!BjyVx_*J&Q{i*Px%O86Xlz|s*QZsj)L?08NDcWAf()5 z2O!z5eh(B}vH`J+TGl?#9y>6E-(d{_)SS^j-BX=swrj zfg*tRcNuRk?Dz&JH5 zAgv>a9h{#Lh)u|^C^Le%+kl-0dwhKFE;BHC6X}#Ac^3_F4=?*<8(Di`Uxhc(i(&%W z70_b>pw_N_55!v{e=&(#*1p#s6R;fv9flA<$ayybq-(5eG3E-8-s=*(}9mc<6 zPlE4Fcw2WM>}zlFOJo9pXg1~E;;}&fQ<2UQE8mq@QjA_n+`AJ_pVsF~eePJKmc#~R zb57}!5lj_3P{fza1Om})YS|;ctVgqX)0u$m_9gNclPF~&`5qHk*M~c>r+FPfcKTWl zcQhun86(~dD{{Fekjj+0(b{$N@7HLVfLM+C-eUsGb%PrD|J;}WjO?W{f$+gmwbyto zkpEPqQ#2jzHPGH;Mb;TVXd_rMlR^E74V3AU5lj^uP~@4+1OicYYS|;ttVh*()0u!Q z^(FEblPF~&`5qHk*M|=y0+0KShei$`YD{P|M&21#cb2KNhUT>rb|XJRct^Ja557JME9v> zkASlt-RHdf3SJiz;3TIC`RDz`z7JVQzQ+XC_2J`)z~h?0(8%FqjY(QF5E}2xCF)OQ zO5JGfIur0~cseryu^RQg#{{7`mU?@Od%D-P4saIwmz}c3kxkdY}TzUqT>9l(weZQ@{2uAHU?)n6sy$;Y@8OUBj9>HEw@NItqSU#Ol#SE*Y zjQpP`c0IFqj#aPKfJP!JP(u5P5R}s4kzze+{Q$p3zH6zsf4HZ6UAF(T5Wj5mJ;cAJ z$Cs)?_?v~C(EE!+{9+$s87<-$d$~gV<`>pP7Fy~?_7vA1Kug>IXkJ2T9l*VThUgWv zMEnO7b_@4(Zz|H4O}>Zp*YtQ#Rmgs` zkP~W8HvToaMD?jmsmn-TtV8UgMfzebS4iJH!I}s|OWpWgiUOJ<*x#@S_P2=^${w5! z(!UdP-r4bzw{~v6W${EO?I3QD{!$Z8_8Q-xTK2ue3E6XI9XpY;X@ItS&YzGdUUg3 z0685VT?3WY5rvkTa))r4Ui0=}(_hr<=XsHT;9kcSR-n8!|mFdfw1?ak7 zChMlyw%CCBe8miGFUJFB;)CajM;gA9qr= z*Vas+m1V!LnZbei>(si#7+EOsf#XI7QB>;y{LWPJ&G}TT@#J|{0(@6MIi)MELc z&#)c7lK5ss}r`*ykRtsQOe>FeSV;ogOWe^xN1zF zmoI1}(u#(7MH-pBQ-Cuc;(hwg0qqERbNqz@jMO};EJmqI)%gT}9xU=`Uzhp9I(Jyc zEge3nSwbtzcc*3zy!i_qGu>hqCFZ~^s^r(r9hm9(48aw^!C+VOAEJ*79WxycBCIUl z;P{sN?dY(ads_v!=;=78ls&L17Evv!1rYOMpYfVrypA#NmHtEGSEW1{Uawad z#;#7dKXZ!F_(v&=U-Y?yRvAie;p3`teqJu2kw~i_;v8vY@=gNIh=}*;I}2DDLN0d_ zyiR|j09!SWD~nm`QWdrIZ$jh^#Tf^)?mXuisQvmhgOW#->C7n(;17FnjdFG2IPN)q z*_?tI3Lk(QfRDkn#u1pI3mr2QZsKwBlb;bYw4+ljBx|eS96cT9nLf$u54EHgK+=mH z$ZLA_eua6g=ipma`WuONmGWRXzFvhGyE^fK%r!>iC8aFh(dQIejVL*X#*=5jI6p7P z&`2cCBrcLhChs)hOo@1(zViToG30Wm!Rz!l3b0u7$g)VLE>*Eie;*>BC}Nq;x_ssV zd&g()QJ;07_G`^AP7il}@dB8}F|DYj!<{>)2i52O)BPQNetEjP^Q+?X>(goHH=WO) zoDMtx@dBSe>9nDCe_dQl*ZpPehp!%#T!R?F7LByj?HMnqxdo^GEjY;Q=Nr3*|M9|r zYvi=^jLGuwaw&L5t;42coPza(SXNlraj{Ow6*lhW?aiDe*UKvycD4a4A7#xlS9Ruk zpZIwRyIifOEqj1f`cC-7WA>-$s2jYkavxVp%snCADlK&4rN;iOURMao;sVo08&RIpPZ7=`()HeI)njlq=ch?%s| z@!PauL(J8M?)5XIU1Kcxn6R>v)6R@0%fpAKU`Dmh{i;Cj+9d$`R$L;n8Tnjszs8Vy zdHgm-mX+(}^%zEUYp&|d^}g^EANXzRYQ-l~4?w;jhA-+9ANXxLyTsfAv14hW6ZO9)UQ_W_a^3~5iNIji!b?_O`GF&v~uIDC-2-)jiuh%Y|`gF;p=*} z>wR*5N2@$#MsH33D2;}%`)nFn`*(DlB=h!(? zR4Z$gj^jCXWm6*XQGLdED zn$Rx%ET#8IC;p~*QYoF{M!h&Lw`<&L{BD=Ep`3OOH(4HDNdsP$nYiZrr+YKk{PpPuE|@<(&MDoC*r~AzO5|5`E*sW%v!_m>U=$T$DL7aQ+D(?xo}Ih zT<45-P0tZ;89t#eeCrD86VKYdRkNS3K55LvCvE)TKYmX{@Y|9nu4`9;F-_h!^_;PD zP2<8_^R2d6)TESgP4$GDTXN)?=yfRToYrcr>FwEA4`D>yVT#h**ZX~^aiT2G3O;L3 zc&QcVHXcbSF;4B?vC&^>&C8E|+vw15t-po8vbSNOwN*UhtJWKpy({L(+FqlDtBUt> zR~kLL`dyW3b$A=HM#>e9lfwQgdE^9g&)2@E#7%oTJt?&#I|8etcX?9uY^igN)=yKZ zg@(1f!P=6wfd1O)N#k|H8EMY^~u}(D=0K!*D(v)iz~Ek17m{ zUFPsBXxCPb7}D?sec@YIP@j0#XXL9-8Z$9h8$Xzq-xI%rTk^!J?J97y$*)a4w+(Pj zW8GWxt+x2uq?EB*^@N&Ra^%tJbtvoX*=nrm?b%ol;a=QfifP=}`+cWzq%6-0=4?-R zsb!oQAMTk_;|UucyOLlNDsHY5W(aS;^x)Sij|K2U+65J)NGF+L6tIwc~p{t9w12m(uETDz(tC zsySF&vP>jiOnWPOu4ScVY2fN{{ZxI)8XKp3dJO3D`tSkucIH}%KWkwj5*MaFd)kp+oI3LsY_kr z(yoJ}eI=eoInHC}_sf`cKTmNXY)ukItE-Ab*BXAm=Ig<#J|5LJWydG5k(C^NzwFw& zrdZkV6@B4bS5TjLdS~RTPZ~3^Wg9=(o!^tVvy!xRCoulM^-<49Sa6>Qy}@KVb-Gd|oi zrNpPTm&`_gp*5dU`fZ~_zqNAfwYm33YwN#^BX_Cms_cXCfAqDi6!Q*cI}6u;(9_}F z%KEylsZ;M^x0O6Tz|vs(+J~38cTcBhrFLW|VP%~IVCY*%=cTmvo=Pn=toaVs7X8ua zaatyyov@(=qwI zwd|eRQ^6Q*Ye|@E$dE>p^QbKkJ z)tK(cMqqqWwYg7O0M_!D<RElaTWrnI zlTFvsx9;EWom%U8-cNrwG}_+6Y_pJc;ZONgEz`Sb(_sE!ZSmQtZ|-TfU-b2~^=H^d ziaPpD;OV+v*V~fM{{^#Y48NQHGW;%d>>A6QWZO^;#Zgc3(UX0n)h8QApnN)ImET#n zr4+hX<@M}5-Q%Yxdq=5DHV<)BSsG*ypuc=OZ%w*n`w(+79p9z>qtz!HNYsBJ?p@kH zmU?G;^h5TE4}HCQFbjwnT&quZ5Va%A05%r>`YLp`h4gciJw&;_>h3IBf8EQ0{|nyR zEhXq1xmpXCRmJ$VYIC2miLB-En;-2rd{bBDvEyJJ2-LQ_{=@mo`19fF-U5~d?wcO* z3t+Z|evV4y9L7s_0kfs3-de8SpJ=zqeU`-o>yuqmPxp)xquG7Zc&nD~cKAXmy&ZZh z*K<{t_b$E}W|?8kR7dusZMmRrMmoK)G>_ElReIHN7lbJ}x{-uH}}RR>w6z3->f)sjRx*n8{HF8mTt4ZNJxFZQbbOaKp;n*lLQ(&f>=C6+XsLJh zOFv|@`Plsi+$tni+v<~TNbSg4g58O~?j?}zL;c)jBT}w=Ke%Ni3rl)7@@0Xt6S|0VVk$63ND8HR(N%R`qa|f&|@u_?#YnZ*8 z*lX7Hvh?|w@D#b0TdrCiJ;Exa(Mo0g&9f6Fw&YQdearKk!U$M>tWjs>=*gyQ>05V_ zYOUvaKmFa%czX-8T}u`yS+}Z|>D|XX5ptM6Smb;*>RXmZ#?x$EeLb~Ku%8um^qasl z%oeXL`MYnN-RojDue+-|E7t5@!wxo=+3qzwCH?7$b&As)%!0K&H`~9)r^Ba%)uV;K zXLt8(0c&%z4Qw<^+ZeCJ|C(qAtM$ni=2kgHave_KB(|@+)V->`-6{GL{lyBDqd_Gb zSc?0^2$FWK!W zYuX{6n=H$ZtOZ!1xV}e4%{th3v{ZR4RavOi2jkdVa%B0M%u_?wIJfFB$5*u4d2L=D zu=QwXpzK0hoDVHqAbp_k(ekvKx53_`-3vAvSebSgvhG>;BP){bZ~SP_#-Y}+-v%4e zk$d`R1gIlBAN}a-WryIzI7XLy^5-em8P?dw?2Wd_gIq2YJ1hF^`?Yo^BY zMHZrEZnhGQzZ-rvSUp<%PwbeFelXKtwmI2SG}31H-59ULuTiuX)%s*HQrYmAil>$1 zY?V5GJ<0PkHHeHcr`z*2h>;Cv_wzLb~vwUsPJDZ(!OHEU6llcgxd*iL~Cn@-y6?N*01?GR6= zmVLx*S6}niEp+hHcr}k@D?5%Dwz1GHIkFE;=Ha1!yX}Kt&lRn9vzs>vcNtkfWfxlC zhhchI%;*DskM^0>ybTr!?XR%Wz!tN+kQL8N_aj@2?r;2h&&HwFu_Fje%8`2rX#}Vv zYZ(3L>+cO%Pl%)N#mGJR^E3O`ihjCxthTr(Y)WRo|g~i2oo+JxPYBlF-uZgTBc>Vop7)Puh6V{Ku zHrr9g4~B7wW%->j@0sb3O0C;cMzhR}vpprtvZ=V$#$%eLv`#mCk#WtEO(jJ)=2y?` zDM?PYloZp9yD|2hc2)|pDZU!HCx3osAzIN-501Sp*~093TjYin z?UD7Dv0?-?V$*|G0}nTreh^E_(m=~Lx?A&%r|W6Vb_%ocaHnVF$j;|=xL+Gbdq!4k zJx>;cfR|6Io2)zZmzwLjysc(s@GI&`xc!i^*pibi4XK{&7B(ujM+P=)c8F!}{wuUH zlzU@dc1(uf6#H=ex+x|Eu^DF{myLX7urXj4fcdZI92mzBCn_&*oWW|yx)4sb6uQtIzg@;c07KNSfKU@P{I99O?6(AamZhap$uw;Gqu>mPdu#7KrX< z9A9<}p}2w4csUk?%z<(+byP zF50)-4r1hpuAXw@)U+wPf%YYceVe7B(V2e?I#~y_SHT##SmQ#ck@L};%NjrJX?1<= zv(U9=RnQ348toU+{laA7wyi{8%?h9x7^LGapcQSj0LZFi_noW{UA@L(x_d)+8tnqA zC0oT(tFrW(NN;#b!D`K-QONWSw(WL->(+`|HUEf?)_001Y9S@KHhw6^<(C>+F2vyd zzIl>XuF)FL=Z&!M!&pOh5j|zsuI0k?Vf(UV*j4PQ^TfMaF?il$ZZNJD;DvmPJ5JUhBBV z{IL!sw!WgnD}`rX@pnpgwXiirsf&{$0JsVY+uicN#glYROKp)T%7KCUPCVJTOYLCKNJ#gXuaa zzizFlRa?F9Xx$G$Q5Y$~weg%V9=+7aY9RLP_sx^Ea*ftFHgAM|AI2JD%zDbMUCUKO zhT)#92zC{F>OAqTR_vL#*!&U1_*DZYjo%LAwk1#T8stN;;WNOk;m3bOl)kR^0IkZ& zi(#;mgM znBf=W)_B8L+?ptS#&N})J>tVsc1}HgCsr;tEjDTFn-%cHq*ru!4)Dw>4o%6f7S1)Z zW#64r%g)?+lZ?STcaAYC=*Ln|IdMnY6mQlZ0Atu4o%tD1PApja0-S-unie`)CEPMG zmo#0cD)UE;VT-Tr9a}uUYeI*(-g7EB4mKiH!{#n|Uh?lHXh_>%3K*vetOK2w(j! z)6{}5w*1w2PDgKir^8p*^v;)R?%LBC8|?7bHN8Cc`E8xK?3$c-Y06Ks?ZareYfq>9 z@>m1TnOj-+LhQcCTb)yvvh7ZZyp=x4?|$a7QJy)eIFxom&?c+e9_LToN-Wg<6Kct9 z0>sJlNcEGYwB`yk`*UZYw^>VHYHU}A@yB<#Vk=QUvP;ixW$|CGz`8fTi}ronTIuWQ zqj5-;DbtLj@yEm?Z;|g@eMJv<<9#+xr>K8g1BS!(wI47yj3Fv=0;{^lF_*f)sY%}A zfmW1_AucuiD$v$w476s3;-s#5wakz{+FI>$y*Hh(8rOPZXN;MZbxtV0R%<*yps(td zX==g6T5f0@qN6u1(cyw?dgmfFFYM`zX>~Z^nqL0f;(CcEuE~k>rTip|tF{cVr_+7; z&U@#ht*m=tZ8UO1=c}b`dlMojq!04Qo;hNaXTB$XLVhKSw}|2Sx_d1d-zGwxN41|U zrPV@M&)+z^yUiN$Qlr&k=VtG4#a5zzWNx0@%HozaPrGfc^!4=7_?gO-X~xmGS>kB7 z$afyx*E+z)4IVX>W}AwH&5vea9>X7|dv zCNa2S6Y>bQHBA^0bGcZ*6ep@R9$mm!FUvHwXr<*}hSNKG!|NTswWfExUvsgZ&e%+c zcdhB=&0BJ@H97IBl%HhzG`B3Pr_+7;zI5k&t*m=t=X~T{&IwD|J=mRL(Fa(Wvo0~N zD9@Zq96xh^hSE8PsjoJeL~t&o=(^4p(R; z>PL3uxvea|R&$}-)=FPb&x`}9OqpgJtFvBjk?&lyulJLk-nEmRu5(V)^PleIExi}x zuIKpcZ&tkZUvRQh%X4<)iW~FV_=_0P?)UQB<+osFKDMsc)8XCg^^UYsSV`dotB`sA z+6l0Ks3ojNQPG~o+%W2gk zr`7t)<2i=K;UgDauX|biMoghyh3o0G%I0fw;vRL{*Wz5C(+mD`AwI)3W;Ro%o!N-X zCvN3B9H@EUggav|4|8yv49JcX`pZt(V1Dg#25+($i^0%Ga;N zCF->9MKQqgs#TYdT!^30*33@Iv@;cPlbo`#skq5fqvxHuNiEq5(H3rE?@x!Dtm(N@ z!A@#PtgpjFx?1f__2}QbtnTFWICB!QidqVs3Oh|l=!ZEKSoP6Ku_G%#IVI1^=l8B; zrhzn5`V-l%wYK*Ae=-lQ8kP zV~CYHLy=OvA9h;Czg{OGrxdY&h_KZA>m}r|BqpF($n^*PUwPzX#WcnjiqCLY@qyBZ zx@Pgt-`Lr8E&iHizxcMkgI=lOy^m?TJ8BTuQa!Pht)4{uIPBDt`8}EM5>IFLV

R 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

bT4nH{L7)yZnGaBK=QpEmnT zqs4>7Ei2l2ZsUcuNc>-Rw_eVMeomhss%7iB__p@fEOoLu9Z`;Z!;-S6{uVO(ZLp5$ ztPOikW;2mRgLeCR+ZUlo0yb8RfZ2Px+UenOEa%TnmW;kG_K*I%m1=s}YT%`2SJ3F_ z%DM||Og={2^6VUEJpt9iiWDtT+~FBHvP60v#=>k+jI`BR%W=zxtbOrXvki*gQgd5A zZ~koi2pYv1 zUQ+9`&BXO%yaK;A?ltpWtxvWS*!p3dwdSU3)~ zS0eIL-* znf0Wn)wyhO0fA2L~6Uv<>FFPXHZ{}Sr<#~PsUN4*Ykxg`49=}K8+Ohob ziEMm*o!bxep76`TXOy*-2xoo$f${ioHQ2Dy^8s}zeQhf zEWKdVuy@qEU74f?C5F zhtv^E z*;e$%`P=Hcl{|KLID3cX;+wIFp8;=;^0e|ckM~^MHkKd02#?0E`#R_A@}$_cgWtax->>sF`{C-;DR>*v+Kd zvl7?NTsbRt^Tl{NG1=giVv5ewSzk7OEDo;y2ikc8nBxlv{nC(F9 z-fRu<>`G%?`nGb-c0y~7Mzs{*%|^uTjxvo#W$4*My$xd3eqT!+y?|Fcw{|Ol?u%#I z)#77-XMos0^|(L0AXYSD{hZ^LWq2C1d&FtMFv}uCGfRH9XnCM>lX){0xXz*Vi-ijJxNu;>O{@nx0=H z_eO|qhlU$-7k160&)j%;BKA8x$jmrtW?uaJ@xjcCNw=p%uAMk@R-EdK@nRy+!Q;ft zoENhSYJ60jS33o?Zv^&6T)ZqhAKrY!6ES8ireMsOR>XokGQ79tzm)1~VK=m!Sgx(d zRa>cla^kw|JBVz)r_+dKKC7q!VwxJQ zzFvLsvu8Alz7kt&ILDIuoKa$YkW=j|$yw|>#?^fdx}!1fdRzBqcW7LV_42HN_dQk= zI~x(ZJB>uGQ5kx6UvGn0tl!sCM=#*9&Sjl->%MrVT`eo&tY3?5Qx87DTK$SfOq*8V z?`AcK*v)G^zO{jvtMN7gEB_n>yDQF%Ts55%uQo}ilv9$ zz|ytOSC*%buEAZ+9t67-J591ytN9%;o_O87eL9-ESlp~_qU0Gvt>ti+UnXneKz={Q zGMAdfCBnUm722bhf_O z;gf56=QK6f?CHcTdc1N?FVA`Ab6k5KK3$U&$4vQ27T;|-WKXC2(!P7GB#TqFvhGFk z2l$3lCPtWLt&M8s_#N_^w6VshpVQhUGMz}9Q^w6rms7R}<0SmSnc;2g8Fy4^*sa1! zEu(az{8plLWPP67$~+HanR%u5mbJ2~H;%>oW8aEkho5~%=ic$t*VkZ$TH9=8t-CVM zje%WI{c>nA)1&+JFg_EUu>$X74e! zHN8Bid4uCB>hSrRoY-5+PqLU~%g%Z_-Iw;GYfV&at(A2z+LzBaY%OuEENhidYoPCt z*QAY)Mg5%CE`jNxv^iz0*K|2$KKKv8_QrZ{ThF+oO2cl2XU>?E-%6B@e9CiMndf2r zEw4G=vQ}30##VTL>|1f;@U!md+&g;u?izect8}f*Z`I6^uH&l>k6CISFt4p`PSP}1 zRmmG|tF{U?d;z6yfd(nP%zTrWM#bjCQRRgPe?~vD|jr&CXoR*jPbYI$>GCpa#oH8Hm z`{BPho4IX0@!K=e9D>!ZG(=}S{MG11^>utA@k^cypoCS`Iufdd8M%=L7tH883KIe8dnm*Gt~7OWCgnd$;SlFRbFU_4SEYxDV{b zd5BPc%R_oPtyCor(xwyl$g5cku2FK3nQOpDinOy1+GyBE$wTOC#V~ACDvfwa<{Y)8 zXT6PcunyMc9BX>ovNJZzH6>U_SC?4Eb@Gd}d3iEFjVs)Y>p5B0Uf#qkuEP=*GG}yp z-6L{!VrQ0!R2KQguKYLurk0qaaF1Q%A7%Ra)`Mspl$%cS#6z#(5jid7op}Un4wXa; z4TsPQN~_aq2lF-hYL;oW{^EIsAz`j}(UqZ>#V3T;TRzd#X)Pyli8h@$L|*S%aE6jg z%$xyUQKX$c&_=@|N7^ z^0Ih=Pz#6Bo!UA`9$aa)KHA!w-tIvjEy?*g(k(=xc@eZB#0a?Rd0x zi^LI1M$lsZS|0R%bU478p0%hI4siSQW<0O+*3OMnecstQK7E(Zot@p&U-5Ym{y3)Y z#GKo)q}ROthMw;Eo;Uz21I`1+omw(KNl-oOh{ETdit7kL+A?iz40rLv%_*>bxQo|s z{B{=cVCTPqNQHv=B{PKVd&-D^yCZ)m;X_)PQ-+7k2A@+xTGLMYt2)@FkyFZ$8g+)9 zoO0}%{(8x<Fjpe@tZ#&C~u&M8BOJcIb{ zxY3@LVIZEC;Uh|Y1TwBlbO3apFBDP{kssl(Hf&QfmKOn<#3 z;}q*KZu)QB{p)3AmDF{J4AFeh|Eu;q*k9s*S{w-e4iC$Z&<7Tshxr?uq^`$b^`iVv zID;B~2Vf4R<*Pu-Er*Sy*a-Sx&vVJCXwxIY!j9VzdFEb%Hiid`v$bX^!v;Ky`0c2{ zo|gRw93b$ zoMIiuO~1w6zg||hC0&R8-OUI6-^&Nv{u15Lc60N0xLoagFn?8XP+5*4y|1PHR;)pq^WLaD3UE=ENvv)dHpMCaA z_dW;22fWCq*M@ZIdRhz9Dwcj@>NM{azb88JmR}xAV*yy`h#g_SSX$}h7s=ARnF}59 zM^3y9vAn9Z+8j=z&#$!)0$b?VKVxmOrpq_KnvKkU8LPt{Wov_dCzXs35cNjNCeGnK_KSYdYrUoMsL`WbW$oNFV0#qwt4aKI0?G&1vSa>2|-8Zg@odmCPJ|guUU> z@mJ^{hDY?Tl9|Jl50Cb~8vbSPEAnWAdtB(4!&Yyfa(t{U#;c_aF3DI6JUfxB@IQ+dD4!y_1x7b zz72h6nS3mt+hoSvPO=pqsng(&|2^ROXX|B_<{2(@JZnQYTA>5ISekcYp<@o;_cFxb zsnTjw-hk$@XYKP&E_BRcYZFgIzCl=x%pA5l?Dw=bm?M?U9H!n#{GZY4Gsh~KIgGq( zI_Bn_W)9zH?&|ZSKFr}C!~eU>XZ(?JbDBA9y4@$z4L{O;B{PSA4F6;J@%Rb){}Fzq zf0fJ}rhNEu@5%6Qdr!!t4eoKFV-8!reai8%wivIDZus%w$@FgrlwrK*G;`S6^gK`V zycRm1tF?Vd8;9x-edhUhbi6 z+KJb4*~d!P>C2<_b2s)=<+i88{}qZFjr1t|9V4qJzzFkeQb@xE)8X&Je+_@9=dM2S z-N@wc3`PD@PLYf@@no&nx$Ck%!)qY&#K`HJl~2HEaj3B@SYTWA#w_MPstDXeOqt_e$Oc5TmZx!c}6Qc_RsmV z44V?WO6-6V3(aJ?;spD@Jx`iG6bxZseX8a6hk_Tt=^A;OdBWar&yx=SzGeoyzwKz= zXPjoPK#4|X^dnO!Q$pF9CGh+znI{gC4iWh)z0@#Yub9Cb$qaZ_4KLvNRt#V%m*}7O zq~HdTKfrNHE?}Huq02k~bR9W`^MEWn&hN-DCo!qS1Sql4Of$0I|LQzx`cSZeef6o9 z?+pb9fWI~JG;@Nzug;U^_n=|{yI*xQ_n5CUFQ7ytGy0Jglqn(V%m{dXmCO?dHHV1u zm0oIU z?3lhI!;-|B66&YKLNm=Me*ejN()6J~{`=}vE&mE#n^?f6L9`%BeQqd%KvoM3BqUGqoLcpb`V%Q5GrR4!SJls5?VQ}2{5*C zbpY?Xh`iJ4KqIpTz%!lEJ}7#j87PqvzE&t|*^zyV@P(d*&s)7#FG}RE_WH6T{hXg< zwOeCwkq{RSuwlnf)9bl=! z>R)q#TINlPETBxO&Ac|idsxZ5yT&ejO&~mMW#+4u|LLp=gh$+?p}U+wi}NB%iK$!fR8{(h}m$I8bWjsAt?TeQFF>WHLUFn|p!2c^s^$bFuNaRB@X zi?kR3&vr)s*!-6oEc`VWsAb-y$P&s_%>Z~0D@MS(YwW`9|3u7dWag`t|LNHO!;jpf zq1(JB5LgosDrao~Te0l_OF7y48QW;}53B&x=)RG$?ek1$v=542Xa-7Tgs&^tvbWE? zobk9;1caW2&s)7#C3xRJ=8p8$USD?PpYxNfc5CeK*Q#}_a=g*#Ur4@1`V8CWoZj^+ebAGyI$q{9hof(0k2CDsqA^QQO>(Kkc&$%}|PDv&?8 zyOLG5Wdci$`xy!G!yn)Y&Ia5Esb&N{ouTQ51@v@6^88X6%9gLt6*F+}q06odyeMYC zGpYDM<_7rS8Y^HH+qb-ldOG&-7?Nie&+7$!oqPUx%z!d2#=$Hu_(4bWkas*XhM`D@ zhs=$FDby0Pm^yEY=Ma4}te^adxTAs(fSxN^Wm|r*)VQaSJc3{q=5^$0$LTy1yTY2z zFm}TYdOG2IX0c&!c?(_f1@|Ai>}tV_;tM>JiX~*mfN!>Og_1M$bnNUge9s#*uOjqy z?)&2r0m`&^2(!3g4IRyW-tovIh9VvAGdBwUP)n_^P%FP$-y!;D3?O+DaYqG908>}8 z%C@Xwsc~N;`2@i=;1tdr+z+W{4?UeB?}jn-bVB_#YiQ*@TexBo?rF65q`#iwoAEckjn!{^m5Q^E=GKRiR`wy7Ik*74$ zeP+qbByt+RDXWooAkqeLSaaS90Zh6nv+0qqCI>_9f(1 zh~TRg3td`yP<~Ep6{O}5cKYu^=P`scgWwDsuz!|0na1ky3&HoGb8Z`XD7XaY<;=Wd zIC*#G5ur-oS>44PKBEp8-FGz8R%8y-XE{wi9Dx4tJ$!Z)cLOK+*nD?t-hgik?bNy+ zZ|Lc+xJ2Hk@alb)0xL7+P6f$ncf$G7TG1$dm2NHM?2~k>@=qd?NAySvifTNkZBKIL zjTC&Ja-*|V33e*v6$m>$#V~%YOREmb&uOiI)a=1ds9oqh!dK=GocqExvdsA|R)-m4 zEP}IG8+j<01ZT|5tYUb`Y>X@-RB2}L${yzMh&u29-_J~2kvB|_a+-V?0R7>6cytu^ z0w?*{ym@LKfwxMBTP#__6_fDQ5bam)c`EgEFV}l1s2|1N1<7f@!uir#%_zN>ZY|{O zrF5(ESR%Sh^ic{#_swbBkzDyF1>dFI=xjxTy$gBu!A=}$RwSfLD-Ly9s~#aE&Z;CW_Tzh8Tz7ywFA-3Le3^EHkGV?lT){OFNiDm1YL7>|hQ*QU?~` zyP0V#vWDr$oF*UqPk;Cxemsghfs=e}{yjB|z<;H~ES8+%ibu%*=}q!y3i8(~b~8v$ zD-P#NYd1vc&va`cXTPOel@HV21)0N*)7v}O`CL1FZ|BH9@5d;2WxPA(9LJJ3Vx+t` zCEne;r@^8hu_C6kC#!Ude3U$xbVg=Rmm(h}?f|BrqaoEg=L?>n)5HO;cmk(Raym(^ zi1-;8Bz6Jr0T|RPJ#oW$kGQMShZf-1Gism09YDGqTkuFp^}FolMFdMq}FlmiW9UPA?gySJWJ86yyE8Sciz!_llpmgv$Z$lf3UpTZ)E2Ko_AK- zB3V^@fOR58x9aFJFYq#@eDIvy;P~_xJBNG@EDm-V3wtl-ys6ya_9-4fGb7Nx1I0-d zQ&75P0@qh`Ejv)G)4t9bf#>IR%LI<7-|8;Q4)$VPRDMm29ZU;ZoB=xpxs#Rf^yY-; z$!BvCJ|o3vV{Cw2zNaExvxOTYym zJ-jc5ytQj1r~NwY$g=AxGLbVPkawXQK-+4AT}HH_ct_jh;oGswPjbx$yiBW8{848J z9zXRw>C6(mPL|nuT(JbjRVrD8%gz+ela71aI}G7stN}K|{)NOFv8`8nNkgd^&=y36v0y%;5x8D_j;TF7D(%u{kFE8*$QL*_A`4IF?JpN;VZ za`~Q$bj>5C7&VpaS((?zBSzbo;J!>s3saYSrTaR=-rO_T)A)||F!yxgL(F2s*t5Y;m=MElC^*rg!AG}VM z*_&MP2Sr{j`Gm{PACJuSF`v!-@FOWc8)FdU@;w#lnqf>a zhAP*yGOv$ejB$^(Je!mj+AjA)^mT^HtNjq8BlSinro=2Zw65J8IqlaecTGEsYIjnH zBIc@gQg4%oe;cd(B-aeW%d|ShCUsWfu~pBL°kYfVgp-OClTP#o5hX}Ij{;ymfN zKfc2%F2*!=pB!D1Y3!Z;hI1M8c@X}CrQUR=aq|>JU$KjyE7LGPs;yjOJQ>g68sO!I zUtE@NTyH#@Zh*8ymrD|A*v znRafG`9wU+p^CJ$u{zE#c8m1*ZLsSl?y+{pL(L=n9Ee&sT07_A6J?pqCEgZp!8_LQ ziJvRCFi)zj>|%U4p204#rZ?ZqavEVaT^( z6@6VgcOkP1p>kcXW*V7Q=&XjsD&n~fRive!Zc{Ld-6B2yO8{l7J?8hkYkMiz%)!rr zsCA>Ya~=j!mdWhlZDA9_=Sg@^1G>G9t<1+I{N6+`WN*(BFo!OwxHb)&U&9?npfdHeXo^yJzL;tvD-VTUu^ zoK3vx9OC*ZF<7rS#NY1~>l3}Tig2RmAvWWL|+DR>ZpLm6=;Y*!X7o1z{+e$;3)^+lVh37AI zEsr=zXRa08;>d8j^9fcfW^UnWZ!@h}^mRJNE%ApC&vnbP^MbQvA2(mE2&`yC4m7V| zgm~Ut$sNv*qwL)kZ9~h8%-ZvqO zcu%K&)#d(cS;Ln)tu8pz*k_fZPCEnOf3z_H&tK|VR-x67R(E75-q{7K6~+v-V&Q3T zGp$(kb42&c8zOcC(G)$&DAnHquIk)-B$2 zrg41A>cr2GX|OxI@{3ALV?Q#EgJQSD9y=y{SM|G`8?AUu@8ZLg&WC1A)evqg3)yem zHSwaE#U`E+t+ffxA)A?nr)w6mrfZo6JBDEn)Ge5W{CCnB(fnH3xfFfbubEhey#Di= zLiUYveP$bsTg5i;HJZ{paIBJGnepm%@x*kEq`F2Fj$|u1%Hs2PC6Hwd28oW^yQjM z$m>6^BY@MRSCs2Br(oPFPJu_!eD*W*mS|LKa^@7dOxCfs!@aNve(SgM2QTAk_A?h= zO#yz_ljY@<^0udSzx^E&7xQw0CHxFo13SShAE(3`_9J^ZD0WNiv17t_P&02qrX#y1 zICHsLf52y`k%iK??V5PeEMXJRg4P-YaYSd9;OUwXtm#@t!0zA$OOP*4It!Y4Yv)w- z<(e_b>p!m@w6F(o?nFD@9^Lfu=7VC0m3As$x{r~K}U5g%9mhYKyf*^Q! zcu110BoTQMA>=|BBXWGIik{}s#?TC$i*wWXW)PIm#UT317J`PM)tD0Ih=HM|1wADO z_km`xn8t=0tC@?_Y6|HCl<{>z-TH&%{Qhh09s4DAJgJmYFLvqT$=GY{*Vqv|a{bp@ zdu{AF+zUJ3w_e>Q;blC{E@tL3?vm&fKes{4mnr6LPwRf$J0#BM%XH<1u>l@?SRsBM zc2)n3T@=2X;_waKPrZM-_oKuH9g&!PM>adW*kPT{gt)vjX1E|la=oTc8Gp2KlFcx?L?R2dp}CN(0$6jiYyXSjN+Vgx4bhS08SMPnL1t)sZIg24$9Gvw(UH;e^l zK>MEIg^ox}B`vLY%YxA2%`FIXtHKWzFKohza?wvo7LOb+NWOLpf#Zi|I$?uUCh;Wa z%Nirhp!{b~J8npIBCQBZ2|LJttWA&iQ{Hm=DsAOBLO5kk`z~wGi#x7=`rODtk(jQ} zqA;C=_)PB-tI1P|ZEp(k={MSY^3$8J^VvoyP$T_U8N=%>`?Acb0eJQ}zPp}a*430*`q zk&uC;_PA>y`Z8TKT2OmAI#{MhJOA0sE~g#6ln6mSYusmn8WPXZ!kpG_=G;#yPq%hc zw|MQ{)RM+ieAjwcwNA7~ul9xb^=|63cUaf^ty5g4{)?2>2y)y|QVBV*Iv0@xt9U{V zl*`iT#D|{ftrhi9yN#ur=iI9-En7s58rmErQLDerFX=5}Na#S4d)&7WcUxaHVo-ZI zLRhB9n<;NO?Fgns3G!0|NfpQ;@fBr8f-s+* z2~9|HEm1<7gEVS&;*JNEs(EiwLqZ6W-s8^a=(BXus6p-JC}Ei%?W|}oyPS3uQz8ZV zrh%pk^pJRt8s_x7=SC4%b`;a~@bntLIf}R*a(+rFmAtZZ`3;Nbx;|C>q#QZq>DPlI zG@>$M6X(HY@my0{mW6~GIAgM)V&qn#g`ShCq5wc zPv^*qmh2y~1Y;{H{D(|ZoX|u`a}u>ntQnG+bLKuszUyJh_Xz(P`b+VeRIc_tx=4Sd zj{YPmw|tV6x2kLnUcR(H#5R%*Vz9Yu({E7)_T-b{14;EclE~971kvJMEUK8}3{Hs4 zqP?axQah1dBZ^?<6jY4sD#Xxp_FPG`swX7Ds-DudJz@z}u&T|ADS9jkJ>8pfD*K&V{pnRJofN*(=4a&CDnDQ{KT zAG~~NlZb63yTo8~*QVd13|95c;hv=Y99iV)7NThJE*53XaR+C{WwBsW8WeFSp$b+` zLB*)ALJ&Qt(UtVeKC7Bl%{dVfYcRHw!uO~|8zs$2)h^LyNMp{K`!D(Khb6xyd~ZNW zDQ=U>y-Z)Gj{YP`xBQuux2kLsUcR(b#5R(xVz9Yu({E7+tNQ-%&yU|pK_OS~zTjqZawUhFakQOOd4h?C=wHZ2Q= z=d^4Y4ci9xkccyQ=RpUFW{nrPgY{F;tZ~XGYsRZa9;|Ut184*ZTb@3Q?jd8G@mS-A z28fjUP||ethT?t~_;>}G)Si1pwRpr$MYfN-CrU~!tS!%bVU1g>HBX~&;FC7j5JB2M z;Rqqo`TmMB}+UaPL4*}wCo$6)3S6lEFV})BI@9sH|t5HS>pu?Vf_>|YrL>^ylNc6 z8V5sQu|VFIFOc?HFvb~=HE!5|*s0%1wvLt%?stKY-;l}hx%H#PBa$kTe%xtMQfgss zdESfmXSaA7eFLquxrPtYUJJ(yiO%<8gnQc=vi%u}SNc$?ogABN!y)pi%JbDGa}V|s zihCYwNW?*InILAc&0O@ilJAuEvy^nOOilYR-W01y_avf%_XA3}e!91l{~Sde29)vq z_~dXW;gPZjVUt&hPx{joXhAR>-!SV~vNc8K+DQlDLhY!}+KT_K9$rz-5KOvj%8u+B-5i7=U2(eVHELWY; z@QHr%4B~#b=5MS`%7pro^#gt}OeEwXJIr#v`t`nh5Q|x1KH*6YOoItzTi4^9`Sj~ey(tju%rI%PzvR3qm{ zB&WMb7MUfk;~8>me=oy14L-P0T*pJJg3@juo2_XlNloiqb=|kuCe3+$Egt6tS~>Um z@?GdHp1+CFk8H%sqBk?qnjy`Bw=ocTZ|aQGhY~$!gXd|-L%AJ?^<)?+=kbjyaFu+_-YlH8He@o~TjLv+(vGjj zfLiLbEQa<0PfZ4phuu8yRhUu8oGxAk0)*XLm zHl>F=sU}x;p*WHO6x4kitwlWfZFy5IVMxw!FD*);|3+S(qc-GArBgx{_?XY2AI8quc|o&1Iy~S5AI$ z?9rcbu1prIloCEGv0z1m;BqX;n>yq4p+wK==Xu)kTyB+OJsHN!d3>V^T*#U&=+}=E z*M_WyCu>eIPie=8V?bSg%DLmilE*j7@C&hUtt?laqw$@7!ZzIR)_lHE0dsnN;jw@p z4U;mbXXLA1tULbEkkofPsU}xQD~@ykwRPV|+ZRuMTi#Sl7_IwW=ZI)rU}B9g^xw$K zb5u^AVyd&i_xs?4c=~W;O6!bEsL^L-o|D%pqlTp#IX@z4-Gw7gTA1*4&2>CUof@4Q z&TR0(?czEfT@{peY}ss0yMbz&JJ)jy`J^99M=a{VX84>3RSofmw?OxTx zCiJ|vQN+>c+OoZg_oDVUP~-16|JMC}H=tbXkTVYG^Z!-^ukQDXP!K%xr`<3L&MVTx$Dq^XBDU-x@#Zj(14-<<~6Q1O3DD44Gsr4s%6QL#|# z5HO^WaY;Km2FZcrRv)Y;%Uz@+D#i*278)p#W8sn;5wN&Ih@e>OSLl-2GxXcevwcd` z8GkbhX&CASIb|Ha#)iJFN5i}B{m3(J@_8o=)vCO3QvZ!vuZ+igv?WjT5bAu=epP}%@^9%Z0W>zu9qe6qOH=rlO<7bqZTF^KL5UL?A)!mZAeQ8Y85YkkCHfPjX z&e6RX4*kaQqi|xh#4hLg9K?F`iFQKq@A4XnE1iJms{e+0U{1^`Jmcv}qmA*|ih2la zj4iHaV=8O!mLj%wf4RiR8qC`o@irqpnR7kUzMpqvYl|x^+7r1z{c^`3 zX2;p>JVm8Yo$+_0Fpc5slEc@~aK80u$a^LHK5@2uacwZxV7L37oA%RDeAW%FXU?m-yJtd0Qj0XQVghoRKnmOEuSeLsV+c8^G+T zobEBkxLD%z7U$1{^8HPodaWKXkv{4al9Z;fw>K{#?=Ky5A~tMzrek7B~64 z`G&4X%I{M?+I;yMHu-!FKOHEm{cGCaCWK$o)WdtU`z?Fard@BW=LXl1{%6S|Y)C(& zUq8|p5*LmXViejhX-E8^Q}S0&R+BY2(h=!{a=>>3C31XUawDo2SE#oq3WWLvtw}Rce#no&~W$oQkbV2^pEb#F#;BAdM1V(y)&KW7Aw^VbjH$m+5qAB{@;e*`?a?n zg3ElN^6uAtR!8Tv7CM71`FsmZN053P8ybXmuJj0ToI1}b>JsKW*BtbAN9%@8VV)E9 z3Yvq1u>UaX7II$HFLabc_hEB^RPCCIjQ6U%goQ%miLO!Ckn^IxLDpjx_Ie@vFjve0|98&FmyX1ds?7w5d74+o%KF%v&sQesN!$)h4f9UWsHFoy zi}4I4Tz7yjL!JZ75q<~uv~9*rL*lKsb5)9GY;_sCv?ZD|Jt68YDLp|+a2eaS<=XL_uF&- zQnvQICQrwn6Jyzu*Pqy0(sQ8~TYn)f?ZdV_)6=M|)sh~`{7%r9=q_tMqx%mXz6W`p zKM~V%Eap*Ex{WYb%t6%3 zsAfA~S|Z2lHWTO!rg>$otdFhveC>k<$!+e`Fi#MTT3Qffs%I$S+7q-n^3q_A@bR#x zZ8K&%B5%E&t5Q5;tIOD>FVdXp2~l?`S5cUB-}b(IODXQ0o~Ng@EqBQ4F<$G_YWlk| zwK>bvoM^t)o6j>}7^|uAZp-cPs?ZbC+a2eaS<=XL_uF&-R(=h6O`eWDC&scRufJ() zNza8|Z2g6_^e@};OjD(@R!f>R^YKArq`R#7jP8>TKaV`mpNQ#s7W1gbo?>a(^5;&U zu20&M$9w&$bUFP$48QfO?@C_gOEc4b+GlliZginH+LF&YH9b-4acpRj+PTst#c}E^ zxu{Q?^IUt>+a0YNdZl?z)GcWa4#J-2s9(x?QODF#4*i$S1^*9RTb1!%)$RjnBH~HY zQQwsFqRvV9+%y*1E`*#Hbx+c!m?r{wU0R^zUoy{$Iw<7`-AfaqU#^2{=Se@LI(U;% zURnWN`G3&lM}3ryfI417cg_C;(^SoKc+Y$swC#Q<((Z)0Vh*BSN;TX0(v!Ki!e#=k z$uzHwmG!YTpRaw;p}BtpHO%Wqqm~{BP1ZA%aQzYbEP2E*NBGm&)3zBiEt$98&Q&R% zvDIbl(wJ$^^n|Fpl&g4Nx^H`5zNZv-PS4X*+Lk-y^%$@9X*K=bnA)7>X-+iX>doi5 z=Z-Z-jdxpahX;$EklyY%$IOyOuDjo!`%pe4c}<>@Q|J-8m!1G-PPG&?gPQ@Bdo5ukqiO`AI_->vZU#q%lI1^lQFJlLoDo&(|zy$YO4< z+E%ywzqaS{g~Zdw$BBo{Uvr*!*nQICy4K1|Yh>Nb_okJXG-a+aw3@`+O4_oLpEPF9 zXC#%@tmH?{8SVbQ3ug-QpNlye>^Vf<%Z}+4HD@uZ7yrf47^Ns*QDde(*wLedIWX-> z&Wl>IJfasC7;CHK=`6m{+XA^u8z0A!tM!O6IwHkwaq{2 z(Iw&UVwxqJHBzPrT+Gp#hpt`n=c2T;41YG3br-hWhhBzZNuY zTl3hL(o;(sz3IuAj*a`?lE?dG`XqEhXkUgu^tYuc8oyoUCw)?^15UjS4wUxh3*O{O z-_*)yMFf3P%*8|Dw%6_cpY6Gvq$sdr(uR!W7bV;wwR-nCtVk#dC-`pu^R53!4J=bC(77E*@DLx-ZU_J zLwzi0qk>*)YaTmhdZJWk&md@|xbH1_yicZmL4$&>V)$+USJJDD|FFzY+Lu_TL%$(y z1X_wud6OsYOe>#N60|Qdw^wbe+x@TGb6IhC?)W(I;Q6cW@1p*t#dVE{mu6&t*Ufx; zT6swe0HXu+X5BxEDM?^Z*8lLP`LS;GGX$Gu97{RwY%gG+Crr&vR1-!bo5d1}uiLwF6-99S=Hu6dxK z2eEPMKEsH>)QKD7MQAjRwKW^kCd5b*b-DZ5_axMo9rNw2Z7QY8<67Bm(< z6LwGNG0@0ptZn{%zJHwU=~@e$HD=v(Op7@>{}W9^m}P11q~95yoWT#zgeS__L)n7Y z{UhEt=p^)p`tv|{6SO#6^VrMNQ!9H0L4U&-x8(UwpI=OI?q1(+pE#0ueL0c#{_GS@ z1xR@hDU{%w6%=wfjkFNu0+;t4{ zX%gbEc|y<5lU~4d1MYzl#*=vJ%lmU3LZ%&Q@->Fhv;p{im_LYX8A={85mOnwoJuNb z4$$m?u>CF;xRcI6TBkTVk7Oa8LE=hB<2nHItRc^}HZ87lxDV4hggLZ(k>;bt=N{1` zBwd2)!1J*$=EPG}eg3I~<_6D~pg+jDJP)vb|HU3Sme_z`S!oW?A2hl0K;hjpWUSpi zs$saF8X%GFx$MAkoq=lU2gbflPayN999D&)X_W?PqbcaAwZwHzfixK218<2&2j*YO zVa=Kr!^`_v6jx)3&$rCA5!Q$9xzrL!=d!Z7aDLJfFmBU7BppHIOd5i(hA;S*QEJWJ zqT!2+imrZs4FS78K(oT9Nkf3O+z`9R2b6|jPP-;x?;g*`bOMTs!IQO?qz`D&c!cmi zO_~5o%sA2uB($%dOWND7kv}81e&>A3lgBp-Z0y)x{y0MBHBVU9dFGj8dq@lshw;ci zL}*@hOGKaPDhi|@`5MD$*c_h)^JOrtK*b}@V=BY5D5-?w(X*fj@Vi*xP6%E)plK8h zt~4f&$jvK+(YX$##Z?aXVLE^?hjuU0L$vtZBd9(hd)0yGV_(dP=gIn9QwL8Kyfy-w z&$&Dgkaz#-9;lN2Fd$3ec+h;4EAI*3J;SNmo1+?r!l?m{-=53<8AtG{r5}j*ujmP! zyp+SLFl?*R;9hWhPpu`c<96v2yodhpY;+*-r5x6*=@h)Yk3}&)miTv@!t9LOv;hgRN6v)TKN~*fTSh6)y(P`J>{HI1*Y7+1mQJqXqzKTR?{X2!qlbb% zKVE0ghP9y`9}&Z2={5Z9aS*}h4cc+}-V>gWA#_F0;K@$tyg|np8C-?SDKX;+r6=sI zo=e)>=bL5yL>$h@jh^!R1Y(5xfyl1GfoREB3!QVCU~^#GOoT`X`XJS~mPG>QgS`U}U$=F!0D zT%*zADu??pL>}faAoDh#d$b)f5ofCoJRkdFPCQxH=bAcrS>SOHuyxL*ttx`OqGsd~ zBCk*J<}WTvWZlE7jS%^>Qz1`Q#IF?}QWo`?HrscGlocaVxDniHSd#C!T_?U-_F1oX zRs2X$=kJ}Wxo%Nc8A=%P^%#<(gwFX^7=FEESX$_Kf%|K;h1MU@H4H}7fbbocWYYXkCt*_<_0}sTzN<^Uj?oH9jPj}q_mGB<&_ke z@)J<2;zI-rxMt4m>ocy|=O_sgz|lL&VFty4AEt)MDH3HlU*i$q4R=i!w_7xUu@!S`_#AE?EJQGf{HtD;@X;}H-C5$HA*QOnt zrF^Fy|M{%B%~MErj?>&?oRF2{m_!SgVO^YXSWRpE(Yh%t*Pthqr6~`6?RHk$v?f zaX;mh7E&lN5pwg4LrWDVvc1d|Pq=~k4Bp9k+VKd_=0b3i@C@&b;RtyttKX9Lerg=_ zvhb0&TjHY@4}3UbA5K_)hV+Z!A*_jJw{xtN(j_t~DPgub77|jMbIoJ9Kt6MhBM6?3 zVIiTH92?2@C3{+Ki4*o&;^TcgLqu&}LPPS5<*l@M_QFLa>M=ys;0dv?%B{B}A+(z2 z)M1Ug|AJlMSRZqWxO-MJoD=p{NwKnN6`L8Jw|V;c>}RwM#_*_f!W@iaLOCfd98w}2 zq~IBcmMVl}J5(#4Pyq88ylC^Zqa9ZHg`ggx6W$v`1@fj{ibjt6);hGv9C za-<`x5U65?WS01NiO%p%o0rgxJUDqP4cQ3glo-bFQG+M^5mC*N@K#N83bA7v_V!|z z%PHc?86FylfrsVjvyx(Ea}QXP@DR<@&u3YpZ4g7WGj-UR9Dm3UG^K?qO1y$hJLAw& zg;#9fVa1b7&wK`N&phqe<)Sc*umE&?h@Rgn8Q~iFr zb9j1<-}RmK>CgE6s{46JT@N`|LrJfB^$j_r_`Or#ZAg5vos>UgQgxjrc5fN<$fongaG=T_}5Ez&@p8ef}Pf1FJaaW`Z3f% zTj@&v|7`NJ{t{g}{_RgFGySCfcYS@lD!w!Q@z-}bu?s6pDVB27v-8vnYSO{uOT|-* zc}BS`Djj~73VLtzF3ZyPfa{aNuuMag}+{srxxlkf8@0PdRfy&(~n7klucLi z|K4OI{Ur)+{M+B8%=D9PmglQIFLB2VF-Yi@zrMSISRGkPF<+w=l&4lulZF(p5uRGi zGstwrT@@8weZ*L@YF&b=8v5AUoUH#LjI%K ze|5^HEBTM@|C|02tuy}Z?^9;_Npt3&1M#YOne?i^r-S|0oJpbIiZd(n-^hOr{d&%C z)UU%YpyNNlzY8h+T|!PQnb%f2wj6tk*IoAHw|;xXzC6#bZi5&Iiq(dv@=8`jz9%*9 zUmUE6c&YAbH?_V*iY<@*u;ijQHBR@zt}Kd^SF1{uJZk@3G2M3U>^S^ujYjTi@`+Po zz3GHPcB0FJ*J$4NCZ8BMC-3oXU&AQ$a4kFjw);CvbD=sx1!lWW##GH z>a!E6pz*$<{=hsAT>BIB{soPWA9`&(;bG;P;PLU#=*SM$OgdU|-gAMSyEi}WUp zSNxPl(tA9HQSNzsBY4TaU_uo@&r|GJ8`}{#S-r+vKM=2fMEeEJ z>V4GxbN}e@0sMdN9x=XxX7y6Odvy5$eCl(51Gak>;#u!%>WaJf8fBQ0HqOIwAiLrM?(U%IbDSWMEljFs6-4Pd7pcnFoM&_zm@dQTva?~ zxIzOhjwI<43GnP@Jf6C>ULt@tmo@rU`p?k7tC#+HE|u)hGo4BJZC)$sV}>i4zr~TfT*~`AyBUwC zZmpMcew&+Q`zY(Hy|*&{tCR70E|rAOGo8utZC)+;AFFOld4JR?Bt%L?*3hc3@lCABWDrd&>Qp9P2iK z4jX`E@BV4j`OoR7-)A0!roTh$?{an2^j9>xe4b??(_;^4a(hZ9IbIfplJaU!o<`$2?D~{UqU=MIxhXxjt(BOS%5l$@ttqnrv3;B-LXPFKE-qBjGbc zg;c-H=gieg-p2ya;7Qu&=@t?`-`O_THT4=trupY>sjvX=77~B_tF~R&&)Ybqd2_9n z>-dR+S<$RiEBoWn@pq58Ka*x%=a0qOB=G)m)bY>hsLy8}gJvHqVkOB}G`f49Wg*$4 z|8H`8N+xMu7KD-xQhX)NM{5G_VIk#X5h$ekC0dew%=5I`NpikfATqj^?xU8!l6eJjwh#-9pYsci-l^R$k-CwEVm+ z6(Zo>Lbo6PCYhhNagzUebFG%^^NF}v(X3P}`{U5(caOL~lWAS{@6hi{4(}gDeg2$| zx_jm^Xz@EV`lUQy(dg-UmW6zeuD{9cDVbz?Sqn-!$nup;A1w#GhlQ+<_Ww~X+b_|Q z?_-{))lQQ3&DxOBwTvG%`lXEj>g0Xq2~VCsNEI$UN(=><52T(=RJ!RQupw3qkH_~RMPnCkoe7ary0+D zAI4@feDwQCI(>Xna(N#KoIB6teaYk{$Gw8&-6Ibm=FepQ0sS=pcMG|{#T!+wA?Gjg z=koqr`Cpz%xo%(HKS{UmI(|HsDtZ4^$^YiL(~$KQyS;@0;3ECAZVQRO&F#%^=Nfm$ zdw28NJ+;T%84y54-Q>*~GgN&R)80rS~uNdAf%-a-U$!2-NDh5X;=&9*f>*2%LOumo)KHO1||M5kc(kfG9fjP&$f#lUA&mZQ`@W2qf zc$j}{S)M&D-WWW1YGkm)pJRcy*gzghxeY)bKbZ}{YyrcA@eFM6ez zm7@Y|0eJ7TlC9O_n!>L?A%m3ePVf`VP(exIPe|TH`To;3^7!!7cLH?m1aPTd2q2{; zvnK>_Y+ftel}Yl)l&qzHcJ|MBB?@?p2!w*<`~Rj{1iG*B(a$UZIYw$|h|1Azk-aw_#&+E1dbYJ3=pWy(V5LMFbjdaOd zQt{*kw5#~f3;fyw&{1~Ii@yG>R+Ia+2jHNA0ro>bIRZFp@}ku5*XZLPCH~8dAx8k% z1n|)3y|%kJt|@%?HLm`?Yy&CX-DeF^Bl`H4RPNKCr=QQipSF>?hez)1-{I|_r2UkZ zT%M%;WAkL;u1s1#Mv|{3{jR|)CH}Y4fAR||y?u~r_0x`h zz`49X%;>NO7>wZZx6ApPxHq`LFXex10EQ{BPEC6Oh|i!Cm`xt%8)aA70JP&^i68d? z=QzNl6#KYx{t@>C%El`2Y*+y4AGtlD0mm1Kz7)pFm%*?^#3_mHGh8rcX~r;Ng04tk zU)&GeJ*5uvyYCI=OBGPS_%x--M*~m)T>Ga7K_egOL(}9ZRB%+$b6n7Ak8Xtvx{pVA z%pm|k|TGx&}!8r~XW^~vOTzLv@;y&R9zr+Ao1Pp8NG^X7G#Oq-Hu;YOo7idqy z65lKfm8~G0jBAp8i5nanL|egG7y(p38wn*i?nv~dFjiKHVTFia65VHbVa(FlDJHDY z6=_!S!rtKSF?EpZeV;HZH=u*@aY~bq)}Q`O-aiQX`A8p{UO%CQql%v6g--i*D@#Im zW1MUSW0j^~{PdY~yY8`9)@%V4&%AqP-a?)4A#U*H$-{dR_5^h1*;CTqAFJ5<^?d>= z*XGAP#e?m>?a;U9UPXyM{&Jl1JgxQ|TOD$5^6$^--D!_N$G+i+9X#!O1nP9YUqE>? zI@=#$`4z2w0;Bqxu9p~>Xk9z@2Iu%-n9*ThFqk683!AuKxWO;60rmmIAUus}KLPPu z*hlO*A;$~awXnpGdxLp%9H-z-Xy0>>qeLG+7^gf>t3AhDhwKN|4uG8Aopup)>>rMp!qdKs zpibvI3Y0gavmFGMU(wo0FsiTVdWms~w(AR`h~v|%J4gI3oxZzsV84?PQ7(@yr<}u3 z@;F4LdtI^R-aVb8h8k6z4@Hbb5b}sZym1q1Xz_k%NFweq;Ok&W0t92|C(@iDYem;Ng>A-?X7_s7r# z?KNocFGmwd=0Vy=LeEf!_=*aIRp0g%>%N!hV`BWoJWs2ih$hzLmGoXf5E^rx7Mjr5 z#ZT#k9yBgu2~B&A13jS!jg$O}7K#|u=5)Pe5sQQ-@ClothZL=kr(uzC$qfMm>S5ou0QHAKQ{<0>meO-E$0JE00z^3Cvj%xVrOVmDkjCc)Ak zSJ@g|qK&j9;~Y`!SM*tk0t7Ne7K&B~>NI)s4q`Rqfizg8p_Fzho|VfO+2KGuowz?B z=f$W(tY#atq78NQwZzxG@%|X9pgoiHOWONm)mpzo6-eko>Sz5`d_@XkHQNqk-S-lG z%>BhYPpkciDy)x!-U~=V_fn^YDl~TSQ#zptjhEO$)2`$|Pbfm;B)_7CCI+=RT`yS- zBcTdB#bzj?Mj7Wr6(doGJn9g$KcR>g?}vsg;!XutuOSN%jafA!%{iimOjvEfN>S4h zVeC%mf|Yzf{2sBI1De)8xr# zh!u=q(qNH?Qd(om(Zza-KE!IaygiG?qxQ?)3mtvM=tHb>%U4t)@pW&!KZY(qCJox# zX4P6BLKjHtLHgG|TBu@Bo744@#WWJSSbemALFnS}^u3*ZetQCjTUxY73=8&)*J-CX3tenN%u>EE zhAze#t^E#YJjflbgr`?0ydnHnC*4z0{8ol0$mKd_=@M-a?b0M)`m-`GiZ(zeGh}hl z;O!&bmI&j#Xk!3bq?n}%ajduUbHst|q(B@_&k+Z8nutUEh$3Xa(7osM-V9|pIyoD0 z_`NyO@cXILLK+%xL>-<^C_`h*QAeOBl%cWas6%7T(MCic33c$i3e>T&IH$>`A^Lo% z!_G6TY!>S&-YKg*Ad$8FofhpCZ=xOJEYz_H@l9EEhC0R>EiA&?C3mzEo?hK#ZSz~* z>>iWiw?eWV0`eHMbcsTUifIxs{aKk8MIoS;8R9r-@b+iOBS$2T0&>JLMo@pM8T z8e5J=0zIJ*jWtIj8f%V1A}UE}1f*D?k&VSeO*RkF=R+g1QPgN;-Q?jXwP??H6YU&l zp^;69gUYHiG&0WU4o@mpHo2pf@bv0FYn$Kde)otJzZDYi5D>|jrAu@|^h}eC>Cei% zC^`Yn%#g@IgSS6JBso%X6p$m4xWhT|1`tPzgPM@YdMiIiDA-;KgyM8SAQ=*&P7|Ss zA5n+wAG-IP-kYHkM>A(56u&n|CVoG4TF6A>jVQ&_36*GUIZ6rigi18l9HnTiIXa1G zC7~4Jzg{p(*_{3vCoSmrtL}fa)SHG1SBm-kk~4zgO{ zk3vxmbdu~P7mZdn+*d>|vN4ot-P@9V#P7`!ir{OMJpPs(8^JDO=tyg zxduMTtt6{^+kC5J1*oGoJu6-rg_HiI@CsI;45hRX%W!iz1F;}AS19Fmp_W~A^3is( zKTAIgYUztp8It{EwZI>RrWz@63KUN+oUL^HBclxf}L zl6}SR%@K>=kEi_}O2pzemKQ}Y8mrLDQFTq|rTggUC83ud^s>X5aE@i(Gz4>eI-Y@G zhIArafmD|HW7DcK%zV4-r2B@YWnG*Y$GabPia7s=YiZe>fnGRk?+o-(W0jglBmY8v zUC(N1S;8*6_-55UTy5U4fKisPOE0XHVHd+r4chS%h}v+Ic8bVK;X667WK{f!M_QJa zCG2uwJ1`P*91dR=N354fmY?I(Lk`n5AWN25RA4zB5oujZtbEjrHGI6{ znbp{cP3o>`os;|`yG>pcm28OVRHKp>Z;j^vd=?ektDd2d5o=72QiiSQ<04VXKKGdK zFsbRiaBp)|;`iW4#K-SxAA7^!>uEgU?kxYW8kHO{Mxl~;-T!)h!KkDIJS*8)Wo{=>x=GVKOZU~E^p!g6;a7~8$r4|?QF0VAR`ei5!tdpykbUk^ z`^}@}q^9@6z0FaG--9C$AHS!4><#~}Y4^!l{$4c-Ibe)JA@REZ<@$nANC#SYHoM7r zQONTC^;fm8{d_2dsJ(^1S41JZ0gdcsyG}OjJn{YOtm@$3#o5w@g@hd*6|J+CUu5^m zi=vJV(Qs?j(c-PqCqlKiV_rkr+ zQHS4yBMl$Fr+w@Vf39g4%31zhHR?EEj6xmpy8qzng`*Dq5V4K?JnVD-8M{(^cgmZx z2j$>&w?G{oc8PpfiXz~lJy6Ng8+K|3IrgOwG7U+Xr zAsN!>h>%s%m%<)wqWKw8sUu{)Dx@)@pKl?J)gphWkVTC&wBNgB=NPy0pU3`DBa52W z{(Rk=P=sthDed<#M;bo%gfx824SI2>$yrEa_v_sPX>`~g@|`D&e!JJ$X|lxMt58Nw zueRDY=DSP!-yz-qZf^5f<||U}%R?94%>#K~aYl#lX^)X~6)9`XkVT55%Sr}WcvM|h zauFSim5c-%5k{WQQN~yx40e8G=%RaE)0cu&9HZ#gQL)k>Z<5qt3wQB7qS+a4|XriXIH(&QAd8YKdRP_ktI7ujUZ}T`|ovcLJP9tq_p3| z98vh#6Qb}jH|WJ3BWEFs-3Qk$3{fa%*7aGutdkH)>s_L1d33GU9W!f>$h&(PhF}N# zj9xV`#c0uV=U7AhjtIKB)uPSY?o;)m*y7p?;fgvAZ$cIQNlI&vOpYi@Dq)KJG>#fm zyqiVXrQC!mt|Fnt6E}L|?N$`gV8II(c~?jtNUlr*a5Oq%%51B{ad>TtpMB;R#JpE=#9#IC`S(R@8J7 zALol@>FzP7ahQJHkwi(g_Mf+SOP*3Kdqj(OUKBB)_s?w<r-BF{4nqEQ?vdicmgd)nckG+8!f{nt*Mt(pMb6R7SO(O2@h_;D%CEC~@_cGkT z?(-SFYTyV?=m1;H@rC#ur)}g`iZ*Y%Kh%rjiHITM^{f*+%BZ^~)!8{6I^RPl@)f9J zI;%q{qARF`BUqb@ID$1i;RwoQ>2x|rPpsXFB2HDr5{g+B)7>M^=rC=%V~CP!?JjTe zmi(Yv)`u4Fy!c^2%b!~y#u+Vn{XF=g|48?h@WY(eJ;wZe&qj?GYI+Gj$O@x-5Pm4r zKK2G)2o?z+8~Fi0%xR5P7KpfKBU&Qjb^YMI7sd})b`*E_@bntLIexeva(+rFmAn#8 z0Kd$+-iJHa_`Q1?PYL(y^>CW_`%5ML92i?kJ-wXah?MT0UP8vzDSau6Fo{Ahl3>DpTVnqu-5a;&ka!&U#vom1?l~r8ans+eah=JLRm|}Rk zcZT=V5{_7fu?t7!=`q~HGLG=EB^*)G5j(_d#18SwsVV6Da}=RyR5jf~3yu=zC_?-i zMYMQ5NMo0>HCj;JHh*l|CfeMH9pbgb5GBPaec8z!DXo1V2{W)(3Mxj_6+Y0JeS7Ko?$^_HNNQw)dPf-xcn0ty*}Y#XB#C zP^8=(Lktz&b7n;gLlEKi@p4XgLuX}2oS?Fbh+FdxCM+@7S>|4TAZqY-f2SoZu?k}s zmdMj%xQAsd;bTi!qNF2!h}Vc8;+1n#(Dvs@Lh+_*x`h}VDa?_C_%)Jf@p{n4E@f-P zpt^1T*tATvxe-6aYl$ODiu3!jvpZ5+J3SI^V09E!jHN4#pwj?L`emQ_jaBoX6M|rD zCH3fXh9^?Gdvpm|S10?)L&{_8&N=h_-X6KCg&A7B^Wq4_$jx!YP|-c7R?xCk8v)+{+J-bf)<>52qzOu?k}sp2*W)OfA(sG92+bu*B+Aa(<2-(8e`6GdzL(t&&Up z=f@Lml+k}MM-46BduE03zx_!<4S8BtgoGgW%>O9S`=T@4a9GioD%`+#>y_dNzEofV z@Bs2LzDkA*7~_n0Fzs^|h9Dms1^3FeGd+0XDIS)!mEnvQZ=h7NRFsrjSXX;2T0BDl zZ4W0}Si&JC-WV8fi9gmA+5mOUI75achNmT!+N$^1uDf zgdy^@N7jlMf;Sy>kmHLIQ?Ozxj8US9SB)x!9E2n!E#tRjD1kA~c&uvB32T{h(f^5i zZSWNL%G%0MM~gR5D%mbdN-eCbJr*sV;eocd6D=&Ek`jFkjJHH1>k4~-$!44(!xh8h zk~(~e2t#)q(UK5{Xdj*93_Z!3_R&S0K~ypud!!@2h}VcO;+66huIRpNb7oir{k=*q z@t+@Sv=K>&kQQ&CRI+B2lv-F;Y-9Dy5oqQgg-?4=p1$EN!GNFE}{;i zkJ;EG9nnU-Mzj&H{ez#Ki8}UnuAipgduDGq4A|lMQODtqx3pihuAWX_vIS!}Rg~L= zI*57Hplf8|k#S4wg?J^>m~(_v=JtsOPf>U)l(E{e*t|1te{ zEDqg0J2M>%h~@&+qWcIfFXVgfJ?c`&BkU^TZm2cmGfKN7&7#M=(rkj;@K;;P5suJ2 z_!;Z6&O~pkHs4X8?m^yPUbf9M#6r2gw6MTY?TTx>xZ`70P_nORglUI)O$+H@6*27Q z=t%9ic$x)Sh9V}?4wI6jxszQ?eq4E4sFb}R#?RBM0y7bzsX^DcNM%~Q5+|ut$q~w$ z+gcht#R{%467Oo0+usyE;{6X-$2Soxv6>9;M4E3>fseG0^>o%>_qm^s z$TL~s3(3V%XxB#f`8e%o=Q(fBooaUAobGcw0Um3PMhNE8NW)kb@;&!+>Qcw!04pwV zs5Rqr4|YeIMUT)$4??2&-!0`Bp0%-Qm%c1bnN_I4cU-G`kT;u`ZSxFoQLZnoMb^uV zOV8coxOZh$P_h;2Nu~YqH7%UheO~&bwRxHaS-B#j(_Wd9qq&ofPkw26T5l-3T8y8k z6_+bnl87hOplej8GA&+-@>Hti2qVtzMh%`K8dnI9ceTmw?+e}W{)elhvxxLqO@`JY z%{Qq)cdXNbMoucw-K$4?YeQck1kQXIP|V`iT-EV#SH{Ot0y-q76;0U%FD-?d+yu6FXD`?+l<^ynyZFQCke!>b9O zdJmh-!>$}r9BF+$ug6_?+MSG_^kB{5gYJ`6*t^q#@|s*MW4&3?*p*MMgjlJuB@aya z$!K`C`(`uk=HX=11wI(_FNJM~-*n%Mh5sE<9T)Qk{c(A^8u z^6kjd)WKTTeR74I?&m}jQ!53>@Go~BefIdSpJ*l`{rd5*FsiIdLWjb@CG9vEyhi@& z>s7$OiH`VJC%Gse`NK}TM?MmIn|KlLIuB%XPw0lez>v(ES@7Cx{rzBONu8@ zariPDLR7906T3$~UUA9|&$P+sJ+OYQKh87};#68=u?Wdr+j+VlFbPOjwygeRM&&cd zn3i%h6GCY8gN;$z16`w*fRPyfKBI(m3Oap-`Z~i#*v&@Foovg|s5DZ?-kL{xQwvSL zo`>eGySMqcd+O2IG^2B_-?d`O4a^3k0e>pWotLe-Li+yv6l+Z=U1eKb&Cc*KzIoQW zAzUlh;@W-C@8Bb4R)mMy4`?$q=W8aoC&o+fcS{O?N6ja#2{ZS1Yr4BRKIHkyYw+PJ z?0sS;+g$93z1bXj$ElTk6{)c$4-EQHUXaYsx0`7<6yLxFKA84zg{6nzb>EJK?;`K; zVjl0lVO`x#_ia~LAXzVqd3*!ut9y7g;Ovy94jzi#!z&?T0;{>=7~ba2qkYowc1H6J zEbNiUj|Gj_0pm(B7+#ir(($V>w=lA7a>B0b$Bw(fhtR7pR{?V;I^u1eZ=?JiXXm_# zubs!2@^hgz`U0~uYlg1dd5ROE{<<6E=PJQBu;TCyG(@UgA+!-pydq*9&$P+sJ+OYQ zzZ?DMLbyt6EWAHk@^p_qn6WdZeiyu{i#cw!<&+z+