diff --git a/README.md b/README.md index 1dbc38e..b214218 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,54 @@ # tws-zombies-scripts -Combined working repo for the TWS Plutonium Zombies GSC scripts across two games. +Combined working repo for the TWS Plutonium Zombies scripts and native plugins across two games. ## Layout -- [t5/](t5/) — Plutonium **T5** (Call of Duty: Black Ops 1) Zombies scripts. Most-developed baseline. See [t5/README.md](t5/README.md) and [t5/NOTES.md](t5/NOTES.md). -- [t6/](t6/) — Plutonium **T6** (Call of Duty: Black Ops 2) Zombies scripts. Ported from T5; at parity on common, HUD, AFK, lock, telemetry, join and leaderboard. See [t6/README.md](t6/README.md), [t6/NOTES.md](t6/NOTES.md), and [t6/tools/check.ps1](t6/tools/check.ps1). +- [t5/scripts/](t5/scripts/) — T5 GSC modules, [README](t5/scripts/README.md), and [notes](t5/scripts/NOTES.md). +- [t5/plugins/](t5/plugins/) — `tws-t5-gsc-utils`, based on [alicealys/t5-gsc-utils](https://github.com/alicealys/t5-gsc-utils), with TWS HTTP changes. +- [t6/scripts/](t6/scripts/) — T6 GSC modules, [README](t6/scripts/README.md), [notes](t6/scripts/NOTES.md), and compiler checks. +- [t6/plugins/](t6/plugins/) — `tws-t6-gsc-utils`, based on [alicealys/t6-gsc-utils](https://github.com/alicealys/t6-gsc-utils), with the same HTTP changes. + +The scripts still deploy to the game's existing `storage/t5/scripts/sp/` or +`storage/t6/scripts/zm/` directories; the new source layout does not change GSC namespaces. + +## Building plugins + +Plugin source is vendored as regular tracked files so local modifications travel +with this repository. Each plugin's `upstream.json` records its upstream revision +and exact Git dependency revisions. Dependencies and build outputs are ignored. +The dependency loader replaces upstream's submodule setup; nested upstream +`.github` files are retained for reference and do not run as this repo's CI. + +Use Windows with Visual Studio 2022 C++ build tools, a Windows SDK, Git, and +Python 3.9+. From a Developer PowerShell with `msbuild` on PATH, at the repository root: + +```powershell +.\tools\build_plugins.ps1 -Game all -Configuration Release +``` + +This restores pinned dependencies, generates both projects, and builds x86 DLLs. +Upstream's Premake setup also downloads its MySQL client dependency on first use. +Outputs are `t5/plugins/tws-t5-gsc-utils/build/bin/Release/tws-t5-gsc-utils.dll` and +`t6/plugins/tws-t6-gsc-utils/build/bin/Release/tws-t6-gsc-utils.dll`. +Remove the old `t5-gsc-utils.dll` or `t6-gsc-utils.dll` before installing the +corresponding `tws-` DLL in `Plutonium/plugins/`; load only one version per game. +**Restart the server process**; +restarting only the map cannot load an updated plugin. + +Use `python tools/fetch_plugin_deps.py all --check` to verify dependency revisions +without fetching. Use `python3 tools/test_plugin_http.py` on macOS/Linux with a +C++20 compiler and libcurl development files to compile the real HTTP utilities +and test local stalled connections, response deadlines, status reporting and recovery. +These portable tests do not build the Windows DLLs or exercise Plutonium's VM. +Run `python3 tools/test_plugin_component.py` to compile the completion/option +handling against host engine shims with address and undefined-behavior sanitizers. + +Both plugins now default to a 5-second connection timeout and 15-second total +transfer timeout. Request options can override these with positive millisecond +values. Telemetry explicitly sets those limits and logs native HTTP status and +timings. See the [T5](t5/plugins/README.md) and [T6](t6/plugins/README.md) plugin notes. +Requests are still sent once, in order, with no retries or event timestamp changes. ## Parity @@ -15,13 +58,13 @@ Modules currently in each tree: | Module | T5 | T6 | |------------|-------------------------------|---------------------------------| -| common | `t5/tws_t5_common.gsc` | `t6/tws_t6_common.gsc` | -| hud | `t5/tws_t5_hud.gsc` | `t6/tws_t6_hud.gsc` | -| afk | `t5/tws_t5_afk.gsc` | `t6/tws_t6_afk.gsc` | -| lock | `t5/tws_t5_lock.gsc` | `t6/tws_t6_lock.gsc` | -| telemetry | `t5/tws_t5_telemetry.gsc` | `t6/tws_t6_telemetry.gsc` — queued `game_start` / `heartbeat` / `round_change` / `game_end` matching T5 | -| join | `t5/tws_t5_join.gsc` | `t6/tws_t6_join.gsc` | -| leaderboard | `t5/tws_t5_leaderboard.gsc` | `t6/tws_t6_leaderboard.gsc` | +| common | `t5/scripts/tws_t5_common.gsc` | `t6/scripts/tws_t6_common.gsc` | +| hud | `t5/scripts/tws_t5_hud.gsc` | `t6/scripts/tws_t6_hud.gsc` | +| afk | `t5/scripts/tws_t5_afk.gsc` | `t6/scripts/tws_t6_afk.gsc` | +| lock | `t5/scripts/tws_t5_lock.gsc` | `t6/scripts/tws_t6_lock.gsc` | +| telemetry | `t5/scripts/tws_t5_telemetry.gsc` | `t6/scripts/tws_t6_telemetry.gsc` — queued `game_start` / `heartbeat` / `round_change` / `game_end` matching T5 | +| join | `t5/scripts/tws_t5_join.gsc` | `t6/scripts/tws_t6_join.gsc` | +| leaderboard | `t5/scripts/tws_t5_leaderboard.gsc` | `t6/scripts/tws_t6_leaderboard.gsc` | ## Map record announcements @@ -71,12 +114,14 @@ Partial page results never replace the cache. The API has no pagination snapshot token: changed totals/record rounds reject the refresh, but concurrent renames can still move entries between pages. Refreshes deduplicate GUIDs. -Requests have a 15-second GSC wait limit. The plugins cannot cancel a native -request, so a stalled request prevents new leaderboard requests until it finishes; +Requests have a 15-second GSC wait limit. The TWS plugins also enforce native +5-second connection and 15-second transfer limits. The leaderboard waits for native +completion before starting another request; player threads never wait directly on HTTP. Responses at the plugins' 20 KiB truncation boundary are rejected. The complete JSON grammar, UTF-8, string escapes and response shape are checked before consuming data. These guards also reject -HTML/error bodies; the plugins do not expose HTTP status or response headers. +HTML/error bodies. Leaderboard still uses body validation; the new native HTTP +status diagnostics are consumed by telemetry. Run the portable data/structure checks with `node tools/check_leaderboard.mjs`. They execute the scripts' data helpers with engine-value shims, not a game VM. @@ -126,7 +171,7 @@ not prove API acceptance; inspect the API response or ingestion records. ## Validating T6 ```powershell -powershell -File t6/tools/check.ps1 +powershell -File t6/scripts/tools/check.ps1 -CompilerPath C:\path\to\gsc-tool.exe ``` -Requires `gsc-tool` 1.4.10 (T6 PC) on PATH. +Requires PowerShell and `gsc-tool` (T6 PC). diff --git a/t5/plugins/README.md b/t5/plugins/README.md new file mode 100644 index 0000000..706f6b3 --- /dev/null +++ b/t5/plugins/README.md @@ -0,0 +1,78 @@ +# tws-t5-gsc-utils + +Source: [alicealys/t5-gsc-utils](https://github.com/alicealys/t5-gsc-utils). +The baseline commit and dependency revisions are in [tws-t5-gsc-utils/upstream.json](tws-t5-gsc-utils/upstream.json). +Source is tracked here directly; dependency checkouts are restored from that +manifest and ignored. Follow the [build instructions](../../README.md#building-plugins). +Upstream API documentation is in [tws-t5-gsc-utils/README.md](tws-t5-gsc-utils/README.md). + +## HTTP deadlines and diagnostics + +All native HTTP requests default to `connect_timeout_ms = 5000` and +`timeout_ms = 15000`. The latter includes connection setup; the shorter limit +wins during connection. Both can be overridden in the existing options array +with positive integer milliseconds. Zero, negative and non-integer overrides +are rejected before a request is created. Neither retries nor redirects are +added. TLS certificate verification remains enabled. + +These are libcurl transfer deadlines, starting when a worker begins the request. +Time waiting for a worker or the game thread to deliver completion is measured +separately. A native timeout releases the request and emits `done` with +`success = false`, a transport error, and diagnostics. Each request is sent once. +The telemetry sender then logs the failed event and advances its ordered queue. +A timeout does not prove the API did not already accept the event. + +The existing completion values remain in the same positions. An optional fourth +value adds diagnostics: + +```c +request waittill("done", response, success, error, diagnostics); +``` + +`success` continues to mean transport success, including completed HTTP 4xx/5xx +responses. Existing scripts that read only the first three values still work. +An updated reporter on an old plugin logs that diagnostics and native deadlines +are unverified; deploy the matching DLL to enable the fix. + +| Field | Meaning | +| --- | --- | +| `http_status` | HTTP response code; `0` if none was received | +| `curl_code` | libcurl result; `0` is transport success, `28` is timeout | +| `dns_ms` | Time to finish name resolution | +| `connect_ms` | Time to establish TCP | +| `tls_ms` | Time to finish TLS; `0` if no TLS handshake completed | +| `first_byte_ms` | Time to first response byte; `0` if none arrived | +| `total_ms` | Total native transfer duration | +| `worker_queue_ms` | Time waiting for the plugin worker pool | +| `notify_ms` | Time from native completion to notifying GSC | +| `connect_timeout_ms`, `timeout_ms` | Effective native deadlines | + +Curl milestone times are cumulative from transfer start, not durations to add +together. Diagnostics are provided for failures as well as successful transfers; +an unfinished milestone can be zero. No request headers, tokens or URLs are +included in diagnostics. Responses retain the existing 20 KiB truncation behavior. + +Telemetry logs the status and timings for each completion at log level 1. +Non-2xx responses log at level 0 with a bounded response preview. Health counters +keep transport `completed`/`failed` and track `http_rejected` separately; an HTTP +rejection is a completed transport. Producers, queue ordering and payloads are +unchanged. Heartbeats are not coalesced, failed reports are not retried, and API +receipt timestamps remain the existing behavior. + +## Validation and rollout + +Run `python3 tools/test_plugin_http.py` from the repository root for portable +native HTTP tests. Build the x86 DLL on Windows, then on a test game server check: + +1. Updated telemetry reports HTTP status and all timing fields on normal requests. +2. An endpoint that accepts a connection but does not reply produces a native + timeout near 15 seconds, followed by the next queued request. +3. HTTP 401/429/500 responses increment `http_rejected` without blocking the queue; + transport errors increment `failed` instead. +4. Leaderboard and older three-value HTTP consumers still receive completion. +5. After a full server-process restart, verify all four telemetry event types in + Electric Cherry. A successful transport alone is not API acceptance. + +The localhost tests compile the actual HTTP utility against the host libcurl. +They do not validate Windows linking, its Schannel backend or game VM notification +handling. Those checks require the Windows build and a running Plutonium server. diff --git a/t5/plugins/tws-t5-gsc-utils/.gitattributes b/t5/plugins/tws-t5-gsc-utils/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/t5/plugins/tws-t5-gsc-utils/.github/dependabot.yml b/t5/plugins/tws-t5-gsc-utils/.github/dependabot.yml new file mode 100644 index 0000000..f1cef93 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: gitsubmodule + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/.github/workflows/build.yml b/t5/plugins/tws-t5-gsc-utils/.github/workflows/build.yml new file mode 100644 index 0000000..74d50b8 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/.github/workflows/build.yml @@ -0,0 +1,80 @@ +name: Build + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + types: [opened, synchronize, reopened] +concurrency: + group: ${{ github.ref }} + cancel-in-progress: false +jobs: + build: + name: Build binaries + runs-on: windows-2022 + strategy: + matrix: + configuration: + - Debug + - Release + steps: + - name: Check out files + uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 0 + lfs: false + + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Generate project files + run: tools/premake5 vs2022 + + - name: Set up problem matching + uses: ammaraskar/msvc-problem-matcher@master + + - name: Build ${{matrix.configuration}} binaries + run: msbuild /m /v:minimal /p:Configuration=${{matrix.configuration}} /p:PlatformTarget=x86 build/tws-t5-gsc-utils.sln + + - name: Upload ${{matrix.configuration}} binaries + uses: actions/upload-artifact@v4 + with: + name: ${{matrix.configuration}} binaries + path: | + build/bin/${{matrix.configuration}}/tws-t5-gsc-utils.dll + deploy: + name: Deploy artifacts + needs: build + runs-on: ubuntu-latest + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') + steps: + - name: Setup main environment + if: github.ref == 'refs/heads/main' + run: echo "ALICE_MASTER_PATH=${{ secrets.ALICE_MASTER_SSH_PATH }}" >> $GITHUB_ENV + + - name: Download Release binaries + uses: actions/download-artifact@v4 + with: + name: Release binaries + + - name: Install SSH key + uses: shimataro/ssh-key-action@v2.7.0 + with: + key: ${{ secrets.ALICE_MASTER_SSH_PRIVATE_KEY }} + known_hosts: 'just-a-placeholder-so-we-dont-get-errors' + + - name: Add known hosts + run: ssh-keyscan -H ${{ secrets.ALICE_MASTER_SSH_ADDRESS }} >> ~/.ssh/known_hosts + + - name: Remove old data files + run: ssh ${{ secrets.ALICE_MASTER_SSH_USER }}@${{ secrets.ALICE_MASTER_SSH_ADDRESS }} rm -rf ${{ env.ALICE_MASTER_PATH }}/t5-gsc-utils/* + + - name: Upload binary + run: rsync -avz tws-t5-gsc-utils.dll ${{ secrets.ALICE_MASTER_SSH_USER }}@${{ secrets.ALICE_MASTER_SSH_ADDRESS }}:${{ env.ALICE_MASTER_PATH }}/t5-gsc-utils/ + + - name: Publish changes + run: ssh ${{ secrets.ALICE_MASTER_SSH_USER }}@${{ secrets.ALICE_MASTER_SSH_ADDRESS }} ${{ secrets.ALICE_MASTER_SSH_CHANGE_PUBLISH_COMMAND }} diff --git a/t5/plugins/tws-t5-gsc-utils/.gitignore b/t5/plugins/tws-t5-gsc-utils/.gitignore new file mode 100644 index 0000000..161cbeb --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/.gitignore @@ -0,0 +1,165 @@ +### Windows + +version.hpp + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Shortcuts +*.lnk + +### OSX + +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Visual Studio + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +build + +# Visual Studio 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +### IDA +*.id0 +*.id1 +*.id2 +*.nam +*.til + +### Custom user files +# User scripts +user*.bat +*.code-workspace + +deps/mysql + +# Dependencies restored from upstream.json by tools/fetch_plugin_deps.py +/deps/GSL/ +/deps/asmjit/ +/deps/curl/ +/deps/date/ +/deps/json/ +/deps/libtomcrypt/ +/deps/libtommath/ +/deps/minhook/ +/deps/plutonium-sdk/ +/deps/sqlpp11/ +/deps/zlib/ diff --git a/t5/plugins/tws-t5-gsc-utils/README.md b/t5/plugins/tws-t5-gsc-utils/README.md new file mode 100644 index 0000000..37f765e --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/README.md @@ -0,0 +1,295 @@ +# tws-t5-gsc-utils +This is the TWS-maintained source snapshot. For the local HTTP timeout and +diagnostics changes, builds and installation, use the [TWS plugin notes](../README.md). +The upstream binary download below does not include these local changes. + +T5 version of [t6-gsc-utils](https://github.com/alicealys/t6-gsc-utils). +If you wish for any feature to be added please create an [issue](https://github.com/alicealys/t5-gsc-utils/issues/new). + +# Installation +* Download the latest version [t5-gsc-utils.dll](https://github.alicent.cat/t5-gsc-utils/t5-gsc-utils.dll) +* Copy it to `Plutonium/plugins/` + +# Features + +* Easily add almost any C++ function into GSC: + + C++ + ```c++ + { + gsc::function::add("myfunc", [](int a, const std::string& b) + { + printf("%i %s\n", a, b.data()); + }); + } + ``` + + GSC + ```gsc + init() + { + myFunc(1, "hello world"); + } + ``` + +* Non-primitive GSC types such as **arrays** and **structs** (soon) are also supported and can be implicitly converted to STL containers: + + C++ + ```c++ + { + gsc::function::add("sort", [](std::vector vec) + { + std::sort(vec.begin(), vec.end()); + return vec; + }); + } + ``` + + GSC + ```gsc + init() + { + arr = []; + arr[arr.size] = 10; + arr[arr.size] = 5; + arr[arr.size] = 3; + arr[arr.size] = 2; + arr[arr.size] = 9; + + for (i = 0; i < arr.size; i++) + { + print(arr[i]); + } + + print(""); + + arr = sort(arr); + + for (i = 0; i < arr.size; i++) + { + print(arr[i]); + } + } + ``` + + More examples can be found [here](https://github.com/alicealys/t5-gsc-utils/tree/main/src/component) + +# Functions & Methods + +A list of all the functions and methods that are added by this plugin. + +## IO + * `fileExists(path)`: Returns true if the file exists. + * `writeFile(path, data[, append])`: Creates a file if it doesn't exist and writes/appends text to it. + * `appendFile(path, data)`: Creates a file if it doesn't exist and appends text to it. + * `readFile(path)`: Reads a file. + * `fileSize(path)`: Returns file size in bytes. + * `createDirectory(path)`: Creates a directory. + * `directoryExists(path)`: Returns true if the directory exists. + * `directoryIsEmpty(path)`: Returns true if the directory is empty. + * `listFiles(path)`: Returns the list of files in the directory as an array. + * `copyFolder(source, target)`, `copyDirectory(source, target)`: Copies a folder. + * `removeDirectory(path[, recursive])`: Removes a directory and optionally all its contents. +## Command + * `executeCommand(command)`, `command::execute(command)`: Executes a console command. + + ```gsc + fast_restart() + { + executeCommand("fast_restart"); + } + ``` + * `addCommand(name, callback)`, `command::add(name, callback)`: Adds a console command. + + ```gsc + init() + { + addCommand("test_cmd", ::test_cmd); + } + + test_cmd(args) + { + assert(args[0] == "test_cmd"); + print("Hello world", args.size); + } + ``` + * `addClientCommand(name, callback)`, `command::add_sv(name, callback)`: Adds a client command (can be executed by players through the console). + + ```gsc + init() + { + command::add_sv("suicide", ::sv_cmd_suicide); + } + + sv_cmd_suicide(args) + { + self suicide(); + } + ``` + * `self tell(message)`: Prints a message to a player's chat. + * `say(message)`: Prints a message to all players' chat. + + ```gsc + init() + { + level thread on_player_connected(); + } + + on_player_connected() + { + while (true) + { + level waittill("connected", player); + say(string::format("%s connected!", player.name)); + player tell(string::format("Hello %s!", player.name)); + } + } + ``` + * `sendServerCommand(client_num, reliable, text)`: Executes SV_GameSendServerCommand. +## String + * `va(fmt, ...)`, `formatString(fmt, ...)`, `sprintf(fmt, ...)` + `string::va(fmt, ...)`, `string::format(fmt, ...)`: Formats a string: + + ```gsc + init() + { + print(va("hello %i %f %s", 1, 1.2, "world")); + } + ``` + * `printf(fmt, ...)`: Formats and prints a string: + + ```gsc + init() + { + printf("hello %i %f %s", 1, 1.2, "world"); + } + ``` + * `tolower(string)`, `string::to_lower(string)`: Converts the string to lowercase. + * `toupper(string)`, `string::to_upper(string)`: Converts the string to uppercase. + * `string::is_numeric(string)`: Return true if the string contains only numbers. + * `string::starts_with(string, start)`: Returns true if the string starts with the specified substring. + * `string::ends_with(string, end)`: Returns true if the string ends with the specified substring. + * `string::replace(string, what, with)`: Replaces the specified substring with another string. + * `string::regex_replace(string, expr, with)`: Replaces the matching substring with another string. + * `string::regex_match(string, expr)`: Determines if a regex matches the entire string and returns the matches as an array. + * `string::regex_search(string, expr)`: Determines if a regex matches part of the string. + ```gsc + init() + { + assert(string::to_upper("hello") == "HELLO"); + assert(string::to_lower("HELLO") == "hello"); + assert(string::is_numeric("12345")); + assert(string::starts_with("hello world", "hello")); + assert(string::ends_with("hello world", "world")); + assert(string::replace("hello", "e", "o") == "hollo"); + assert(string::regex_replace("hello", "h|e|l|o", "a") == "aaaaa"); + assert(string::regex_match("hello", "^hello$").size == 1); + assert(string::regex_search("hello", "e|o")); + } + ``` +## Misc + * `array(...)`: Creates an array from a list of arguments: + + ```gsc + init() + { + arr = array(1, 2, 3, 4); + } + ``` + * `type(value)`, `typeof(value)`: + + ```gsc + init() + { + assert(type("hello") == "string"); + assert(type(array()) == "array"); + } + ``` +## MySQL + +You can access a mysql database using the following functions: + +* `mysql::set_config(config)`: Must be called before calling other mysql functions, config should be a struct of this kind: + + ```gsc + init() + { + config = spawnstruct(); + config.host = "localhost"; + config.user = "root"; + config.password = "password"; + config.port = 3306; + config.database = "database_name"; + mysql::set_config(config); + } + ``` +* `mysql::query(query)`: Executes an sql statement, returns a query object: + + ```gsc + init() + { + // call mysql::set_config + + query = mysql::query("select * from `players` where guid=1"); + query waittill("done", result); + if (result.size > 0) + { + print("player name " + result[0]["name"]); + } + } + ``` +* `mysql::prepared_statement(query: string, params...)`: Executes a prepared statement, params can be a list of arguments or an array: + + ```gsc + init() + { + // call mysql::set_config + + // use variadic args for the parameters + { + query = mysql::prepared_statement("insert into `players` (`guid`, `name`) values (?, ?)", 123, "foo"); + query waittill("done", result, affected_rows, error); + } + + // use an array for the parameters + { + params = array(123, "foo"); + query = mysql::prepared_statement("insert into `players` (`guid`, `name`) values (?, ?)", params); + query waittill("done", result, affected_rows, error); + } + } + ``` +## Int64 +These int64 functions allow you to use int64 values within GSC, which normally only supports 32 bit integers. +This is useful if you store int64 values in a file or a database and need to access them from GSC. +The values are stored as strings but all the functions below can take either a string or int for an int64 argument. +`int64` represents a value that is either a `string` or an `int`. +* `int64::op(a: int64, op: string, b: int64): int64|bool`: Performs an operation between 2 int64 values (stored as strings) + ```gsc + { + result = int64::op("2583528945238953952", "+", "12039152933205235"); + assert(result == "2595568098172159187"); + + result = int64::op("5834258295282538925", ">", 1); + assert(result == true); + } + ``` + List of supported operators: + * `+` + * `-` + * `*` + * `/` + * `&` + * `^` + * `|` + * `~` + * `%` + * `>>` + * `<<` + * `++` + * `--` +* `int64::is_int(value: int64): bool`: Returns true if an int64 value fits within an int32 value. +* `int64::to_int(value: int64): int`: Converts an int64 value to an int32 value. +* `int64::min(a: int64, b: int64): int64`: Returns the minimum between 2 int64 values. +* `int64::max(a: int64, b: int64): int64`: Returns the maximum between 2 int64 values. + diff --git a/t5/plugins/tws-t5-gsc-utils/deps/premake/asmjit.lua b/t5/plugins/tws-t5-gsc-utils/deps/premake/asmjit.lua new file mode 100644 index 0000000..ee93259 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/deps/premake/asmjit.lua @@ -0,0 +1,34 @@ +asmjit = { + source = path.join(dependencies.basePath, "asmjit"), +} + +function asmjit.import() + links { "asmjit" } + asmjit.includes() +end + +function asmjit.includes() + includedirs { + path.join(asmjit.source, "src") + } + + defines { + "ASMJIT_STATIC" + } +end + +function asmjit.project() + project "asmjit" + language "C++" + + asmjit.includes() + + files { + path.join(asmjit.source, "src/**.cpp"), + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, asmjit) diff --git a/t5/plugins/tws-t5-gsc-utils/deps/premake/curl.lua b/t5/plugins/tws-t5-gsc-utils/deps/premake/curl.lua new file mode 100644 index 0000000..3d36749 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/deps/premake/curl.lua @@ -0,0 +1,73 @@ +curl = { + source = path.join(dependencies.basePath, "curl"), +} + +function curl.import() + links { "curl" } + + filter "toolset:msc*" + links { "Crypt32.lib" } + filter {} + + curl.includes() +end + +function curl.includes() +filter "toolset:msc*" + includedirs { + path.join(curl.source, "include"), + } + + defines { + "CURL_STRICTER", + "CURL_STATICLIB", + "CURL_DISABLE_LDAP", + } +filter {} +end + +function curl.project() + if not os.istarget("windows") then + return + end + + project "curl" + language "C" + + curl.includes() + + includedirs { + path.join(curl.source, "lib"), + } + + files { + path.join(curl.source, "lib/**.c"), + path.join(curl.source, "lib/**.h"), + } + + defines { + "BUILDING_LIBCURL", + } + + filter "toolset:msc*" + + defines { + "USE_SCHANNEL", + "USE_WINDOWS_SSPI", + "USE_THREADS_WIN32", + } + + filter "toolset:not msc*" + + defines { + "USE_GNUTLS", + "USE_THREADS_POSIX", + } + + filter {} + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, curl) \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/deps/premake/gsl.lua b/t5/plugins/tws-t5-gsc-utils/deps/premake/gsl.lua new file mode 100644 index 0000000..7a2daf6 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/deps/premake/gsl.lua @@ -0,0 +1,19 @@ +gsl = { + source = path.join(dependencies.basePath, "GSL"), +} + +function gsl.import() + gsl.includes() +end + +function gsl.includes() + includedirs { + path.join(gsl.source, "include") + } +end + +function gsl.project() + +end + +table.insert(dependencies, gsl) diff --git a/t5/plugins/tws-t5-gsc-utils/deps/premake/json.lua b/t5/plugins/tws-t5-gsc-utils/deps/premake/json.lua new file mode 100644 index 0000000..e5eb493 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/deps/premake/json.lua @@ -0,0 +1,19 @@ +json = { + source = path.join(dependencies.basePath, "json"), +} + +function json.import() + json.includes() +end + +function json.includes() + includedirs { + path.join(json.source, "single_include/*") + } +end + +function json.project() + +end + +table.insert(dependencies, json) diff --git a/t5/plugins/tws-t5-gsc-utils/deps/premake/libtomcrypt.lua b/t5/plugins/tws-t5-gsc-utils/deps/premake/libtomcrypt.lua new file mode 100644 index 0000000..6c6f28d --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/deps/premake/libtomcrypt.lua @@ -0,0 +1,61 @@ +libtomcrypt = { + source = path.join(dependencies.basePath, "libtomcrypt"), +} + +function libtomcrypt.import() + links { + "libtomcrypt" + } + + libtomcrypt.includes() +end + +function libtomcrypt.includes() + includedirs { + path.join(libtomcrypt.source, "src/headers") + } + + defines { + "LTC_NO_FAST", + "LTC_NO_PROTOTYPES", + "LTC_NO_RSA_BLINDING", + } +end + +function libtomcrypt.project() + project "libtomcrypt" + language "C" + + libtomcrypt.includes() + libtommath.import() + + files { + path.join(libtomcrypt.source, "src/**.c"), + } + + removefiles { + path.join(libtomcrypt.source, "src/**/*tab.c"), + path.join(libtomcrypt.source, "src/encauth/ocb3/**.c"), + } + + defines { + "_CRT_SECURE_NO_WARNINGS", + "LTC_SOURCE", + "_LIB", + "USE_LTM" + } + + removedefines { + "_DLL", + "_USRDLL" + } + + linkoptions { + "-IGNORE:4221" + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, libtomcrypt) diff --git a/t5/plugins/tws-t5-gsc-utils/deps/premake/libtommath.lua b/t5/plugins/tws-t5-gsc-utils/deps/premake/libtommath.lua new file mode 100644 index 0000000..ab4cdde --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/deps/premake/libtommath.lua @@ -0,0 +1,52 @@ +libtommath = { + source = path.join(dependencies.basePath, "libtommath"), +} + +function libtommath.import() + links { + "libtommath" + } + + libtommath.includes() +end + +function libtommath.includes() + includedirs { + libtommath.source + } + + defines { + "LTM_DESC", + "__STDC_IEC_559__", + "MP_NO_DEV_URANDOM", + } +end + +function libtommath.project() + project "libtommath" + language "C" + + libtommath.includes() + + files { + path.join(libtommath.source, "*.c"), + } + + defines { + "_LIB" + } + + removedefines { + "_DLL", + "_USRDLL" + } + + linkoptions { + "-IGNORE:4221" + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, libtommath) diff --git a/t5/plugins/tws-t5-gsc-utils/deps/premake/minhook.lua b/t5/plugins/tws-t5-gsc-utils/deps/premake/minhook.lua new file mode 100644 index 0000000..396d4d3 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/deps/premake/minhook.lua @@ -0,0 +1,31 @@ +minhook = { + source = path.join(dependencies.basePath, "minhook"), +} + +function minhook.import() + links { "minhook" } + minhook.includes() +end + +function minhook.includes() + includedirs { + path.join(minhook.source, "include") + } +end + +function minhook.project() + project "minhook" + language "C" + + minhook.includes() + + files { + path.join(minhook.source, "src/**.h"), + path.join(minhook.source, "src/**.c"), + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, minhook) diff --git a/t5/plugins/tws-t5-gsc-utils/deps/premake/minizip.lua b/t5/plugins/tws-t5-gsc-utils/deps/premake/minizip.lua new file mode 100644 index 0000000..4a5754b --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/deps/premake/minizip.lua @@ -0,0 +1,43 @@ +minizip = { + source = path.join(dependencies.basePath, "zlib/contrib/minizip"), +} + +function minizip.import() + links { "minizip" } + zlib.import() + minizip.includes() +end + +function minizip.includes() + includedirs { + minizip.source + } + + zlib.includes() +end + +function minizip.project() + project "minizip" + language "C" + + minizip.includes() + + files { + path.join(minizip.source, "*.h"), + path.join(minizip.source, "*.c"), + } + + removefiles { + path.join(minizip.source, "miniunz.c"), + path.join(minizip.source, "minizip.c"), + } + + defines { + "_CRT_SECURE_NO_DEPRECATE", + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, minizip) diff --git a/t5/plugins/tws-t5-gsc-utils/deps/premake/mysql.lua b/t5/plugins/tws-t5-gsc-utils/deps/premake/mysql.lua new file mode 100644 index 0000000..a35778f --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/deps/premake/mysql.lua @@ -0,0 +1,52 @@ +if (os.host() ~= "windows") then + error("automatic mysql installation is not supported on your os") +end + +mysql = { + source = path.join(dependencies.basePath, "mysql"), + version = "5.7.43", + download = "https://cdn.alicent.cat/mysql-5.7.43-win32.zip", +} + +function mysql.install() + local hfile = io.open(string.format("%s/include/mysql.h", mysql.source), "r") + if (hfile) then + return + end + + os.execute(string.format("mkdir \"%s\" 2> nul", mysql.source)) + + local folder = path.join(mysql.source, "mysql-5.7.43-win32") + local archive = path.join(mysql.source, "mysql-5.7.43-win32.zip") + + print("Downloading MYSQL") + os.execute(string.format("curl \"%s\" -L -o \"%s\"", mysql.download, archive)) + + os.execute(string.format("powershell -command \"Expand-Archive -Force \\\"%s\\\" \\\"%s\\\"\"", archive, mysql.source)) + os.execute(string.format("powershell -command \"mv \\\"%s/*\\\" \\\"%s\\\"\"", folder, mysql.source)) + os.execute(string.format("powershell -command \"rm \\\"%s\\\"\"", archive)) + os.execute(string.format("rmdir \"%s\"", folder)) +end + +function mysql.import() + mysql.install() + mysql.includes() +end + +function mysql.includes() + includedirs { + path.join(mysql.source, "include"), + } +end + +function mysql.project() + project "mysql" + language "C" + + mysql.includes() + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, mysql) diff --git a/t5/plugins/tws-t5-gsc-utils/deps/premake/plutonium-sdk.lua b/t5/plugins/tws-t5-gsc-utils/deps/premake/plutonium-sdk.lua new file mode 100644 index 0000000..22d8ab7 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/deps/premake/plutonium-sdk.lua @@ -0,0 +1,18 @@ +plutonium_sdk = { + source = path.join(dependencies.basePath, "plutonium-sdk"), +} + +function plutonium_sdk.import() + plutonium_sdk.includes() +end + +function plutonium_sdk.includes() + includedirs { + plutonium_sdk.source, + } +end + +function plutonium_sdk.project() +end + +table.insert(dependencies, plutonium_sdk) diff --git a/t5/plugins/tws-t5-gsc-utils/deps/premake/sqlpp11.lua b/t5/plugins/tws-t5-gsc-utils/deps/premake/sqlpp11.lua new file mode 100644 index 0000000..1f96b16 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/deps/premake/sqlpp11.lua @@ -0,0 +1,26 @@ +sqlpp11 = { + source = path.join(dependencies.basePath, "sqlpp11"), +} + +function sqlpp11.import() + sqlpp11.includes() +end + +function sqlpp11.includes() + includedirs { + path.join(sqlpp11.source, "include"), + path.join(dependencies.basePath, "date/include"), + } +end + +function sqlpp11.project() + project "sqlpp11" + language "C++" + + sqlpp11.includes() + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, sqlpp11) diff --git a/t5/plugins/tws-t5-gsc-utils/deps/premake/zlib.lua b/t5/plugins/tws-t5-gsc-utils/deps/premake/zlib.lua new file mode 100644 index 0000000..566a707 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/deps/premake/zlib.lua @@ -0,0 +1,39 @@ +zlib = { + source = path.join(dependencies.basePath, "zlib"), +} + +function zlib.import() + links { "zlib" } + zlib.includes() +end + +function zlib.includes() + includedirs { + zlib.source + } + + defines { + "ZLIB_CONST", + } +end + +function zlib.project() + project "zlib" + language "C" + + zlib.includes() + + files { + path.join(zlib.source, "*.h"), + path.join(zlib.source, "*.c"), + } + + defines { + "_CRT_SECURE_NO_DEPRECATE", + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, zlib) diff --git a/t5/plugins/tws-t5-gsc-utils/generate.bat b/t5/plugins/tws-t5-gsc-utils/generate.bat new file mode 100644 index 0000000..951520e --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/generate.bat @@ -0,0 +1,3 @@ +@echo off +call git submodule update --init --recursive +tools\premake5.exe vs2022 \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/premake5.lua b/t5/plugins/tws-t5-gsc-utils/premake5.lua new file mode 100644 index 0000000..ee99510 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/premake5.lua @@ -0,0 +1,105 @@ +dependencies = { + basePath = "./deps" +} + +function dependencies.load() + dir = path.join(dependencies.basePath, "premake/*.lua") + deps = os.matchfiles(dir) + + for i, dep in pairs(deps) do + dep = dep:gsub(".lua", "") + require(dep) + end +end + +function dependencies.imports() + for i, proj in pairs(dependencies) do + if type(i) == 'number' then + proj.import() + end + end +end + +function dependencies.projects() + for i, proj in pairs(dependencies) do + if type(i) == 'number' then + proj.project() + end + end +end + +dependencies.load() + +workspace "tws-t5-gsc-utils" + location "./build" + objdir "%{wks.location}/obj/%{cfg.buildcfg}" + targetdir "%{wks.location}/bin/%{cfg.buildcfg}" + targetname "%{prj.name}" + + configurations { "Debug", "Release", } + + language "C++" + cppdialect "C++20" + + architecture "x86" + + systemversion "latest" + symbols "On" + staticruntime "On" + editandcontinue "Off" + warnings "Extra" + characterset "ASCII" + + flags + { + "NoIncrementalLink", + "MultiProcessorCompile", + } + + filter "configurations:Release" + optimize "Full" + defines { "NDEBUG" } + filter {} + + filter "configurations:Debug" + optimize "Debug" + defines { "DEBUG", "_DEBUG" } + filter {} + + startproject "tws-t5-gsc-utils" + + project "tws-t5-gsc-utils" + kind "SharedLib" + language "C++" + + files + { + "./src/**.h", + "./src/**.hpp", + "./src/**.cpp", + "./src/**.rc", + } + + includedirs + { + "%{prj.location}/src", + "./src", + "./deps/mysql/include" + } + + libdirs {"./deps/mysql/lib"} + + resincludedirs + { + "$(ProjectDir)src" + } + + linkoptions {"/DELAYLOAD:libmysql.dll"} + + pchheader "stdinc.hpp" + pchsource "src/stdinc.cpp" + + dependencies.imports() + + group "Dependencies" + dependencies.projects() \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/command.cpp b/t5/plugins/tws-t5-gsc-utils/src/component/command.cpp new file mode 100644 index 0000000..bec33ed --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/command.cpp @@ -0,0 +1,352 @@ +#include "stdinc.hpp" +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "command.hpp" +#include "gsc.hpp" +#include "scripting.hpp" +#include "scheduler.hpp" + +#include +#include +#include + +namespace command +{ + namespace + { + std::unordered_map> handlers; + std::unordered_map> handlers_sv; + + std::vector script_commands; + std::vector script_sv_commands; + utils::memory::allocator allocator; + + utils::hook::detour client_command_hook; + + std::vector chat_callbacks; + + game::CmdArgs* get_cmd_args() + { + return static_cast(game::Sys_GetValue(4)); + } + + void main_handler() + { + params params = {}; + + const auto command = utils::string::to_lower(params[0]); + + if (handlers.find(command) != handlers.end()) + { + handlers[command](params); + } + } + + bool can_add_callback = true; + bool handle_chat_command(const int client_num, const params_sv& params) + { + const auto _0 = gsl::finally([] + { + can_add_callback = true; + }); + can_add_callback = false; + + auto hide = false; + const scripting::entity player = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, client_num, 0, 0); + + const auto command = utils::string::to_lower(params[0]); + const auto message = params.join(1).substr(1); + + for (const auto& callback : chat_callbacks) + { + const auto res = callback(player, {message, command == "say_team"}); + if (res.is() && !res.as()) + { + hide = true; + } + } + + return hide; + } + + void client_command_stub(const int client_num) + { + params_sv params = {}; + if (params.size() < 1) + { + client_command_hook.invoke(client_num); + return; + } + + const auto command = utils::string::to_lower(params[0]); + if (params.size() > 1 && (command == "say" || command == "say_team") && + handle_chat_command(client_num, params)) + { + return; + } + + if (handlers_sv.find(command) != handlers_sv.end()) + { + handlers_sv[command](client_num, params); + } + + client_command_hook.invoke(client_num); + } + + void add_script_command(const std::string& name, const std::function& callback) + { + script_commands.push_back(name); + const auto name_ = allocator.duplicate_string(name); + add(name_, callback); + } + + void add_script_sv_command(const std::string& name, const std::function& callback) + { + script_sv_commands.push_back(name); + add_sv(name, callback); + } + + void clear() + { + for (const auto& name : script_commands) + { + handlers.erase(name); + game::Cmd_RemoveCommand(name.data()); + } + + for (const auto& name : script_sv_commands) + { + handlers_sv.erase(name); + } + + allocator.clear(); + script_commands.clear(); + script_sv_commands.clear(); + chat_callbacks.clear(); + } + } + + params::params() + : nesting_(get_cmd_args()->nesting) + { + } + + int params::size() const + { + const auto cmd_args = get_cmd_args(); + return cmd_args->argc[cmd_args->nesting]; + } + + const char* params::get(int index) const + { + if (index >= this->size()) + { + return ""; + } + + const auto cmd_args = get_cmd_args(); + return cmd_args->argv[this->nesting_][index]; + } + + std::string params::join(int index) const + { + std::string result = {}; + + for (auto i = index; i < this->size(); i++) + { + if (i > index) + { + result.append(" "); + } + + result.append(this->get(i)); + } + + return result; + } + + std::vector params::get_all() const + { + std::vector params_; + for (auto i = 0; i < this->size(); i++) + { + params_.push_back(this->get(i)); + } + return params_; + } + + params_sv::params_sv() + : nesting_(game::sv_cmd_args->nesting) + { + } + + int params_sv::size() const + { + return game::sv_cmd_args->argc[this->nesting_]; + } + + const char* params_sv::get(const int index) const + { + if (index >= this->size()) + { + return ""; + } + + return game::sv_cmd_args->argv[this->nesting_][index]; + } + + std::string params_sv::join(const int index) const + { + std::string result = {}; + + for (auto i = index; i < this->size(); i++) + { + if (i > index) + { + result.append(" "); + } + + result.append(this->get(i)); + } + + return result; + } + + std::vector params_sv::get_all() const + { + std::vector params_; + for (auto i = 0; i < this->size(); i++) + { + params_.push_back(this->get(i)); + } + return params_; + } + + void add_raw(const char* name, void (*callback)()) + { + game::Cmd_AddCommandInternal(name, callback, utils::memory::get_allocator()->allocate()); + } + + void add(const char* name, std::function callback) + { + const auto command = utils::string::to_lower(name); + + if (handlers.find(command) == handlers.end()) + { + add_raw(name, main_handler); + } + + handlers[command] = callback; + } + + void add_sv(const std::string& name, std::function callback) + { + const auto command = utils::string::to_lower(name); + if (handlers_sv.find(command) == handlers_sv.end()) + { + handlers_sv[command] = std::move(callback); + } + } + + void execute(std::string command, const bool sync) + { + command += "\n"; + + if (sync) + { + game::Cmd_ExecuteSingleCommand(0, 0, command.data()); + } + else + { + game::Cbuf_AddText(0, command.data()); + } + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + scripting::on_shutdown(clear); + + client_command_hook.create(SELECT_VALUE(0x4AF770, 0x63DB70), client_command_stub); + + gsc::function::add_multiple([](const std::string& command) + { + execute(command, false); + }, "executecommand", "command::execute"); + + gsc::function::add_multiple([](const std::string& name, const scripting::function& function) + { + command::add_script_command(name, [function](const command::params& params) + { + const auto params_ = params.get_all(); + scheduler::once([=]() + { + scripting::array array; + + for (auto i = 0; i < params.size(); i++) + { + array.emplace_back(params[i]); + } + + function({array}); + }); + }); + }, "addcommand", "command::add"); + + gsc::function::add_multiple([](const std::string& name, const scripting::function& function) + { + command::add_script_sv_command(name, [function](const int client_num, const command::params_sv& params) + { + const auto params_ = params.get_all(); + scheduler::once([=]() + { + const scripting::entity player = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, client_num, 0, 0); + + scripting::array array; + + for (auto i = 0; i < params.size(); i++) + { + array.emplace_back(params[i]); + } + + function(player, {array}); + }, scheduler::pipeline::server); + }); + }, "addclientcommand", "command::add_sv"); + + gsc::method::add("tell", [](const scripting::entity& player, const std::string& msg) + { + const auto entref = player.get_entity_reference(); + if (entref.classnum) + { + throw std::runtime_error("Not a player entity"); + } + + game::SV_GameSendServerCommand(entref.entnum, 0, utils::string::va("h \"%s\"", msg.data())); + }); + + gsc::function::add("say", [](const std::string& msg) + { + game::SV_GameSendServerCommand(-1, 0, utils::string::va("h \"%s\"", msg.data())); + }); + + gsc::function::add("sendservercommand", game::SV_GameSendServerCommand.get()); + + gsc::function::add_multiple([](const scripting::function& callback) + { + if (!can_add_callback) + { + throw std::runtime_error("Cannot add a callback in this context"); + } + + chat_callbacks.push_back(callback); + }, "onplayersay", "command::on_player_say"); + } + }; +} + +REGISTER_COMPONENT(command::component) diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/command.hpp b/t5/plugins/tws-t5-gsc-utils/src/component/command.hpp new file mode 100644 index 0000000..0602bb9 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/command.hpp @@ -0,0 +1,48 @@ +#pragma once + +namespace command +{ + class params + { + public: + params(); + + int size() const; + const char* get(int index) const; + std::string join(int index) const; + std::vector get_all() const; + + const char* operator[](const int index) const + { + return this->get(index); + } + + private: + int nesting_; + }; + + class params_sv + { + public: + params_sv(); + + int size() const; + const char* get(int index) const; + std::string join(int index) const; + std::vector get_all() const; + + const char* operator[](const int index) const + { + return this->get(index); + } + + private: + int nesting_; + }; + + void add_raw(const char* name, void (*callback)()); + void add(const char* name, std::function callback); + void add_sv(const std::string& name, std::function callback); + + void execute(std::string command, const bool sync = false); +} \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/exception.cpp b/t5/plugins/tws-t5-gsc-utils/src/component/exception.cpp new file mode 100644 index 0000000..3f9930e --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/exception.cpp @@ -0,0 +1,147 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include "scheduler.hpp" + +#include "gsc.hpp" +#include "json.hpp" +#include "scripting.hpp" +#include "game/scripting/execution.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace exception +{ + namespace + { + thread_local struct + { + DWORD code = 0; + PVOID address = nullptr; + } exception_data; + + void show_mouse_cursor() + { + while (ShowCursor(TRUE) < 0); + } + + void display_error_dialog() + { + std::string error_str = utils::string::va("Fatal error (0x%08X) at 0x%p.\n" + "A minidump has been written.\n\n", + exception_data.code, exception_data.address); + + error_str += "Make sure to update your graphics card drivers and install operating system updates!"; + + utils::thread::suspend_other_threads(); + show_mouse_cursor(); + MessageBoxA(nullptr, error_str.data(), "Plutonium T5 ERROR", MB_ICONERROR); + TerminateProcess(GetCurrentProcess(), exception_data.code); + } + + void reset_state() + { + display_error_dialog(); + } + + size_t get_reset_state_stub() + { + static auto* stub = utils::hook::assemble([](utils::hook::assembler& a) + { + a.sub(esp, 0x10); + a.or_(esp, 0x8); + a.jmp(reset_state); + }); + + return reinterpret_cast(stub); + } + + std::string generate_crash_info(const LPEXCEPTION_POINTERS exceptioninfo) + { + std::string info{}; + const auto line = [&info](const std::string& text) + { + info.append(text); + info.append("\r\n"); + }; + + line("Plutonium T5 Crash Dump"); + line(""); + line("Timestamp: "s + utils::string::get_timestamp()); + line(utils::string::va("Exception: 0x%08X", exceptioninfo->ExceptionRecord->ExceptionCode)); + line(utils::string::va("Address: 0x%lX", exceptioninfo->ExceptionRecord->ExceptionAddress)); + +#pragma warning(push) +#pragma warning(disable: 4996) + OSVERSIONINFOEXA version_info; + ZeroMemory(&version_info, sizeof(version_info)); + version_info.dwOSVersionInfoSize = sizeof(version_info); + GetVersionExA(reinterpret_cast(&version_info)); +#pragma warning(pop) + + line(utils::string::va("OS Version: %u.%u", version_info.dwMajorVersion, version_info.dwMinorVersion)); + + return info; + } + + void write_minidump(const LPEXCEPTION_POINTERS exceptioninfo) + { + const std::string crash_name = utils::string::va("minidumps/plutonium-t5-crash-%s.zip", + utils::string::get_timestamp().data()); + + utils::compression::zip::archive zip_file{}; + zip_file.add("crash.dmp", create_minidump(exceptioninfo)); + zip_file.add("info.txt", generate_crash_info(exceptioninfo)); + zip_file.write(crash_name, "Plutonium T5 Crash Dump"); + } + + bool is_harmless_error(const LPEXCEPTION_POINTERS exceptioninfo) + { + const auto code = exceptioninfo->ExceptionRecord->ExceptionCode; + return code == STATUS_INTEGER_OVERFLOW || code == STATUS_FLOAT_OVERFLOW || code == STATUS_SINGLE_STEP; + } + + LONG WINAPI exception_filter(const LPEXCEPTION_POINTERS exceptioninfo) + { + if (is_harmless_error(exceptioninfo)) + { + return EXCEPTION_CONTINUE_EXECUTION; + } + + write_minidump(exceptioninfo); + + exception_data.code = exceptioninfo->ExceptionRecord->ExceptionCode; + exception_data.address = exceptioninfo->ExceptionRecord->ExceptionAddress; + exceptioninfo->ContextRecord->Eip = get_reset_state_stub(); + + return EXCEPTION_CONTINUE_EXECUTION; + } + + LPTOP_LEVEL_EXCEPTION_FILTER WINAPI set_unhandled_exception_filter_stub(LPTOP_LEVEL_EXCEPTION_FILTER) + { + // Don't register anything here... + return &exception_filter; + } + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { +//#ifdef DEBUG + SetUnhandledExceptionFilter(exception_filter); + utils::hook::jump(reinterpret_cast(&SetUnhandledExceptionFilter), set_unhandled_exception_filter_stub); +//#endif + } + }; +} + +REGISTER_COMPONENT(exception::component) diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/gsc.cpp b/t5/plugins/tws-t5-gsc-utils/src/component/gsc.cpp new file mode 100644 index 0000000..7bb94d8 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/gsc.cpp @@ -0,0 +1,277 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/structs.hpp" +#include "game/game.hpp" + +#include "gsc.hpp" + +#include +#include +#include + +namespace gsc +{ + std::unordered_map functions; + std::unordered_map methods; + + std::unordered_map function_wraps; + std::unordered_map method_wraps; + + namespace + { + utils::hook::detour get_function_hook; + utils::hook::detour get_method_hook; + + std::vector get_arguments() + { + std::vector args; + + for (auto i = 0; static_cast(i) < game::scr_VmPub->outparamcount; i++) + { + const auto value = game::scr_VmPub->top[-i]; + args.push_back(value); + } + + return args; + } + + void return_value(const scripting::script_value& value) + { + if (game::scr_VmPub->outparamcount) + { + game::Scr_ClearOutParams(game::SCRIPTINSTANCE_SERVER); + } + + scripting::push_value(value); + } + + script_function get_function_stub(const char** name, int* type) + { + const auto iter = function_wraps.find(*name); + if (iter != function_wraps.end()) + { + return reinterpret_cast(iter->second); + } + + return get_function_hook.invoke(name, type); + } + + script_function get_method_stub(const char** name, int* type) + { + const auto iter = method_wraps.find(*name); + if (iter != method_wraps.end()) + { + return reinterpret_cast(iter->second); + } + + return get_method_hook.invoke(name, type); + } + + void print(int, const char* fmt, ...) + { + char buffer[2048]; + + va_list ap; + va_start(ap, fmt); + + vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, fmt, ap); + + va_end(ap); + + printf("%s", buffer); + } + + utils::hook::detour scr_settings_hook; + void scr_settings_stub(int /*developer*/, int developer_script, int /*abort_on_error*/, int inst) + { + scr_settings_hook.invoke(developer_script, developer_script, 0, inst); + } + + utils::hook::detour scr_get_builtin_hook; + unsigned int scr_get_builtin_stub(int inst, game::sval_u func_name) + { + const auto type = *reinterpret_cast(func_name.block); + if (type != 28) + { + return scr_get_builtin_hook.invoke(inst, func_name); + } + + const auto func_namea = *reinterpret_cast(reinterpret_cast(func_name.block) + 4); + const auto typea = *reinterpret_cast(func_namea); + if (typea != 20) + { + return scr_get_builtin_hook.invoke(inst, func_name); + } + + const auto func_nameb = *reinterpret_cast(reinterpret_cast(func_namea) + 4); + const auto typeb = *reinterpret_cast(func_nameb); + + if (typeb == 23) // script::function type call + { + const auto namespace_ = game::SL_ConvertToString( + *reinterpret_cast(reinterpret_cast(func_nameb) + 4), game::SCRIPTINSTANCE_SERVER); + const auto name = game::SL_ConvertToString( + *reinterpret_cast(reinterpret_cast(func_nameb) + 8), game::SCRIPTINSTANCE_SERVER); + + const auto full_name = utils::string::va("%s::%s", namespace_, name); + if (functions.find(full_name) != functions.end()) + { + return game::SL_GetString(full_name, 0, game::SCRIPTINSTANCE_SERVER); + } + } + + return scr_get_builtin_hook.invoke(inst, func_name); + } + + // Scr_NotifyId doesn't exist, Scr_NotifyNum_Internal calls FindVariableId to get the variable id from entnum, classnum & clientNum + // to not have to recreate Scr_NotifyId we simply make FindVariableId return `entnum` (which in this case will be the id) if `clientNum` == -1 + std::size_t find_variable_id_stub_plutonium = 0; + unsigned int find_variable_id_stub(int inst, int entnum, unsigned int classnum, int client_num) + { + if (client_num == -1) + { + return entnum; + } + + return utils::hook::invoke(find_variable_id_stub_plutonium, inst, entnum, classnum, client_num); + } + } + + void call_function(const function_t& function) + { + const auto args = get_arguments(); + + try + { + const auto value = function(args); + return_value(value); + } + catch (const std::exception& e) + { + game::Scr_Error(game::SCRIPTINSTANCE_SERVER, e.what(), false); + } + } + + void call_method(const function_t& method, const game::scr_entref_t entref) + { + const auto args = get_arguments(); + + try + { + const scripting::entity entity = game::Scr_GetEntityId( + game::SCRIPTINSTANCE_SERVER, entref.entnum, entref.classnum, 0); + + std::vector args_{}; + args_.push_back(entity); + for (const auto& arg : args) + { + args_.push_back(arg); + } + + const auto value = method(args_); + return_value(value); + } + catch (const std::exception& e) + { + game::Scr_Error(game::SCRIPTINSTANCE_SERVER, e.what(), false); + } + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + // Don't com_error on gsc errors + utils::hook::nop(SELECT_VALUE(0x5A17E1, 0x4D9BB1), 5); + utils::hook::jump(SELECT_VALUE(0x5DFC40, 0x568B90), print); + + scr_settings_hook.create(SELECT_VALUE(0x4CEEA0, 0x55D010), scr_settings_stub); + + get_function_hook.create(utils::hook::extract(SELECT_VALUE(0x8A02FB, 0x8DE11B) + 1), get_function_stub); + get_method_hook.create(utils::hook::extract(SELECT_VALUE(0x8A052E, 0x8DE34E) + 1), get_method_stub); + + scr_get_builtin_hook.create(SELECT_VALUE(0x4ACAC0, 0x411490), scr_get_builtin_stub); + + // \n******* script runtime error *******\n%s\n + utils::hook::set(SELECT_VALUE(0x9FC5C0 + 40, 0xAABA68 + 40), '\n'); + utils::hook::set(SELECT_VALUE(0x9FC5C0 + 41, 0xAABA68 + 41), '\0'); + + find_variable_id_stub_plutonium = utils::hook::extract(SELECT_VALUE(0x41D2B5, 0x416325) + 1); + utils::hook::call(SELECT_VALUE(0x41D2B5, 0x416325), find_variable_id_stub); + + gsc::function::add("array", [](const scripting::variadic_args& va) + { + scripting::array array{}; + + for (const auto& arg : va) + { + array.emplace_back(arg); + } + + return array; + }); + + gsc::function::add_multiple([](const scripting::script_value& value) + { + return value.type_name(); + }, "typeof", "type"); + + gsc::function::add("debug::get_var_count", []() + { + auto count = 0; + + if (game::environment::is_sp()) + { + for (auto i = 1; i < 0x5FFE; i++) + { + const auto var = game::scr_VarGlob->variableList_mp[i]; + count += var.w.status != 0; + } + } + else + { + for (auto i = 1; i < 0x7FFE; i++) + { + const auto var = game::scr_VarGlob->variableList_mp[i]; + count += var.w.status != 0; + } + } + + return count; + }); + + gsc::function::add("toint", [](const std::string& str, const scripting::variadic_args& va) + { + auto radix = 10; + if (!va.empty()) + { + radix = va[0]; + } + + return static_cast(std::strtoull(str.data(), nullptr, radix)); + }); + + gsc::function::add("os::date", [](const scripting::variadic_args& va) + { + std::string format = "%Y-%m-%dT%H:%M:%S%z"; + if (!va.empty()) + { + format = va[0].as(); + } + + tm ltime{}; + char timestamp[MAX_PATH] = {0}; + const auto time = _time64(nullptr); + + _localtime64_s(<ime, &time); + std::strftime(timestamp, sizeof(timestamp) - 1, format.data(), <ime); + + return std::string(timestamp); + }); + } + }; +} + +REGISTER_COMPONENT(gsc::component) diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/gsc.hpp b/t5/plugins/tws-t5-gsc-utils/src/component/gsc.hpp new file mode 100644 index 0000000..04f35c1 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/gsc.hpp @@ -0,0 +1,128 @@ +#pragma once +#include "game/scripting/array.hpp" +#include "game/scripting/execution.hpp" + +#if _HAS_CXX20 +namespace gsc +{ + using function_t = std::function; + using script_function = void(*)(game::scr_entref_t); + + extern std::unordered_map functions; + extern std::unordered_map methods; + + extern std::unordered_map function_wraps; + extern std::unordered_map method_wraps; + + // auto = []{} forces template reevaluation + + template + auto wrap_function(const std::function& f, std::index_sequence) + { + return [f]([[maybe_unused]] const scripting::function_arguments& args) + { + f(args[I]...); + return scripting::script_value{}; + }; + } + + template + auto wrap_function(const std::function& f, std::index_sequence) + { + return [f]([[maybe_unused]] const scripting::function_arguments& args) + { + return f(args[I]...); + }; + } + + template + auto wrap_function(const std::function& f, std::index_sequence) + { + return [f]([[maybe_unused]] const scripting::function_arguments& args) + { + return scripting::script_value{f(args[I]...)}; + }; + } + + template + auto wrap_function(const std::function& f) + { + return wrap_function(f, std::index_sequence_for{}); + } + + template + auto wrap_function(F f) + { + return wrap_function(std::function(f)); + } + + void call_function(const function_t& function); + void call_method(const function_t& method, const game::scr_entref_t entref); + + namespace function + { + template + void add_internal(const std::string& name, F function) + { + static auto called = false; + assert(!called); + called = true; + + const auto lower = utils::string::to_lower(name); + static const auto [iterator, was_inserted] = functions.insert(std::make_pair(lower, function)); + static const auto& function_ptr = iterator->second; + + function_wraps[lower] = []() + { + call_function(function_ptr); + }; + } + + template + void add(const std::string& name, F f) + { + add_internal(name, wrap_function(f)); + } + + template + void add_multiple(F f, Args&& ...names) + { + (add(names, f), ...); + } + } + + namespace method + { + template + void add_internal(const std::string& name, F function) + { + static auto called = false; + assert(!called); + called = true; + + const auto lower = utils::string::to_lower(name); + static const auto [iterator, was_inserted] = functions.insert(std::make_pair(lower, function)); + static const auto& function_ptr = iterator->second; + + method_wraps[lower] = [](game::scr_entref_t entref) + { + call_method(function_ptr, entref); + }; + } + + template + void add(const std::string& name, F f) + { + add_internal(name, wrap_function(f)); + } + + template + void add_multiple(F f, Args&& ...names) + { + (add(names, f), ...); + } + } +} +#else +#error pre c++20 is not supported +#endif diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/http.cpp b/t5/plugins/tws-t5-gsc-utils/src/component/http.cpp new file mode 100644 index 0000000..cf91384 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/http.cpp @@ -0,0 +1,287 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/structs.hpp" +#include "game/game.hpp" + +#include "gsc.hpp" +#include "scheduler.hpp" +#include "scripting.hpp" + +#include +#include +#include + +namespace http +{ + namespace + { + constexpr const auto max_result_size = 0x5000u; + + struct http_request_params_t + { + std::string method; + std::string url; + std::string fields; + utils::http::headers headers; + utils::http::timeouts limits; + }; + + struct http_request_t + { + http_request_params_t params; + scripting::object handle; + std::optional result; + std::chrono::steady_clock::time_point queued_at = std::chrono::steady_clock::now(); + std::chrono::steady_clock::time_point started_at; + std::chrono::steady_clock::time_point finished_at; + std::atomic_bool completed{false}; + }; + + std::vector> requests; + + double elapsed_ms(const std::chrono::steady_clock::time_point start, + const std::chrono::steady_clock::time_point end) + { + return std::chrono::duration(end - start).count(); + } + + void notify_request_result(std::shared_ptr& request) + { + const auto handle_id = request->handle.get_entity_id(); + const utils::http::result missing; + const auto& result = request->result ? *request->result : missing; + scripting::array diagnostics; + diagnostics["http_status"] = static_cast(result.status); + diagnostics["curl_code"] = static_cast(result.code); + diagnostics["dns_ms"] = result.dns_ms; + diagnostics["connect_ms"] = result.connect_ms; + diagnostics["tls_ms"] = result.tls_ms; + diagnostics["first_byte_ms"] = result.first_byte_ms; + diagnostics["total_ms"] = result.total_ms; + diagnostics["worker_queue_ms"] = elapsed_ms(request->queued_at, request->started_at); + diagnostics["notify_ms"] = elapsed_ms(request->finished_at, std::chrono::steady_clock::now()); + diagnostics["connect_timeout_ms"] = static_cast(request->params.limits.connect_timeout_ms); + diagnostics["timeout_ms"] = static_cast(request->params.limits.timeout_ms); + + if (!request->result.has_value()) + { + scripting::notify(handle_id, "done", {{}, false, "unknown error", diagnostics}); + return; + } + + const auto error = curl_easy_strerror(result.code); + + if (result.code != CURLE_OK) + { + scripting::notify(handle_id, "done", {{}, false, error, diagnostics}); + return; + } + + auto& buffer = request->result->buffer; + if (buffer.size() >= max_result_size) + { + printf("^3WARNING: http result size bigger than %i bytes (%i), truncating!", max_result_size, + static_cast(buffer.size())); + buffer.resize(max_result_size); + } + + // Keep the original three values; older waiters ignore the fourth. + // HTTP 4xx/5xx are completed transfers, not transport failures. + scripting::notify(handle_id, "done", {buffer, true, {}, diagnostics}); + } + + void check_requests() + { + std::vector> ready; + for (auto i = requests.begin(); i != requests.end(); ) + { + auto& request = *i; + if (!request->completed) + { + ++i; + continue; + } + else + { + ready.emplace_back(request); + i = requests.erase(i); + } + } + // A resumed script may enqueue another request. Do not hold an iterator + // into requests while notifying the VM. + for (auto& request : ready) + notify_request_result(request); + } + + scripting::object create_request(const http_request_params_t& params) + { + const auto request = std::make_shared(); + requests.emplace_back(request); + + request->params = params; + scheduler::thread_pool.push([request] + { + request->started_at = std::chrono::steady_clock::now(); + try + { + request->result = utils::http::get_data( + request->params.url, + request->params.fields, + request->params.headers, + request->params.method, + request->params.limits); + } + catch (...) + { + // Exceptions must also release GSC waiters through the done event. + request->result = std::nullopt; + } + request->finished_at = std::chrono::steady_clock::now(); + request->completed = true; + }); + + return request->handle; + } + + template + void push_request(T&& r) + { + const auto request = std::make_shared(std::forward(r)); + requests.emplace_back(request); + } + + void wait_and_clear_requests() + { + for (auto& task : requests) + { + while (!task->completed) + { + std::this_thread::sleep_for(10ms); + } + } + + requests.clear(); + } + + long timeout_option(const scripting::array& options, const char* name, const long fallback) + { + const auto value = options[name]; + if (value.get_raw().type == game::SCRIPT_NONE) + return fallback; + if (!value.is() || value.as() <= 0) + throw std::runtime_error(std::string(name) + " must be a positive integer in milliseconds"); + return value.as(); + } + + void parse_request_options(http_request_params_t& params, const scripting::array& options) + { + params.limits.connect_timeout_ms = timeout_option(options, "connect_timeout_ms", params.limits.connect_timeout_ms); + params.limits.timeout_ms = timeout_option(options, "timeout_ms", params.limits.timeout_ms); + const auto fields = options["parameters"]; + const auto body = options["body"]; + const auto headers = options["headers"]; + const auto method = options["method"]; + + if (method.is()) + { + params.method = method.as(); + } + + if (fields.is()) + { + const auto fields_ = fields.as(); + const auto keys = fields_.get_keys(); + + for (const auto& key : keys) + { + if (!key.is()) + { + continue; + } + + const auto key_ = key.as(); + const auto value = fields_[key].to_string(); + params.fields += key_ + "=" + value + "&"; + } + + } + else if (body.is()) + { + params.fields = body.as(); + } + + if (headers.is()) + { + const auto headers_arr = headers.as(); + const auto keys = headers_arr.get_keys(); + + for (const auto& key : keys) + { + if (!key.is()) + { + continue; + } + + const auto key_str = key.as(); + const auto value = headers_arr[key].to_string(); + + params.headers[key_str] = value; + } + } + } + } + + class component final : public component_interface + { + public: + void on_shutdown([[maybe_unused]] plugin::plugin* plugin) override + { + scripting::on_shutdown(wait_and_clear_requests); + } + + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + scheduler::loop(check_requests, scheduler::server); + scripting::on_shutdown(wait_and_clear_requests); + + gsc::function::add_multiple([](const std::string& url) + { + http_request_params_t params{}; + params.url = url; + return create_request(params); + }, "http::get", "httpget", "curl"); + + gsc::function::add("http::request", [](const std::string& url, const scripting::variadic_args& va) + { + http_request_params_t params{}; + params.url = url; + + if (va.size() > 0) + { + const auto options = va[0].as(); + parse_request_options(params, options); + } + + return create_request(params); + }); + + gsc::function::add_multiple([](const std::string& url, const scripting::variadic_args& va) + { + http_request_params_t params{}; + params.url = url; + params.method = "POST"; + + if (va.size() > 0) + { + const auto options = va[0].as(); + parse_request_options(params, options); + } + + return create_request(params); + }, "httppost", "http::post"); + } + }; +} + +REGISTER_COMPONENT(http::component) diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/int64.cpp b/t5/plugins/tws-t5-gsc-utils/src/component/int64.cpp new file mode 100644 index 0000000..dcf21dc --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/int64.cpp @@ -0,0 +1,119 @@ +#include +#include "loader/component_loader.hpp" + +#include "gsc.hpp" + +#include +#include + +#define INT64_OPERATION(expr) \ + [](const std::int64_t a, [[maybe_unused]] const std::int64_t b) \ + { \ + return expr; \ + } \ + +namespace int64 +{ + namespace + { + std::int64_t get_int64_arg(const scripting::variadic_args& args, const size_t index, bool optional) + { + if (optional && index >= static_cast(args.size())) + { + return 0; + } + + if (args[index].is()) + { + return static_cast(args[index].as()); + } + + if (args[index].is()) + { + return std::strtoll(args[index].as(), nullptr, 0); + } + + throw std::runtime_error(std::format("parameter {} does not have type 'string' or 'int'", index)); + } + + std::unordered_map> operations = + { + {"+", INT64_OPERATION(a + b)}, + {"-", INT64_OPERATION(a - b)}, + {"*", INT64_OPERATION(a * b)}, + {"/", INT64_OPERATION(a / b)}, + {"&", INT64_OPERATION(a & b)}, + {"^", INT64_OPERATION(a ^ b)}, + {"|", INT64_OPERATION(a | b)}, + {"~", INT64_OPERATION(~a)}, + {"%", INT64_OPERATION(a % b)}, + {">>", INT64_OPERATION(a >> b)}, + {"<<", INT64_OPERATION(a << b)}, + {"++", INT64_OPERATION(a + 1)}, + {"--", INT64_OPERATION(a - 1)}, + }; + + std::unordered_map> comparisons = + { + {">", INT64_OPERATION(a > b)}, + {">=", INT64_OPERATION(a >= b)}, + {"==", INT64_OPERATION(a == b)}, + {"!=", INT64_OPERATION(a != b)}, + {"<=", INT64_OPERATION(a <= b)}, + {"<", INT64_OPERATION(a < b)}, + }; + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + gsc::function::add_multiple([](const scripting::variadic_args& args) + { + auto value = get_int64_arg(args, 0, false); + return value <= std::numeric_limits::max() && value >= std::numeric_limits::min(); + }, "int64_is_int", "int64::is_int"); + + gsc::function::add_multiple([](const scripting::variadic_args& args) + { + return static_cast(get_int64_arg(args, 0, false)); + }, "int64_to_int", "int64::to_int"); + + gsc::function::add_multiple([](const scripting::variadic_args& args) -> scripting::script_value + { + auto a = get_int64_arg(args, 0, false); + const auto op = args[1].as(); + auto b = get_int64_arg(args, 2, true); + + if (const auto iter = operations.find(op); iter != operations.end()) + { + return std::to_string(iter->second(a, b)); + } + + if (const auto iter = comparisons.find(op); iter != comparisons.end()) + { + return iter->second(a, b); + } + + throw std::runtime_error("invalid int64 operation"); + }, "int64_op", "int64::op"); + + gsc::function::add_multiple([](const scripting::variadic_args& args) + { + const auto a = get_int64_arg(args, 0, false); + const auto b = get_int64_arg(args, 1, false); + return std::to_string(std::min(a, b)); + }, "int64_min", "int64::min"); + + gsc::function::add_multiple([](const scripting::variadic_args& args) + { + const auto a = get_int64_arg(args, 0, false); + const auto b = get_int64_arg(args, 1, false); + return std::to_string(std::max(a, b)); + }, "int64_max", "int64::max"); + } + }; +} + +REGISTER_COMPONENT(int64::component) diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/io.cpp b/t5/plugins/tws-t5-gsc-utils/src/component/io.cpp new file mode 100644 index 0000000..6e357a5 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/io.cpp @@ -0,0 +1,75 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/structs.hpp" +#include "game/game.hpp" + +#include "gsc.hpp" + +#include + +namespace io +{ + class component final : public component_interface + { + public: + void on_after_dvar_init([[maybe_unused]] plugin::plugin* plugin) override + { + const auto fs_homepath = game::Dvar_FindVar("fs_homepath"); + if (fs_homepath == nullptr) + { + return; + } + + std::filesystem::current_path(fs_homepath->current.string); + printf("working directory: %s", fs_homepath->current.string); + } + + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + gsc::function::add_multiple([](const std::string& file, const std::string& data, + const scripting::variadic_args& va) + { + auto append = false; + + if (va.size() > 0) + { + append = va[0]; + } + + return utils::io::write_file(file, data, append); + }, "writefile", "io::write_file"); + + gsc::function::add_multiple([](const std::string& file, const std::string& data) + { + return utils::io::write_file(file, data, true); + }, "appendfile", "io::append_file"); + + gsc::function::add_multiple(utils::io::file_exists, "fileexists", "io::file_exists"); + gsc::function::add_multiple(utils::io::move_file, "movefile", "io::move_file"); + gsc::function::add_multiple(utils::io::file_size, "filesize", "io::file_size"); + gsc::function::add_multiple(utils::io::create_directory, "createdirectory", "io::create_directory"); + gsc::function::add_multiple(utils::io::directory_exists, "directoryexists", "io::directory_exists"); + gsc::function::add_multiple(utils::io::directory_is_empty, "directoryisempty", "io::directory_is_empty"); + gsc::function::add_multiple(utils::io::list_files, "listfiles", "io::list_files"); + gsc::function::add_multiple(utils::io::remove_file, "removefile", "io::remove_file"); + + gsc::function::add_multiple([](const std::filesystem::path& src, const scripting::variadic_args& va) + { + bool recursive = false; + if (va.size() > 0) + { + recursive = va[0]; + } + + utils::io::remove_directory(src, recursive); + }, "removedirectory", "io::remove_directory"); + + gsc::function::add_multiple(utils::io::copy_folder, "copyfolder", "io::copy_folder"); + gsc::function::add_multiple(utils::io::copy_folder, "copydirectory", "io::copy_directory"); + gsc::function::add_multiple(static_cast(utils::io::read_file), "readfile", "io::read_file"); + } + }; +} + +REGISTER_COMPONENT(io::component) diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/json.cpp b/t5/plugins/tws-t5-gsc-utils/src/component/json.cpp new file mode 100644 index 0000000..4aceafc --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/json.cpp @@ -0,0 +1,281 @@ +#include +#include "loader/component_loader.hpp" + +#include "scheduler.hpp" + +#include "game/scripting/event.hpp" +#include "game/scripting/execution.hpp" +#include "game/scripting/array.hpp" + +#include "gsc.hpp" +#include "json.hpp" +#include "scripting.hpp" + +#include + +namespace json +{ + namespace + { + std::unordered_set dumped_objects; + + nlohmann::json gsc_to_json(scripting::script_value value, bool print_id); + + nlohmann::json array_to_json(const scripting::array& array, bool print_id) + { + nlohmann::json obj; + + auto string_indexed = -1; + const auto keys = array.get_keys(); + for (auto i = 0u; i < keys.size(); i++) + { + const auto is_int = keys[i].is(); + const auto is_string = keys[i].is(); + + if (string_indexed == -1) + { + string_indexed = is_string; + } + + if (!string_indexed && is_int) + { + const auto index = keys[i].as(); + obj[index] = gsc_to_json(array[index], print_id); + } + else if (string_indexed && is_string) + { + const auto key = keys[i].as(); + obj.emplace(key, gsc_to_json(array[key], print_id)); + } + } + + return obj; + } + + nlohmann::json object_to_json(const scripting::object& object, bool print_id) + { + const auto id = object.get_entity_id(); + if (dumped_objects.find(id) != dumped_objects.end()) + { + return utils::string::va("[struct reference %i]", id); + } + + dumped_objects.insert(id); + nlohmann::json obj; + + if (print_id) + { + obj["__id"] = id; + } + + const auto keys = object.get_keys(); + for (const auto& key : keys) + { + if (key == "__id") + { + continue; + } + + obj.emplace(key, gsc_to_json(object[key], print_id)); + } + + return obj; + } + + nlohmann::json vector_to_array(const float* value) + { + nlohmann::json obj; + obj.push_back(value[0]); + obj.push_back(value[1]); + obj.push_back(value[2]); + + return obj; + } + + nlohmann::json gsc_to_json(scripting::script_value value, bool print_id) + { + const auto& variable = value.get_raw(); + + if (value.is()) + { + return value.as(); + } + + if (value.is()) + { + return value.as(); + } + + if (value.is()) + { + return value.as(); + } + + if (value.is()) + { + return vector_to_array(variable.u.vectorValue); + } + + if (value.is()) + { + return object_to_json(variable.u.uintValue, print_id); + } + + if (value.is()) + { + return array_to_json(variable.u.uintValue, print_id); + } + + if (value.is()) + { + return object_to_json(variable.u.uintValue, print_id); + } + + if (variable.type == game::SCRIPT_NONE) + { + return {}; + } + + if (value.is()) + { + return "[function]"; + } + + if (variable.type == game::SCRIPT_CODEPOS) + { + return "[codepos]"; + } + + if (variable.type == game::SCRIPT_END) + { + return "[precodepos]"; + } + + return utils::string::va("[%s]", value.type_name().data()); + } + + scripting::script_value json_to_gsc(nlohmann::json obj) + { + const auto type = obj.type(); + + switch (type) + { + case (nlohmann::detail::value_t::number_integer): + case (nlohmann::detail::value_t::number_unsigned): + return obj.get(); + case (nlohmann::detail::value_t::number_float): + return obj.get(); + case (nlohmann::detail::value_t::string): + return obj.get(); + case (nlohmann::detail::value_t::array): + { + scripting::array array; + + for (const auto& [key, value] : obj.items()) + { + array.emplace_back(json_to_gsc(value)); + } + + return array.get_raw(); + } + case (nlohmann::detail::value_t::object): + { + scripting::array array; + + for (const auto& [key, value] : obj.items()) + { + array[key] = json_to_gsc(value); + } + + return array.get_raw(); + } + } + + return {}; + } + } + + std::string gsc_to_string(const scripting::script_value& value) + { + dumped_objects = {}; + return gsc_to_json(value, false).dump(); + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + gsc::function::add_multiple([](const scripting::variadic_args& va) + { + scripting::array array; + + for (auto i = 0u; i < va.size(); i += 2) + { + if (i >= va.size() - 1) + { + continue; + } + + const auto key = va[i].as(); + array[key] = va[i + 1]; + } + + return array; + }, "createmap", "json::create_map"); + + gsc::function::add_multiple([](const std::string json) + { + const auto obj = nlohmann::json::parse(json); + return json_to_gsc(obj); + }, "jsonparse", "json::parse"); + + gsc::function::add_multiple([](const scripting::script_value& value, + const scripting::variadic_args& va) + { + auto indent = -1; + auto print_id = false; + + if (va.size() > 0) + { + indent = va[0].as(); + } + + if (va.size() > 1) + { + print_id = va[1].as(); + } + + dumped_objects = {}; + return gsc_to_json(value, print_id).dump(indent).substr(0, 0x5000); + }, "jsonserialize", "json::serialize"); + + gsc::function::add_multiple([](std::string file, const scripting::script_value value, + const scripting::variadic_args& va) + { + auto indent = -1; + auto print_id = false; + + if (!file.ends_with(".json")) + { + file.append(".json"); + } + + if (va.size() > 0) + { + indent = va[0].as(); + } + + if (va.size() > 1) + { + print_id = va[1].as(); + } + + dumped_objects = {}; + return utils::io::write_file(file, gsc_to_json(value, print_id).dump(indent)); + }, "jsondump", "json::dump"); + } + }; +} + +REGISTER_COMPONENT(json::component) diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/json.hpp b/t5/plugins/tws-t5-gsc-utils/src/component/json.hpp new file mode 100644 index 0000000..2c07f88 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/json.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace json +{ + std::string gsc_to_string(const scripting::script_value& _value); +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/mysql.cpp b/t5/plugins/tws-t5-gsc-utils/src/component/mysql.cpp new file mode 100644 index 0000000..1648669 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/mysql.cpp @@ -0,0 +1,545 @@ +#include +#include "loader/component_loader.hpp" + +#include "mysql.hpp" +#include "component/gsc.hpp" +#include "component/scheduler.hpp" +#include "component/scripting.hpp" + +#include + +namespace mysql +{ + namespace + { + struct mysql_result_t + { + MYSQL_RES* result; + MYSQL_STMT* stmt; + uint64_t affected_rows; + std::string error; + }; + + struct task_t + { + scripting::object handle; + mysql_result_t result{}; + std::atomic_bool completed; + }; + + std::vector> tasks; + std::array connection_pool; + + scripting::script_value field_to_value(const MYSQL_FIELD* field, const std::string& row) + { + switch (field->type) + { + case enum_field_types::MYSQL_TYPE_INT24: + case enum_field_types::MYSQL_TYPE_LONG: + case enum_field_types::MYSQL_TYPE_SHORT: + return std::atoi(row.data()); + case enum_field_types::MYSQL_TYPE_LONGLONG: + return row; + case enum_field_types::MYSQL_TYPE_FLOAT: + case enum_field_types::MYSQL_TYPE_DOUBLE: + return static_cast(std::atof(row.data())); + } + + return row; + } + + scripting::script_value bind_to_value(const MYSQL_BIND* bind) + { + switch (bind->buffer_type) + { + case enum_field_types::MYSQL_TYPE_INT24: + case enum_field_types::MYSQL_TYPE_LONG: + case enum_field_types::MYSQL_TYPE_SHORT: + return *reinterpret_cast(bind->buffer); + case enum_field_types::MYSQL_TYPE_LONGLONG: + return std::to_string(*reinterpret_cast(bind->buffer)); + case enum_field_types::MYSQL_TYPE_FLOAT: + return *reinterpret_cast(bind->buffer); + case enum_field_types::MYSQL_TYPE_DOUBLE: + return static_cast(*reinterpret_cast(bind->buffer)); + case enum_field_types::MYSQL_TYPE_STRING: + return std::string{reinterpret_cast(bind->buffer), bind->buffer_length}; + } + + return {}; + } + + scripting::array generate_result(MYSQL_STMT* stmt) + { + if (stmt == nullptr) + { + return {}; + } + + utils::memory::allocator allocator; + + scripting::array result_arr; + + const auto meta = mysql_stmt_result_metadata(stmt); + if (meta == nullptr) + { + return {}; + } + + const auto column_count = mysql_num_fields(meta); + const auto fields = mysql_fetch_fields(meta); + + const auto is_null = allocator.allocate_array(column_count); + const auto errors = allocator.allocate_array(column_count); + const auto real_lengths = allocator.allocate_array(column_count); + const auto binds = allocator.allocate_array(column_count); + + for (auto i = 0u; i < column_count; i++) + { + binds[i].length = &real_lengths[i]; + binds[i].is_null = &is_null[i]; + binds[i].error = &errors[i]; + } + + if (mysql_stmt_bind_result(stmt, binds) != 0 || + mysql_stmt_store_result(stmt) != 0) + { + return {}; + } + + while (mysql_stmt_fetch(stmt) != MYSQL_NO_DATA) + { + for (auto i = 0u; i < column_count; i++) + { + switch (fields[i].type) + { + case enum_field_types::MYSQL_TYPE_INT24: + case enum_field_types::MYSQL_TYPE_LONG: + case enum_field_types::MYSQL_TYPE_SHORT: + { + binds[i].buffer_type = MYSQL_TYPE_LONG; + binds[i].buffer = allocator.allocate(); + binds[i].buffer_length = sizeof(int); + break; + }; + case enum_field_types::MYSQL_TYPE_LONGLONG: + { + binds[i].buffer_type = MYSQL_TYPE_LONGLONG; + binds[i].buffer = allocator.allocate(); + binds[i].buffer_length = sizeof(std::int64_t); + break; + }; + case enum_field_types::MYSQL_TYPE_FLOAT: + binds[i].buffer_type = MYSQL_TYPE_FLOAT; + binds[i].buffer = allocator.allocate(); + binds[i].buffer_length = sizeof(float); + break; + case enum_field_types::MYSQL_TYPE_DOUBLE: + { + binds[i].buffer_type = MYSQL_TYPE_DOUBLE; + binds[i].buffer = allocator.allocate(); + binds[i].buffer_length = sizeof(double); + break; + } + default: + binds[i].buffer_type = MYSQL_TYPE_STRING; + binds[i].buffer = allocator.allocate_array(real_lengths[i]); + binds[i].buffer_length = real_lengths[i]; + break; + } + + mysql_stmt_fetch_column(stmt, &binds[i], i, 0); + } + + scripting::array row_arr; + + for (auto i = 0u; i < column_count; i++) + { + const auto field = &fields[i]; + const std::string field_name = {field->name, field->name_length}; + row_arr[field_name] = bind_to_value(&binds[i]); + } + + result_arr.emplace_back(row_arr); + } + + mysql_free_result(meta); + + return result_arr; + } + + scripting::array generate_result(MYSQL_RES* result) + { + if (result == nullptr) + { + return {}; + } + + scripting::array result_arr; + + const auto num_rows = mysql_num_rows(result); + const auto num_fields = mysql_num_fields(result); + const auto fields = mysql_fetch_fields(result); + + for (auto i = 0u; i < num_rows; i++) + { + scripting::array row_arr; + + const auto row = mysql_fetch_row(result); + const auto lengths = mysql_fetch_lengths(result); + + for (auto f = 0u; f < num_fields; f++) + { + const auto field = &fields[f]; + + const std::string field_str = {field->name, field->name_length}; + const std::string row_str = {row[f], lengths[f]}; + const auto value = field_to_value(field, row_str); + + row_arr[field_str] = value; + } + + result_arr.emplace_back(row_arr); + } + + return result_arr; + } + + template + scripting::object create_mysql_query(F&& cb) + { + auto task = std::make_shared(); + tasks.emplace_back(task); + + scheduler::thread_pool.push([=] + { + try + { + mysql::access([&](database_t& db) + { + task->result = cb(db); + task->completed = true; + }); + } + catch (const std::exception& e) + { + printf("^1MySQL ERROR: %s\n", e.what()); + task->completed = true; + } + }); + + return task->handle; + } + + void wait_and_clear_tasks() + { + for (auto& task : tasks) + { + while (!task->completed) + { + std::this_thread::sleep_for(10ms); + } + } + + tasks.clear(); + } + + void notify_task_result(std::shared_ptr& task) + { + const auto result = task->result.result + ? generate_result(task->result.result) + : generate_result(task->result.stmt); + + const auto rows = static_cast(task->result.affected_rows); + scripting::notify(task->handle.get_entity_id(), "done", {result, rows, task->result.error}); + + if (task->result.result != nullptr) + { + mysql_free_result(task->result.result); + task->result.result = nullptr; + } + + if (task->result.stmt != nullptr) + { + mysql_stmt_close(task->result.stmt); + task->result.stmt = nullptr; + } + } + + void check_tasks() + { + for (auto i = tasks.begin(); i != tasks.end(); ) + { + auto& task = *i; + if (!task->completed) + { + ++i; + continue; + } + else + { + notify_task_result(task); + i = tasks.erase(i); + } + } + } + + template + MYSQL_BIND* bind_statement_args(const T& args, std::size_t& bind_count) + { + bind_count = args.size(); + const auto binds = utils::memory::allocate_array(bind_count); + + for (auto i = 0u; i < args.size(); i++) + { + const auto& arg = args[i]; + const auto& raw_value = arg.get_raw(); + + switch (raw_value.type) + { + case game::SCRIPT_FLOAT: + { + binds[i].buffer = utils::memory::allocate(); + binds[i].buffer_type = MYSQL_TYPE_FLOAT; + *reinterpret_cast(binds[i].buffer) = raw_value.u.floatValue; + break; + } + case game::SCRIPT_INTEGER: + { + binds[i].buffer = utils::memory::allocate(); + binds[i].buffer_type = MYSQL_TYPE_LONG; + *reinterpret_cast(binds[i].buffer) = raw_value.u.intValue; + break; + } + case game::SCRIPT_STRING: + { + const auto str = arg.as(); + const auto str_copy = utils::memory::duplicate_string(str); + binds[i].buffer = str_copy; + binds[i].buffer_length = str.size(); + binds[i].buffer_type = MYSQL_TYPE_STRING; + break; + } + default: + { + binds[i].buffer_type = MYSQL_TYPE_NULL; + break; + } + } + } + + return binds; + } + + void cleanup_connections() + { + for (auto& connection : connection_pool) + { + connection.cleanup(); + } + } + + void free_statement_binds(MYSQL_BIND* binds, const std::size_t bind_count) + { + if (binds == nullptr) + { + return; + } + + for (auto i = 0u; i < bind_count; i++) + { + utils::memory::free(binds[i].buffer); + } + + utils::memory::free(binds); + } + } + + utils::concurrency::container& get_config() + { + static utils::concurrency::container config; + static auto initialized = false; + + if (!initialized) + { + config.access([&](sql::connection_config& cfg) + { + cfg.user = "root"; + cfg.password = "root"; + cfg.host = "localhost"; + cfg.port = 3306; + cfg.database = "default"; + }); + + initialized = true; + } + return config; + } + + void connection::check() + { + const auto now = std::chrono::high_resolution_clock::now(); + const auto diff = now - this->start_; + + if (this->db.get() == nullptr || !this->db->ping_server() || diff >= connection_timeout) + { + get_config().access([&](sql::connection_config& cfg) + { + this->db = std::make_unique(cfg); + this->start_ = now; + }); + } + + this->last_access_ = now; + } + + void connection::cleanup() + { + std::unique_lock lock(this->mutex, std::try_to_lock); + if (!lock.owns_lock()) + { + return; + } + + const auto now = std::chrono::high_resolution_clock::now(); + const auto diff = now - this->last_access_; + if (diff >= connection_timeout) + { + this->db.reset(); + } + } + + connection* get_connection(std::unique_lock& lock) + { + static thread_local connection* last_connection{}; + if (last_connection != nullptr) + { + lock = std::unique_lock(last_connection->mutex, std::try_to_lock); + if (lock.owns_lock()) + { + return last_connection; + } + } + + for (auto i = 0u; i < connection_pool.size(); i++) + { + auto connection = &connection_pool[i]; + if (connection == last_connection) + { + continue; + } + + lock = std::unique_lock(connection->mutex, std::try_to_lock); + if (!lock.owns_lock()) + { + continue; + } + + last_connection = connection; + return connection; + } + + return nullptr; + } + + class component final : public component_interface + { + public: + void on_shutdown([[maybe_unused]] plugin::plugin* plugin) override + { + wait_and_clear_tasks(); + } + + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + scripting::on_shutdown(wait_and_clear_tasks); + scheduler::loop(cleanup_connections, scheduler::async, 60s); + scheduler::loop(check_tasks, scheduler::server); + + gsc::function::add("mysql::set_config", [](const scripting::object& config) + { + get_config().access([&](sql::connection_config& cfg) + { + cfg.host = config["host"].as(); + cfg.user = config["user"].as(); + cfg.password = config["password"].as(); + cfg.port = config["port"].as(); + cfg.database = config["database"].as(); + }); + }); + + gsc::function::add_multiple([](const std::string& query) + { + return create_mysql_query([=](database_t& db) + { + const auto handle = db->get_handle(); + + mysql_result_t result{}; + if (mysql_query(handle, query.data()) != 0) + { + result.error = mysql_error(handle); + return result; + } + + result.result = mysql_store_result(handle); + result.affected_rows = mysql_affected_rows(handle); + + return result; + }); + }, "mysql::query", "mysql::execute"); + + gsc::function::add("mysql::prepared_statement", [](const std::string& query, const scripting::variadic_args& values) + { + MYSQL_BIND* binds = nullptr; + auto bind_count = 0u; + + try + { + if (values.size() > 0 && values[0].is()) + { + binds = bind_statement_args(values[0].as(), bind_count); + } + else + { + binds = bind_statement_args(values, bind_count); + } + + const auto handle = create_mysql_query([binds, bind_count, query](database_t& db) + { + const auto _0 = gsl::finally([=] + { + free_statement_binds(binds, bind_count); + }); + + mysql_result_t result{}; + + const auto handle = db->get_handle(); + const auto stmt = mysql_stmt_init(handle); + + if (mysql_stmt_prepare(stmt, query.data(), query.size()) != 0 || + (binds != nullptr && mysql_stmt_bind_param(stmt, binds) != 0) || + mysql_stmt_execute(stmt) != 0) + { + result.error = mysql_stmt_error(stmt); + return result; + } + + result.stmt = stmt; + result.affected_rows = mysql_stmt_affected_rows(stmt); + + return result; + }); + + return handle; + } + catch (const std::exception& e) + { + free_statement_binds(binds, bind_count); + throw e; + } + }); + } + }; +} + +REGISTER_COMPONENT(mysql::component) diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/mysql.hpp b/t5/plugins/tws-t5-gsc-utils/src/component/mysql.hpp new file mode 100644 index 0000000..bd88662 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/mysql.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#pragma warning(push) +#pragma warning(disable: 4127) +#pragma warning(disable: 4267) +#pragma warning(disable: 4018) +#pragma warning(disable: 4996) +#pragma warning(disable: 4244) +#include +#include +#pragma warning(pop) + +namespace sql = sqlpp::mysql; + +namespace mysql +{ + constexpr auto max_connections = 100; + constexpr auto connection_timeout = 200s; + + using database_mutex_t = std::recursive_mutex; + using database_t = std::unique_ptr; + + utils::concurrency::container& get_config(); + + class connection + { + public: + connection() = default; + void check(); + void cleanup(); + + database_t db; + database_mutex_t mutex; + + private: + std::chrono::high_resolution_clock::time_point start_; + std::chrono::high_resolution_clock::time_point last_access_; + + }; + + connection* get_connection(std::unique_lock& lock); + + template + T access(F&& accessor) + { + std::unique_lock lock; + auto conn = get_connection(lock); + if (conn == nullptr) + { + throw std::runtime_error("out of connections"); + } + + conn->check(); + return accessor(conn->db); + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/scheduler.cpp b/t5/plugins/tws-t5-gsc-utils/src/component/scheduler.cpp new file mode 100644 index 0000000..2d85400 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/scheduler.cpp @@ -0,0 +1,175 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "scheduler.hpp" + +#include +#include + +namespace scheduler +{ + utils::thread_pool thread_pool; + + namespace + { + utils::hook::detour server_frame_hook; + bool kill_thread = false; + + struct task + { + std::function handler{}; + std::chrono::milliseconds interval{}; + std::chrono::high_resolution_clock::time_point last_call{}; + }; + + using task_list = std::vector; + + class task_pipeline + { + public: + void add(task&& task) + { + new_callbacks_.access([&task](task_list& tasks) + { + tasks.emplace_back(std::move(task)); + }); + } + + void execute() + { + callbacks_.access([&](task_list& tasks) + { + this->merge_callbacks(); + + for (auto i = tasks.begin(); i != tasks.end();) + { + const auto now = std::chrono::high_resolution_clock::now(); + const auto diff = now - i->last_call; + + if (diff < i->interval) + { + ++i; + continue; + } + + i->last_call = now; + + const auto res = i->handler(); + if (res == cond_end) + { + i = tasks.erase(i); + } + else + { + ++i; + } + } + }); + } + + private: + utils::concurrency::container new_callbacks_; + utils::concurrency::container callbacks_; + + void merge_callbacks() + { + callbacks_.access([&](task_list& tasks) + { + new_callbacks_.access([&](task_list& new_tasks) + { + tasks.insert(tasks.end(), std::move_iterator(new_tasks.begin()), std::move_iterator(new_tasks.end())); + new_tasks = {}; + }); + }); + } + }; + + std::thread thread; + task_pipeline pipelines[pipeline::count]; + + void execute(const pipeline type) + { + assert(type >= 0 && type < pipeline::count); + pipelines[type].execute(); + } + + void execute_server() + { + execute(pipeline::server); + } + + void server_frame_stub() + { + execute(pipeline::server); + server_frame_hook.invoke(); + } + } + + void schedule(const std::function& callback, const pipeline type, + const std::chrono::milliseconds delay) + { + assert(type >= 0 && type < pipeline::count); + + task task; + task.handler = callback; + task.interval = delay; + task.last_call = std::chrono::high_resolution_clock::now(); + + pipelines[type].add(std::move(task)); + } + + void loop(const std::function& callback, const pipeline type, + const std::chrono::milliseconds delay) + { + schedule([callback]() + { + callback(); + return cond_continue; + }, type, delay); + } + + void once(const std::function& callback, const pipeline type, + const std::chrono::milliseconds delay) + { + schedule([callback]() + { + callback(); + return cond_end; + }, type, delay); + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + thread_pool.initialize(8); + thread_pool.start(); + + thread = std::thread([]() + { + while (!kill_thread) + { + execute(pipeline::async); + std::this_thread::sleep_for(10ms); + } + }); + + server_frame_hook.create(SELECT_VALUE(0x43E340, 0x46B680), server_frame_stub); + } + + void on_shutdown([[maybe_unused]] plugin::plugin* plugin) override + { + thread_pool.stop(); + kill_thread = true; + + if (thread.joinable()) + { + thread.join(); + } + } + }; +} + +REGISTER_COMPONENT(scheduler::component) diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/scheduler.hpp b/t5/plugins/tws-t5-gsc-utils/src/component/scheduler.hpp new file mode 100644 index 0000000..46ccef1 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/scheduler.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace scheduler +{ + enum pipeline + { + server, + async, + count, + }; + + static const bool cond_continue = false; + static const bool cond_end = true; + + void schedule(const std::function& callback, pipeline type = pipeline::server, + std::chrono::milliseconds delay = 0ms); + void loop(const std::function& callback, pipeline type = pipeline::server, + std::chrono::milliseconds delay = 0ms); + void once(const std::function& callback, pipeline type = pipeline::server, + std::chrono::milliseconds delay = 0ms); + + extern utils::thread_pool thread_pool; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/scripting.cpp b/t5/plugins/tws-t5-gsc-utils/src/component/scripting.cpp new file mode 100644 index 0000000..3c133bd --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/scripting.cpp @@ -0,0 +1,78 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "game/scripting/event.hpp" +#include "game/scripting/execution.hpp" + +#include "scripting.hpp" +#include "scheduler.hpp" + +#include +#include + +namespace scripting +{ + std::unordered_map> fields_table; + + std::unordered_map method_map; + std::unordered_map function_map; + + std::unordered_map> script_function_table; + std::unordered_map>> script_function_table_sort; + std::unordered_map> script_function_table_rev; + + std::unordered_map canonical_string_table; + + namespace + { + utils::hook::detour g_shutdown_game_hook; + utils::hook::detour sl_get_canonical_string_hook; + + std::vector> shutdown_callbacks; + void g_shutdown_game_stub(const int free_scripts) + { + if (free_scripts) + { + canonical_string_table.clear(); + } + + for (const auto& callback : shutdown_callbacks) + { + callback(); + } + + return g_shutdown_game_hook.invoke(free_scripts); + } + + unsigned int sl_transfer_canonical_string_stub(game::scriptInstance_t inst, unsigned int string_value) + { + const auto result = sl_get_canonical_string_hook.invoke(inst, string_value); + const auto str = game::SL_ConvertToString(string_value, inst); + if (str) + { + canonical_string_table[result] = game::SL_ConvertToString(string_value, inst); + } + + return result; + } + } + + void on_shutdown(const std::function& callback) + { + shutdown_callbacks.push_back(callback); + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + g_shutdown_game_hook.create(SELECT_VALUE(0x607700, 0x540950), g_shutdown_game_stub); + sl_get_canonical_string_hook.create(SELECT_VALUE(0x5F3F40, 0x622530), sl_transfer_canonical_string_stub); + } + }; +} + +REGISTER_COMPONENT(scripting::component) diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/scripting.hpp b/t5/plugins/tws-t5-gsc-utils/src/component/scripting.hpp new file mode 100644 index 0000000..57d3868 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/scripting.hpp @@ -0,0 +1,19 @@ +#pragma once + +namespace scripting +{ + extern std::unordered_map> fields_table; + + extern std::unordered_map> script_function_table; + extern std::unordered_map>> script_function_table_sort; + extern std::unordered_map> script_function_table_rev; + + extern std::unordered_map method_map; + extern std::unordered_map function_map; + + extern std::unordered_map canonical_string_table; + + using script_function = void(*)(game::scr_entref_t); + + void on_shutdown(const std::function& callback); +} \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/string.cpp b/t5/plugins/tws-t5-gsc-utils/src/component/string.cpp new file mode 100644 index 0000000..16dc912 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/string.cpp @@ -0,0 +1,218 @@ +#include +#include "loader/component_loader.hpp" + +#include "gsc.hpp" + +// lua/lstrlib.c +#define MAX_FORMAT 32 +#define L_FMTFLAGSF "-+#0 " +#define L_FMTFLAGSX "-#0" +#define L_FMTFLAGSI "-+0 " +#define L_FMTFLAGSU "-0" +#define L_FMTFLAGSC "-" + +namespace string +{ + namespace + { + // lua/lstrlib.c + const char* getformat(const char* strfrmt, char* form) + { + const auto len = std::strspn(strfrmt, L_FMTFLAGSF "123456789.") + 1; + if (len >= MAX_FORMAT - 10) + { + throw std::runtime_error("invalid format (too long)"); + } + + *(form++) = '%'; + std::memcpy(form, strfrmt, len * sizeof(char)); + *(form + len) = '\0'; + return strfrmt + len - 1; + } + + // lua/lstrlib.c + const char* get_2_digits(const char* s) + { + if (isdigit(static_cast(*s))) + { + s++; + if (isdigit(static_cast(*s))) + { + s++; + } + } + + return s; + } + + // lua/lstrlib.c + void check_format(const char* form, const char* flags, int precision) + { + const char* spec = form + 1; + spec += std::strspn(spec, flags); + if (*spec != '0') + { + spec = get_2_digits(spec); + if (*spec == '.' && precision) + { + spec++; + spec = get_2_digits(spec); + } + } + if (!std::isalpha(static_cast(*spec))) + { + throw std::runtime_error(utils::string::va("invalid conversion specification: '%s'", form)); + } + } + + // partially lua/lstrlib.c + std::string format_string(const std::string& fmt, const scripting::variadic_args& va) + { + std::string buffer{}; + size_t va_index{}; + const char* strfrmt = fmt.data(); + const char* strfrmt_end = strfrmt + fmt.size(); + + while (strfrmt < strfrmt_end) + { + if (*strfrmt != '%') + { + buffer.push_back(*strfrmt++); + } + else if (*++strfrmt == '%') + { + buffer.push_back(*strfrmt++); + } + else + { + char form[MAX_FORMAT]{}; + const char* flags = ""; + strfrmt = getformat(strfrmt, form); + + switch (*strfrmt++) + { + case 'd': + case 'i': + flags = L_FMTFLAGSI; + goto intcase; + case 'u': + case 'p': + flags = L_FMTFLAGSU; + goto intcase; + case 'o': + case 'x': + case 'X': + flags = L_FMTFLAGSX; + intcase: + { + check_format(form, flags, 1); + const auto value = va[va_index].as(); + buffer.append(utils::string::va(form, value)); + va_index++; + break; + } + case 'f': + case 'F': + case 'e': + case 'E': + case 'g': + case 'G': + { + check_format(form, L_FMTFLAGSF, 1); + const auto value = va[va_index].as(); + buffer.append(utils::string::va(form, value)); + va_index++; + break; + } + case 'c': + { + const auto value = va[va_index].as(); + check_format(form, L_FMTFLAGSC, 0); + buffer.append(utils::string::va(form, static_cast(value))); + va_index++; + break; + } + case 's': + { + const auto str = va[va_index].as(); + buffer.append(str); + va_index++; + break; + } + default: + { + throw std::runtime_error(utils::string::va("invalid conversion '%s' to 'format'", form)); + } + } + } + } + + return buffer; + } + } + + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + gsc::function::add_multiple(format_string, "va", "string::va", + "formatstring", "string::format", "sprintf"); + + gsc::function::add("printf", [](const std::string& fmt, const scripting::variadic_args& va) + { + printf("%s", format_string(fmt, va).data()); + }); + + gsc::function::add("print", [](const scripting::variadic_args& va) + { + std::string buffer{}; + + for (const auto& arg : va) + { + buffer.append(utils::string::va("%s\t", arg.to_string().data())); + } + + printf("%s\n", buffer.data()); + }); + + gsc::function::add_multiple(utils::string::to_upper, "toupper", "string::to_upper"); + gsc::function::add_multiple(utils::string::to_lower, "tolower", "string::to_lower"); + + gsc::function::add("string::is_numeric", utils::string::is_numeric); + gsc::function::add("string::starts_with", utils::string::starts_with); + gsc::function::add("string::ends_with", utils::string::ends_with); + gsc::function::add("string::replace", utils::string::replace); + + gsc::function::add("string::regex_replace", [](const std::string& str, const std::regex& expr, + const std::string& with) + { + return std::regex_replace(str, expr, with); + }); + + gsc::function::add("string::regex_match", [](const std::string& str, const std::regex& expr) + { + scripting::array array_match{}; + std::smatch match{}; + + if (std::regex_match(str, match, std::regex(expr))) + { + for (const auto& s : match) + { + array_match.emplace_back((s.str())); + } + } + + return array_match; + }); + + gsc::function::add("string::regex_search", [](const std::string& str, const std::regex& expr) + { + return std::regex_search(str, expr); + }); + } + }; +} + +REGISTER_COMPONENT(string::component) diff --git a/t5/plugins/tws-t5-gsc-utils/src/component/user_info.cpp b/t5/plugins/tws-t5-gsc-utils/src/component/user_info.cpp new file mode 100644 index 0000000..bc78896 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/component/user_info.cpp @@ -0,0 +1,136 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "gsc.hpp" + +#include +#include + +namespace user_info +{ + namespace + { + using user_info_map = std::unordered_map; + std::array user_info_overrides; + + utils::hook::detour scr_shutdown_system_hook; + + void clear_client_overrides(unsigned int client_num) + { + user_info_overrides[client_num].clear(); + } + + void clear_all_overrides() + { + for (auto& entry : user_info_overrides) + { + entry.clear(); + } + } + + void scr_shutdown_system_stub(const game::scriptInstance_t inst, const unsigned char sys, const int b_complete) + { + clear_all_overrides(); + scr_shutdown_system_hook.invoke(inst, sys, b_complete); + } + + void sv_get_user_info_stub(const int index, char* buffer, const int buffer_size) + { + game::SV_GetUserinfo(index, buffer, buffer_size); + + utils::info_string map(buffer); + + for (const auto& [key, val] : user_info_overrides[index]) + { + if (val.empty()) + { + map.remove(key); + } + else + { + map.set(key, val); + } + } + + const auto user_info = map.build(); + strncpy_s(buffer, buffer_size, user_info.data(), _TRUNCATE); + } + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + utils::hook::call(SELECT_VALUE(0x5D38EB, 0x4A75E2), sv_get_user_info_stub); + utils::hook::call(SELECT_VALUE(0x67FFE9, 0x548DB0), sv_get_user_info_stub); + + plugin->get_interface()->callbacks()->on_player_connect(clear_client_overrides); + plugin->get_interface()->callbacks()->on_player_disconnect(clear_client_overrides); + + scr_shutdown_system_hook.create(SELECT_VALUE(0x596D40, 0x540780), scr_shutdown_system_stub); + + gsc::method::add_multiple([](const scripting::entity& player, const std::string& name) -> void + { + const auto entref = player.get_entity_reference(); + if (entref.classnum) + { + throw std::runtime_error("Not a player entity"); + } + + if (name.empty()) + { + throw std::runtime_error("set_name: Illegal parameter!"); + } + + user_info_overrides[entref.entnum]["name"] = name; + game::ClientUserinfoChanged(entref.entnum); + }, "user_info::set_name", "set_name"); + + gsc::method::add_multiple([](const scripting::entity& player) -> void + { + const auto entref = player.get_entity_reference(); + if (entref.classnum) + { + throw std::runtime_error("Not a player entity"); + } + + user_info_overrides[entref.entnum].erase("name"); + game::ClientUserinfoChanged(entref.entnum); + }, "user_info::reset_name", "reset_name"); + + gsc::method::add_multiple([](const scripting::entity& player, const std::string& tag) -> void + { + const auto entref = player.get_entity_reference(); + if (entref.classnum) + { + throw std::runtime_error("Not a player entity"); + } + + if (tag.empty()) + { + throw std::runtime_error("set_clantag: Illegal parameter!"); + } + + user_info_overrides[entref.entnum]["clanAbbrev"] = tag; + game::ClientUserinfoChanged(entref.entnum); + }, "user_info::set_clantag", "set_clantag"); + + gsc::method::add_multiple([](const scripting::entity& player) -> void + { + const auto entref = player.get_entity_reference(); + if (entref.classnum) + { + throw std::runtime_error("Not a player entity"); + } + + user_info_overrides[entref.entnum].erase("clanAbbrev"); + game::ClientUserinfoChanged(entref.entnum); + }, "user_info::reset_clantag", "reset_clantag"); + } + }; +} + +REGISTER_COMPONENT(user_info::component) diff --git a/t5/plugins/tws-t5-gsc-utils/src/dllmain.cpp b/t5/plugins/tws-t5-gsc-utils/src/dllmain.cpp new file mode 100644 index 0000000..6906b91 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/dllmain.cpp @@ -0,0 +1,78 @@ +#include +#include "loader/component_loader.hpp" + +#include "plugin.hpp" + +#include "game/game.hpp" + +#include +#include +#include +#include +#include + +namespace +{ + utils::hook::detour load_library_hook; + + std::string extract_resource(const std::string& name, const int resource) + { + const auto data = utils::nt::load_resource(resource); + const auto path = std::filesystem::current_path() / "tmp" / name; + const auto path_str = path.generic_string(); + + if (!utils::io::write_file(path_str, data)) + { + const auto current = utils::io::read_file(path_str); + const auto hash_current = utils::cryptography::md5::compute(current); + const auto hash_target = utils::cryptography::md5::compute(data); + if (hash_target != hash_current) + { + throw std::runtime_error("failed to extract libmysql.dll!"); + } + } + + return path_str; + } + + HMODULE __stdcall load_library_stub(LPCSTR lib_name, HANDLE file, DWORD flags) + { + if (lib_name == "libmysql.dll"s) + { + const auto path = extract_resource(lib_name, LIBMYSQL_DLL); + const auto handle = load_library_hook.invoke_pascal(path.data(), file, flags); + + if (handle != nullptr) + { + return handle; + } + else + { + throw std::runtime_error(std::format("failed to load libmysql.dll: {}", GetLastError())); + } + } + + return load_library_hook.invoke_pascal(lib_name, file, flags); + } +} + +PLUTONIUM_API plutonium::sdk::plugin* PLUTONIUM_CALLBACK on_initialize() +{ + return plugin::get(); +} + +BOOL APIENTRY DllMain(HMODULE module, DWORD ul_reason_for_call, LPVOID /*reserved*/) +{ + if (ul_reason_for_call == DLL_PROCESS_ATTACH) + { + utils::nt::library::set_current_handle(module); + load_library_hook.create(LoadLibraryExA, load_library_stub); + } + + if (ul_reason_for_call == DLL_PROCESS_DETACH) + { + component_loader::on_shutdown(); + } + + return TRUE; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/exception/minidump.cpp b/t5/plugins/tws-t5-gsc-utils/src/exception/minidump.cpp new file mode 100644 index 0000000..9160d96 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/exception/minidump.cpp @@ -0,0 +1,94 @@ +#include +#include "minidump.hpp" + +#include +#pragma comment(lib, "dbghelp.lib") + +#include + +namespace exception +{ + namespace + { + constexpr MINIDUMP_TYPE get_minidump_type() + { + const auto type = MiniDumpIgnoreInaccessibleMemory // + | MiniDumpWithHandleData // + | MiniDumpScanMemory // + | MiniDumpWithProcessThreadData // + | MiniDumpWithFullMemoryInfo // + | MiniDumpWithThreadInfo // + | MiniDumpWithUnloadedModules; + + return static_cast(type); + } + + std::string get_temp_filename() + { + char filename[MAX_PATH] = {0}; + char pathname[MAX_PATH] = {0}; + + GetTempPathA(sizeof(pathname), pathname); + GetTempFileNameA(pathname, "Plutonium T6 -", 0, filename); + return filename; + } + + HANDLE write_dump_to_temp_file(const LPEXCEPTION_POINTERS exceptioninfo) + { + MINIDUMP_EXCEPTION_INFORMATION minidump_exception_info = {GetCurrentThreadId(), exceptioninfo, FALSE}; + + auto* const file_handle = CreateFileA(get_temp_filename().data(), GENERIC_WRITE | GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, + FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, + nullptr); + + if (!MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file_handle, get_minidump_type(), + &minidump_exception_info, + nullptr, + nullptr)) + { + MessageBoxA(nullptr, "There was an error creating the minidump! Hit OK to close the program.", + "Minidump Error", MB_OK | MB_ICONERROR); + TerminateProcess(GetCurrentProcess(), 123); + } + + return file_handle; + } + + std::string read_file(HANDLE file_handle) + { + FlushFileBuffers(file_handle); + SetFilePointer(file_handle, 0, nullptr, FILE_BEGIN); + + std::string buffer{}; + + DWORD bytes_read = 0; + char temp_bytes[0x2000]; + + do + { + if (!ReadFile(file_handle, temp_bytes, sizeof(temp_bytes), &bytes_read, nullptr)) + { + return {}; + } + + buffer.append(temp_bytes, bytes_read); + } + while (bytes_read == sizeof(temp_bytes)); + + return buffer; + } + } + + std::string create_minidump(const LPEXCEPTION_POINTERS exceptioninfo) + { + auto* const file_handle = write_dump_to_temp_file(exceptioninfo); + + const auto _ = gsl::finally([file_handle]() + { + CloseHandle(file_handle); + }); + + return read_file(file_handle); + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/exception/minidump.hpp b/t5/plugins/tws-t5-gsc-utils/src/exception/minidump.hpp new file mode 100644 index 0000000..9378639 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/exception/minidump.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace exception +{ + std::string create_minidump(LPEXCEPTION_POINTERS exceptioninfo); +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/game.cpp b/t5/plugins/tws-t5-gsc-utils/src/game/game.cpp new file mode 100644 index 0000000..aab471a --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/game.cpp @@ -0,0 +1,123 @@ +#include +#include "game.hpp" + +#include + +namespace game +{ + gamemode current = *reinterpret_cast(0x401337) == 0x281488C0 + ? gamemode::multiplayer + : gamemode::singleplayer; + + namespace environment + { + bool is_mp() + { + return current == gamemode::multiplayer; + } + + bool is_sp() + { + return current == gamemode::singleplayer; + } + } + + void SV_GetServerinfo(char* buffer, int bufferSize) + { + if (bufferSize < 1) + { + Com_Error(ERR_DROP, "\x15SV_GetServerinfo: bufferSize == %i", bufferSize); + } + + I_strncpyz(buffer, Dvar_InfoString(0, 4), bufferSize); + } + + void AddRefToValue(scriptInstance_t inst, const VariableValue* value) + { + AddRefToValue_(inst, value->type, value->u); + } + + void RemoveRefToValue(scriptInstance_t inst, const int type, VariableUnion value) + { + if (game::environment::is_mp()) + { + mp::RemoveRefToValue(inst, type, value); + } + else + { + game::VariableValue var{}; + var.type = type; + var.u = value; + + sp::RemoveRefToValue(inst, &var); + } + } + + unsigned int GetObjectType(scriptInstance_t, unsigned int id) + { + if (game::environment::is_sp()) + { + return game::scr_VarGlob->variableList_sp[id + 1].w.type & 0x1F; + } + else + { + return game::scr_VarGlob->variableList_mp[id + 1].w.type & 0x1F; + } + } + + unsigned int AllocVariable(scriptInstance_t inst) + { + static const auto func = utils::hook::assemble([](utils::hook::assembler& a) + { + a.mov(eax, 0); + a.call(SELECT_VALUE(0, 0x8E49A0)); + a.ret(); + }); + + return utils::hook::invoke(func, inst); + } + + void Scr_NotifyId(scriptInstance_t inst, int /*client_num*/, unsigned int id, + unsigned int string_value, unsigned int paramcount) + { + game::Scr_NotifyNum_Internal(inst, -1, id, 0, string_value, paramcount); + } + + VariableValue Scr_GetArrayIndexValue(scriptInstance_t, unsigned int name) + { + VariableValue value{}; + + if (name >= 0x10000) + { + if (name >= 0x17FFE) + { + value.type = 6; + value.u.intValue = name - 0x800000; + } + else + { + value.type = 1; + value.u.intValue = name - 0x10000; + } + } + else + { + value.type = 2; + value.u.intValue = name; + } + + return value; + } + + unsigned int Scr_GetSelf(scriptInstance_t, unsigned int threadId) + { + if (game::environment::is_sp()) + { + return game::scr_VarGlob->variableList_sp[threadId + 1].u.o.size; + } + else + { + return game::scr_VarGlob->variableList_mp[threadId + 1].u.o.u.self; + } + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/game.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/game.hpp new file mode 100644 index 0000000..a25a180 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/game.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include "structs.hpp" + +#define SELECT_VALUE(sp, mp) (game::environment::is_sp() ? (sp) : (mp)) + +namespace game +{ + enum gamemode + { + none, + multiplayer, + singleplayer + }; + + extern gamemode current; + + namespace environment + { + bool is_mp(); + bool is_sp(); + } + + template + class symbol + { + public: + symbol(const size_t sp_address, const size_t mp_address) + : sp_object_(reinterpret_cast(sp_address)) + , mp_object_(reinterpret_cast(mp_address)) + { + } + + T* get() const + { + if (environment::is_mp()) + { + return mp_object_; + } + + return sp_object_; + } + + operator T* () const + { + return this->get(); + } + + T* operator->() const + { + return this->get(); + } + + private: + T* sp_object_; + T* mp_object_; + }; + + void SV_GetServerinfo(char* buffer, int bufferSize); + + void AddRefToValue(scriptInstance_t inst, const VariableValue* value); + void RemoveRefToValue(scriptInstance_t inst, const int type, VariableUnion value); + + unsigned int GetObjectType(scriptInstance_t, unsigned int id); + + unsigned int AllocVariable(scriptInstance_t inst); + + VariableValue Scr_GetArrayIndexValue(scriptInstance_t inst, unsigned int name); + unsigned int Scr_GetSelf(scriptInstance_t inst, unsigned int threadId); + + void Scr_NotifyId(scriptInstance_t inst, int client_num, unsigned int id, + unsigned int string_value, unsigned int paramcount); +} + +#include "symbols.hpp" diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/array.cpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/array.cpp new file mode 100644 index 0000000..d076c6b --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/array.cpp @@ -0,0 +1,418 @@ +#include +#include "array.hpp" +#include "execution.hpp" + +namespace scripting +{ + namespace + { + std::vector get_keys_sp(unsigned int id) + { + std::vector result; + + auto current = game::scr_VarGlob->variableList_sp[id + 1].nextSibling; + + while (current) + { + const auto var = &game::scr_VarGlob->variableList_sp[current + 0x6000]; + const auto key_value = game::Scr_GetArrayIndexValue(game::SCRIPTINSTANCE_SERVER, var->w.status >> 8); + result.push_back(key_value); + + const auto next_sibling = game::scr_VarGlob->variableList_sp[current + 0x6000].nextSibling; + if (!next_sibling) + { + break; + } + + current = static_cast(game::scr_VarGlob->variableList_sp[next_sibling + 0x6000].id); + } + + return result; + } + + std::vector get_keys_mp(unsigned int id) + { + std::vector result; + + auto current = game::scr_VarGlob->variableList_mp[id + 1].nextSibling; + + while (current) + { + const auto var = &game::scr_VarGlob->variableList_mp[current + 0x8000]; + const auto key_value = game::Scr_GetArrayIndexValue(game::SCRIPTINSTANCE_SERVER, var->w.status >> 8); + result.push_back(key_value); + + const auto next_sibling = game::scr_VarGlob->variableList_mp[current + 0x8000].nextSibling; + if (!next_sibling) + { + break; + } + + current = game::scr_VarGlob->variableList_mp[next_sibling + 0x8000].hash.id; + } + + return result; + } + } + + array_value::array_value(const array* array, const script_value& key) + : array_(array) + , key_(key) + { + const auto value = this->array_->get(key); + this->script_value::operator=(value); + } + + void array_value::operator=(const script_value& value) + { + this->array_->set(this->key_, value); + this->script_value::operator=(value); + } + + array::array(const std::uint32_t id) + : id_(id) + { + this->add(); + } + + array::array(const array& other) + { + this->operator=(other); + } + + array::array(array&& other) noexcept + { + this->id_ = other.id_; + other.id_ = 0; + } + + array::array() + { + this->id_ = game::Scr_AllocArray(game::SCRIPTINSTANCE_SERVER); + } + + array::~array() + { + this->release(); + } + + array& array::operator=(const array& other) + { + if (&other != this) + { + this->release(); + this->id_ = other.id_; + this->add(); + } + + return *this; + } + + array& array::operator=(array&& other) noexcept + { + if (&other != this) + { + this->release(); + this->id_ = other.id_; + other.id_ = 0; + } + + return *this; + } + + void array::add() const + { + if (this->id_) + { + game::VariableValue value{}; + value.u.uintValue = this->id_; + value.type = game::SCRIPT_OBJECT; + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value); + } + } + + void array::release() const + { + if (this->id_) + { + game::VariableValue value{}; + value.u.uintValue = this->id_; + value.type = game::SCRIPT_OBJECT; + + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, value.type, value.u); + } + } + + std::vector array::get_keys() const + { + return SELECT_VALUE(get_keys_sp, get_keys_mp)(this->id_); + } + + std::uint32_t array::size() const + { + return static_cast(game::Scr_GetSelf(game::SCRIPTINSTANCE_SERVER, this->id_)); + } + + std::uint32_t array::push_back(const script_value& value) const + { + this->set(this->size(), value); + return this->size(); + } + + void array::erase(const std::uint32_t index) const + { + const auto variable_id = game::FindArrayVariable(game::SCRIPTINSTANCE_SERVER, this->id_, index); + if (variable_id) + { + game::RemoveVariableValue(game::SCRIPTINSTANCE_SERVER, this->id_, variable_id); + } + } + + void array::erase(const std::string& key) const + { + const auto string_value = game::SL_GetString(key.data(), 0, game::SCRIPTINSTANCE_SERVER); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + if (variable_id) + { + game::RemoveVariableValue(game::SCRIPTINSTANCE_SERVER, this->id_, variable_id); + } + } + + script_value array::get(const std::string& key) const + { + const auto string_value = game::SL_GetString(key.data(), 0, game::SCRIPTINSTANCE_SERVER); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + + if (!variable_id) + { + return {}; + } + + game::VariableValue variable_{}; + + if (game::environment::is_sp()) + { + const auto variable = &game::scr_VarGlob->variableList_sp[variable_id + 0x6000]; + variable_.type = variable->w.type & 0x1F; + variable_.u = variable->u.u; + } + else + { + const auto variable = &game::scr_VarGlob->variableList_mp[variable_id + 0x8000]; + variable_.type = variable->w.type & 0x1F; + variable_.u = variable->u.u; + } + + return variable_; + } + + void array::erase(const script_value& key) const + { + if (key.is()) + { + return this->erase(key.as()); + } + + if (key.is()) + { + return this->erase(key.as()); + } + } + + void array::erase(const array_iterator& iter) const + { + this->erase(iter->first); + } + + script_value array::get(const unsigned int index) const + { + const auto variable_id = game::FindArrayVariable(game::SCRIPTINSTANCE_SERVER, this->id_, index); + + if (!variable_id) + { + return {}; + } + + game::VariableValue variable_{}; + + if (game::environment::is_sp()) + { + const auto variable = &game::scr_VarGlob->variableList_sp[variable_id + 0x6000]; + variable_.type = variable->w.type & 0x1F; + variable_.u = variable->u.u; + } + else + { + const auto variable = &game::scr_VarGlob->variableList_mp[variable_id + 0x8000]; + variable_.type = variable->w.type & 0x1F; + variable_.u = variable->u.u; + } + + return variable_; + } + + script_value array::get(const script_value& key) const + { + if (key.is()) + { + return this->get(key.as()); + } + + if (key.is()) + { + return this->get(key.as()); + } + + throw std::runtime_error(std::format("invalid key type '{}'", key.type_name())); + } + + void array::set(const std::string& key, const script_value& value) const + { + const auto& value_ = value.get_raw(); + const auto variable_id = this->get_value_id(key); + + if (!variable_id) + { + return; + } + + game::VariableValue previous{}; + + if (game::environment::is_sp()) + { + const auto variable = &game::scr_VarGlob->variableList_sp[variable_id + 0x6000]; + previous.type = variable->w.type & 0x1F; + previous.u = variable->u.u; + + variable->w.type |= value_.type; + variable->u.u = value_.u; + } + else + { + const auto variable = &game::scr_VarGlob->variableList_mp[variable_id + 0x8000]; + previous.type = variable->w.type & 0x1F; + previous.u = variable->u.u; + + variable->w.type |= value_.type; + variable->u.u = value_.u; + } + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value_); + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, previous.type, previous.u); + } + + void array::set(const std::uint32_t index, const script_value& value) const + { + const auto& value_ = value.get_raw(); + const auto variable_id = this->get_value_id(index); + + if (!variable_id) + { + return; + } + + game::VariableValue previous{}; + + if (game::environment::is_sp()) + { + const auto variable = &game::scr_VarGlob->variableList_sp[variable_id + 0x6000]; + previous.type = variable->w.type & 0x1F; + previous.u = variable->u.u; + + variable->w.type |= value_.type; + variable->u.u = value_.u; + } + else + { + const auto variable = &game::scr_VarGlob->variableList_mp[variable_id + 0x8000]; + previous.type = variable->w.type & 0x1F; + previous.u = variable->u.u; + + variable->w.type |= value_.type; + variable->u.u = value_.u; + } + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value_); + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, previous.type, previous.u); + } + + void array::set(const script_value& key, const script_value& value) const + { + if (key.is()) + { + return this->set(key.as(), value); + } + + if (key.is()) + { + return this->set(key.as(), value); + } + + throw std::runtime_error(std::format("invalid key type '{}'", key.type_name())); + } + + std::uint32_t array::get_entity_id() const + { + return this->id_; + } + + std::uint32_t array::get_value_id(const std::string& key) const + { + const auto string_value = game::SL_GetString(key.data(), 0, game::SCRIPTINSTANCE_SERVER); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + + if (!variable_id) + { + return game::GetNewVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + } + + return variable_id; + } + + std::uint32_t array::get_value_id(const std::uint32_t index) const + { + const auto variable_id = game::FindArrayVariable(game::SCRIPTINSTANCE_SERVER, this->id_, index); + if (!variable_id) + { + return game::GetNewArrayVariable(game::SCRIPTINSTANCE_SERVER, this->id_, index); + } + + return variable_id; + } + + entity array::get_raw() const + { + return entity(this->id_); + } + + array_value array::operator[](const script_value& key) const + { + return array_value(this, key); + } + + array_iterator array::begin() const + { + const auto keys = this->get_keys(); + return array_iterator(this, keys, 0); + } + + array_iterator array::end() const + { + return array_iterator(this); + } + + array_iterator array::find(const script_value& key) const + { + const auto keys = this->get_keys(); + for (auto i = 0u; i < keys.size(); i++) + { + if (keys[i] == key) + { + return array_iterator(this, keys, i); + } + } + + return array_iterator(this); + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/array.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/array.hpp new file mode 100644 index 0000000..dab9522 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/array.hpp @@ -0,0 +1,107 @@ +#pragma once + +#include "script_value.hpp" +#include "container_iterator.hpp" + +namespace scripting +{ + class array_value : public script_value + { + public: + array_value(const array* array, const script_value& key); + void operator=(const script_value& value); + + template + T as() const + { + try + { + return script_value::as(); + } + catch (const std::exception& e) + { + throw std::runtime_error(std::format("array value '{}' {}", this->key_.to_string(), e.what())); + } + } + + private: + script_value key_; + const array* array_; + + }; + + template + class array_iterator_base : public IteratorType + { + public: + array_iterator_base(const array* container) + : IteratorType(container) + { + } + + array_iterator_base(const array* container, const std::vector& keys, const std::int64_t key_index) + : IteratorType(container, keys, key_index) + { + } + }; + + using array_iterator = array_iterator_base>; + + class array final + { + public: + array(); + array(const std::uint32_t id); + + array(const array& other); + array(array&& other) noexcept; + + ~array(); + + array& operator=(const array& other); + array& operator=(array&& other) noexcept; + + std::vector get_keys() const; + std::uint32_t size() const; + + std::uint32_t push_back(const script_value& value) const; + + template + void emplace_back(Args&&... args) + { + this->push_back(std::forward(args)...); + } + + void erase(const std::uint32_t index) const; + void erase(const std::string& key) const; + void erase(const script_value& key) const; + void erase(const array_iterator& iter) const; + + script_value get(const script_value&) const; + script_value get(const std::string&) const; + script_value get(const std::uint32_t) const; + + void set(const script_value&, const script_value&) const; + void set(const std::string&, const script_value&) const; + void set(const std::uint32_t, const script_value&) const; + + std::uint32_t get_entity_id() const; + + std::uint32_t get_value_id(const std::string&) const; + std::uint32_t get_value_id(const std::uint32_t) const; + + entity get_raw() const; + + array_value operator[](const script_value& key) const; + + array_iterator begin() const; + array_iterator end() const; + array_iterator find(const script_value& key) const; + + private: + void add() const; + void release() const; + + std::uint32_t id_{}; + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/container_iterator.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/container_iterator.hpp new file mode 100644 index 0000000..3278177 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/container_iterator.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include "script_value.hpp" + +namespace scripting +{ + template + class container_iterator + { + public: + static constexpr std::int64_t container_iterator_end_key = -1; + + container_iterator(const ContainerType* container) + : container_(container) + , key_index_(container_iterator_end_key) + { + } + + container_iterator(const ContainerType* container, const std::vector& keys, const std::int64_t key_index) + : container_(container) + , keys_(keys) + , key_index_(key_index) + { + this->update_pair(); + } + + std::pair& operator*() + { + return this->pair_.value(); + } + + std::pair* operator->() + { + return &this->pair_.value(); + } + + const std::pair& operator*() const + { + return this->pair_.value(); + } + + const std::pair* operator->() const + { + return &this->pair_.value(); + } + + container_iterator& operator++() + { + if (this->key_index_ == container_iterator_end_key) + { + return *this; + } + + const auto size = this->keys_.size(); + if (this->key_index_ + 1 >= size) + { + this->key_index_ = container_iterator_end_key; + return *this; + } + + this->key_index_++; + this->update_pair(); + + return *this; + } + + container_iterator operator++(int) + { + const auto pre = *this; + this->operator++(); + return pre; + } + + friend bool operator==(const container_iterator& a, const container_iterator& b) + { + return a.container_ == b.container_ && a.key_index_ == b.key_index_; + }; + + friend bool operator!=(const container_iterator& a, const container_iterator& b) + { + return a.container_ != b.container_ || a.key_index_ != b.key_index_; + }; + + private: + void update_pair() + { + const auto index = static_cast(this->key_index_); + const auto& key = this->keys_[index]; + const auto value = this->container_->operator[](key); + this->pair_.emplace(std::make_pair(key, value)); + } + + const ContainerType* container_; + std::int64_t key_index_; + std::vector keys_; + std::optional> pair_; + + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/entity.cpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/entity.cpp new file mode 100644 index 0000000..d7ebd6d --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/entity.cpp @@ -0,0 +1,120 @@ +#include +#include "entity.hpp" +#include "script_value.hpp" +#include "execution.hpp" + +namespace scripting +{ + entity::entity() + : entity(0) + { + } + + entity::entity(const entity& other) : entity(other.entity_id_) + { + } + + entity::entity(entity&& other) noexcept + { + this->entity_id_ = other.entity_id_; + other.entity_id_ = 0; + } + + entity::entity(const unsigned int entity_id) + : entity_id_(entity_id) + { + this->add(); + } + + entity::~entity() + { + this->release(); + } + + entity& entity::operator=(const entity& other) + { + if (&other != this) + { + this->release(); + this->entity_id_ = other.entity_id_; + this->add(); + } + + return *this; + } + + entity& entity::operator=(entity&& other) noexcept + { + if (&other != this) + { + this->release(); + this->entity_id_ = other.entity_id_; + other.entity_id_ = 0; + } + + return *this; + } + + unsigned int entity::get_entity_id() const + { + return this->entity_id_; + } + + game::scr_entref_t entity::get_entity_reference() const + { + if (!this->entity_id_) + { + const auto not_null = static_cast(~0ui16); + return game::scr_entref_t{not_null, not_null}; + } + + game::scr_entref_t entref{}; + game::Scr_GetEntityIdRef(&entref, game::SCRIPTINSTANCE_SERVER, this->get_entity_id()); + return entref; + } + + bool entity::operator==(const entity& other) const noexcept + { + return this->get_entity_id() == other.get_entity_id(); + } + + bool entity::operator!=(const entity& other) const noexcept + { + return !this->operator==(other); + } + + void entity::add() const + { + if (this->entity_id_) + { + game::VariableValue value; + value.type = game::SCRIPT_OBJECT; + value.u.uintValue = this->entity_id_; + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value); + } + } + + void entity::release() const + { + if (this->entity_id_) + { + game::VariableValue value; + value.type = game::SCRIPT_OBJECT; + value.u.uintValue = this->entity_id_; + + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, value.type, value.u); + } + } + + void entity::set(const std::string& field, const script_value& value) const + { + set_entity_field(*this, field, value); + } + + template <> + script_value entity::get(const std::string& field) const + { + return get_entity_field(*this, field); + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/entity.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/entity.hpp new file mode 100644 index 0000000..22382fa --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/entity.hpp @@ -0,0 +1,47 @@ +#pragma once +#include "game/game.hpp" +#include "script_value.hpp" + +namespace scripting +{ + class entity final + { + public: + entity(); + entity(unsigned int entity_id); + + entity(const entity& other); + entity(entity&& other) noexcept; + + ~entity(); + + entity& operator=(const entity& other); + entity& operator=(entity&& other) noexcept; + + void set(const std::string& field, const script_value& value) const; + + template + T get(const std::string& field) const; + + unsigned int get_entity_id() const; + game::scr_entref_t get_entity_reference() const; + + bool operator ==(const entity& other) const noexcept; + bool operator !=(const entity& other) const noexcept; + + private: + unsigned int entity_id_; + + void add() const; + void release() const; + }; + + template <> + script_value entity::get(const std::string& field) const; + + template + T entity::get(const std::string& field) const + { + return this->get(field).as(); + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/event.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/event.hpp new file mode 100644 index 0000000..bc1d53e --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/event.hpp @@ -0,0 +1,13 @@ +#pragma once +#include "script_value.hpp" +#include "entity.hpp" + +namespace scripting +{ + struct event + { + std::string name; + entity entity{}; + std::vector arguments; + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/execution.cpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/execution.cpp new file mode 100644 index 0000000..cd885d1 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/execution.cpp @@ -0,0 +1,154 @@ +#include +#include "execution.hpp" +#include "safe_execution.hpp" +#include "stack_isolation.hpp" + +#include "component/scripting.hpp" + +namespace scripting +{ + namespace + { + game::VariableValue* allocate_argument() + { + game::VariableValue* value_ptr = ++game::scr_VmPub->top; + ++game::scr_VmPub->inparamcount; + return value_ptr; + } + + script_value get_return_value() + { + if (game::scr_VmPub->inparamcount == 0) + { + return {}; + } + + game::Scr_ClearOutParams(game::SCRIPTINSTANCE_SERVER); + game::scr_VmPub->outparamcount = game::scr_VmPub->inparamcount; + game::scr_VmPub->inparamcount = 0; + + return script_value(game::scr_VmPub->top[1 - game::scr_VmPub->outparamcount]); + } + + int get_field_id(const int classnum, const std::string& field) + { + if (scripting::fields_table[classnum].find(field) != scripting::fields_table[classnum].end()) + { + return scripting::fields_table[classnum][field]; + } + + return -1; + } + } + + void push_value(const script_value& value) + { + auto* value_ptr = allocate_argument(); + *value_ptr = value.get_raw(); + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, value_ptr); + } + + void notify(const entity& entity, const std::string& event, const std::vector& arguments) + { + stack_isolation _; + for (auto i = arguments.rbegin(); i != arguments.rend(); ++i) + { + push_value(*i); + } + + const auto event_id = game::SL_GetString(event.data(), 0, game::SCRIPTINSTANCE_SERVER); + game::Scr_NotifyId(game::SCRIPTINSTANCE_SERVER, 0, entity.get_entity_id(), event_id, game::scr_VmPub->inparamcount); + } + + script_value exec_ent_thread(const entity& entity, const char* pos, const std::vector& arguments) + { + const auto id = entity.get_entity_id(); + + stack_isolation _; + for (auto i = arguments.rbegin(); i != arguments.rend(); ++i) + { + push_value(*i); + } + + game::VariableValue value{}; + value.type = game::SCRIPT_OBJECT; + value.u.uintValue = id; + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value); + + const auto local_id = game::AllocThread(game::SCRIPTINSTANCE_SERVER, id); + const auto result = game::VM_Execute(game::SCRIPTINSTANCE_SERVER, local_id, pos, arguments.size()); + game::RemoveRefToObject(game::SCRIPTINSTANCE_SERVER, result); + + return get_return_value(); + } + + script_value get_custom_field(const entity& entity, const std::string& field) + { + const object object = entity.get_entity_id(); + return object.get(field); + } + + void set_custom_field(const entity& entity, const std::string& field, const script_value& value) + { + const object object = entity.get_entity_id(); + object.set(field, value); + } + + void set_entity_field(const entity& entity, const std::string& field, const script_value& value) + { + const auto entref = entity.get_entity_reference(); + const int id = get_field_id(entref.classnum, field); + + if (id != -1) + { + stack_isolation _; + push_value(value); + + game::scr_VmPub->outparamcount = game::scr_VmPub->inparamcount; + game::scr_VmPub->inparamcount = 0; + + if (!safe_execution::set_entity_field(entref, id)) + { + throw std::runtime_error("Failed to set value for field '" + field + "'"); + } + } + else + { + set_custom_field(entity, field, value); + } + } + + script_value get_entity_field(const entity& entity, const std::string& field) + { + const auto entref = entity.get_entity_reference(); + const auto id = get_field_id(entref.classnum, field); + + if (id != -1) + { + stack_isolation _; + + game::VariableValue value{}; + if (!safe_execution::get_entity_field(entref, id, &value)) + { + throw std::runtime_error("Failed to get value for field '" + field + "'"); + } + + const auto __ = gsl::finally([&value]() + { + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, value.type, value.u); + }); + + return value; + } + else + { + return get_custom_field(entity, field); + } + } + + unsigned int make_object() + { + return 0; + } +} \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/execution.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/execution.hpp new file mode 100644 index 0000000..c4b2c3c --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/execution.hpp @@ -0,0 +1,22 @@ +#pragma once +#include "game/game.hpp" +#include "entity.hpp" +#include "array.hpp" +#include "object.hpp" +#include "function.hpp" +#include "thread.hpp" +#include "script_value.hpp" + +namespace scripting +{ + void push_value(const script_value& value); + + script_value exec_ent_thread(const entity& entity, const char* pos, const std::vector& arguments); + + void set_entity_field(const entity& entity, const std::string& field, const script_value& value); + script_value get_entity_field(const entity& entity, const std::string& field); + + void notify(const entity& entity, const std::string& event, const std::vector& arguments); + + unsigned int make_object(); +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/function.cpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/function.cpp new file mode 100644 index 0000000..5f511ac --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/function.cpp @@ -0,0 +1,42 @@ +#include +#include "function.hpp" +#include "execution.hpp" +#include "../../component/scripting.hpp" + +namespace scripting +{ + function::function(const char* pos) + : pos_(pos) + { + } + + script_value function::get_raw() const + { + game::VariableValue value; + value.type = game::SCRIPT_FUNCTION; + value.u.codePosValue = this->pos_; + + return value; + } + + const char* function::get_pos() const + { + return this->pos_; + } + + std::string function::get_name() const + { + if (scripting::script_function_table_rev.find(this->pos_) != scripting::script_function_table_rev.end()) + { + const auto& func = scripting::script_function_table_rev[this->pos_]; + return utils::string::va("%s::%s", func.first.data(), func.second.data()); + } + + return "unknown function"; + } + + script_value function::call(const entity& self, const arguments& arguments) const + { + return exec_ent_thread(self, this->pos_, arguments); + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/function.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/function.hpp new file mode 100644 index 0000000..b0eb4f8 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/function.hpp @@ -0,0 +1,48 @@ +#pragma once +#include "entity.hpp" +#include "script_value.hpp" + +namespace scripting +{ + class function + { + public: + function(const char*); + + script_value get_raw() const; + const char* get_pos() const; + std::string get_name() const; + + script_value call(const entity& self, const arguments& arguments) const; + + script_value operator()(const entity& self, const arguments& arguments) const + { + return this->call(self, arguments); + } + + script_value operator()(const arguments& arguments) const + { + return this->call(*game::levelEntityId, arguments); + } + + script_value operator()() const + { + return this->call(*game::levelEntityId, {}); + } + + template + arguments operator()(T... arguments) const + { + return this->call(*game::levelEntityId, {arguments...}); + } + + template + arguments operator()(const entity& self, T... arguments) const + { + return this->call(self, {arguments...}); + } + + private: + const char* pos_; + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/object.cpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/object.cpp new file mode 100644 index 0000000..df7acd5 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/object.cpp @@ -0,0 +1,326 @@ +#include +#include "object.hpp" +#include "execution.hpp" + +#include "../../component/scripting.hpp" + +namespace scripting +{ + namespace + { + std::string get_string(unsigned int canonical_string) + { + if (canonical_string_table.find(canonical_string) != canonical_string_table.end()) + { + return canonical_string_table[canonical_string]; + } + + return ""; + } + + unsigned int get_canonical_string(const std::string& str) + { + for (const auto& value : canonical_string_table) + { + if (value.second == str) + { + return value.first; + } + } + + return game::SL_GetCanonicalString(game::SCRIPTINSTANCE_SERVER, str.data()); + } + + std::vector get_keys_sp(unsigned int id) + { + std::vector result; + + auto current = game::scr_VarGlob->variableList_sp[id + 1].nextSibling; + + while (current) + { + const auto var = &game::scr_VarGlob->variableList_sp[current + 0x6000]; + const auto key_value = get_string(var->w.status >> 8); + result.push_back(key_value); + + const auto next_sibling = game::scr_VarGlob->variableList_sp[current + 0x6000].nextSibling; + if (!next_sibling) + { + break; + } + + current = static_cast(game::scr_VarGlob->variableList_sp[next_sibling + 0x6000].id); + } + + return result; + } + + std::vector get_keys_mp(unsigned int id) + { + std::vector result; + + auto current = game::scr_VarGlob->variableList_mp[id + 1].nextSibling; + + while (current) + { + const auto var = &game::scr_VarGlob->variableList_mp[current + 0x8000]; + const auto key_value = get_string(var->w.status >> 8); + result.push_back(key_value); + + const auto next_sibling = game::scr_VarGlob->variableList_mp[current + 0x8000].nextSibling; + if (!next_sibling) + { + break; + } + + current = game::scr_VarGlob->variableList_mp[next_sibling + 0x8000].hash.id; + } + + return result; + } + } + + object_value::object_value(unsigned int parent_id, unsigned int id) + : id_(id) + , parent_id_(parent_id) + { + if (!this->id_) + { + return; + } + + game::VariableValue variable_{}; + + if (game::environment::is_sp()) + { + const auto variable = &game::scr_VarGlob->variableList_sp[this->id_ + 0x6000]; + variable_.type = variable->w.type & 0x1F; + variable_.u = variable->u.u; + } + else + { + const auto variable = &game::scr_VarGlob->variableList_mp[this->id_ + 0x8000]; + variable_.type = variable->w.type & 0x1F; + variable_.u = variable->u.u; + } + + this->value_ = variable_; + } + + void object_value::operator=(const script_value& value) + { + if (!this->id_) + { + return; + } + + const auto& value_0 = value.get_raw(); + + game::VariableValue previous{}; + + if (game::environment::is_sp()) + { + const auto variable = &game::scr_VarGlob->variableList_sp[this->id_ + 0x6000]; + previous.type = variable->w.type & 0x1F; + previous.u = variable->u.u; + + variable->w.type |= value_0.type; + variable->u.u = value_0.u; + } + else + { + const auto variable = &game::scr_VarGlob->variableList_mp[this->id_ + 0x8000]; + previous.type = variable->w.type & 0x1F; + previous.u = variable->u.u; + + variable->w.type |= value_0.type; + variable->u.u = value_0.u; + } + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value_0); + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, previous.type, previous.u); + + this->value_ = value_0; + } + + object::object(const unsigned int id) + : id_(id) + { + this->add(); + } + + object::object(const object& other) + { + this->operator=(other); + } + + object::object(object&& other) noexcept + { + this->id_ = other.id_; + other.id_ = 0; + } + + object::object() + { + this->id_ = game::AllocObject(game::SCRIPTINSTANCE_SERVER); + } + + object::~object() + { + this->release(); + } + + object& object::operator=(const object& other) + { + if (&other != this) + { + this->release(); + this->id_ = other.id_; + this->add(); + } + + return *this; + } + + object& object::operator=(object&& other) noexcept + { + if (&other != this) + { + this->release(); + this->id_ = other.id_; + other.id_ = 0; + } + + return *this; + } + + void object::add() const + { + if (this->id_) + { + game::VariableValue value{}; + value.u.uintValue = this->id_; + value.type = game::SCRIPT_OBJECT; + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value); + } + } + + void object::release() const + { + if (this->id_) + { + game::VariableValue value{}; + value.u.uintValue = this->id_; + value.type = game::SCRIPT_OBJECT; + + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, value.type, value.u); + } + } + + std::vector object::get_keys() const + { + return SELECT_VALUE(get_keys_sp, get_keys_mp)(this->id_); + } + + unsigned int object::size() const + { + return game::Scr_GetSelf(game::SCRIPTINSTANCE_SERVER, this->id_); + } + + void object::erase(const std::string& /*key*/) const + { + /*const auto string_value = game::SL_GetCanonicalString(key.data(), 0); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + if (variable_id) + { + game::RemoveVariableValue(game::SCRIPTINSTANCE_SERVER, this->id_, variable_id); + }*/ + } + + script_value object::get(const std::string& key) const + { + const auto string_value = get_canonical_string(key); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + + if (!variable_id) + { + return {}; + } + + game::VariableValue variable_{}; + + if (game::environment::is_sp()) + { + const auto variable = &game::scr_VarGlob->variableList_sp[variable_id + 0x6000]; + variable_.type = variable->w.type & 0x1F; + variable_.u = variable->u.u; + } + else + { + const auto variable = &game::scr_VarGlob->variableList_mp[variable_id + 0x8000]; + variable_.type = variable->w.type & 0x1F; + variable_.u = variable->u.u; + } + + return variable_; + } + + void object::set(const std::string& key, const script_value& value) const + { + const auto& value_ = value.get_raw(); + const auto variable_id = this->get_value_id(key); + + if (!variable_id) + { + return; + } + + game::VariableValue previous{}; + + if (game::environment::is_sp()) + { + const auto variable = &game::scr_VarGlob->variableList_sp[variable_id + 0x6000]; + previous.type = variable->w.type & 0x1F; + previous.u = variable->u.u; + + variable->w.type |= value_.type; + variable->u.u = value_.u; + } + else + { + const auto variable = &game::scr_VarGlob->variableList_mp[variable_id + 0x8000]; + previous.type = variable->w.type & 0x1F; + previous.u = variable->u.u; + + variable->w.type |= value_.type; + variable->u.u = value_.u; + } + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value_); + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, previous.type, previous.u); + } + + unsigned int object::get_entity_id() const + { + return this->id_; + } + + unsigned int object::get_value_id(const std::string& key) const + { + const auto string_value = get_canonical_string(key); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + + if (!variable_id) + { + return game::GetNewVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + } + + return variable_id; + } + + entity object::get_raw() const + { + return entity(this->id_); + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/object.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/object.hpp new file mode 100644 index 0000000..974444f --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/object.hpp @@ -0,0 +1,53 @@ +#pragma once +#include "script_value.hpp" + +namespace scripting +{ + class object_value : public script_value + { + public: + object_value(unsigned int, unsigned int); + void operator=(const script_value&); + private: + unsigned int id_; + unsigned int parent_id_; + }; + + class object final + { + public: + object(); + object(const unsigned int); + + object(const object& other); + object(object&& other) noexcept; + + ~object(); + + object& operator=(const object& other); + object& operator=(object&& other) noexcept; + + std::vector get_keys() const; + unsigned int size() const; + void erase(const std::string&) const; + + script_value get(const std::string&) const; + void set(const std::string&, const script_value&) const; + + unsigned int get_entity_id() const; + unsigned int get_value_id(const std::string&) const; + + entity get_raw() const; + + object_value operator[](const std::string& key) const + { + return {this->id_, this->get_value_id(key)}; + } + + private: + void add() const; + void release() const; + + unsigned int id_{}; + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/safe_execution.cpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/safe_execution.cpp new file mode 100644 index 0000000..c7805b4 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/safe_execution.cpp @@ -0,0 +1,67 @@ +#include +#include "safe_execution.hpp" + +#pragma warning(push) +#pragma warning(disable: 4611) + +namespace scripting::safe_execution +{ + bool execute_with_seh(const script_function function, const game::scr_entref_t& entref) + { + __try + { + function(entref); + return true; + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + return false; + } + } + + bool call(const script_function function, const game::scr_entref_t& entref) + { + *game::g_script_error_level += 1; + if (game::_setjmp(&game::g_script_error[*game::g_script_error_level], 0)) + { + *game::g_script_error_level -= 1; + return false; + } + + const auto result = execute_with_seh(function, entref); + *game::g_script_error_level -= 1; + return result; + } + + bool set_entity_field(const game::scr_entref_t& entref, const int offset) + { + *game::g_script_error_level += 1; + if (game::_setjmp(&game::g_script_error[*game::g_script_error_level], 0)) + { + *game::g_script_error_level -= 1; + return false; + } + + game::Scr_SetObjectField(entref.classnum, entref.entnum, offset); + + *game::g_script_error_level -= 1; + return true; + } + + bool get_entity_field(const game::scr_entref_t& entref, const int offset, game::VariableValue* value) + { + *game::g_script_error_level += 1; + if (game::_setjmp(&game::g_script_error[*game::g_script_error_level], 0)) + { + value->type = game::SCRIPT_NONE; + value->u.intValue = 0; + *game::g_script_error_level -= 1; + return false; + } + + *value = game::GetEntityFieldValue(game::SCRIPTINSTANCE_SERVER, entref.classnum, entref.entnum, 0, offset); + + *game::g_script_error_level -= 1; + return true; + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/safe_execution.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/safe_execution.hpp new file mode 100644 index 0000000..7091973 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/safe_execution.hpp @@ -0,0 +1,14 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting::safe_execution +{ + using script_function = void(*)(game::scr_entref_t); + + bool execute_with_seh(const script_function function, const game::scr_entref_t& entref); + + bool call(script_function function, const game::scr_entref_t& entref); + + bool set_entity_field(const game::scr_entref_t& entref, int offset); + bool get_entity_field(const game::scr_entref_t& entref, int offset, game::VariableValue* value); +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/script_value.cpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/script_value.cpp new file mode 100644 index 0000000..3f6e403 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/script_value.cpp @@ -0,0 +1,491 @@ +#include +#include "script_value.hpp" +#include "entity.hpp" +#include "array.hpp" +#include "function.hpp" +#include "object.hpp" + +namespace scripting +{ + /*********************************************** + * Constructors + **********************************************/ + + script_value::script_value(const game::VariableValue& value) + : value_(value) + { + } + + script_value::script_value(void* value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_INTEGER; + variable.u.intValue = reinterpret_cast(value); + + this->value_ = variable; + } + + script_value::script_value(const int value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_INTEGER; + variable.u.intValue = value; + + this->value_ = variable; + } + + script_value::script_value(const unsigned int value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_INTEGER; + variable.u.uintValue = value; + + this->value_ = variable; + } + + script_value::script_value(const bool value) + : script_value(static_cast(value)) + { + } + + script_value::script_value(const float value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_FLOAT; + variable.u.floatValue = value; + + this->value_ = variable; + } + + script_value::script_value(const double value) + : script_value(static_cast(value)) + { + } + + script_value::script_value(const char* value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_STRING; + variable.u.stringValue = game::SL_GetString(value, 0, game::SCRIPTINSTANCE_SERVER); + + const auto _ = gsl::finally([&variable]() + { + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, variable.type, variable.u); + }); + + this->value_ = variable; + } + + script_value::script_value(const std::string& value) + : script_value(value.data()) + { + } + + script_value::script_value(const entity& value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_OBJECT; + variable.u.pointerValue = value.get_entity_id(); + + this->value_ = variable; + } + + script_value::script_value(const array& value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_OBJECT; + variable.u.pointerValue = value.get_entity_id(); + + this->value_ = variable; + } + + script_value::script_value(const object& value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_OBJECT; + variable.u.pointerValue = value.get_entity_id(); + + this->value_ = variable; + } + + script_value::script_value(const function& value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_FUNCTION; + variable.u.codePosValue = value.get_pos(); + + this->value_ = variable; + } + + script_value::script_value(const vector& value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_VECTOR; + variable.u.vectorValue = game::Scr_AllocVector(game::SCRIPTINSTANCE_SERVER, value); + + const auto _ = gsl::finally([&variable]() + { + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, variable.type, variable.u); + }); + + this->value_ = variable; + } + + /*********************************************** + * Integer + **********************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::SCRIPT_INTEGER; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + int script_value::get() const + { + return this->get_raw().u.intValue; + } + + template <> + unsigned int script_value::get() const + { + return this->get_raw().u.uintValue; + } + + template <> + bool script_value::get() const + { + return this->get_raw().u.uintValue != 0; + } + + template <> + unsigned short script_value::get() const + { + return static_cast(this->get_raw().u.uintValue); + } + + template <> + short script_value::get() const + { + return static_cast(this->get_raw().u.intValue); + } + + template <> + char script_value::get() const + { + return static_cast(this->get_raw().u.intValue); + } + + /*********************************************** + * Float + **********************************************/ + + template <> + bool script_value::is() const + { + const auto type = this->get_raw().type; + return type == game::SCRIPT_FLOAT || type == game::SCRIPT_INTEGER; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + float script_value::get() const + { + const auto type = this->get_raw().type; + if (type == game::SCRIPT_INTEGER) + { + return static_cast(this->get_raw().u.intValue); + } + + return this->get_raw().u.floatValue; + } + + template <> + double script_value::get() const + { + const auto type = this->get_raw().type; + if (type == game::SCRIPT_INTEGER) + { + return static_cast(this->get_raw().u.intValue); + } + + return static_cast(this->get_raw().u.floatValue); + } + + /*********************************************** + * String + **********************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::SCRIPT_STRING + || this->get_raw().type == game::SCRIPT_ISTRING; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + const char* script_value::get() const + { + return game::SL_ConvertToString(static_cast(this->get_raw().u.stringValue), + game::SCRIPTINSTANCE_SERVER); + } + + template <> + std::string script_value::get() const + { + return this->get(); + } + + /*********************************************** + * Entity + **********************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::SCRIPT_OBJECT; + } + + template <> + entity script_value::get() const + { + return entity(this->get_raw().u.pointerValue); + } + + /*********************************************** + * Array + **********************************************/ + + template <> + bool script_value::is() const + { + if (this->get_raw().type != game::SCRIPT_OBJECT) + { + return false; + } + + const auto id = this->get_raw().u.uintValue; + const auto type = game::GetObjectType(game::SCRIPTINSTANCE_SERVER, id); + + return type == game::SCRIPT_ARRAY; + } + + template <> + array script_value::get() const + { + return array(this->get_raw().u.uintValue); + } + + /*********************************************** + * Struct + **********************************************/ + + template <> + bool script_value::is() const + { + if (this->get_raw().type != game::SCRIPT_OBJECT) + { + return false; + } + + const auto id = this->get_raw().u.uintValue; + const auto type = game::GetObjectType(game::SCRIPTINSTANCE_SERVER, id); + + return type == game::SCRIPT_STRUCT || type == game::SCRIPT_ENTITY; + } + + template <> + object script_value::get() const + { + return object(this->get_raw().u.uintValue); + } + + /*********************************************** + * Function + **********************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::SCRIPT_FUNCTION; + } + + template <> + function script_value::get() const + { + return function(this->get_raw().u.codePosValue); + } + + + /*********************************************** + * Vector + **********************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::SCRIPT_VECTOR; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + vector script_value::get() const + { + return this->get_raw().u.vectorValue; + } + + /*********************************************** + * + **********************************************/ + + template <> + bool script_value::is() const + { + return true; + } + + template <> + script_value script_value::get() const + { + return this; + } + + const game::VariableValue& script_value::get_raw() const + { + return this->value_.get(); + } + + std::string script_value::to_string() const + { + if (this->is()) + { + return utils::string::va("%i", this->as()); + } + + if (this->is()) + { + return utils::string::va("%f", this->as()); + } + + if (this->is()) + { + return this->as(); + } + + if (this->is()) + { + const auto vec = this->as(); + return utils::string::va("(%g, %g, %g)", + vec.get_x(), + vec.get_y(), + vec.get_z() + ); + } + + if (this->is()) + { + const auto func = this->as(); + const auto& name = func.get_name(); + return utils::string::va("[[ %s ]]", name.data()); + } + + return this->type_name(); + } + + function_argument::function_argument(const arguments& args, const script_value& value, const size_t index, const bool exists) + : script_value(value) + , values_(args) + , index_(index) + , exists_(exists) + { + + } + + function_arguments::function_arguments(const arguments& values) + : values_(values) + { + } + + variadic_args::variadic_args(const size_t begin) + : std::vector({}) + , begin_(begin) + { + } + + function_argument variadic_args::operator[](size_t index) const + { + if (index >= this->size()) + { + throw std::runtime_error(utils::string::va("parameter %d does not exist", this->begin_ + index)); + } + + return std::vector::operator[](index); + } + + function_argument function_arguments::operator[](const size_t index) const + { + if (index >= values_.size()) + { + return {values_, {}, index, false}; + } + + return {values_, values_[index], index, true}; + } + + arguments function_arguments::get_raw() const + { + return this->values_; + } + + size_t function_arguments::size() const + { + return this->values_.size(); + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/script_value.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/script_value.hpp new file mode 100644 index 0000000..af6331d --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/script_value.hpp @@ -0,0 +1,399 @@ +#pragma once +#include "game/game.hpp" +#include "variable_value.hpp" +#include "vector.hpp" + +#include + +namespace scripting +{ + class entity; + class array; + class object; + class function; + class script_value; + + namespace + { + std::unordered_map typenames = + { + {0, "undefined"}, + {1, "object"}, + {2, "string"}, + {3, "localized string"}, + {4, "vector"}, + {5, "float"}, + {6, "int"}, + {7, "codepos"}, + {8, "precodepos"}, + {9, "function"}, + {10, "stack"}, + {11, "animation"}, + {12, "developer codepos"}, + {13, "thread"}, + {14, "thread"}, + {15, "thread"}, + {16, "thread"}, + {17, "struct"}, + {18, "removed entity"}, + {19, "entity"}, + {20, "array"}, + {21, "removed thread"}, + }; + + std::string get_typename(const game::VariableValue& value) + { + auto type_ = 0; + if (value.type == game::SCRIPT_OBJECT) + { + type_ = game::GetObjectType(game::SCRIPTINSTANCE_SERVER, value.u.uintValue); + } + else + { + type_ = value.type; + } + + if (typenames.find(type_) != typenames.end()) + { + return typenames[type_]; + } + + return "unknown"; + } + + template + std::string get_c_typename() + { + auto& info = typeid(T); + + if (info == typeid(std::string)) + { + return "string"; + } + + if (info == typeid(const char*)) + { + return "string"; + } + + if (info == typeid(entity)) + { + return "entity"; + } + + if (info == typeid(array)) + { + return "array"; + } + + if (info == typeid(object)) + { + return "struct"; + } + + if (info == typeid(function)) + { + return "function"; + } + + if (info == typeid(vector)) + { + return "vector"; + } + + return info.name(); + } + } + + using arguments = std::vector; + + class script_value + { + public: + script_value() = default; + script_value(const game::VariableValue& value); + + script_value(void* value); + + script_value(int value); + script_value(unsigned int value); + script_value(bool value); + + script_value(float value); + script_value(double value); + + script_value(const char* value); + script_value(const std::string& value); + + script_value(const entity& value); + script_value(const array& value); + script_value(const object& value); + + script_value(const function& value); + + script_value(const vector& value); + + private: + template + T get() const + { + if constexpr (std::is_pointer::value) + { + if (this->is()) + { + return reinterpret_cast(this->as()); + } + } + + if constexpr (std::is_constructible::value) + { + if (this->is()) + { + return T(this->as()); + } + } + + throw std::runtime_error("Invalid type"); + } + public: + +#define ADD_TYPE(type) \ + template <> \ + bool is() const; \ +private: \ + template <> \ + type get<>() const; \ +public: \ + + template + bool is() const + { + if constexpr (std::is_pointer::value) + { + if (this->is()) + { + return true; + } + } + + if (std::is_constructible::value && this->is()) + { + return true; + } + + return false; + } + + ADD_TYPE(bool) + ADD_TYPE(int) + ADD_TYPE(unsigned int) + ADD_TYPE(unsigned short) + ADD_TYPE(float) + ADD_TYPE(float*) + ADD_TYPE(double) + ADD_TYPE(const char*) + ADD_TYPE(std::string) + + ADD_TYPE(vector) + ADD_TYPE(array) + ADD_TYPE(object) + ADD_TYPE(function) + ADD_TYPE(entity) + ADD_TYPE(script_value) + + template + T as() const + { + if (!this->is()) + { + const auto type = get_typename(this->get_raw()); + const auto c_type = get_c_typename(); + throw std::runtime_error(utils::string::va("has type '%s' but should be '%s'", type.data(), c_type.data())); + } + + return get(); + } + + template + T as_or(const T& default_value) const + { + if (!this->is()) + { + return default_value; + } + + return get(); + } + + std::string type_name() const + { + return get_typename(this->get_raw()); + } + + template class C, class T, typename ArrayType = array> + script_value(const C>& container) + { + ArrayType array_{}; + + for (const auto& value : container) + { + array_.emplace_back(value); + } + + game::VariableValue value{}; + value.type = game::SCRIPT_OBJECT; + value.u.pointerValue = array_.get_entity_id(); + + this->value_ = value; + } + + template + arguments operator()(T... arguments) const + { + return this->as().call({arguments...}); + } + + std::string to_string() const; + + friend bool operator==(const script_value& a, const script_value& b) + { + const auto& value_raw_a = a.get_raw(); + const auto& value_raw_b = b.get_raw(); + + return value_raw_a.type != value_raw_b.type && value_raw_a.u.uintValue == value_raw_b.u.uintValue; + } + + const game::VariableValue& get_raw() const; + + variable_value value_{}; + + }; + + class function_argument; + + class variadic_args : public std::vector + { + public: + variadic_args(const size_t begin); + + function_argument operator[](size_t index) const; + private: + size_t begin_; + }; + + class function_argument : public script_value + { + public: + function_argument(const arguments& args, const script_value& value, const size_t index, const bool exists); + + template + T as() const + { + if (!this->exists_) + { + throw std::runtime_error(utils::string::va("parameter %d does not exist", this->index_)); + } + + try + { + return script_value::as(); + } + catch (const std::exception& e) + { + throw std::runtime_error(utils::string::va("parameter %d %s", + this->index_, e.what())); + } + } + + template + T* as_ptr() const + { + const auto value = script_value::as(); + + if (value == nullptr) + { + throw std::runtime_error("is null"); + } + + return reinterpret_cast(value); + } + + template <> + variadic_args as() const + { + variadic_args args{this->index_}; + + for (auto i = this->index_; i < this->values_.size(); i++) + { + args.emplace_back(this->values_, this->values_[i], i, true); + } + + return args; + } + + operator variadic_args() const + { + return this->as(); + } + + template class C, class T, class ArrayType = array> + operator C>() const + { + const auto container_type = get_c_typename>>(); + if (!script_value::is()) + { + const auto type = get_typename(this->get_raw()); + + throw std::runtime_error(utils::string::va("has type '%s' but should be '%s'", + type.data(), + container_type.data() + )); + } + + C> container{}; + const auto array = script_value::as(); + for (auto i = 0u; i < array.size(); i++) + { + try + { + container.emplace_back(array.get(i).as()); + } + catch (const std::exception& e) + { + throw std::runtime_error(utils::string::va("element %d of parameter %d of type '%s' %s", + i, this->index_, container_type.data(), e.what())); + } + } + + return container; + } + + template + operator T() const + { + return this->as(); + } + + private: + arguments values_{}; + size_t index_{}; + bool exists_{}; + + }; + + class function_arguments + { + public: + function_arguments(const arguments& values); + + function_argument operator[](const size_t index) const; + + arguments get_raw() const; + + size_t size() const; + + private: + arguments values_{}; + + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/stack_isolation.cpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/stack_isolation.cpp new file mode 100644 index 0000000..ee72c5c --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/stack_isolation.cpp @@ -0,0 +1,27 @@ +#include +#include "stack_isolation.hpp" + +namespace scripting +{ + stack_isolation::stack_isolation() + { + this->in_param_count_ = game::scr_VmPub->inparamcount; + this->out_param_count_ = game::scr_VmPub->outparamcount; + this->top_ = game::scr_VmPub->top; + this->max_stack_ = game::scr_VmPub->maxstack; + + game::scr_VmPub->top = this->stack_; + game::scr_VmPub->maxstack = &this->stack_[ARRAYSIZE(this->stack_) - 1]; + game::scr_VmPub->inparamcount = 0; + game::scr_VmPub->outparamcount = 0; + } + + stack_isolation::~stack_isolation() + { + game::Scr_ClearOutParams(game::SCRIPTINSTANCE_SERVER); + game::scr_VmPub->inparamcount = this->in_param_count_; + game::scr_VmPub->outparamcount = this->out_param_count_; + game::scr_VmPub->top = this->top_; + game::scr_VmPub->maxstack = this->max_stack_; + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/stack_isolation.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/stack_isolation.hpp new file mode 100644 index 0000000..8dffd4b --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/stack_isolation.hpp @@ -0,0 +1,25 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting +{ + class stack_isolation final + { + public: + stack_isolation(); + ~stack_isolation(); + + stack_isolation(stack_isolation&&) = delete; + stack_isolation(const stack_isolation&) = delete; + stack_isolation& operator=(stack_isolation&&) = delete; + stack_isolation& operator=(const stack_isolation&) = delete; + + private: + game::VariableValue stack_[512]{}; + + game::VariableValue* max_stack_; + game::VariableValue* top_; + unsigned int in_param_count_; + unsigned int out_param_count_; + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/thread.cpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/thread.cpp new file mode 100644 index 0000000..975002e --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/thread.cpp @@ -0,0 +1,67 @@ +#include +#include "thread.hpp" +#include "execution.hpp" +#include "../../component/scripting.hpp" + +namespace scripting +{ + thread::thread(unsigned int id) + : id_(id) + , type_(game::GetObjectType(game::SCRIPTINSTANCE_SERVER, id)) + { + } + + script_value thread::get_raw() const + { + game::VariableValue value; + value.type = game::SCRIPT_OBJECT; + value.u.uintValue = this->id_; + + return value; + } + + unsigned int thread::get_entity_id() const + { + return this->id_; + } + + unsigned int thread::get_type() const + { + return this->type_; + } + + unsigned int thread::get_wait_time() const + { + return 0; + } + + unsigned int thread::get_notify_name_id() const + { + return 0; + } + + unsigned int thread::get_self() const + { + return game::Scr_GetSelf(game::SCRIPTINSTANCE_SERVER, this->id_); + } + + std::string thread::get_notify_name() const + { + return game::SL_ConvertToString(this->get_notify_name_id(), game::SCRIPTINSTANCE_SERVER); + } + + const char* thread::get_pos() const + { + return 0; + } + + const char* thread::get_start_pos() const + { + return 0; + } + + void thread::kill() const + { + + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/thread.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/thread.hpp new file mode 100644 index 0000000..29d8476 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/thread.hpp @@ -0,0 +1,32 @@ +#pragma once +#include "entity.hpp" +#include "script_value.hpp" + +namespace scripting +{ + class thread + { + public: + thread(unsigned int); + + script_value get_raw() const; + + unsigned int get_entity_id() const; + unsigned int get_type() const; + + unsigned int get_self() const; + unsigned int get_wait_time() const; + unsigned int get_notify_name_id() const; + std::string get_notify_name() const; + + const char* get_pos() const; + const char* get_start_pos() const; + + void kill() const; + + private: + unsigned int id_{}; + unsigned int type_{}; + + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/variable_value.cpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/variable_value.cpp new file mode 100644 index 0000000..cdd5e9b --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/variable_value.cpp @@ -0,0 +1,68 @@ +#include +#include "variable_value.hpp" + +namespace scripting +{ + variable_value::variable_value(const game::VariableValue& value) + { + this->assign(value); + } + + variable_value::variable_value(const variable_value& other) noexcept + { + this->operator=(other); + } + + variable_value::variable_value(variable_value&& other) noexcept + { + this->operator=(std::move(other)); + } + + variable_value& variable_value::operator=(const variable_value& other) noexcept + { + if (this != &other) + { + this->release(); + this->assign(other.value_); + } + + return *this; + } + + variable_value& variable_value::operator=(variable_value&& other) noexcept + { + if (this != &other) + { + this->release(); + this->value_ = other.value_; + other.value_.type = game::SCRIPT_NONE; + } + + return *this; + } + + variable_value::~variable_value() + { + this->release(); + } + + const game::VariableValue& variable_value::get() const + { + return this->value_; + } + + void variable_value::assign(const game::VariableValue& value) + { + this->value_ = value; + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &this->value_); + } + + void variable_value::release() + { + if (this->value_.type != game::SCRIPT_NONE) + { + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, this->value_.type, this->value_.u); + this->value_.type = game::SCRIPT_NONE; + } + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/variable_value.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/variable_value.hpp new file mode 100644 index 0000000..7a96261 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/variable_value.hpp @@ -0,0 +1,27 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting +{ + class variable_value + { + public: + variable_value() = default; + variable_value(const game::VariableValue& value); + variable_value(const variable_value& other) noexcept; + variable_value(variable_value&& other) noexcept; + + variable_value& operator=(const variable_value& other) noexcept; + variable_value& operator=(variable_value&& other) noexcept; + + ~variable_value(); + + const game::VariableValue& get() const; + + private: + void assign(const game::VariableValue& value); + void release(); + + game::VariableValue value_{{0}, game::SCRIPT_NONE}; + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/vector.cpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/vector.cpp new file mode 100644 index 0000000..143e29a --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/vector.cpp @@ -0,0 +1,85 @@ +#include +#include "vector.hpp" + +namespace scripting +{ + vector::vector(const float* value) + { + for (auto i = 0; i < 3; ++i) + { + this->value_[i] = value[i]; + } + } + + vector::vector(const game::vec3_t& value) + : vector(&value[0]) + { + } + + vector::vector(const float x, const float y, const float z) + { + this->value_[0] = x; + this->value_[1] = y; + this->value_[2] = z; + } + + vector::operator game::vec3_t& () + { + return this->value_; + } + + vector::operator const game::vec3_t& () const + { + return this->value_; + } + + game::vec_t& vector::operator[](const size_t i) + { + if (i >= 3) + { + throw std::runtime_error("Out of bounds."); + } + + return this->value_[i]; + } + + const game::vec_t& vector::operator[](const size_t i) const + { + if (i >= 3) + { + throw std::runtime_error("Out of bounds."); + } + + return this->value_[i]; + } + + float vector::get_x() const + { + return this->operator[](0); + } + + float vector::get_y() const + { + return this->operator[](1); + } + + float vector::get_z() const + { + return this->operator[](2); + } + + void vector::set_x(const float value) + { + this->operator[](0) = value; + } + + void vector::set_y(const float value) + { + this->operator[](1) = value; + } + + void vector::set_z(const float value) + { + this->operator[](2) = value; + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/scripting/vector.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/vector.hpp new file mode 100644 index 0000000..0bb1595 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/scripting/vector.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting +{ + class vector final + { + public: + vector() = default; + vector(const float* value); + vector(const game::vec3_t& value); + vector(float x, float y, float z); + + operator game::vec3_t& (); + operator const game::vec3_t& () const; + + game::vec_t& operator[](size_t i); + const game::vec_t& operator[](size_t i) const; + + float get_x() const; + float get_y() const; + float get_z() const; + + void set_x(float value); + void set_y(float value); + void set_z(float value); + + private: + game::vec3_t value_{ 0 }; + }; +} \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/structs.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/structs.hpp new file mode 100644 index 0000000..672b45c --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/structs.hpp @@ -0,0 +1,857 @@ +#pragma once + +namespace game +{ + typedef float vec_t; + typedef vec_t vec2_t[2]; + typedef vec_t vec3_t[3]; + typedef vec_t vec4_t[4]; + + enum scriptInstance_t + { + SCRIPTINSTANCE_SERVER, + SCRIPTINSTANCE_CLIENT, + }; + + struct scr_entref_t + { + unsigned short entnum; + unsigned short classnum; + int client; + }; + + struct BuiltinMethodDef + { + const char* actionString; + unsigned int constId; + int min_args; + int max_args; + void(__cdecl* actionFunc)(scr_entref_t); + int type; + }; + + struct BuiltinFunctionDef + { + const char* actionString; + unsigned int constId; + int min_args; + int max_args; + void(__cdecl* actionFunc)(); + int type; + }; + + enum scriptType_e + { + SCRIPT_NONE = 0, + SCRIPT_OBJECT = 1, + SCRIPT_STRING = 2, + SCRIPT_ISTRING = 3, + SCRIPT_VECTOR = 4, + SCRIPT_FLOAT = 5, + SCRIPT_INTEGER = 6, + SCRIPT_CODEPOS = 7, + SCRIPT_END = 8, + SCRIPT_FUNCTION = 9, + SCRIPT_THREAD = 13, + SCRIPT_NOTIFY_THREAD = 14, + SCRIPT_TIME_THREAD = 15, + SCRIPT_STRUCT = 17, + SCRIPT_ENTITY = 19, + SCRIPT_ARRAY = 20, + SCRIPT_FREE = 0x17, + }; + + struct VariableStackBuffer + { + const char* pos; + unsigned __int16 size; + unsigned __int16 bufLen; + unsigned __int16 localId; + char time; + char buf[1]; + }; + + union VariableUnion + { + int intValue; + unsigned int uintValue; + float floatValue; + unsigned int stringValue; + const float* vectorValue; + const char* codePosValue; + unsigned int pointerValue; + VariableStackBuffer* stackValue; + unsigned int entityOffset; + }; + + struct VariableValue + { + VariableUnion u; + int type; + }; + + struct function_stack_t + { + char* pos; + VariableValue* top; + unsigned int localId; + unsigned int localVarCount; + VariableValue* startTop; + }; + + struct function_frame_t + { + function_stack_t fs; + }; + + struct scrVmPub_t + { + unsigned int* localVars; + VariableValue* maxstack; + int function_count; + function_frame_t* function_frame; + VariableValue* top; + bool abort_on_error; + bool terminal_error; + bool block_execution; + unsigned int inparamcount; + unsigned int outparamcount; + function_frame_t function_frame_start[32]; + VariableValue stack[2048]; + void(__cdecl* notifyListeners[1])(unsigned int, unsigned int); + }; + + static_assert(offsetof(scrVmPub_t, top) == 16); + static_assert(offsetof(scrVmPub_t, inparamcount) == 24); + + struct scrVarPub_t + { + const char* fieldBuffer; + unsigned __int16 canonicalStrCount; + bool developer; + bool developer_script; + bool evaluate; + const char* error_message; + int error_index; + unsigned int time; + unsigned int timeArrayId; + unsigned int pauseArrayId; + unsigned int levelId; + unsigned int gameId; + unsigned int animId; + unsigned int freeEntList; + unsigned int tempVariable; + bool bInited; + unsigned __int16 savecount; + unsigned int checksum; + unsigned int entId; + unsigned int entFieldName; + void* programHunkUser; + const char* programBuffer; + const char* endScriptBuffer; + unsigned __int16* saveIdMap; + unsigned __int16* saveIdMapRev; + unsigned int numScriptThreads; + unsigned int numScriptValues; + unsigned int numScriptObjects; + const char* varUsagePos; + int ext_threadcount; + int totalObjectRefCount; + volatile int totalVectorRefCount; + }; + + union Variable_u + { + unsigned int prev; + unsigned int prevSibling; + }; + + struct Variable + { + unsigned int id; + Variable_u u; + }; + + union ObjectInfo_u + { + unsigned __int16 entnum; + unsigned __int16 size; + unsigned int nextEntId; + unsigned int self; + }; + + struct ObjectInfo + { + unsigned __int16 refCount; + ObjectInfo_u u; + }; + + union VariableValueInternal_u + { + unsigned int next; + VariableUnion u; + ObjectInfo o; + }; + + union VariableValueInternal_w + { + unsigned int status; + unsigned int type; + unsigned int name; + unsigned int classnum; + unsigned int notifyName; + unsigned int waitTime; + unsigned int parentLocalId; + }; + + union VariableValueInternal_v + { + unsigned int next; + unsigned int index; + }; + + enum fsMode_t + { + FS_READ = 0x0, + FS_WRITE = 0x1, + FS_APPEND = 0x2, + FS_APPEND_SYNC = 0x3, + }; + + enum errorParm_t + { + ERR_FATAL = 0x0, + ERR_DROP = 0x1, + ERR_SERVERDISCONNECT = 0x2, + ERR_DISCONNECT = 0x3, + ERR_SCRIPT = 0x4, + ERR_SCRIPT_DROP = 0x5, + ERR_LOCALIZATION = 0x6, + }; + + enum conChannel_t + { + CON_CHANNEL_DONT_FILTER = 0x0, + CON_CHANNEL_ERROR = 0x1, + CON_CHANNEL_GAMENOTIFY = 0x2, + CON_CHANNEL_BOLDGAME = 0x3, + CON_CHANNEL_SUBTITLE = 0x4, + CON_CHANNEL_OBITUARY = 0x5, + CON_CHANNEL_LOGFILEONLY = 0x6, + CON_CHANNEL_CONSOLEONLY = 0x7, + CON_CHANNEL_GFX = 0x8, + CON_CHANNEL_SOUND = 0x9, + CON_CHANNEL_FILES = 0xA, + CON_CHANNEL_DEVGUI = 0xB, + CON_CHANNEL_PROFILE = 0xC, + CON_CHANNEL_UI = 0xD, + CON_CHANNEL_CLIENT = 0xE, + CON_CHANNEL_SERVER = 0xF, + CON_CHANNEL_SYSTEM = 0x10, + CON_CHANNEL_PLAYERWEAP = 0x11, + CON_CHANNEL_AI = 0x12, + CON_CHANNEL_ANIM = 0x13, + CON_CHANNEL_PHYS = 0x14, + CON_CHANNEL_FX = 0x15, + CON_CHANNEL_LEADERBOARDS = 0x16, + CON_CHANNEL_LIVE = 0x17, + CON_CHANNEL_PARSERSCRIPT = 0x18, + CON_CHANNEL_SCRIPT = 0x19, + CON_CHANNEL_SPAWNSYSTEM = 0x1A, + CON_CHANNEL_COOPINFO = 0x1B, + CON_CHANNEL_SERVERDEMO = 0x1C, + CON_CHANNEL_DDL = 0x1D, + CON_CHANNEL_NETWORK = 0x1E, + CON_CHANNEL_SCHEDULER = 0x1F, + CON_FIRST_DEBUG_CHANNEL = 0x1F, + CON_CHANNEL_TASK = 0x20, + CON_CHANNEL_SPU = 0x21, + + CON_BUILTIN_CHANNEL_COUNT = 0x22, + }; + + namespace sp + { + struct ObjectInfo + { + unsigned __int16 refCount; + unsigned __int16 size; + }; + + union VariableValueInternal_u + { + VariableUnion u; + ObjectInfo o; + }; + + struct VariableValueInternal + { + unsigned int id; + VariableValueInternal_u u; + VariableValueInternal_w w; + char __pad1[2]; + unsigned __int16 nextSibling; + }; + + static_assert(sizeof(VariableValueInternal) == 16); + static_assert(offsetof(VariableValueInternal, nextSibling) == 14); + static_assert(offsetof(VariableValueInternal, u) == 4); + static_assert(offsetof(VariableValueInternal, w) == 8); + } + + namespace mp + { + struct VariableValueInternal + { + Variable hash; + VariableValueInternal_u u; + VariableValueInternal_w w; + VariableValueInternal_v v; + unsigned int nextSibling; + }; + } + + union scrVarGlob_t + { + sp::VariableValueInternal* variableList_sp; + mp::VariableValueInternal* variableList_mp; + }; + + struct scr_classStruct_t + { + unsigned __int16 id; + unsigned __int16 entArrayId; + char charId; + const char* name; + }; + + enum gclientFlag + { + NOCLIP = 1 << 0, + UFO = 1 << 1, + }; + + enum entityFlag + { + FL_GODMODE = 1 << 0, + FL_DEMI_GODMODE = 1 << 1, + FL_NOTARGET = 1 << 2, + FL_SUPPORTS_LINKTO = 1 << 12, + }; // TODO: Finish + + struct gclient_s + { + char __pad0[0x18]; + int eflags; + char __pad1[0x5668]; + int flags; + }; + + struct gentity_s + { + int number; + char __pad0[0x150]; + gclient_s* client; // 340 + char __pad1[0x30]; + int flags; // 392 + char __pad2[0x190]; + }; + + static_assert(sizeof(gentity_s) == 0x31C); + + enum clientState_t + { + CS_FREE, + CS_ZOMBIE, + CS_RECONNECTING, + CS_CONNECTED, + CS_CLIENTLOADING, + CS_ACTIVE, + }; + + enum netsrc_t + { + NS_CLIENT1 = 0x0, + NS_CLIENT2 = 0x1, + NS_CLIENT3 = 0x2, + NS_CLIENT4 = 0x3, + NS_SERVER = 0x4, + NS_PACKET = 0x5, + NS_NULL = -1, + }; + + enum netadrtype_t + { + NA_BOT = 0x0, + NA_BAD = 0x1, + NA_LOOPBACK = 0x2, + NA_BROADCAST = 0x3, + NA_IP = 0x4, + }; + + struct netadr_t + { + union + { + unsigned char ip[4]; + unsigned int inaddr; + }; + unsigned __int16 port; + netadrtype_t type; + netsrc_t localNetID; + unsigned __int16 serverID; + }; + + static_assert(sizeof(netadr_t) == 0x14); + + struct netProfileInfo_t + { + unsigned char __pad0[0x5E0]; + }; + + struct netchan_t + { + int outgoingSequence; + netsrc_t sock; + int dropped; + int incomingSequence; + netadr_t remoteAddress; + int qport; + int fragmentSequence; + int fragmentLength; + unsigned char* fragmentBuffer; + int fragmentBufferSize; + int unsentFragments; + int unsentOnLoan; + int unsentFragmentStart; + int unsentLength; + unsigned char* unsentBuffer; + int unsentBufferSize; + int reliable_fragments; + unsigned char fragment_send_count[128]; + unsigned int fragment_ack[4]; + int lowest_send_count; + netProfileInfo_t prof; + }; + + static_assert(sizeof(netchan_t) == 0x6C8); + + struct PredictedVehicleDef + { + bool fullPhysics; + vec3_t origin; + vec3_t angles; + vec3_t tVel; + vec3_t aVel; + int serverTime; + }; + + static_assert(sizeof(PredictedVehicleDef) == 0x38); + + struct clientHeader_t + { + clientState_t state; + int sendAsActive; + int deltaMessage; + int rateDelayed; + int hasAckedBaselineData; + int hugeSnapshotSent; + netchan_t netchan; + vec3_t predictedOrigin; + int predictedOriginServerTime; + int migrationState; + PredictedVehicleDef predictedVehicle; + }; + + static_assert(sizeof(clientHeader_t) == 0x72C); + + struct client_s + { + clientHeader_t header; + const char* dropReason; + char userinfo[1024]; + unsigned char __pad0[0x3F75C]; + int bIsTestClient; + unsigned char __pad1[0xDEF0]; + }; + + static_assert(sizeof(client_s) == 0x4E180); + + struct cmd_function_t + { + cmd_function_t* next; + const char* name; + const char* autoCompleteDir; + const char* autoCompleteExt; + void(__cdecl* function)(); + int flags; + }; + + struct CmdArgs + { + int nesting; + int localClientNum[8]; + int controllerIndex[8]; + void* itemDef[8]; + int argshift[8]; + int argc[8]; + const char** argv[8]; + char textPool[8192]; + const char* argvPool[512]; + int usedTextPool[8]; + int totalUsedArgvPool; + int totalUsedTextPool; + }; + + enum dvarType_t + { + DVAR_TYPE_INVALID = 0x0, + DVAR_TYPE_BOOL = 0x1, + DVAR_TYPE_FLOAT = 0x2, + DVAR_TYPE_FLOAT_2 = 0x3, + DVAR_TYPE_FLOAT_3 = 0x4, + DVAR_TYPE_FLOAT_4 = 0x5, + DVAR_TYPE_INT = 0x6, + DVAR_TYPE_ENUM = 0x7, + DVAR_TYPE_STRING = 0x8, + DVAR_TYPE_COLOR = 0x9, + DVAR_TYPE_INT64 = 0xA, + DVAR_TYPE_LINEAR_COLOR_RGB = 0xB, + DVAR_TYPE_COLOR_XYZ = 0xC, + DVAR_TYPE_COUNT = 0xD, + }; + + union DvarValue + { + bool enabled; + int integer; + unsigned int unsignedInt; + __int64 integer64; + unsigned __int64 unsignedInt64; + float value; + vec4_t vector; + const char* string; + char color[4]; + }; + + struct $A37BA207B3DDD6345C554D4661813EDD + { + int stringCount; + const char* const* strings; + }; + + struct $9CA192F9DB66A3CB7E01DE78A0DEA53D + { + int min; + int max; + }; + + struct $251C2428A496074035CACA7AAF3D55BD + { + float min; + float max; + }; + + struct DvarLimits_integer64 + { + __int64 min; + __int64 max; + }; + + union DvarLimits + { + $A37BA207B3DDD6345C554D4661813EDD enumeration; + $9CA192F9DB66A3CB7E01DE78A0DEA53D integer; + $251C2428A496074035CACA7AAF3D55BD value; + $251C2428A496074035CACA7AAF3D55BD vector; + DvarLimits_integer64 integer64; + }; + + struct dvar_t + { + const char* name; + const char* description; + int hash; + unsigned int flags; + dvarType_t type; + bool modified; + bool loadedFromSaveGame; + DvarValue current; + DvarValue latched; + DvarValue saved; + DvarValue reset; + DvarLimits domain; + dvar_t* hashNext; + }; + + static_assert(offsetof(dvar_t, hashNext) == 104); + static_assert(offsetof(dvar_t, current) == 24); + + struct GSC_OBJ + { + char magic[8]; + unsigned int source_crc; + unsigned int include_offset; + unsigned int animtree_offset; + unsigned int cseg_offset; + unsigned int stringtablefixup_offset; + unsigned int exports_offset; + unsigned int imports_offset; + unsigned int fixup_offset; + unsigned int profile_offset; + unsigned int cseg_size; + unsigned __int16 name; + unsigned __int16 stringtablefixup_count; + unsigned __int16 exports_count; + unsigned __int16 imports_count; + unsigned __int16 fixup_count; + unsigned __int16 profile_count; + char include_count; + char animtree_count; + char flags; + }; + + struct GSC_EXPORT_ITEM + { + unsigned int checksum; + unsigned int address; + unsigned __int16 name; + char param_count; + char flags; + }; + + // gsc-tool + enum class opcode : std::uint8_t + { + OP_End = 0x0, + OP_Return = 0x1, + OP_GetUndefined = 0x2, + OP_GetZero = 0x3, + OP_GetByte = 0x4, + OP_GetNegByte = 0x5, + OP_GetUnsignedShort = 0x6, + OP_GetNegUnsignedShort = 0x7, + OP_GetInteger = 0x8, + OP_GetFloat = 0x9, + OP_GetString = 0xA, + OP_GetIString = 0xB, + OP_GetVector = 0xC, + OP_GetLevelObject = 0xD, + OP_GetAnimObject = 0xE, + OP_GetSelf = 0xF, + OP_GetLevel = 0x10, + OP_GetGame = 0x11, + OP_GetAnim = 0x12, + OP_GetAnimation = 0x13, + OP_GetGameRef = 0x14, + OP_GetFunction = 0x15, + OP_CreateLocalVariable = 0x16, + OP_SafeCreateLocalVariables = 0x17, + OP_RemoveLocalVariables = 0x18, + OP_EvalLocalVariableCached = 0x19, + OP_EvalArray = 0x1A, + OP_EvalLocalArrayRefCached = 0x1B, + OP_EvalArrayRef = 0x1C, + OP_ClearArray = 0x1D, + OP_EmptyArray = 0x1E, + OP_GetSelfObject = 0x1F, + OP_EvalFieldVariable = 0x20, + OP_EvalFieldVariableRef = 0x21, + OP_ClearFieldVariable = 0x22, + OP_SafeSetVariableFieldCached = 0x23, + OP_SafeSetWaittillVariableFieldCached = 0x24, + OP_ClearParams = 0x25, + OP_CheckClearParams = 0x26, + OP_EvalLocalVariableRefCached = 0x27, + OP_SetVariableField = 0x28, + OP_CallBuiltin = 0x29, + OP_CallBuiltinMethod = 0x2A, + OP_Wait = 0x2B, + OP_WaitTillFrameEnd = 0x2C, + OP_PreScriptCall = 0x2D, + OP_ScriptFunctionCall = 0x2E, + OP_ScriptFunctionCallPointer = 0x2F, + OP_ScriptMethodCall = 0x30, + OP_ScriptMethodCallPointer = 0x31, + OP_ScriptThreadCall = 0x32, + OP_ScriptThreadCallPointer = 0x33, + OP_ScriptMethodThreadCall = 0x34, + OP_ScriptMethodThreadCallPointer = 0x35, + OP_DecTop = 0x36, + OP_CastFieldObject = 0x37, + OP_CastBool = 0x38, + OP_BoolNot = 0x39, + OP_BoolComplement = 0x3A, + OP_JumpOnFalse = 0x3B, + OP_JumpOnTrue = 0x3C, + OP_JumpOnFalseExpr = 0x3D, + OP_JumpOnTrueExpr = 0x3E, + OP_Jump = 0x3F, + OP_JumpBack = 0x40, + OP_Inc = 0x41, + OP_Dec = 0x42, + OP_Bit_Or = 0x43, + OP_Bit_Xor = 0x44, + OP_Bit_And = 0x45, + OP_Equal = 0x46, + OP_NotEqual = 0x47, + OP_LessThan = 0x48, + OP_GreaterThan = 0x49, + OP_LessThanOrEqualTo = 0x4A, + OP_GreaterThanOrEqualTo = 0x4B, + OP_ShiftLeft = 0x4C, + OP_ShiftRight = 0x4D, + OP_Plus = 0x4E, + OP_Minus = 0x4F, + OP_Multiply = 0x50, + OP_Divide = 0x51, + OP_Modulus = 0x52, + OP_SizeOf = 0x53, + OP_WaitTillMatch = 0x54, + OP_WaitTill = 0x55, + OP_Notify = 0x56, + OP_EndOn = 0x57, + OP_VoidCodePos = 0x58, + OP_Switch = 0x59, + OP_EndSwitch = 0x5A, + OP_Vector = 0x5B, + OP_GetHash = 0x5C, + OP_RealWait = 0x5D, + OP_VectorConstant = 0x5E, + OP_IsDefined = 0x5F, + OP_VectorScale = 0x60, + OP_AnglesToUp = 0x61, + OP_AnglesToRight = 0x62, + OP_AnglesToForward = 0x63, + OP_AngleClamp180 = 0x64, + OP_VectorToAngles = 0x65, + OP_Abs = 0x66, + OP_GetTime = 0x67, + OP_GetDvar = 0x68, + OP_GetDvarInt = 0x69, + OP_GetDvarFloat = 0x6A, + OP_GetDvarVector = 0x6B, + OP_GetDvarColorRed = 0x6C, + OP_GetDvarColorGreen = 0x6D, + OP_GetDvarColorBlue = 0x6E, + OP_GetDvarColorAlpha = 0x6F, + OP_FirstArrayKey = 0x70, + OP_NextArrayKey = 0x71, + OP_ProfileStart = 0x72, + OP_ProfileStop = 0x73, + OP_SafeDecTop = 0x74, + OP_Nop = 0x75, + OP_Abort = 0x76, + OP_Object = 0x77, + OP_ThreadObject = 0x78, + OP_EvalLocalVariable = 0x79, + OP_EvalLocalVariableRef = 0x7A, + OP_DevblockBegin = 0x7B, + OP_DevblockEnd = 0x7C, + OP_Breakpoint = 0x7D, + OP_Count = 0x7E, + }; + + enum XAssetType + { + ASSET_TYPE_XMODELPIECES = 0x0, + ASSET_TYPE_PHYSPRESET = 0x1, + ASSET_TYPE_PHYSCONSTRAINTS = 0x2, + ASSET_TYPE_DESTRUCTIBLEDEF = 0x3, + ASSET_TYPE_XANIMPARTS = 0x4, + ASSET_TYPE_XMODEL = 0x5, + ASSET_TYPE_MATERIAL = 0x6, + ASSET_TYPE_TECHNIQUE_SET = 0x7, + ASSET_TYPE_IMAGE = 0x8, + ASSET_TYPE_SOUND = 0x9, + ASSET_TYPE_SOUND_PATCH = 0xA, + ASSET_TYPE_CLIPMAP = 0xB, + ASSET_TYPE_CLIPMAP_PVS = 0xC, + ASSET_TYPE_COMWORLD = 0xD, + ASSET_TYPE_GAMEWORLD_SP = 0xE, + ASSET_TYPE_GAMEWORLD_MP = 0xF, + ASSET_TYPE_MAP_ENTS = 0x10, + ASSET_TYPE_GFXWORLD = 0x11, + ASSET_TYPE_LIGHT_DEF = 0x12, + ASSET_TYPE_UI_MAP = 0x13, + ASSET_TYPE_FONT = 0x14, + ASSET_TYPE_FONTICON = 0x15, + ASSET_TYPE_MENULIST = 0x16, + ASSET_TYPE_MENU = 0x17, + ASSET_TYPE_LOCALIZE_ENTRY = 0x18, + ASSET_TYPE_WEAPON = 0x19, + ASSET_TYPE_WEAPONDEF = 0x1A, + ASSET_TYPE_WEAPON_VARIANT = 0x1B, + ASSET_TYPE_WEAPON_FULL = 0x1C, + ASSET_TYPE_ATTACHMENT = 0x1D, + ASSET_TYPE_ATTACHMENT_UNIQUE = 0x1E, + ASSET_TYPE_WEAPON_CAMO = 0x1F, + ASSET_TYPE_SNDDRIVER_GLOBALS = 0x20, + ASSET_TYPE_FX = 0x21, + ASSET_TYPE_IMPACT_FX = 0x22, + ASSET_TYPE_AITYPE = 0x23, + ASSET_TYPE_MPTYPE = 0x24, + ASSET_TYPE_MPBODY = 0x25, + ASSET_TYPE_MPHEAD = 0x26, + ASSET_TYPE_CHARACTER = 0x27, + ASSET_TYPE_XMODELALIAS = 0x28, + ASSET_TYPE_RAWFILE = 0x29, + ASSET_TYPE_STRINGTABLE = 0x2A, + ASSET_TYPE_LEADERBOARD = 0x2B, + ASSET_TYPE_XGLOBALS = 0x2C, + ASSET_TYPE_DDL = 0x2D, + ASSET_TYPE_GLASSES = 0x2E, + ASSET_TYPE_EMBLEMSET = 0x2F, + ASSET_TYPE_SCRIPTPARSETREE = 0x30, + ASSET_TYPE_KEYVALUEPAIRS = 0x31, + ASSET_TYPE_VEHICLEDEF = 0x32, + ASSET_TYPE_MEMORYBLOCK = 0x33, + ASSET_TYPE_ADDON_MAP_ENTS = 0x34, + ASSET_TYPE_TRACER = 0x35, + ASSET_TYPE_SKINNEDVERTS = 0x36, + ASSET_TYPE_QDB = 0x37, + ASSET_TYPE_SLUG = 0x38, + ASSET_TYPE_FOOTSTEP_TABLE = 0x39, + ASSET_TYPE_FOOTSTEPFX_TABLE = 0x3A, + ASSET_TYPE_ZBARRIER = 0x3B, + ASSET_TYPE_COUNT = 0x3C, + ASSET_TYPE_STRING = 0x3C, + ASSET_TYPE_ASSETLIST = 0x3D, + ASSET_TYPE_REPORT = 0x3E, + ASSET_TYPE_DEPEND = 0x3F, + ASSET_TYPE_FULL_COUNT = 0x40, + }; + + struct ScriptParseTree + { + const char* name; + int len; + GSC_OBJ* obj; + }; + + struct objFileInfo_t + { + GSC_OBJ* activeVersion; + char __pad[0x24]; + }; + + union XAssetHeader + { + ScriptParseTree* scriptParseTree; + }; + + union sval_u + { + unsigned __int8 type; + unsigned int stringValue; + unsigned int idValue; + float floatValue; + int intValue; + sval_u* node; + unsigned int sourcePosValue; + const char* codePosValue; + const char* debugString; + void* block; + }; + +} \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/src/game/symbols.hpp b/t5/plugins/tws-t5-gsc-utils/src/game/symbols.hpp new file mode 100644 index 0000000..822083c --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/game/symbols.hpp @@ -0,0 +1,148 @@ +#pragma once + +#define WEAK __declspec(selectany) + +namespace game +{ + // Functions + + WEAK symbol BG_StringHashValue{0x0, 0x0}; + + WEAK symbol G_LogPrintf{0x69EA30, 0x5CD250}; + + WEAK symbol Cbuf_InsertText{0x0, 0x0}; + WEAK symbol Cbuf_AddText{0x49B930, 0x56EF70}; + WEAK symbol Cmd_ExecuteSingleCommand{0x619D00, 0x50B470}; + WEAK symbol Cmd_AddCommandInternal{0x661400, 0x6AD580}; + WEAK symbol Cmd_Argv{0x0, 0x0}; + WEAK symbol Cmd_RemoveCommand{0x5F1A90, 0x527EA0}; + + WEAK symbol Com_Error{0x651D90, 0x627380}; + WEAK symbol Com_Printf{0x43BF30, 0x4126C0}; + WEAK symbol Com_Printf_NoFilter{0x566BC0, 0x64C260}; + WEAK symbol Com_PrintWarning{0x51CE50, 0x54B6A0}; + + WEAK symbol Dvar_FindVar{0x5AE810, 0x512F70}; + WEAK symbol Dvar_GetInt{0x0, 0x0}; + WEAK symbol Dvar_RegisterInt{0x0, 0x0}; + WEAK symbol Dvar_InfoString{0x613060, 0x54A8B0}; + + WEAK symbol DB_FindXAssetHeader{0x0, 0x0}; + + WEAK symbol I_CleanStr{0x0, 0x0}; + WEAK symbol I_strncpyz{0x5D4D60, 0x5A7140}; + + WEAK symbol Player_GetMethod{0x0, 0x0}; + WEAK symbol Scr_GetCommonFunction{0x0, 0x0}; + WEAK symbol Scr_GetMethod{0x0, 0x0}; + + WEAK symbol Scr_AddEntity{0x0, 0x0}; + WEAK symbol Scr_AddFloat{0x0, 0x0}; + WEAK symbol Scr_AddInt{0x0, 0x0}; + WEAK symbol Scr_AddString{0x0, 0x0}; + WEAK symbol Scr_AddVector{0x0, 0x0}; + WEAK symbol Scr_AddObject{0x0, 0x0}; + + WEAK symbol Scr_ClearOutParams{0x654D10, 0x588680}; + + WEAK symbol AllocObject{0x603400, 0x6970B0}; + WEAK symbol AllocThread{0x69E140, 0x43CA60}; + WEAK symbol RemoveRefToObject{0x5517B0, 0x698FA0}; + WEAK symbol RemoveRefToVector{0x0, 0x0}; + WEAK symbol AddRefToValue_{0x53FD50, 0x6706B0}; + + namespace mp + { + WEAK symbol RemoveRefToValue{0x0, 0x4249C0}; + } + + namespace sp + { + WEAK symbol RemoveRefToValue{0x556830, 0x0}; + } + + WEAK symbol RemoveVariableValue{0x0, 0x0}; + + WEAK symbol FindVariable{0x476720, 0x5DF7E0}; + WEAK symbol FindArrayVariable{0x62F8E0, 0x509230}; + WEAK symbol GetVariable{0x0, 0x0}; + WEAK symbol GetNewVariable{0x674820, 0x5B3750}; + WEAK symbol GetNewArrayVariable{0x689C60, 0x468BC0}; + + WEAK symbol Scr_SetObjectField{0x0, 0x0}; + WEAK symbol GetEntityFieldValue{0x0, 0x0}; + + WEAK symbol Scr_Notify{0x0, 0x0}; + WEAK symbol Scr_NotifyNum_Internal{0x41D270, 0x4162E0}; + WEAK symbol Scr_NotifyNum{0x0, 0x0}; + + WEAK symbol SL_GetString{0x463130, 0x4B1770}; + WEAK symbol SL_ConvertToString{0x687530, 0x624C70}; + WEAK symbol SL_GetCanonicalString{0x4FB120, 0x68D860}; + + WEAK symbol Scr_GetNumParam{0x0, 0x0}; + WEAK symbol Scr_GetEntity{0x0, 0x0}; + WEAK symbol Scr_GetFloat{0x0, 0x0}; + WEAK symbol Scr_GetInt{0x0, 0x0}; + WEAK symbol Scr_GetString{0x0, 0x0}; + WEAK symbol Scr_GetVector{0x0, 0x0}; + WEAK symbol Scr_AllocVector{0x41B400, 0x4C4440}; + WEAK symbol Scr_GetFunctionHandle{0x0, 0x0}; + WEAK symbol Scr_AddClassField{0x0, 0x0}; + WEAK symbol Scr_GetEntityId{0x4B7050, 0x6A07D0}; + WEAK symbol GetStartLocalId{0x0, 0x0}; + WEAK symbol Scr_TerminateRunningThread{0x0, 0x0}; + WEAK symbol Scr_Error{0x4B5940, 0x6245E0}; + WEAK symbol Scr_ParamError{0x0, 0x0}; + WEAK symbol Scr_ObjectError{0x0, 0x0}; + WEAK symbol Scr_GetEntityIdRef{0x433B60, 0x6A53C0}; + WEAK symbol Scr_AllocArray{0x438800, 0x5B8400}; + + WEAK symbol GetPlayerEntity{0x0, 0x0}; + + WEAK symbol VM_Execute{0x8ACE60, 0x8EADE0}; + + WEAK symbol SV_GetUserinfo{0x50DCD0, 0x6916A0}; + WEAK symbol SV_GameDropClient{0x0, 0x0}; + WEAK symbol SV_IsTestClient{0x0, 0x0}; + WEAK symbol SV_GameSendServerCommand{0x543CF0, 0x6B8730}; + + WEAK symbol ClientDisconnect{0x4F4000, 0x66FA00}; + WEAK symbol ClientUserinfoChanged{0x67FFC0, 0x548D80}; + + WEAK symbol Sys_GetValue{0x67D4F0, 0x529EB0}; + WEAK symbol Sys_Milliseconds{0x0, 0x0}; + + WEAK symbol FS_FOpenFileByMode{0x4DD530, 0x40C790}; + WEAK symbol FS_FCloseFile{0x46CAA0, 0x533020}; + + WEAK symbol longjmp{0x96B980, 0x9D05C4}; + WEAK symbol _setjmp{0x969EAC, 0x9CED5C}; + + // Variables + + WEAK symbol g_script_error_level{0x32D1E18, 0x3DD4A18}; + WEAK symbol g_script_error{0x32D0D98, 0x3DD3998}; + + WEAK symbol scr_VmPub{0x32C8738, 0x3DCB338}; + WEAK symbol scr_VarGlob{0x32C8580, 0x3DCB180}; + WEAK symbol scr_VarPub{0x32C8680, 0x3DCB280}; + WEAK symbol scr_starttime{0x0, 0x0}; + WEAK symbol fs{0x0, 0x0}; + + WEAK symbol sv_configstrings{0x0, 0x0}; + + WEAK symbol g_classMap{0x0, 0x0}; + + WEAK symbol g_entities{0x0, 0x0}; + WEAK symbol levelEntityId{0x32C86A0, 0x3DCB2A0}; + + WEAK symbol svs_clients{0x0, 0x0}; + + WEAK symbol sv_cmd_args{0x243D208, 0x355BD88}; + + WEAK symbol logFile{0x1C0417C, 0x3443838}; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/loader/component_interface.hpp b/t5/plugins/tws-t5-gsc-utils/src/loader/component_interface.hpp new file mode 100644 index 0000000..2f5b7c6 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/loader/component_interface.hpp @@ -0,0 +1,35 @@ +#pragma once +#include "../plugin.hpp" + +class component_interface +{ +public: + virtual ~component_interface() + { + } + + virtual void on_startup([[maybe_unused]] plugin::plugin* plugin) + { + } + + virtual void on_dvar_init([[maybe_unused]] plugin::plugin* plugin) + { + } + + virtual void on_after_dvar_init([[maybe_unused]] plugin::plugin* plugin) + { + } + + virtual void on_game_init([[maybe_unused]] plugin::plugin* plugin) + { + } + + virtual void on_shutdown([[maybe_unused]] plugin::plugin* plugin) + { + } + + virtual bool is_supported() + { + return true; + } +}; diff --git a/t5/plugins/tws-t5-gsc-utils/src/loader/component_loader.cpp b/t5/plugins/tws-t5-gsc-utils/src/loader/component_loader.cpp new file mode 100644 index 0000000..0b4c7ce --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/loader/component_loader.cpp @@ -0,0 +1,72 @@ +#include +#include "component_loader.hpp" + +void component_loader::register_component(const std::string& name, std::unique_ptr&& component_) +{ + get_components().push_back(std::make_pair(name, std::move(component_))); +} + +#define ON_CALLBACK(__name__) \ + void component_loader::__name__() \ + { \ + static auto handled = false; \ + if (handled) \ + { \ + return; \ + } \ + \ + handled = true; \ + \ + for (const auto& component_ : get_components()) \ + { \ + try \ + { \ + component_.second->__name__(plugin::get()); \ + } \ + catch (const std::exception& e) \ + { \ + printf("error executing component \"%s\" callback \"%s\": %s\n", component_.first.data(), #__name__, e.what()); \ + } \ + } \ + } \ + +ON_CALLBACK(on_startup); +ON_CALLBACK(on_dvar_init); +ON_CALLBACK(on_after_dvar_init); +ON_CALLBACK(on_shutdown); + +void component_loader::clean() +{ + auto& components = get_components(); + for (auto i = components.begin(); i != components.end();) + { + if (!(*i).second->is_supported()) + { + (*i).second->on_shutdown(plugin::get()); + i = components.erase(i); + } + else + { + ++i; + } + } +} + +void component_loader::trigger_premature_shutdown() +{ + throw premature_shutdown_trigger(); +} + +std::vector>>& component_loader::get_components() +{ + using component_vector = std::vector>>; + using component_vector_container = std::unique_ptr>; + + static component_vector_container components(new component_vector, [](component_vector* component_vector) + { + on_shutdown(); + delete component_vector; + }); + + return *components; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/loader/component_loader.hpp b/t5/plugins/tws-t5-gsc-utils/src/loader/component_loader.hpp new file mode 100644 index 0000000..0444283 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/loader/component_loader.hpp @@ -0,0 +1,59 @@ +#pragma once +#include "component_interface.hpp" + +class component_loader final +{ +public: + class premature_shutdown_trigger final : public std::exception + { + [[nodiscard]] const char* what() const noexcept override + { + return "Premature shutdown requested"; + } + }; + + template + class installer final + { + static_assert(std::is_base_of::value, "component has invalid base class"); + + public: + installer(const std::string& name) + { + register_component(name, std::make_unique()); + } + }; + + template + static T* get() + { + for (const auto& component_ : get_components()) + { + if (typeid(*component_.get()) == typeid(T)) + { + return reinterpret_cast(component_.get()); + } + } + + return nullptr; + } + + static void register_component(const std::string& name, std::unique_ptr&& component); + + static void on_startup(); + static void on_dvar_init(); + static void on_after_dvar_init(); + static void on_shutdown(); + static void clean(); + + static void trigger_premature_shutdown(); + +private: + static std::vector>>& get_components(); +}; + +#define REGISTER_COMPONENT(name) \ +namespace \ +{ \ + static component_loader::installer __component(#name); \ +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/plugin.cpp b/t5/plugins/tws-t5-gsc-utils/src/plugin.cpp new file mode 100644 index 0000000..d320578 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/plugin.cpp @@ -0,0 +1,75 @@ +#include +#include "loader/component_loader.hpp" + +#include "plugin.hpp" + +#include +#include + +namespace plugin +{ + namespace + { + void printf_stub(const char* fmt, ...) + { + char buffer[0x2000] = {}; + + va_list ap; + va_start(ap, fmt); + + vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, fmt, ap); + + va_end(ap); + + const auto trimmed = utils::string::trim(buffer); + get()->get_interface()->logging()->info(trimmed); + } + } + + std::uint32_t plugin::plugin_version() + { + return 1; + } + + const char* plugin::plugin_name() + { + return "tws-t5-gsc-utils"; + } + + bool plugin::is_game_supported([[maybe_unused]] plutonium::sdk::game game) + { + return game == plutonium::sdk::game::t5; + } + + void plugin::on_startup(plutonium::sdk::iinterface* interface_ptr, plutonium::sdk::game game) + { + this->interface_ = interface_ptr; + this->game_ = game; + utils::hook::jump(reinterpret_cast(&printf), printf_stub); + + component_loader::on_startup(); + interface_ptr->callbacks()->on_dvar_init(&component_loader::on_dvar_init); + interface_ptr->callbacks()->on_after_dvar_init(&component_loader::on_after_dvar_init); + } + + void plugin::on_shutdown() + { + component_loader::on_shutdown(); + } + + plutonium::sdk::iinterface* plugin::get_interface() + { + return this->interface_; + } + + plutonium::sdk::game plugin::get_game() + { + return this->game_; + } + + plugin* get() + { + static plugin instance; + return &instance; + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/plugin.hpp b/t5/plugins/tws-t5-gsc-utils/src/plugin.hpp new file mode 100644 index 0000000..3d18d09 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/plugin.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +namespace plugin +{ + class plugin : public plutonium::sdk::plugin + { + public: + ~plugin() = default; + + std::uint32_t plugin_version() override; + const char* plugin_name() override; + + bool is_game_supported([[maybe_unused]] plutonium::sdk::game game) override; + + void on_startup(plutonium::sdk::iinterface* interface_ptr, plutonium::sdk::game game) override; + void on_shutdown() override; + + plutonium::sdk::iinterface* get_interface(); + plutonium::sdk::game get_game(); + + private: + plutonium::sdk::iinterface* interface_{}; + plutonium::sdk::game game_{}; + + }; + + plugin* get(); +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/resource.hpp b/t5/plugins/tws-t5-gsc-utils/src/resource.hpp new file mode 100644 index 0000000..beabe45 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/resource.hpp @@ -0,0 +1,3 @@ +#pragma once + +#define LIBMYSQL_DLL 100 diff --git a/t5/plugins/tws-t5-gsc-utils/src/resource.rc b/t5/plugins/tws-t5-gsc-utils/src/resource.rc new file mode 100644 index 0000000..335da49 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/resource.rc @@ -0,0 +1,3 @@ +#include "resource.hpp" + +LIBMYSQL_DLL RCDATA "../../deps/mysql/lib/libmysql.dll" diff --git a/t5/plugins/tws-t5-gsc-utils/src/stdinc.cpp b/t5/plugins/tws-t5-gsc-utils/src/stdinc.cpp new file mode 100644 index 0000000..49e54fa --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/stdinc.cpp @@ -0,0 +1,24 @@ +#include + +extern "C" +{ + int s_read_arc4random(void*, size_t) + { + return -1; + } + + int s_read_getrandom(void*, size_t) + { + return -1; + } + + int s_read_urandom(void*, size_t) + { + return -1; + } + + int s_read_ltm_rng(void*, size_t) + { + return -1; + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/stdinc.hpp b/t5/plugins/tws-t5-gsc-utils/src/stdinc.hpp new file mode 100644 index 0000000..7d898a4 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/stdinc.hpp @@ -0,0 +1,60 @@ +#pragma once + +#pragma warning(disable: 6011) +#pragma warning(disable: 6054) +#pragma warning(disable: 6387) +#pragma warning(disable: 26451) +#pragma warning(disable: 26812) +#pragma warning(disable: 28182) + +#define DLL_EXPORT extern "C" __declspec(dllexport) +#define WIN32_LEAN_AND_MEAN +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef max +#undef max +#endif + +#ifdef min +#undef min +#endif + +#include +#include + +#include + +#include +#include + +#pragma comment(lib, "ntdll.lib") +#pragma comment(lib, "ws2_32.lib") +#pragma comment(lib, "urlmon.lib" ) +#pragma comment(lib, "iphlpapi.lib") +#pragma comment(lib, "Crypt32.lib") +#pragma comment(lib, "libmysql.lib") +#pragma comment(lib, "delayimp.lib") + +#include "resource.hpp" + +using namespace std::literals; \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/binary_resource.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/binary_resource.cpp new file mode 100644 index 0000000..4eb5e6c --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/binary_resource.cpp @@ -0,0 +1,76 @@ +#include +#include "binary_resource.hpp" + +#include +#include "nt.hpp" +#include "io.hpp" + +namespace utils +{ + namespace + { + std::string get_temp_folder() + { + char path[MAX_PATH] = {0}; + if (!GetTempPathA(sizeof(path), path)) + { + throw std::runtime_error("Unable to get temp path"); + } + + return path; + } + + std::string write_exitisting_temp_file(const std::string& file, const std::string& data, + const bool fatal_if_overwrite_fails) + { + const auto temp = get_temp_folder(); + auto file_path = temp + file; + + std::string current_data; + if (!io::read_file(file_path, ¤t_data)) + { + if (!io::write_file(file_path, data)) + { + throw std::runtime_error("Failed to write file: " + file_path); + } + + return file_path; + } + + if (current_data == data || io::write_file(file_path, data) || !fatal_if_overwrite_fails) + { + return file_path; + } + + throw std::runtime_error( + "Temporary file was already written, but differs. It can't be overwritten as it's still in use: " + + file_path); + } + } + + binary_resource::binary_resource(const int id, std::string file) + : filename_(std::move(file)) + { + this->resource_ = nt::load_resource(id); + + if (this->resource_.empty()) + { + throw std::runtime_error("Unable to load resource: " + std::to_string(id)); + } + } + + std::string binary_resource::get_extracted_file(const bool fatal_if_overwrite_fails) + { + if (this->path_.empty()) + { + this->path_ = write_exitisting_temp_file(this->filename_, this->resource_, fatal_if_overwrite_fails); + } + + return this->path_; + } + + const std::string& binary_resource::get_data() const + { + return this->resource_; + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/binary_resource.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/binary_resource.hpp new file mode 100644 index 0000000..da19af1 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/binary_resource.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace utils +{ + class binary_resource + { + public: + binary_resource(int id, std::string file); + + std::string get_extracted_file(bool fatal_if_overwrite_fails = false); + const std::string& get_data() const; + + private: + std::string resource_; + std::string filename_; + std::string path_; + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/compression.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/compression.cpp new file mode 100644 index 0000000..3a6b9e2 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/compression.cpp @@ -0,0 +1,170 @@ +#include +#include "memory.hpp" +#include "compression.hpp" + +#include +#include + +#include + +#include "io.hpp" + +namespace utils::compression +{ + namespace zlib + { + namespace + { + class zlib_stream + { + public: + zlib_stream() + { + memset(&stream_, 0, sizeof(stream_)); + valid_ = inflateInit(&stream_) == Z_OK; + } + + zlib_stream(zlib_stream&&) = delete; + zlib_stream(const zlib_stream&) = delete; + zlib_stream& operator=(zlib_stream&&) = delete; + zlib_stream& operator=(const zlib_stream&) = delete; + + ~zlib_stream() + { + if (valid_) + { + inflateEnd(&stream_); + } + } + + z_stream& get() + { + return stream_; // + } + + bool is_valid() const + { + return valid_; + } + + private: + bool valid_{false}; + z_stream stream_{}; + }; + } + + std::string decompress(const std::string& data) + { + std::string buffer{}; + zlib_stream stream_container{}; + if (!stream_container.is_valid()) + { + return {}; + } + + int ret{}; + size_t offset = 0; + static thread_local uint8_t dest[CHUNK] = {0}; + auto& stream = stream_container.get(); + + do + { + const auto input_size = std::min(sizeof(dest), data.size() - offset); + stream.avail_in = static_cast(input_size); + stream.next_in = reinterpret_cast(data.data()) + offset; + offset += stream.avail_in; + + do + { + stream.avail_out = sizeof(dest); + stream.next_out = dest; + + ret = inflate(&stream, Z_NO_FLUSH); + if (ret != Z_OK && ret != Z_STREAM_END) + { + return {}; + } + + buffer.insert(buffer.end(), dest, dest + sizeof(dest) - stream.avail_out); + } + while (stream.avail_out == 0); + } + while (ret != Z_STREAM_END); + + return buffer; + } + + std::string compress(const std::string& data) + { + std::string result{}; + auto length = compressBound(static_cast(data.size())); + result.resize(length); + + if (compress2(reinterpret_cast(result.data()), &length, + reinterpret_cast(data.data()), static_cast(data.size()), + Z_BEST_COMPRESSION) != Z_OK) + { + return {}; + } + + result.resize(length); + return result; + } + } + + namespace zip + { + namespace + { + bool add_file(zipFile& zip_file, const std::string& filename, const std::string& data) + { + const auto zip_64 = data.size() > 0xffffffff ? 1 : 0; + if (ZIP_OK != zipOpenNewFileInZip64(zip_file, filename.data(), nullptr, nullptr, 0, nullptr, 0, nullptr, + Z_DEFLATED, Z_BEST_COMPRESSION, zip_64)) + { + return false; + } + + const auto _ = gsl::finally([&zip_file]() + { + zipCloseFileInZip(zip_file); + }); + + return ZIP_OK == zipWriteInFileInZip(zip_file, data.data(), static_cast(data.size())); + } + } + + void archive::add(std::string filename, std::string data) + { + this->files_[std::move(filename)] = std::move(data); + } + + bool archive::write(const std::string& filename, const std::string& comment) + { + // Hack to create the directory :3 + io::write_file(filename, {}); + io::remove_file(filename); + + auto* zip_file = zipOpen64(filename.data(), 0); + if (!zip_file) + { + return false; + } + + const auto _ = gsl::finally([&zip_file, &comment]() + { + zipClose(zip_file, comment.empty() ? nullptr : comment.data()); + }); + + for (const auto& file : this->files_) + { + if (!add_file(zip_file, file.first, file.second)) + { + return false; + } + } + + return true; + } + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/compression.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/compression.hpp new file mode 100644 index 0000000..dfe36ad --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/compression.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#define CHUNK 16384u + +namespace utils::compression +{ + namespace zlib + { + std::string compress(const std::string& data); + std::string decompress(const std::string& data); + } + + namespace zip + { + class archive + { + public: + void add(std::string filename, std::string data); + bool write(const std::string& filename, const std::string& comment = {}); + + private: + std::unordered_map files_; + }; + } +}; diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/concurrency.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/concurrency.hpp new file mode 100644 index 0000000..05c5d3a --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/concurrency.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +namespace utils::concurrency +{ + template + class container + { + public: + template + R access(F&& accessor) const + { + std::lock_guard _{mutex_}; + return accessor(object_); + } + + template + R access(F&& accessor) + { + std::lock_guard _{mutex_}; + return accessor(object_); + } + + template + R access_with_lock(F&& accessor) const + { + std::unique_lock lock{mutex_}; + return accessor(object_, lock); + } + + template + R access_with_lock(F&& accessor) + { + std::unique_lock lock{mutex_}; + return accessor(object_, lock); + } + + T& get_raw() { return object_; } + const T& get_raw() const { return object_; } + + private: + mutable MutexType mutex_{}; + T object_{}; + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/cryptography.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/cryptography.cpp new file mode 100644 index 0000000..3d32ca2 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/cryptography.cpp @@ -0,0 +1,691 @@ +#include + +#include "cryptography.hpp" +#include "nt.hpp" +#include + +#undef max +using namespace std::string_literals; + +/// http://www.opensource.apple.com/source/CommonCrypto/CommonCrypto-55010/Source/libtomcrypt/doc/libTomCryptDoc.pdf + +#pragma warning(push) +#pragma warning(disable: 4996) + +namespace utils::cryptography +{ + namespace + { + struct __ + { + __() + { + ltc_mp = ltm_desc; + + register_cipher(&aes_desc); + register_cipher(&des3_desc); + + register_prng(&sprng_desc); + register_prng(&fortuna_desc); + register_prng(&yarrow_desc); + + register_hash(&sha1_desc); + register_hash(&sha256_desc); + register_hash(&sha512_desc); + } + } ___; + + [[maybe_unused]] const char* cs(const uint8_t* data) + { + return reinterpret_cast(data); + } + + [[maybe_unused]] char* cs(uint8_t* data) + { + return reinterpret_cast(data); + } + + [[maybe_unused]] const uint8_t* cs(const char* data) + { + return reinterpret_cast(data); + } + + [[maybe_unused]] uint8_t* cs(char* data) + { + return reinterpret_cast(data); + } + + [[maybe_unused]] unsigned long ul(const size_t value) + { + return static_cast(value); + } + + class prng + { + public: + prng(const ltc_prng_descriptor& descriptor, const bool autoseed = true) + : state_(std::make_unique()) + , descriptor_(descriptor) + { + this->id_ = register_prng(&descriptor); + if (this->id_ == -1) + { + throw std::runtime_error("PRNG "s + this->descriptor_.name + " could not be registered!"); + } + + if (autoseed) + { + this->auto_seed(); + } + else + { + this->descriptor_.start(this->state_.get()); + } + } + + ~prng() + { + this->descriptor_.done(this->state_.get()); + } + + prng_state* get_state() const + { + this->descriptor_.ready(this->state_.get()); + return this->state_.get(); + } + + int get_id() const + { + return this->id_; + } + + void add_entropy(const void* data, const size_t length) const + { + this->descriptor_.add_entropy(static_cast(data), ul(length), this->state_.get()); + } + + void read(void* data, const size_t length) const + { + this->descriptor_.read(static_cast(data), ul(length), this->get_state()); + } + + private: + int id_; + std::unique_ptr state_; + const ltc_prng_descriptor& descriptor_; + + void auto_seed() const + { + rng_make_prng(128, this->id_, this->state_.get(), nullptr); + + int i[4]; // uninitialized data + auto* i_ptr = &i; + this->add_entropy(reinterpret_cast(&i), sizeof(i)); + this->add_entropy(reinterpret_cast(&i_ptr), sizeof(i_ptr)); + + auto t = time(nullptr); + this->add_entropy(reinterpret_cast(&t), sizeof(t)); + } + }; + + const prng prng_(fortuna_desc); + } + + ecc::key::key() + { + ZeroMemory(&this->key_storage_, sizeof(this->key_storage_)); + } + + ecc::key::~key() + { + this->free(); + } + + ecc::key::key(key&& obj) noexcept + : key() + { + this->operator=(std::move(obj)); + } + + ecc::key::key(const key& obj) + : key() + { + this->operator=(obj); + } + + ecc::key& ecc::key::operator=(key&& obj) noexcept + { + if (this != &obj) + { + std::memmove(&this->key_storage_, &obj.key_storage_, sizeof(this->key_storage_)); + ZeroMemory(&obj.key_storage_, sizeof(obj.key_storage_)); + } + + return *this; + } + + ecc::key& ecc::key::operator=(const key& obj) + { + if (this != &obj && obj.is_valid()) + { + this->deserialize(obj.serialize(obj.key_storage_.type)); + } + + return *this; + } + + bool ecc::key::is_valid() const + { + return (!memory::is_set(&this->key_storage_, 0, sizeof(this->key_storage_))); + } + + ecc_key& ecc::key::get() + { + return this->key_storage_; + } + + const ecc_key& ecc::key::get() const + { + return this->key_storage_; + } + + std::string ecc::key::get_public_key() const + { + uint8_t buffer[512] = {0}; + unsigned long length = sizeof(buffer); + + if (ecc_ansi_x963_export(&this->key_storage_, buffer, &length) == CRYPT_OK) + { + return std::string(cs(buffer), length); + } + + return {}; + } + + void ecc::key::set(const std::string& pub_key_buffer) + { + this->free(); + + if (ecc_ansi_x963_import(cs(pub_key_buffer.data()), + ul(pub_key_buffer.size()), + &this->key_storage_) != CRYPT_OK) + { + ZeroMemory(&this->key_storage_, sizeof(this->key_storage_)); + } + } + + void ecc::key::deserialize(const std::string& key) + { + this->free(); + + if (ecc_import(cs(key.data()), ul(key.size()), + &this->key_storage_) != CRYPT_OK + ) + { + ZeroMemory(&this->key_storage_, sizeof(this->key_storage_)); + } + } + + std::string ecc::key::serialize(const int type) const + { + uint8_t buffer[4096] = {0}; + unsigned long length = sizeof(buffer); + + if (ecc_export(buffer, &length, type, &this->key_storage_) == CRYPT_OK) + { + return std::string(cs(buffer), length); + } + + return ""; + } + + void ecc::key::free() + { + if (this->is_valid()) + { + ecc_free(&this->key_storage_); + } + + ZeroMemory(&this->key_storage_, sizeof(this->key_storage_)); + } + + bool ecc::key::operator==(key& key) const + { + return (this->is_valid() && key.is_valid() && this->serialize(PK_PUBLIC) == key.serialize(PK_PUBLIC)); + } + + uint64_t ecc::key::get_hash() const + { + const auto hash = sha1::compute(this->get_public_key()); + if (hash.size() >= 8) + { + return *reinterpret_cast(hash.data()); + } + + return 0; + } + + ecc::key ecc::generate_key(const int bits) + { + key key; + ecc_make_key(prng_.get_state(), prng_.get_id(), bits / 8, &key.get()); + + return key; + } + + ecc::key ecc::generate_key(const int bits, const std::string& entropy) + { + key key{}; + const prng yarrow(yarrow_desc, false); + yarrow.add_entropy(entropy.data(), entropy.size()); + + ecc_make_key(yarrow.get_state(), yarrow.get_id(), bits / 8, &key.get()); + + return key; + } + + std::string ecc::sign_message(const key& key, const std::string& message) + { + if (!key.is_valid()) return ""; + + uint8_t buffer[512]; + unsigned long length = sizeof(buffer); + + ecc_sign_hash(cs(message.data()), ul(message.size()), buffer, &length, prng_.get_state(), prng_.get_id(), + &key.get()); + + return std::string(cs(buffer), length); + } + + bool ecc::verify_message(const key& key, const std::string& message, const std::string& signature) + { + if (!key.is_valid()) return false; + + auto result = 0; + return (ecc_verify_hash(cs(signature.data()), + ul(signature.size()), + cs(message.data()), + ul(message.size()), &result, + &key.get()) == CRYPT_OK && result != 0); + } + + bool ecc::encrypt(const key& key, std::string& data) + { + std::string out_data{}; + out_data.resize(std::max(ul(data.size() * 3), ul(0x100))); + + auto out_len = ul(out_data.size()); + auto crypt = [&]() + { + return ecc_encrypt_key(cs(data.data()), ul(data.size()), cs(out_data.data()), &out_len, + prng_.get_state(), prng_.get_id(), find_hash("sha512"), &key.get()); + }; + + auto res = crypt(); + + if (res == CRYPT_BUFFER_OVERFLOW) + { + out_data.resize(out_len); + res = crypt(); + } + + if (res != CRYPT_OK) + { + return false; + } + + out_data.resize(out_len); + data = std::move(out_data); + return true; + } + + bool ecc::decrypt(const key& key, std::string& data) + { + std::string out_data{}; + out_data.resize(std::max(ul(data.size() * 3), ul(0x100))); + + auto out_len = ul(out_data.size()); + auto crypt = [&]() + { + return ecc_decrypt_key(cs(data.data()), ul(data.size()), cs(out_data.data()), &out_len, &key.get()); + }; + + auto res = crypt(); + + if (res == CRYPT_BUFFER_OVERFLOW) + { + out_data.resize(out_len); + res = crypt(); + } + + if (res != CRYPT_OK) + { + return false; + } + + out_data.resize(out_len); + data = std::move(out_data); + return true; + } + + std::string rsa::encrypt(const std::string& data, const std::string& hash, const std::string& key) + { + rsa_key new_key; + rsa_import(cs(key.data()), ul(key.size()), &new_key); + const auto _ = gsl::finally([&]() + { + rsa_free(&new_key); + }); + + + std::string out_data{}; + out_data.resize(std::max(ul(data.size() * 3), ul(0x100))); + + auto out_len = ul(out_data.size()); + auto crypt = [&]() + { + return rsa_encrypt_key_ex(cs(data.data()), ul(data.size()), cs(out_data.data()), &out_len, cs(hash.data()), + ul(hash.size()), prng_.get_state(), prng_.get_id(), find_hash("sha512"), LTC_PKCS_1_V1_5, &new_key); + }; + + auto res = crypt(); + + if (res == CRYPT_BUFFER_OVERFLOW) + { + out_data.resize(out_len); + res = crypt(); + } + + if (res == CRYPT_OK) + { + out_data.resize(out_len); + return out_data; + } + + return {}; + } + + std::string des3::encrypt(const std::string& data, const std::string& iv, const std::string& key) + { + std::string enc_data; + enc_data.resize(data.size()); + + symmetric_CBC cbc; + const auto des3 = find_cipher("3des"); + + cbc_start(des3, cs(iv.data()), cs(key.data()), static_cast(key.size()), 0, &cbc); + cbc_encrypt(cs(data.data()), cs(enc_data.data()), ul(data.size()), &cbc); + cbc_done(&cbc); + + return enc_data; + } + + std::string des3::decrypt(const std::string& data, const std::string& iv, const std::string& key) + { + std::string dec_data; + dec_data.resize(data.size()); + + symmetric_CBC cbc; + const auto des3 = find_cipher("3des"); + + cbc_start(des3, cs(iv.data()), cs(key.data()), static_cast(key.size()), 0, &cbc); + cbc_decrypt(cs(data.data()), cs(dec_data.data()), ul(data.size()), &cbc); + cbc_done(&cbc); + + return dec_data; + } + + std::string tiger::compute(const std::string& data, const bool hex) + { + return compute(cs(data.data()), data.size(), hex); + } + + std::string tiger::compute(const uint8_t* data, const size_t length, const bool hex) + { + uint8_t buffer[24] = {0}; + + hash_state state; + tiger_init(&state); + tiger_process(&state, data, ul(length)); + tiger_done(&state, buffer); + + std::string hash(cs(buffer), sizeof(buffer)); + if (!hex) return hash; + + return string::dump_hex(hash, ""); + } + + std::string aes::encrypt(const std::string& data, const std::string& iv, const std::string& key) + { + std::string aligned_data = data; + const auto mod = aligned_data.size() % 16; + if (mod != 0) + { + aligned_data.resize(aligned_data.size() + (16 - mod)); + } + + std::string enc_data; + enc_data.resize(aligned_data.size()); + + symmetric_CBC cbc; + const auto aes = find_cipher("aes"); + + cbc_start(aes, cs(iv.data()), cs(key.data()), + static_cast(key.size()), 0, &cbc); + cbc_encrypt(cs(aligned_data.data()), + cs(enc_data.data()), + ul(aligned_data.size()), &cbc); + cbc_done(&cbc); + + return enc_data; + } + + std::string aes::decrypt(const std::string& data, const std::string& iv, const std::string& key) + { + std::string dec_data; + dec_data.resize(data.size()); + + symmetric_CBC cbc; + const auto aes = find_cipher("aes"); + + cbc_start(aes, cs(iv.data()), cs(key.data()), + static_cast(key.size()), 0, &cbc); + cbc_decrypt(cs(data.data()), + cs(dec_data.data()), + ul(data.size()), &cbc); + cbc_done(&cbc); + + return dec_data; + } + + std::string hmac_sha1::compute(const std::string& data, const std::string& key) + { + std::string buffer; + buffer.resize(20); + + hmac_state state; + hmac_init(&state, find_hash("sha1"), cs(key.data()), ul(key.size())); + hmac_process(&state, cs(data.data()), static_cast(data.size())); + + auto out_len = ul(buffer.size()); + hmac_done(&state, cs(buffer.data()), &out_len); + + buffer.resize(out_len); + return buffer; + } + + std::string sha1::compute(const std::string& data, const bool hex) + { + return compute(cs(data.data()), data.size(), hex); + } + + std::string sha1::compute(const uint8_t* data, const size_t length, const bool hex) + { + uint8_t buffer[20] = {0}; + + hash_state state; + sha1_init(&state); + sha1_process(&state, data, ul(length)); + sha1_done(&state, buffer); + + std::string hash(cs(buffer), sizeof(buffer)); + if (!hex) return hash; + + return string::dump_hex(hash, ""); + } + + std::string sha256::compute(const std::string& data, const bool hex) + { + return compute(cs(data.data()), data.size(), hex); + } + + std::string sha256::compute(const uint8_t* data, const size_t length, const bool hex) + { + uint8_t buffer[32] = {0}; + + hash_state state; + sha256_init(&state); + sha256_process(&state, data, ul(length)); + sha256_done(&state, buffer); + + std::string hash(cs(buffer), sizeof(buffer)); + if (!hex) return hash; + + return string::dump_hex(hash, ""); + } + + std::string sha512::compute(const std::string& data, const bool hex) + { + return compute(cs(data.data()), data.size(), hex); + } + + std::string sha512::compute(const uint8_t* data, const size_t length, const bool hex) + { + uint8_t buffer[64] = {0}; + + hash_state state; + sha512_init(&state); + sha512_process(&state, data, ul(length)); + sha512_done(&state, buffer); + + std::string hash(cs(buffer), sizeof(buffer)); + if (!hex) return hash; + + return string::dump_hex(hash, ""); + } + + namespace md5 + { + std::string compute(const std::string& data, bool hex) + { + return compute(cs(data.data()), data.size(), hex); + } + + std::string compute(const uint8_t* data, size_t length, bool hex) + { + uint8_t buffer[16] = {0}; + + hash_state state; + md5_init(&state); + md5_process(&state, data, ul(length)); + md5_done(&state, buffer); + + std::string hash(cs(buffer), sizeof(buffer)); + if (!hex) return hash; + + return string::dump_hex(hash, ""); + } + } + + std::string base64::encode(const uint8_t* data, const size_t len) + { + std::string result; + result.resize((len + 2) * 2); + + auto out_len = ul(result.size()); + if (base64_encode(data, ul(len), result.data(), &out_len) != CRYPT_OK) + { + return {}; + } + + result.resize(out_len); + return result; + } + + std::string base64::encode(const std::string& data) + { + return base64::encode(cs(data.data()), static_cast(data.size())); + } + + std::string base64::decode(const std::string& data) + { + std::string result; + result.resize((data.size() + 2) * 2); + + auto out_len = ul(result.size()); + if (base64_decode(data.data(), ul(data.size()), cs(result.data()), &out_len) != CRYPT_OK) + { + return {}; + } + + result.resize(out_len); + return result; + } + + unsigned int jenkins_one_at_a_time::compute(const std::string& data) + { + return compute(data.data(), data.size()); + } + + unsigned int jenkins_one_at_a_time::compute(const char* key, const size_t len) + { + unsigned int hash, i; + for (hash = i = 0; i < len; ++i) + { + hash += key[i]; + hash += (hash << 10); + hash ^= (hash >> 6); + } + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return hash; + } + + uint32_t random::get_integer() + { + uint32_t result; + random::get_data(&result, sizeof(result)); + return result; + } + + uint32_t random::get_integer(const std::uint32_t min, const std::uint32_t max) + { + const auto range = max - min + 1; + const auto value = random::get_integer(); + return value % range + min; + } + + std::string random::get_challenge() + { + std::string result; + result.resize(sizeof(uint32_t)); + random::get_data(result.data(), result.size()); + return string::dump_hex(result, ""); + } + + void random::get_data(void* data, const size_t size) + { + prng_.read(data, size); + } + + std::string random::get_data(const size_t size) + { + std::string data; + data.resize(size); + random::get_data(data.data(), size); + return data; + } +} + +#pragma warning(pop) diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/cryptography.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/cryptography.hpp new file mode 100644 index 0000000..01409a7 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/cryptography.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include +#include +#include "string.hpp" + +namespace utils::cryptography +{ + namespace ecc + { + class key final + { + public: + key(); + ~key(); + + key(key&& obj) noexcept; + key(const key& obj); + key& operator=(key&& obj) noexcept; + key& operator=(const key& obj); + + bool is_valid() const; + + ecc_key& get(); + const ecc_key& get() const; + + std::string get_public_key() const; + + void set(const std::string& pub_key_buffer); + + void deserialize(const std::string& key); + + std::string serialize(int type = PK_PRIVATE) const; + + void free(); + + bool operator==(key& key) const; + + uint64_t get_hash() const; + + private: + ecc_key key_storage_{}; + }; + + key generate_key(int bits); + key generate_key(int bits, const std::string& entropy); + std::string sign_message(const key& key, const std::string& message); + bool verify_message(const key& key, const std::string& message, const std::string& signature); + + bool encrypt(const key& key, std::string& data); + bool decrypt(const key& key, std::string& data); + } + + namespace rsa + { + std::string encrypt(const std::string& data, const std::string& hash, const std::string& key); + std::string decrypt(const std::string& data, const std::string& hash, const std::string& key); + } + + namespace des3 + { + std::string encrypt(const std::string& data, const std::string& iv, const std::string& key); + std::string decrypt(const std::string& data, const std::string& iv, const std::string& key); + } + + namespace tiger + { + std::string compute(const std::string& data, bool hex = false); + std::string compute(const uint8_t* data, size_t length, bool hex = false); + } + + namespace aes + { + std::string encrypt(const std::string& data, const std::string& iv, const std::string& key); + std::string decrypt(const std::string& data, const std::string& iv, const std::string& key); + } + + namespace hmac_sha1 + { + std::string compute(const std::string& data, const std::string& key); + } + + namespace sha1 + { + std::string compute(const std::string& data, bool hex = false); + std::string compute(const uint8_t* data, size_t length, bool hex = false); + } + + namespace sha256 + { + std::string compute(const std::string& data, bool hex = false); + std::string compute(const uint8_t* data, size_t length, bool hex = false); + } + + namespace sha512 + { + std::string compute(const std::string& data, bool hex = false); + std::string compute(const uint8_t* data, size_t length, bool hex = false); + } + + namespace md5 + { + std::string compute(const std::string& data, bool hex = false); + std::string compute(const uint8_t* data, size_t length, bool hex = false); + } + + namespace argon2 + { + template + std::string compute(const std::string& data, bool hex = false) + { + std::uint8_t buffer[HashLen]{}; + std::uint8_t salt[SaltLen]{}; + + argon2i_hash_raw(TCost, MCost, Threads, data.data(), data.size(), salt, SaltLen, buffer, HashLen); + + const auto str = std::string{reinterpret_cast(buffer), HashLen}; + if (hex) + { + return string::dump_hex(str, "", false); + } + else + { + return str; + } + } + } + + namespace base64 + { + std::string encode(const uint8_t* data, size_t len); + std::string encode(const std::string& data); + std::string decode(const std::string& data); + } + + namespace jenkins_one_at_a_time + { + unsigned int compute(const std::string& data); + unsigned int compute(const char* key, size_t len); + }; + + namespace random + { + uint32_t get_integer(); + uint32_t get_integer(const std::uint32_t min, const std::uint32_t max); + std::string get_challenge(); + void get_data(void* data, size_t size); + std::string get_data(const size_t size); + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/flags.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/flags.cpp new file mode 100644 index 0000000..9d467f4 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/flags.cpp @@ -0,0 +1,54 @@ +#include + +#include "flags.hpp" +#include "string.hpp" + +#include + +namespace utils::flags +{ + void parse_flags(std::vector& flags) + { + int num_args; + auto* const argv = CommandLineToArgvW(GetCommandLineW(), &num_args); + + flags.clear(); + + if (argv) + { + for (auto i = 0; i < num_args; ++i) + { + std::wstring wide_flag(argv[i]); + if (wide_flag[0] == L'-') + { + wide_flag.erase(wide_flag.begin()); + flags.emplace_back(string::convert(wide_flag)); + } + } + + LocalFree(argv); + } + } + + bool has_flag(const std::string& flag) + { + static auto parsed = false; + static std::vector enabled_flags; + + if (!parsed) + { + parse_flags(enabled_flags); + parsed = true; + } + + for (const auto& entry : enabled_flags) + { + if (string::to_lower(entry) == string::to_lower(flag)) + { + return true; + } + } + + return false; + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/flags.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/flags.hpp new file mode 100644 index 0000000..7f09cae --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/flags.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace utils::flags +{ + bool has_flag(const std::string& flag); +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/hook.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/hook.cpp new file mode 100644 index 0000000..22c57c0 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/hook.cpp @@ -0,0 +1,208 @@ +#include +#include "hook.hpp" +#include "string.hpp" +// iw6x-client + +namespace utils::hook +{ + namespace + { + [[maybe_unused]] class _ + { + public: + _() + { + if (MH_Initialize() != MH_OK) + { + throw std::runtime_error("Failed to initialize MinHook"); + } + } + + ~_() + { + MH_Uninitialize(); + } + } __; + } + + asmjit::Error assembler::call(void* target) + { + return Assembler::call(size_t(target)); + } + + asmjit::Error assembler::jmp(void* target) + { + return Assembler::jmp(size_t(target)); + } + + detour::detour(const size_t place, void* target) : detour(reinterpret_cast(place), target) + { + } + + detour::detour(void* place, void* target) + { + this->create(place, target); + } + + detour::~detour() + { + this->clear(); + } + + void detour::enable() const + { + MH_EnableHook(this->place_); + } + + void detour::disable() const + { + MH_DisableHook(this->place_); + } + + void detour::create(void* place, void* target) + { + this->clear(); + this->place_ = place; + + if (MH_CreateHook(this->place_, target, &this->original_) != MH_OK) + { + throw std::runtime_error(string::va("Unable to create hook at location: %p", this->place_)); + } + + this->enable(); + } + + void detour::create(const size_t place, void* target) + { + this->create(reinterpret_cast(place), target); + } + + void detour::clear() + { + if (this->place_) + { + MH_RemoveHook(this->place_); + } + + this->place_ = nullptr; + this->original_ = nullptr; + } + + void* detour::get_original() const + { + return this->original_; + } + + void nop(void* place, const size_t length) + { + DWORD old_protect{}; + VirtualProtect(place, length, PAGE_EXECUTE_READWRITE, &old_protect); + + std::memset(place, 0x90, length); + + VirtualProtect(place, length, old_protect, &old_protect); + FlushInstructionCache(GetCurrentProcess(), place, length); + } + + void nop(const size_t place, const size_t length) + { + nop(reinterpret_cast(place), length); + } + + void copy(void* place, const void* data, const size_t length) + { + DWORD old_protect{}; + VirtualProtect(place, length, PAGE_EXECUTE_READWRITE, &old_protect); + + std::memmove(place, data, length); + + VirtualProtect(place, length, old_protect, &old_protect); + FlushInstructionCache(GetCurrentProcess(), place, length); + } + + void copy(const size_t place, const void* data, const size_t length) + { + copy(reinterpret_cast(place), data, length); + } + + bool is_relatively_far(const void* pointer, const void* data, int offset) + { + const int64_t diff = size_t(data) - (size_t(pointer) + offset); + const auto small_diff = int32_t(diff); + return diff != int64_t(small_diff); + } + + void call(void* pointer, void* data) + { + if (is_relatively_far(pointer, data)) + { + throw std::runtime_error("Too far away to create 32bit relative branch"); + } + + auto* patch_pointer = PBYTE(pointer); + set(patch_pointer, 0xE8); + set(patch_pointer + 1, int32_t(size_t(data) - (size_t(pointer) + 5))); + } + + void call(const size_t pointer, void* data) + { + return call(reinterpret_cast(pointer), data); + } + + void call(const size_t pointer, const size_t data) + { + return call(pointer, reinterpret_cast(data)); + } + + void set(std::uintptr_t address, std::vector&& bytes) + { + DWORD oldProtect = 0; + + auto* place = reinterpret_cast(address); + VirtualProtect(place, bytes.size(), PAGE_EXECUTE_READWRITE, &oldProtect); + memcpy(place, bytes.data(), bytes.size()); + VirtualProtect(place, bytes.size(), oldProtect, &oldProtect); + FlushInstructionCache(GetCurrentProcess(), place, bytes.size()); + } + + void set(std::uintptr_t address, void* buffer, size_t size) + { + DWORD oldProtect = 0; + + auto* place = reinterpret_cast(address); + VirtualProtect(place, size, PAGE_EXECUTE_READWRITE, &oldProtect); + memcpy(place, buffer, size); + VirtualProtect(place, size, oldProtect, &oldProtect); + FlushInstructionCache(GetCurrentProcess(), place, size); + } + + void jump(std::uintptr_t address, void* destination) + { + if (!address) return; + + std::uint8_t* bytes = new std::uint8_t[5]; + *bytes = 0xE9; + *reinterpret_cast(bytes + 1) = CalculateRelativeJMPAddress(address, destination); + + set(address, bytes, 5); + + delete[] bytes; + } + + void* assemble(const std::function& asm_function) + { + static asmjit::JitRuntime runtime; + + asmjit::CodeHolder code; + code.init(runtime.environment()); + + assembler a(&code); + + asm_function(a); + + void* result = nullptr; + runtime.add(&result, &code); + + return result; + } +} \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/hook.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/hook.hpp new file mode 100644 index 0000000..9e34126 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/hook.hpp @@ -0,0 +1,146 @@ +#pragma once + +#define CalculateRelativeJMPAddress(X, Y) (((std::uintptr_t)Y - (std::uintptr_t)X) - 5) + +#include +#include + +using namespace asmjit::x86; + +namespace utils::hook +{ + class assembler : public Assembler + { + public: + using Assembler::Assembler; + using Assembler::call; + using Assembler::jmp; + + asmjit::Error call(void* target); + asmjit::Error jmp(void* target); + }; + + class detour + { + public: + detour() = default; + detour(void* place, void* target); + detour(size_t place, void* target); + ~detour(); + + detour(detour&& other) noexcept + { + this->operator=(std::move(other)); + } + + detour& operator= (detour&& other) noexcept + { + if (this != &other) + { + this->~detour(); + + this->place_ = other.place_; + this->original_ = other.original_; + + other.place_ = nullptr; + other.original_ = nullptr; + } + + return *this; + } + + detour(const detour&) = delete; + detour& operator= (const detour&) = delete; + + void enable() const; + void disable() const; + + void create(void* place, void* target); + void create(size_t place, void* target); + void clear(); + + template + T* get() const + { + return static_cast(this->get_original()); + } + + template + T invoke(Args... args) + { + return static_cast(this->get_original())(args...); + } + + template + T invoke_pascal(Args... args) + { + return static_cast(this->get_original())(args...); + } + + [[nodiscard]] void* get_original() const; + + private: + void* place_{}; + void* original_{}; + }; + + void nop(void* place, size_t length); + void nop(size_t place, size_t length); + + void copy(void* place, const void* data, size_t length); + void copy(size_t place, const void* data, size_t length); + + bool is_relatively_far(const void* pointer, const void* data, int offset = 5); + + void call(void* pointer, void* data); + void call(size_t pointer, void* data); + void call(size_t pointer, size_t data); + + void jump(std::uintptr_t address, void* destination); + + void* assemble(const std::function& asm_function); + + template + T extract(void* address) + { + const auto data = static_cast(address); + const auto offset = *reinterpret_cast(data); + return reinterpret_cast(data + offset + 4); + } + + template + T extract(const size_t address) + { + return extract(reinterpret_cast(address)); + } + + template + static void set(void* place, T value) + { + DWORD old_protect; + VirtualProtect(place, sizeof(T), PAGE_EXECUTE_READWRITE, &old_protect); + + *static_cast(place) = value; + + VirtualProtect(place, sizeof(T), old_protect, &old_protect); + FlushInstructionCache(GetCurrentProcess(), place, sizeof(T)); + } + + template + static void set(const size_t place, T value) + { + return set(reinterpret_cast(place), value); + } + + template + static T invoke(size_t func, Args... args) + { + return reinterpret_cast(func)(args...); + } + + template + static T invoke(void* func, Args... args) + { + return static_cast(func)(args...); + } +} \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/http.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/http.cpp new file mode 100644 index 0000000..42f8269 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/http.cpp @@ -0,0 +1,112 @@ +#ifdef _WIN32 +#include +#endif +#include "http.hpp" + +#include +#include + +#ifdef _WIN32 +#pragma comment(lib, "ws2_32.lib") +#endif + +namespace utils::http +{ + namespace + { + size_t write_callback(void* contents, const size_t size, const size_t nmemb, void* userp) noexcept + { + const auto buffer = static_cast(userp); + const auto total_size = size * nmemb; + try + { + buffer->append(static_cast(contents), total_size); + } + catch (...) + { + return 0; // Never unwind a C++ exception through libcurl. + } + return total_size; + } + + double timing_ms(CURL* curl, const CURLINFO info) + { + double seconds = 0; + curl_easy_getinfo(curl, info, &seconds); + return seconds * 1000.0; + } + } + + std::optional get_data(const std::string& url, const std::string& fields, + const headers& headers, const std::string& method, const timeouts& limits) + { + result response; + if (limits.connect_timeout_ms <= 0 || limits.timeout_ms <= 0) + { + response.code = CURLE_BAD_FUNCTION_ARGUMENT; + return response; + } + const std::unique_ptr handle(curl_easy_init(), curl_easy_cleanup); + const auto curl = handle.get(); + if (!curl) + { + return {}; + } + + std::unique_ptr header_list(nullptr, curl_slist_free_all); + + for (const auto& header : headers) + { + auto data = header.first + ": " + header.second; + const auto appended = curl_slist_append(header_list.get(), data.data()); + if (!appended) + { + response.code = CURLE_OUT_OF_MEMORY; + return response; + } + header_list.release(); + header_list.reset(appended); + } + + std::string buffer{}; + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list.get()); + curl_easy_setopt(curl, CURLOPT_URL, url.data()); + // Real native deadlines: a script-side wait limit cannot stop this transfer. + auto code = curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, limits.connect_timeout_ms); + if (code == CURLE_OK) + code = curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, limits.timeout_ms); + if (code != CURLE_OK) + { + response.code = code; + return response; + } + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); + + if (!fields.empty()) + { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, fields.data()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, static_cast(fields.size())); + } + + if (!method.empty()) + { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.data()); + } + + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); + + response.code = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.status); + response.dns_ms = timing_ms(curl, CURLINFO_NAMELOOKUP_TIME); + response.connect_ms = timing_ms(curl, CURLINFO_CONNECT_TIME); + response.tls_ms = timing_ms(curl, CURLINFO_APPCONNECT_TIME); + response.first_byte_ms = timing_ms(curl, CURLINFO_STARTTRANSFER_TIME); + response.total_ms = timing_ms(curl, CURLINFO_TOTAL_TIME); + if (response.code == CURLE_OK) + response.buffer = std::move(buffer); + return response; + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/http.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/http.hpp new file mode 100644 index 0000000..c7d91f1 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/http.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +#include + +namespace utils::http +{ + struct result + { + CURLcode code = CURLE_FAILED_INIT; + std::string buffer; + long status = 0; + // Cumulative milliseconds from the start of curl_easy_perform. + double dns_ms = 0; + double connect_ms = 0; + double tls_ms = 0; + double first_byte_ms = 0; + double total_ms = 0; + }; + + struct timeouts + { + long connect_timeout_ms = 5000; + long timeout_ms = 15000; + }; + + using headers = std::unordered_map; + + std::optional get_data(const std::string& url, const std::string& fields = {}, + const headers& headers = {}, const std::string& method = {}, const timeouts& limits = {}); +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/info_string.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/info_string.cpp new file mode 100644 index 0000000..836ef07 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/info_string.cpp @@ -0,0 +1,67 @@ +#include + +#include "info_string.hpp" +#include "string.hpp" + +namespace utils +{ + info_string::info_string(const std::string& buffer) + { + this->parse(buffer); + } + + void info_string::set(const std::string& key, const std::string& value) + { + this->key_value_pairs_[key] = value; + } + + void info_string::remove(const std::string& key) + { + this->key_value_pairs_.erase(key); + } + + std::string info_string::get(const std::string& key) const + { + const auto value = this->key_value_pairs_.find(key); + if (value != this->key_value_pairs_.end()) + { + return value->second; + } + + return {}; + } + + void info_string::parse(std::string buffer) + { + if (buffer[0] == '\\') + { + buffer = buffer.substr(1); + } + + const auto key_values = string::split(buffer, '\\'); + for (size_t i = 0; !key_values.empty() && i < (key_values.size() - 1); i += 2) + { + const auto& key = key_values[i]; + const auto& value = key_values[i + 1]; + + if (!this->key_value_pairs_.contains(key)) + { + this->key_value_pairs_[key] = value; + } + } + } + + std::string info_string::build() const + { + std::string info_string; + for (const auto& [key, val] : this->key_value_pairs_) + { + info_string.append("\\"); + info_string.append(key); + info_string.append("\\"); + info_string.append(val); + } + + return info_string; + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/info_string.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/info_string.hpp new file mode 100644 index 0000000..c1f09dd --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/info_string.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace utils +{ + class info_string + { + public: + info_string() = default; + info_string(const std::string& buffer); + + void set(const std::string& key, const std::string& value); + void remove(const std::string& key); + + [[nodiscard]] std::string get(const std::string& key) const; + [[nodiscard]] std::string build() const; + + private: + std::unordered_map key_value_pairs_{}; + + void parse(std::string buffer); + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/io.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/io.cpp new file mode 100644 index 0000000..f16a9a6 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/io.cpp @@ -0,0 +1,135 @@ +#include +#include +#include "io.hpp" + +namespace utils::io +{ + bool remove_file(const std::string& file) + { + return DeleteFileA(file.data()) == TRUE; + } + + bool move_file(const std::string& src, const std::string& target) + { + return MoveFileA(src.data(), target.data()) == TRUE; + } + + bool file_exists(const std::string& file) + { + return std::ifstream(file).good(); + } + + bool write_file(const std::string& file, const std::string& data, const bool append) + { + const auto pos = file.find_last_of("/\\"); + if (pos != std::string::npos) + { + create_directory(file.substr(0, pos)); + } + + std::ofstream stream( + file, std::ios::binary | std::ofstream::out | (append ? std::ofstream::app : 0)); + + if (stream.is_open()) + { + stream.write(data.data(), data.size()); + stream.close(); + return true; + } + + return false; + } + + std::string read_file(const std::string& file) + { + std::string data; + read_file(file, &data); + return data; + } + + bool read_file(const std::string& file, std::string* data) + { + if (!data) return false; + data->clear(); + + if (file_exists(file)) + { + std::ifstream stream(file, std::ios::binary); + if (!stream.is_open()) return false; + + stream.seekg(0, std::ios::end); + const std::streamsize size = stream.tellg(); + stream.seekg(0, std::ios::beg); + + if (size > -1) + { + data->resize(static_cast(size)); + stream.read(const_cast(data->data()), size); + stream.close(); + return true; + } + } + + return false; + } + + size_t file_size(const std::string& file) + { + if (file_exists(file)) + { + std::ifstream stream(file, std::ios::binary); + + if (stream.good()) + { + stream.seekg(0, std::ios::end); + return static_cast(stream.tellg()); + } + } + + return 0; + } + + bool create_directory(const std::string& directory) + { + return std::filesystem::create_directories(directory); + } + + bool directory_exists(const std::string& directory) + { + return std::filesystem::is_directory(directory); + } + + bool directory_is_empty(const std::string& directory) + { + return std::filesystem::is_empty(directory); + } + + std::vector list_files(const std::string& directory) + { + std::vector files; + + for (auto& file : std::filesystem::directory_iterator(directory)) + { + files.push_back(file.path().generic_string()); + } + + return files; + } + + void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target) + { + std::filesystem::copy(src, target, std::filesystem::copy_options::overwrite_existing | std::filesystem::copy_options::recursive); + } + + size_t remove_directory(const std::filesystem::path& src, bool recursive) + { + if (recursive) + { + return static_cast(std::filesystem::remove_all(src)); + } + else + { + return static_cast(std::filesystem::remove(src)); + } + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/io.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/io.hpp new file mode 100644 index 0000000..cdee176 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/io.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +namespace utils::io +{ + bool remove_file(const std::string& file); + bool move_file(const std::string& src, const std::string& target); + bool file_exists(const std::string& file); + bool write_file(const std::string& file, const std::string& data, bool append = false); + bool read_file(const std::string& file, std::string* data); + std::string read_file(const std::string& file); + size_t file_size(const std::string& file); + bool create_directory(const std::string& directory); + bool directory_exists(const std::string& directory); + bool directory_is_empty(const std::string& directory); + std::vector list_files(const std::string& directory); + void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target); + size_t remove_directory(const std::filesystem::path& src, bool recursive = false); +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/memory.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/memory.cpp new file mode 100644 index 0000000..3809feb --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/memory.cpp @@ -0,0 +1,109 @@ +#include +#include "memory.hpp" + +namespace utils +{ + memory::allocator memory::mem_allocator_; + + memory::allocator::~allocator() + { + this->clear(); + } + + void memory::allocator::clear() + { + std::lock_guard _(this->mutex_); + + for (auto& data : this->pool_) + { + memory::free(data); + } + + this->pool_.clear(); + } + + void memory::allocator::free(void* data) + { + std::lock_guard _(this->mutex_); + + const auto j = std::find(this->pool_.begin(), this->pool_.end(), data); + if (j != this->pool_.end()) + { + memory::free(data); + this->pool_.erase(j); + } + } + + void memory::allocator::free(const void* data) + { + this->free(const_cast(data)); + } + + void* memory::allocator::allocate(const size_t length) + { + std::lock_guard _(this->mutex_); + + const auto data = memory::allocate(length); + this->pool_.push_back(data); + return data; + } + + bool memory::allocator::empty() const + { + return this->pool_.empty(); + } + + char* memory::allocator::duplicate_string(const std::string& string) + { + std::lock_guard _(this->mutex_); + + const auto data = memory::duplicate_string(string); + this->pool_.push_back(data); + return data; + } + + void* memory::allocate(const size_t length) + { + return calloc(length, 1); + } + + char* memory::duplicate_string(const std::string& string) + { + const auto new_string = allocate_array(string.size() + 1); + std::memcpy(new_string, string.data(), string.size()); + return new_string; + } + + void memory::free(void* data) + { + if (data) + { + ::free(data); + } + } + + void memory::free(const void* data) + { + free(const_cast(data)); + } + + bool memory::is_set(const void* mem, const char chr, const size_t length) + { + const auto mem_arr = static_cast(mem); + + for (size_t i = 0; i < length; ++i) + { + if (mem_arr[i] != chr) + { + return false; + } + } + + return true; + } + + memory::allocator* memory::get_allocator() + { + return &memory::mem_allocator_; + } +} \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/memory.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/memory.hpp new file mode 100644 index 0000000..6d6a8b5 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/memory.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +namespace utils +{ + class memory final + { + public: + class allocator final + { + public: + ~allocator(); + + void clear(); + + void free(void* data); + + void free(const void* data); + + void* allocate(size_t length); + + template + inline T* allocate() + { + return this->allocate_array(1); + } + + template + inline T* allocate_array(const size_t count = 1) + { + return static_cast(this->allocate(count * sizeof(T))); + } + + bool empty() const; + + char* duplicate_string(const std::string& string); + + private: + std::mutex mutex_; + std::vector pool_; + }; + + static void* allocate(size_t length); + + template + static inline T* allocate() + { + return allocate_array(1); + } + + template + static inline T* allocate_array(const size_t count = 1) + { + return static_cast(allocate(count * sizeof(T))); + } + + static char* duplicate_string(const std::string& string); + + static void free(void* data); + static void free(const void* data); + + static bool is_set(const void* mem, char chr, size_t length); + + static bool is_bad_read_ptr(const void* ptr); + static bool is_bad_code_ptr(const void* ptr); + static bool is_rdata_ptr(void* ptr); + + static allocator* get_allocator(); + + private: + static allocator mem_allocator_; + }; +} \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/nt.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/nt.cpp new file mode 100644 index 0000000..ebcd6b8 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/nt.cpp @@ -0,0 +1,250 @@ +#include +#include "nt.hpp" +#include "string.hpp" + +namespace utils::nt +{ + HMODULE library::current_handle_; + + library library::load(const std::string& name) + { + return library(LoadLibraryA(name.data())); + } + + library library::load(const std::filesystem::path& path) + { + return library::load(path.generic_string()); + } + + library library::get_by_address(void* address) + { + HMODULE handle = nullptr; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, static_cast(address), &handle); + return library(handle); + } + + void library::set_current_handle(HMODULE handle) + { + current_handle_ = handle; + } + + HMODULE library::get_current_handle() + { + return current_handle_; + } + + library::library() + { + this->module_ = GetModuleHandleA(nullptr); + } + + library::library(const std::string& name) + { + this->module_ = GetModuleHandleA(name.data()); + } + + library::library(const HMODULE handle) + { + this->module_ = handle; + } + + bool library::operator==(const library& obj) const + { + return this->module_ == obj.module_; + } + + library::operator bool() const + { + return this->is_valid(); + } + + library::operator HMODULE() const + { + return this->get_handle(); + } + + PIMAGE_NT_HEADERS library::get_nt_headers() const + { + if (!this->is_valid()) return nullptr; + return reinterpret_cast(this->get_ptr() + this->get_dos_header()->e_lfanew); + } + + PIMAGE_DOS_HEADER library::get_dos_header() const + { + return reinterpret_cast(this->get_ptr()); + } + + PIMAGE_OPTIONAL_HEADER library::get_optional_header() const + { + if (!this->is_valid()) return nullptr; + return &this->get_nt_headers()->OptionalHeader; + } + + std::vector library::get_section_headers() const + { + std::vector headers; + + auto nt_headers = this->get_nt_headers(); + auto section = IMAGE_FIRST_SECTION(nt_headers); + + for (uint16_t i = 0; i < nt_headers->FileHeader.NumberOfSections; ++i, ++section) + { + if (section) headers.push_back(section); + else OutputDebugStringA("There was an invalid section :O"); + } + + return headers; + } + + std::uint8_t* library::get_ptr() const + { + return reinterpret_cast(this->module_); + } + + void library::unprotect() const + { + if (!this->is_valid()) return; + + DWORD protection; + VirtualProtect(this->get_ptr(), this->get_optional_header()->SizeOfImage, PAGE_EXECUTE_READWRITE, + &protection); + } + + size_t library::get_relative_entry_point() const + { + if (!this->is_valid()) return 0; + return this->get_nt_headers()->OptionalHeader.AddressOfEntryPoint; + } + + void* library::get_entry_point() const + { + if (!this->is_valid()) return nullptr; + return this->get_ptr() + this->get_relative_entry_point(); + } + + bool library::is_valid() const + { + return this->module_ != nullptr && this->get_dos_header()->e_magic == IMAGE_DOS_SIGNATURE; + } + + std::string library::get_name() const + { + if (!this->is_valid()) return ""; + + auto path = this->get_path(); + const auto pos = path.find_last_of("/\\"); + if (pos == std::string::npos) return path; + + return path.substr(pos + 1); + } + + std::string library::get_path() const + { + if (!this->is_valid()) return ""; + + char name[MAX_PATH] = {0}; + GetModuleFileNameA(this->module_, name, sizeof name); + + return name; + } + + std::string library::get_folder() const + { + if (!this->is_valid()) return ""; + + const auto path = std::filesystem::path(this->get_path()); + return path.parent_path().generic_string(); + } + + void library::free() + { + if (this->is_valid()) + { + FreeLibrary(this->module_); + this->module_ = nullptr; + } + } + + HMODULE library::get_handle() const + { + return this->module_; + } + + void** library::get_iat_entry(const std::string& module_name, const std::string& proc_name) const + { + if (!this->is_valid()) return nullptr; + + const library other_module(module_name); + if (!other_module.is_valid()) return nullptr; + + auto* const target_function = other_module.get_proc(proc_name); + if (!target_function) return nullptr; + + auto* header = this->get_optional_header(); + if (!header) return nullptr; + + auto* import_descriptor = reinterpret_cast(this->get_ptr() + header->DataDirectory + [IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); + + while (import_descriptor->Name) + { + if (!_stricmp(reinterpret_cast(this->get_ptr() + import_descriptor->Name), module_name.data())) + { + auto* original_thunk_data = reinterpret_cast(import_descriptor-> + OriginalFirstThunk + this->get_ptr()); + auto* thunk_data = reinterpret_cast(import_descriptor->FirstThunk + this-> + get_ptr()); + + while (original_thunk_data->u1.AddressOfData) + { + const size_t ordinal_number = original_thunk_data->u1.AddressOfData & 0xFFFFFFF; + + if (ordinal_number > 0xFFFF) continue; + + if (GetProcAddress(other_module.module_, reinterpret_cast(ordinal_number)) == + target_function) + { + return reinterpret_cast(&thunk_data->u1.Function); + } + + ++original_thunk_data; + ++thunk_data; + } + + //break; + } + + ++import_descriptor; + } + + return nullptr; + } + + void raise_hard_exception() + { + int data = false; + const library ntdll("ntdll.dll"); + ntdll.invoke_pascal("RtlAdjustPrivilege", 19, true, false, &data); + ntdll.invoke_pascal("NtRaiseHardError", 0xC000007B, 0, nullptr, nullptr, 6, &data); + } + + std::string load_resource(const int id) + { + const auto self_handle = library::get_current_handle(); + const auto res = FindResource(self_handle, MAKEINTRESOURCE(id), RT_RCDATA); + if (res == nullptr) + { + return {}; + } + + const auto handle = LoadResource(self_handle, res); + if (handle == nullptr) + { + return {}; + } + + const auto str = LPSTR(LockResource(handle)); + const auto size = SizeofResource(self_handle, res); + return std::string{str, size}; + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/nt.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/nt.hpp new file mode 100644 index 0000000..4ba32c7 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/nt.hpp @@ -0,0 +1,112 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +// min and max is required by gdi, therefore NOMINMAX won't work +#ifdef max +#undef max +#endif + +#ifdef min +#undef min +#endif + +#include +#include +#include + +namespace utils::nt +{ + class library final + { + public: + static library load(const std::string& name); + static library load(const std::filesystem::path& path); + static library get_by_address(void* address); + + static void set_current_handle(HMODULE handle); + static HMODULE get_current_handle(); + + library(); + explicit library(const std::string& name); + explicit library(HMODULE handle); + + library(const library& a) : module_(a.module_) + { + } + + bool operator!=(const library& obj) const { return !(*this == obj); }; + bool operator==(const library& obj) const; + + operator bool() const; + operator HMODULE() const; + + void unprotect() const; + void* get_entry_point() const; + size_t get_relative_entry_point() const; + + bool is_valid() const; + std::string get_name() const; + std::string get_path() const; + std::string get_folder() const; + std::uint8_t* get_ptr() const; + void free(); + + HMODULE get_handle() const; + + template + T get_proc(const std::string& process) const + { + if (!this->is_valid()) T{}; + return reinterpret_cast(GetProcAddress(this->module_, process.data())); + } + + template + std::function get(const std::string& process) const + { + if (!this->is_valid()) return std::function(); + return static_cast(this->get_proc(process)); + } + + template + T invoke(const std::string& process, Args ... args) const + { + auto method = this->get(process); + if (method) return method(args...); + return T(); + } + + template + T invoke_pascal(const std::string& process, Args ... args) const + { + auto method = this->get(process); + if (method) return method(args...); + return T(); + } + + template + T invoke_this(const std::string& process, void* this_ptr, Args ... args) const + { + auto method = this->get(this_ptr, process); + if (method) return method(args...); + return T(); + } + + std::vector get_section_headers() const; + + PIMAGE_NT_HEADERS get_nt_headers() const; + PIMAGE_DOS_HEADER get_dos_header() const; + PIMAGE_OPTIONAL_HEADER get_optional_header() const; + + void** get_iat_entry(const std::string& module_name, const std::string& proc_name) const; + + private: + HMODULE module_; + static HMODULE current_handle_; + + }; + + __declspec(noreturn) void raise_hard_exception(); + std::string load_resource(int id); +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/string.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/string.cpp new file mode 100644 index 0000000..b75990d --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/string.cpp @@ -0,0 +1,157 @@ +#include +#include "string.hpp" +#include +#include +#include + +namespace utils::string +{ + const char* va(const char* fmt, ...) + { + static thread_local va_provider<8, 256> provider; + + va_list ap; + va_start(ap, fmt); + + const char* result = provider.get(fmt, ap); + + va_end(ap); + return result; + } + + std::vector split(const std::string& s, const char delim) + { + std::stringstream ss(s); + std::string item; + std::vector elems; + + while (std::getline(ss, item, delim)) + { + elems.push_back(item); // elems.push_back(std::move(item)); // if C++11 (based on comment from @mchiasson) + } + + return elems; + } + + std::string to_lower(std::string text) + { + std::transform(text.begin(), text.end(), text.begin(), [](const unsigned char input) + { + return static_cast(tolower(input)); + }); + + return text; + } + + std::string to_upper(std::string text) + { + std::transform(text.begin(), text.end(), text.begin(), [](const unsigned char input) + { + return static_cast(toupper(input)); + }); + + return text; + } + + bool starts_with(const std::string& text, const std::string& substring) + { + return text.find(substring) == 0; + } + + bool ends_with(const std::string& text, const std::string& substring) + { + if (substring.size() > text.size()) return false; + return std::equal(substring.rbegin(), substring.rend(), text.rbegin()); + } + + bool is_numeric(const std::string& text) + { + return std::to_string(atoi(text.data())) == text; + } + + std::string dump_hex(const std::string& data, const std::string& separator) + { + std::string result; + + for (unsigned int i = 0; i < data.size(); ++i) + { + if (i > 0) + { + result.append(separator); + } + + result.append(va("%02X", data[i] & 0xFF)); + } + + return result; + } + + std::string replace(std::string str, const std::string& from, const std::string& to) + { + if (from.empty()) + { + return str; + } + + size_t start_pos = 0; + while ((start_pos = str.find(from, start_pos)) != std::string::npos) + { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); + } + + return str; + } + + std::string convert(const std::wstring& wstr) + { + std::string result; + result.reserve(wstr.size()); + + for (const auto& chr : wstr) + { + result.push_back(static_cast(chr)); + } + + return result; + } + + std::wstring convert(const std::string& str) + { + std::wstring result; + result.reserve(str.size()); + + for (const auto& chr : str) + { + result.push_back(static_cast(chr)); + } + + return result; + } + + std::string get_timestamp() + { + tm ltime{}; + char timestamp[MAX_PATH] = { 0 }; + const auto time = _time64(nullptr); + + _localtime64_s(<ime, &time); + strftime(timestamp, sizeof(timestamp) - 1, "%Y-%m-%d-%H-%M-%S", <ime); + + return timestamp; + } + + std::string trim(const std::string& str, const std::string& whitespace) + { + const auto first = str.find_first_not_of(whitespace); + if (first == std::string::npos) + { + return {}; + } + + const auto last = str.find_last_not_of(whitespace); + const auto range = last - first + 1; + + return str.substr(first, range); + } +} \ No newline at end of file diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/string.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/string.hpp new file mode 100644 index 0000000..d0cb4dc --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/string.hpp @@ -0,0 +1,101 @@ +#pragma once +#include "memory.hpp" +#include + +#ifndef ARRAYSIZE +template +size_t ARRAYSIZE(Type(&)[n]) { return n; } +#endif + +namespace utils::string +{ + template + class va_provider final + { + public: + static_assert(Buffers != 0 && MinBufferSize != 0, "Buffers and MinBufferSize mustn't be 0"); + + va_provider() : current_buffer_(0) + { + } + + char* get(const char* format, const va_list ap) + { + ++this->current_buffer_ %= ARRAYSIZE(this->string_pool_); + auto entry = &this->string_pool_[this->current_buffer_]; + + if (!entry->size || !entry->buffer) + { + throw std::runtime_error("String pool not initialized"); + } + + while (true) + { + const int res = vsnprintf_s(entry->buffer, entry->size, _TRUNCATE, format, ap); + if (res > 0) break; // Success + if (res == 0) return nullptr; // Error + + entry->double_size(); + } + + return entry->buffer; + } + + private: + class entry final + { + public: + explicit entry(const size_t _size = MinBufferSize) : size(_size), buffer(nullptr) + { + if (this->size < MinBufferSize) this->size = MinBufferSize; + this->allocate(); + } + + ~entry() + { + if (this->buffer) memory::get_allocator()->free(this->buffer); + this->size = 0; + this->buffer = nullptr; + } + + void allocate() + { + if (this->buffer) memory::get_allocator()->free(this->buffer); + this->buffer = memory::get_allocator()->allocate_array(this->size + 1); + } + + void double_size() + { + this->size *= 2; + this->allocate(); + } + + size_t size; + char* buffer; + }; + + size_t current_buffer_; + entry string_pool_[Buffers]; + }; + + const char* va(const char* fmt, ...); + + std::vector split(const std::string& s, char delim); + + std::string to_lower(std::string text); + std::string to_upper(std::string text); + bool starts_with(const std::string& text, const std::string& substring); + bool ends_with(const std::string& text, const std::string& substring); + bool is_numeric(const std::string& text); + + std::string dump_hex(const std::string& data, const std::string& separator = " "); + + std::string replace(std::string str, const std::string& from, const std::string& to); + + std::string convert(const std::wstring& wstr); + std::wstring convert(const std::string& str); + + std::string get_timestamp(); + + std::string trim(const std::string& str, const std::string& whitespace = " \t\n\r\f\v"); +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/thread.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/thread.cpp new file mode 100644 index 0000000..1c20ff5 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/thread.cpp @@ -0,0 +1,89 @@ +#include +#include "thread.hpp" +#include "string.hpp" + +#include + +#include + +namespace utils::thread +{ + std::vector get_thread_ids() + { + auto* const h = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, GetCurrentProcessId()); + if (h == INVALID_HANDLE_VALUE) + { + return {}; + } + + const auto _ = gsl::finally([h]() + { + CloseHandle(h); + }); + + THREADENTRY32 entry{}; + entry.dwSize = sizeof(entry); + if (!Thread32First(h, &entry)) + { + return {}; + } + + std::vector ids{}; + + do + { + const auto check_size = entry.dwSize < FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + + sizeof(entry.th32OwnerProcessID); + entry.dwSize = sizeof(entry); + + if (check_size && entry.th32OwnerProcessID == GetCurrentProcessId()) + { + ids.emplace_back(entry.th32ThreadID); + } + } + while (Thread32Next(h, &entry)); + + return ids; + } + + void for_each_thread(const std::function& callback) + { + const auto ids = get_thread_ids(); + + for (const auto& id : ids) + { + auto* const thread = OpenThread(THREAD_ALL_ACCESS, FALSE, id); + if (thread != nullptr) + { + const auto _ = gsl::finally([thread]() + { + CloseHandle(thread); + }); + + callback(thread); + } + } + } + + void suspend_other_threads() + { + for_each_thread([](const HANDLE thread) + { + if (GetThreadId(thread) != GetCurrentThreadId()) + { + SuspendThread(thread); + } + }); + } + + void resume_other_threads() + { + for_each_thread([](const HANDLE thread) + { + if (GetThreadId(thread) != GetCurrentThreadId()) + { + ResumeThread(thread); + } + }); + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/thread.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/thread.hpp new file mode 100644 index 0000000..4d67c1a --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/thread.hpp @@ -0,0 +1,19 @@ +#pragma once +#include + +namespace utils::thread +{ + template + std::thread create_named_thread(const std::string& name, Args&&... args) + { + auto t = std::thread(std::forward(args)...); + set_name(t, name); + return t; + } + + std::vector get_thread_ids(); + void for_each_thread(const std::function& callback); + + void suspend_other_threads(); + void resume_other_threads(); +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/thread_pool.cpp b/t5/plugins/tws-t5-gsc-utils/src/utils/thread_pool.cpp new file mode 100644 index 0000000..092c89b --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/thread_pool.cpp @@ -0,0 +1,96 @@ +#include + +#include "thread_pool.hpp" + +namespace utils +{ + thread_pool::worker::worker() + { + } + + void thread_pool::worker::start(thread_pool& pool) + { + this->thread_ = std::thread([&] + { + this->loop(pool); + }); + } + + void thread_pool::worker::stop() + { + if (this->thread_.joinable()) + { + this->thread_.join(); + } + } + + void thread_pool::worker::loop(thread_pool& pool) + { + while (!pool.stopped_) + { + pool.run_job(); + } + } + + thread_pool::job_ptr thread_pool::pop_job() + { + thread_pool::job_ptr job = std::move(this->queue_.front()); + this->queue_.pop_front(); + return job; + } + + void thread_pool::run_job() + { + std::unique_lock lock(this->mutex_); + + this->event_.wait(lock, [&]() + { + return !this->queue_.empty() || this->stopped_; + }); + + if (this->stopped_ || this->queue_.empty()) + { + return; + } + + auto job = this->pop_job(); + lock.unlock(); + job->operator()(); + } + + thread_pool::thread_pool(const std::size_t num_workers) + { + this->initialize(num_workers); + } + + void thread_pool::initialize(const std::size_t num_workers) + { + for (auto i = 0u; i < num_workers; i++) + { + this->workers_.emplace_back(std::make_unique()); + } + } + + void thread_pool::start() + { + for (auto& worker : this->workers_) + { + worker->start(*this); + } + } + + void thread_pool::update() + { + + } + + void thread_pool::stop() + { + this->stopped_ = true; + this->event_.notify_all(); + for (auto& worker : this->workers_) + { + worker->stop(); + } + } +} diff --git a/t5/plugins/tws-t5-gsc-utils/src/utils/thread_pool.hpp b/t5/plugins/tws-t5-gsc-utils/src/utils/thread_pool.hpp new file mode 100644 index 0000000..9b9bcb2 --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/src/utils/thread_pool.hpp @@ -0,0 +1,60 @@ +#pragma once + +namespace utils +{ + class thread_pool + { + public: + using job = std::function; + using job_ptr = std::unique_ptr; + + friend class worker; + class worker + { + public: + worker(); + + friend class thread_pool; + + void start(thread_pool& pool); + void stop(); + void loop(thread_pool& pool); + + private: + std::thread thread_; + + }; + + thread_pool() = default; + thread_pool(const std::size_t num_workers); + void initialize(const std::size_t num_workers); + + void start(); + void update(); + void stop(); + + template + void push(F&& job) + { + if (this->stopped_) + { + return; + } + + std::lock_guard lock(this->mutex_); + this->queue_.emplace_back(std::make_unique(std::forward(job))); + this->event_.notify_one(); + } + + private: + thread_pool::job_ptr pop_job(); + void run_job(); + + std::mutex mutex_; + std::atomic_bool stopped_; + std::deque queue_; + std::condition_variable event_; + std::deque> workers_; + + }; +} diff --git a/t5/plugins/tws-t5-gsc-utils/tools/premake5.exe b/t5/plugins/tws-t5-gsc-utils/tools/premake5.exe new file mode 100644 index 0000000..1a637aa Binary files /dev/null and b/t5/plugins/tws-t5-gsc-utils/tools/premake5.exe differ diff --git a/t5/plugins/tws-t5-gsc-utils/upstream.json b/t5/plugins/tws-t5-gsc-utils/upstream.json new file mode 100644 index 0000000..170baba --- /dev/null +++ b/t5/plugins/tws-t5-gsc-utils/upstream.json @@ -0,0 +1,61 @@ +{ + "repository": "https://github.com/alicealys/t5-gsc-utils", + "commit": "8a87903b88ccd78071905785ac37794fcaeae79b", + "dependencies": [ + { + "path": "deps/GSL", + "url": "https://github.com/microsoft/GSL.git", + "commit": "d9fc52e20e32fcc804502480a781120b210afd41" + }, + { + "path": "deps/asmjit", + "url": "https://github.com/asmjit/asmjit.git", + "commit": "33a31f04e83845d15effbb9ca27a8f117205baf8" + }, + { + "path": "deps/curl", + "url": "https://github.com/curl/curl.git", + "commit": "ab885eccd6c82a949e21cfab9af778c0fd44d0ae" + }, + { + "path": "deps/date", + "url": "https://github.com/HowardHinnant/date", + "commit": "e32a0d809c3d3c4797826825dc2d0b287281e3b2" + }, + { + "path": "deps/json", + "url": "https://github.com/nlohmann/json.git", + "commit": "b6d00d18974358ab1d55dcc3379d0ee3052deb4e" + }, + { + "path": "deps/libtomcrypt", + "url": "https://github.com/libtom/libtomcrypt.git", + "commit": "a8ed78c2ca8a7dcb50ad789702ac0586125544f0" + }, + { + "path": "deps/libtommath", + "url": "https://github.com/libtom/libtommath.git", + "commit": "ae40a87a920099a7d9d00979570e0c8d917a1fd7" + }, + { + "path": "deps/minhook", + "url": "https://github.com/TsudaKageyu/minhook.git", + "commit": "4a455528f61b5a375b1f9d44e7d296d47f18bb18" + }, + { + "path": "deps/plutonium-sdk", + "url": "https://github.com/plutoniummod/plutonium-sdk.git", + "commit": "17e9a0a4d5e1133b50f879e3db07e97d1bf92e10" + }, + { + "path": "deps/sqlpp11", + "url": "https://github.com/rbock/sqlpp11.git", + "commit": "1806fe4dd89c9fba3a21341c76e6947fb2b5d6df" + }, + { + "path": "deps/zlib", + "url": "https://github.com/madler/zlib.git", + "commit": "21767c654d31d2dccdde4330529775c6c5fd5389" + } + ] +} diff --git a/t5/NOTES.md b/t5/scripts/NOTES.md similarity index 100% rename from t5/NOTES.md rename to t5/scripts/NOTES.md diff --git a/t5/README.md b/t5/scripts/README.md similarity index 93% rename from t5/README.md rename to t5/scripts/README.md index f5f25e2..d5954c1 100644 --- a/t5/README.md +++ b/t5/scripts/README.md @@ -43,7 +43,7 @@ the enable dvar does not bypass native linking dependencies. Records are read from the public Electric Cherry API with no credentials and work independently of telemetry and HUD. Spectator newcomers receive the same private announcement as playable joins. T5 matches its engine map to -the API's map list. [Shared configuration, caching and validation](../README.md#map-record-announcements) +the API's map list. [Shared configuration, caching and validation](../../README.md#map-record-announcements) apply to both games. ## Player commands @@ -303,10 +303,10 @@ and [spawn entry point](https://github.com/plutoniummod/t5-scripts/blob/main/ZM/ ## tws_t5_telemetry.gsc -Uses `t5-gsc-utils` HTTP/JSON components to POST directly to Electric Cherry's +Uses `tws-t5-gsc-utils` HTTP/JSON components to POST directly to Electric Cherry's `/api/telemetry`. Install alongside `tws_t5_common.gsc` in `storage/t5/scripts/sp/`. No HUD dependency. Reports run whether or not a beta is active. -Follow the shared [authentication setup](../README.md#telemetry-authentication-t5-and-t6) +Follow the shared [authentication setup](../../README.md#telemetry-authentication-t5-and-t6) to configure the service before upgrading. The reporter token is embedded in `level.telemetry_token`; it must match the service's `EC_TELEMETRY_TOKEN`. Short embedded tokens and non-HTTPS URLs disable reporting at init. @@ -375,11 +375,14 @@ worker, and already queued heartbeats drain before `game_end`. There is no discovery, beta token, session ID, sequence field, custom event, external helper, or separate beta acknowledgement. Delivery is best effort: no retries or persistence across map reloads, and no cross-game exactly-once -guarantee. A pending request delays later reports; the current plugin has no -request timeout option. A map reload can discard queued reports. +guarantee. The [TWS plugin build](../plugins/README.md) enforces a 5-second +connection and 15-second total transfer timeout. A timed-out report is logged +and skipped so later reports can proceed. A map reload can discard queued reports. -Transport failures log to the server console. The plugin does not expose -HTTP status codes, so its success flag does not establish API acceptance. +Transport failures and non-2xx HTTP responses log to the server console. The TWS +plugin exposes HTTP status and cumulative DNS/TCP/TLS/first-byte/total timings, +plus worker-queue and completion-notification delays. Transport success alone +does not establish API acceptance. Verify acceptance at Electric Cherry: success is HTTP 200 with `ok: true` and telemetry containing `receivedAt`; invalid payloads return 400 and non-JSON content types return 415. @@ -388,14 +391,15 @@ non-JSON content types return 415. Default logging includes initialization, match start/end, each request's event/round and local ID, queue wait and request duration in milliseconds, -transport failures/recovery, and the final queue drain. IDs reset on map +transport failures/recovery, HTTP rejection/status/timings, and the final queue drain. IDs reset on map load and only appear in logs; payloads are unchanged. Every log includes the server, map, and game time in milliseconds. An independent watchdog prints health every 30 seconds, even before match start and while HTTP is pending or the game-end queue is draining. It shows the sender stage, current snapshot capture, pending queue size (excluding -the in-flight request), total queued, completed transports, failures, and +the in-flight request), total queued, completed transports, transport failures, +HTTP rejections, and time since the last completion (`-1` means none). Warnings remain enabled at log level `0`: @@ -420,12 +424,14 @@ Missing request handles and reported transport failures are counted and skipped so the sender can continue with the next event. Empty responses produce a warning. Arbitrary response bodies are not parsed as JSON, because malformed/proxy responses must not break the sender. The watchdog -does not cancel or retry stuck requests: the current plugin offers neither -timeout nor cancellation through GSC, and starting another request could -reorder events or accumulate native workers. A stall remains blocked until -the plugin completes it; these logs identify that condition, not fix it. +does not launch overlapping requests or retry failed events. Native timeouts +release stalled transfers. A request still pending after 30 seconds warrants +checking the installed plugin version, worker-queue delay and completion delivery. +If diagnostics are absent, the reporter warns that native deadlines are unverified; +install the TWS plugin build and restart the server process. -Requires a plugin build containing +Native deadlines and completion diagnostics require the [TWS plugin build](../plugins/README.md). +The upstream baseline contains [`http::request`](https://github.com/alicealys/t5-gsc-utils/blob/main/src/component/http.cpp) and [`jsonSerialize`](https://github.com/alicealys/t5-gsc-utils/blob/main/src/component/json.cpp). Disabling the dvar does not remove that compile-time dependency; remove the @@ -458,7 +464,9 @@ and [score tracking](https://github.com/plutoniummod/t5-scripts/blob/main/ZM/Com server and confirm the API rejection is visible in the response even though transport completes. Also check an empty or HTML response. 7. Point a test server at an endpoint that accepts a connection but does - not reply. Verify recurring `HTTP stalled` warnings and increasing + not reply. With the TWS plugin, verify a native timeout near 15 seconds and + continued queue consumption. With an older plugin, expect recurring + `HTTP stalled` warnings and increasing pending counts while gameplay continues, including after game over. Release the delayed reply and verify the queue resumes in order and the watchdog stops after the final drain. Test a refused connection diff --git a/t5/tws_t5_afk.gsc b/t5/scripts/tws_t5_afk.gsc similarity index 100% rename from t5/tws_t5_afk.gsc rename to t5/scripts/tws_t5_afk.gsc diff --git a/t5/tws_t5_common.gsc b/t5/scripts/tws_t5_common.gsc similarity index 100% rename from t5/tws_t5_common.gsc rename to t5/scripts/tws_t5_common.gsc diff --git a/t5/tws_t5_hud.gsc b/t5/scripts/tws_t5_hud.gsc similarity index 100% rename from t5/tws_t5_hud.gsc rename to t5/scripts/tws_t5_hud.gsc diff --git a/t5/tws_t5_join.gsc b/t5/scripts/tws_t5_join.gsc similarity index 100% rename from t5/tws_t5_join.gsc rename to t5/scripts/tws_t5_join.gsc diff --git a/t5/tws_t5_leaderboard.gsc b/t5/scripts/tws_t5_leaderboard.gsc similarity index 100% rename from t5/tws_t5_leaderboard.gsc rename to t5/scripts/tws_t5_leaderboard.gsc diff --git a/t5/tws_t5_lock.gsc b/t5/scripts/tws_t5_lock.gsc similarity index 100% rename from t5/tws_t5_lock.gsc rename to t5/scripts/tws_t5_lock.gsc diff --git a/t5/tws_t5_telemetry.gsc b/t5/scripts/tws_t5_telemetry.gsc similarity index 86% rename from t5/tws_t5_telemetry.gsc rename to t5/scripts/tws_t5_telemetry.gsc index aec6db5..4129d53 100644 --- a/t5/tws_t5_telemetry.gsc +++ b/t5/scripts/tws_t5_telemetry.gsc @@ -1,4 +1,4 @@ -/* tws_t5_telemetry.gsc - Electric Cherry game reports via t5-gsc-utils. */ +/* tws_t5_telemetry.gsc - Electric Cherry game reports via tws-t5-gsc-utils. */ #include maps\_utility; #include common_scripts\utility; @@ -55,6 +55,8 @@ init() level.telemetry_sequence = 0; level.telemetry_completed = 0; level.telemetry_failed = 0; + level.telemetry_http_rejected = 0; + level.telemetry_missing_diagnostics_warned = false; level.telemetry_failure_streak = 0; level.telemetry_last_completed = -1; level.telemetry_last_capture = getTime(); @@ -294,6 +296,8 @@ telemetry_send(entry) options["method"] = "POST"; options["headers"] = headers; options["body"] = entry["body"]; + options["connect_timeout_ms"] = 5000; + options["timeout_ms"] = 15000; request = http::request(level.telemetry_url, options); if (!isDefined(request)) @@ -305,9 +309,15 @@ telemetry_send(entry) } level.telemetry_sender_state = "waiting_http"; - request waittill("done", response, success, error); + request waittill("done", response, success, error, diagnostics); elapsed = getTime() - level.telemetry_request_started; level.telemetry_last_completed = getTime(); + timing = telemetry_http_diagnostics(diagnostics); + if (!isDefined(diagnostics) && !level.telemetry_missing_diagnostics_warned) + { + level.telemetry_missing_diagnostics_warned = true; + telemetry_log(0, "WARNING: plugin returned no HTTP diagnostics; native timeouts are unverified. Install the TWS plugin build and restart the server process"); + } // The plugin's success flag reports transport completion, not HTTP status. // No automatic retry: the receiver may already have accepted the event. @@ -315,7 +325,7 @@ telemetry_send(entry) { if (!isDefined(error)) error = "unknown transport error"; - telemetry_transport_failed(label + " elapsed_ms=" + elapsed + " error=" + telemetry_preview(error)); + telemetry_transport_failed(label + " elapsed_ms=" + elapsed + timing + " error=" + telemetry_preview(error)); } else { @@ -323,7 +333,18 @@ telemetry_send(entry) if (level.telemetry_failure_streak > 0) telemetry_log(0, "Transport recovered after " + level.telemetry_failure_streak + " failures; " + label); level.telemetry_failure_streak = 0; - telemetry_log(1, "Transport completed " + label + " elapsed_ms=" + elapsed + " (API acceptance unverified)"); + if (isDefined(diagnostics) && isDefined(diagnostics["http_status"])) + { + if (diagnostics["http_status"] < 200 || diagnostics["http_status"] >= 300) + { + level.telemetry_http_rejected++; + telemetry_log(0, "ERROR: HTTP rejected " + label + " elapsed_ms=" + elapsed + timing + " body=" + telemetry_preview(response)); + } + else + telemetry_log(1, "HTTP completed " + label + " elapsed_ms=" + elapsed + timing); + } + else + telemetry_log(1, "Transport completed " + label + " elapsed_ms=" + elapsed + timing + " (API acceptance unverified)"); if (!isDefined(response) || response == "") telemetry_log(0, "WARNING: empty API response for " + label); else @@ -334,6 +355,24 @@ telemetry_send(entry) level.telemetry_sender_state = "idle"; } +// The native plugin appends diagnostics while preserving the old done arguments. +// Curl timings are cumulative milestones, measured in real milliseconds. +telemetry_http_diagnostics(diagnostics) +{ + if (!isDefined(diagnostics)) + return " diagnostics=unavailable"; + + keys = strTok("http_status,curl_code,dns_ms,connect_ms,tls_ms,first_byte_ms,total_ms,worker_queue_ms,notify_ms,connect_timeout_ms,timeout_ms", ","); + detail = ""; + for (i = 0; i < keys.size; i++) + { + key = keys[i]; + if (isDefined(diagnostics[key])) + detail += " " + key + "=" + int(diagnostics[key]); + } + return detail; +} + telemetry_transport_failed(detail) { level.telemetry_failed++; @@ -383,7 +422,7 @@ telemetry_preview(value) } // Independent of producers and the HTTP wait; survives game over until drain. -// The plugin cannot cancel requests. Warn without opening overlapping POSTs +// Native requests have deadlines. Warn without opening overlapping POSTs // that could arrive out of order or exhaust its native worker pool. telemetry_watchdog() { @@ -400,7 +439,7 @@ telemetry_watchdog() telemetry_log(1, "Health started=" + level.telemetry_started + " ended=" + level.telemetry_ended + " sender=" + level.telemetry_sender_state + " capturing=" + level.telemetry_capture_event + " pending=" + pending + " queued_total=" + level.telemetry_sequence + - " completed=" + level.telemetry_completed + " failed=" + level.telemetry_failed + + " completed=" + level.telemetry_completed + " failed=" + level.telemetry_failed + " http_rejected=" + level.telemetry_http_rejected + " last_done_ms_ago=" + completion_age); if (isDefined(level.telemetry_inflight)) @@ -409,7 +448,7 @@ telemetry_watchdog() if (age >= 30000) telemetry_log(0, "WARNING: HTTP stalled " + telemetry_event_label(level.telemetry_inflight) + " elapsed_ms=" + age + " pending=" + pending + " stage=" + level.telemetry_sender_state + - "; waiting for plugin completion (no cancellation API)"); + "; native deadline exceeded; check plugin version, worker queue and completion delivery"); } else if (pending > 0) { diff --git a/t6/plugins/README.md b/t6/plugins/README.md new file mode 100644 index 0000000..91e68c4 --- /dev/null +++ b/t6/plugins/README.md @@ -0,0 +1,78 @@ +# tws-t6-gsc-utils + +Source: [alicealys/t6-gsc-utils](https://github.com/alicealys/t6-gsc-utils). +The baseline commit and dependency revisions are in [tws-t6-gsc-utils/upstream.json](tws-t6-gsc-utils/upstream.json). +Source is tracked here directly; dependency checkouts are restored from that +manifest and ignored. Follow the [build instructions](../../README.md#building-plugins). +Upstream API documentation is in [tws-t6-gsc-utils/README.md](tws-t6-gsc-utils/README.md). + +## HTTP deadlines and diagnostics + +All native HTTP requests default to `connect_timeout_ms = 5000` and +`timeout_ms = 15000`. The latter includes connection setup; the shorter limit +wins during connection. Both can be overridden in the existing options array +with positive integer milliseconds. Zero, negative and non-integer overrides +are rejected before a request is created. Neither retries nor redirects are +added. TLS certificate verification remains enabled. + +These are libcurl transfer deadlines, starting when a worker begins the request. +Time waiting for a worker or the game thread to deliver completion is measured +separately. A native timeout releases the request and emits `done` with +`success = false`, a transport error, and diagnostics. Each request is sent once. +The telemetry sender then logs the failed event and advances its ordered queue. +A timeout does not prove the API did not already accept the event. + +The existing completion values remain in the same positions. An optional fourth +value adds diagnostics: + +```c +request waittill("done", response, success, error, diagnostics); +``` + +`success` continues to mean transport success, including completed HTTP 4xx/5xx +responses. Existing scripts that read only the first three values still work. +An updated reporter on an old plugin logs that diagnostics and native deadlines +are unverified; deploy the matching DLL to enable the fix. + +| Field | Meaning | +| --- | --- | +| `http_status` | HTTP response code; `0` if none was received | +| `curl_code` | libcurl result; `0` is transport success, `28` is timeout | +| `dns_ms` | Time to finish name resolution | +| `connect_ms` | Time to establish TCP | +| `tls_ms` | Time to finish TLS; `0` if no TLS handshake completed | +| `first_byte_ms` | Time to first response byte; `0` if none arrived | +| `total_ms` | Total native transfer duration | +| `worker_queue_ms` | Time waiting for the plugin worker pool | +| `notify_ms` | Time from native completion to notifying GSC | +| `connect_timeout_ms`, `timeout_ms` | Effective native deadlines | + +Curl milestone times are cumulative from transfer start, not durations to add +together. Diagnostics are provided for failures as well as successful transfers; +an unfinished milestone can be zero. No request headers, tokens or URLs are +included in diagnostics. Responses retain the existing 20 KiB truncation behavior. + +Telemetry logs the status and timings for each completion at log level 1. +Non-2xx responses log at level 0 with a bounded response preview. Health counters +keep transport `completed`/`failed` and track `http_rejected` separately; an HTTP +rejection is a completed transport. Producers, queue ordering and payloads are +unchanged. Heartbeats are not coalesced, failed reports are not retried, and API +receipt timestamps remain the existing behavior. + +## Validation and rollout + +Run `python3 tools/test_plugin_http.py` from the repository root for portable +native HTTP tests. Build the x86 DLL on Windows, then on a test game server check: + +1. Updated telemetry reports HTTP status and all timing fields on normal requests. +2. An endpoint that accepts a connection but does not reply produces a native + timeout near 15 seconds, followed by the next queued request. +3. HTTP 401/429/500 responses increment `http_rejected` without blocking the queue; + transport errors increment `failed` instead. +4. Leaderboard and older three-value HTTP consumers still receive completion. +5. After a full server-process restart, verify all four telemetry event types in + Electric Cherry. A successful transport alone is not API acceptance. + +The localhost tests compile the actual HTTP utility against the host libcurl. +They do not validate Windows linking, its Schannel backend or game VM notification +handling. Those checks require the Windows build and a running Plutonium server. diff --git a/t6/plugins/tws-t6-gsc-utils/.github/dependabot.yml b/t6/plugins/tws-t6-gsc-utils/.github/dependabot.yml new file mode 100644 index 0000000..f1cef93 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: +- package-ecosystem: gitsubmodule + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/.github/workflows/build.yml b/t6/plugins/tws-t6-gsc-utils/.github/workflows/build.yml new file mode 100644 index 0000000..c6a26b1 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/.github/workflows/build.yml @@ -0,0 +1,80 @@ +name: Build + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + types: [opened, synchronize, reopened] +concurrency: + group: ${{ github.ref }} + cancel-in-progress: false +jobs: + build: + name: Build binaries + runs-on: windows-2022 + strategy: + matrix: + configuration: + - Debug + - Release + steps: + - name: Check out files + uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 0 + lfs: false + + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Generate project files + run: tools/premake5 vs2022 + + - name: Set up problem matching + uses: ammaraskar/msvc-problem-matcher@master + + - name: Build ${{matrix.configuration}} binaries + run: msbuild /m /v:minimal /p:Configuration=${{matrix.configuration}} /p:PlatformTarget=x86 build/tws-t6-gsc-utils.sln + + - name: Upload ${{matrix.configuration}} binaries + uses: actions/upload-artifact@v4 + with: + name: ${{matrix.configuration}} binaries + path: | + build/bin/${{matrix.configuration}}/tws-t6-gsc-utils.dll + deploy: + name: Deploy artifacts + needs: build + runs-on: ubuntu-latest + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') + steps: + - name: Setup main environment + if: github.ref == 'refs/heads/main' + run: echo "ALICE_MASTER_PATH=${{ secrets.ALICE_MASTER_SSH_PATH }}" >> $GITHUB_ENV + + - name: Download Release binaries + uses: actions/download-artifact@v4 + with: + name: Release binaries + + - name: Install SSH key + uses: shimataro/ssh-key-action@v2.7.0 + with: + key: ${{ secrets.ALICE_MASTER_SSH_PRIVATE_KEY }} + known_hosts: 'just-a-placeholder-so-we-dont-get-errors' + + - name: Add known hosts + run: ssh-keyscan -H ${{ secrets.ALICE_MASTER_SSH_ADDRESS }} >> ~/.ssh/known_hosts + + - name: Remove old data files + run: ssh ${{ secrets.ALICE_MASTER_SSH_USER }}@${{ secrets.ALICE_MASTER_SSH_ADDRESS }} rm -rf ${{ env.ALICE_MASTER_PATH }}/t6-gsc-utils/* + + - name: Upload binary + run: rsync -avz tws-t6-gsc-utils.dll ${{ secrets.ALICE_MASTER_SSH_USER }}@${{ secrets.ALICE_MASTER_SSH_ADDRESS }}:${{ env.ALICE_MASTER_PATH }}/t6-gsc-utils/ + + - name: Publish changes + run: ssh ${{ secrets.ALICE_MASTER_SSH_USER }}@${{ secrets.ALICE_MASTER_SSH_ADDRESS }} ${{ secrets.ALICE_MASTER_SSH_CHANGE_PUBLISH_COMMAND }} diff --git a/t6/plugins/tws-t6-gsc-utils/.gitignore b/t6/plugins/tws-t6-gsc-utils/.gitignore new file mode 100644 index 0000000..161cbeb --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/.gitignore @@ -0,0 +1,165 @@ +### Windows + +version.hpp + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Shortcuts +*.lnk + +### OSX + +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Visual Studio + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +build + +# Visual Studio 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +### IDA +*.id0 +*.id1 +*.id2 +*.nam +*.til + +### Custom user files +# User scripts +user*.bat +*.code-workspace + +deps/mysql + +# Dependencies restored from upstream.json by tools/fetch_plugin_deps.py +/deps/GSL/ +/deps/asmjit/ +/deps/curl/ +/deps/date/ +/deps/json/ +/deps/libtomcrypt/ +/deps/libtommath/ +/deps/minhook/ +/deps/plutonium-sdk/ +/deps/sqlpp11/ +/deps/zlib/ diff --git a/t6/plugins/tws-t6-gsc-utils/README.md b/t6/plugins/tws-t6-gsc-utils/README.md new file mode 100644 index 0000000..8a1a7e1 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/README.md @@ -0,0 +1,841 @@ +# tws-t6-gsc-utils +This is the TWS-maintained source snapshot. For the local HTTP timeout and +diagnostics changes, builds and installation, use the [TWS plugin notes](../README.md). +The upstream binary download below does not include these local changes. + +T6 version of [iw5-gsc-utils](https://github.com/alicealys/iw5-gsc-utils). +If you wish for any feature to be added please create an [issue](https://github.com/alicealys/t6-gsc-utils/issues/new). + +# Installation +* Download the latest version [t6-gsc-utils.dll](https://github.alicent.cat/t6-gsc-utils/t6-gsc-utils.dll) +* Copy it to `Plutonium/plugins/` + +# Features + +## Bitwise operators +The T6 GSC compiler is missing support for bitwise operators (or, xor, and, ...), instead you can use the corresponding functions: +```c +init() +{ + flags = 0; + flags = or(flags, 1); // flags |= 1; + flags = and(flags, not(1)); // flags &= ~1; + flags = xor(flags, 1); // flags ^= 1; + + if (and(flags, 1)) // flags & 1 + { + // [...] + } +} +``` + +## Misc +* `arrayRemoveKey(array, key)`: Removes an array element by its (string) key. + + ```c + init() + { + array = []; + array["foo"] = 1; + arrayRemoveKey(array, "foo"); + } + ``` +* `structGet(struct, key)`: Equivalent to `struct.field_name`. + + ```c + init() + { + object = spawnStruct(); + object.foo = 1; + print(structGet(object, "foo")); + } + ``` +* `structSet(struct, key, value)`: Equivalent to `struct.field_name = value`. + + ```c + init() + { + object = spawnStruct(); + structSet(object, "foo", 1); + print(structGet(object, "foo")); + } + ``` +* `structRemove(struct, key)`: Removes an element from a struct. + + ```c + init() + { + object = spawnStruct(); + object.foo = 1; + structRemove(object, "foo"); + } + ``` +* `getStructKeys(struct)`: Returns an array containing all the struct's keys. + + ```c + init() + { + object = spawnStruct(); + object.foo = 1; + object.bar = 2; + + keys = getStructKeys(object); + + for (i = 0; i < keys.size; i++) + { + print(keys[i], structGet(object, keys[i])); + } + } + ``` +* `isFunctionPtr(value)`: Returns true if value is a function. + + ```c + init() + { + assert(isFunctionPtr(::func) == true); + assert(isFunctionPtr("func") == false); + } + + func() + { + print("Hello world"); + } + ``` +* `isEntity(value)`: Returns true if value is an entity (ex. player, script_model, hudelem) + + ```c + init() + { + model = spawn("script_model", (0, 0, 0)); + array = []; + + assert(isEntity(model) == true); + assert(isEntity(array) == false); + } + ``` +* `isStruct(value)`: Return true if value is a struct. + + ```c + init() + { + object = spawnStruct(); + array = []; + + assert(isStruct(object) == true); + assert(isStruct(array) == false); + } + ``` +* `typeof(value)`: Returns the name of a value's type. + + ```c + init() + { + foo = 1; + bar = "hello world"; + baz = []; + + assert(typeof(foo) == "int"); + assert(typeof(bar) == "string"); + assert(typeof(baz) == "array"); + } + ``` +* `entity set(field, value)`: Sets an entity's field. + + ```c + init() + { + ent = spawn("script_origin", (0, 0, 0)); + ent set("origin", (100, 100, 100)); + } + ``` +* `entity get(field)`: Gets an entity's field. + + ```c + init() + { + ent = spawn("script_origin", (0, 0, 0)); + print(ent get("origin")); + } + ``` + +## Entity fields +* `flags`: Get/Set a player's entity flags. + + ```c + onPlayerSpawned() + { + while (true) + { + self waittill("spawned_player"); + self.flags = xor(self.flags, 1); // FL_GODMODE + } + } + ``` + + Enum of all flags: + + ```c + { + FL_GODMODE = 0x1, + FL_DEMI_GODMODE = 0x2, + FL_NOTARGET = 0x4, + FL_NO_KNOCKBACK = 0x8, + FL_DROPPED_ITEM = 0x10, + FL_NO_BOTS = 0x20, + FL_NO_HUMANS = 0x40, + FL_TOGGLE = 0x80, + FL_SOFTACTIVATE = 0x100, + FL_LOW_PRIORITY_USEABLE = 0x200, + FL_NO_TACTICAL_INSERTION = 0x400, + FL_DYNAMICPATH = 0x800, + FL_SUPPORTS_LINKTO = 0x1000, + FL_NO_AUTO_ANIM_UPDATE = 0x2000, + FL_GRENADE_TOUCH_DAMAGE = 0x4000, + FL_GRENADE_MARTYRDOM = 0x8000, + FL_MISSILE_DESTABILIZED = 0x10000, + FL_STABLE_MISSILES = 0x20000, + FL_REPEAT_ANIM_UPDATE = 0x40000, + FL_VEHICLE_TARGET = 0x80000, + FL_GROUND_ENT = 0x100000, + FL_CURSOR_HINT = 0x200000, + FL_USE_TURRET = 0x400000, + FL_MISSILE_ATTRACTOR = 0x800000, + FL_TARGET = 0x1000000, + FL_WEAPON_BEING_GRABBED = 0x2000000, + FL_OBSTACLE = 0x4000000, + FL_DODGE_LEFT = 0x8000000, + FL_DODGE_RIGHT = 0x10000000, + FL_BADPLACE_VOLUME = 0x20000000, + FL_AUTO_BLOCKPATHS = 0x40000000, + FL_MOVER_SLIDE = 0x80000000, + }; + ``` +* `clientflags`: Get/Set a player's client flags. + + ```c + onPlayerSpawned() + { + while (true) + { + self waittill("spawned_player"); + self.clientflags = xor(self.clientflags, 1); // NOCLIP + } + } + ``` + +## Entity Methods +* `entity noclip()`: Toggles Noclip. +* `entity ufo()`: Toggles Ufo. +* `entity god()`: Toggles God mode. +* `entity demigod()`: Toggles Demigod mode. +* `entity notarget()`: Toggles Notarget. + +## Command +* `executeCommand(command)`: Executes a console command. + + ```c + fast_restart() + { + executeCommand("fast_restart"); + } + ``` +* `addCommand(name, callback)`: Adds a console command. + + ```c + init() + { + addCommand("test_cmd", ::test_cmd); + } + + test_cmd(args) + { + assert(args[0] == "test_cmd"); + print("Hello world", args.size); + } + ``` + +## Chat +* `entity setname(name)`: Sets a player's name. + + ```c + onPlayerConnected() + { + while (true) + { + level waittill("connected", player); + player setName("hello world"); + } + } + ``` +* `entity rename(name)`: Same as `setname`. +* `entity resetName()`: Resets a player's name to the original. + + ```c + onPlayerConnected() + { + while (true) + { + level waittill("connected", player); + player setName("hello world"); + player resetName(); + } + } + ``` +* `entity setClantag(clantag)`: Sets a player's clantag. + + ```c + onPlayerConnected() + { + while (true) + { + level waittill("connected", player); + player setClantag("hello world"); + } + } + ``` +* `entity resetClantag()`: Resets a player's clantag to the original. + + ```c + onPlayerConnected() + { + while (true) + { + level waittill("connected", player); + player setClantag("hello world"); + player resetClantag(); + } + } + ``` +* `say(message)`: Sends a chat message to all players. + + ```c + onPlayerConnected() + { + while (true) + { + level waittill("connected", player); + say(player.name + " connected!"); + } + } + ``` +* `entity tell(message)`: Sends a chat message to a player. + + ```c + onPlayerConnected() + { + while (true) + { + level waittill("connected", player); + player tell("Welcome back " + player.name + "!"); + } + } + ``` +* `sendServerCommand(clientnum, cmd)`: Executes SV_GameSendServerCommand. + + ```c + onPlayerConnected() + { + while (true) + { + level waittill("connected", player); + sendServerCommand(-1, "j \"Hello world\""); // -1 -> all clients + } + } + ``` + +* `entity sendServerCommand(cmd)`: Executes SV_GameSendServerCommand on a specified client. + + ```c + onPlayerConnected() + { + while (true) + { + level waittill("connected", player); + player sendServerCommand("j \"Hello world\""); + } + } + ``` +* `chat::register_callback(callback[, sync])`: Registers a chat callback. + * `callback: function(text: string, mode: int)`: the GSC function that will be executed, `self` is the player that sent the message. + * `sync: bool (default: false)`: whether the callback should be executed immediately or in the next frame, setting it to true allows you to hide the message by making the callback return true. + **WARNING**: Using certain GSC function in the callback when `sync` is true can cause crashes (example: any function that causes damage), so only set it to true when you really need it. + ```gsc + init() + { + chat::register_callback(::chat_callback); + } + + chat_callback(text, mode) + { + self tell(string::format("You said %s", text)); + } + ``` + +* `chat::register_command(name, callback[, hide_message[, sync])`: Regisers a chat command. + * `name`: can be either a string or an array of strings. + * `callback: function(args: array)`: the GSC function that will be executed, `self` is the player that sent the message. + * `hide_message: bool (default: false)`: specifies whether the message sent by the player should be hidden from the chat. + * `sync: bool (default: false)`: as described in `chat::register_callback` + + ```gsc + init() + { + chat::register_command("!hello", ::cmd_hello, true); + } + + cmd_hello(args) + { + assert(string::to_lower(args[0]) == "!hello"); + self tell("world"); + } + ``` +* `chat::disable_command(name)`: Disables a chat command. +* `chat::enable_command(name)`: Enables a chat command. + +# IO +The basepath for all IO functions is `Plutonium/storage/t6` + + * `fileExists(path)`: Returns true if the file exists. + * `writeFile(path, data[, append])`: Creates a file if it doesn't exist and writes/appends text to it. + * `readFile(path)`: Reads a file. + * `fileSize(path)`: Returns file size in bytes. + * `createDirectory(path)`: Creates a directory. + * `directoryExists(path)`: Returns true if the directory exists. + * `directoryIsEmpty(path)`: Returns true if the directory is empty. + * `listFiles(path)`: Returns the list of files in the directory as an array. + * `copyFolder(source, target)`: Copies a folder. + * `jsonPrint(...)`: Prints values as json. + * `httpGet(url)`: Creates a GET HTTP request- + + ```c + init() + { + req = httpGet("https://example.com"); + req waittill("done", result); + print(result); + } + ``` + * `httpPost(url, data, headers)`: Creates a [POST HTTP request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) -> + + ```c + headers = []; + headers["Content-Type"] = "application/json"; + + data = []; + data["test_key"] = "test_value"; + + req = httpPost("https://example.com", jsonSerialize(data, 0), headers); + req waittill("done", result); + print(result); + ``` +# JSON + +* `jsonSerialize(variable[, indent])`: Converts GSC variables (such as arrays) into JSON: + + ```c + init() + { + array = []; + array[0] = 1; + array[1] = 2; + json = jsonSerialize(array, 4); + + print(json); + + /* + [ + 2, + 1 + ] + */ + } + ``` + + This function can also be useful to reveal contents of existing arrays such as `game`: + ```c + init() + { + print(jsonSerialize(game["allies_model"], 4)); + + /* + { + "ASSAULT": "[function]", + "GHILLIE": "[function]", + "JUGGERNAUT": "[function]", + "LMG": "[function]", + "RIOT": "[function]", + "SHOTGUN": "[function]", + "SMG": "[function]", + "SNIPER": "[function]" + } + */ + + print(jsonSerialize(game["music"], 4)); + + /* + { + "defeat_allies": "UK_defeat_music", + "defeat_axis": "IC_defeat_music", + "losing_allies": "UK_losing_music", + "losing_axis": "IC_losing_music", + "losing_time": "mp_time_running_out_losing", + "nuke_music": "nuke_music", + "spawn_allies": "UK_spawn_music", + "spawn_axis": "IC_spawn_music", + "suspense": [ + "mp_suspense_06", + "mp_suspense_05", + "mp_suspense_04", + "mp_suspense_03", + "mp_suspense_02", + "mp_suspense_01" + ], + "victory_allies": "UK_victory_music", + "victory_axis": "IC_victory_music", + "winning_allies": "UK_winning_music", + "winning_axis": "IC_winning_music" + } + */ + } + ``` +* `jsonParse(json)`: Converts JSON into a GSC variable: + + ```c + init() + { + array = jsonParse("[1,2,3,4]"); + print(array[0] + " " + array[1] + " " + array[2] + " " + array[3]); + + /* + 1 2 3 4 + */ + } + ``` +* `createMap(...)`: Creates a string-indexed array: + + ```c + init() + { + array = map("first", 1, "second", 2); + print(jsonSerialize(array, 4)); + /* + { + "first": 1, + "second": 2 + } + */ + } + ``` +* `jsonDump(file, value[, indent])`: Dumps a json value to a file. + + ```c + init() + { + jsonDump("level.txt", level, 4); + } + ``` + +# Debugging +* `crash()`: Crashes the server. +* `breakpoint(message)`: Pauses the server's execution and shows a message box with the passed message and call stack. +* `assert(condition)`: Throws an error if condition is false. + +None of these functions will do anything unless `developer_script` is set to `1`, so make sure to set it before attempting to use them. + +Additionally, running the server with the `--gsc-debug` flag will set a custom crash handler to the server which will dump the GSC VM stack trace (along with local variables) to a text file (called `gsc_vm_dump.txt`) along with the regular minidump. These dumps will be saved to `Plutonium/storage/t6/minidumps`. + +# Threads +* `killThread(function[, owner])`: Kills a running/wait/waittill thread based on the function it's executing and optionally even the thread owner object/entity. + + ```c + /* + This will kill the execution of "test_thread" after 5 seconds. + */ + + init() + { + level thread test_thread(); + level thread test_kill_thread(); + } + + test_kill_thread() + { + wait 5; + killThread(::test_thread, level); + } + + test_thread() + { + while (true) + { + wait 1; + print("Hello world"); + } + } + ``` +* `killAllThreads(function[, owner])`: Same as `killThread` but kills all matching threads. + +# Bots + +Custom bot names and clantags can be set by creating a file in `Plutonium/storage/t6/bots.txt` +This feature is now supported by Plutonium and you should refer to the official forum for assistance. + +* `dropAllBots()`: Kick all test clients from the lobby. + +# GameLog + +Remove the clan tag from the name of a player from the game log. This fixes IW4M erroneously thining the full name of a player is made of the clan tag plus the name instead of just the name. +This also patches the behaviour of the .name getter function for players for GSC scripts. In short, this only affects GSC and the game log. + +* `sv_display_clan_tag`: This dvar toggles the patch + +# MySQL + +You can access a mysql database using the following functions: + +* `mysql::set_config(config)`: Must be called before calling other mysql functions, config should be a struct of this kind: + + ```gsc + init() + { + config = spawnstruct(); + config.host = "localhost"; + config.user = "root"; + config.password = "password"; + config.port = 3306; + config.database = "database_name"; + mysql::set_config(config); + } + ``` +* `mysql::query(query)`: Executes an sql statement, returns a query object: + + ```gsc + init() + { + // call mysql::set_config + + query = mysql::execute("select * from `players` where guid=1"); + query waittill("done", result); + if (result.size > 0) + { + print("player name " + result[0]["name"]); + } + } + ``` +* `mysql::prepared_statement(query: string, params...)`: Executes a prepared statement, params can be a list of arguments or an array: + + ```gsc + init() + { + // call mysql::set_config + + // use variadic args for the parameters + { + query = mysql::prepared_statement("insert into `players` (`guid`, `name`) values (?, ?)", 123, "foo"); + query waittill("done", result, affected_rows, error); + } + + // use an array for the parameters + { + params = array(123, "foo"); + query = mysql::prepared_statement("insert into `players` (`guid`, `name`) values (?, ?)", params); + query waittill("done", result, affected_rows, error); + } + } + ``` + +# Hud Elements +* `hudelem::set_text(text: string)`: Sets the text of a hud element by reserving a config string and modifying its value. + This function can be used to prevent the "G_FindConfigstringIndex: overflow (%d): " error. + ```gsc + { + // ... + elem = newhudelement(); + elem.x = 100; + elem.y = 100; + elem.font = "objective"; + elem.fontscale = 1; + i = 0; + while (true) + { + elem hudelem::set_text(string::format("frame %i", i++)); + wait 0.05; + } + } + ``` + +## Int64 +These int64 functions allow you to use int64 values within GSC, which normally only supports 32 bit integers. +This is useful if you store int64 values in a file or a database and need to access them from GSC. +The values are stored as strings but all the functions below can take either a string or int for an int64 argument. +`int64` represents a value that is either a `string` or an `int`. +* `int64::op(a: int64, op: string, b: int64): int64|bool`: Performs an operation between 2 int64 values (stored as strings) + ```gsc + { + result = int64::op("2583528945238953952", "+", "12039152933205235"); + assert(result == "2595568098172159187"); + + result = int64::op("5834258295282538925", ">", 1); + assert(result == true); + } + ``` + List of supported operators: + * `+` + * `-` + * `*` + * `/` + * `&` + * `^` + * `|` + * `~` + * `%` + * `>>` + * `<<` + * `++` + * `--` +* `int64::is_int(value: int64): bool`: Returns true if an int64 value fits within an int32 value. +* `int64::to_int(value: int64): int`: Converts an int64 value to an int32 value. +* `int64::min(a: int64, b: int64): int64`: Returns the minimum between 2 int64 values. +* `int64::max(a: int64, b: int64): int64`: Returns the maximum between 2 int64 values. + +## Function list +* `dropallbots` +* `say` +* `sendservercommand` +* `onplayersay` +* `chat::register_callback` +* `chat::register_command` +* `chat::disable_command` +* `chat::enable_command` +* `executecommand` +* `command::execute` +* `addcommand` +* `command::add` +* `addclientcommand` +* `command::add_sv` +* `crash` +* `breakpoint` +* `assert` +* `assertmsg` +* `throw` +* `killthread` +* `killallthreads` +* `getvarusage` +* `getchildvarusage` +* `getusagestats` +* `removeconfigstring` +* `replaceconfigstring` +* `getcurrentthread` +* `printcallstack` +* `getcallstack` +* `getfunctionargcount` +* `arrayremovekey` +* `xor` +* `not` +* `and` +* `or` +* `structget` +* `structset` +* `structremove` +* `getstructkeys` +* `isfunctionptr` +* `isentity` +* `isstruct` +* `typeof` +* `worldget` +* `worldset` +* `http::get` +* `httpget` +* `curl` +* `http::request` +* `httppost` +* `int64_is_int` +* `int64::is_int` +* `int64_to_int` +* `int64::to_int` +* `int64_op` +* `int64::op` +* `int64_min` +* `int64::min` +* `int64_max` +* `int64::max` +* `hashstring` +* `writefile` +* `io::write_file` +* `appendfile` +* `io::append_file` +* `fileexists` +* `io::file_exists` +* `movefile` +* `io::move_file` +* `filesize` +* `io::file_size` +* `createdirectory` +* `io::create_directory` +* `directoryexists` +* `io::directory_exists` +* `directoryisempty` +* `io::directory_is_empty` +* `listfiles` +* `io::list_files` +* `removefile` +* `io::remove_file` +* `removedirectory` +* `io::remove_directory` +* `copyfolder` +* `io::copy_folder` +* `copydirectory` +* `io::copy_directory` +* `readfile` +* `io::read_file` +* `createmap` +* `json::create_map` +* `jsonparse` +* `json::parse` +* `jsonserialize` +* `json::serialize` +* `jsondump` +* `json::dump` +* `jsonprint` +* `json::print` +* `mysql::set_config` +* `mysql::query` +* `mysql::prepared_statement` +* `createnotifygroup` +* `va` +* `string::va` +* `formatstring` +* `string::format` +* `sprintf` +* `printf` +* `print` +* `toupper` +* `string::to_upper` +* `tolower` +* `string::to_lower` +* `string::is_numeric` +* `string::starts_with` +* `string::ends_with` +* `string::replace` +* `string::regex_replace` +* `string::regex_match` +* `string::regex_search` +* `dumpturret` +## Method list +* `rename` +* `setname` +* `resetname` +* `resetclantag` +* `setclantag` +* `tell` +* `sendservercommand` +* `hudelem::set_text` +* `get` +* `set` +* `noclip` +* `ufo` +* `god` +* `demigod` +* `notarget` +* `setturretfield` +* `getturretfield` diff --git a/t6/plugins/tws-t6-gsc-utils/deps/premake/asmjit.lua b/t6/plugins/tws-t6-gsc-utils/deps/premake/asmjit.lua new file mode 100644 index 0000000..ee93259 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/deps/premake/asmjit.lua @@ -0,0 +1,34 @@ +asmjit = { + source = path.join(dependencies.basePath, "asmjit"), +} + +function asmjit.import() + links { "asmjit" } + asmjit.includes() +end + +function asmjit.includes() + includedirs { + path.join(asmjit.source, "src") + } + + defines { + "ASMJIT_STATIC" + } +end + +function asmjit.project() + project "asmjit" + language "C++" + + asmjit.includes() + + files { + path.join(asmjit.source, "src/**.cpp"), + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, asmjit) diff --git a/t6/plugins/tws-t6-gsc-utils/deps/premake/curl.lua b/t6/plugins/tws-t6-gsc-utils/deps/premake/curl.lua new file mode 100644 index 0000000..3d36749 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/deps/premake/curl.lua @@ -0,0 +1,73 @@ +curl = { + source = path.join(dependencies.basePath, "curl"), +} + +function curl.import() + links { "curl" } + + filter "toolset:msc*" + links { "Crypt32.lib" } + filter {} + + curl.includes() +end + +function curl.includes() +filter "toolset:msc*" + includedirs { + path.join(curl.source, "include"), + } + + defines { + "CURL_STRICTER", + "CURL_STATICLIB", + "CURL_DISABLE_LDAP", + } +filter {} +end + +function curl.project() + if not os.istarget("windows") then + return + end + + project "curl" + language "C" + + curl.includes() + + includedirs { + path.join(curl.source, "lib"), + } + + files { + path.join(curl.source, "lib/**.c"), + path.join(curl.source, "lib/**.h"), + } + + defines { + "BUILDING_LIBCURL", + } + + filter "toolset:msc*" + + defines { + "USE_SCHANNEL", + "USE_WINDOWS_SSPI", + "USE_THREADS_WIN32", + } + + filter "toolset:not msc*" + + defines { + "USE_GNUTLS", + "USE_THREADS_POSIX", + } + + filter {} + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, curl) \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/deps/premake/gsl.lua b/t6/plugins/tws-t6-gsc-utils/deps/premake/gsl.lua new file mode 100644 index 0000000..7a2daf6 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/deps/premake/gsl.lua @@ -0,0 +1,19 @@ +gsl = { + source = path.join(dependencies.basePath, "GSL"), +} + +function gsl.import() + gsl.includes() +end + +function gsl.includes() + includedirs { + path.join(gsl.source, "include") + } +end + +function gsl.project() + +end + +table.insert(dependencies, gsl) diff --git a/t6/plugins/tws-t6-gsc-utils/deps/premake/json.lua b/t6/plugins/tws-t6-gsc-utils/deps/premake/json.lua new file mode 100644 index 0000000..e5eb493 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/deps/premake/json.lua @@ -0,0 +1,19 @@ +json = { + source = path.join(dependencies.basePath, "json"), +} + +function json.import() + json.includes() +end + +function json.includes() + includedirs { + path.join(json.source, "single_include/*") + } +end + +function json.project() + +end + +table.insert(dependencies, json) diff --git a/t6/plugins/tws-t6-gsc-utils/deps/premake/libtomcrypt.lua b/t6/plugins/tws-t6-gsc-utils/deps/premake/libtomcrypt.lua new file mode 100644 index 0000000..6c6f28d --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/deps/premake/libtomcrypt.lua @@ -0,0 +1,61 @@ +libtomcrypt = { + source = path.join(dependencies.basePath, "libtomcrypt"), +} + +function libtomcrypt.import() + links { + "libtomcrypt" + } + + libtomcrypt.includes() +end + +function libtomcrypt.includes() + includedirs { + path.join(libtomcrypt.source, "src/headers") + } + + defines { + "LTC_NO_FAST", + "LTC_NO_PROTOTYPES", + "LTC_NO_RSA_BLINDING", + } +end + +function libtomcrypt.project() + project "libtomcrypt" + language "C" + + libtomcrypt.includes() + libtommath.import() + + files { + path.join(libtomcrypt.source, "src/**.c"), + } + + removefiles { + path.join(libtomcrypt.source, "src/**/*tab.c"), + path.join(libtomcrypt.source, "src/encauth/ocb3/**.c"), + } + + defines { + "_CRT_SECURE_NO_WARNINGS", + "LTC_SOURCE", + "_LIB", + "USE_LTM" + } + + removedefines { + "_DLL", + "_USRDLL" + } + + linkoptions { + "-IGNORE:4221" + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, libtomcrypt) diff --git a/t6/plugins/tws-t6-gsc-utils/deps/premake/libtommath.lua b/t6/plugins/tws-t6-gsc-utils/deps/premake/libtommath.lua new file mode 100644 index 0000000..ab4cdde --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/deps/premake/libtommath.lua @@ -0,0 +1,52 @@ +libtommath = { + source = path.join(dependencies.basePath, "libtommath"), +} + +function libtommath.import() + links { + "libtommath" + } + + libtommath.includes() +end + +function libtommath.includes() + includedirs { + libtommath.source + } + + defines { + "LTM_DESC", + "__STDC_IEC_559__", + "MP_NO_DEV_URANDOM", + } +end + +function libtommath.project() + project "libtommath" + language "C" + + libtommath.includes() + + files { + path.join(libtommath.source, "*.c"), + } + + defines { + "_LIB" + } + + removedefines { + "_DLL", + "_USRDLL" + } + + linkoptions { + "-IGNORE:4221" + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, libtommath) diff --git a/t6/plugins/tws-t6-gsc-utils/deps/premake/minhook.lua b/t6/plugins/tws-t6-gsc-utils/deps/premake/minhook.lua new file mode 100644 index 0000000..396d4d3 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/deps/premake/minhook.lua @@ -0,0 +1,31 @@ +minhook = { + source = path.join(dependencies.basePath, "minhook"), +} + +function minhook.import() + links { "minhook" } + minhook.includes() +end + +function minhook.includes() + includedirs { + path.join(minhook.source, "include") + } +end + +function minhook.project() + project "minhook" + language "C" + + minhook.includes() + + files { + path.join(minhook.source, "src/**.h"), + path.join(minhook.source, "src/**.c"), + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, minhook) diff --git a/t6/plugins/tws-t6-gsc-utils/deps/premake/minizip.lua b/t6/plugins/tws-t6-gsc-utils/deps/premake/minizip.lua new file mode 100644 index 0000000..4a5754b --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/deps/premake/minizip.lua @@ -0,0 +1,43 @@ +minizip = { + source = path.join(dependencies.basePath, "zlib/contrib/minizip"), +} + +function minizip.import() + links { "minizip" } + zlib.import() + minizip.includes() +end + +function minizip.includes() + includedirs { + minizip.source + } + + zlib.includes() +end + +function minizip.project() + project "minizip" + language "C" + + minizip.includes() + + files { + path.join(minizip.source, "*.h"), + path.join(minizip.source, "*.c"), + } + + removefiles { + path.join(minizip.source, "miniunz.c"), + path.join(minizip.source, "minizip.c"), + } + + defines { + "_CRT_SECURE_NO_DEPRECATE", + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, minizip) diff --git a/t6/plugins/tws-t6-gsc-utils/deps/premake/mysql.lua b/t6/plugins/tws-t6-gsc-utils/deps/premake/mysql.lua new file mode 100644 index 0000000..a35778f --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/deps/premake/mysql.lua @@ -0,0 +1,52 @@ +if (os.host() ~= "windows") then + error("automatic mysql installation is not supported on your os") +end + +mysql = { + source = path.join(dependencies.basePath, "mysql"), + version = "5.7.43", + download = "https://cdn.alicent.cat/mysql-5.7.43-win32.zip", +} + +function mysql.install() + local hfile = io.open(string.format("%s/include/mysql.h", mysql.source), "r") + if (hfile) then + return + end + + os.execute(string.format("mkdir \"%s\" 2> nul", mysql.source)) + + local folder = path.join(mysql.source, "mysql-5.7.43-win32") + local archive = path.join(mysql.source, "mysql-5.7.43-win32.zip") + + print("Downloading MYSQL") + os.execute(string.format("curl \"%s\" -L -o \"%s\"", mysql.download, archive)) + + os.execute(string.format("powershell -command \"Expand-Archive -Force \\\"%s\\\" \\\"%s\\\"\"", archive, mysql.source)) + os.execute(string.format("powershell -command \"mv \\\"%s/*\\\" \\\"%s\\\"\"", folder, mysql.source)) + os.execute(string.format("powershell -command \"rm \\\"%s\\\"\"", archive)) + os.execute(string.format("rmdir \"%s\"", folder)) +end + +function mysql.import() + mysql.install() + mysql.includes() +end + +function mysql.includes() + includedirs { + path.join(mysql.source, "include"), + } +end + +function mysql.project() + project "mysql" + language "C" + + mysql.includes() + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, mysql) diff --git a/t6/plugins/tws-t6-gsc-utils/deps/premake/plutonium-sdk.lua b/t6/plugins/tws-t6-gsc-utils/deps/premake/plutonium-sdk.lua new file mode 100644 index 0000000..22d8ab7 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/deps/premake/plutonium-sdk.lua @@ -0,0 +1,18 @@ +plutonium_sdk = { + source = path.join(dependencies.basePath, "plutonium-sdk"), +} + +function plutonium_sdk.import() + plutonium_sdk.includes() +end + +function plutonium_sdk.includes() + includedirs { + plutonium_sdk.source, + } +end + +function plutonium_sdk.project() +end + +table.insert(dependencies, plutonium_sdk) diff --git a/t6/plugins/tws-t6-gsc-utils/deps/premake/sqlpp11.lua b/t6/plugins/tws-t6-gsc-utils/deps/premake/sqlpp11.lua new file mode 100644 index 0000000..1f96b16 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/deps/premake/sqlpp11.lua @@ -0,0 +1,26 @@ +sqlpp11 = { + source = path.join(dependencies.basePath, "sqlpp11"), +} + +function sqlpp11.import() + sqlpp11.includes() +end + +function sqlpp11.includes() + includedirs { + path.join(sqlpp11.source, "include"), + path.join(dependencies.basePath, "date/include"), + } +end + +function sqlpp11.project() + project "sqlpp11" + language "C++" + + sqlpp11.includes() + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, sqlpp11) diff --git a/t6/plugins/tws-t6-gsc-utils/deps/premake/zlib.lua b/t6/plugins/tws-t6-gsc-utils/deps/premake/zlib.lua new file mode 100644 index 0000000..566a707 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/deps/premake/zlib.lua @@ -0,0 +1,39 @@ +zlib = { + source = path.join(dependencies.basePath, "zlib"), +} + +function zlib.import() + links { "zlib" } + zlib.includes() +end + +function zlib.includes() + includedirs { + zlib.source + } + + defines { + "ZLIB_CONST", + } +end + +function zlib.project() + project "zlib" + language "C" + + zlib.includes() + + files { + path.join(zlib.source, "*.h"), + path.join(zlib.source, "*.c"), + } + + defines { + "_CRT_SECURE_NO_DEPRECATE", + } + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, zlib) diff --git a/t6/plugins/tws-t6-gsc-utils/generate.bat b/t6/plugins/tws-t6-gsc-utils/generate.bat new file mode 100644 index 0000000..2bad27b --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/generate.bat @@ -0,0 +1,4 @@ +@echo off +echo Updating submodules... +call git submodule update --init --recursive +call tools\premake5 %* vs2022 diff --git a/t6/plugins/tws-t6-gsc-utils/premake5.lua b/t6/plugins/tws-t6-gsc-utils/premake5.lua new file mode 100644 index 0000000..90b36b4 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/premake5.lua @@ -0,0 +1,104 @@ +dependencies = { + basePath = "./deps" +} + +function dependencies.load() + dir = path.join(dependencies.basePath, "premake/*.lua") + deps = os.matchfiles(dir) + + for i, dep in pairs(deps) do + dep = dep:gsub(".lua", "") + require(dep) + end +end + +function dependencies.imports() + for i, proj in pairs(dependencies) do + if type(i) == 'number' then + proj.import() + end + end +end + +function dependencies.projects() + for i, proj in pairs(dependencies) do + if type(i) == 'number' then + proj.project() + end + end +end + +dependencies.load() + +workspace "tws-t6-gsc-utils" + location "./build" + objdir "%{wks.location}/obj/%{cfg.buildcfg}" + targetdir "%{wks.location}/bin/%{cfg.buildcfg}" + targetname "%{prj.name}" + + configurations { "Debug", "Release", } + + language "C++" + cppdialect "C++20" + + architecture "x86" + + systemversion "latest" + symbols "On" + staticruntime "On" + editandcontinue "Off" + warnings "Extra" + characterset "ASCII" + + flags + { + "NoIncrementalLink", + "MultiProcessorCompile", + } + + filter "configurations:Release" + optimize "Full" + defines { "NDEBUG" } + filter {} + + filter "configurations:Debug" + optimize "Debug" + defines { "DEBUG", "_DEBUG" } + filter {} + + startproject "tws-t6-gsc-utils" + project "tws-t6-gsc-utils" + kind "SharedLib" + language "C++" + + files + { + "./src/**.h", + "./src/**.hpp", + "./src/**.cpp", + "./src/**.rc", + } + + includedirs + { + "%{prj.location}/src", + "./src", + "./deps/mysql/include" + } + + libdirs {"./deps/mysql/lib"} + + resincludedirs + { + "$(ProjectDir)src" + } + + linkoptions {"/DELAYLOAD:libmysql.dll"} + + pchheader "stdinc.hpp" + pchsource "src/stdinc.cpp" + + dependencies.imports() + + group "Dependencies" + dependencies.projects() \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/chat.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/chat.cpp new file mode 100644 index 0000000..deb8ed8 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/chat.cpp @@ -0,0 +1,348 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "gsc.hpp" +#include "scripting.hpp" +#include "scheduler.hpp" + +#include +#include +#include + +namespace chat +{ + namespace + { + struct chat_command_t + { + scripting::function callback; + bool hide_message; + bool enabled; + bool sync; + }; + + struct chat_callback_t + { + scripting::function callback; + bool sync; + }; + + using userinfo_map = std::unordered_map; + + utils::hook::detour sv_get_user_info_hook; + utils::hook::detour client_connect_hook; + utils::hook::detour g_say_hook; + + std::array userinfo_overrides; + std::unordered_map chat_commands; + std::vector chat_callbacks; + + void sv_get_user_info_stub(int index, char* buffer, int buffer_size) + { + sv_get_user_info_hook.invoke(index, buffer, buffer_size); + + utils::info_string map(buffer); + + for (const auto& [key, val] : userinfo_overrides[index]) + { + if (val.empty()) + { + map.remove(key); + } + else + { + map.set(key, val); + } + } + + const auto userinfo = map.build(); + strncpy_s(buffer, 1024, userinfo.data(), _TRUNCATE); + } + + void clear_client_overrides(unsigned int client) + { + userinfo_overrides[client].clear(); + } + + bool handle_chat_command(game::gentity_s* ent, const std::string& text) + { + const auto trimmed = utils::string::trim(text); + const auto args = utils::string::split(trimmed, ' '); + if (args.size() <= 0) + { + return false; + } + + const auto name = utils::string::to_lower(args[0]); + const auto iter = chat_commands.find(name); + if (iter == chat_commands.end()) + { + return false; + } + + if (!iter->second.enabled) + { + return false; + } + + if (iter->second.sync) + { + const auto entity_id = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, ent->number, 0, 0); + const auto result = iter->second.callback(entity_id, {args}); + + if (result.is()) + { + return result.as(); + } + } + else + { + scheduler::once([=] + { + const auto entity_id = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, ent->number, 0, 0); + iter->second.callback(entity_id, {args}); + }, scheduler::server); + } + + return iter->second.hide_message; + } + + bool handle_chat_callback(game::gentity_s* ent, const std::string& text, const int mode) + { + auto hidden = false; + + for (const auto& chat_callback : chat_callbacks) + { + if (chat_callback.sync) + { + const auto entity_id = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, ent->number, 0, 0); + const auto result = chat_callback.callback(entity_id, {text, mode}); + + if (result.is()) + { + hidden = hidden || result.as(); + } + } + else + { + scheduler::once([=] + { + const auto entity_id = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, ent->number, 0, 0); + chat_callback.callback(entity_id, {text, mode}); + }, scheduler::server); + } + } + + return hidden; + } + + void g_say_stub(game::gentity_s* ent, game::gentity_s* target, int mode, const char* text) + { + auto hidden = false; + hidden = hidden || handle_chat_command(ent, text); + hidden = hidden || handle_chat_callback(ent, text, mode); + + if (!hidden) + { + g_say_hook.invoke(ent, target, mode, text); + } + } + + void client_clean_name(const char* in, char* out, int out_size) + { + utils::hook::invoke(SELECT(0x424F00, 0x655010), out, in, out_size); // I_strncpyz + } + + __declspec(naked) void client_clean_name_stub() + { + __asm + { + pushad + + push [esp + 0x20 + 0x4] // outSize + push edx // out + push ecx // in + call client_clean_name + add esp, 0xC + + popad + ret + } + } + + game::scr_entref_t check_entity(const scripting::entity& entity) + { + const auto ent = entity.get_entity_reference(); + if (ent.classnum != 0) + { + throw std::runtime_error("invalid entity"); + } + + if (game::g_entities[ent.entnum].client == nullptr) + { + throw std::runtime_error("not a player entity"); + } + + return ent; + } + + template + void enable_chat_command(const std::string& name) + { + const auto lower = utils::string::to_lower(name); + const auto iter = chat_commands.find(lower); + if (iter == chat_commands.end()) + { + return; + } + + iter->second.enabled = value; + } + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + const auto plutonium_g_say_stub = utils::hook::extract(SELECT(0x6A7A40, 0x493DF0) + 1); + g_say_hook.create(plutonium_g_say_stub, g_say_stub); + sv_get_user_info_hook.create(SELECT(0x68BB90, 0x4C10F0), sv_get_user_info_stub); + + plugin->get_interface()->callbacks()->on_player_connect(clear_client_overrides); + plugin->get_interface()->callbacks()->on_player_disconnect(clear_client_overrides); + + utils::hook::call(SELECT(0x4ED764, 0x427E84), client_clean_name_stub); + utils::hook::call(SELECT(0x4ED79F, 0x427EBF), client_clean_name_stub); + + scripting::on_shutdown([]() + { + for (auto& entry : userinfo_overrides) + { + entry.clear(); + } + + chat_callbacks.clear(); + chat_commands.clear(); + }); + + gsc::method::add("rename", [](const scripting::entity& entity, const std::string& name) + { + const auto ent = check_entity(entity); + userinfo_overrides[ent.entnum]["name"] = name; + game::ClientUserInfoChanged(ent.entnum); + }); + + gsc::method::add("setname", [](const scripting::entity& entity, const std::string& name) + { + const auto ent = check_entity(entity); + userinfo_overrides[ent.entnum]["name"] = name; + game::ClientUserInfoChanged(ent.entnum); + }); + + gsc::method::add("resetname", [](const scripting::entity& entity) + { + const auto ent = check_entity(entity); + userinfo_overrides[ent.entnum].erase("name"); + game::ClientUserInfoChanged(ent.entnum); + }); + + gsc::method::add("resetclantag", [](const scripting::entity& entity) + { + const auto ent = check_entity(entity); + userinfo_overrides[ent.entnum].erase("clantag"); + userinfo_overrides[ent.entnum].erase("clanAbbrev"); + userinfo_overrides[ent.entnum].erase("clanAbbrevEV"); + game::ClientUserInfoChanged(ent.entnum); + }); + + gsc::method::add("setclantag", [](const scripting::entity& entity, const std::string& name) + { + const auto ent = check_entity(entity); + userinfo_overrides[ent.entnum]["clantag"] = name; + userinfo_overrides[ent.entnum]["clanAbbrev"] = name; + userinfo_overrides[ent.entnum]["clanAbbrevEV"] = "1"; + game::ClientUserInfoChanged(ent.entnum); + }); + + gsc::method::add("tell", [](const scripting::entity& entity, const std::string& msg) + { + const auto ent = check_entity(entity); + game::SV_GameSendServerCommand(ent.entnum, 0, utils::string::va("j \"%s\"", msg.data())); + }); + + gsc::function::add("say", [](const std::string& msg) + { + game::SV_GameSendServerCommand(-1, 0, utils::string::va("j \"%s\"", msg.data())); + }); + + gsc::function::add("sendservercommand", [](const int client, const std::string& cmd) + { + game::SV_GameSendServerCommand(client, 0, cmd.data()); + }); + + gsc::method::add("sendservercommand", [](const scripting::entity& entity, const std::string& cmd) + { + const auto ent = check_entity(entity); + game::SV_GameSendServerCommand(ent.entnum, 0, cmd.data()); + }); + + gsc::function::add_multiple([](const scripting::function& callback, const scripting::variadic_args& va) + { + chat_callback_t chat_callback{}; + chat_callback.sync = false; + chat_callback.callback = callback; + + if (va.size() >= 1) + { + chat_callback.sync = va[0].as(); + } + + chat_callbacks.emplace_back(chat_callback); + }, "onplayersay", "chat::register_callback"); + + gsc::function::add("chat::register_command", [](const scripting::function_argument& names_or_name, + const scripting::function& callback, const scripting::variadic_args& va) + { + chat_command_t command{}; + command.hide_message = true; + command.sync = false; + command.callback = callback; + command.enabled = true; + + if (va.size() >= 1) + { + command.hide_message = va[0].as(); + } + + if (va.size() >= 2) + { + command.sync = va[1].as(); + } + + if (names_or_name.is()) + { + const std::vector names = names_or_name; + for (const auto& name : names) + { + const auto lower = utils::string::to_lower(name); + chat_commands.insert(std::make_pair(lower, command)); + } + } + else + { + const auto name = names_or_name.as(); + const auto lower = utils::string::to_lower(name); + chat_commands.insert(std::make_pair(lower, command)); + } + }); + + gsc::function::add("chat::disable_command", enable_chat_command); + gsc::function::add("chat::enable_command", enable_chat_command); + } + }; +} + +REGISTER_COMPONENT(chat::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/clients.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/clients.cpp new file mode 100644 index 0000000..98f2a71 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/clients.cpp @@ -0,0 +1,120 @@ +#include +#include "loader/component_loader.hpp" + +#include "gsc.hpp" + +namespace clients +{ + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + gsc::function::add("dropallbots", [] + { + const auto* dvar = game::Dvar_FindVar("com_maxclients"); + const auto client_count = game::Dvar_GetInt(dvar); + + for (auto i = 0; i < client_count; ++i) + { + if (game::SV_IsTestClient(i)) + { + game::SV_GameDropClient(i, "GAME_DROPPEDFORINACTIVITY"); + } + } + }); + + gsc::method::add("noclip", [](const scripting::entity& entity) + { + const auto entref = entity.get_entity_reference(); + const auto* ent = game::GetPlayerEntity(entref); + + if (ent->client->flags & game::gclientFlag::NOCLIP) + { + ent->client->flags &= ~game::gclientFlag::NOCLIP; + } + else + { + ent->client->flags |= game::gclientFlag::NOCLIP; + } + + game::SV_GameSendServerCommand(entref.entnum, 0, utils::string::va("%c \"%s\"", 0x4F, + (ent->client->flags & game::gclientFlag::NOCLIP) ? "GAME_NOCLIPON" : "GAME_NOCLIPOFF")); + }); + + gsc::method::add("ufo", [](const scripting::entity& entity) + { + const auto entref = entity.get_entity_reference(); + const auto* ent = game::GetPlayerEntity(entref); + + if (ent->client->flags & game::gclientFlag::UFO) + { + ent->client->flags &= ~game::gclientFlag::UFO; + } + else + { + ent->client->flags |= game::gclientFlag::UFO; + } + + game::SV_GameSendServerCommand(entref.entnum, 0, utils::string::va("%c \"%s\"", 0x4F, + (ent->client->flags & game::gclientFlag::UFO) ? "GAME_UFOON" : "GAME_UFOOFF")); + }); + + gsc::method::add("god", [](const scripting::entity& entity) + { + const auto entref = entity.get_entity_reference(); + auto* ent = game::GetEntity(entref); + + if (ent->flags & game::entityFlag::FL_GODMODE) + { + ent->flags &= ~game::entityFlag::FL_GODMODE; + } + else + { + ent->flags |= game::entityFlag::FL_GODMODE; + } + + game::SV_GameSendServerCommand(entref.entnum, 0, utils::string::va("%c \"%s\"", 0x4F, + (ent->flags & game::entityFlag::FL_GODMODE) ? "GAME_GODMODE_ON" : "GAME_GODMODE_OFF")); + }); + + gsc::method::add("demigod", [](const scripting::entity& entity) + { + const auto entref = entity.get_entity_reference(); + auto* ent = game::GetEntity(entref); + + if (ent->flags & game::entityFlag::FL_DEMI_GODMODE) + { + ent->flags &= ~game::entityFlag::FL_DEMI_GODMODE; + } + else + { + ent->flags |= game::entityFlag::FL_DEMI_GODMODE; + } + + game::SV_GameSendServerCommand(entref.entnum, 0, utils::string::va("%c \"%s\"", 0x4F, + (ent->flags & game::entityFlag::FL_DEMI_GODMODE) ? "GAME_DEMI_GODMODE_ON" : "GAME_DEMI_GODMODE_OFF")); + }); + + gsc::method::add("notarget", [](const scripting::entity& entity) + { + const auto entref = entity.get_entity_reference(); + auto* ent = game::GetEntity(entref); + + if (ent->flags & game::entityFlag::FL_NOTARGET) + { + ent->flags &= ~game::entityFlag::FL_NOTARGET; + } + else + { + ent->flags |= game::entityFlag::FL_NOTARGET; + } + + game::SV_GameSendServerCommand(entref.entnum, 0, utils::string::va("%c \"%s\"", 0x4F, + (ent->flags & game::entityFlag::FL_NOTARGET) ? "GAME_NOTARGETON" : "GAME_NOTARGETOFF")); + }); + } + }; +} + +REGISTER_COMPONENT(clients::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/command.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/command.cpp new file mode 100644 index 0000000..accfe30 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/command.cpp @@ -0,0 +1,334 @@ +#include "stdinc.hpp" +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "command.hpp" +#include "gsc.hpp" +#include "scheduler.hpp" +#include "scripting.hpp" + +#include +#include +#include + +namespace command +{ + std::unordered_map> handlers; + std::unordered_map> handlers_sv; + + std::vector script_commands; + std::vector script_sv_commands; + utils::memory::allocator allocator; + + utils::hook::detour client_command_hook; + + game::CmdArgs* get_cmd_args() + { + return static_cast(game::Sys_GetValue(4)); + } + + void main_handler() + { + params params = {}; + + const auto command = utils::string::to_lower(params[0]); + + if (handlers.find(command) != handlers.end()) + { + handlers[command](params); + } + } + + params::params() + : nesting_(get_cmd_args()->nesting) + { + } + + int params::size() const + { + const auto cmd_args = get_cmd_args(); + return cmd_args->argc[this->nesting_]; + } + + const char* params::get(int index) const + { + if (index >= this->size()) + { + return ""; + } + + const auto cmd_args = get_cmd_args(); + return cmd_args->argv[this->nesting_][index]; + } + + std::string params::join(int index) const + { + std::string result = {}; + + for (int i = index; i < this->size(); i++) + { + if (i > index) + { + result.append(" "); + } + + result.append(this->get(i)); + } + + return result; + } + + params_sv::params_sv() + : nesting_(game::sv_cmd_args->nesting) + { + } + + int params_sv::size() const + { + return game::sv_cmd_args->argc[this->nesting_]; + } + + const char* params_sv::get(const int index) const + { + if (index >= this->size()) + { + return ""; + } + + return game::sv_cmd_args->argv[this->nesting_][index]; + } + + std::string params_sv::join(const int index) const + { + std::string result = {}; + + for (auto i = index; i < this->size(); i++) + { + if (i > index) + { + result.append(" "); + } + + result.append(this->get(i)); + } + + return result; + } + + std::vector params_sv::get_all() const + { + std::vector params; + + for (auto i = 0; i < this->size(); i++) + { + params.emplace_back(this->get(i)); + } + + return params; + } + + void add_raw(const char* name, void (*callback)()) + { + game::Cmd_AddCommandInternal(name, callback, utils::memory::get_allocator()->allocate()); + } + + void add(const char* name, const std::function& callback) + { + const auto command = utils::string::to_lower(name); + + if (handlers.find(command) == handlers.end()) + { + add_raw(name, main_handler); + } + + handlers[command] = callback; + } + + void add_sv(const std::string& name, const std::function& callback) + { + const auto command = utils::string::to_lower(name); + if (handlers_sv.find(command) == handlers_sv.end()) + { + handlers_sv[command] = callback; + } + } + + void add_script_command(const std::string& name, const std::function& callback) + { + script_commands.push_back(name); + add(allocator.duplicate_string(name), callback); + } + + void add_script_sv_command(const std::string& name, const std::function& callback) + { + script_sv_commands.push_back(name); + add_sv(name, callback); + } + + void clear_script_commands() + { + for (const auto& name : script_commands) + { + handlers.erase(name); + game::Cmd_RemoveCommand(name.data()); + } + + for (const auto& name : script_sv_commands) + { + handlers_sv.erase(name); + } + + allocator.clear(); + script_commands.clear(); + script_sv_commands.clear(); + } + + void execute(std::string command, const bool sync) + { + command += "\n"; + + if (sync) + { + game::Cmd_ExecuteSingleCommand(0, 0, command.data()); + } + else + { + game::Cbuf_AddText(0, command.data()); + } + } + + void client_command_stub(const int client_num) + { + params_sv params = {}; + + const auto command = utils::string::to_lower(params[0]); + if (handlers_sv.find(command) != handlers_sv.end()) + { + handlers_sv[command](client_num, params); + return; + } + + client_command_hook.invoke(client_num); + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + scripting::on_shutdown(clear_script_commands); + + client_command_hook.create(SELECT(0x47E590, 0x4490C0), client_command_stub); + + add("notifylevel", [](const params& params) + { + if (params.size() < 2) + { + printf("Usage: notifylevel [...]\n"); + return; + } + + if (!*game::levelEntityId) + { + printf("Level not loaded\n"); + return; + } + + const auto argc = params.size(); + + for (auto i = argc - 1; i > 1; i--) + { + const auto arg = params[i]; + scripting::push_value(arg); + } + + const auto name = game::SL_GetString(params[1], 0); + game::Scr_NotifyId(game::SCRIPTINSTANCE_SERVER, 0, *game::levelEntityId, name, argc - 2); + }); + + add("notifynum", [](const params& params) + { + if (params.size() < 3) + { + printf("Usage: notifynum [...]\n"); + return; + } + + if (!*game::levelEntityId) + { + printf("Level not loaded\n"); + return; + } + + const auto client = atoi(params[1]); + const auto sv_maxclients = game::Dvar_FindVar("sv_maxclients")->current.integer; + + if (client < 0 || client >= sv_maxclients) + { + return; + } + + if (game::g_entities[client].client == nullptr) + { + return; + } + + const auto argc = params.size(); + + for (auto i = argc - 1; i > 2; i--) + { + const auto arg = params[i]; + scripting::push_value(arg); + } + + const auto name = game::SL_GetString(params[2], 0); + game::Scr_NotifyNum(client, 0, name, argc - 3); + }); + + gsc::function::add_multiple([](const std::string& cmd) + { + execute(cmd); + }, "executecommand", "command::execute"); + + gsc::function::add_multiple([](const std::string& name, const scripting::function& function) + { + add_script_command(name, [function](const params& params) + { + scripting::array array; + + for (auto i = 0; i < params.size(); i++) + { + array.emplace_back(params[i]); + } + + function({array.get_raw()}); + }); + }, "addcommand", "command::add"); + + gsc::function::add_multiple([](const std::string& name, const scripting::function& function) + { + add_script_sv_command(name, [function](const int client_num, const params_sv& params) + { + const auto params_ = params.get_all(); + scheduler::once([=]() + { + const scripting::entity player = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, client_num, 0, 0); + + scripting::array array; + + for (auto i = 0; i < params.size(); i++) + { + array.emplace_back(params[i]); + } + + function(player, {array}); + }, scheduler::pipeline::server); + }); + }, "addclientcommand", "command::add_sv"); + + } + }; +} + +REGISTER_COMPONENT(command::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/command.hpp b/t6/plugins/tws-t6-gsc-utils/src/component/command.hpp new file mode 100644 index 0000000..ff2f9b3 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/command.hpp @@ -0,0 +1,51 @@ +#pragma once + +namespace command +{ + class params + { + public: + params(); + + [[nodiscard]] int size() const; + [[nodiscard]] const char* get(int index) const; + [[nodiscard]] std::string join(int index) const; + + [[nodiscard]] const char* operator[](const int index) const + { + return this->get(index); + } + + private: + int nesting_; + }; + + class params_sv + { + public: + params_sv(); + + [[nodiscard]] int size() const; + [[nodiscard]] const char* get(int index) const; + [[nodiscard]] std::string join(int index) const; + [[nodiscard]] std::vector get_all() const; + + [[nodiscard]] const char* operator[](const int index) const + { + return this->get(index); + } + + private: + int nesting_; + }; + + void add_raw(const char* name, void (*callback)()); + void add(const char* name, const std::function& callback); + + void add_sv(const std::string& name, const std::function& callback); + + void add_script_command(const std::string& name, const std::function& callback); + void clear_script_commands(); + + void execute(std::string command, bool sync = false); +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/config_strings.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/config_strings.cpp new file mode 100644 index 0000000..eaf58cf --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/config_strings.cpp @@ -0,0 +1,151 @@ +#include +#include "loader/component_loader.hpp" + +#include "gsc.hpp" +#include "scripting.hpp" + +#include + +namespace config_strings +{ + namespace + { + std::unordered_set reserved_config_strings; + std::unordered_map hudelem_config_strings; + + bool is_string_reserved(const std::uint32_t offset) + { + return reserved_config_strings.contains(offset); + } + + std::uint32_t get_config_string() + { + auto index = -1; + const auto scr_const_ = *reinterpret_cast(SELECT(0x24E5588, 0x24B7188)); + for (auto i = 1; i < 512; i++) + { + const auto string_value = game::sv_configstrings[i + 488]; + if (string_value == scr_const_ && !is_string_reserved(i)) + { + index = i; + break; + } + } + + if (index == -1) + { + throw std::runtime_error("out of config strings"); + } + + reserved_config_strings.insert(index); + + return index; + } + + utils::hook::detour scr_free_hud_elem_hook; + void scr_free_hud_elem_stub(game::game_hudelem_s* hud) + { + const auto _0 = gsl::finally([&] + { + scr_free_hud_elem_hook.invoke(hud); + }); + + const auto iter = hudelem_config_strings.find(hud); + if (iter == hudelem_config_strings.end()) + { + return; + } + + game::SV_SetConfigString(iter->second + 488, 0); + reserved_config_strings.erase(iter->second); + hudelem_config_strings.erase(iter); + } + + void g_localized_string_index_stub_1(utils::hook::assembler& a) + { + const auto not_reserved = a.newLabel(); + + a.add(esp, 4); + + a.pushad(); + a.push(esi); + a.call(is_string_reserved); + a.cmp(al, 0); + a.jz(not_reserved); + + a.pop(esi); + a.popad(); + a.jmp(SELECT(0x6D657E, 0x467D6E)); + + a.bind(not_reserved); + a.pop(esi); + a.popad(); + + a.cmp(eax, ecx); + a.jmp(SELECT(0x6D6578, 0x467D68)); + } + + void g_localized_string_index_stub_2(utils::hook::assembler& a) + { + const auto not_reserved = a.newLabel(); + + a.add(esp, 4); + + a.pushad(); + a.push(esi); + a.call(is_string_reserved); + a.cmp(al, 0); + a.jz(not_reserved); + + a.pop(esi); + a.popad(); + a.jmp(SELECT(0x6D6638, 0x467E2E)); + + a.bind(not_reserved); + a.pop(esi); + a.popad(); + + a.cmp(eax, ecx); + a.jmp(SELECT(0x6D663E, 0x467E28)); + } + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + utils::hook::jump(SELECT(0x6D6573, 0x467D63), utils::hook::assemble(g_localized_string_index_stub_1)); + utils::hook::jump(SELECT(0x6D6633, 0x467E23), utils::hook::assemble(g_localized_string_index_stub_2)); + scr_free_hud_elem_hook.create(SELECT(0x56E800, 0x6186A0), scr_free_hud_elem_stub); + + scripting::on_shutdown([&] + { + reserved_config_strings.clear(); + hudelem_config_strings.clear(); + }); + + gsc::method::add("hudelem::set_text", [](const scripting::entity& entity, const std::string& text) + { + const auto ent = entity.get_entity_reference(); + const auto hudelem = &game::g_hudelems[ent.entnum]; + const auto iter = hudelem_config_strings.find(hudelem); + + if (iter == hudelem_config_strings.end()) + { + const auto index = get_config_string(); + hudelem->elem.text = static_cast(index); + hudelem_config_strings.insert(std::make_pair(hudelem, index)); + } + else + { + hudelem->elem.text = static_cast(iter->second); + } + + game::SV_SetConfigString(hudelem->elem.text + 488, text.data()); + }); + } + }; +} + +REGISTER_COMPONENT(config_strings::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/cryptography.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/cryptography.cpp new file mode 100644 index 0000000..3631db2 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/cryptography.cpp @@ -0,0 +1,135 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "gsc.hpp" +#include "io.hpp" +#include "scheduler.hpp" +#include "scripting.hpp" + +#include +#include +#include +#include + +namespace cryptography +{ + namespace + { + std::vector keys; + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + scripting::on_shutdown([] + { + keys.clear(); + }); + + gsc::function::add("ecc::generate_key", [](int bits) + { + const auto key = utils::cryptography::ecc::generate_key(bits); + keys.emplace_back(key); + return keys.size() - 1; + }); + + gsc::function::add("ecc::create_key_from_public_key", [](const std::string& data) + { + utils::cryptography::ecc::key key; + key.set(utils::string::hex_to_bin(data)); + keys.emplace_back(key); + return keys.size() - 1; + }); + + gsc::function::add("ecc::encrypt", [](const std::uint32_t key_index, const std::string& data) + { + if (key_index < 0 || key_index >= keys.size()) + { + throw std::runtime_error("invalid key"); + } + + const auto& key = keys.at(key_index); + std::string encrypted = data; + if (!utils::cryptography::ecc::encrypt(key, encrypted)) + { + throw std::runtime_error("failed to encrypt"); + } + + return utils::string::bin_to_hex(encrypted); + }); + + gsc::function::add("ecc::decrypt", [](const std::uint32_t key_index, const std::string& data) + { + if (key_index < 0 || key_index >= keys.size()) + { + throw std::runtime_error("invalid key"); + } + + const auto& key = keys.at(key_index); + std::string decrypted = utils::string::hex_to_bin(data); + if (!utils::cryptography::ecc::decrypt(key, decrypted)) + { + throw std::runtime_error("failed to decrypt"); + } + + return decrypted; + }); + + gsc::function::add("ecc::import_key", [](const std::string& data) + { + utils::cryptography::ecc::key key; + key.deserialize(utils::string::hex_to_bin(data)); + keys.emplace_back(key); + return keys.size() - 1; + }); + + gsc::function::add("ecc::export_public_key", [](const std::uint32_t key_index) + { + if (key_index < 0 || key_index >= keys.size()) + { + throw std::runtime_error("invalid key"); + } + + const auto& key = keys.at(key_index); + return utils::string::bin_to_hex(key.serialize(PK_PUBLIC)); + }); + + gsc::function::add("ecc::export_private_key", [](const std::uint32_t key_index) + { + if (key_index < 0 || key_index >= keys.size()) + { + throw std::runtime_error("invalid key"); + } + + const auto& key = keys.at(key_index); + return utils::string::bin_to_hex(key.serialize(PK_PRIVATE)); + }); + + gsc::function::add("aes::encrypt", [](const std::string& data, const std::string& iv, const std::string& key) + { + return utils::string::bin_to_hex(utils::cryptography::aes::encrypt(data, + utils::string::hex_to_bin(iv), + utils::string::hex_to_bin(key)) + ); + }); + + gsc::function::add("aes::decrypt", [](const std::string& data, const std::string& iv, const std::string& key) + { + return utils::cryptography::aes::decrypt(utils::string::hex_to_bin(data), + utils::string::hex_to_bin(iv), + utils::string::hex_to_bin(key)); + }); + + gsc::function::add("cryptography::random_bytes", [](const int len) + { + return utils::string::bin_to_hex(utils::cryptography::random::get_data(len)); + }); + } + }; +} + +REGISTER_COMPONENT(cryptography::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/exception.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/exception.cpp new file mode 100644 index 0000000..cdcc65f --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/exception.cpp @@ -0,0 +1,145 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include "scheduler.hpp" + +#include "gsc.hpp" +#include "json.hpp" +#include "scripting.hpp" +#include "game/scripting/execution.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace exception +{ + namespace + { + thread_local struct + { + DWORD code = 0; + PVOID address = nullptr; + } exception_data; + + void show_mouse_cursor() + { + while (ShowCursor(TRUE) < 0); + } + + void display_error_dialog() + { + std::string error_str = utils::string::va("Fatal error (0x%08X) at 0x%p.\n" + "A minidump has been written.\n\n", + exception_data.code, exception_data.address); + + error_str += "Make sure to update your graphics card drivers and install operating system updates!"; + + utils::thread::suspend_other_threads(); + show_mouse_cursor(); + MessageBoxA(nullptr, error_str.data(), "Plutonium T6 ERROR", MB_ICONERROR); + TerminateProcess(GetCurrentProcess(), exception_data.code); + } + + void reset_state() + { + display_error_dialog(); + } + + size_t get_reset_state_stub() + { + static auto* stub = utils::hook::assemble([](utils::hook::assembler& a) + { + a.sub(esp, 0x10); + a.or_(esp, 0x8); + a.jmp(reset_state); + }); + + return reinterpret_cast(stub); + } + + std::string generate_crash_info(const LPEXCEPTION_POINTERS exceptioninfo) + { + std::string info{}; + const auto line = [&info](const std::string& text) + { + info.append(text); + info.append("\r\n"); + }; + + line("Plutonium T6 Crash Dump"); + line(""); + line("Timestamp: "s + utils::string::get_timestamp()); + line(utils::string::va("Exception: 0x%08X", exceptioninfo->ExceptionRecord->ExceptionCode)); + line(utils::string::va("Address: 0x%lX", exceptioninfo->ExceptionRecord->ExceptionAddress)); + +#pragma warning(push) +#pragma warning(disable: 4996) + OSVERSIONINFOEXA version_info; + ZeroMemory(&version_info, sizeof(version_info)); + version_info.dwOSVersionInfoSize = sizeof(version_info); + GetVersionExA(reinterpret_cast(&version_info)); +#pragma warning(pop) + + line(utils::string::va("OS Version: %u.%u", version_info.dwMajorVersion, version_info.dwMinorVersion)); + + return info; + } + + void write_minidump(const LPEXCEPTION_POINTERS exceptioninfo) + { + const std::string crash_name = utils::string::va("minidumps/plutonium-t6-crash-%s.zip", + utils::string::get_timestamp().data()); + + utils::compression::zip::archive zip_file{}; + zip_file.add("crash.dmp", create_minidump(exceptioninfo)); + zip_file.add("info.txt", generate_crash_info(exceptioninfo)); + zip_file.write(crash_name, "Plutonium T6 Crash Dump"); + } + + bool is_harmless_error(const LPEXCEPTION_POINTERS exceptioninfo) + { + const auto code = exceptioninfo->ExceptionRecord->ExceptionCode; + return code == STATUS_INTEGER_OVERFLOW || code == STATUS_FLOAT_OVERFLOW || code == STATUS_SINGLE_STEP; + } + + LONG WINAPI exception_filter(const LPEXCEPTION_POINTERS exceptioninfo) + { + if (is_harmless_error(exceptioninfo)) + { + return EXCEPTION_CONTINUE_EXECUTION; + } + + write_minidump(exceptioninfo); + + exception_data.code = exceptioninfo->ExceptionRecord->ExceptionCode; + exception_data.address = exceptioninfo->ExceptionRecord->ExceptionAddress; + exceptioninfo->ContextRecord->Eip = get_reset_state_stub(); + + return EXCEPTION_CONTINUE_EXECUTION; + } + + LPTOP_LEVEL_EXCEPTION_FILTER WINAPI set_unhandled_exception_filter_stub(LPTOP_LEVEL_EXCEPTION_FILTER) + { + // Don't register anything here... + return &exception_filter; + } + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + SetUnhandledExceptionFilter(exception_filter); + utils::hook::jump(reinterpret_cast(&SetUnhandledExceptionFilter), set_unhandled_exception_filter_stub); + } + }; +} + +REGISTER_COMPONENT(exception::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/gsc.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/gsc.cpp new file mode 100644 index 0000000..06ff7de --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/gsc.cpp @@ -0,0 +1,585 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/structs.hpp" +#include "game/game.hpp" + +#include "gsc.hpp" +#include "json.hpp" + +#include "scheduler.hpp" +#include "scripting.hpp" +#include "command.hpp" + +#include +#include + +namespace gsc +{ + std::unordered_map functions; + std::unordered_map methods; + + std::unordered_map function_wraps; + std::unordered_map method_wraps; + + namespace + { + utils::hook::detour player_get_method_hook; + utils::hook::detour scr_get_common_function_hook; + utils::hook::detour scr_error_internal_hook; + utils::hook::detour scr_post_load_scripts_hook; + + auto field_offset_start = 0xA000; + std::unordered_map custom_fields[class_count]; + + std::string current_namespace; + + std::unordered_map world; + + std::string get_full_name(const std::string& name) + { + if (current_namespace.empty()) + { + return name; + } + + return std::format("{}::{}", current_namespace, name); + } + + void* find_function(const std::string& name) + { + const auto full_name = get_full_name(name); + const auto iter = function_wraps.find(full_name); + if (iter == function_wraps.end()) + { + return nullptr; + } + + return iter->second; + } + + void* find_method(const std::string& name) + { + const auto full_name = get_full_name(name); + const auto iter = method_wraps.find(full_name); + if (iter == method_wraps.end()) + { + return nullptr; + } + + return iter->second; + } + + builtin_function scr_get_common_function(const char** name, int* type, int* min_args, int* max_args) + { + const auto func = reinterpret_cast(find_function(*name)); + + if (func == nullptr) + { + return scr_get_common_function_hook.invoke(name, type, min_args, max_args); + } + + *type = 1; + *min_args = 0; + *max_args = 512; + + return func; + } + + builtin_method player_get_method(const char** name, int* min_args, int* max_args) + { + const auto method = reinterpret_cast(find_method(*name)); + + if (method == nullptr) + { + return player_get_method_hook.invoke(name, min_args, max_args); + } + + *min_args = 0; + *max_args = 512; + + return method; + } + + const gsc::entity_field_t* find_field(unsigned int classnum, unsigned int offset) + { + const auto& class_map = custom_fields[classnum]; + const auto field_iter = class_map.find(offset); + if (field_iter == class_map.end()) + { + return nullptr; + } + + return &field_iter->second; + } + + utils::hook::detour scr_get_object_field_hook; + void scr_get_object_field_stub(unsigned int classnum, int entnum, unsigned int offset) + { + const auto field = find_field(classnum, offset); + if (field == nullptr) + { + return scr_get_object_field_hook.invoke(classnum, entnum, offset); + } + + try + { + const auto result = field->getter(entnum); + return_value(result); + } + catch (const std::exception& e) + { + printf("******* script runtime error *******\n"); + printf("while getting field \"%s\": %s\n", field->name.data(), e.what()); + printf("************************************\n"); + } + } + + utils::hook::detour scr_set_object_field_hook; + int scr_set_object_field_stub(unsigned int classnum, int entnum, unsigned int offset) + { + const auto field = find_field(classnum, offset); + if (field == nullptr) + { + return scr_set_object_field_hook.invoke(classnum, entnum, offset); + } + + const auto args = get_arguments(); + + try + { + field->setter(entnum, args[0]); + } + catch (const std::exception& e) + { + printf("******* script runtime error *******\n"); + printf("while setting field \"%s\": %s\n", field->name.data(), e.what()); + printf("************************************\n"); + } + + return 1; + } + + void scr_post_load_scripts_stub() + { + for (auto i = 0; i < class_id_t::class_count; i++) + { + const auto& class_map = custom_fields[i]; + for (const auto& [offset, field] : class_map) + { + game::Scr_AddClassField(game::SCRIPTINSTANCE_SERVER, i, field.name.data(), offset); + } + } + + return scr_post_load_scripts_hook.invoke(); + } + + __declspec(naked) void scr_error_internal_stub_1_mp() + { + __asm + { + mov eax, 0 + mov edx, eax + imul edx, 0x54 + + push 0x8F3F65 + retn + } + } + + __declspec(naked) void scr_error_internal_stub_1_zm() + { + __asm + { + mov eax, 0 + mov edx, eax + imul edx, 0x54 + + push 0x8F2CC5 + retn + } + } + + void gsc_obj_resolve_stub_1(game::GSC_OBJ* obj, game::GSC_IMPORT_ITEM* item) + { + const auto name = &obj->magic[item->name]; + current_namespace = &obj->magic[item->name_space]; + + if (current_namespace[0] == 0) + { + return; + } + + const auto is_function = find_function(name); + const auto is_method = !is_function && find_method(name); + + if (is_function || is_method) + { + item->name_space += static_cast(current_namespace.size()); + } + } + + __declspec(naked) void gsc_obj_resolve_stub_mp() + { + __asm + { + pushad + push edi + push esi + call gsc_obj_resolve_stub_1 + pop esi + pop edi + popad + + movzx eax, byte ptr[edi + 7] + and eax, 0xF + dec eax + + push 0x6CD7F1 + ret + } + } + + __declspec(naked) void gsc_obj_resolve_stub_zm() + { + __asm + { + pushad + push edi + push esi + call gsc_obj_resolve_stub_1 + pop esi + pop edi + popad + + movzx eax, byte ptr[edi + 7] + and eax, 0xF + dec eax + + push 0x4F0441 + ret + } + } + } + + void call_function(const function_t& function, const std::string& name) + { + const auto args = get_arguments(); + + try + { + const auto value = function(args); + return_value(value); + } + catch (const std::exception& e) + { + printf("******* script runtime error *******\n"); + printf("in call to builtin function \"%s\": %s\n", name.data(), e.what()); + printf("************************************\n"); + } + } + + void call_method(const function_t& method, const std::string& name, const game::scr_entref_t entref) + { + const auto args = get_arguments(); + + try + { + const scripting::entity entity = game::Scr_GetEntityId( + game::SCRIPTINSTANCE_SERVER, entref.entnum, entref.classnum, 0); + + std::vector next_args{}; + next_args.push_back(entity); + for (const auto& arg : args) + { + next_args.push_back(arg); + } + + const auto value = method(next_args); + return_value(value); + } + catch (const std::exception& e) + { + printf("******* script runtime error *******\n"); + printf("in call to builtin method \"%s\": %s\n", name.data(), e.what()); + printf("************************************\n"); + } + } + + namespace field + { + void add(const class_id_t classnum, const std::string& name, const field_getter_t getter, const field_setter_t& setter) + { + const auto offset = field_offset_start++; + auto& class_map = custom_fields[classnum]; + + entity_field_t field{}; + field.name = name; + field.getter = getter; + field.setter = setter; + class_map.insert(std::make_pair(offset, field)); + } + } + + void return_value(const scripting::script_value& value) + { + if (game::scr_VmPub->outparamcount) + { + game::Scr_ClearOutParams(game::SCRIPTINSTANCE_SERVER); + } + + scripting::push_value(value); + } + + std::vector get_arguments() + { + std::vector args; + + for (auto i = 0; static_cast(i) < game::scr_VmPub->outparamcount; i++) + { + const auto value = game::scr_VmPub->top[-i]; + args.push_back(value); + } + + return args; + } + + std::string find_builtin_name(void* function) + { + for (auto i = scripting::function_map.begin(); i != scripting::function_map.end(); ++i) + { + if (i->second.actionFunc == function) + { + return i->second.actionString; + } + } + + for (auto i = function_wraps.begin(); i != function_wraps.end(); ++i) + { + if (i->second == function) + { + return i->first; + } + } + + return {}; + } + + std::string find_builtin_method_name(void* function) + { + for (auto i = scripting::method_map.begin(); i != scripting::method_map.end(); ++i) + { + if (i->second.actionFunc == function) + { + return i->second.actionString; + } + } + + for (auto i = method_wraps.begin(); i != method_wraps.end(); ++i) + { + if (i->second == function) + { + return i->first; + } + } + + return {}; + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + utils::hook::jump(SELECT(0x6CD7E9, 0x4F0439), SELECT(gsc_obj_resolve_stub_mp, gsc_obj_resolve_stub_zm)); + + scr_get_common_function_hook.create(SELECT(0x4B57B0, 0x4AD040), scr_get_common_function); + player_get_method_hook.create(SELECT(0x432480, 0x6F2DB0), player_get_method); + + scr_get_object_field_hook.create(SELECT(0x573160, 0x4224E0), scr_get_object_field_stub); + scr_set_object_field_hook.create(SELECT(0x5B9820, 0x43F2A0), scr_set_object_field_stub); + scr_post_load_scripts_hook.create(SELECT(0x6B75B0, 0x492440), scr_post_load_scripts_stub); + + utils::hook::jump(SELECT(0x8F3F60, 0x8F2CC0), SELECT(scr_error_internal_stub_1_mp, scr_error_internal_stub_1_zm)); + + field::add(class_id_t::class_entity, "eflags", + [](unsigned int entnum) -> scripting::script_value + { + const auto entity = &game::g_entities[entnum]; + return entity->flags; + }, + [](unsigned int entnum, const scripting::script_value& value) + { + const auto entity = &game::g_entities[entnum]; + entity->flags = value.as(); + } + ); + + field::add(class_id_t::class_entity, "eflags2", + [](unsigned int entnum) -> scripting::script_value + { + const auto entity = &game::g_entities[entnum]; + return entity->eFlags2; + }, + [](unsigned int entnum, const scripting::script_value& value) + { + const auto entity = &game::g_entities[entnum]; + entity->eFlags2 = value.as(); + } + ); + + field::add(class_id_t::class_entity, "clientflags", + [](unsigned int entnum) -> scripting::script_value + { + const auto entity = &game::g_entities[entnum]; + return entity->client->flags; + }, + [](unsigned int entnum, const scripting::script_value& value) + { + const auto entity = &game::g_entities[entnum]; + entity->client->flags = value.as(); + } + ); + + field::add(class_id_t::class_entity, "address", + [](unsigned int entnum) -> scripting::script_value + { + if (entnum >= 18) + { + throw std::runtime_error("not a player entity"); + } + + const auto clients = *game::svs_clients; + const auto client = &clients[entnum]; + const auto address = client->header.netchan.remoteAddress.ip; + + const std::string address_str = utils::string::va("%i.%i.%i.%i", + address[0], + address[1], + address[2], + address[3] + ); + + return address_str; + }, + [](unsigned int, const scripting::script_value&) + { + throw std::runtime_error("cannot set address"); + } + ); + + function::add("getfunctionargcount", [](const scripting::function& function) + { + const auto pos = function.get_pos(); + if (*pos != 0x17) // OP_SafeCreateLocalVariables + { + return 0; + } + + return static_cast(pos[1]); + }); + + function::add("arrayremovekey", [](const scripting::array& array, const std::string& key) + { + array.erase(key); + }); + + function::add("xor", [](const int a, const int b) + { + return a ^ b; + }); + + function::add("not", [](const int a) + { + return ~a; + }); + + function::add("and", [](const int a, const int b) + { + return a & b; + }); + + function::add("or", [](const int a, const int b) + { + return a | b; + }); + + function::add("structget", [](const scripting::object& obj, const std::string& key) + { + return obj[key]; + }); + + function::add("structset", [](const scripting::object& obj, const std::string& key, + const scripting::script_value& value) + { + obj[key] = value; + }); + + function::add("struct::get", [](const scripting::object& obj, const std::string& key) + { + return obj[key]; + }); + + function::add("struct::set", [](const scripting::object& obj, const std::string& key, + const scripting::script_value& value) + { + obj[key] = value; + }); + + function::add("structremove", [](const scripting::object& obj, const std::string& key) + { + obj.erase(key); + }); + + function::add("getstructkeys", [](const scripting::object& obj) + { + return obj.get_keys(); + }); + + function::add("isfunctionptr", [](const scripting::script_value& value) + { + return value.is(); + }); + + function::add("isentity", [](const scripting::script_value& value) + { + const auto& raw = value.get_raw(); + const auto type = game::scr_VarGlob->objectVariableValue[raw.u.uintValue].w.type & 0x7F; + return raw.type == game::SCRIPT_OBJECT && type == game::SCRIPT_ENTITY; + }); + + function::add("isstruct", [](const scripting::script_value& value) + { + return value.is(); + }); + + function::add("typeof", [](const scripting::script_value& value) + { + return value.type_name(); + }); + + method::add("get", [](const scripting::entity& entity, const std::string& field) + { + return entity.get(field); + }); + + method::add("set", [](const scripting::entity& entity, const std::string& field, + const scripting::script_value& value) + { + entity.set(field, value); + }); + + function::add("worldget", [](const std::string& key) + { + return world[key]; + }); + + function::add("worldset", [](const std::string& key, const scripting::script_value& value) + { + world[key] = value; + }); + } + }; +} + +REGISTER_COMPONENT(gsc::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/gsc.hpp b/t6/plugins/tws-t6-gsc-utils/src/component/gsc.hpp new file mode 100644 index 0000000..3d4b848 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/gsc.hpp @@ -0,0 +1,171 @@ +#pragma once + +#include "game/scripting/array.hpp" +#include "game/scripting/execution.hpp" +#include "../plugin.hpp" + +#if _HAS_CXX20 +namespace gsc +{ + using function_t = std::function; + using script_function = void(*)(game::scr_entref_t); + + extern std::unordered_map functions; + extern std::unordered_map methods; + + extern std::unordered_map function_wraps; + extern std::unordered_map method_wraps; + + // auto = []{} forces template reevaluation + + template + auto wrap_function(const std::function& f, std::index_sequence) + { + return [f]([[maybe_unused]] const scripting::function_arguments& args) + { + f(args[I]...); + return scripting::script_value{}; + }; + } + + template + auto wrap_function(const std::function& f, std::index_sequence) + { + return [f]([[maybe_unused]] const scripting::function_arguments& args) + { + return f(args[I]...); + }; + } + + template + auto wrap_function(const std::function& f, std::index_sequence) + { + return [f]([[maybe_unused]] const scripting::function_arguments& args) + { + return scripting::script_value{f(args[I]...)}; + }; + } + + template + auto wrap_function(const std::function& f) + { + return wrap_function(f, std::index_sequence_for{}); + } + + template + auto wrap_function(F f) + { + return wrap_function(std::function(f)); + } + + enum class_id_t + { + class_entity, + class_hudelem, + class_pathnode, + class_node, + class_count + }; + + using field_getter_t = std::function; + using field_setter_t = std::function; + + struct entity_field_t + { + std::string name; + field_getter_t getter; + field_setter_t setter; + }; + + void return_value(const scripting::script_value& value); + std::vector get_arguments(); + + using builtin_function = void(*)(); + using builtin_method = void(*)(game::scr_entref_t); + + std::string find_builtin_name(void* function); + std::string find_builtin_method_name(void* function); + + void call_function(const function_t& function, const std::string& name); + void call_method(const function_t& method, const std::string& name, const game::scr_entref_t entref); + + namespace function + { + template + void add_internal(const std::string& name, F function) + { + static auto called = false; + assert(!called); + called = true; + +#ifdef DEBUG + printf("[function] %s\n", name.data()); +#endif + + static const auto lower = utils::string::to_lower(name); + static const auto [iterator, was_inserted] = functions.insert(std::make_pair(lower, function)); + static const auto& function_ptr = iterator->second; + + function_wraps[lower] = []() + { + call_function(function_ptr, lower); + }; + } + + template + void add(const std::string& name, F f) + { + add_internal(name, wrap_function(f)); + } + + template + void add_multiple(F f, Args&& ...names) + { + (add(names, f), ...); + } + } + + namespace method + { + template + void add_internal(const std::string& name, F function) + { + static auto called = false; + assert(!called); + called = true; + +#ifdef DEBUG + printf("[method] %s\n", name.data()); +#endif + + static const auto lower = utils::string::to_lower(name); + static const auto [iterator, was_inserted] = functions.insert(std::make_pair(lower, function)); + static const auto& function_ptr = iterator->second; + + method_wraps[lower] = [](game::scr_entref_t entref) + { + call_method(function_ptr, lower, entref); + }; + } + + template + void add(const std::string& name, F f) + { + add_internal(name, wrap_function(f)); + } + + template + void add_multiple(F f, Args&& ...names) + { + (add(names, f), ...); + } + } + + namespace field + { + void add(const class_id_t classnum, const std::string& name, const field_getter_t getter, const field_setter_t& setter); + } +} +#else +#error pre c++20 is not supported +#endif diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/http.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/http.cpp new file mode 100644 index 0000000..cf91384 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/http.cpp @@ -0,0 +1,287 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/structs.hpp" +#include "game/game.hpp" + +#include "gsc.hpp" +#include "scheduler.hpp" +#include "scripting.hpp" + +#include +#include +#include + +namespace http +{ + namespace + { + constexpr const auto max_result_size = 0x5000u; + + struct http_request_params_t + { + std::string method; + std::string url; + std::string fields; + utils::http::headers headers; + utils::http::timeouts limits; + }; + + struct http_request_t + { + http_request_params_t params; + scripting::object handle; + std::optional result; + std::chrono::steady_clock::time_point queued_at = std::chrono::steady_clock::now(); + std::chrono::steady_clock::time_point started_at; + std::chrono::steady_clock::time_point finished_at; + std::atomic_bool completed{false}; + }; + + std::vector> requests; + + double elapsed_ms(const std::chrono::steady_clock::time_point start, + const std::chrono::steady_clock::time_point end) + { + return std::chrono::duration(end - start).count(); + } + + void notify_request_result(std::shared_ptr& request) + { + const auto handle_id = request->handle.get_entity_id(); + const utils::http::result missing; + const auto& result = request->result ? *request->result : missing; + scripting::array diagnostics; + diagnostics["http_status"] = static_cast(result.status); + diagnostics["curl_code"] = static_cast(result.code); + diagnostics["dns_ms"] = result.dns_ms; + diagnostics["connect_ms"] = result.connect_ms; + diagnostics["tls_ms"] = result.tls_ms; + diagnostics["first_byte_ms"] = result.first_byte_ms; + diagnostics["total_ms"] = result.total_ms; + diagnostics["worker_queue_ms"] = elapsed_ms(request->queued_at, request->started_at); + diagnostics["notify_ms"] = elapsed_ms(request->finished_at, std::chrono::steady_clock::now()); + diagnostics["connect_timeout_ms"] = static_cast(request->params.limits.connect_timeout_ms); + diagnostics["timeout_ms"] = static_cast(request->params.limits.timeout_ms); + + if (!request->result.has_value()) + { + scripting::notify(handle_id, "done", {{}, false, "unknown error", diagnostics}); + return; + } + + const auto error = curl_easy_strerror(result.code); + + if (result.code != CURLE_OK) + { + scripting::notify(handle_id, "done", {{}, false, error, diagnostics}); + return; + } + + auto& buffer = request->result->buffer; + if (buffer.size() >= max_result_size) + { + printf("^3WARNING: http result size bigger than %i bytes (%i), truncating!", max_result_size, + static_cast(buffer.size())); + buffer.resize(max_result_size); + } + + // Keep the original three values; older waiters ignore the fourth. + // HTTP 4xx/5xx are completed transfers, not transport failures. + scripting::notify(handle_id, "done", {buffer, true, {}, diagnostics}); + } + + void check_requests() + { + std::vector> ready; + for (auto i = requests.begin(); i != requests.end(); ) + { + auto& request = *i; + if (!request->completed) + { + ++i; + continue; + } + else + { + ready.emplace_back(request); + i = requests.erase(i); + } + } + // A resumed script may enqueue another request. Do not hold an iterator + // into requests while notifying the VM. + for (auto& request : ready) + notify_request_result(request); + } + + scripting::object create_request(const http_request_params_t& params) + { + const auto request = std::make_shared(); + requests.emplace_back(request); + + request->params = params; + scheduler::thread_pool.push([request] + { + request->started_at = std::chrono::steady_clock::now(); + try + { + request->result = utils::http::get_data( + request->params.url, + request->params.fields, + request->params.headers, + request->params.method, + request->params.limits); + } + catch (...) + { + // Exceptions must also release GSC waiters through the done event. + request->result = std::nullopt; + } + request->finished_at = std::chrono::steady_clock::now(); + request->completed = true; + }); + + return request->handle; + } + + template + void push_request(T&& r) + { + const auto request = std::make_shared(std::forward(r)); + requests.emplace_back(request); + } + + void wait_and_clear_requests() + { + for (auto& task : requests) + { + while (!task->completed) + { + std::this_thread::sleep_for(10ms); + } + } + + requests.clear(); + } + + long timeout_option(const scripting::array& options, const char* name, const long fallback) + { + const auto value = options[name]; + if (value.get_raw().type == game::SCRIPT_NONE) + return fallback; + if (!value.is() || value.as() <= 0) + throw std::runtime_error(std::string(name) + " must be a positive integer in milliseconds"); + return value.as(); + } + + void parse_request_options(http_request_params_t& params, const scripting::array& options) + { + params.limits.connect_timeout_ms = timeout_option(options, "connect_timeout_ms", params.limits.connect_timeout_ms); + params.limits.timeout_ms = timeout_option(options, "timeout_ms", params.limits.timeout_ms); + const auto fields = options["parameters"]; + const auto body = options["body"]; + const auto headers = options["headers"]; + const auto method = options["method"]; + + if (method.is()) + { + params.method = method.as(); + } + + if (fields.is()) + { + const auto fields_ = fields.as(); + const auto keys = fields_.get_keys(); + + for (const auto& key : keys) + { + if (!key.is()) + { + continue; + } + + const auto key_ = key.as(); + const auto value = fields_[key].to_string(); + params.fields += key_ + "=" + value + "&"; + } + + } + else if (body.is()) + { + params.fields = body.as(); + } + + if (headers.is()) + { + const auto headers_arr = headers.as(); + const auto keys = headers_arr.get_keys(); + + for (const auto& key : keys) + { + if (!key.is()) + { + continue; + } + + const auto key_str = key.as(); + const auto value = headers_arr[key].to_string(); + + params.headers[key_str] = value; + } + } + } + } + + class component final : public component_interface + { + public: + void on_shutdown([[maybe_unused]] plugin::plugin* plugin) override + { + scripting::on_shutdown(wait_and_clear_requests); + } + + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + scheduler::loop(check_requests, scheduler::server); + scripting::on_shutdown(wait_and_clear_requests); + + gsc::function::add_multiple([](const std::string& url) + { + http_request_params_t params{}; + params.url = url; + return create_request(params); + }, "http::get", "httpget", "curl"); + + gsc::function::add("http::request", [](const std::string& url, const scripting::variadic_args& va) + { + http_request_params_t params{}; + params.url = url; + + if (va.size() > 0) + { + const auto options = va[0].as(); + parse_request_options(params, options); + } + + return create_request(params); + }); + + gsc::function::add_multiple([](const std::string& url, const scripting::variadic_args& va) + { + http_request_params_t params{}; + params.url = url; + params.method = "POST"; + + if (va.size() > 0) + { + const auto options = va[0].as(); + parse_request_options(params, options); + } + + return create_request(params); + }, "httppost", "http::post"); + } + }; +} + +REGISTER_COMPONENT(http::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/int64.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/int64.cpp new file mode 100644 index 0000000..dcf21dc --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/int64.cpp @@ -0,0 +1,119 @@ +#include +#include "loader/component_loader.hpp" + +#include "gsc.hpp" + +#include +#include + +#define INT64_OPERATION(expr) \ + [](const std::int64_t a, [[maybe_unused]] const std::int64_t b) \ + { \ + return expr; \ + } \ + +namespace int64 +{ + namespace + { + std::int64_t get_int64_arg(const scripting::variadic_args& args, const size_t index, bool optional) + { + if (optional && index >= static_cast(args.size())) + { + return 0; + } + + if (args[index].is()) + { + return static_cast(args[index].as()); + } + + if (args[index].is()) + { + return std::strtoll(args[index].as(), nullptr, 0); + } + + throw std::runtime_error(std::format("parameter {} does not have type 'string' or 'int'", index)); + } + + std::unordered_map> operations = + { + {"+", INT64_OPERATION(a + b)}, + {"-", INT64_OPERATION(a - b)}, + {"*", INT64_OPERATION(a * b)}, + {"/", INT64_OPERATION(a / b)}, + {"&", INT64_OPERATION(a & b)}, + {"^", INT64_OPERATION(a ^ b)}, + {"|", INT64_OPERATION(a | b)}, + {"~", INT64_OPERATION(~a)}, + {"%", INT64_OPERATION(a % b)}, + {">>", INT64_OPERATION(a >> b)}, + {"<<", INT64_OPERATION(a << b)}, + {"++", INT64_OPERATION(a + 1)}, + {"--", INT64_OPERATION(a - 1)}, + }; + + std::unordered_map> comparisons = + { + {">", INT64_OPERATION(a > b)}, + {">=", INT64_OPERATION(a >= b)}, + {"==", INT64_OPERATION(a == b)}, + {"!=", INT64_OPERATION(a != b)}, + {"<=", INT64_OPERATION(a <= b)}, + {"<", INT64_OPERATION(a < b)}, + }; + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + gsc::function::add_multiple([](const scripting::variadic_args& args) + { + auto value = get_int64_arg(args, 0, false); + return value <= std::numeric_limits::max() && value >= std::numeric_limits::min(); + }, "int64_is_int", "int64::is_int"); + + gsc::function::add_multiple([](const scripting::variadic_args& args) + { + return static_cast(get_int64_arg(args, 0, false)); + }, "int64_to_int", "int64::to_int"); + + gsc::function::add_multiple([](const scripting::variadic_args& args) -> scripting::script_value + { + auto a = get_int64_arg(args, 0, false); + const auto op = args[1].as(); + auto b = get_int64_arg(args, 2, true); + + if (const auto iter = operations.find(op); iter != operations.end()) + { + return std::to_string(iter->second(a, b)); + } + + if (const auto iter = comparisons.find(op); iter != comparisons.end()) + { + return iter->second(a, b); + } + + throw std::runtime_error("invalid int64 operation"); + }, "int64_op", "int64::op"); + + gsc::function::add_multiple([](const scripting::variadic_args& args) + { + const auto a = get_int64_arg(args, 0, false); + const auto b = get_int64_arg(args, 1, false); + return std::to_string(std::min(a, b)); + }, "int64_min", "int64::min"); + + gsc::function::add_multiple([](const scripting::variadic_args& args) + { + const auto a = get_int64_arg(args, 0, false); + const auto b = get_int64_arg(args, 1, false); + return std::to_string(std::max(a, b)); + }, "int64_max", "int64::max"); + } + }; +} + +REGISTER_COMPONENT(int64::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/io.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/io.cpp new file mode 100644 index 0000000..91c756e --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/io.cpp @@ -0,0 +1,133 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "gsc.hpp" +#include "io.hpp" +#include "scheduler.hpp" +#include "scripting.hpp" + +#include +#include +#include +#include +#include + +namespace io +{ + namespace + { + struct watched_file + { + std::unique_ptr watcher; + scripting::object handle; + }; + + std::vector watched_files; + + void poll_files() + { + for (auto& file : watched_files) + { + std::string data; + if (file.watcher->run(1ms, &data, true)) + { + scripting::notify(file.handle.get_entity_id(), "data", {data}); + } + } + } + } + + class component final : public component_interface + { + public: + void on_after_dvar_init([[maybe_unused]] plugin::plugin* plugin) override + { + const auto fs_homepath = game::Dvar_FindVar("fs_homepath"); + if (fs_homepath == nullptr) + { + return; + } + + std::filesystem::current_path(fs_homepath->current.string); + printf("working directory: %s", fs_homepath->current.string); + } + + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + gsc::function::add("hashstring", [](const char* str) + { + return game::BG_StringHashValue(str); + }); + + gsc::function::add_multiple([](const std::string& file, const std::string& data, + const scripting::variadic_args& va) + { + auto append = false; + + if (va.size() > 0) + { + append = va[0]; + } + + return utils::io::write_file(file, data, append); + }, "writefile", "io::write_file"); + + gsc::function::add_multiple([](const std::string& file, const std::string& data) + { + return utils::io::write_file(file, data, true); + }, "appendfile", "io::append_file"); + + gsc::function::add_multiple(utils::io::file_exists, "fileexists", "io::file_exists"); + gsc::function::add_multiple(utils::io::move_file, "movefile", "io::move_file"); + gsc::function::add_multiple(utils::io::file_size, "filesize", "io::file_size"); + gsc::function::add_multiple(utils::io::create_directory, "createdirectory", "io::create_directory"); + gsc::function::add_multiple(utils::io::directory_exists, "directoryexists", "io::directory_exists"); + gsc::function::add_multiple(utils::io::directory_is_empty, "directoryisempty", "io::directory_is_empty"); + gsc::function::add_multiple(utils::io::list_files, "listfiles", "io::list_files"); + gsc::function::add_multiple(utils::io::remove_file, "removefile", "io::remove_file"); + + gsc::function::add_multiple([](const std::string& directory, const scripting::variadic_args& va) + { + auto recursive = false; + if (va.size() > 0) + { + recursive = va[0]; + } + + utils::io::remove_directory(directory, recursive); + }, "removedirectory", "io::remove_directory"); + + gsc::function::add_multiple(utils::io::copy_folder, "copyfolder", "io::copy_folder"); + gsc::function::add_multiple(utils::io::copy_folder, "copydirectory", "io::copy_directory"); + gsc::function::add_multiple(static_cast(utils::io::read_file), "readfile", "io::read_file"); + + if (utils::flags::has_flag("experimental-utils")) + { + scheduler::loop(poll_files, scheduler::server_packet_loop); + + scripting::on_shutdown([]() + { + watched_files.clear(); + }); + + gsc::function::add("io::watch_file", [](const std::string& file) + { + scripting::object handle; + + auto watcher = std::make_unique(file); + watcher->init(); + watched_file watched_file{}; + watched_file.watcher = std::move(watcher); + watched_file.handle = handle; + watched_files.emplace_back(std::move(watched_file)); + + return handle; + }); + } + } + }; +} + +REGISTER_COMPONENT(io::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/io.hpp b/t6/plugins/tws-t6-gsc-utils/src/component/io.hpp new file mode 100644 index 0000000..cf90d51 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/io.hpp @@ -0,0 +1,6 @@ +#include + +namespace io +{ + +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/json.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/json.cpp new file mode 100644 index 0000000..83ddda6 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/json.cpp @@ -0,0 +1,319 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/scripting/event.hpp" +#include "game/scripting/execution.hpp" +#include "game/scripting/array.hpp" + +#include "gsc.hpp" +#include "json.hpp" +#include "scripting.hpp" + +#include + +namespace json +{ + namespace + { + constexpr const auto max_str_len = 0x5000u; + + struct dump_context_t + { + std::unordered_set visited_objects; + }; + + nlohmann::json gsc_to_json(const scripting::script_value& value, bool print_id, dump_context_t& ctx); + + nlohmann::json array_to_json(const scripting::array& array, bool print_id, dump_context_t& ctx) + { + if (array.size() == 0) + { + return nlohmann::json::array(); + } + + nlohmann::json obj; + + auto string_indexed = -1; + const auto keys = array.get_keys(); + + for (const auto& key : keys) + { + const auto is_int = key.is(); + const auto is_string = key.is(); + + if (string_indexed == -1) + { + string_indexed = is_string; + } + + if (!string_indexed && is_int) + { + const auto index = key.as(); + obj[index] = gsc_to_json(array[index], print_id, ctx); + } + else if (string_indexed && is_string) + { + const auto key_str = key.as(); + obj.emplace(key_str, gsc_to_json(array[key_str], print_id, ctx)); + } + } + + return obj; + } + + nlohmann::json object_to_json(const scripting::object& object, bool print_id, dump_context_t& ctx) + { + const auto id = object.get_entity_id(); + if (ctx.visited_objects.find(id) != ctx.visited_objects.end()) + { + return utils::string::va("[struct reference %i]", id); + } + + ctx.visited_objects.insert(id); + auto obj = nlohmann::json::object(); + + if (print_id) + { + obj["__id"] = id; + } + + const auto keys = object.get_keys(); + for (const auto& key : keys) + { + if (key == "__id") + { + continue; + } + + obj.emplace(key, gsc_to_json(object[key], print_id, ctx)); + } + + return obj; + } + + nlohmann::json vector_to_array(const float* value) + { + nlohmann::json obj; + obj.push_back(value[0]); + obj.push_back(value[1]); + obj.push_back(value[2]); + return obj; + } + + nlohmann::json gsc_to_json(const scripting::script_value& value, bool print_id, dump_context_t& ctx) + { + const auto& variable = value.get_raw(); + + if (value.is()) + { + return value.as(); + } + + if (value.is()) + { + return value.as(); + } + + if (value.is()) + { + return value.as(); + } + + if (value.is()) + { + return vector_to_array(variable.u.vectorValue); + } + + if (value.is()) + { + return object_to_json(variable.u.uintValue, print_id, ctx); + } + + if (value.is()) + { + return array_to_json(variable.u.uintValue, print_id, ctx); + } + + if (value.is()) + { + return object_to_json(variable.u.uintValue, print_id, ctx); + } + + if (value.is()) + { + const auto function = value.as(); + return utils::string::va("[[ %s ]]", function.get_name().data()); + } + + if (variable.type == game::SCRIPT_NONE) + { + return {}; + } + + if (variable.type == game::SCRIPT_CODEPOS) + { + const auto function = scripting::find_function(variable.u.codePosValue); + return utils::string::va("[codepos: %s]", function.data()); + } + + if (variable.type == game::SCRIPT_END) + { + const auto function = scripting::find_function(variable.u.codePosValue); + return utils::string::va("[precodepos: %s]", function.data()); + } + + return utils::string::va("[%s]", value.type_name().data()); + } + + scripting::script_value json_to_gsc(nlohmann::json obj) + { + const auto type = obj.type(); + + switch (type) + { + case (nlohmann::detail::value_t::boolean): + return obj.get(); + case (nlohmann::detail::value_t::number_integer): + case (nlohmann::detail::value_t::number_unsigned): + return obj.get(); + case (nlohmann::detail::value_t::number_float): + return obj.get(); + case (nlohmann::detail::value_t::string): + return obj.get(); + case (nlohmann::detail::value_t::array): + { + scripting::array array; + + for (const auto& [key, value] : obj.items()) + { + array.emplace_back(json_to_gsc(value)); + } + + return array.get_raw(); + } + case (nlohmann::detail::value_t::object): + { + scripting::array array; + + for (const auto& [key, value] : obj.items()) + { + array[key] = json_to_gsc(value); + } + + return array.get_raw(); + } + } + + return {}; + } + } + + std::string gsc_to_string(const scripting::script_value& value) + { + dump_context_t ctx; + return gsc_to_json(value, false, ctx).dump(); + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + gsc::function::add_multiple([](const scripting::variadic_args& args) + { + scripting::array array; + + for (auto i = 0u; i < args.size(); i += 2) + { + if (i >= args.size() - 1) + { + continue; + } + + const auto key = args[i].as(); + array[key] = args[i + 1]; + } + + return array; + }, "createmap", "json::create_map"); + + gsc::function::add_multiple([](const std::string& json) + { + const auto obj = nlohmann::json::parse(json); + return json_to_gsc(obj); + }, "jsonparse", "json::parse"); + + gsc::function::add_multiple([](const scripting::script_value& value, const scripting::variadic_args& args) + { + auto indent = -1; + auto print_id = false; + + if (args.size() > 0) + { + indent = args[0].as(); + } + + if (args.size() > 1) + { + print_id = args[1].as(); + } + + dump_context_t ctx; + const auto json = gsc_to_json(value, print_id, ctx); + auto dump = json.dump(indent); + + if (dump.size() > max_str_len) + { + printf("^3WARNING: json dump longer than %i bytes, truncating!", max_str_len); + dump.resize(max_str_len); + } + + return dump; + }, "jsonserialize", "json::serialize"); + + gsc::function::add_multiple([](const std::string& file, const scripting::script_value& value, const scripting::variadic_args& va) + { + std::string file_name = file; + + auto indent = -1; + auto print_id = false; + + if (!file_name.ends_with(".json")) + { + file_name.append(".json"); + } + + if (va.size() > 0) + { + indent = va[0].as(); + } + + if (va.size() > 1) + { + print_id = va[1].as(); + } + + dump_context_t ctx; + const auto json = gsc_to_json(value, print_id, ctx); + const auto dump = json.dump(indent); + + return utils::io::write_file(file_name, dump); + }, "jsondump", "json::dump"); + + gsc::function::add_multiple([](const scripting::variadic_args& args) + { + std::string buffer; + + for (const auto& arg : args) + { + buffer.append(gsc_to_string(arg)); + buffer.append("\t"); + } + + printf("%s\n", buffer.data()); + }, "jsonprint", "json::print"); + } + }; +} + +REGISTER_COMPONENT(json::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/json.hpp b/t6/plugins/tws-t6-gsc-utils/src/component/json.hpp new file mode 100644 index 0000000..30a2c69 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/json.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace json +{ + std::string gsc_to_string(const scripting::script_value& value); +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/logfile.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/logfile.cpp new file mode 100644 index 0000000..8c18e6d --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/logfile.cpp @@ -0,0 +1,49 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include +#include +#include +#include +#include + +namespace logfile +{ + namespace + { + std::string filename; + std::mutex mutex; + + bool is_logging_enabled() + { + static const auto is_enabled = utils::flags::has_flag("log"); + return is_enabled; + } + } + + void log_hook(const std::string& buffer) + { + if (!is_logging_enabled()) + { + return; + } + + std::lock_guard _(mutex); + utils::io::write_file(filename, buffer, true); + } + + class component final : public component_interface + { + public: + void on_after_dvar_init([[maybe_unused]] plugin::plugin* plugin) override + { + utils::io::create_directory("logs"); + filename = utils::string::va("logs/console-%s.log", + utils::string::get_timestamp().data()); + } + }; +} + +REGISTER_COMPONENT(logfile::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/logfile.hpp b/t6/plugins/tws-t6-gsc-utils/src/component/logfile.hpp new file mode 100644 index 0000000..1d01017 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/logfile.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace logfile +{ + void log_hook(const std::string& buffer); +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/mysql.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/mysql.cpp new file mode 100644 index 0000000..1648669 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/mysql.cpp @@ -0,0 +1,545 @@ +#include +#include "loader/component_loader.hpp" + +#include "mysql.hpp" +#include "component/gsc.hpp" +#include "component/scheduler.hpp" +#include "component/scripting.hpp" + +#include + +namespace mysql +{ + namespace + { + struct mysql_result_t + { + MYSQL_RES* result; + MYSQL_STMT* stmt; + uint64_t affected_rows; + std::string error; + }; + + struct task_t + { + scripting::object handle; + mysql_result_t result{}; + std::atomic_bool completed; + }; + + std::vector> tasks; + std::array connection_pool; + + scripting::script_value field_to_value(const MYSQL_FIELD* field, const std::string& row) + { + switch (field->type) + { + case enum_field_types::MYSQL_TYPE_INT24: + case enum_field_types::MYSQL_TYPE_LONG: + case enum_field_types::MYSQL_TYPE_SHORT: + return std::atoi(row.data()); + case enum_field_types::MYSQL_TYPE_LONGLONG: + return row; + case enum_field_types::MYSQL_TYPE_FLOAT: + case enum_field_types::MYSQL_TYPE_DOUBLE: + return static_cast(std::atof(row.data())); + } + + return row; + } + + scripting::script_value bind_to_value(const MYSQL_BIND* bind) + { + switch (bind->buffer_type) + { + case enum_field_types::MYSQL_TYPE_INT24: + case enum_field_types::MYSQL_TYPE_LONG: + case enum_field_types::MYSQL_TYPE_SHORT: + return *reinterpret_cast(bind->buffer); + case enum_field_types::MYSQL_TYPE_LONGLONG: + return std::to_string(*reinterpret_cast(bind->buffer)); + case enum_field_types::MYSQL_TYPE_FLOAT: + return *reinterpret_cast(bind->buffer); + case enum_field_types::MYSQL_TYPE_DOUBLE: + return static_cast(*reinterpret_cast(bind->buffer)); + case enum_field_types::MYSQL_TYPE_STRING: + return std::string{reinterpret_cast(bind->buffer), bind->buffer_length}; + } + + return {}; + } + + scripting::array generate_result(MYSQL_STMT* stmt) + { + if (stmt == nullptr) + { + return {}; + } + + utils::memory::allocator allocator; + + scripting::array result_arr; + + const auto meta = mysql_stmt_result_metadata(stmt); + if (meta == nullptr) + { + return {}; + } + + const auto column_count = mysql_num_fields(meta); + const auto fields = mysql_fetch_fields(meta); + + const auto is_null = allocator.allocate_array(column_count); + const auto errors = allocator.allocate_array(column_count); + const auto real_lengths = allocator.allocate_array(column_count); + const auto binds = allocator.allocate_array(column_count); + + for (auto i = 0u; i < column_count; i++) + { + binds[i].length = &real_lengths[i]; + binds[i].is_null = &is_null[i]; + binds[i].error = &errors[i]; + } + + if (mysql_stmt_bind_result(stmt, binds) != 0 || + mysql_stmt_store_result(stmt) != 0) + { + return {}; + } + + while (mysql_stmt_fetch(stmt) != MYSQL_NO_DATA) + { + for (auto i = 0u; i < column_count; i++) + { + switch (fields[i].type) + { + case enum_field_types::MYSQL_TYPE_INT24: + case enum_field_types::MYSQL_TYPE_LONG: + case enum_field_types::MYSQL_TYPE_SHORT: + { + binds[i].buffer_type = MYSQL_TYPE_LONG; + binds[i].buffer = allocator.allocate(); + binds[i].buffer_length = sizeof(int); + break; + }; + case enum_field_types::MYSQL_TYPE_LONGLONG: + { + binds[i].buffer_type = MYSQL_TYPE_LONGLONG; + binds[i].buffer = allocator.allocate(); + binds[i].buffer_length = sizeof(std::int64_t); + break; + }; + case enum_field_types::MYSQL_TYPE_FLOAT: + binds[i].buffer_type = MYSQL_TYPE_FLOAT; + binds[i].buffer = allocator.allocate(); + binds[i].buffer_length = sizeof(float); + break; + case enum_field_types::MYSQL_TYPE_DOUBLE: + { + binds[i].buffer_type = MYSQL_TYPE_DOUBLE; + binds[i].buffer = allocator.allocate(); + binds[i].buffer_length = sizeof(double); + break; + } + default: + binds[i].buffer_type = MYSQL_TYPE_STRING; + binds[i].buffer = allocator.allocate_array(real_lengths[i]); + binds[i].buffer_length = real_lengths[i]; + break; + } + + mysql_stmt_fetch_column(stmt, &binds[i], i, 0); + } + + scripting::array row_arr; + + for (auto i = 0u; i < column_count; i++) + { + const auto field = &fields[i]; + const std::string field_name = {field->name, field->name_length}; + row_arr[field_name] = bind_to_value(&binds[i]); + } + + result_arr.emplace_back(row_arr); + } + + mysql_free_result(meta); + + return result_arr; + } + + scripting::array generate_result(MYSQL_RES* result) + { + if (result == nullptr) + { + return {}; + } + + scripting::array result_arr; + + const auto num_rows = mysql_num_rows(result); + const auto num_fields = mysql_num_fields(result); + const auto fields = mysql_fetch_fields(result); + + for (auto i = 0u; i < num_rows; i++) + { + scripting::array row_arr; + + const auto row = mysql_fetch_row(result); + const auto lengths = mysql_fetch_lengths(result); + + for (auto f = 0u; f < num_fields; f++) + { + const auto field = &fields[f]; + + const std::string field_str = {field->name, field->name_length}; + const std::string row_str = {row[f], lengths[f]}; + const auto value = field_to_value(field, row_str); + + row_arr[field_str] = value; + } + + result_arr.emplace_back(row_arr); + } + + return result_arr; + } + + template + scripting::object create_mysql_query(F&& cb) + { + auto task = std::make_shared(); + tasks.emplace_back(task); + + scheduler::thread_pool.push([=] + { + try + { + mysql::access([&](database_t& db) + { + task->result = cb(db); + task->completed = true; + }); + } + catch (const std::exception& e) + { + printf("^1MySQL ERROR: %s\n", e.what()); + task->completed = true; + } + }); + + return task->handle; + } + + void wait_and_clear_tasks() + { + for (auto& task : tasks) + { + while (!task->completed) + { + std::this_thread::sleep_for(10ms); + } + } + + tasks.clear(); + } + + void notify_task_result(std::shared_ptr& task) + { + const auto result = task->result.result + ? generate_result(task->result.result) + : generate_result(task->result.stmt); + + const auto rows = static_cast(task->result.affected_rows); + scripting::notify(task->handle.get_entity_id(), "done", {result, rows, task->result.error}); + + if (task->result.result != nullptr) + { + mysql_free_result(task->result.result); + task->result.result = nullptr; + } + + if (task->result.stmt != nullptr) + { + mysql_stmt_close(task->result.stmt); + task->result.stmt = nullptr; + } + } + + void check_tasks() + { + for (auto i = tasks.begin(); i != tasks.end(); ) + { + auto& task = *i; + if (!task->completed) + { + ++i; + continue; + } + else + { + notify_task_result(task); + i = tasks.erase(i); + } + } + } + + template + MYSQL_BIND* bind_statement_args(const T& args, std::size_t& bind_count) + { + bind_count = args.size(); + const auto binds = utils::memory::allocate_array(bind_count); + + for (auto i = 0u; i < args.size(); i++) + { + const auto& arg = args[i]; + const auto& raw_value = arg.get_raw(); + + switch (raw_value.type) + { + case game::SCRIPT_FLOAT: + { + binds[i].buffer = utils::memory::allocate(); + binds[i].buffer_type = MYSQL_TYPE_FLOAT; + *reinterpret_cast(binds[i].buffer) = raw_value.u.floatValue; + break; + } + case game::SCRIPT_INTEGER: + { + binds[i].buffer = utils::memory::allocate(); + binds[i].buffer_type = MYSQL_TYPE_LONG; + *reinterpret_cast(binds[i].buffer) = raw_value.u.intValue; + break; + } + case game::SCRIPT_STRING: + { + const auto str = arg.as(); + const auto str_copy = utils::memory::duplicate_string(str); + binds[i].buffer = str_copy; + binds[i].buffer_length = str.size(); + binds[i].buffer_type = MYSQL_TYPE_STRING; + break; + } + default: + { + binds[i].buffer_type = MYSQL_TYPE_NULL; + break; + } + } + } + + return binds; + } + + void cleanup_connections() + { + for (auto& connection : connection_pool) + { + connection.cleanup(); + } + } + + void free_statement_binds(MYSQL_BIND* binds, const std::size_t bind_count) + { + if (binds == nullptr) + { + return; + } + + for (auto i = 0u; i < bind_count; i++) + { + utils::memory::free(binds[i].buffer); + } + + utils::memory::free(binds); + } + } + + utils::concurrency::container& get_config() + { + static utils::concurrency::container config; + static auto initialized = false; + + if (!initialized) + { + config.access([&](sql::connection_config& cfg) + { + cfg.user = "root"; + cfg.password = "root"; + cfg.host = "localhost"; + cfg.port = 3306; + cfg.database = "default"; + }); + + initialized = true; + } + return config; + } + + void connection::check() + { + const auto now = std::chrono::high_resolution_clock::now(); + const auto diff = now - this->start_; + + if (this->db.get() == nullptr || !this->db->ping_server() || diff >= connection_timeout) + { + get_config().access([&](sql::connection_config& cfg) + { + this->db = std::make_unique(cfg); + this->start_ = now; + }); + } + + this->last_access_ = now; + } + + void connection::cleanup() + { + std::unique_lock lock(this->mutex, std::try_to_lock); + if (!lock.owns_lock()) + { + return; + } + + const auto now = std::chrono::high_resolution_clock::now(); + const auto diff = now - this->last_access_; + if (diff >= connection_timeout) + { + this->db.reset(); + } + } + + connection* get_connection(std::unique_lock& lock) + { + static thread_local connection* last_connection{}; + if (last_connection != nullptr) + { + lock = std::unique_lock(last_connection->mutex, std::try_to_lock); + if (lock.owns_lock()) + { + return last_connection; + } + } + + for (auto i = 0u; i < connection_pool.size(); i++) + { + auto connection = &connection_pool[i]; + if (connection == last_connection) + { + continue; + } + + lock = std::unique_lock(connection->mutex, std::try_to_lock); + if (!lock.owns_lock()) + { + continue; + } + + last_connection = connection; + return connection; + } + + return nullptr; + } + + class component final : public component_interface + { + public: + void on_shutdown([[maybe_unused]] plugin::plugin* plugin) override + { + wait_and_clear_tasks(); + } + + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + scripting::on_shutdown(wait_and_clear_tasks); + scheduler::loop(cleanup_connections, scheduler::async, 60s); + scheduler::loop(check_tasks, scheduler::server); + + gsc::function::add("mysql::set_config", [](const scripting::object& config) + { + get_config().access([&](sql::connection_config& cfg) + { + cfg.host = config["host"].as(); + cfg.user = config["user"].as(); + cfg.password = config["password"].as(); + cfg.port = config["port"].as(); + cfg.database = config["database"].as(); + }); + }); + + gsc::function::add_multiple([](const std::string& query) + { + return create_mysql_query([=](database_t& db) + { + const auto handle = db->get_handle(); + + mysql_result_t result{}; + if (mysql_query(handle, query.data()) != 0) + { + result.error = mysql_error(handle); + return result; + } + + result.result = mysql_store_result(handle); + result.affected_rows = mysql_affected_rows(handle); + + return result; + }); + }, "mysql::query", "mysql::execute"); + + gsc::function::add("mysql::prepared_statement", [](const std::string& query, const scripting::variadic_args& values) + { + MYSQL_BIND* binds = nullptr; + auto bind_count = 0u; + + try + { + if (values.size() > 0 && values[0].is()) + { + binds = bind_statement_args(values[0].as(), bind_count); + } + else + { + binds = bind_statement_args(values, bind_count); + } + + const auto handle = create_mysql_query([binds, bind_count, query](database_t& db) + { + const auto _0 = gsl::finally([=] + { + free_statement_binds(binds, bind_count); + }); + + mysql_result_t result{}; + + const auto handle = db->get_handle(); + const auto stmt = mysql_stmt_init(handle); + + if (mysql_stmt_prepare(stmt, query.data(), query.size()) != 0 || + (binds != nullptr && mysql_stmt_bind_param(stmt, binds) != 0) || + mysql_stmt_execute(stmt) != 0) + { + result.error = mysql_stmt_error(stmt); + return result; + } + + result.stmt = stmt; + result.affected_rows = mysql_stmt_affected_rows(stmt); + + return result; + }); + + return handle; + } + catch (const std::exception& e) + { + free_statement_binds(binds, bind_count); + throw e; + } + }); + } + }; +} + +REGISTER_COMPONENT(mysql::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/mysql.hpp b/t6/plugins/tws-t6-gsc-utils/src/component/mysql.hpp new file mode 100644 index 0000000..bd88662 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/mysql.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#pragma warning(push) +#pragma warning(disable: 4127) +#pragma warning(disable: 4267) +#pragma warning(disable: 4018) +#pragma warning(disable: 4996) +#pragma warning(disable: 4244) +#include +#include +#pragma warning(pop) + +namespace sql = sqlpp::mysql; + +namespace mysql +{ + constexpr auto max_connections = 100; + constexpr auto connection_timeout = 200s; + + using database_mutex_t = std::recursive_mutex; + using database_t = std::unique_ptr; + + utils::concurrency::container& get_config(); + + class connection + { + public: + connection() = default; + void check(); + void cleanup(); + + database_t db; + database_mutex_t mutex; + + private: + std::chrono::high_resolution_clock::time_point start_; + std::chrono::high_resolution_clock::time_point last_access_; + + }; + + connection* get_connection(std::unique_lock& lock); + + template + T access(F&& accessor) + { + std::unique_lock lock; + auto conn = get_connection(lock); + if (conn == nullptr) + { + throw std::runtime_error("out of connections"); + } + + conn->check(); + return accessor(conn->db); + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/network.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/network.cpp new file mode 100644 index 0000000..08c035d --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/network.cpp @@ -0,0 +1,210 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "gsc.hpp" +#include "io.hpp" +#include "scheduler.hpp" +#include "scripting.hpp" + +#include +#include +#include + +namespace network +{ + namespace + { + struct socket_t + { + SOCKET socket; + scripting::object handle; + }; + + std::vector connected_sockets; + std::unordered_map handle_to_socket; + + void poll_sockets() + { + for (const auto& inst : connected_sockets) + { + std::string data; + data.reserve(0x1000); + + auto result = 0; + do + { + char buffer[0x1000]{}; + result = recv(inst.socket, buffer, sizeof(buffer), 0); + if (result != -1 && result != 0) + { + data.append(buffer); + } + } while (result != -1 && result != 0); + + if (data.size() == 0) + { + continue; + } + + auto start_index = 0; + scripting::array chunks; + + for (auto i = 0u; i < data.size(); i++) + { + const auto is_eof = data[i] == EOF; + const auto is_end = i == data.size() - 1; + if (is_eof || is_end) + { + const auto chunk = data.substr(start_index, i - start_index); + chunks.emplace_back(chunk); + start_index = i + 1; + } + } + + scripting::notify(inst.handle.get_entity_id(), "data", {chunks.get_raw()}); + } + } + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + if (!utils::flags::has_flag("experimental-utils")) + { + return; + } + + scripting::on_shutdown([] + { + for (const auto& inst : connected_sockets) + { + closesocket(inst.socket); + } + + connected_sockets.clear(); + handle_to_socket.clear(); + }); + + scheduler::loop(poll_sockets, scheduler::server_packet_loop); + + gsc::function::add("network::connect", [](const std::string& type, const std::string& address, const int port) + -> scripting::script_value + { + static std::unordered_map socket_types = + { + {"tcp", SOCK_STREAM}, + {"udp", SOCK_DGRAM}, + }; + + const auto type_lower = utils::string::to_lower(type); + const auto type_id = socket_types.find(type_lower); + if (type_id == socket_types.end()) + { + throw std::runtime_error("invalid socket type"); + } + + const auto sock = socket(AF_INET, type_id->second, 0); + + u_long mode = 1; + if (ioctlsocket(sock, FIONBIO, &mode) == SOCKET_ERROR) + { + closesocket(sock); + return {}; + } + + int flag = 1; + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&flag, sizeof(int)); + + SOCKADDR_IN sin{}; + sin.sin_family = AF_INET; + sin.sin_port = htons(static_cast(port)); + inet_pton(AF_INET, address.data(), &sin.sin_addr); + + if (connect(sock, reinterpret_cast(&sin), sizeof(sin)) == SOCKET_ERROR) + { + const auto error = WSAGetLastError(); + if (error != WSAEWOULDBLOCK && error != WSAEINPROGRESS) + { + closesocket(sock); + return {}; + } + } + + scripting::object handle; + + socket_t socket_ref{}; + socket_ref.socket = sock; + socket_ref.handle = handle; + + const auto handle_id = handle.get_entity_id(); + handle_to_socket[handle_id] = sock; + + connected_sockets.emplace_back(std::move(socket_ref)); + + return handle; + }); + + gsc::function::add("network::send", [](const scripting::object& handle, std::string data) + { + const auto handle_id = handle.get_entity_id(); + const auto iter = handle_to_socket.find(handle_id); + if (iter == handle_to_socket.end()) + { + return -1; + } + + const auto sock = iter->second; + data += EOF; + return send(sock, data.data(), data.size(), 0); + }); + + gsc::function::add("network::close", [](const scripting::object& handle) + { + const auto handle_id = handle.get_entity_id(); + const auto iter = handle_to_socket.find(handle_id); + if (iter == handle_to_socket.end()) + { + return; + } + + closesocket(iter->second); + handle_to_socket.erase(iter); + + for (auto i = connected_sockets.begin(), end = connected_sockets.end(); i != end; ++i) + { + if (i->handle.get_entity_id() == handle_id) + { + connected_sockets.erase(i); + break; + } + } + }); + + gsc::function::add("network::is_connected", [](const scripting::object& handle) + { + const auto handle_id = handle.get_entity_id(); + const auto iter = handle_to_socket.find(handle_id); + if (iter == handle_to_socket.end()) + { + return (int)0; + } + + char code{}; + int size = sizeof(code); + getsockopt(iter->second, SOL_SOCKET, SO_ERROR, &code, &size); + return static_cast(code); + }); + + gsc::function::add("network::get_last_error", []() + { + return WSAGetLastError(); + }); + } + }; +} + +REGISTER_COMPONENT(network::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/notifies.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/notifies.cpp new file mode 100644 index 0000000..a9ddaac --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/notifies.cpp @@ -0,0 +1,211 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/scripting/stack_isolation.hpp" +#include "gsc.hpp" +#include "scheduler.hpp" +#include "scripting.hpp" + +#include +#include +#include + +namespace notifies +{ + namespace + { + struct notify_group + { + unsigned int owner_id; + unsigned int notify_target; + unsigned int main_notify; + std::unordered_set sub_notifies; + }; + + using notify_groups_t = std::vector; + + bool in_notify_queue; + notify_groups_t notify_groups_queue; + + utils::hook::detour vm_notify_hook; + + void push_sl_string(unsigned int value) + { + game::VariableValue var{}; + var.type = game::SCRIPT_STRING; + var.u.stringValue = value; + scripting::push_value(var); + } + + void vm_notify_stub(game::scriptInstance_t inst, unsigned int notify_list_owner_id, + unsigned int string_value, game::VariableValue* top) + { + vm_notify_hook.invoke(inst, notify_list_owner_id, string_value, top); + + if (in_notify_queue) + { + return; + } + + const auto disconnect_str = *reinterpret_cast(0x24B71CE); + + in_notify_queue = true; + const auto _0 = gsl::finally([] + { + in_notify_queue = false; + }); + + std::unordered_map> notified; + + for (auto i = notify_groups_queue.begin(); i != notify_groups_queue.end(); ) + { + if (i->owner_id == notify_list_owner_id) + { + const auto should_notify = i->sub_notifies.find(string_value) != i->sub_notifies.end(); + const auto should_delete = string_value == disconnect_str || notified[i->notify_target].contains(i->main_notify); + const auto object_type = game::scr_VarGlob->objectVariableValue[i->notify_target].w.type; + + if (object_type != game::SCRIPT_FREE && should_notify) + { + scripting::stack_isolation _1; + push_sl_string(string_value); + game::Scr_NotifyId(inst, 0, i->notify_target, i->main_notify, 1); + notified[i->notify_target].insert(i->main_notify); + } + + if (should_notify || should_delete) + { + i = notify_groups_queue.erase(i); + continue; + } + } + + ++i; + } + } + + void clear_notify_group(unsigned int id) + { + for (auto i = notify_groups_queue.begin(); i != notify_groups_queue.end(); ) + { + if (i->owner_id == id) + { + i = notify_groups_queue.erase(i); + } + else + { + ++i; + } + } + } + + void free_variable_stub_1(utils::hook::assembler& a) + { + a.pushad(); + a.push(ebx); + a.call(clear_notify_group); + a.pop(ebx); + a.popad(); + + a.movzx(ecx, word_ptr(edi, 2)); + a.xor_(edx, edx); + a.jmp(0x550EA5); + } + + void free_variable_stub_2(utils::hook::assembler& a) + { + a.pushad(); + a.push(edi); + a.call(clear_notify_group); + a.pop(edi); + a.popad(); + + a.mov(word_ptr(esi, 2), di); + a.pop(edi); + a.jmp(0x415EA2); + } + + void* client_connect_stub(int client_num, int script_pers_id) + { + const scripting::entity player = game::Scr_GetEntityId(game::SCRIPTINSTANCE_SERVER, client_num, 0, 0); + scripting::notify(*game::levelEntityId, "direct_connect", {player}); + return utils::hook::invoke(0x41BE10, client_num, script_pers_id); + } + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + if (!game::environment::t6zm()) + { + return; + } + + vm_notify_hook.create(0x8F3620, vm_notify_stub); + + utils::hook::jump(0x550E9F, utils::hook::assemble(free_variable_stub_1)); + utils::hook::jump(0x415E9D, utils::hook::assemble(free_variable_stub_2)); + + utils::hook::call(0x501F0F, client_connect_stub); + + scripting::on_shutdown([] + { + notify_groups_queue.clear(); + }); + + gsc::function::add("createnotifygroup", [](const scripting::variadic_args& args) + -> scripting::script_value + { + std::unordered_set notifies; + + const auto& entity = args[0].get_raw(); + if (entity.type != game::SCRIPT_OBJECT) + { + throw std::runtime_error("argument 1 must be a script object"); + } + + const auto& entity_target = args[1].get_raw(); + if (entity_target.type != game::SCRIPT_OBJECT) + { + throw std::runtime_error("argument 1 must be a script object"); + } + + const auto main_notify_str = args[2].as(); + const auto main_notify = game::SL_GetString(main_notify_str.data(), 0); + + for (auto i = 3u; i < args.size(); i++) + { + const auto& arg = args[i]; + if (arg.is()) + { + const auto arr = arg.as(); + for (const auto& [key, value] : arr) + { + const auto str = value.as(); + notifies.insert(game::SL_GetString(str.data(), 0)); + } + } + else if (arg.is()) + { + const auto str = arg.as(); + notifies.insert(game::SL_GetString(str.data(), 0)); + } + } + + if (notifies.size() <= 0) + { + throw std::runtime_error("must pass atleast 1 notify"); + } + + notify_groups_queue.emplace_back(entity.u.uintValue, entity_target.u.uintValue, + main_notify, notifies); + + return {}; + }); + } + }; +} + +REGISTER_COMPONENT(notifies::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/patches.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/patches.cpp new file mode 100644 index 0000000..f59c5e3 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/patches.cpp @@ -0,0 +1,72 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "game/scripting/safe_execution.hpp" +#include "component/gsc.hpp" + +#include +#include + +namespace patches +{ + namespace + { + const game::dvar_t* sv_display_clan_tag; + + char* cs_display_name_stub(const void* client_state, int type) + { + if (!sv_display_clan_tag->current.enabled) + { + type = 1; + } + + return utils::hook::invoke(SELECT(0x59AE90, 0x563AD0), client_state, type); + } + + void* bg_get_client_field_stub(int a1, const char* a2) + { + const auto result = utils::hook::invoke(0x5B4220, a1, a2); + if (!result) + { + const auto err = utils::string::va("no client field named %s found", a2); + game::Scr_Error(game::SCRIPTINSTANCE_SERVER, err, false); + } + + return result; + } + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + patch(); + + if (game::environment::t6zm()) + { + patch_zm(); + } + } + + void patch() + { + sv_display_clan_tag = game::Dvar_RegisterBool("sv_display_clan_tag", false, 0, "Display the clan tag in the game log"); + utils::hook::call(SELECT(0x820F1C, 0x81F91C), cs_display_name_stub); + } + + void patch_zm() + { + // Fix some common crashes, not necessary because of `patch()` but just to be sure + + // sets an always null pointer :/ + utils::hook::nop(0x6FF36E, 0x2); + + // related to custom perks and tombstone/electric chair on motd and possibly other contexts + utils::hook::call(0x85842A, bg_get_client_field_stub); + } + }; +} + +REGISTER_COMPONENT(patches::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/scheduler.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/scheduler.cpp new file mode 100644 index 0000000..cbcf76c --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/scheduler.cpp @@ -0,0 +1,185 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" +#include "scheduler.hpp" + +#include +#include + +namespace scheduler +{ + utils::thread_pool thread_pool; + + namespace + { + struct task + { + std::function handler{}; + std::chrono::milliseconds interval{}; + std::chrono::high_resolution_clock::time_point last_call{}; + }; + + using task_list = std::vector; + + utils::hook::detour com_server_packet_event_hook; + + class task_pipeline + { + public: + void add(task&& task) + { + new_callbacks_.access([&task](task_list& tasks) + { + tasks.emplace_back(std::move(task)); + }); + } + + void execute() + { + callbacks_.access([&](task_list& tasks) + { + this->merge_callbacks(); + + for (auto i = tasks.begin(); i != tasks.end();) + { + const auto now = std::chrono::high_resolution_clock::now(); + const auto diff = now - i->last_call; + + if (diff < i->interval) + { + ++i; + continue; + } + + i->last_call = now; + + const auto res = i->handler(); + if (res == cond_end) + { + i = tasks.erase(i); + } + else + { + ++i; + } + } + }); + } + + private: + utils::concurrency::container new_callbacks_; + utils::concurrency::container callbacks_; + + void merge_callbacks() + { + callbacks_.access([&](task_list& tasks) + { + new_callbacks_.access([&](task_list& new_tasks) + { + tasks.insert(tasks.end(), std::move_iterator(new_tasks.begin()), std::move_iterator(new_tasks.end())); + new_tasks = {}; + }); + }); + } + }; + + std::thread thread; + std::atomic_bool killed = false; + task_pipeline pipelines[pipeline::count]; + + void execute(const pipeline type) + { + assert(type >= 0 && type < pipeline::count); + pipelines[type].execute(); + } + + void execute_server() + { + execute(pipeline::server); + } + + void server_frame_stub(utils::hook::assembler& a) + { + a.pushad(); + a.call(execute_server); + a.popad(); + + a.jmp(SELECT(0x49E910, 0x5001A0)); + } + + void com_server_packet_event_stub() + { + execute(pipeline::server_packet_loop); + com_server_packet_event_hook.invoke(); + } + } + + void schedule(const std::function& callback, const pipeline type, + const std::chrono::milliseconds delay) + { + assert(type >= 0 && type < pipeline::count); + + task task; + task.handler = callback; + task.interval = delay; + task.last_call = std::chrono::high_resolution_clock::now(); + + pipelines[type].add(std::move(task)); + } + + void loop(const std::function& callback, const pipeline type, + const std::chrono::milliseconds delay) + { + schedule([callback]() + { + callback(); + return cond_continue; + }, type, delay); + } + + void once(const std::function& callback, const pipeline type, + const std::chrono::milliseconds delay) + { + schedule([callback]() + { + callback(); + return cond_end; + }, type, delay); + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + thread_pool.initialize(8); + thread_pool.start(); + + thread = std::thread([]() + { + while (!killed) + { + execute(pipeline::async); + std::this_thread::sleep_for(10ms); + } + }); + + utils::hook::jump(SELECT(0x4A59F7, 0x6AA2F7), utils::hook::assemble(server_frame_stub)); + com_server_packet_event_hook.create(SELECT(0x42F850, 0x549300), com_server_packet_event_stub); + } + + void on_shutdown([[maybe_unused]] plugin::plugin* plugin) override + { + thread_pool.stop(); + killed = true; + + if (thread.joinable()) + { + thread.join(); + } + } + }; +} + +REGISTER_COMPONENT(scheduler::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/scheduler.hpp b/t6/plugins/tws-t6-gsc-utils/src/component/scheduler.hpp new file mode 100644 index 0000000..6a8dffc --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/scheduler.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace scheduler +{ + enum pipeline + { + server, + server_packet_loop, + async, + count, + }; + + static const bool cond_continue = false; + static const bool cond_end = true; + + void schedule(const std::function& callback, pipeline type = pipeline::server, + std::chrono::milliseconds delay = 0ms); + void loop(const std::function& callback, pipeline type = pipeline::server, + std::chrono::milliseconds delay = 0ms); + void once(const std::function& callback, pipeline type = pipeline::server, + std::chrono::milliseconds delay = 0ms); + + extern utils::thread_pool thread_pool; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/scripting.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/scripting.cpp new file mode 100644 index 0000000..c7abd87 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/scripting.cpp @@ -0,0 +1,283 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "game/scripting/event.hpp" +#include "game/scripting/execution.hpp" + +#include "scripting.hpp" +#include "scheduler.hpp" + +#include +#include + +namespace scripting +{ + std::unordered_map> fields_table; + + std::unordered_map method_map; + std::unordered_map function_map; + + std::unordered_map> script_function_table; + std::unordered_map>> script_function_table_sort; + + namespace + { + utils::hook::detour scr_add_class_field_hook; + utils::hook::detour g_shutdown_game_hook; + + void scr_add_class_field_stub(game::scriptInstance_t inst, unsigned int classnum, char const* name, unsigned int offset) + { + if (fields_table[classnum].find(name) == fields_table[classnum].end()) + { + fields_table[classnum][name] = offset; + } + + scr_add_class_field_hook.invoke(inst, classnum, name, offset); + } + + game::BuiltinMethodDef get_method(const std::string& name) + { + game::BuiltinMethodDef method{}; + + auto pName = name.data(); + int arg = 0; + + const auto func = game::Scr_GetMethod(&pName, &arg, &arg, &arg); + + if (func) + { + method.actionFunc = reinterpret_cast(func); + method.actionString = pName; + } + + return method; + } + + void load_functions() + { + for (auto i = 0; i < 278; i++) + { + const auto method = game::player_methods[i]; + method_map[method.actionString] = method; + } + + for (auto i = 0; i < 22; i++) + { + const auto method = game::scriptEnt_methods[i]; + method_map[method.actionString] = method; + } + + for (auto i = 0; i < 114; i++) + { + const auto method = game::scriptVehicle_methods[i]; + method_map[method.actionString] = method; + } + + for (auto i = 0; i < 29; i++) + { + const auto method = game::hudElem_methods[i]; + method_map[method.actionString] = method; + } + + for (auto i = 0; i < 29; i++) + { + const auto method = game::helicopter_methods[i]; + method_map[method.actionString] = method; + } + + for (auto i = 0; i < 109; i++) + { + const auto method = game::actor_methods[i]; + method_map[method.actionString] = method; + } + + for (auto i = 0; i < 95; i++) + { + const auto method = game::builtInCommon_methods[i]; + method_map[method.actionString] = method; + } + + for (auto i = 0; i < 286; i++) + { + const auto method = game::builtIn_methods[i]; + method_map[method.actionString] = method; + } + + for (auto i = 0; i < 334; i++) + { + const auto function = game::functions[i]; + function_map[function.actionString] = function; + } + + for (auto i = 0; i < 220; i++) + { + const auto function = game::common_functions[i]; + function_map[function.actionString] = function; + } + + method_map["notifyonplayercommand"] = get_method("notifyonplayercommand"); + } + + utils::hook::detour scr_load_script_hook; + + void extract_obj_functions(game::GSC_OBJ* obj) + { + const auto exported = reinterpret_cast(&obj->magic[obj->exports_offset]); + const auto count = obj->exports_count; + auto iter = &obj->magic[obj->exports_offset + 8]; + + std::string filename = &obj->magic[obj->name]; + if (filename.ends_with(".gsc")) + { + filename = filename.substr(0, filename.size() - 4); + } + + std::string original = filename; + auto filename_count = 0; + while (script_function_table.find(filename) != script_function_table.end()) + { + filename = utils::string::va("%s_%i", original.data(), filename_count++); + } + + for (auto i = 0; i < count; i++) + { + const auto index = *reinterpret_cast(iter); + const auto function = &obj->magic[index]; + const auto address = reinterpret_cast(reinterpret_cast(obj) + exported[i].address); + + script_function_table[filename][function] = address; + script_function_table_sort[filename].push_back({function, address}); + + if (i == count - 1) + { + script_function_table[filename]["$end"] = &obj->magic[obj->fixup_offset]; + script_function_table_sort[filename].push_back({"$end", &obj->magic[obj->fixup_offset]}); + } + + iter += 12; + } + } + + utils::hook::detour scr_post_load_scripts_hook; + unsigned int post_load_scripts_stub(unsigned int inst) + { + script_function_table.clear(); + script_function_table_sort.clear(); + + const auto script_count = *reinterpret_cast(SELECT(0x2DB9F18, 0x2D8A218)); + const auto scripts = reinterpret_cast(SELECT(0x2DA2FE8, 0x2D732E8)); + + for (auto i = 0u; i < script_count; i++) + { + const auto obj = scripts[i].activeVersion; + extract_obj_functions(obj); + } + + return scr_post_load_scripts_hook.invoke(inst); + } + + std::vector> shutdown_callbacks; + void g_shutdown_game_stub(const int free_scripts) + { + for (const auto& callback : shutdown_callbacks) + { + callback(); + } + + return g_shutdown_game_hook.invoke(free_scripts); + } + } + + script_function find_function_ptr(const std::string& _name) + { + const auto name = utils::string::to_lower(_name); + + if (function_map.find(name) != function_map.end()) + { + const auto func = function_map[name].actionFunc; + return reinterpret_cast(func); + } + + if (method_map.find(name) != method_map.end()) + { + const auto func = method_map[name].actionFunc; + return reinterpret_cast(func); + } + + return nullptr; + } + + std::string find_function(const char* pos) + { + for (const auto& file : script_function_table_sort) + { + for (auto i = file.second.begin(); i != file.second.end() && std::next(i) != file.second.end(); ++i) + { + const auto next = std::next(i); + if (pos >= i->second && pos < next->second) + { + return utils::string::va("%s::%s", file.first.data(), i->first.data()); + } + } + } + + return "unknown function"; + } + + std::optional> find_function_pair(const char* pos) + { + for (const auto& file : script_function_table_sort) + { + for (auto i = file.second.begin(); i != file.second.end() && std::next(i) != file.second.end(); ++i) + { + const auto next = std::next(i); + if (pos >= i->second && pos < next->second) + { + return {{file.first, i->first}}; + } + } + } + + return {}; + } + + const char* find_function_start(const char* pos) + { + for (const auto& file : script_function_table_sort) + { + for (auto i = file.second.begin(); i != file.second.end() && std::next(i) != file.second.end(); ++i) + { + const auto next = std::next(i); + if (pos >= i->second && pos < next->second) + { + return i->second; + } + } + } + + return nullptr; + } + + void on_shutdown(const std::function& callback) + { + shutdown_callbacks.push_back(callback); + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + g_shutdown_game_hook.create(SELECT(0x60DCF0, 0x688A40), g_shutdown_game_stub); + + scr_add_class_field_hook.create(SELECT(0x6B7620, 0x438AD0), scr_add_class_field_stub); + scr_post_load_scripts_hook.create(SELECT(0x642EB0, 0x425F80), post_load_scripts_stub); + + load_functions(); + } + }; +} + +REGISTER_COMPONENT(scripting::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/scripting.hpp b/t6/plugins/tws-t6-gsc-utils/src/component/scripting.hpp new file mode 100644 index 0000000..8c616aa --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/scripting.hpp @@ -0,0 +1,22 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting +{ + extern std::unordered_map> fields_table; + + extern std::unordered_map> script_function_table; + extern std::unordered_map>> script_function_table_sort; + + extern std::unordered_map method_map; + extern std::unordered_map function_map; + + using script_function = void(*)(game::scr_entref_t); + + script_function find_function_ptr(const std::string& name); + std::string find_function(const char* pos); + const char* find_function_start(const char* pos); + std::optional> find_function_pair(const char* pos); + + void on_shutdown(const std::function& callback); +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/string.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/string.cpp new file mode 100644 index 0000000..c1f7aad --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/string.cpp @@ -0,0 +1,201 @@ +#include +#include "loader/component_loader.hpp" + +#include "gsc.hpp" + +// lua/lstrlib.c +#define MAX_FORMAT 32 +#define L_FMTFLAGSF "-+#0 " +#define L_FMTFLAGSX "-#0" +#define L_FMTFLAGSI "-+0 " +#define L_FMTFLAGSU "-0" +#define L_FMTFLAGSC "-" + +namespace string +{ + namespace + { + // lua/lstrlib.c + const char* getformat(const char* strfrmt, char* form) + { + const auto len = std::strspn(strfrmt, L_FMTFLAGSF "123456789.") + 1; + if (len >= MAX_FORMAT - 10) + { + throw std::runtime_error("invalid format (too long)"); + } + + *(form++) = '%'; + std::memcpy(form, strfrmt, len * sizeof(char)); + *(form + len) = '\0'; + return strfrmt + len - 1; + } + + // lua/lstrlib.c + const char* get_2_digits(const char* s) + { + if (isdigit(static_cast(*s))) + { + s++; + if (isdigit(static_cast(*s))) + { + s++; + } + } + + return s; + } + + // lua/lstrlib.c + void check_format(const char* form, const char* flags, int precision) + { + const char* spec = form + 1; + spec += std::strspn(spec, flags); + if (*spec != '0') + { + spec = get_2_digits(spec); + if (*spec == '.' && precision) + { + spec++; + spec = get_2_digits(spec); + } + } + if (!std::isalpha(static_cast(*spec))) + { + throw std::runtime_error(utils::string::va("invalid conversion specification: '%s'", form)); + } + } + + // partially lua/lstrlib.c + std::string format_string(const std::string& fmt, const scripting::variadic_args& va) + { + std::string buffer{}; + size_t va_index{}; + const char* strfrmt = fmt.data(); + const char* strfrmt_end = strfrmt + fmt.size(); + + while (strfrmt < strfrmt_end) + { + if (*strfrmt != '%') + { + buffer.push_back(*strfrmt++); + } + else if (*++strfrmt == '%') + { + buffer.push_back(*strfrmt++); + } + else + { + char form[MAX_FORMAT]{}; + const char* flags = ""; + strfrmt = getformat(strfrmt, form); + + switch (*strfrmt++) + { + case 'd': + case 'i': + flags = L_FMTFLAGSI; + goto intcase; + case 'u': + case 'p': + flags = L_FMTFLAGSU; + goto intcase; + case 'o': + case 'x': + case 'X': + flags = L_FMTFLAGSX; + intcase: + { + check_format(form, flags, 1); + const auto value = va[va_index].as(); + buffer.append(utils::string::va(form, value)); + va_index++; + break; + } + case 'f': + case 'F': + case 'e': + case 'E': + case 'g': + case 'G': + { + check_format(form, L_FMTFLAGSF, 1); + const auto value = va[va_index].as(); + buffer.append(utils::string::va(form, value)); + va_index++; + break; + } + case 'c': + { + const auto value = va[va_index].as(); + check_format(form, L_FMTFLAGSC, 0); + buffer.append(utils::string::va(form, static_cast(value))); + va_index++; + break; + } + case 's': + { + const auto str = va[va_index].as(); + buffer.append(str); + va_index++; + break; + } + default: + { + throw std::runtime_error(utils::string::va("invalid conversion '%s' to 'format'", form)); + } + } + } + } + + return buffer; + } + } + + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + gsc::function::add_multiple(format_string, "va", "string::va", + "formatstring", "string::format", "sprintf"); + + gsc::function::add_multiple(utils::string::to_upper, "toupper", "string::to_upper"); + gsc::function::add_multiple(utils::string::to_lower, "tolower", "string::to_lower"); + + gsc::function::add("string::is_numeric", utils::string::is_numeric); + gsc::function::add("string::starts_with", utils::string::starts_with); + gsc::function::add("string::ends_with", utils::string::ends_with); + gsc::function::add("string::replace", utils::string::replace); + + gsc::function::add("string::regex_replace", [](const std::string& str, const std::regex& expr, + const std::string& with) + { + return std::regex_replace(str, expr, with); + }); + + gsc::function::add("string::regex_match", [](const std::string& str, const std::regex& expr) + { + scripting::array array_match{}; + std::smatch match{}; + + if (std::regex_match(str, match, std::regex(expr))) + { + for (const auto& s : match) + { + array_match.emplace_back((s.str())); + } + } + + return array_match; + }); + + gsc::function::add("string::regex_search", [](const std::string& str, const std::regex& expr) + { + return std::regex_search(str, expr); + }); + } + }; +} + +REGISTER_COMPONENT(string::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/component/turrets.cpp b/t6/plugins/tws-t6-gsc-utils/src/component/turrets.cpp new file mode 100644 index 0000000..b48315d --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/component/turrets.cpp @@ -0,0 +1,418 @@ +#include +#include "loader/component_loader.hpp" +#include "game/game.hpp" + +#include "gsc.hpp" +#include "game/structs.hpp" +#include "game/game.hpp" + +#include +#include + +#define TURRET_DUMP_FIELD(__field__) \ + data[#__field__] = turret->__field__; \ + +#define TURRET_ADD_FIELD_INTERNAL(__name__, __getter__, __setter__) \ + {utils::string::to_lower(#__name__), turret_field{offsetof(game::TurretInfo, __name__), __getter__, __setter__}} \ + +#define TURRET_ADD_FIELD_GETTER(__name__, __getter__) \ + TURRET_ADD_FIELD_INTERNAL(__name__, __getter__, get_default_setter(dummy_info.__name__, #__name__, offsetof(game::TurretInfo, __name__))) \ + +#define TURRET_ADD_FIELD_SETTER(__name__, __setter__) \ + TURRET_ADD_FIELD_INTERNAL(__name__, get_default_getter(dummy_info.__name__, offsetof(game::TurretInfo, __name__)), __setter__) \ + +#define TURRET_ADD_FIELD(__name__) \ + TURRET_ADD_FIELD_INTERNAL(__name__, get_default_getter(dummy_info.__name__, offsetof(game::TurretInfo, __name__)), get_default_setter(dummy_info.__name__, #__name__, offsetof(game::TurretInfo, __name__))) \ + +#define NEW_MAX_TURRETS 255 + +namespace turret +{ + namespace + { + game::TurretInfo turret_info_store_relocated[NEW_MAX_TURRETS]; + + game::TurretInfo dummy_info{}; + + using getter_t = std::function; + using setter_t = std::function; + + struct turret_field + { + std::size_t offset; + getter_t getter; + setter_t setter; + }; + + template + getter_t get_default_getter([[maybe_unused]] T, const size_t offset) + { + return [=](game::TurretInfo* info) + { + return *reinterpret_cast(reinterpret_cast(info) + offset); + }; + } + + template + typename std::enable_if::value, setter_t>::type + get_default_setter([[maybe_unused]] T, const std::string& name, const size_t offset) + { + return [=](game::TurretInfo* info, const scripting::function_argument& value) + { + if (!value.is()) + { + const auto type_name = value.type_name(); + throw std::runtime_error(utils::string::va("type '%s' is not valid for field '%s'", type_name.data(), name.data())); + } + + const auto vec = value.as(); + const auto val = reinterpret_cast(reinterpret_cast(info) + offset); + val[0] = vec.get_x(); + val[1] = vec.get_y(); + val[2] = vec.get_z(); + }; + } + + template + typename std::enable_if::value, setter_t>::type + get_default_setter([[maybe_unused]] T, const std::string& name, const size_t offset) + { + return [=](game::TurretInfo* info, const scripting::function_argument& value) + { + if (!value.is()) + { + const auto type_name = value.type_name(); + throw std::runtime_error(utils::string::va("type '%s' is not valid for field '%s'", type_name.data(), name.data())); + } + + *reinterpret_cast(reinterpret_cast(info) + offset) = value.as(); + }; + } + + scripting::script_value turret_get_team(game::TurretInfo* info) + { + return utils::hook::invoke(0x4D7A30, info->eTeam); + } + + void turret_set_team(game::TurretInfo* info, const scripting::function_argument& value) + { + const auto str = value.as(); + const auto team_names = reinterpret_cast(0xD39970); + for (auto i = 0; i < game::TEAM_NUM_TEAMS; i++) + { + if (team_names[i] == str) + { + info->eTeam = static_cast(i); + return; + } + } + + throw std::runtime_error(utils::string::va("invalid team '%s'", str.data())); + } + + std::unordered_map turret_fields = + { + TURRET_ADD_FIELD(inuse), + TURRET_ADD_FIELD(state), + TURRET_ADD_FIELD(flags), + TURRET_ADD_FIELD(fireTime), + TURRET_ADD_FIELD(manualTarget.infoIndex), + TURRET_ADD_FIELD(manualTarget.number), + TURRET_ADD_FIELD(target.infoIndex), + TURRET_ADD_FIELD(target.number), + TURRET_ADD_FIELD(targetPos), + TURRET_ADD_FIELD(targetTime), + TURRET_ADD_FIELD(missOffsetNormalized), + TURRET_ADD_FIELD(arcmin[0]), + TURRET_ADD_FIELD(arcmin[1]), + TURRET_ADD_FIELD(arcmax[0]), + TURRET_ADD_FIELD(arcmax[1]), + TURRET_ADD_FIELD(initialYawmin), + TURRET_ADD_FIELD(initialYawmax), + TURRET_ADD_FIELD(forwardAngleDot), + TURRET_ADD_FIELD(dropPitch), + TURRET_ADD_FIELD(scanningPitch), + TURRET_ADD_FIELD(convergenceTime[0]), + TURRET_ADD_FIELD(convergenceTime[1]), + TURRET_ADD_FIELD(suppressTime), + TURRET_ADD_FIELD(maxRangeSquared), + TURRET_ADD_FIELD(detachSentient.infoIndex), + TURRET_ADD_FIELD(detachSentient.number), + TURRET_ADD_FIELD(stance), + TURRET_ADD_FIELD(prevStance), + TURRET_ADD_FIELD(fireSndDelay), + TURRET_ADD_FIELD(accuracy), + TURRET_ADD_FIELD(userOrigin), + TURRET_ADD_FIELD(prevSentTarget), + TURRET_ADD_FIELD(aiSpread), + TURRET_ADD_FIELD(playerSpread), + TURRET_ADD_FIELD_INTERNAL(eTeam, turret_get_team, turret_set_team), + TURRET_ADD_FIELD(heatVal), + TURRET_ADD_FIELD(overheating), + TURRET_ADD_FIELD(fireBarrel), + TURRET_ADD_FIELD(scanSpeed), + TURRET_ADD_FIELD(scanDecelYaw), + TURRET_ADD_FIELD(scanPauseTime), + TURRET_ADD_FIELD(originError), + TURRET_ADD_FIELD(anglesError), + TURRET_ADD_FIELD(pitchCap), + TURRET_ADD_FIELD(triggerDown), + TURRET_ADD_FIELD(fireSnd), + TURRET_ADD_FIELD(fireSndPlayer), + TURRET_ADD_FIELD(startFireSnd), + TURRET_ADD_FIELD(startFireSndPlayer), + TURRET_ADD_FIELD(loopFireEnd), + TURRET_ADD_FIELD(loopFireEndPlayer), + TURRET_ADD_FIELD(rotateLoopSnd), + TURRET_ADD_FIELD(rotateLoopSndPlayer), + TURRET_ADD_FIELD(rotateStopSnd), + TURRET_ADD_FIELD(rotateStopSndPlayer), + TURRET_ADD_FIELD(sndIsFiring), + TURRET_ADD_FIELD(targetOffset), + TURRET_ADD_FIELD(onTargetAngle), + TURRET_ADD_FIELD(previousAngles), + TURRET_ADD_FIELD(previousAngles), + }; + + utils::hook::detour bg_get_weapon_def_hook; + game::WeaponDef* bg_get_weapon_def_stub(const unsigned int weapon) + { + auto asset = bg_get_weapon_def_hook.invoke(weapon); + + if (asset->maxTurnSpeed[0] == 0.f && asset->maxTurnSpeed[1] == 0.f) + { + asset->maxTurnSpeed[0] = 360.f; + asset->maxTurnSpeed[1] = 360.f; + asset->minTurnSpeed[0] = 1.f; + asset->minTurnSpeed[1] = 1.f; + } + + return asset; + } + + void g_init_turrets_stub() + { + for (int i = 0; i < NEW_MAX_TURRETS; ++i) + { + turret_info_store_relocated[i].inuse = 0; + } + + // level.turrets = turret_info_store_relocated; + utils::hook::set(0x233CAB8, turret_info_store_relocated); + } + + auto turret_index = 0; + const auto turret_index_ptr = &turret_index; + + game::TurretInfo* g_spawn_turret_relocate() + { + turret_index = 0; + game::TurretInfo* turret_info{}; + + for (auto i = 0; i < NEW_MAX_TURRETS; ++i) + { + turret_info = &turret_info_store_relocated[i]; + if (!turret_info->inuse) + { + break; + } + + turret_index++; + } + + return turret_info; + } + + void __declspec(naked) g_spawn_turret_stub() + { + __asm + { + push esi; + pushad; + call g_spawn_turret_relocate; + mov[esp + 0x20], eax; + popad; + pop esi; + mov ecx, [turret_index_ptr]; + push 0x60AE90; + retn; + } + } + + game::gentity_s* g_spawn_turret_static_entity_stub(int /*unk1*/) + { + return utils::hook::invoke(0x54FCC0); + } + + void reallocate_turrets() + { + // Modify G_InitTurrets to initialize more tuurets + utils::hook::jump(0x61E850, g_init_turrets_stub); + // Modify G_SpawnTurret to check for a larger amount of turretInfoStore + utils::hook::jump(0x60AE3B, g_spawn_turret_stub); + // Modify G_SpawnturetStaticEntity to call G_Spawn instead + utils::hook::jump(0x6E1B00, g_spawn_turret_static_entity_stub); + // Patch G_SpawnTurret com_error to happen at a higher limit (255) + utils::hook::set(0x60AE92, NEW_MAX_TURRETS); + + // Patch level.num_gentities to min of 78 to not preallocate turret slots + utils::hook::set(0x67EEA2 + 6, 78); + // Patch SV_LocateGameData arg so sv.num_entities is the same as the above + utils::hook::set(0x67EE9F + 1, 78); + // G_FreeEntity remove turrets from slot calculations + utils::hook::set(0x6658E7 + 2, 78); + } + } + + class component final : public component_interface + { + public: + void on_startup([[maybe_unused]] plugin::plugin* plugin) override + { + if (!game::environment::t6zm()) + { + return; + } + + gsc::method::add("setturretfield", [](const scripting::entity& entity, const std::string& field, const scripting::variadic_args& args) + { + const auto ent = entity.get_entity_reference(); + if (ent.classnum != 0) + { + throw std::runtime_error("Invalid entity"); + } + + const auto turret = game::g_entities[ent.entnum].pTurretInfo; + if (turret == nullptr) + { + throw std::runtime_error("Not a valid turret entity"); + } + const auto field_name = utils::string::to_lower(field); + const auto itr = turret_fields.find(field_name); + + if (itr == turret_fields.end()) + { + throw std::runtime_error(utils::string::va("Invalid field %s", field_name.data())); + } + + itr->second.setter(turret, args[0]); + }); + + gsc::method::add("getturretfield", [](const scripting::entity& entity, const std::string& field) + { + const auto ent = entity.get_entity_reference(); + if (ent.classnum != 0) + { + throw std::runtime_error("Invalid entity"); + } + + const auto turret = game::g_entities[ent.entnum].pTurretInfo; + if (turret == nullptr) + { + throw std::runtime_error("Not a valid turret entity"); + } + + const auto field_name = utils::string::to_lower(field); + const auto itr = turret_fields.find(field_name); + + if (itr == turret_fields.end()) + { + throw std::runtime_error(utils::string::va("Invalid field %s", field_name.data())); + } + + return itr->second.getter(turret); + }); + + gsc::function::add("dumpturret", [](const scripting::entity& entity, const std::string& name) + { + const auto ent = entity.get_entity_reference(); + if (ent.classnum != 0) + { + throw std::runtime_error("Invalid entity"); + } + + const auto turret = game::g_entities[ent.entnum].pTurretInfo; + if (turret == nullptr) + { + throw std::runtime_error("Not a valid turret entity"); + } + + nlohmann::ordered_json data; + + TURRET_DUMP_FIELD(inuse); + TURRET_DUMP_FIELD(state); + TURRET_DUMP_FIELD(flags); + TURRET_DUMP_FIELD(fireTime); + data["manualTarget"]["infoIndex"] = turret->manualTarget.infoIndex; + data["manualTarget"]["number"] = turret->manualTarget.number; + data["target"]["infoIndex"] = turret->target.infoIndex; + data["target"]["number"] = turret->target.number; + TURRET_DUMP_FIELD(targetPos); + TURRET_DUMP_FIELD(targetTime); + TURRET_DUMP_FIELD(missOffsetNormalized); + TURRET_DUMP_FIELD(arcmin); + TURRET_DUMP_FIELD(arcmax); + TURRET_DUMP_FIELD(initialYawmin); + TURRET_DUMP_FIELD(initialYawmax); + TURRET_DUMP_FIELD(forwardAngleDot); + TURRET_DUMP_FIELD(dropPitch); + TURRET_DUMP_FIELD(scanningPitch); + TURRET_DUMP_FIELD(convergenceTime); + TURRET_DUMP_FIELD(suppressTime); + TURRET_DUMP_FIELD(maxRangeSquared); + data["detachSentient"]["infoIndex"] = turret->detachSentient.infoIndex; + data["detachSentient"]["number"] = turret->detachSentient.number; + TURRET_DUMP_FIELD(stance); + TURRET_DUMP_FIELD(prevStance); + TURRET_DUMP_FIELD(fireSndDelay); + TURRET_DUMP_FIELD(accuracy); + TURRET_DUMP_FIELD(userOrigin); + TURRET_DUMP_FIELD(prevSentTarget); + TURRET_DUMP_FIELD(aiSpread); + TURRET_DUMP_FIELD(playerSpread); + TURRET_DUMP_FIELD(eTeam); + TURRET_DUMP_FIELD(heatVal); + TURRET_DUMP_FIELD(overheating); + TURRET_DUMP_FIELD(fireBarrel); + TURRET_DUMP_FIELD(scanSpeed); + TURRET_DUMP_FIELD(scanDecelYaw); + TURRET_DUMP_FIELD(scanPauseTime); + TURRET_DUMP_FIELD(originError); + TURRET_DUMP_FIELD(anglesError); + TURRET_DUMP_FIELD(pitchCap); + TURRET_DUMP_FIELD(triggerDown); + TURRET_DUMP_FIELD(fireSnd); + TURRET_DUMP_FIELD(fireSndPlayer); + TURRET_DUMP_FIELD(startFireSnd); + TURRET_DUMP_FIELD(startFireSndPlayer); + TURRET_DUMP_FIELD(loopFireEnd); + TURRET_DUMP_FIELD(loopFireEndPlayer); + TURRET_DUMP_FIELD(rotateLoopSnd); + TURRET_DUMP_FIELD(rotateLoopSndPlayer); + TURRET_DUMP_FIELD(rotateStopSnd); + TURRET_DUMP_FIELD(rotateStopSndPlayer); + TURRET_DUMP_FIELD(sndIsFiring); + TURRET_DUMP_FIELD(targetOffset); + TURRET_DUMP_FIELD(onTargetAngle); + TURRET_DUMP_FIELD(turretRotateState); + TURRET_DUMP_FIELD(previousAngles); + + const std::string buffer = data.dump(4); + const std::string turret_entity_path = utils::string::va("entity_dumps/turrets/%s.json", name.data()); + + return utils::io::write_file(turret_entity_path, buffer, false); + }); + + // Check for whether a weapon can be spawned as a turret + utils::hook::nop(0x60AF4E, 5); + + // Don't use weapon def value for turrets turnspeed + bg_get_weapon_def_hook.create(0x5906B0, bg_get_weapon_def_stub); + + // Reallocate turrets to NEW_MAX_TURRETS + reallocate_turrets(); + } + }; +} + +REGISTER_COMPONENT(turret::component) diff --git a/t6/plugins/tws-t6-gsc-utils/src/dllmain.cpp b/t6/plugins/tws-t6-gsc-utils/src/dllmain.cpp new file mode 100644 index 0000000..e7c8b87 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/dllmain.cpp @@ -0,0 +1,78 @@ +#include +#include "loader/component_loader.hpp" + +#include "game/game.hpp" + +#include "plugin.hpp" + +#include +#include +#include +#include +#include + +namespace +{ + utils::hook::detour load_library_hook; + + std::string extract_resource(const std::string& name, const int resource) + { + const auto data = utils::nt::load_resource(resource); + const auto path = std::filesystem::current_path() / "tmp" / name; + const auto path_str = path.generic_string(); + + if (!utils::io::write_file(path_str, data)) + { + const auto current = utils::io::read_file(path_str); + const auto hash_current = utils::cryptography::md5::compute(current); + const auto hash_target = utils::cryptography::md5::compute(data); + if (hash_target != hash_current) + { + throw std::runtime_error("failed to extract libmysql.dll!"); + } + } + + return path_str; + } + + HMODULE __stdcall load_library_stub(LPCSTR lib_name, HANDLE file, DWORD flags) + { + if (lib_name == "libmysql.dll"s) + { + const auto path = extract_resource(lib_name, LIBMYSQL_DLL); + const auto handle = load_library_hook.invoke_pascal(path.data(), file, flags); + + if (handle != nullptr) + { + return handle; + } + else + { + throw std::runtime_error(std::format("failed to load libmysql.dll: {}", GetLastError())); + } + } + + return load_library_hook.invoke_pascal(lib_name, file, flags); + } +} + +PLUTONIUM_API plutonium::sdk::plugin* PLUTONIUM_CALLBACK on_initialize() +{ + return plugin::get(); +} + +BOOL APIENTRY DllMain(HMODULE module, DWORD ul_reason_for_call, LPVOID /*reserved*/) +{ + if (ul_reason_for_call == DLL_PROCESS_ATTACH) + { + utils::nt::library::set_current_handle(module); + load_library_hook.create(LoadLibraryExA, load_library_stub); + } + + if (ul_reason_for_call == DLL_PROCESS_DETACH) + { + component_loader::on_shutdown(); + } + + return TRUE; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/exception/minidump.cpp b/t6/plugins/tws-t6-gsc-utils/src/exception/minidump.cpp new file mode 100644 index 0000000..9160d96 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/exception/minidump.cpp @@ -0,0 +1,94 @@ +#include +#include "minidump.hpp" + +#include +#pragma comment(lib, "dbghelp.lib") + +#include + +namespace exception +{ + namespace + { + constexpr MINIDUMP_TYPE get_minidump_type() + { + const auto type = MiniDumpIgnoreInaccessibleMemory // + | MiniDumpWithHandleData // + | MiniDumpScanMemory // + | MiniDumpWithProcessThreadData // + | MiniDumpWithFullMemoryInfo // + | MiniDumpWithThreadInfo // + | MiniDumpWithUnloadedModules; + + return static_cast(type); + } + + std::string get_temp_filename() + { + char filename[MAX_PATH] = {0}; + char pathname[MAX_PATH] = {0}; + + GetTempPathA(sizeof(pathname), pathname); + GetTempFileNameA(pathname, "Plutonium T6 -", 0, filename); + return filename; + } + + HANDLE write_dump_to_temp_file(const LPEXCEPTION_POINTERS exceptioninfo) + { + MINIDUMP_EXCEPTION_INFORMATION minidump_exception_info = {GetCurrentThreadId(), exceptioninfo, FALSE}; + + auto* const file_handle = CreateFileA(get_temp_filename().data(), GENERIC_WRITE | GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, + FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, + nullptr); + + if (!MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), file_handle, get_minidump_type(), + &minidump_exception_info, + nullptr, + nullptr)) + { + MessageBoxA(nullptr, "There was an error creating the minidump! Hit OK to close the program.", + "Minidump Error", MB_OK | MB_ICONERROR); + TerminateProcess(GetCurrentProcess(), 123); + } + + return file_handle; + } + + std::string read_file(HANDLE file_handle) + { + FlushFileBuffers(file_handle); + SetFilePointer(file_handle, 0, nullptr, FILE_BEGIN); + + std::string buffer{}; + + DWORD bytes_read = 0; + char temp_bytes[0x2000]; + + do + { + if (!ReadFile(file_handle, temp_bytes, sizeof(temp_bytes), &bytes_read, nullptr)) + { + return {}; + } + + buffer.append(temp_bytes, bytes_read); + } + while (bytes_read == sizeof(temp_bytes)); + + return buffer; + } + } + + std::string create_minidump(const LPEXCEPTION_POINTERS exceptioninfo) + { + auto* const file_handle = write_dump_to_temp_file(exceptioninfo); + + const auto _ = gsl::finally([file_handle]() + { + CloseHandle(file_handle); + }); + + return read_file(file_handle); + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/exception/minidump.hpp b/t6/plugins/tws-t6-gsc-utils/src/exception/minidump.hpp new file mode 100644 index 0000000..9378639 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/exception/minidump.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace exception +{ + std::string create_minidump(LPEXCEPTION_POINTERS exceptioninfo); +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/game.cpp b/t6/plugins/tws-t6-gsc-utils/src/game/game.cpp new file mode 100644 index 0000000..ab9cf81 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/game.cpp @@ -0,0 +1,85 @@ +#include +#include "game.hpp" + +#include + +namespace game +{ + gamemode current = reinterpret_cast(0xC2F028) == "multiplayer"s + ? gamemode::multiplayer + : gamemode::zombies; + + namespace environment + { + bool t6mp() + { + return current == gamemode::multiplayer; + } + + bool t6zm() + { + return current == gamemode::zombies; + } + } + + gentity_s* GetEntity(scr_entref_t entref) + { + if (entref.classnum != 0) + { + game::Scr_ObjectError(SCRIPTINSTANCE_SERVER, "Not an entity"); + return nullptr; + } + + assert(entref.entnum < (1 << 10)); + + return &game::g_entities[entref.entnum]; + } + + scr_entref_t Scr_GetEntityIdRef(unsigned int ent_id) + { + scr_entref_t entref{}; + + const auto v2 = &game::scr_VarGlob->objectVariableValue[ent_id]; + + entref.entnum = v2->u.f.next; + entref.classnum = gsl::narrow_cast(v2->w.classnum >> 8); + + return entref; + } + + void Scr_TerminateWaitThread(scriptInstance_t inst, unsigned int local_id, unsigned int start_local_id) + { + static const auto func = utils::hook::assemble([](utils::hook::assembler& a) + { + a.pushad(); + a.mov(esi, dword_ptr(esp, 0x24)); + a.push(dword_ptr(esp, 0x28)); + a.push(dword_ptr(esp, 0x2C)); + a.call(SELECT(0x8F4620, 0x8F3380)); + a.add(esp, 0x8); + a.popad(); + + a.ret(); + }); + + utils::hook::invoke(func, inst, local_id, start_local_id); + } + + void Scr_TerminateWaittillThread(scriptInstance_t inst, unsigned int local_id, unsigned int start_local_id) + { + static const auto func = utils::hook::assemble([](utils::hook::assembler& a) + { + a.pushad(); + a.mov(esi, dword_ptr(esp, 0x24)); + a.mov(edi, dword_ptr(esp, 0x28)); + a.push(dword_ptr(esp, 0x2C)); + a.call(SELECT(0x8F4750, 0x8F34B0)); + a.add(esp, 0x4); + a.popad(); + + a.ret(); + }); + + utils::hook::invoke(func, inst, local_id, start_local_id); + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/game.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/game.hpp new file mode 100644 index 0000000..2e9d4dc --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/game.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "structs.hpp" + +#define SELECT(mp, zm) (game::environment::t6mp() ? mp : zm) + +namespace game +{ + enum gamemode + { + none, + multiplayer, + zombies + }; + + extern gamemode current; + + namespace environment + { + bool t6mp(); + bool t6zm(); + } + + template + class symbol + { + public: + symbol(const size_t t6mp, const size_t t6zm) + : t6mp_(reinterpret_cast(t6mp)) + , t6zm_(reinterpret_cast(t6zm)) + { + } + + T* get() const + { + if (environment::t6mp()) + { + return t6mp_; + } + + return t6zm_; + } + + void set(const size_t ptr) + { + this->t6mp_ = reinterpret_cast(ptr); + this->t6zm_ = reinterpret_cast(ptr); + } + + operator T* () const + { + return this->get(); + } + + T* operator->() const + { + return this->get(); + } + + private: + T* t6mp_; + T* t6zm_; + }; + + gentity_s* GetEntity(scr_entref_t entref); + scr_entref_t Scr_GetEntityIdRef(unsigned int entId); + void Scr_TerminateWaitThread(scriptInstance_t inst, unsigned int localId, unsigned int startLocalId); + void Scr_TerminateWaittillThread(scriptInstance_t inst, unsigned int localId, unsigned int startLocalId); +} + +#include "symbols.hpp" diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/array.cpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/array.cpp new file mode 100644 index 0000000..2df03e1 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/array.cpp @@ -0,0 +1,354 @@ +#include + +#include "array.hpp" +#include "execution.hpp" + +namespace scripting +{ + array_value::array_value(const array* array, const script_value& key) + : array_(array) + , key_(key) + { + const auto value = this->array_->get(key); + this->script_value::operator=(value); + } + + void array_value::operator=(const script_value& value) + { + this->array_->set(this->key_, value); + this->script_value::operator=(value); + } + + array::array(const std::uint32_t id) + : id_(id) + { + this->add(); + } + + array::array(const array& other) + { + this->operator=(other); + } + + array::array(array&& other) noexcept + { + this->id_ = other.id_; + other.id_ = 0; + } + + array::array() + { + this->id_ = make_array(); + } + + array::~array() + { + this->release(); + } + + array& array::operator=(const array& other) + { + if (&other != this) + { + this->release(); + this->id_ = other.id_; + this->add(); + } + + return *this; + } + + array& array::operator=(array&& other) noexcept + { + if (&other != this) + { + this->release(); + this->id_ = other.id_; + other.id_ = 0; + } + + return *this; + } + + void array::add() const + { + if (this->id_) + { + game::VariableValue value{}; + value.u.uintValue = this->id_; + value.type = game::SCRIPT_OBJECT; + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value); + } + } + + void array::release() const + { + if (this->id_) + { + game::VariableValue value{}; + value.u.uintValue = this->id_; + value.type = game::SCRIPT_OBJECT; + + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, value.type, value.u); + } + } + + std::vector array::get_keys() const + { + std::vector result; + + auto current = game::scr_VarGlob->objectVariableChildren[this->id_].firstChild; + + while (current) + { + const auto var = game::scr_VarGlob->childVariableValue[current]; + + if (var.type == game::SCRIPT_NONE) + { + current = var.nextSibling; + continue; + } + + const auto string_value = static_cast(var.name_lo) + (var.k.keys.name_hi << 8); + const auto* str = game::SL_ConvertToString(string_value); + + script_value key; + if (string_value < 0x40000 && str) + { + key = str; + } + else + { + key = (string_value - 0x800000) & 0xFFFFFF; + } + + result.push_back(key); + + current = var.nextSibling; + } + + return result; + } + + std::uint32_t array::size() const + { + return game::Scr_GetSelf(game::SCRIPTINSTANCE_SERVER, this->id_); + } + + std::uint32_t array::push_back(const script_value& value) const + { + this->set(this->size(), value); + return this->size(); + } + + void array::erase(const std::uint32_t index) const + { + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, (index - 0x800000) & 0xFFFFFF); + if (variable_id) + { + game::RemoveVariableValue(game::SCRIPTINSTANCE_SERVER, this->id_, variable_id); + } + } + + void array::erase(const std::string& key) const + { + const auto string_value = game::SL_GetString(key.data(), 0); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + if (variable_id) + { + game::RemoveVariableValue(game::SCRIPTINSTANCE_SERVER, this->id_, variable_id); + } + } + + void array::erase(const script_value& key) const + { + if (key.is()) + { + return this->erase(key.as()); + } + + if (key.is()) + { + return this->erase(key.as()); + } + } + + void array::erase(const array_iterator& iter) const + { + this->erase(iter->first); + } + + script_value array::get(const std::string& key) const + { + const auto string_value = game::SL_GetString(key.data(), 0); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + + if (!variable_id) + { + return {}; + } + + const auto value = game::scr_VarGlob->childVariableValue[variable_id]; + game::VariableValue variable{}; + variable.u = value.u.u; + variable.type = static_cast(value.type); + + return variable; + } + + script_value array::get(const std::uint32_t index) const + { + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, (index - 0x800000) & 0xFFFFFF); + + if (!variable_id) + { + return {}; + } + + const auto value = game::scr_VarGlob->childVariableValue[variable_id]; + game::VariableValue variable{}; + variable.u = value.u.u; + variable.type = static_cast(value.type); + + return variable; + } + + script_value array::get(const script_value& key) const + { + if (key.is()) + { + return this->get(key.as()); + } + + if (key.is()) + { + return this->get(key.as()); + } + + throw std::runtime_error(std::format("invalid key type '{}'", key.type_name())); + } + + void array::set(const std::string& key, const script_value& value) const + { + const auto variable_id = this->get_value_id(key); + + if (!variable_id) + { + return; + } + + const auto& value_raw = value.get_raw(); + const auto variable = &game::scr_VarGlob->childVariableValue[variable_id]; + game::VariableValue variable_value{}; + variable_value.type = variable->type; + variable_value.u = variable->u.u; + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value_raw); + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, variable_value.type, variable_value.u); + + variable->type = gsl::narrow_cast(value_raw.type); + variable->u.u = value_raw.u; + } + + void array::set(const std::uint32_t index, const script_value& value) const + { + const auto variable_id = this->get_value_id(index); + + if (!variable_id) + { + return; + } + + const auto& value_raw = value.get_raw(); + const auto variable = &game::scr_VarGlob->childVariableValue[variable_id]; + game::VariableValue variable_value{}; + variable_value.type = variable->type; + variable_value.u = variable->u.u; + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value_raw); + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, variable_value.type, variable_value.u); + + variable->type = gsl::narrow_cast(value_raw.type); + variable->u.u = value_raw.u; + } + + void array::set(const script_value& key, const script_value& value) const + { + if (key.is()) + { + return this->set(key.as(), value); + } + + if (key.is()) + { + return this->set(key.as(), value); + } + + throw std::runtime_error(std::format("invalid key type '{}'", key.type_name())); + } + + std::uint32_t array::get_entity_id() const + { + return this->id_; + } + + std::uint32_t array::get_value_id(const std::string& key) const + { + const auto string_value = game::SL_GetString(key.data(), 0); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + + if (!variable_id) + { + return game::GetNewVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + } + + return variable_id; + } + + std::uint32_t array::get_value_id(const std::uint32_t index) const + { + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, (index - 0x800000) & 0xFFFFFF); + if (!variable_id) + { + return game::GetNewArrayVariable(game::SCRIPTINSTANCE_SERVER, this->id_, index); + } + + return variable_id; + } + + entity array::get_raw() const + { + return entity(this->id_); + } + + array_value array::operator[](const script_value& key) const + { + return array_value(this, key); + } + + array_iterator array::begin() const + { + const auto keys = this->get_keys(); + return array_iterator(this, keys, 0); + } + + array_iterator array::end() const + { + return array_iterator(this); + } + + array_iterator array::find(const script_value& key) const + { + const auto keys = this->get_keys(); + for (auto i = 0u; i < keys.size(); i++) + { + if (keys[i] == key) + { + return array_iterator(this, keys, i); + } + } + + return array_iterator(this); + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/array.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/array.hpp new file mode 100644 index 0000000..3428aff --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/array.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include "script_value.hpp" +#include "container_iterator.hpp" + +namespace scripting +{ + class array; + + class array_value : public script_value + { + public: + array_value(const array* array, const script_value& key); + void operator=(const script_value& value); + + template + T as() const + { + try + { + return script_value::as(); + } + catch (const std::exception& e) + { + throw std::runtime_error(std::format("array value '{}' {}", this->key_.to_string(), e.what())); + } + } + + private: + script_value key_; + const array* array_; + + }; + + template + class array_iterator_base : public IteratorType + { + public: + array_iterator_base(const array* container) + : IteratorType(container) + { + } + + array_iterator_base(const array* container, const std::vector& keys, const std::int64_t key_index) + : IteratorType(container, keys, key_index) + { + } + }; + + using array_iterator = array_iterator_base>; + + class array final + { + public: + array(); + array(const std::uint32_t id); + + array(const array& other); + array(array&& other) noexcept; + + ~array(); + + array& operator=(const array& other); + array& operator=(array&& other) noexcept; + + std::vector get_keys() const; + std::uint32_t size() const; + + std::uint32_t push_back(const script_value& value) const; + + template + void emplace_back(Args&&... args) + { + this->push_back(std::forward(args)...); + } + + void erase(const std::uint32_t index) const; + void erase(const std::string& key) const; + void erase(const script_value& key) const; + void erase(const array_iterator& iter) const; + + script_value get(const script_value&) const; + script_value get(const std::string&) const; + script_value get(const std::uint32_t) const; + + void set(const script_value&, const script_value&) const; + void set(const std::string&, const script_value&) const; + void set(const std::uint32_t, const script_value&) const; + + std::uint32_t get_entity_id() const; + + std::uint32_t get_value_id(const std::string&) const; + std::uint32_t get_value_id(std::uint32_t) const; + + entity get_raw() const; + + array_value operator[](const script_value& key) const; + + array_iterator begin() const; + array_iterator end() const; + array_iterator find(const script_value& key) const; + + private: + void add() const; + void release() const; + + std::uint32_t id_{}; + + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/container_iterator.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/container_iterator.hpp new file mode 100644 index 0000000..3278177 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/container_iterator.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include "script_value.hpp" + +namespace scripting +{ + template + class container_iterator + { + public: + static constexpr std::int64_t container_iterator_end_key = -1; + + container_iterator(const ContainerType* container) + : container_(container) + , key_index_(container_iterator_end_key) + { + } + + container_iterator(const ContainerType* container, const std::vector& keys, const std::int64_t key_index) + : container_(container) + , keys_(keys) + , key_index_(key_index) + { + this->update_pair(); + } + + std::pair& operator*() + { + return this->pair_.value(); + } + + std::pair* operator->() + { + return &this->pair_.value(); + } + + const std::pair& operator*() const + { + return this->pair_.value(); + } + + const std::pair* operator->() const + { + return &this->pair_.value(); + } + + container_iterator& operator++() + { + if (this->key_index_ == container_iterator_end_key) + { + return *this; + } + + const auto size = this->keys_.size(); + if (this->key_index_ + 1 >= size) + { + this->key_index_ = container_iterator_end_key; + return *this; + } + + this->key_index_++; + this->update_pair(); + + return *this; + } + + container_iterator operator++(int) + { + const auto pre = *this; + this->operator++(); + return pre; + } + + friend bool operator==(const container_iterator& a, const container_iterator& b) + { + return a.container_ == b.container_ && a.key_index_ == b.key_index_; + }; + + friend bool operator!=(const container_iterator& a, const container_iterator& b) + { + return a.container_ != b.container_ || a.key_index_ != b.key_index_; + }; + + private: + void update_pair() + { + const auto index = static_cast(this->key_index_); + const auto& key = this->keys_[index]; + const auto value = this->container_->operator[](key); + this->pair_.emplace(std::make_pair(key, value)); + } + + const ContainerType* container_; + std::int64_t key_index_; + std::vector keys_; + std::optional> pair_; + + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/entity.cpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/entity.cpp new file mode 100644 index 0000000..c8edd81 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/entity.cpp @@ -0,0 +1,123 @@ +#include +#include "entity.hpp" +#include "script_value.hpp" +#include "execution.hpp" + +namespace scripting +{ + entity::entity() + : entity(0) + { + } + + entity::entity(const entity& other) : entity(other.entity_id_) + { + } + + entity::entity(entity&& other) noexcept + { + this->entity_id_ = other.entity_id_; + other.entity_id_ = 0; + } + + entity::entity(const unsigned int entity_id) + : entity_id_(entity_id) + { + this->add(); + } + + entity::~entity() + { + this->release(); + } + + entity& entity::operator=(const entity& other) + { + if (&other != this) + { + this->release(); + this->entity_id_ = other.entity_id_; + this->add(); + } + + return *this; + } + + entity& entity::operator=(entity&& other) noexcept + { + if (&other != this) + { + this->release(); + this->entity_id_ = other.entity_id_; + other.entity_id_ = 0; + } + + return *this; + } + + unsigned int entity::get_entity_id() const + { + return this->entity_id_; + } + + game::scr_entref_t entity::get_entity_reference() const + { + if (!this->entity_id_) + { + const auto not_null = static_cast(~0ui16); + return game::scr_entref_t{not_null, not_null}; + } + + return game::Scr_GetEntityIdRef(this->get_entity_id()); + } + + bool entity::operator==(const entity& other) const noexcept + { + return this->get_entity_id() == other.get_entity_id(); + } + + bool entity::operator!=(const entity& other) const noexcept + { + return !this->operator==(other); + } + + void entity::add() const + { + if (this->entity_id_) + { + game::VariableValue value; + value.type = game::SCRIPT_OBJECT; + value.u.uintValue = this->entity_id_; + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value); + } + } + + void entity::release() const + { + if (this->entity_id_) + { + game::VariableValue value; + value.type = game::SCRIPT_OBJECT; + value.u.uintValue = this->entity_id_; + + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, value.type, value.u); + } + } + + void entity::set(const std::string& field, const script_value& value) const + { + set_entity_field(*this, field, value); + } + + template <> + script_value entity::get(const std::string& field) const + { + return get_entity_field(*this, field); + } + + script_value entity::call(const std::string& name, const std::vector& arguments) const + { + return call_function(name, *this, arguments); + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/entity.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/entity.hpp new file mode 100644 index 0000000..76fd66c --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/entity.hpp @@ -0,0 +1,49 @@ +#pragma once +#include "game/game.hpp" +#include "script_value.hpp" + +namespace scripting +{ + class entity final + { + public: + entity(); + entity(unsigned int entity_id); + + entity(const entity& other); + entity(entity&& other) noexcept; + + ~entity(); + + entity& operator=(const entity& other); + entity& operator=(entity&& other) noexcept; + + void set(const std::string& field, const script_value& value) const; + + template + T get(const std::string& field) const; + + script_value call(const std::string& name, const std::vector& arguments = {}) const; + + unsigned int get_entity_id() const; + game::scr_entref_t get_entity_reference() const; + + bool operator ==(const entity& other) const noexcept; + bool operator !=(const entity& other) const noexcept; + + private: + unsigned int entity_id_; + + void add() const; + void release() const; + }; + + template <> + script_value entity::get(const std::string& field) const; + + template + T entity::get(const std::string& field) const + { + return this->get(field).as(); + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/event.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/event.hpp new file mode 100644 index 0000000..bc1d53e --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/event.hpp @@ -0,0 +1,13 @@ +#pragma once +#include "script_value.hpp" +#include "entity.hpp" + +namespace scripting +{ + struct event + { + std::string name; + entity entity{}; + std::vector arguments; + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/execution.cpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/execution.cpp new file mode 100644 index 0000000..5476f68 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/execution.cpp @@ -0,0 +1,210 @@ +#include +#include "execution.hpp" +#include "safe_execution.hpp" +#include "stack_isolation.hpp" + +#include "component/scripting.hpp" + +namespace scripting +{ + namespace + { + game::VariableValue* allocate_argument() + { + game::VariableValue* value_ptr = ++game::scr_VmPub->top; + ++game::scr_VmPub->inparamcount; + return value_ptr; + } + + script_value get_return_value() + { + if (game::scr_VmPub->inparamcount == 0) + { + return {}; + } + + game::Scr_ClearOutParams(game::SCRIPTINSTANCE_SERVER); + game::scr_VmPub->outparamcount = game::scr_VmPub->inparamcount; + game::scr_VmPub->inparamcount = 0; + + return script_value(game::scr_VmPub->top[1 - game::scr_VmPub->outparamcount]); + } + + int get_field_id(const int classnum, const std::string& field) + { + if (scripting::fields_table[classnum].find(field) != scripting::fields_table[classnum].end()) + { + return scripting::fields_table[classnum][field]; + } + + return -1; + } + } + + void push_value(const script_value& value) + { + auto* value_ptr = allocate_argument(); + *value_ptr = value.get_raw(); + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, value_ptr); + } + + void notify(const entity& entity, const std::string& event, const std::vector& arguments) + { + stack_isolation _; + for (auto i = arguments.rbegin(); i != arguments.rend(); ++i) + { + push_value(*i); + } + + const auto event_id = game::SL_GetString(event.data(), 0); + game::Scr_NotifyId(game::SCRIPTINSTANCE_SERVER, 0, entity.get_entity_id(), event_id, game::scr_VmPub->inparamcount); + } + + script_value call_function(const std::string& name, const entity& entity, + const std::vector& arguments) + { + const auto entref = entity.get_entity_reference(); + const auto function = find_function_ptr(name); + if (!function) + { + throw std::runtime_error("Unknown function '" + name + "'"); + } + + stack_isolation _; + + for (auto i = arguments.rbegin(); i != arguments.rend(); ++i) + { + push_value(*i); + } + + game::scr_VmPub->outparamcount = game::scr_VmPub->inparamcount; + game::scr_VmPub->inparamcount = 0; + + if (!safe_execution::call(function, entref)) + { + const auto error = reinterpret_cast(SELECT(0x2E27C70, 0x2DF7F70)); + throw std::runtime_error(error); + } + + return get_return_value(); + } + + script_value call_function(const std::string& name, const std::vector& arguments) + { + return call_function(name, entity(), arguments); + } + + template <> + script_value call(const std::string& name, const std::vector& arguments) + { + return call_function(name, arguments); + } + + script_value exec_ent_thread(const entity& entity, const char* pos, const std::vector& arguments) + { + const auto id = entity.get_entity_id(); + + stack_isolation _; + for (auto i = arguments.rbegin(); i != arguments.rend(); ++i) + { + push_value(*i); + } + + game::VariableValue value{}; + value.type = game::SCRIPT_OBJECT; + value.u.uintValue = id; + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value); + + const auto local_id = game::AllocThread(game::SCRIPTINSTANCE_SERVER, id); + const auto result = game::VM_Execute(game::SCRIPTINSTANCE_SERVER, local_id, pos, arguments.size()); + game::RemoveRefToObject(game::SCRIPTINSTANCE_SERVER, result); + + return get_return_value(); + } + + script_value get_custom_field(const entity& entity, const std::string& field) + { + const object object = entity.get_entity_id(); + return object.get(field); + } + + void set_custom_field(const entity& entity, const std::string& field, const script_value& value) + { + const object object = entity.get_entity_id(); + object.set(field, value); + } + + void set_entity_field(const entity& entity, const std::string& field, const script_value& value) + { + const auto entref = entity.get_entity_reference(); + const int id = get_field_id(entref.classnum, field); + + if (id != -1) + { + stack_isolation _; + push_value(value); + + game::scr_VmPub->outparamcount = game::scr_VmPub->inparamcount; + game::scr_VmPub->inparamcount = 0; + + if (!safe_execution::set_entity_field(entref, id)) + { + throw std::runtime_error("Failed to set value for field '" + field + "'"); + } + } + else + { + set_custom_field(entity, field, value); + } + } + + script_value get_entity_field(const entity& entity, const std::string& field) + { + const auto entref = entity.get_entity_reference(); + const auto id = get_field_id(entref.classnum, field); + + if (id != -1) + { + stack_isolation _; + + game::VariableValue value{}; + if (!safe_execution::get_entity_field(entref, id, &value)) + { + throw std::runtime_error("Failed to get value for field '" + field + "'"); + } + + const auto __ = gsl::finally([&value]() + { + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, value.type, value.u); + }); + + return value; + } + else + { + return get_custom_field(entity, field); + } + } + + unsigned int make_array() + { + unsigned int index = 0; + const auto variable = game::AllocVariable(game::SCRIPTINSTANCE_SERVER, &index); + variable->w.type = game::SCRIPT_ARRAY; + variable->u.f.prev = 0; + variable->u.f.next = 0; + + return index; + } + + unsigned int make_object() + { + unsigned int index = 0; + const auto variable = game::AllocVariable(game::SCRIPTINSTANCE_SERVER, &index); + variable->w.type = game::SCRIPT_STRUCT; + variable->u.f.prev = 0; + + return index; + } +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/execution.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/execution.hpp new file mode 100644 index 0000000..a5f8bd5 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/execution.hpp @@ -0,0 +1,39 @@ +#pragma once +#include "game/game.hpp" +#include "entity.hpp" +#include "array.hpp" +#include "object.hpp" +#include "function.hpp" +#include "thread.hpp" +#include "script_value.hpp" + +namespace scripting +{ + void push_value(const script_value& value); + + script_value call_function(const std::string& name, const std::vector& arguments); + script_value call_function(const std::string& name, const entity& entity, + const std::vector& arguments); + + template + T call(const std::string& name, const std::vector& arguments = {}); + + template <> + script_value call(const std::string& name, const std::vector& arguments); + + template + T call(const std::string& name, const std::vector& arguments) + { + return call(name, arguments).as(); + } + + script_value exec_ent_thread(const entity& entity, const char* pos, const std::vector& arguments); + + void set_entity_field(const entity& entity, const std::string& field, const script_value& value); + script_value get_entity_field(const entity& entity, const std::string& field); + + void notify(const entity& entity, const std::string& event, const std::vector& arguments); + + unsigned int make_array(); + unsigned int make_object(); +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/function.cpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/function.cpp new file mode 100644 index 0000000..d07f46a --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/function.cpp @@ -0,0 +1,41 @@ +#include +#include "function.hpp" +#include "execution.hpp" +#include "../../component/scripting.hpp" + +namespace scripting +{ + function::function(const char* pos) + : pos_(pos) + { + } + + script_value function::get_raw() const + { + game::VariableValue value; + value.type = game::SCRIPT_FUNCTION; + value.u.codePosValue = this->pos_; + + return value; + } + + const char* function::get_pos() const + { + return this->pos_; + } + + std::string function::get_name() const + { + return scripting::find_function(this->pos_); + } + + script_value function::call(const entity& self, std::vector arguments) const + { + if (this->pos_ == 0) + { + throw std::runtime_error("function pos not set"); + } + + return exec_ent_thread(self, this->pos_, arguments); + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/function.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/function.hpp new file mode 100644 index 0000000..487d475 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/function.hpp @@ -0,0 +1,36 @@ +#pragma once +#include "entity.hpp" +#include "script_value.hpp" + +namespace scripting +{ + class function + { + public: + function() = default; + function(const char*); + + script_value get_raw() const; + const char* get_pos() const; + std::string get_name() const; + + script_value call(const entity& self, std::vector arguments) const; + + script_value operator()(const entity& self, std::vector arguments) const + { + return this->call(self, arguments); + } + + script_value operator()(std::vector arguments) const + { + return this->call(*game::levelEntityId, arguments); + } + + script_value operator()() const + { + return this->call(*game::levelEntityId, {}); + } + private: + const char* pos_; + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/object.cpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/object.cpp new file mode 100644 index 0000000..8003f80 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/object.cpp @@ -0,0 +1,270 @@ +#include + +#include "object.hpp" +#include "execution.hpp" + +namespace scripting +{ + object_value::object_value(const object* object, const std::string& key) + : object_(object) + , key_(key) + { + const auto value = this->object_->get(key); + this->script_value::operator=(value); + } + + void object_value::operator=(const script_value& value) + { + this->object_->set(this->key_, value); + this->script_value::operator=(value); + } + + object::object(const std::uint32_t id) + : id_(id) + { + this->add(); + } + + object::object(const object& other) + { + this->operator=(other); + } + + object::object(object&& other) noexcept + { + this->id_ = other.id_; + other.id_ = 0; + } + + object::object() + { + this->id_ = make_object(); + } + + object::~object() + { + this->release(); + } + + object& object::operator=(const object& other) + { + if (&other != this) + { + this->release(); + this->id_ = other.id_; + this->add(); + } + + return *this; + } + + object& object::operator=(object&& other) noexcept + { + if (&other != this) + { + this->release(); + this->id_ = other.id_; + other.id_ = 0; + } + + return *this; + } + + void object::add() const + { + if (this->id_) + { + game::VariableValue value{}; + value.u.uintValue = this->id_; + value.type = game::SCRIPT_OBJECT; + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value); + } + } + + void object::release() const + { + if (this->id_) + { + game::VariableValue value{}; + value.u.uintValue = this->id_; + value.type = game::SCRIPT_OBJECT; + + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, value.type, value.u); + } + } + + void object::iterate_keys(const std::function& callback) const + { + auto current = game::scr_VarGlob->objectVariableChildren[this->id_].firstChild; + + while (current) + { + const auto var = game::scr_VarGlob->childVariableValue[current]; + const auto string_id = static_cast(var.name_lo) + (var.k.keys.name_hi << 8); + + // pluto has this table relocated + static const auto max_string_id = *reinterpret_cast(SELECT(0x6927A1 + 2, 0x51A9A1 + 2)); + if (string_id < max_string_id) + { + static const auto address = *reinterpret_cast(SELECT(0x635D7E + 3, 0x5466C5 + 3)); + const auto string = reinterpret_cast(address)[string_id]; + if (string != nullptr && callback(string)) + { + return; + } + } + + current = var.nextSibling; + } + } + + std::vector object::get_keys() const + { + std::vector result; + iterate_keys([&](const std::string& key) + { + result.emplace_back(key); + return false; + }); + + return result; + } + + std::optional object::get_next_key(const std::string& current) const + { + auto is_next = false; + std::optional next; + + iterate_keys([&](const std::string& key) + { + if (is_next) + { + next.emplace(key); + return true; + } + + if (key == current) + { + is_next = true; + } + + return false; + }); + + return next; + } + + std::uint32_t object::size() const + { + return game::Scr_GetSelf(game::SCRIPTINSTANCE_SERVER, this->id_); + } + + void object::erase(const std::string& key) const + { + const auto string_value = game::SL_GetCanonicalString(key.data(), 0); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + if (variable_id) + { + game::RemoveVariableValue(game::SCRIPTINSTANCE_SERVER, this->id_, variable_id); + } + } + + void object::erase(const object_iterator& iter) const + { + this->erase(iter->first); + } + + script_value object::get(const std::string& key) const + { + const auto string_value = game::SL_GetCanonicalString(key.data(), 0); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + + if (!variable_id) + { + return {}; + } + + const auto value = game::scr_VarGlob->childVariableValue[variable_id]; + game::VariableValue variable{}; + variable.u = value.u.u; + variable.type = static_cast(value.type); + + return variable; + } + + void object::set(const std::string& key, const script_value& value) const + { + const auto& value_raw = value.get_raw(); + const auto variable_id = this->get_value_id(key); + + if (!variable_id) + { + return; + } + + const auto variable = &game::scr_VarGlob->childVariableValue[variable_id]; + game::VariableValue variable_{}; + variable_.type = variable->type; + variable_.u = variable->u.u; + + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &value_raw); + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, variable_.type, variable_.u); + + variable->type = gsl::narrow_cast(value_raw.type); + variable->u.u = value_raw.u; + } + + std::uint32_t object::get_entity_id() const + { + return this->id_; + } + + std::uint32_t object::get_value_id(const std::string& key) const + { + const auto string_value = game::SL_GetCanonicalString(key.data(), 0); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + + if (!variable_id) + { + return game::GetNewVariable(game::SCRIPTINSTANCE_SERVER, this->id_, string_value); + } + + return variable_id; + } + + entity object::get_raw() const + { + return entity(this->id_); + } + + object_value object::operator[](const std::string& key) const + { + return object_value(this, key); + } + + object_iterator object::begin() const + { + const auto keys = this->get_keys(); + return object_iterator(this, keys, 0); + } + + object_iterator object::end() const + { + return object_iterator(this); + } + + object_iterator object::find(const std::string& key) const + { + const auto keys = this->get_keys(); + for (auto i = 0u; i < keys.size(); i++) + { + if (keys[i] == key) + { + return object_iterator(this, keys, i); + } + } + + return object_iterator(this); + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/object.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/object.hpp new file mode 100644 index 0000000..c695d18 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/object.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include "script_value.hpp" +#include "container_iterator.hpp" + +namespace scripting +{ + class object; + + class object_value : public script_value + { + public: + object_value(const object* object, const std::string& key); + void operator=(const script_value& value); + + template + T as() const + { + try + { + return script_value::as(); + } + catch (const std::exception& e) + { + throw std::runtime_error(std::format("object field '{}' {}", this->key_, e.what())); + } + } + + private: + const object* object_; + std::string key_; + + }; + + template + class object_iterator_base : public IteratorType + { + public: + object_iterator_base(const object* container) + : IteratorType(container) + { + } + + object_iterator_base(const object* container, const std::vector& keys, const std::int64_t key_index) + : IteratorType(container, keys, key_index) + { + } + }; + + using object_iterator = object_iterator_base>; + + class object final + { + public: + object(); + object(const std::uint32_t id); + + object(const object& other); + object(object&& other) noexcept; + + ~object(); + + object& operator=(const object& other); + object& operator=(object&& other) noexcept; + + void iterate_keys(const std::function& callback) const; + std::vector get_keys() const; + std::optional get_next_key(const std::string& current) const; + + std::uint32_t size() const; + void erase(const std::string& key) const; + void erase(const object_iterator& iter) const; + + script_value get(const std::string&) const; + void set(const std::string&, const script_value&) const; + + std::uint32_t get_entity_id() const; + std::uint32_t get_value_id(const std::string&) const; + + entity get_raw() const; + + object_value operator[](const std::string& key) const; + + object_iterator begin() const; + object_iterator end() const; + object_iterator find(const std::string& key) const; + + private: + void add() const; + void release() const; + + std::uint32_t id_{}; + + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/safe_execution.cpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/safe_execution.cpp new file mode 100644 index 0000000..8dfe4bd --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/safe_execution.cpp @@ -0,0 +1,67 @@ +#include +#include "safe_execution.hpp" + +#pragma warning(push) +#pragma warning(disable: 4611) + +namespace scripting::safe_execution +{ + bool execute_with_seh(const script_function function, const game::scr_entref_t& entref) + { + __try + { + function(entref); + return true; + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + return false; + } + } + + bool call(const script_function function, const game::scr_entref_t& entref) + { + *game::g_script_error_level += 1; + if (game::_setjmp(&game::g_script_error[*game::g_script_error_level], 0, 0, 0)) + { + *game::g_script_error_level -= 1; + return false; + } + + const auto result = execute_with_seh(function, entref); + *game::g_script_error_level -= 1; + return result; + } + + bool set_entity_field(const game::scr_entref_t& entref, const int offset) + { + *game::g_script_error_level += 1; + if (game::_setjmp(&game::g_script_error[*game::g_script_error_level], 0, 0, 0)) + { + *game::g_script_error_level -= 1; + return false; + } + + game::Scr_SetObjectField(entref.classnum, entref.entnum, offset); + + *game::g_script_error_level -= 1; + return true; + } + + bool get_entity_field(const game::scr_entref_t& entref, const int offset, game::VariableValue* value) + { + *game::g_script_error_level += 1; + if (game::_setjmp(&game::g_script_error[*game::g_script_error_level], 0, 0, 0)) + { + value->type = game::SCRIPT_NONE; + value->u.intValue = 0; + *game::g_script_error_level -= 1; + return false; + } + + *value = game::GetEntityFieldValue(game::SCRIPTINSTANCE_SERVER, entref.classnum, entref.entnum, 0, offset); + + *game::g_script_error_level -= 1; + return true; + } +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/safe_execution.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/safe_execution.hpp new file mode 100644 index 0000000..7091973 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/safe_execution.hpp @@ -0,0 +1,14 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting::safe_execution +{ + using script_function = void(*)(game::scr_entref_t); + + bool execute_with_seh(const script_function function, const game::scr_entref_t& entref); + + bool call(script_function function, const game::scr_entref_t& entref); + + bool set_entity_field(const game::scr_entref_t& entref, int offset); + bool get_entity_field(const game::scr_entref_t& entref, int offset, game::VariableValue* value); +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/script_value.cpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/script_value.cpp new file mode 100644 index 0000000..2364d73 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/script_value.cpp @@ -0,0 +1,478 @@ +#include +#include "script_value.hpp" +#include "entity.hpp" +#include "array.hpp" +#include "function.hpp" +#include "object.hpp" + +namespace scripting +{ + /*********************************************** + * Constructors + **********************************************/ + + script_value::script_value(const game::VariableValue& value) + : value_(value) + { + } + + script_value::script_value(void* value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_INTEGER; + variable.u.intValue = reinterpret_cast(value); + + this->value_ = variable; + } + + script_value::script_value(const int value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_INTEGER; + variable.u.intValue = value; + + this->value_ = variable; + } + + script_value::script_value(const unsigned int value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_INTEGER; + variable.u.uintValue = value; + + this->value_ = variable; + } + + script_value::script_value(const bool value) + : script_value(static_cast(value)) + { + } + + script_value::script_value(const float value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_FLOAT; + variable.u.floatValue = value; + + this->value_ = variable; + } + + script_value::script_value(const double value) + : script_value(static_cast(value)) + { + } + + script_value::script_value(const char* value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_STRING; + variable.u.stringValue = game::SL_GetString(value, 0); + + const auto _ = gsl::finally([&variable]() + { + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, variable.type, variable.u); + }); + + this->value_ = variable; + } + + script_value::script_value(const std::string& value) + : script_value(value.data()) + { + } + + script_value::script_value(const entity& value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_OBJECT; + variable.u.pointerValue = value.get_entity_id(); + + this->value_ = variable; + } + + script_value::script_value(const array& value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_OBJECT; + variable.u.pointerValue = value.get_entity_id(); + + this->value_ = variable; + } + + script_value::script_value(const object& value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_OBJECT; + variable.u.pointerValue = value.get_entity_id(); + + this->value_ = variable; + } + + script_value::script_value(const function& value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_FUNCTION; + variable.u.codePosValue = value.get_pos(); + + this->value_ = variable; + } + + script_value::script_value(const vector& value) + { + game::VariableValue variable{}; + variable.type = game::SCRIPT_VECTOR; + variable.u.vectorValue = game::Scr_AllocVector(game::SCRIPTINSTANCE_SERVER, value); + + const auto _ = gsl::finally([&variable]() + { + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, variable.type, variable.u); + }); + + this->value_ = variable; + } + + /*********************************************** + * Integer + **********************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::SCRIPT_INTEGER; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + int script_value::get() const + { + return this->get_raw().u.intValue; + } + + template <> + unsigned int script_value::get() const + { + return this->get_raw().u.uintValue; + } + + template <> + bool script_value::get() const + { + return this->get_raw().u.uintValue != 0; + } + + template <> + unsigned short script_value::get() const + { + return static_cast(this->get_raw().u.uintValue); + } + + template <> + short script_value::get() const + { + return static_cast(this->get_raw().u.intValue); + } + + template <> + char script_value::get() const + { + return static_cast(this->get_raw().u.intValue); + } + + /*********************************************** + * Float + **********************************************/ + + template <> + bool script_value::is() const + { + const auto type = this->get_raw().type; + return type == game::SCRIPT_FLOAT || type == game::SCRIPT_INTEGER; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + float script_value::get() const + { + const auto type = this->get_raw().type; + if (type == game::SCRIPT_INTEGER) + { + return static_cast(this->get_raw().u.intValue); + } + + return this->get_raw().u.floatValue; + } + + template <> + double script_value::get() const + { + const auto type = this->get_raw().type; + if (type == game::SCRIPT_INTEGER) + { + return static_cast(this->get_raw().u.intValue); + } + + return static_cast(this->get_raw().u.floatValue); + } + + /*********************************************** + * String + **********************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::SCRIPT_STRING + || this->get_raw().type == game::SCRIPT_ISTRING; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + const char* script_value::get() const + { + return game::SL_ConvertToString(static_cast(this->get_raw().u.stringValue)); + } + + template <> + std::string script_value::get() const + { + return this->get(); + } + + /*********************************************** + * Entity + **********************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::SCRIPT_OBJECT; + } + + template <> + entity script_value::get() const + { + return entity(this->get_raw().u.pointerValue); + } + + /*********************************************** + * Array + **********************************************/ + + template <> + bool script_value::is() const + { + if (this->get_raw().type != game::SCRIPT_OBJECT) + { + return false; + } + + const auto id = this->get_raw().u.uintValue; + const auto type = game::scr_VarGlob->objectVariableValue[id].w.type & 0x7F; + + return type == game::SCRIPT_ARRAY; + } + + template <> + array script_value::get() const + { + return array(this->get_raw().u.uintValue); + } + + /*********************************************** + * Struct + **********************************************/ + + template <> + bool script_value::is() const + { + if (this->get_raw().type != game::SCRIPT_OBJECT) + { + return false; + } + + const auto id = this->get_raw().u.uintValue; + const auto type = game::scr_VarGlob->objectVariableValue[id].w.type & 0x7F; + + return type == game::SCRIPT_STRUCT || type == game::SCRIPT_ENTITY; + } + + template <> + object script_value::get() const + { + return object(this->get_raw().u.uintValue); + } + + /*********************************************** + * Function + **********************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::SCRIPT_FUNCTION; + } + + template <> + function script_value::get() const + { + return function(this->get_raw().u.codePosValue); + } + + + /*********************************************** + * Vector + **********************************************/ + + template <> + bool script_value::is() const + { + return this->get_raw().type == game::SCRIPT_VECTOR; + } + + template <> + bool script_value::is() const + { + return this->is(); + } + + template <> + vector script_value::get() const + { + return this->get_raw().u.vectorValue; + } + + /*********************************************** + * + **********************************************/ + + const game::VariableValue& script_value::get_raw() const + { + return this->value_.get(); + } + + std::string script_value::to_string() const + { + if (this->is()) + { + return utils::string::va("%i", this->as()); + } + + if (this->is()) + { + return utils::string::va("%f", this->as()); + } + + if (this->is()) + { + return this->as(); + } + + if (this->is()) + { + const auto vec = this->as(); + return utils::string::va("(%g, %g, %g)", + vec.get_x(), + vec.get_y(), + vec.get_z() + ); + } + + if (this->is()) + { + const auto func = this->as(); + const auto& name = func.get_name(); + return utils::string::va("[[ %s ]]", name.data()); + } + + return this->type_name(); + } + + function_argument::function_argument(const arguments& args, const script_value& value, const size_t index, const bool exists) + : script_value(value) + , values_(args) + , index_(index) + , exists_(exists) + { + + } + + function_arguments::function_arguments(const arguments& values) + : values_(values) + { + } + + variadic_args::variadic_args(const size_t begin) + : std::vector({}) + , begin_(begin) + { + } + + function_argument variadic_args::operator[](size_t index) const + { + if (index >= this->size()) + { + throw std::runtime_error(utils::string::va("parameter %d does not exist", this->begin_ + index)); + } + + return std::vector::operator[](index); + } + + function_argument function_arguments::operator[](const size_t index) const + { + if (index >= values_.size()) + { + return {values_, {}, index, false}; + } + + return {values_, values_[index], index, true}; + } + + arguments function_arguments::get_raw() const + { + return this->values_; + } + + size_t function_arguments::size() const + { + return this->values_.size(); + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/script_value.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/script_value.hpp new file mode 100644 index 0000000..93719d8 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/script_value.hpp @@ -0,0 +1,393 @@ +#pragma once +#include "game/game.hpp" +#include "variable_value.hpp" +#include "vector.hpp" + +#include + +namespace scripting +{ + class entity; + class array; + class object; + class function; + class script_value; + + namespace + { + std::unordered_map typenames = + { + {0, "undefined"}, + {1, "object"}, + {2, "string"}, + {3, "localized string"}, + {4, "vector"}, + {5, "hash"}, + {6, "float"}, + {7, "int"}, + {8, "codepos"}, + {9, "precodepos"}, + {10, "function"}, + {11, "stack"}, + {12, "animation"}, + {13, "developer codepos"}, + {14, "thread"}, + {15, "thread"}, + {16, "thread"}, + {17, "thread"}, + {18, "struct"}, + {19, "removed entity"}, + {20, "entity"}, + {21, "array"}, + {22, "removed thread"}, + {23, ""}, + {24, "thread list"}, + {25, "ent list"}, + }; + + std::string get_typename(const game::VariableValue& value) + { + if (value.type == game::SCRIPT_OBJECT) + { + const auto type = game::scr_VarGlob->objectVariableValue[value.u.uintValue].w.type & 0x7F; + return typenames[type]; + } + else + { + return typenames[value.type]; + } + } + + template + std::string get_c_typename() + { + auto& info = typeid(T); + + if (info == typeid(std::string)) + { + return "string"; + } + + if (info == typeid(const char*)) + { + return "string"; + } + + if (info == typeid(entity)) + { + return "entity"; + } + + if (info == typeid(array)) + { + return "array"; + } + + if (info == typeid(object)) + { + return "struct"; + } + + if (info == typeid(function)) + { + return "function"; + } + + if (info == typeid(vector)) + { + return "vector"; + } + + return info.name(); + } + } + + using arguments = std::vector; + + class script_value + { + public: + script_value() = default; + script_value(const game::VariableValue& value); + + script_value(void* value); + + script_value(int value); + script_value(unsigned int value); + script_value(bool value); + + script_value(float value); + script_value(double value); + + script_value(const char* value); + script_value(const std::string& value); + + script_value(const entity& value); + script_value(const array& value); + script_value(const object& value); + + script_value(const function& value); + + script_value(const vector& value); + + private: + template + T get() const + { + if constexpr (std::is_pointer::value) + { + if (this->is()) + { + return reinterpret_cast(this->as()); + } + } + + if constexpr (std::is_constructible::value) + { + if (this->is()) + { + return T(this->as()); + } + } + + throw std::runtime_error("Invalid type"); + } + public: + +#define ADD_TYPE(type) \ + template <> \ + bool is() const; \ +private: \ + template <> \ + type get<>() const; \ +public: \ + + template + bool is() const + { + if (std::is_pointer::value && this->is()) + { + return true; + } + + if (std::is_constructible::value && this->is()) + { + return true; + } + + return false; + } + + ADD_TYPE(bool) + ADD_TYPE(int) + ADD_TYPE(unsigned int) + ADD_TYPE(unsigned short) + ADD_TYPE(float) + ADD_TYPE(float*) + ADD_TYPE(double) + ADD_TYPE(const char*) + ADD_TYPE(std::string) + + ADD_TYPE(vector) + ADD_TYPE(array) + ADD_TYPE(object) + ADD_TYPE(function) + ADD_TYPE(entity) + ADD_TYPE(script_value) + + template + T as() const + { + if (!this->is()) + { + const auto type = get_typename(this->get_raw()); + const auto c_type = get_c_typename(); + throw std::runtime_error(utils::string::va("has type '%s' but should be '%s'", type.data(), c_type.data())); + } + + return get(); + } + + template + T as_or(const T& default_value) const + { + if (!this->is()) + { + return default_value; + } + + return get(); + } + + std::string type_name() const + { + return get_typename(this->get_raw()); + } + + template class C, class T, typename ArrayType = array> + script_value(const C>& container) + { + ArrayType array_{}; + + for (const auto& value : container) + { + array_.emplace_back(value); + } + + game::VariableValue value{}; + value.type = game::SCRIPT_OBJECT; + value.u.pointerValue = array_.get_entity_id(); + + this->value_ = value; + } + + template + arguments operator()(T... arguments) const + { + return this->as().call({arguments...}); + } + + std::string to_string() const; + + friend bool operator==(const script_value& a, const script_value& b) + { + const auto& value_raw_a = a.get_raw(); + const auto& value_raw_b = b.get_raw(); + + return value_raw_a.type != value_raw_b.type && value_raw_a.u.uintValue == value_raw_b.u.uintValue; + } + + const game::VariableValue& get_raw() const; + + variable_value value_{}; + + }; + + class function_argument; + + class variadic_args : public std::vector + { + public: + variadic_args(const size_t begin); + + function_argument operator[](size_t index) const; + private: + size_t begin_; + }; + + class function_argument : public script_value + { + public: + function_argument(const arguments& args, const script_value& value, const size_t index, const bool exists); + + template + T as() const + { + if (!this->exists_) + { + throw std::runtime_error(utils::string::va("parameter %d does not exist", this->index_)); + } + + try + { + return script_value::as(); + } + catch (const std::exception& e) + { + throw std::runtime_error(utils::string::va("parameter %d %s", + this->index_, e.what())); + } + } + + template + T* as_ptr() const + { + const auto value = script_value::as(); + + if (value == nullptr) + { + throw std::runtime_error("is null"); + } + + return reinterpret_cast(value); + } + + template <> + variadic_args as() const + { + variadic_args args{this->index_}; + + for (auto i = this->index_; i < this->values_.size(); i++) + { + args.emplace_back(this->values_, this->values_[i], i, true); + } + + return args; + } + + operator variadic_args() const + { + return this->as(); + } + + template class C, class T, class ArrayType = array> + operator C>() const + { + const auto container_type = get_c_typename>>(); + if (!script_value::is()) + { + const auto type = get_typename(this->get_raw()); + + throw std::runtime_error(utils::string::va("has type '%s' but should be '%s'", + type.data(), + container_type.data() + )); + } + + C> container{}; + const auto array = script_value::as(); + for (auto i = 0u; i < array.size(); i++) + { + try + { + container.emplace_back(array.get(i).as()); + } + catch (const std::exception& e) + { + throw std::runtime_error(utils::string::va("element %d of parameter %d of type '%s' %s", + i, this->index_, container_type.data(), e.what())); + } + } + + return container; + } + + template + operator T() const + { + return this->as(); + } + + private: + arguments values_{}; + size_t index_{}; + bool exists_{}; + + }; + + class function_arguments + { + public: + function_arguments(const arguments& values); + + function_argument operator[](const size_t index) const; + + arguments get_raw() const; + + size_t size() const; + + private: + arguments values_{}; + + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/stack_isolation.cpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/stack_isolation.cpp new file mode 100644 index 0000000..ee72c5c --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/stack_isolation.cpp @@ -0,0 +1,27 @@ +#include +#include "stack_isolation.hpp" + +namespace scripting +{ + stack_isolation::stack_isolation() + { + this->in_param_count_ = game::scr_VmPub->inparamcount; + this->out_param_count_ = game::scr_VmPub->outparamcount; + this->top_ = game::scr_VmPub->top; + this->max_stack_ = game::scr_VmPub->maxstack; + + game::scr_VmPub->top = this->stack_; + game::scr_VmPub->maxstack = &this->stack_[ARRAYSIZE(this->stack_) - 1]; + game::scr_VmPub->inparamcount = 0; + game::scr_VmPub->outparamcount = 0; + } + + stack_isolation::~stack_isolation() + { + game::Scr_ClearOutParams(game::SCRIPTINSTANCE_SERVER); + game::scr_VmPub->inparamcount = this->in_param_count_; + game::scr_VmPub->outparamcount = this->out_param_count_; + game::scr_VmPub->top = this->top_; + game::scr_VmPub->maxstack = this->max_stack_; + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/stack_isolation.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/stack_isolation.hpp new file mode 100644 index 0000000..8dffd4b --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/stack_isolation.hpp @@ -0,0 +1,25 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting +{ + class stack_isolation final + { + public: + stack_isolation(); + ~stack_isolation(); + + stack_isolation(stack_isolation&&) = delete; + stack_isolation(const stack_isolation&) = delete; + stack_isolation& operator=(stack_isolation&&) = delete; + stack_isolation& operator=(const stack_isolation&) = delete; + + private: + game::VariableValue stack_[512]{}; + + game::VariableValue* max_stack_; + game::VariableValue* top_; + unsigned int in_param_count_; + unsigned int out_param_count_; + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/thread.cpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/thread.cpp new file mode 100644 index 0000000..27d8327 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/thread.cpp @@ -0,0 +1,141 @@ +#include +#include "thread.hpp" +#include "execution.hpp" +#include "../../component/scripting.hpp" + +namespace scripting +{ + thread::thread(unsigned int id) + : id_(id) + , type_(game::scr_VarGlob->objectVariableValue[id].w.type & 0x7F) + { + } + + script_value thread::get_raw() const + { + game::VariableValue value; + value.type = game::SCRIPT_OBJECT; + value.u.uintValue = this->id_; + + return value; + } + + unsigned int thread::get_entity_id() const + { + return this->id_; + } + + unsigned int thread::get_type() const + { + return this->type_; + } + + unsigned int thread::get_wait_time() const + { + return game::scr_VarGlob->objectVariableValue[this->id_].w.waitTime >> 8; + } + + unsigned int thread::get_notify_name_id() const + { + return game::scr_VarGlob->objectVariableValue[this->id_].w.notifyName >> 8; + } + + unsigned int thread::get_self() const + { + return game::Scr_GetSelf(game::SCRIPTINSTANCE_SERVER, this->id_); + } + + std::string thread::get_notify_name() const + { + return game::SL_ConvertToString(this->get_notify_name_id()); + } + + const char* thread::get_pos() const + { + const auto start_local_id = game::GetStartLocalId(game::SCRIPTINSTANCE_SERVER, this->id_); + + if (this->type_ == game::SCRIPT_THREAD) + { + for (auto frame = game::scr_VmPub->function_frame; frame != game::scr_VmPub->function_frame_start; --frame) + { + if (frame->fs.localId == this->id_) + { + return frame->fs.pos; + } + } + } + + unsigned int stack_id = 0; + + if (this->type_ == game::SCRIPT_TIME_THREAD) + { + const auto waittime = this->get_wait_time(); + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, game::scr_VarPub->timeArrayId, waittime); + const auto object_id = game::scr_VarGlob->childVariableValue[variable_id].u.f.prev; + + stack_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, object_id, start_local_id + 0x10000); + } + + if (this->type_ == game::SCRIPT_NOTIFY_THREAD) + { + const auto notify_name = this->get_notify_name_id(); + + if (!notify_name) + { + stack_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, start_local_id, 0x16001); + } + else + { + const auto self = game::Scr_GetSelf(game::SCRIPTINSTANCE_SERVER, start_local_id); + + const auto variable_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, game::scr_VarPub->pauseArrayId, self + 0x10000); + const auto object_id = game::scr_VarGlob->childVariableValue[variable_id].u.f.prev; + + const auto variable_id2 = game::FindVariable(game::SCRIPTINSTANCE_SERVER, object_id, start_local_id + 0x10000); + const auto variable = &game::scr_VarGlob->childVariableValue[variable_id2]; + + const auto variable_id3 = game::FindVariable(game::SCRIPTINSTANCE_SERVER, variable->u.u.uintValue, 0x18000); + const auto object_id2 = game::scr_VarGlob->childVariableValue[variable_id3].u.f.prev; + + const auto variable_id4 = game::FindVariable(game::SCRIPTINSTANCE_SERVER, object_id2, notify_name); + const auto object_id3 = game::scr_VarGlob->childVariableValue[variable_id4].u.f.prev; + + stack_id = game::FindVariable(game::SCRIPTINSTANCE_SERVER, object_id3, start_local_id + 0x10000); + } + } + + const auto stack = &game::scr_VarGlob->childVariableValue[stack_id]; + if (stack && stack->type == 11) + { + return stack->u.u.stackValue->pos; + } + + return 0; + } + + const char* thread::get_start_pos() const + { + const auto current = this->get_pos(); + return scripting::find_function_start(current); + } + + void thread::kill() const + { + const auto start_local_id = game::GetStartLocalId(game::SCRIPTINSTANCE_SERVER, this->id_); + + if (this->type_ == game::SCRIPT_THREAD) + { + game::Scr_TerminateRunningThread(game::SCRIPTINSTANCE_SERVER, this->id_); + } + + if (this->type_ == game::SCRIPT_TIME_THREAD) + { + game::Scr_TerminateWaitThread(game::SCRIPTINSTANCE_SERVER, this->id_, start_local_id); + } + + if (this->type_ == game::SCRIPT_NOTIFY_THREAD) + { + game::Scr_TerminateWaittillThread(game::SCRIPTINSTANCE_SERVER, this->id_, start_local_id); + } + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/thread.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/thread.hpp new file mode 100644 index 0000000..29d8476 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/thread.hpp @@ -0,0 +1,32 @@ +#pragma once +#include "entity.hpp" +#include "script_value.hpp" + +namespace scripting +{ + class thread + { + public: + thread(unsigned int); + + script_value get_raw() const; + + unsigned int get_entity_id() const; + unsigned int get_type() const; + + unsigned int get_self() const; + unsigned int get_wait_time() const; + unsigned int get_notify_name_id() const; + std::string get_notify_name() const; + + const char* get_pos() const; + const char* get_start_pos() const; + + void kill() const; + + private: + unsigned int id_{}; + unsigned int type_{}; + + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/variable_value.cpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/variable_value.cpp new file mode 100644 index 0000000..cdd5e9b --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/variable_value.cpp @@ -0,0 +1,68 @@ +#include +#include "variable_value.hpp" + +namespace scripting +{ + variable_value::variable_value(const game::VariableValue& value) + { + this->assign(value); + } + + variable_value::variable_value(const variable_value& other) noexcept + { + this->operator=(other); + } + + variable_value::variable_value(variable_value&& other) noexcept + { + this->operator=(std::move(other)); + } + + variable_value& variable_value::operator=(const variable_value& other) noexcept + { + if (this != &other) + { + this->release(); + this->assign(other.value_); + } + + return *this; + } + + variable_value& variable_value::operator=(variable_value&& other) noexcept + { + if (this != &other) + { + this->release(); + this->value_ = other.value_; + other.value_.type = game::SCRIPT_NONE; + } + + return *this; + } + + variable_value::~variable_value() + { + this->release(); + } + + const game::VariableValue& variable_value::get() const + { + return this->value_; + } + + void variable_value::assign(const game::VariableValue& value) + { + this->value_ = value; + game::AddRefToValue(game::SCRIPTINSTANCE_SERVER, &this->value_); + } + + void variable_value::release() + { + if (this->value_.type != game::SCRIPT_NONE) + { + game::RemoveRefToValue(game::SCRIPTINSTANCE_SERVER, this->value_.type, this->value_.u); + this->value_.type = game::SCRIPT_NONE; + } + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/variable_value.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/variable_value.hpp new file mode 100644 index 0000000..7a96261 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/variable_value.hpp @@ -0,0 +1,27 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting +{ + class variable_value + { + public: + variable_value() = default; + variable_value(const game::VariableValue& value); + variable_value(const variable_value& other) noexcept; + variable_value(variable_value&& other) noexcept; + + variable_value& operator=(const variable_value& other) noexcept; + variable_value& operator=(variable_value&& other) noexcept; + + ~variable_value(); + + const game::VariableValue& get() const; + + private: + void assign(const game::VariableValue& value); + void release(); + + game::VariableValue value_{{0}, game::SCRIPT_NONE}; + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/vector.cpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/vector.cpp new file mode 100644 index 0000000..143e29a --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/vector.cpp @@ -0,0 +1,85 @@ +#include +#include "vector.hpp" + +namespace scripting +{ + vector::vector(const float* value) + { + for (auto i = 0; i < 3; ++i) + { + this->value_[i] = value[i]; + } + } + + vector::vector(const game::vec3_t& value) + : vector(&value[0]) + { + } + + vector::vector(const float x, const float y, const float z) + { + this->value_[0] = x; + this->value_[1] = y; + this->value_[2] = z; + } + + vector::operator game::vec3_t& () + { + return this->value_; + } + + vector::operator const game::vec3_t& () const + { + return this->value_; + } + + game::vec_t& vector::operator[](const size_t i) + { + if (i >= 3) + { + throw std::runtime_error("Out of bounds."); + } + + return this->value_[i]; + } + + const game::vec_t& vector::operator[](const size_t i) const + { + if (i >= 3) + { + throw std::runtime_error("Out of bounds."); + } + + return this->value_[i]; + } + + float vector::get_x() const + { + return this->operator[](0); + } + + float vector::get_y() const + { + return this->operator[](1); + } + + float vector::get_z() const + { + return this->operator[](2); + } + + void vector::set_x(const float value) + { + this->operator[](0) = value; + } + + void vector::set_y(const float value) + { + this->operator[](1) = value; + } + + void vector::set_z(const float value) + { + this->operator[](2) = value; + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/scripting/vector.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/vector.hpp new file mode 100644 index 0000000..0bb1595 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/scripting/vector.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "game/game.hpp" + +namespace scripting +{ + class vector final + { + public: + vector() = default; + vector(const float* value); + vector(const game::vec3_t& value); + vector(float x, float y, float z); + + operator game::vec3_t& (); + operator const game::vec3_t& () const; + + game::vec_t& operator[](size_t i); + const game::vec_t& operator[](size_t i) const; + + float get_x() const; + float get_y() const; + float get_z() const; + + void set_x(float value); + void set_y(float value); + void set_z(float value); + + private: + game::vec3_t value_{ 0 }; + }; +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/structs.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/structs.hpp new file mode 100644 index 0000000..1cae888 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/structs.hpp @@ -0,0 +1,1540 @@ +#pragma once + +namespace game +{ + typedef float vec_t; + typedef vec_t vec2_t[2]; + typedef vec_t vec3_t[3]; + typedef vec_t vec4_t[4]; + + enum scriptInstance_t + { + SCRIPTINSTANCE_SERVER, + SCRIPTINSTANCE_CLIENT, + }; + + struct scr_entref_t + { + unsigned short entnum; + unsigned short classnum; + int client; + }; + + struct BuiltinMethodDef + { + const char* actionString; + unsigned int constId; + int min_args; + int max_args; + void(__cdecl* actionFunc)(scr_entref_t); + int type; + }; + + struct BuiltinFunctionDef + { + const char* actionString; + unsigned int constId; + int min_args; + int max_args; + void(__cdecl* actionFunc)(); + int type; + }; + + enum scriptType_e + { + SCRIPT_NONE = 0, + SCRIPT_OBJECT = 1, + SCRIPT_STRING = 2, + SCRIPT_ISTRING = 3, + SCRIPT_VECTOR = 4, + SCRIPT_FLOAT = 6, + SCRIPT_INTEGER = 7, + SCRIPT_CODEPOS = 8, + SCRIPT_END = 9, + SCRIPT_FUNCTION = 10, + SCRIPT_THREAD = 0xE, + SCRIPT_NOTIFY_THREAD = 0xF, + SCRIPT_TIME_THREAD = 0x10, + SCRIPT_STRUCT = 18, + SCRIPT_ENTITY = 20, + SCRIPT_ARRAY = 21, + SCRIPT_FREE = 0x17, + }; + + struct VariableStackBuffer + { + const char* pos; + unsigned __int16 size; + unsigned __int16 bufLen; + unsigned __int16 localId; + char time; + char buf[1]; + }; + + union VariableUnion + { + int intValue; + unsigned int uintValue; + float floatValue; + unsigned int stringValue; + const float* vectorValue; + const char* codePosValue; + unsigned int pointerValue; + VariableStackBuffer* stackValue; + unsigned int entityOffset; + }; + + struct VariableValue + { + int type; + VariableUnion u; + }; + + struct function_stack_t + { + char* pos; + VariableValue* top; + unsigned int localId; + unsigned int localVarCount; + VariableValue* startTop; + }; + + struct function_frame_t + { + function_stack_t fs; + }; + + struct scrVmPub_t + { + unsigned int* localVars; + VariableValue* maxstack; + int function_count; + function_frame_t* function_frame; + VariableValue* top; + bool abort_on_error; + bool terminal_error; + bool block_execution; + unsigned int inparamcount; + unsigned int outparamcount; + function_frame_t function_frame_start[32]; + VariableValue stack[2048]; + void(__cdecl* notifyListeners[1])(unsigned int, unsigned int); + }; + + struct scrVarPub_t + { + const char* fieldBuffer; + bool developer; + bool developer_script; + bool evaluate; + const char* error_message; + int error_index; + unsigned int time; + unsigned int timeArrayId; + unsigned int pauseArrayId; + unsigned int levelId; + unsigned int gameId; + unsigned int animId; + unsigned int freeEntList; + unsigned int tempVariable; + bool bInited; + unsigned __int16 savecount; + unsigned int checksum; + unsigned int entId; + unsigned int entFieldName; + void* programHunkUser; + char* programBuffer; + char* endScriptBuffer; + unsigned __int16* saveIdMap; + unsigned __int16* saveIdMapRev; + unsigned int numScriptThreads; + unsigned int numScriptValues; + unsigned int numScriptObjects; + char* varUsagePos; + int ext_threadcount; + int totalObjectRefCount; + volatile int totalVectorRefCount; + int allocationCount; + }; + + struct ObjectVariableChildren + { + unsigned int firstChild; + unsigned int lastChild; + }; + + struct ObjectVariableValue_u_f + { + unsigned __int16 prev; + unsigned __int16 next; + }; + + union ObjectVariableValue_u_o_u + { + unsigned __int16 size; + unsigned __int16 entnum; + unsigned __int16 nextEntId; + unsigned __int16 self; + }; + + struct ObjectVariableValue_u_o + { + unsigned __int16 refCount; + ObjectVariableValue_u_o_u u; + }; + + union ObjectVariableValue_w + { + unsigned int type; + unsigned int classnum; + unsigned int notifyName; + unsigned int waitTime; + unsigned int parentLocalId; + }; + + struct ChildVariableValue_u_f + { + unsigned __int16 prev; + unsigned __int16 next; + }; + + union ChildVariableValue_u + { + ChildVariableValue_u_f f; + VariableUnion u; + }; + + struct ChildBucketMatchKeys_keys + { + unsigned __int16 name_hi; + unsigned __int16 parentId; + }; + + union ChildBucketMatchKeys + { + ChildBucketMatchKeys_keys keys; + unsigned int match; + }; + + struct ChildVariableValue + { + ChildVariableValue_u u; + unsigned int next; + char pad[4]; + char type; + char name_lo; + char _pad[2]; + ChildBucketMatchKeys k; + unsigned int nextSibling; + unsigned int prevSibling; + }; + + union ObjectVariableValue_u + { + ObjectVariableValue_u_f f; + ObjectVariableValue_u_o o; + }; + + struct ObjectVariableValue + { + ObjectVariableValue_u u; + ObjectVariableValue_w w; + }; + + struct scrVarGlob_t + { + ObjectVariableValue* objectVariableValue; + char __pad0[0x7C]; + ObjectVariableChildren* objectVariableChildren; + char __pad1[0x7C]; + unsigned __int16* childVariableBucket; + char __pad2[0x7C]; + ChildVariableValue* childVariableValue; + }; + + struct scr_classStruct_t + { + unsigned __int16 id; + unsigned __int16 entArrayId; + char charId; + const char* name; + }; + + enum gclientFlag + { + NOCLIP = 1 << 0, + UFO = 1 << 1, + }; + + enum entityFlag + { + FL_GODMODE = 1 << 0, + FL_DEMI_GODMODE = 1 << 1, + FL_NOTARGET = 1 << 2, + FL_SUPPORTS_LINKTO = 1 << 12, + }; // TODO: Finish + + struct gclient_s + { + char __pad0[0x18]; + int eflags; + char __pad1[0x5668]; + int flags; + }; + + struct EntHandle + { + unsigned __int16 number; + unsigned __int16 infoIndex; + }; + + /* 2885 */ + struct SentientHandle + { + unsigned __int16 number; + unsigned __int16 infoIndex; + }; + + /* 46 */ + enum team_t : __int32 + { + TEAM_FREE = 0x0, + TEAM_BAD = 0x0, + TEAM_ALLIES = 0x1, + TEAM_AXIS = 0x2, + TEAM_THREE = 0x3, + TEAM_FOUR = 0x4, + TEAM_FIVE = 0x5, + TEAM_SIX = 0x6, + TEAM_SEVEN = 0x7, + TEAM_EIGHT = 0x8, + TEAM_NUM_PLAYING_TEAMS = 0x9, + TEAM_SPECTATOR = 0x9, + TEAM_NUM_TEAMS = 0xA, + TEAM_LOCALPLAYERS = 0xB, + TEAM_FIRST_PLAYING_TEAM = 0x1, + TEAM_LAST_PLAYING_TEAM = 0x8, + }; + + /* 343 */ + enum TurretRotateState : __int32 + { + TURRET_ROTATE_STOPPED = 0x0, + TURRET_ROTATE_STOPPING = 0x1, + TURRET_ROTATE_MOVING = 0x2, + }; + + struct TurretInfo + { + bool inuse; + int state; + int flags; + int fireTime; + EntHandle manualTarget; + EntHandle target; + vec3_t targetPos; + int targetTime; + vec3_t missOffsetNormalized; + float arcmin[2]; + float arcmax[2]; + float initialYawmin; + float initialYawmax; + float forwardAngleDot; + float dropPitch; + float scanningPitch; + int convergenceTime[2]; + int suppressTime; + float maxRangeSquared; + SentientHandle detachSentient; + int stance; + int prevStance; + int fireSndDelay; + float accuracy; + vec3_t userOrigin; + int prevSentTarget; + float aiSpread; + float playerSpread; + team_t eTeam; + float heatVal; + bool overheating; + int fireBarrel; + float scanSpeed; + float scanDecelYaw; + int scanPauseTime; + vec3_t originError; + vec3_t anglesError; + float pitchCap; + int triggerDown; + unsigned int fireSnd; + unsigned int fireSndPlayer; + unsigned int startFireSnd; + unsigned int startFireSndPlayer; + unsigned int loopFireEnd; + unsigned int loopFireEndPlayer; + unsigned int rotateLoopSnd; + unsigned int rotateLoopSndPlayer; + unsigned int rotateStopSnd; + unsigned int rotateStopSndPlayer; + int sndIsFiring; + vec3_t targetOffset; + float onTargetAngle; + TurretRotateState turretRotateState; + vec3_t previousAngles; + }; + + struct gentity_s + { + int number; + char __pad0[4]; + int eFlags2; + char __pad1[328]; + gclient_s* client; // 340 + char __pad2[0x4]; + char __pad3[0x4]; + TurretInfo* pTurretInfo; + char __pad4[0x24]; + int flags; // 392 + char __pad5[0x190]; + }; + + static_assert(sizeof(gentity_s) == 0x31C); + + enum entity_class + { + CLASS_NUM_ENTITY = 0x0, + CLASS_NUM_HUDELEM = 0x1, + CLASS_NUM_PATHNODE = 0x2, + CLASS_NUM_VEHICLENODE = 0x3, + CLASS_NUM_DYNENTITY = 0x4, + CLASS_NUM_MENUELEM = 0x5, + CLASS_NUM_COUNT = 0x6, + }; + + union hudelem_color_t + { + int rgba; + }; + + struct hudelem_s + { + float x; + float y; + float z; + float fontScale; + float fromFontScale; + int fontScaleStartTime; + hudelem_color_t color; + hudelem_color_t fromColor; + int fadeStartTime; + int scaleStartTime; + float fromX; + float fromY; + int moveStartTime; + int time; + int duration; + float value; + float sort; + hudelem_color_t glowColor; + int fxBirthTime; + unsigned int flags; + __int16 targetEntNum; + __int16 fontScaleTime; + __int16 fadeTime; + __int16 label; + __int16 width; + __int16 height; + __int16 fromWidth; + __int16 fromHeight; + __int16 scaleTime; + __int16 moveTime; + __int16 text; + unsigned __int16 fxLetterTime; + unsigned __int16 fxDecayStartTime; + unsigned __int16 fxDecayDuration; + unsigned __int16 fxRedactDecayStartTime; + unsigned __int16 fxRedactDecayDuration; + char type; + char font; + char alignOrg; + char alignScreen; + char materialIndex; + char offscreenMaterialIdx; + char fromAlignOrg; + char fromAlignScreen; + char soundID; + char ui3dWindow; + }; + + struct game_hudelem_s + { + hudelem_s elem; + int clientNum; + int team; + int archived; + }; + + enum clientState_t + { + CS_FREE, + CS_ZOMBIE, + CS_RECONNECTING, + CS_CONNECTED, + CS_CLIENTLOADING, + CS_ACTIVE, + }; + + enum netsrc_t + { + NS_CLIENT1 = 0x0, + NS_CLIENT2 = 0x1, + NS_CLIENT3 = 0x2, + NS_CLIENT4 = 0x3, + NS_SERVER = 0x4, + NS_PACKET = 0x5, + NS_NULL = -1, + }; + + enum netadrtype_t + { + NA_BOT = 0x0, + NA_BAD = 0x1, + NA_LOOPBACK = 0x2, + NA_BROADCAST = 0x3, + NA_IP = 0x4, + }; + + struct netadr_t + { + union + { + unsigned char ip[4]; + unsigned int inaddr; + }; + unsigned __int16 port; + netadrtype_t type; + netsrc_t localNetID; + unsigned __int16 serverID; + }; + + static_assert(sizeof(netadr_t) == 0x14); + + struct netProfileInfo_t + { + unsigned char __pad0[0x5E0]; + }; + + struct netchan_t + { + int outgoingSequence; + netsrc_t sock; + int dropped; + int incomingSequence; + netadr_t remoteAddress; + int qport; + int fragmentSequence; + int fragmentLength; + unsigned char* fragmentBuffer; + int fragmentBufferSize; + int unsentFragments; + int unsentOnLoan; + int unsentFragmentStart; + int unsentLength; + unsigned char* unsentBuffer; + int unsentBufferSize; + int reliable_fragments; + unsigned char fragment_send_count[128]; + unsigned int fragment_ack[4]; + int lowest_send_count; + netProfileInfo_t prof; + }; + + static_assert(sizeof(netchan_t) == 0x6C8); + + struct PredictedVehicleDef + { + bool fullPhysics; + vec3_t origin; + vec3_t angles; + vec3_t tVel; + vec3_t aVel; + int serverTime; + }; + + static_assert(sizeof(PredictedVehicleDef) == 0x38); + + struct clientHeader_t + { + clientState_t state; + int sendAsActive; + int deltaMessage; + int rateDelayed; + int hasAckedBaselineData; + int hugeSnapshotSent; + netchan_t netchan; + vec3_t predictedOrigin; + int predictedOriginServerTime; + int migrationState; + PredictedVehicleDef predictedVehicle; + }; + + static_assert(sizeof(clientHeader_t) == 0x72C); + + struct client_s + { + clientHeader_t header; + const char* dropReason; + char userinfo[1024]; + unsigned char __pad0[0x3F75C]; + int bIsTestClient; + unsigned char __pad1[0xDEF0]; + }; + + static_assert(offsetof(client_s, header.netchan) == 24); + static_assert(offsetof(netchan_t, remoteAddress) == 16); + + static_assert(sizeof(client_s) == 0x4E180); + + struct cmd_function_t + { + cmd_function_t* next; + const char* name; + const char* autoCompleteDir; + const char* autoCompleteExt; + void(__cdecl* function)(); + int flags; + }; + + struct CmdArgs + { + int nesting; + int localClientNum[8]; + int controllerIndex[8]; + void* itemDef[8]; + int argshift[8]; + int argc[8]; + const char** argv[8]; + char textPool[8192]; + const char* argvPool[512]; + int usedTextPool[8]; + int totalUsedArgvPool; + int totalUsedTextPool; + }; + + enum dvarType_t + { + DVAR_TYPE_INVALID = 0x0, + DVAR_TYPE_BOOL = 0x1, + DVAR_TYPE_FLOAT = 0x2, + DVAR_TYPE_FLOAT_2 = 0x3, + DVAR_TYPE_FLOAT_3 = 0x4, + DVAR_TYPE_FLOAT_4 = 0x5, + DVAR_TYPE_INT = 0x6, + DVAR_TYPE_ENUM = 0x7, + DVAR_TYPE_STRING = 0x8, + DVAR_TYPE_COLOR = 0x9, + DVAR_TYPE_INT64 = 0xA, + DVAR_TYPE_LINEAR_COLOR_RGB = 0xB, + DVAR_TYPE_COLOR_XYZ = 0xC, + DVAR_TYPE_COUNT = 0xD, + }; + + union DvarValue + { + bool enabled; + int integer; + unsigned int unsignedInt; + __int64 integer64; + unsigned __int64 unsignedInt64; + float value; + vec4_t vector; + const char* string; + char color[4]; + }; + + struct $A37BA207B3DDD6345C554D4661813EDD + { + int stringCount; + const char* const* strings; + }; + + struct $9CA192F9DB66A3CB7E01DE78A0DEA53D + { + int min; + int max; + }; + + struct $251C2428A496074035CACA7AAF3D55BD + { + float min; + float max; + }; + + union DvarLimits + { + $A37BA207B3DDD6345C554D4661813EDD enumeration; + $9CA192F9DB66A3CB7E01DE78A0DEA53D integer; + $251C2428A496074035CACA7AAF3D55BD value; + $251C2428A496074035CACA7AAF3D55BD vector; + }; + + struct dvar_t + { + const char* name; + const char* description; + int hash; + unsigned int flags; + dvarType_t type; + bool modified; + DvarValue current; + DvarValue latched; + DvarValue reset; + DvarLimits domain; + dvar_t* hashNext; + }; + + struct GSC_OBJ + { + char magic[8]; + unsigned int source_crc; + unsigned int include_offset; + unsigned int animtree_offset; + unsigned int cseg_offset; + unsigned int stringtablefixup_offset; + unsigned int exports_offset; + unsigned int imports_offset; + unsigned int fixup_offset; + unsigned int profile_offset; + unsigned int cseg_size; + unsigned __int16 name; + unsigned __int16 stringtablefixup_count; + unsigned __int16 exports_count; + unsigned __int16 imports_count; + unsigned __int16 fixup_count; + unsigned __int16 profile_count; + char include_count; + char animtree_count; + char flags; + }; + + struct GSC_EXPORT_ITEM + { + unsigned int checksum; + unsigned int address; + unsigned __int16 name; + char param_count; + char flags; + }; + + struct GSC_IMPORT_ITEM + { + unsigned __int16 name; + unsigned __int16 name_space; + unsigned __int16 num_address; + char param_count; + char flags; + }; + + // gsc-tool + enum class opcode : std::uint8_t + { + OP_End = 0x0, + OP_Return = 0x1, + OP_GetUndefined = 0x2, + OP_GetZero = 0x3, + OP_GetByte = 0x4, + OP_GetNegByte = 0x5, + OP_GetUnsignedShort = 0x6, + OP_GetNegUnsignedShort = 0x7, + OP_GetInteger = 0x8, + OP_GetFloat = 0x9, + OP_GetString = 0xA, + OP_GetIString = 0xB, + OP_GetVector = 0xC, + OP_GetLevelObject = 0xD, + OP_GetAnimObject = 0xE, + OP_GetSelf = 0xF, + OP_GetLevel = 0x10, + OP_GetGame = 0x11, + OP_GetAnim = 0x12, + OP_GetAnimation = 0x13, + OP_GetGameRef = 0x14, + OP_GetFunction = 0x15, + OP_CreateLocalVariable = 0x16, + OP_SafeCreateLocalVariables = 0x17, + OP_RemoveLocalVariables = 0x18, + OP_EvalLocalVariableCached = 0x19, + OP_EvalArray = 0x1A, + OP_EvalLocalArrayRefCached = 0x1B, + OP_EvalArrayRef = 0x1C, + OP_ClearArray = 0x1D, + OP_EmptyArray = 0x1E, + OP_GetSelfObject = 0x1F, + OP_EvalFieldVariable = 0x20, + OP_EvalFieldVariableRef = 0x21, + OP_ClearFieldVariable = 0x22, + OP_SafeSetVariableFieldCached = 0x23, + OP_SafeSetWaittillVariableFieldCached = 0x24, + OP_ClearParams = 0x25, + OP_CheckClearParams = 0x26, + OP_EvalLocalVariableRefCached = 0x27, + OP_SetVariableField = 0x28, + OP_CallBuiltin = 0x29, + OP_CallBuiltinMethod = 0x2A, + OP_Wait = 0x2B, + OP_WaitTillFrameEnd = 0x2C, + OP_PreScriptCall = 0x2D, + OP_ScriptFunctionCall = 0x2E, + OP_ScriptFunctionCallPointer = 0x2F, + OP_ScriptMethodCall = 0x30, + OP_ScriptMethodCallPointer = 0x31, + OP_ScriptThreadCall = 0x32, + OP_ScriptThreadCallPointer = 0x33, + OP_ScriptMethodThreadCall = 0x34, + OP_ScriptMethodThreadCallPointer = 0x35, + OP_DecTop = 0x36, + OP_CastFieldObject = 0x37, + OP_CastBool = 0x38, + OP_BoolNot = 0x39, + OP_BoolComplement = 0x3A, + OP_JumpOnFalse = 0x3B, + OP_JumpOnTrue = 0x3C, + OP_JumpOnFalseExpr = 0x3D, + OP_JumpOnTrueExpr = 0x3E, + OP_Jump = 0x3F, + OP_JumpBack = 0x40, + OP_Inc = 0x41, + OP_Dec = 0x42, + OP_Bit_Or = 0x43, + OP_Bit_Xor = 0x44, + OP_Bit_And = 0x45, + OP_Equal = 0x46, + OP_NotEqual = 0x47, + OP_LessThan = 0x48, + OP_GreaterThan = 0x49, + OP_LessThanOrEqualTo = 0x4A, + OP_GreaterThanOrEqualTo = 0x4B, + OP_ShiftLeft = 0x4C, + OP_ShiftRight = 0x4D, + OP_Plus = 0x4E, + OP_Minus = 0x4F, + OP_Multiply = 0x50, + OP_Divide = 0x51, + OP_Modulus = 0x52, + OP_SizeOf = 0x53, + OP_WaitTillMatch = 0x54, + OP_WaitTill = 0x55, + OP_Notify = 0x56, + OP_EndOn = 0x57, + OP_VoidCodePos = 0x58, + OP_Switch = 0x59, + OP_EndSwitch = 0x5A, + OP_Vector = 0x5B, + OP_GetHash = 0x5C, + OP_RealWait = 0x5D, + OP_VectorConstant = 0x5E, + OP_IsDefined = 0x5F, + OP_VectorScale = 0x60, + OP_AnglesToUp = 0x61, + OP_AnglesToRight = 0x62, + OP_AnglesToForward = 0x63, + OP_AngleClamp180 = 0x64, + OP_VectorToAngles = 0x65, + OP_Abs = 0x66, + OP_GetTime = 0x67, + OP_GetDvar = 0x68, + OP_GetDvarInt = 0x69, + OP_GetDvarFloat = 0x6A, + OP_GetDvarVector = 0x6B, + OP_GetDvarColorRed = 0x6C, + OP_GetDvarColorGreen = 0x6D, + OP_GetDvarColorBlue = 0x6E, + OP_GetDvarColorAlpha = 0x6F, + OP_FirstArrayKey = 0x70, + OP_NextArrayKey = 0x71, + OP_ProfileStart = 0x72, + OP_ProfileStop = 0x73, + OP_SafeDecTop = 0x74, + OP_Nop = 0x75, + OP_Abort = 0x76, + OP_Object = 0x77, + OP_ThreadObject = 0x78, + OP_EvalLocalVariable = 0x79, + OP_EvalLocalVariableRef = 0x7A, + OP_DevblockBegin = 0x7B, + OP_DevblockEnd = 0x7C, + OP_Breakpoint = 0x7D, + OP_Count = 0x7E, + }; + + enum XAssetType + { + ASSET_TYPE_XMODELPIECES = 0x0, + ASSET_TYPE_PHYSPRESET = 0x1, + ASSET_TYPE_PHYSCONSTRAINTS = 0x2, + ASSET_TYPE_DESTRUCTIBLEDEF = 0x3, + ASSET_TYPE_XANIMPARTS = 0x4, + ASSET_TYPE_XMODEL = 0x5, + ASSET_TYPE_MATERIAL = 0x6, + ASSET_TYPE_TECHNIQUE_SET = 0x7, + ASSET_TYPE_IMAGE = 0x8, + ASSET_TYPE_SOUND = 0x9, + ASSET_TYPE_SOUND_PATCH = 0xA, + ASSET_TYPE_CLIPMAP = 0xB, + ASSET_TYPE_CLIPMAP_PVS = 0xC, + ASSET_TYPE_COMWORLD = 0xD, + ASSET_TYPE_GAMEWORLD_SP = 0xE, + ASSET_TYPE_GAMEWORLD_MP = 0xF, + ASSET_TYPE_MAP_ENTS = 0x10, + ASSET_TYPE_GFXWORLD = 0x11, + ASSET_TYPE_LIGHT_DEF = 0x12, + ASSET_TYPE_UI_MAP = 0x13, + ASSET_TYPE_FONT = 0x14, + ASSET_TYPE_FONTICON = 0x15, + ASSET_TYPE_MENULIST = 0x16, + ASSET_TYPE_MENU = 0x17, + ASSET_TYPE_LOCALIZE_ENTRY = 0x18, + ASSET_TYPE_WEAPON = 0x19, + ASSET_TYPE_WEAPONDEF = 0x1A, + ASSET_TYPE_WEAPON_VARIANT = 0x1B, + ASSET_TYPE_WEAPON_FULL = 0x1C, + ASSET_TYPE_ATTACHMENT = 0x1D, + ASSET_TYPE_ATTACHMENT_UNIQUE = 0x1E, + ASSET_TYPE_WEAPON_CAMO = 0x1F, + ASSET_TYPE_SNDDRIVER_GLOBALS = 0x20, + ASSET_TYPE_FX = 0x21, + ASSET_TYPE_IMPACT_FX = 0x22, + ASSET_TYPE_AITYPE = 0x23, + ASSET_TYPE_MPTYPE = 0x24, + ASSET_TYPE_MPBODY = 0x25, + ASSET_TYPE_MPHEAD = 0x26, + ASSET_TYPE_CHARACTER = 0x27, + ASSET_TYPE_XMODELALIAS = 0x28, + ASSET_TYPE_RAWFILE = 0x29, + ASSET_TYPE_STRINGTABLE = 0x2A, + ASSET_TYPE_LEADERBOARD = 0x2B, + ASSET_TYPE_XGLOBALS = 0x2C, + ASSET_TYPE_DDL = 0x2D, + ASSET_TYPE_GLASSES = 0x2E, + ASSET_TYPE_EMBLEMSET = 0x2F, + ASSET_TYPE_SCRIPTPARSETREE = 0x30, + ASSET_TYPE_KEYVALUEPAIRS = 0x31, + ASSET_TYPE_VEHICLEDEF = 0x32, + ASSET_TYPE_MEMORYBLOCK = 0x33, + ASSET_TYPE_ADDON_MAP_ENTS = 0x34, + ASSET_TYPE_TRACER = 0x35, + ASSET_TYPE_SKINNEDVERTS = 0x36, + ASSET_TYPE_QDB = 0x37, + ASSET_TYPE_SLUG = 0x38, + ASSET_TYPE_FOOTSTEP_TABLE = 0x39, + ASSET_TYPE_FOOTSTEPFX_TABLE = 0x3A, + ASSET_TYPE_ZBARRIER = 0x3B, + ASSET_TYPE_COUNT = 0x3C, + ASSET_TYPE_STRING = 0x3C, + ASSET_TYPE_ASSETLIST = 0x3D, + ASSET_TYPE_REPORT = 0x3E, + ASSET_TYPE_DEPEND = 0x3F, + ASSET_TYPE_FULL_COUNT = 0x40, + }; + + struct ScriptParseTree + { + const char* name; + int len; + GSC_OBJ* obj; + }; + + struct objFileInfo_t + { + GSC_OBJ* activeVersion; + char __pad[0x24]; + }; + + union XAssetHeader + { + ScriptParseTree* scriptParseTree; + }; + + struct WeaponDef + { + const char* szOverlayName; + void** gunXModel; + void* handXModel; + const char* szModeName; + unsigned __int16* notetrackSoundMapKeys; + unsigned __int16* notetrackSoundMapValues; + int playerAnimType; + int weapType; + int weapClass; + int penetrateType; + int impactType; + int inventoryType; + int fireType; + int clipType; + int barrelType; + int itemIndex; + const char* parentWeaponName; + int iJamFireTime; + int overheatWeapon; + float overheatRate; + float cooldownRate; + float overheatEndVal; + bool coolWhileFiring; + bool fuelTankWeapon; + int iTankLifeTime; + int offhandClass; + int offhandSlot; + int stance; + const void* viewFlashEffect; + const void* worldFlashEffect; + const void* barrelCooldownEffect; + int barrelCooldownMinCount; + vec3_t vViewFlashOffset; + vec3_t vWorldFlashOffset; + const char* pickupSound; + const char* pickupSoundPlayer; + const char* ammoPickupSound; + const char* ammoPickupSoundPlayer; + const char* projectileSound; + const char* pullbackSound; + const char* pullbackSoundPlayer; + const char* fireSound; + const char* fireSoundPlayer; + const char* fireLoopSound; + const char* fireLoopSoundPlayer; + const char* fireLoopEndSound; + const char* fireLoopEndSoundPlayer; + const char* fireStartSound; + const char* fireStopSound; + const char* fireKillcamSound; + const char* fireStartSoundPlayer; + const char* fireStopSoundPlayer; + const char* fireKillcamSoundPlayer; + const char* fireLastSound; + const char* fireLastSoundPlayer; + const char* emptyFireSound; + const char* emptyFireSoundPlayer; + const char* crackSound; + const char* whizbySound; + const char* meleeSwipeSound; + const char* meleeSwipeSoundPlayer; + const char* meleeHitSound; + const char* meleeMissSound; + const char* rechamberSound; + const char* rechamberSoundPlayer; + const char* reloadSound; + const char* reloadSoundPlayer; + const char* reloadEmptySound; + const char* reloadEmptySoundPlayer; + const char* reloadStartSound; + const char* reloadStartSoundPlayer; + const char* reloadEndSound; + const char* reloadEndSoundPlayer; + const char* rotateLoopSound; + const char* rotateLoopSoundPlayer; + const char* rotateStopSound; + const char* rotateStopSoundPlayer; + const char* deploySound; + const char* deploySoundPlayer; + const char* finishDeploySound; + const char* finishDeploySoundPlayer; + const char* breakdownSound; + const char* breakdownSoundPlayer; + const char* finishBreakdownSound; + const char* finishBreakdownSoundPlayer; + const char* detonateSound; + const char* detonateSoundPlayer; + const char* nightVisionWearSound; + const char* nightVisionWearSoundPlayer; + const char* nightVisionRemoveSound; + const char* nightVisionRemoveSoundPlayer; + const char* altSwitchSound; + const char* altSwitchSoundPlayer; + const char* raiseSound; + const char* raiseSoundPlayer; + const char* firstRaiseSound; + const char* firstRaiseSoundPlayer; + const char* adsRaiseSoundPlayer; + const char* adsLowerSoundPlayer; + const char* putawaySound; + const char* putawaySoundPlayer; + const char* overheatSound; + const char* overheatSoundPlayer; + const char* adsZoomSound; + const char* shellCasing; + const char* shellCasingPlayer; + const char** bounceSound; + const char* standMountedWeapdef; + const char* crouchMountedWeapdef; + const char* proneMountedWeapdef; + int standMountedIndex; + int crouchMountedIndex; + int proneMountedIndex; + const void* viewShellEjectEffect; + const void* worldShellEjectEffect; + const void* viewLastShotEjectEffect; + const void* worldLastShotEjectEffect; + vec3_t vViewShellEjectOffset; + vec3_t vWorldShellEjectOffset; + vec3_t vViewShellEjectRotation; + vec3_t vWorldShellEjectRotation; + void* reticleCenter; + void* reticleSide; + int iReticleCenterSize; + int iReticleSideSize; + int iReticleMinOfs; + int activeReticleType; + vec3_t vStandMove; + vec3_t vStandRot; + vec3_t vDuckedOfs; + vec3_t vDuckedMove; + vec3_t vDuckedSprintOfs; + vec3_t vDuckedSprintRot; + vec2_t vDuckedSprintBob; + float fDuckedSprintCycleScale; + vec3_t vSprintOfs; + vec3_t vSprintRot; + vec2_t vSprintBob; + float fSprintCycleScale; + vec3_t vLowReadyOfs; + vec3_t vLowReadyRot; + vec3_t vRideOfs; + vec3_t vRideRot; + vec3_t vDtpOfs; + vec3_t vDtpRot; + vec2_t vDtpBob; + float fDtpCycleScale; + vec3_t vMantleOfs; + vec3_t vMantleRot; + vec3_t vSlideOfs; + vec3_t vSlideRot; + vec3_t vDuckedRot; + vec3_t vProneOfs; + vec3_t vProneMove; + vec3_t vProneRot; + vec3_t vStrafeMove; + vec3_t vStrafeRot; + float fPosMoveRate; + float fPosProneMoveRate; + float fStandMoveMinSpeed; + float fDuckedMoveMinSpeed; + float fProneMoveMinSpeed; + float fPosRotRate; + float fPosProneRotRate; + float fStandRotMinSpeed; + float fDuckedRotMinSpeed; + float fProneRotMinSpeed; + void** worldModel; + void* worldClipModel; + void* rocketModel; + void* mountedModel; + void* additionalMeleeModel; + void* fireTypeIcon; + void* hudIcon; + int hudIconRatio; + void* indicatorIcon; + int indicatorIconRatio; + void* ammoCounterIcon; + int ammoCounterIconRatio; + int ammoCounterClip; + int iStartAmmo; + int iMaxAmmo; + int shotCount; + const char* szSharedAmmoCapName; + int iSharedAmmoCapIndex; + int iSharedAmmoCap; + bool unlimitedAmmo; + bool ammoCountClipRelative; + int damage[6]; + float damageRange[6]; + int minPlayerDamage; + float damageDuration; + float damageInterval; + int playerDamage; + int iMeleeDamage; + int iDamageType; + unsigned __int16 explosionTag; + int iFireDelay; + int iMeleeDelay; + int meleeChargeDelay; + int iDetonateDelay; + int iSpinUpTime; + int iSpinDownTime; + float spinRate; + const char* spinLoopSound; + const char* spinLoopSoundPlayer; + const char* startSpinSound; + const char* startSpinSoundPlayer; + const char* stopSpinSound; + const char* stopSpinSoundPlayer; + bool applySpinPitch; + int iFireTime; + int iLastFireTime; + int iRechamberTime; + int iRechamberBoltTime; + int iHoldFireTime; + int iDetonateTime; + int iMeleeTime; + int iBurstDelayTime; + int meleeChargeTime; + int iReloadTimeRight; + int iReloadTimeLeft; + int reloadShowRocketTime; + int iReloadEmptyTimeLeft; + int iReloadAddTime; + int iReloadEmptyAddTime; + int iReloadQuickAddTime; + int iReloadQuickEmptyAddTime; + int iReloadStartTime; + int iReloadStartAddTime; + int iReloadEndTime; + int iDropTime; + int iRaiseTime; + int iAltDropTime; + int quickDropTime; + int quickRaiseTime; + int iFirstRaiseTime; + int iEmptyRaiseTime; + int iEmptyDropTime; + int sprintInTime; + int sprintLoopTime; + int sprintOutTime; + int lowReadyInTime; + int lowReadyLoopTime; + int lowReadyOutTime; + int contFireInTime; + int contFireLoopTime; + int contFireOutTime; + int dtpInTime; + int dtpLoopTime; + int dtpOutTime; + int crawlInTime; + int crawlForwardTime; + int crawlBackTime; + int crawlRightTime; + int crawlLeftTime; + int crawlOutFireTime; + int crawlOutTime; + int slideInTime; + int deployTime; + int breakdownTime; + int iFlourishTime; + int nightVisionWearTime; + int nightVisionWearTimeFadeOutEnd; + int nightVisionWearTimePowerUp; + int nightVisionRemoveTime; + int nightVisionRemoveTimePowerDown; + int nightVisionRemoveTimeFadeInStart; + int fuseTime; + int aiFuseTime; + int lockOnRadius; + int lockOnSpeed; + bool requireLockonToFire; + bool noAdsWhenMagEmpty; + bool avoidDropCleanup; + unsigned int stackFire; + float stackFireSpread; + float stackFireAccuracyDecay; + const char* stackSound; + float autoAimRange; + float aimAssistRange; + bool mountableWeapon; + float aimPadding; + float enemyCrosshairRange; + bool crosshairColorChange; + float moveSpeedScale; + float adsMoveSpeedScale; + float sprintDurationScale; + int overlayReticle; + int overlayInterface; + float overlayWidth; + float overlayHeight; + float fAdsBobFactor; + float fAdsViewBobMult; + bool bHoldBreathToSteady; + float fHipSpreadStandMin; + float fHipSpreadDuckedMin; + float fHipSpreadProneMin; + float hipSpreadStandMax; + float hipSpreadDuckedMax; + float hipSpreadProneMax; + float fHipSpreadDecayRate; + float fHipSpreadFireAdd; + float fHipSpreadTurnAdd; + float fHipSpreadMoveAdd; + float fHipSpreadDuckedDecay; + float fHipSpreadProneDecay; + float fHipReticleSidePos; + float fAdsIdleAmount; + float fHipIdleAmount; + float adsIdleSpeed; + float hipIdleSpeed; + float fIdleCrouchFactor; + float fIdleProneFactor; + float fGunMaxPitch; + float fGunMaxYaw; + float swayMaxAngle; + float swayLerpSpeed; + float swayPitchScale; + float swayYawScale; + float swayHorizScale; + float swayVertScale; + float swayShellShockScale; + float adsSwayMaxAngle; + float adsSwayLerpSpeed; + float adsSwayPitchScale; + float adsSwayYawScale; + bool sharedAmmo; + bool bRifleBullet; + bool armorPiercing; + bool bAirburstWeapon; + bool bBoltAction; + bool bUseAltTagFlash; + bool bUseAntiLagRewind; + bool bIsCarriedKillstreakWeapon; + bool aimDownSight; + bool bRechamberWhileAds; + bool bReloadWhileAds; + float adsViewErrorMin; + float adsViewErrorMax; + bool bCookOffHold; + bool bClipOnly; + bool bCanUseInVehicle; + bool bNoDropsOrRaises; + bool adsFireOnly; + bool cancelAutoHolsterWhenEmpty; + bool suppressAmmoReserveDisplay; + bool laserSight; + bool laserSightDuringNightvision; + bool bHideThirdPerson; + bool bHasBayonet; + bool bDualWield; + bool bExplodeOnGround; + bool bThrowBack; + bool bRetrievable; + bool bDieOnRespawn; + bool bNoThirdPersonDropsOrRaises; + bool bContinuousFire; + bool bNoPing; + bool bForceBounce; + bool bUseDroppedModelAsStowed; + bool bNoQuickDropWhenEmpty; + bool bKeepCrosshairWhenADS; + bool bUseOnlyAltWeaoponHideTagsInAltMode; + bool bAltWeaponAdsOnly; + bool bAltWeaponDisableSwitching; + void* killIcon; + int killIconRatio; + bool flipKillIcon; + bool bNoPartialReload; + bool bSegmentedReload; + bool bNoADSAutoReload; + int iReloadAmmoAdd; + int iReloadStartAdd; + const char* szSpawnedGrenadeWeaponName; + const char* szDualWieldWeaponName; + unsigned int dualWieldWeaponIndex; + int iDropAmmoMin; + int iDropAmmoMax; + int iDropClipAmmoMin; + int iDropClipAmmoMax; + int iShotsBeforeRechamber; + bool blocksProne; + bool bShowIndicator; + int isRollingGrenade; + int useBallisticPrediction; + int isValuable; + int isTacticalInsertion; + bool isReviveWeapon; + bool bUseRigidBodyOnVehicle; + int iExplosionRadius; + int iExplosionRadiusMin; + int iIndicatorRadius; + int iExplosionInnerDamage; + int iExplosionOuterDamage; + float damageConeAngle; + int iProjectileSpeed; + int iProjectileSpeedUp; + int iProjectileSpeedRelativeUp; + int iProjectileSpeedForward; + float fProjectileTakeParentVelocity; + int iProjectileActivateDist; + float projLifetime; + float timeToAccelerate; + float projectileCurvature; + void* projectileModel; + int projExplosion; + const void* projExplosionEffect; + bool projExplosionEffectForceNormalUp; + const void* projExplosionEffect2; + bool projExplosionEffect2ForceNormalUp; + const void* projExplosionEffect3; + bool projExplosionEffect3ForceNormalUp; + const void* projExplosionEffect4; + bool projExplosionEffect4ForceNormalUp; + const void* projExplosionEffect5; + bool projExplosionEffect5ForceNormalUp; + const void* projDudEffect; + const char* projExplosionSound; + const char* projDudSound; + const char* mortarShellSound; + const char* tankShellSound; + bool bProjImpactExplode; + bool bProjSentientImpactExplode; + bool bProjExplodeWhenStationary; + bool bBulletImpactExplode; + int stickiness; + int rotateType; + bool plantable; + bool hasDetonator; + bool timedDetonation; + bool bNoCrumpleMissile; + bool rotate; + bool bKeepRolling; + bool holdButtonToThrow; + bool offhandHoldIsCancelable; + bool freezeMovementWhenFiring; + float lowAmmoWarningThreshold; + bool bDisallowAtMatchStart; + float meleeChargeRange; + bool bUseAsMelee; + bool isCameraSensor; + bool isAcousticSensor; + bool isLaserSensor; + bool isHoldUseGrenade; + float* parallelBounce; + float* perpendicularBounce; + const void* projTrailEffect; + vec3_t vProjectileColor; + int guidedMissileType; + float maxSteeringAccel; + int projIgnitionDelay; + const void* projIgnitionEffect; + const char* projIgnitionSound; + float fAdsAimPitch; + float fAdsCrosshairInFrac; + float fAdsCrosshairOutFrac; + int adsGunKickReducedKickBullets; + float adsGunKickReducedKickPercent; + float fAdsGunKickPitchMin; + float fAdsGunKickPitchMax; + float fAdsGunKickYawMin; + float fAdsGunKickYawMax; + float fAdsGunKickAccel; + float fAdsGunKickSpeedMax; + float fAdsGunKickSpeedDecay; + float fAdsGunKickStaticDecay; + float fAdsViewKickPitchMin; + float fAdsViewKickPitchMax; + float fAdsViewKickMinMagnitude; + float fAdsViewKickYawMin; + float fAdsViewKickYawMax; + float fAdsRecoilReductionRate; + float fAdsRecoilReductionLimit; + float fAdsRecoilReturnRate; + float fAdsViewScatterMin; + float fAdsViewScatterMax; + float fAdsSpread; + int hipGunKickReducedKickBullets; + float hipGunKickReducedKickPercent; + float fHipGunKickPitchMin; + float fHipGunKickPitchMax; + float fHipGunKickYawMin; + float fHipGunKickYawMax; + float fHipGunKickAccel; + float fHipGunKickSpeedMax; + float fHipGunKickSpeedDecay; + float fHipGunKickStaticDecay; + float fHipViewKickPitchMin; + float fHipViewKickPitchMax; + float fHipViewKickMinMagnitude; + float fHipViewKickYawMin; + float fHipViewKickYawMax; + float fHipViewScatterMin; + float fHipViewScatterMax; + float fAdsViewKickCenterDuckedScale; + float fAdsViewKickCenterProneScale; + float fAntiQuickScopeTime; + float fAntiQuickScopeScale; + float fAntiQuickScopeSpreadMultiplier; + float fAntiQuickScopeSpreadMax; + float fAntiQuickScopeSwayFactor; + float fightDist; + float maxDist; + const char* accuracyGraphName[2]; + vec2_t* accuracyGraphKnots[2]; + vec2_t* originalAccuracyGraphKnots[2]; + int accuracyGraphKnotCount[2]; + int originalAccuracyGraphKnotCount[2]; + int iPositionReloadTransTime; + float leftArc; + float rightArc; + float topArc; + float bottomArc; + float accuracy; + float aiSpread; + float playerSpread; + float minTurnSpeed[2]; + float maxTurnSpeed[2]; + float pitchConvergenceTime; + float yawConvergenceTime; + float suppressTime; + float maxRange; + float fAnimHorRotateInc; + float fPlayerPositionDist; + const char* szUseHintString; + const char* dropHintString; + int iUseHintStringIndex; + int dropHintStringIndex; + float horizViewJitter; + float vertViewJitter; + float cameraShakeScale; + int cameraShakeDuration; + int cameraShakeRadius; + float explosionCameraShakeScale; + int explosionCameraShakeDuration; + int explosionCameraShakeRadius; + const char* szScript; + float destabilizationRateTime; + float destabilizationCurvatureMax; + int destabilizeDistance; + float* locationDamageMultipliers; + const char* fireRumble; + const char* meleeImpactRumble; + const char* reloadRumble; + const char* explosionRumble; + void* tracerType; + void* enemyTracerType; + float adsDofStart; + float adsDofEnd; + float hipDofStart; + float hipDofEnd; + float scanSpeed; + float scanAccel; + int scanPauseTime; + const char* flameTableFirstPerson; + const char* flameTableThirdPerson; + void* flameTableFirstPersonPtr; + void* flameTableThirdPersonPtr; + const void* tagFx_preparationEffect; + const void* tagFlash_preparationEffect; + bool doGibbing; + float maxGibDistance; + float altScopeADSTransInTime; + float altScopeADSTransOutTime; + int iIntroFireTime; + int iIntroFireLength; + const void* meleeSwipeEffect; + const void* meleeImpactEffect; + const void* meleeImpactNoBloodEffect; + const char* throwBackType; + void* weaponCamo; + float customFloat0; + float customFloat1; + float customFloat2; + int customBool0; + int customBool1; + int customBool2; + }; + + static_assert(sizeof(WeaponDef) == 2448); +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/game/symbols.hpp b/t6/plugins/tws-t6-gsc-utils/src/game/symbols.hpp new file mode 100644 index 0000000..8adb330 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/game/symbols.hpp @@ -0,0 +1,137 @@ +#pragma once + +#define WEAK __declspec(selectany) + +namespace game +{ + // Functions + + WEAK symbol BG_StringHashValue{0x6BB660, 0x612780}; + + WEAK symbol Cbuf_InsertText{0x4A5FF0, 0x702E40}; + WEAK symbol Cbuf_AddText{0x5C6F10, 0x6B9D20}; + WEAK symbol Cmd_ExecuteSingleCommand{0x6B9C10, 0x43B8D0}; + WEAK symbol Cmd_AddCommandInternal{0x5B3070, 0x4DC2A0}; + WEAK symbol Cmd_Argv{0x5608F0, 0x6B3D40}; + WEAK symbol Cmd_RemoveCommand{0x6033D0, 0x65EB30}; + + WEAK symbol ClientUserInfoChanged{0x4ED6A0, 0x427DC0}; + + WEAK symbol Dvar_FindVar{0x563A70, 0x673C80}; + WEAK symbol Dvar_GetInt{0x44DFC0, 0x6909F0}; + WEAK symbol Dvar_RegisterInt{0x5F24E0, 0x5A3D70}; + WEAK symbol Dvar_RegisterBool{0x4E0170, 0x5FBCC0}; + + WEAK symbol DB_FindXAssetHeader{0x6F9030, 0x43F460}; + + WEAK symbol I_CleanStr{0x44F2B0, 0x483F40}; + + WEAK symbol Player_GetMethod{0x432480, 0x6F2DB0}; + WEAK symbol Scr_GetCommonFunction{0x691110, 0x4EB070}; + WEAK symbol Scr_GetMethod{0x59D090, 0x486120}; + + WEAK symbol Scr_AddEntity{0x4C20F0, 0x5D8F80}; + WEAK symbol Scr_AddFloat{0x579130, 0x503480}; + WEAK symbol Scr_AddInt{0x57AFF0, 0x643A40}; + WEAK symbol Scr_AddString{0x4F1650, 0x6A7A70}; + WEAK symbol Scr_AddVector{0x4C1A40, 0x4FAB00}; + WEAK symbol Scr_AddObject{0x539B50, 0x584DF0}; + + WEAK symbol Scr_ClearOutParams{0x591410, 0x627540}; + + WEAK symbol AllocObject{0x6FB1B0, 0x6FE9D0}; + WEAK symbol AllocVariable{0x8F3150, 0x8F1EB0}; + WEAK symbol AllocThread{0x49FDF0, 0x482C40}; + WEAK symbol RemoveRefToObject{0x6FB1B0, 0x550DC0}; + WEAK symbol RemoveRefToVector{0x50C580, 0x432600}; + WEAK symbol AddRefToValue{0x488FA0, 0x4E5060}; + WEAK symbol RemoveRefToValue{0x55E820, 0x42E7D0}; + WEAK symbol RemoveVariableValue{0x56A820, 0x6CC320}; + + WEAK symbol FindVariable{0x6EB1B0, 0x5FE180}; + WEAK symbol GetVariable{0, 0x5CD170}; + WEAK symbol GetNewVariable{0x5AD920, 0x5C18A0}; + WEAK symbol GetNewArrayVariable{0x5D80C0, 0x42C260}; + + WEAK symbol Scr_SetObjectField{0x5B9820, 0x43F2A0}; + WEAK symbol GetEntityFieldValue{0x693130, 0x6DD0E0}; + + WEAK symbol Scr_Notify{0x644AB0, 0x495420}; + WEAK symbol Scr_NotifyId{0x4FDC10, 0x4BC0D0}; + WEAK symbol Scr_NotifyNum{0x578690, 0x664770}; + + WEAK symbol SL_GetString{0x602C40, 0x4601E0}; + WEAK symbol SL_ConvertToString{0x422D10, 0x532230}; + WEAK symbol SL_GetCanonicalString{0x692740, 0x546640}; + + WEAK symbol Scr_GetNumParam{0x42E990, 0x680EA0}; + WEAK symbol Scr_GetEntity{0x48F250, 0x489100}; + WEAK symbol Scr_GetFloat{0x633400, 0x625EE0}; + WEAK symbol Scr_GetInt{0x45D840, 0x49A060}; + WEAK symbol Scr_GetString{0x67C6A0, 0x488110}; + WEAK symbol Scr_GetVector{0x4CBCC0, 0x4BB100}; + WEAK symbol Scr_AllocVector{0x510D30, 0x44D8C0}; + WEAK symbol Scr_GetSelf{0x4682C0, 0x6C4E60}; + WEAK symbol Scr_GetFunctionHandle{0x416D30, 0x53E5A0}; + WEAK symbol Scr_AddClassField{0x6B7620, 0x438AD0}; + WEAK symbol Scr_GetEntityId{0x5765B0, 0x488430}; + WEAK symbol GetStartLocalId{0x402760, 0x5D6CD0}; + WEAK symbol Scr_TerminateRunningThread{0x8F45A0, 0x8F3300}; + WEAK symbol Scr_Error{0x403AE0, 0x5E81C0}; + WEAK symbol Scr_ParamError{0x415A30, 0x617CA0}; + WEAK symbol Scr_ObjectError{0x561660, 0x6B67E0}; + WEAK symbol Scr_TerminalError{0x698C50, 0x410440}; + + WEAK symbol GetPlayerEntity{0x48B760, 0x4BF6F0}; + + WEAK symbol VM_Execute{0x8F9550, 0x8F82B0}; + + WEAK symbol SV_GameDropClient{0x6D2980, 0x677690}; + WEAK symbol SV_IsTestClient{0x4E64B0, 0x42C300}; + WEAK symbol SV_GameSendServerCommand{0x45D7D0, 0x40D450}; + WEAK symbol SV_SetConfigString{0x6EAA00, 0x673E40}; + + WEAK symbol Sys_GetValue{0x5EFBA0, 0x59A740}; + WEAK symbol Sys_Milliseconds{0x57EC40, 0x6F4C00}; + + WEAK symbol longjmp{0xA78870, 0xA71AD0}; + WEAK symbol _setjmp{0xA77B10, 0xA70D70}; + + // Variables + + WEAK symbol sv_cmd_args{0x255DD48, 0x252F8C8}; + + WEAK symbol g_script_error_level{0x2E23BC8, 0x2DF3EC8}; + WEAK symbol g_script_error{0x2E22B48, 0x2DF2E48}; + + WEAK symbol scr_VmPub{0x2E1A5D0, 0x2DEA8D0}; + WEAK symbol scr_VarGlob{0x2E1A100, 0x2DEA400}; + WEAK symbol scr_VarPub{0x2E1A500, 0x2DEA800}; + WEAK symbol scr_starttime{0x2E23C48, 0x2DF3F48}; + WEAK symbol fs{0x2E23C08, 0x2DF3F08}; + + WEAK symbol sv_configstrings{0x28E67AC, 0x28B70AC}; + + WEAK symbol g_classMap{0xD6FFE0, 0xD64E60}; + + WEAK symbol common_functions{0xC75160, 0xC6ACC8}; + WEAK symbol functions{0xD4DD28, 0xD42BC8}; + + WEAK symbol player_methods{0xC726A8, 0xC68218}; + WEAK symbol scriptEnt_methods{0xC76F18, 0xC6CA90}; + WEAK symbol scriptVehicle_methods{0xC77538, 0xC6D0B0}; + WEAK symbol hudElem_methods{0xC74580, 0xC6A0E8}; + WEAK symbol helicopter_methods{0xC74E98, 0xC6AA00}; + WEAK symbol actor_methods{0xC871A0, 0xC7CC70}; + WEAK symbol builtInCommon_methods{0xC76600, 0xC6C168}; + WEAK symbol builtIn_methods{0xD4FC78, 0xD44B18}; + + WEAK symbol g_entities{0x21EF7C0, 0x21C13C0}; + WEAK symbol levelEntityId{0x2E1A51C, 0x2DEA81C}; + + WEAK symbol svs_clients{0x291C0C0, 0x28EC9C0}; + + WEAK symbol g_hudelems{0x21C5140, 0x2196D40}; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/loader/component_interface.hpp b/t6/plugins/tws-t6-gsc-utils/src/loader/component_interface.hpp new file mode 100644 index 0000000..2f5b7c6 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/loader/component_interface.hpp @@ -0,0 +1,35 @@ +#pragma once +#include "../plugin.hpp" + +class component_interface +{ +public: + virtual ~component_interface() + { + } + + virtual void on_startup([[maybe_unused]] plugin::plugin* plugin) + { + } + + virtual void on_dvar_init([[maybe_unused]] plugin::plugin* plugin) + { + } + + virtual void on_after_dvar_init([[maybe_unused]] plugin::plugin* plugin) + { + } + + virtual void on_game_init([[maybe_unused]] plugin::plugin* plugin) + { + } + + virtual void on_shutdown([[maybe_unused]] plugin::plugin* plugin) + { + } + + virtual bool is_supported() + { + return true; + } +}; diff --git a/t6/plugins/tws-t6-gsc-utils/src/loader/component_loader.cpp b/t6/plugins/tws-t6-gsc-utils/src/loader/component_loader.cpp new file mode 100644 index 0000000..ad4f5b9 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/loader/component_loader.cpp @@ -0,0 +1,72 @@ +#include +#include "component_loader.hpp" + +void component_loader::register_component(const std::string& name, std::unique_ptr&& component_) +{ + get_components().push_back(std::make_pair(name, std::move(component_))); +} + +#define ON_CALLBACK(__name__) \ + void component_loader::__name__() \ + { \ + static auto handled = false; \ + if (handled) \ + { \ + return; \ + } \ + \ + handled = true; \ + \ + for (const auto& component_ : get_components()) \ + { \ + try \ + { \ + component_.second->__name__(plugin::get()); \ + } \ + catch (const std::exception& e) \ + { \ + printf("error executing component \"%s\" callback \"%s\": %s\n", component_.first.data(), #__name__, e.what()); \ + } \ + } \ + } \ + +ON_CALLBACK(on_startup); +ON_CALLBACK(on_dvar_init); +ON_CALLBACK(on_after_dvar_init); +ON_CALLBACK(on_shutdown); + +void component_loader::clean() +{ + auto& components = get_components(); + for (auto i = components.begin(); i != components.end();) + { + if (!(*i).second->is_supported()) + { + (*i).second->on_shutdown(plugin::get()); + i = components.erase(i); + } + else + { + ++i; + } + } +} + +void component_loader::trigger_premature_shutdown() +{ + throw premature_shutdown_trigger(); +} + +std::vector>>& component_loader::get_components() +{ + using component_vector = std::vector>>; + using component_vector_container = std::unique_ptr>; + + static component_vector_container components(new component_vector, [](component_vector* component_vector) + { + on_shutdown(); + delete component_vector; + }); + + return *components; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/loader/component_loader.hpp b/t6/plugins/tws-t6-gsc-utils/src/loader/component_loader.hpp new file mode 100644 index 0000000..6d6f73f --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/loader/component_loader.hpp @@ -0,0 +1,61 @@ +#pragma once +#include "component_interface.hpp" + +class component_loader final +{ +public: + class premature_shutdown_trigger final : public std::exception + { + [[nodiscard]] const char* what() const noexcept override + { + return "Premature shutdown requested"; + } + }; + + template + class installer final + { + static_assert(std::is_base_of::value, "component has invalid base class"); + + public: + installer(const std::string& name) + { + register_component(name, std::make_unique()); + } + }; + + template + static T* get() + { + for (const auto& component_ : get_components()) + { + if (typeid(*component_.get()) == typeid(T)) + { + return reinterpret_cast(component_.get()); + } + } + + return nullptr; + } + + static void register_component(const std::string& name, std::unique_ptr&& component); + + static void on_startup(); + static void on_dvar_init(); + static void on_after_dvar_init(); + static void on_shutdown(); + static void clean(); + + static void* load_import(const std::string& library, const std::string& function); + + static void trigger_premature_shutdown(); + +private: + static std::vector>>& get_components(); +}; + +#define REGISTER_COMPONENT(name) \ +namespace \ +{ \ + static component_loader::installer __component(#name); \ +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/plugin.cpp b/t6/plugins/tws-t6-gsc-utils/src/plugin.cpp new file mode 100644 index 0000000..57a035e --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/plugin.cpp @@ -0,0 +1,79 @@ +#include +#include "loader/component_loader.hpp" + +#include "component/logfile.hpp" + +#include "plugin.hpp" + +#include +#include + +namespace plugin +{ + namespace + { + void printf_stub(const char* fmt, ...) + { + char buffer[0x2000] = {}; + + va_list ap; + va_start(ap, fmt); + + vsnprintf_s(buffer, sizeof(buffer), _TRUNCATE, fmt, ap); + + va_end(ap); + + logfile::log_hook(buffer); + + const auto trimmed = utils::string::trim(buffer); + get()->get_interface()->logging()->info(trimmed); + } + } + + std::uint32_t plugin::plugin_version() + { + return 1; + } + + const char* plugin::plugin_name() + { + return "tws-t6-gsc-utils"; + } + + bool plugin::is_game_supported([[maybe_unused]] plutonium::sdk::game game) + { + return game == plutonium::sdk::game::t6; + } + + void plugin::on_startup(plutonium::sdk::iinterface* interface_ptr, plutonium::sdk::game game) + { + this->interface_ = interface_ptr; + this->game_ = game; + utils::hook::jump(reinterpret_cast(&printf), printf_stub); + + component_loader::on_startup(); + interface_ptr->callbacks()->on_dvar_init(&component_loader::on_dvar_init); + interface_ptr->callbacks()->on_after_dvar_init(&component_loader::on_after_dvar_init); + } + + void plugin::on_shutdown() + { + component_loader::on_shutdown(); + } + + plutonium::sdk::iinterface* plugin::get_interface() + { + return this->interface_; + } + + plutonium::sdk::game plugin::get_game() + { + return this->game_; + } + + plugin* get() + { + static plugin instance; + return &instance; + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/plugin.hpp b/t6/plugins/tws-t6-gsc-utils/src/plugin.hpp new file mode 100644 index 0000000..3d18d09 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/plugin.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +namespace plugin +{ + class plugin : public plutonium::sdk::plugin + { + public: + ~plugin() = default; + + std::uint32_t plugin_version() override; + const char* plugin_name() override; + + bool is_game_supported([[maybe_unused]] plutonium::sdk::game game) override; + + void on_startup(plutonium::sdk::iinterface* interface_ptr, plutonium::sdk::game game) override; + void on_shutdown() override; + + plutonium::sdk::iinterface* get_interface(); + plutonium::sdk::game get_game(); + + private: + plutonium::sdk::iinterface* interface_{}; + plutonium::sdk::game game_{}; + + }; + + plugin* get(); +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/resource.hpp b/t6/plugins/tws-t6-gsc-utils/src/resource.hpp new file mode 100644 index 0000000..beabe45 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/resource.hpp @@ -0,0 +1,3 @@ +#pragma once + +#define LIBMYSQL_DLL 100 diff --git a/t6/plugins/tws-t6-gsc-utils/src/resource.rc b/t6/plugins/tws-t6-gsc-utils/src/resource.rc new file mode 100644 index 0000000..335da49 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/resource.rc @@ -0,0 +1,3 @@ +#include "resource.hpp" + +LIBMYSQL_DLL RCDATA "../../deps/mysql/lib/libmysql.dll" diff --git a/t6/plugins/tws-t6-gsc-utils/src/stdinc.cpp b/t6/plugins/tws-t6-gsc-utils/src/stdinc.cpp new file mode 100644 index 0000000..f7991bd --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/stdinc.cpp @@ -0,0 +1,25 @@ +#include + +extern "C" +{ + int s_read_arc4random(void*, size_t) + { + return -1; + } + + int s_read_getrandom(void*, size_t) + { + return -1; + } + + int s_read_urandom(void*, size_t) + { + return -1; + } + + int s_read_ltm_rng(void*, size_t) + { + return -1; + } +} + diff --git a/t6/plugins/tws-t6-gsc-utils/src/stdinc.hpp b/t6/plugins/tws-t6-gsc-utils/src/stdinc.hpp new file mode 100644 index 0000000..1c7c340 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/stdinc.hpp @@ -0,0 +1,63 @@ +#pragma once + +#pragma warning(disable: 6011) +#pragma warning(disable: 6054) +#pragma warning(disable: 6387) +#pragma warning(disable: 26451) +#pragma warning(disable: 26812) +#pragma warning(disable: 28182) + +#define DLL_EXPORT extern "C" __declspec(dllexport) +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef max +#undef max +#endif + +#ifdef min +#undef min +#endif + +#include +#include + +#include + +#include +#include + +#pragma comment(lib, "ntdll.lib") +#pragma comment(lib, "ws2_32.lib") +#pragma comment(lib, "urlmon.lib" ) +#pragma comment(lib, "iphlpapi.lib") +#pragma comment(lib, "Crypt32.lib") +#pragma comment(lib, "libmysql.lib") +#pragma comment(lib, "delayimp.lib") + +#include "resource.hpp" + +using namespace std::literals; diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/binary_resource.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/binary_resource.cpp new file mode 100644 index 0000000..4eb5e6c --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/binary_resource.cpp @@ -0,0 +1,76 @@ +#include +#include "binary_resource.hpp" + +#include +#include "nt.hpp" +#include "io.hpp" + +namespace utils +{ + namespace + { + std::string get_temp_folder() + { + char path[MAX_PATH] = {0}; + if (!GetTempPathA(sizeof(path), path)) + { + throw std::runtime_error("Unable to get temp path"); + } + + return path; + } + + std::string write_exitisting_temp_file(const std::string& file, const std::string& data, + const bool fatal_if_overwrite_fails) + { + const auto temp = get_temp_folder(); + auto file_path = temp + file; + + std::string current_data; + if (!io::read_file(file_path, ¤t_data)) + { + if (!io::write_file(file_path, data)) + { + throw std::runtime_error("Failed to write file: " + file_path); + } + + return file_path; + } + + if (current_data == data || io::write_file(file_path, data) || !fatal_if_overwrite_fails) + { + return file_path; + } + + throw std::runtime_error( + "Temporary file was already written, but differs. It can't be overwritten as it's still in use: " + + file_path); + } + } + + binary_resource::binary_resource(const int id, std::string file) + : filename_(std::move(file)) + { + this->resource_ = nt::load_resource(id); + + if (this->resource_.empty()) + { + throw std::runtime_error("Unable to load resource: " + std::to_string(id)); + } + } + + std::string binary_resource::get_extracted_file(const bool fatal_if_overwrite_fails) + { + if (this->path_.empty()) + { + this->path_ = write_exitisting_temp_file(this->filename_, this->resource_, fatal_if_overwrite_fails); + } + + return this->path_; + } + + const std::string& binary_resource::get_data() const + { + return this->resource_; + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/binary_resource.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/binary_resource.hpp new file mode 100644 index 0000000..da19af1 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/binary_resource.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace utils +{ + class binary_resource + { + public: + binary_resource(int id, std::string file); + + std::string get_extracted_file(bool fatal_if_overwrite_fails = false); + const std::string& get_data() const; + + private: + std::string resource_; + std::string filename_; + std::string path_; + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/compression.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/compression.cpp new file mode 100644 index 0000000..3a6b9e2 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/compression.cpp @@ -0,0 +1,170 @@ +#include +#include "memory.hpp" +#include "compression.hpp" + +#include +#include + +#include + +#include "io.hpp" + +namespace utils::compression +{ + namespace zlib + { + namespace + { + class zlib_stream + { + public: + zlib_stream() + { + memset(&stream_, 0, sizeof(stream_)); + valid_ = inflateInit(&stream_) == Z_OK; + } + + zlib_stream(zlib_stream&&) = delete; + zlib_stream(const zlib_stream&) = delete; + zlib_stream& operator=(zlib_stream&&) = delete; + zlib_stream& operator=(const zlib_stream&) = delete; + + ~zlib_stream() + { + if (valid_) + { + inflateEnd(&stream_); + } + } + + z_stream& get() + { + return stream_; // + } + + bool is_valid() const + { + return valid_; + } + + private: + bool valid_{false}; + z_stream stream_{}; + }; + } + + std::string decompress(const std::string& data) + { + std::string buffer{}; + zlib_stream stream_container{}; + if (!stream_container.is_valid()) + { + return {}; + } + + int ret{}; + size_t offset = 0; + static thread_local uint8_t dest[CHUNK] = {0}; + auto& stream = stream_container.get(); + + do + { + const auto input_size = std::min(sizeof(dest), data.size() - offset); + stream.avail_in = static_cast(input_size); + stream.next_in = reinterpret_cast(data.data()) + offset; + offset += stream.avail_in; + + do + { + stream.avail_out = sizeof(dest); + stream.next_out = dest; + + ret = inflate(&stream, Z_NO_FLUSH); + if (ret != Z_OK && ret != Z_STREAM_END) + { + return {}; + } + + buffer.insert(buffer.end(), dest, dest + sizeof(dest) - stream.avail_out); + } + while (stream.avail_out == 0); + } + while (ret != Z_STREAM_END); + + return buffer; + } + + std::string compress(const std::string& data) + { + std::string result{}; + auto length = compressBound(static_cast(data.size())); + result.resize(length); + + if (compress2(reinterpret_cast(result.data()), &length, + reinterpret_cast(data.data()), static_cast(data.size()), + Z_BEST_COMPRESSION) != Z_OK) + { + return {}; + } + + result.resize(length); + return result; + } + } + + namespace zip + { + namespace + { + bool add_file(zipFile& zip_file, const std::string& filename, const std::string& data) + { + const auto zip_64 = data.size() > 0xffffffff ? 1 : 0; + if (ZIP_OK != zipOpenNewFileInZip64(zip_file, filename.data(), nullptr, nullptr, 0, nullptr, 0, nullptr, + Z_DEFLATED, Z_BEST_COMPRESSION, zip_64)) + { + return false; + } + + const auto _ = gsl::finally([&zip_file]() + { + zipCloseFileInZip(zip_file); + }); + + return ZIP_OK == zipWriteInFileInZip(zip_file, data.data(), static_cast(data.size())); + } + } + + void archive::add(std::string filename, std::string data) + { + this->files_[std::move(filename)] = std::move(data); + } + + bool archive::write(const std::string& filename, const std::string& comment) + { + // Hack to create the directory :3 + io::write_file(filename, {}); + io::remove_file(filename); + + auto* zip_file = zipOpen64(filename.data(), 0); + if (!zip_file) + { + return false; + } + + const auto _ = gsl::finally([&zip_file, &comment]() + { + zipClose(zip_file, comment.empty() ? nullptr : comment.data()); + }); + + for (const auto& file : this->files_) + { + if (!add_file(zip_file, file.first, file.second)) + { + return false; + } + } + + return true; + } + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/compression.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/compression.hpp new file mode 100644 index 0000000..dfe36ad --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/compression.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#define CHUNK 16384u + +namespace utils::compression +{ + namespace zlib + { + std::string compress(const std::string& data); + std::string decompress(const std::string& data); + } + + namespace zip + { + class archive + { + public: + void add(std::string filename, std::string data); + bool write(const std::string& filename, const std::string& comment = {}); + + private: + std::unordered_map files_; + }; + } +}; diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/concurrency.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/concurrency.hpp new file mode 100644 index 0000000..05c5d3a --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/concurrency.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +namespace utils::concurrency +{ + template + class container + { + public: + template + R access(F&& accessor) const + { + std::lock_guard _{mutex_}; + return accessor(object_); + } + + template + R access(F&& accessor) + { + std::lock_guard _{mutex_}; + return accessor(object_); + } + + template + R access_with_lock(F&& accessor) const + { + std::unique_lock lock{mutex_}; + return accessor(object_, lock); + } + + template + R access_with_lock(F&& accessor) + { + std::unique_lock lock{mutex_}; + return accessor(object_, lock); + } + + T& get_raw() { return object_; } + const T& get_raw() const { return object_; } + + private: + mutable MutexType mutex_{}; + T object_{}; + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/cryptography.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/cryptography.cpp new file mode 100644 index 0000000..debac67 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/cryptography.cpp @@ -0,0 +1,686 @@ +#include + +#include "cryptography.hpp" +#include "nt.hpp" +#include + +#undef max +using namespace std::string_literals; + +/// http://www.opensource.apple.com/source/CommonCrypto/CommonCrypto-55010/Source/libtomcrypt/doc/libTomCryptDoc.pdf + +namespace utils::cryptography +{ + namespace + { + struct __ + { + __() + { + ltc_mp = ltm_desc; + + register_cipher(&aes_desc); + register_cipher(&des3_desc); + + register_prng(&sprng_desc); + register_prng(&fortuna_desc); + register_prng(&yarrow_desc); + + register_hash(&sha1_desc); + register_hash(&sha256_desc); + register_hash(&sha512_desc); + } + } ___; + + [[maybe_unused]] const char* cs(const uint8_t* data) + { + return reinterpret_cast(data); + } + + [[maybe_unused]] char* cs(uint8_t* data) + { + return reinterpret_cast(data); + } + + [[maybe_unused]] const uint8_t* cs(const char* data) + { + return reinterpret_cast(data); + } + + [[maybe_unused]] uint8_t* cs(char* data) + { + return reinterpret_cast(data); + } + + [[maybe_unused]] unsigned long ul(const size_t value) + { + return static_cast(value); + } + + class prng + { + public: + prng(const ltc_prng_descriptor& descriptor, const bool autoseed = true) + : state_(std::make_unique()) + , descriptor_(descriptor) + { + this->id_ = register_prng(&descriptor); + if (this->id_ == -1) + { + throw std::runtime_error("PRNG "s + this->descriptor_.name + " could not be registered!"); + } + + if (autoseed) + { + this->auto_seed(); + } + else + { + this->descriptor_.start(this->state_.get()); + } + } + + ~prng() + { + this->descriptor_.done(this->state_.get()); + } + + prng_state* get_state() const + { + this->descriptor_.ready(this->state_.get()); + return this->state_.get(); + } + + int get_id() const + { + return this->id_; + } + + void add_entropy(const void* data, const size_t length) const + { + this->descriptor_.add_entropy(static_cast(data), ul(length), this->state_.get()); + } + + void read(void* data, const size_t length) const + { + this->descriptor_.read(static_cast(data), ul(length), this->get_state()); + } + + private: + int id_; + std::unique_ptr state_; + const ltc_prng_descriptor& descriptor_; + + void auto_seed() const + { + rng_make_prng(128, this->id_, this->state_.get(), nullptr); + + int i[4]; // uninitialized data + auto* i_ptr = &i; + this->add_entropy(reinterpret_cast(&i), sizeof(i)); + this->add_entropy(reinterpret_cast(&i_ptr), sizeof(i_ptr)); + + auto t = time(nullptr); + this->add_entropy(reinterpret_cast(&t), sizeof(t)); + } + }; + + const prng prng_(fortuna_desc); + } + + ecc::key::key() + { + ZeroMemory(&this->key_storage_, sizeof(this->key_storage_)); + } + + ecc::key::~key() + { + this->free(); + } + + ecc::key::key(key&& obj) noexcept + : key() + { + this->operator=(std::move(obj)); + } + + ecc::key::key(const key& obj) + : key() + { + this->operator=(obj); + } + + ecc::key& ecc::key::operator=(key&& obj) noexcept + { + if (this != &obj) + { + std::memmove(&this->key_storage_, &obj.key_storage_, sizeof(this->key_storage_)); + ZeroMemory(&obj.key_storage_, sizeof(obj.key_storage_)); + } + + return *this; + } + + ecc::key& ecc::key::operator=(const key& obj) + { + if (this != &obj && obj.is_valid()) + { + this->deserialize(obj.serialize(obj.key_storage_.type)); + } + + return *this; + } + + bool ecc::key::is_valid() const + { + return (!memory::is_set(&this->key_storage_, 0, sizeof(this->key_storage_))); + } + + ecc_key& ecc::key::get() + { + return this->key_storage_; + } + + const ecc_key& ecc::key::get() const + { + return this->key_storage_; + } + + std::string ecc::key::get_public_key() const + { + uint8_t buffer[512] = {0}; + unsigned long length = sizeof(buffer); + + if (ecc_ansi_x963_export(&this->key_storage_, buffer, &length) == CRYPT_OK) + { + return std::string(cs(buffer), length); + } + + return {}; + } + + void ecc::key::set(const std::string& pub_key_buffer) + { + this->free(); + + if (ecc_ansi_x963_import(cs(pub_key_buffer.data()), + ul(pub_key_buffer.size()), + &this->key_storage_) != CRYPT_OK) + { + ZeroMemory(&this->key_storage_, sizeof(this->key_storage_)); + } + } + + void ecc::key::deserialize(const std::string& key) + { + this->free(); + + if (ecc_import(cs(key.data()), ul(key.size()), + &this->key_storage_) != CRYPT_OK + ) + { + ZeroMemory(&this->key_storage_, sizeof(this->key_storage_)); + } + } + + std::string ecc::key::serialize(const int type) const + { + uint8_t buffer[4096] = {0}; + unsigned long length = sizeof(buffer); + + if (ecc_export(buffer, &length, type, &this->key_storage_) == CRYPT_OK) + { + return std::string(cs(buffer), length); + } + + return ""; + } + + void ecc::key::free() + { + if (this->is_valid()) + { + ecc_free(&this->key_storage_); + } + + ZeroMemory(&this->key_storage_, sizeof(this->key_storage_)); + } + + bool ecc::key::operator==(key& key) const + { + return (this->is_valid() && key.is_valid() && this->serialize(PK_PUBLIC) == key.serialize(PK_PUBLIC)); + } + + uint64_t ecc::key::get_hash() const + { + const auto hash = sha1::compute(this->get_public_key()); + if (hash.size() >= 8) + { + return *reinterpret_cast(hash.data()); + } + + return 0; + } + + ecc::key ecc::generate_key(const int bits) + { + key key; + ecc_make_key(prng_.get_state(), prng_.get_id(), bits / 8, &key.get()); + + return key; + } + + ecc::key ecc::generate_key(const int bits, const std::string& entropy) + { + key key{}; + const prng yarrow(yarrow_desc, false); + yarrow.add_entropy(entropy.data(), entropy.size()); + + ecc_make_key(yarrow.get_state(), yarrow.get_id(), bits / 8, &key.get()); + + return key; + } + + std::string ecc::sign_message(const key& key, const std::string& message) + { + if (!key.is_valid()) return ""; + + uint8_t buffer[512]; + unsigned long length = sizeof(buffer); + + ecc_sign_hash(cs(message.data()), ul(message.size()), buffer, &length, prng_.get_state(), prng_.get_id(), + &key.get()); + + return std::string(cs(buffer), length); + } + + bool ecc::verify_message(const key& key, const std::string& message, const std::string& signature) + { + if (!key.is_valid()) return false; + + auto result = 0; + return (ecc_verify_hash(cs(signature.data()), + ul(signature.size()), + cs(message.data()), + ul(message.size()), &result, + &key.get()) == CRYPT_OK && result != 0); + } + + bool ecc::encrypt(const key& key, std::string& data) + { + std::string out_data{}; + out_data.resize(std::max(ul(data.size() * 3), ul(0x100))); + + auto out_len = ul(out_data.size()); + auto crypt = [&]() + { + return ecc_encrypt_key(cs(data.data()), ul(data.size()), cs(out_data.data()), &out_len, + prng_.get_state(), prng_.get_id(), find_hash("sha512"), &key.get()); + }; + + auto res = crypt(); + + if (res == CRYPT_BUFFER_OVERFLOW) + { + out_data.resize(out_len); + res = crypt(); + } + + if (res != CRYPT_OK) + { + return false; + } + + out_data.resize(out_len); + data = std::move(out_data); + return true; + } + + bool ecc::decrypt(const key& key, std::string& data) + { + std::string out_data{}; + out_data.resize(std::max(ul(data.size() * 3), ul(0x100))); + + auto out_len = ul(out_data.size()); + auto crypt = [&]() + { + return ecc_decrypt_key(cs(data.data()), ul(data.size()), cs(out_data.data()), &out_len, &key.get()); + }; + + auto res = crypt(); + + if (res == CRYPT_BUFFER_OVERFLOW) + { + out_data.resize(out_len); + res = crypt(); + } + + if (res != CRYPT_OK) + { + return false; + } + + out_data.resize(out_len); + data = std::move(out_data); + return true; + } + + std::string rsa::encrypt(const std::string& data, const std::string& hash, const std::string& key) + { + rsa_key new_key; + rsa_import(cs(key.data()), ul(key.size()), &new_key); + const auto _ = gsl::finally([&]() + { + rsa_free(&new_key); + }); + + + std::string out_data{}; + out_data.resize(std::max(ul(data.size() * 3), ul(0x100))); + + auto out_len = ul(out_data.size()); + auto crypt = [&]() + { + return rsa_encrypt_key(cs(data.data()), ul(data.size()), cs(out_data.data()), &out_len, cs(hash.data()), + ul(hash.size()), prng_.get_state(), prng_.get_id(), find_hash("sha512"), &new_key); + }; + + auto res = crypt(); + + if (res == CRYPT_BUFFER_OVERFLOW) + { + out_data.resize(out_len); + res = crypt(); + } + + if (res == CRYPT_OK) + { + out_data.resize(out_len); + return out_data; + } + + return {}; + } + + std::string des3::encrypt(const std::string& data, const std::string& iv, const std::string& key) + { + std::string enc_data; + enc_data.resize(data.size()); + + symmetric_CBC cbc; + const auto des3 = find_cipher("3des"); + + cbc_start(des3, cs(iv.data()), cs(key.data()), static_cast(key.size()), 0, &cbc); + cbc_encrypt(cs(data.data()), cs(enc_data.data()), ul(data.size()), &cbc); + cbc_done(&cbc); + + return enc_data; + } + + std::string des3::decrypt(const std::string& data, const std::string& iv, const std::string& key) + { + std::string dec_data; + dec_data.resize(data.size()); + + symmetric_CBC cbc; + const auto des3 = find_cipher("3des"); + + cbc_start(des3, cs(iv.data()), cs(key.data()), static_cast(key.size()), 0, &cbc); + cbc_decrypt(cs(data.data()), cs(dec_data.data()), ul(data.size()), &cbc); + cbc_done(&cbc); + + return dec_data; + } + + std::string tiger::compute(const std::string& data, const bool hex) + { + return compute(cs(data.data()), data.size(), hex); + } + + std::string tiger::compute(const uint8_t* data, const size_t length, const bool hex) + { + uint8_t buffer[24] = {0}; + + hash_state state; + tiger_init(&state); + tiger_process(&state, data, ul(length)); + tiger_done(&state, buffer); + + std::string hash(cs(buffer), sizeof(buffer)); + if (!hex) return hash; + + return string::dump_hex(hash, ""); + } + + std::string aes::encrypt(const std::string& data, const std::string& iv, const std::string& key) + { + std::string aligned_data = data; + const auto mod = aligned_data.size() % 16; + if (mod != 0) + { + aligned_data.resize(aligned_data.size() + (16 - mod)); + } + + std::string enc_data; + enc_data.resize(aligned_data.size()); + + symmetric_CBC cbc; + const auto aes = find_cipher("aes"); + + cbc_start(aes, cs(iv.data()), cs(key.data()), + static_cast(key.size()), 0, &cbc); + cbc_encrypt(cs(aligned_data.data()), + cs(enc_data.data()), + ul(aligned_data.size()), &cbc); + cbc_done(&cbc); + + return enc_data; + } + + std::string aes::decrypt(const std::string& data, const std::string& iv, const std::string& key) + { + std::string dec_data; + dec_data.resize(data.size()); + + symmetric_CBC cbc; + const auto aes = find_cipher("aes"); + + cbc_start(aes, cs(iv.data()), cs(key.data()), + static_cast(key.size()), 0, &cbc); + cbc_decrypt(cs(data.data()), + cs(dec_data.data()), + ul(data.size()), &cbc); + cbc_done(&cbc); + + return dec_data; + } + + std::string hmac_sha1::compute(const std::string& data, const std::string& key) + { + std::string buffer; + buffer.resize(20); + + hmac_state state; + hmac_init(&state, find_hash("sha1"), cs(key.data()), ul(key.size())); + hmac_process(&state, cs(data.data()), static_cast(data.size())); + + auto out_len = ul(buffer.size()); + hmac_done(&state, cs(buffer.data()), &out_len); + + buffer.resize(out_len); + return buffer; + } + + std::string sha1::compute(const std::string& data, const bool hex) + { + return compute(cs(data.data()), data.size(), hex); + } + + std::string sha1::compute(const uint8_t* data, const size_t length, const bool hex) + { + uint8_t buffer[20] = {0}; + + hash_state state; + sha1_init(&state); + sha1_process(&state, data, ul(length)); + sha1_done(&state, buffer); + + std::string hash(cs(buffer), sizeof(buffer)); + if (!hex) return hash; + + return string::dump_hex(hash, ""); + } + + std::string sha256::compute(const std::string& data, const bool hex) + { + return compute(cs(data.data()), data.size(), hex); + } + + std::string sha256::compute(const uint8_t* data, const size_t length, const bool hex) + { + uint8_t buffer[32] = {0}; + + hash_state state; + sha256_init(&state); + sha256_process(&state, data, ul(length)); + sha256_done(&state, buffer); + + std::string hash(cs(buffer), sizeof(buffer)); + if (!hex) return hash; + + return string::dump_hex(hash, ""); + } + + std::string sha512::compute(const std::string& data, const bool hex) + { + return compute(cs(data.data()), data.size(), hex); + } + + std::string sha512::compute(const uint8_t* data, const size_t length, const bool hex) + { + uint8_t buffer[64] = {0}; + + hash_state state; + sha512_init(&state); + sha512_process(&state, data, ul(length)); + sha512_done(&state, buffer); + + std::string hash(cs(buffer), sizeof(buffer)); + if (!hex) return hash; + + return string::dump_hex(hash, ""); + } + + namespace md5 + { + std::string compute(const std::string& data, bool hex) + { + return compute(cs(data.data()), data.size(), hex); + } + + std::string compute(const uint8_t* data, size_t length, bool hex) + { + uint8_t buffer[16] = {0}; + + hash_state state; + md5_init(&state); + md5_process(&state, data, ul(length)); + md5_done(&state, buffer); + + std::string hash(cs(buffer), sizeof(buffer)); + if (!hex) return hash; + + return string::dump_hex(hash, ""); + } + } + + std::string base64::encode(const uint8_t* data, const size_t len) + { + std::string result; + result.resize((len + 2) * 2); + + auto out_len = ul(result.size()); + if (base64_encode(data, ul(len), result.data(), &out_len) != CRYPT_OK) + { + return {}; + } + + result.resize(out_len); + return result; + } + + std::string base64::encode(const std::string& data) + { + return base64::encode(cs(data.data()), static_cast(data.size())); + } + + std::string base64::decode(const std::string& data) + { + std::string result; + result.resize((data.size() + 2) * 2); + + auto out_len = ul(result.size()); + if (base64_decode(data.data(), ul(data.size()), cs(result.data()), &out_len) != CRYPT_OK) + { + return {}; + } + + result.resize(out_len); + return result; + } + + unsigned int jenkins_one_at_a_time::compute(const std::string& data) + { + return compute(data.data(), data.size()); + } + + unsigned int jenkins_one_at_a_time::compute(const char* key, const size_t len) + { + unsigned int hash, i; + for (hash = i = 0; i < len; ++i) + { + hash += key[i]; + hash += (hash << 10); + hash ^= (hash >> 6); + } + hash += (hash << 3); + hash ^= (hash >> 11); + hash += (hash << 15); + return hash; + } + + uint32_t random::get_integer() + { + uint32_t result; + random::get_data(&result, sizeof(result)); + return result; + } + + uint32_t random::get_integer(const std::uint32_t min, const std::uint32_t max) + { + const auto range = max - min + 1; + const auto value = random::get_integer(); + return value % range + min; + } + + std::string random::get_challenge() + { + std::string result; + result.resize(sizeof(uint32_t)); + random::get_data(result.data(), result.size()); + return string::dump_hex(result, ""); + } + + void random::get_data(void* data, const size_t size) + { + prng_.read(data, size); + } + + std::string random::get_data(const size_t size) + { + std::string data; + data.resize(size); + random::get_data(data.data(), size); + return data; + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/cryptography.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/cryptography.hpp new file mode 100644 index 0000000..01409a7 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/cryptography.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include +#include +#include "string.hpp" + +namespace utils::cryptography +{ + namespace ecc + { + class key final + { + public: + key(); + ~key(); + + key(key&& obj) noexcept; + key(const key& obj); + key& operator=(key&& obj) noexcept; + key& operator=(const key& obj); + + bool is_valid() const; + + ecc_key& get(); + const ecc_key& get() const; + + std::string get_public_key() const; + + void set(const std::string& pub_key_buffer); + + void deserialize(const std::string& key); + + std::string serialize(int type = PK_PRIVATE) const; + + void free(); + + bool operator==(key& key) const; + + uint64_t get_hash() const; + + private: + ecc_key key_storage_{}; + }; + + key generate_key(int bits); + key generate_key(int bits, const std::string& entropy); + std::string sign_message(const key& key, const std::string& message); + bool verify_message(const key& key, const std::string& message, const std::string& signature); + + bool encrypt(const key& key, std::string& data); + bool decrypt(const key& key, std::string& data); + } + + namespace rsa + { + std::string encrypt(const std::string& data, const std::string& hash, const std::string& key); + std::string decrypt(const std::string& data, const std::string& hash, const std::string& key); + } + + namespace des3 + { + std::string encrypt(const std::string& data, const std::string& iv, const std::string& key); + std::string decrypt(const std::string& data, const std::string& iv, const std::string& key); + } + + namespace tiger + { + std::string compute(const std::string& data, bool hex = false); + std::string compute(const uint8_t* data, size_t length, bool hex = false); + } + + namespace aes + { + std::string encrypt(const std::string& data, const std::string& iv, const std::string& key); + std::string decrypt(const std::string& data, const std::string& iv, const std::string& key); + } + + namespace hmac_sha1 + { + std::string compute(const std::string& data, const std::string& key); + } + + namespace sha1 + { + std::string compute(const std::string& data, bool hex = false); + std::string compute(const uint8_t* data, size_t length, bool hex = false); + } + + namespace sha256 + { + std::string compute(const std::string& data, bool hex = false); + std::string compute(const uint8_t* data, size_t length, bool hex = false); + } + + namespace sha512 + { + std::string compute(const std::string& data, bool hex = false); + std::string compute(const uint8_t* data, size_t length, bool hex = false); + } + + namespace md5 + { + std::string compute(const std::string& data, bool hex = false); + std::string compute(const uint8_t* data, size_t length, bool hex = false); + } + + namespace argon2 + { + template + std::string compute(const std::string& data, bool hex = false) + { + std::uint8_t buffer[HashLen]{}; + std::uint8_t salt[SaltLen]{}; + + argon2i_hash_raw(TCost, MCost, Threads, data.data(), data.size(), salt, SaltLen, buffer, HashLen); + + const auto str = std::string{reinterpret_cast(buffer), HashLen}; + if (hex) + { + return string::dump_hex(str, "", false); + } + else + { + return str; + } + } + } + + namespace base64 + { + std::string encode(const uint8_t* data, size_t len); + std::string encode(const std::string& data); + std::string decode(const std::string& data); + } + + namespace jenkins_one_at_a_time + { + unsigned int compute(const std::string& data); + unsigned int compute(const char* key, size_t len); + }; + + namespace random + { + uint32_t get_integer(); + uint32_t get_integer(const std::uint32_t min, const std::uint32_t max); + std::string get_challenge(); + void get_data(void* data, size_t size); + std::string get_data(const size_t size); + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/file_watcher.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/file_watcher.cpp new file mode 100644 index 0000000..47eb4a0 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/file_watcher.cpp @@ -0,0 +1,160 @@ +#include +#include + +#include "string.hpp" +#include "file_watcher.hpp" + +namespace utils::io +{ + file_watcher::file_watcher(const std::string& path) + { + this->path_ = path; + const auto pos = this->path_.find_last_of("/\\"); + if (pos != std::string::npos) + { + this->filename_ = path.substr(pos + 1); + } + else + { + this->filename_ = path; + } + } + + file_watcher::~file_watcher() + { +#ifdef WIN32 + const auto close = [](HANDLE handle) + { + if (handle != NULL && handle != INVALID_HANDLE_VALUE) + { + CloseHandle(handle); + } + }; + + close(this->handle_); + close(this->event_handle_); + close(this->stop_handle_); +#endif + } + + bool file_watcher::init() + { +#ifdef WIN32 + std::string folder{}; + const auto pos = this->path_.find_last_of("/\\"); + if (pos != std::string::npos) + { + folder = this->path_.substr(0, pos); + } + + this->handle_ = CreateFile( + folder.data(), + FILE_LIST_DIRECTORY, + FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, + NULL + ); + this->event_handle_ = CreateEvent(NULL, TRUE, FALSE, NULL); + this->stop_handle_ = CreateEvent(NULL, TRUE, FALSE, NULL); + + const auto is_valid = [](HANDLE handle) + { + return handle != NULL && handle != INVALID_HANDLE_VALUE; + }; + + if (!is_valid(this->event_handle_) || + !is_valid(this->handle_ ) || + !is_valid(this->stop_handle_)) + { + return false; + } + + this->stream_ = std::ifstream{this->path_, std::ios::binary}; + this->stream_.seekg(0, std::ios::end); + this->current_pos_ = static_cast(this->stream_.tellg()); + + return true; +#endif + } + + bool file_watcher::run(int wait_time, std::string* data, bool force_check) + { +#ifdef WIN32 + this->stop_ = false; + + char buffer[0x1000] = {0}; + OVERLAPPED overlapped = {}; + overlapped.hEvent = this->event_handle_; + + ReadDirectoryChangesW(this->handle_, buffer, sizeof(buffer), + FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE, NULL, &overlapped, NULL); + + HANDLE handles[2] = + { + this->event_handle_, + this->stop_handle_ + }; + + WaitForMultipleObjects(2, handles, FALSE, wait_time); + if (this->stop_) + { + return false; + } + + const auto info = reinterpret_cast(buffer); + const auto filename = utils::string::convert(info->FileName); + + if (!force_check && (filename != this->filename_ || (info->Action & FILE_GENERIC_WRITE) == 0)) + { + return false; + } + + this->stream_.seekg(0, std::ios::end); + const auto new_size = static_cast(this->stream_.tellg()); + + if (new_size < this->current_pos_) + { + this->current_pos_ = new_size; + return false; + } + + const auto bytes_written = new_size - this->current_pos_; + if (bytes_written == 0) + { + return false; + } + + this->stream_.seekg(-1 * static_cast(bytes_written), std::ios::cur); + this->current_pos_ += bytes_written; + + data->resize(bytes_written); + this->stream_.read(const_cast(data->data()), bytes_written); + return true; +#endif + } + + bool file_watcher::run_blocking(std::string* data, bool force_check) + { +#ifdef WIN32 + return this->run(INFINITE, data, force_check); +#endif + } + + bool file_watcher::run(const std::chrono::high_resolution_clock::duration& wait_time, std::string* data, bool force_check) + { +#ifdef WIN32 + const auto msec = static_cast(std::chrono::duration_cast(wait_time).count()); + return this->run(msec, data, force_check); +#endif + } + + void file_watcher::stop() + { + this->stop_ = true; +#ifdef WIN32 + SetEvent(this->stop_handle_); +#endif + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/file_watcher.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/file_watcher.hpp new file mode 100644 index 0000000..86dd2ca --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/file_watcher.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +namespace utils::io +{ + class file_watcher + { + public: + file_watcher(const std::string& path); + + file_watcher(file_watcher&&) noexcept = delete; + file_watcher& operator=(file_watcher&&) noexcept = delete; + + file_watcher(const file_watcher&) = delete; + file_watcher& operator=(const file_watcher&) = delete; + + ~file_watcher(); + + bool init(); + bool run(int wait_time, std::string* data, bool force_check); + bool run(const std::chrono::high_resolution_clock::duration& wait_time, + std::string* data, bool force_check = false); + bool run_blocking(std::string* data, bool force_check = false); + void stop(); + + private: + std::string path_{}; + std::string filename_{}; + + std::ifstream stream_{}; + size_t current_pos_{}; + +#ifdef WIN32 + HANDLE handle_{}; + HANDLE event_handle_{}; + HANDLE stop_handle_{}; +#endif + + bool stop_{}; + + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/flags.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/flags.cpp new file mode 100644 index 0000000..9d467f4 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/flags.cpp @@ -0,0 +1,54 @@ +#include + +#include "flags.hpp" +#include "string.hpp" + +#include + +namespace utils::flags +{ + void parse_flags(std::vector& flags) + { + int num_args; + auto* const argv = CommandLineToArgvW(GetCommandLineW(), &num_args); + + flags.clear(); + + if (argv) + { + for (auto i = 0; i < num_args; ++i) + { + std::wstring wide_flag(argv[i]); + if (wide_flag[0] == L'-') + { + wide_flag.erase(wide_flag.begin()); + flags.emplace_back(string::convert(wide_flag)); + } + } + + LocalFree(argv); + } + } + + bool has_flag(const std::string& flag) + { + static auto parsed = false; + static std::vector enabled_flags; + + if (!parsed) + { + parse_flags(enabled_flags); + parsed = true; + } + + for (const auto& entry : enabled_flags) + { + if (string::to_lower(entry) == string::to_lower(flag)) + { + return true; + } + } + + return false; + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/flags.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/flags.hpp new file mode 100644 index 0000000..7f09cae --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/flags.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace utils::flags +{ + bool has_flag(const std::string& flag); +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/hook.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/hook.cpp new file mode 100644 index 0000000..7248bec --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/hook.cpp @@ -0,0 +1,247 @@ +#include +#include "hook.hpp" +#include "string.hpp" +// iw6x-client + +namespace utils::hook +{ + void signature::process() + { + if (this->signatures_.empty()) return; + + const auto start = static_cast(this->start_); + + const unsigned int sig_count = this->signatures_.size(); + const auto containers = this->signatures_.data(); + + for (size_t i = 0; i < this->length_; ++i) + { + const auto address = start + i; + + for (unsigned int k = 0; k < sig_count; ++k) + { + const auto container = &containers[k]; + + unsigned int j; + for (j = 0; j < static_cast(container->mask.size()); ++j) + { + if (container->mask[j] != '?' && container->signature[j] != address[j]) + { + break; + } + } + + if (j == container->mask.size()) + { + container->callback(address); + } + } + } + } + + void signature::add(const container& container) + { + signatures_.push_back(container); + } + + namespace + { + [[maybe_unused]] class _ + { + public: + _() + { + if (MH_Initialize() != MH_OK) + { + throw std::runtime_error("Failed to initialize MinHook"); + } + } + + ~_() + { + MH_Uninitialize(); + } + } __; + } + + asmjit::Error assembler::call(void* target) + { + return Assembler::call(size_t(target)); + } + + asmjit::Error assembler::jmp(void* target) + { + return Assembler::jmp(size_t(target)); + } + + detour::detour(const size_t place, void* target) : detour(reinterpret_cast(place), target) + { + } + + detour::detour(void* place, void* target) + { + this->create(place, target); + } + + detour::~detour() + { + this->clear(); + } + + void detour::enable() const + { + MH_EnableHook(this->place_); + } + + void detour::disable() const + { + MH_DisableHook(this->place_); + } + + void detour::create(void* place, void* target) + { + this->clear(); + this->place_ = place; + + if (MH_CreateHook(this->place_, target, &this->original_) != MH_OK) + { + throw std::runtime_error(string::va("Unable to create hook at location: %p", this->place_)); + } + + this->enable(); + } + + void detour::create(const size_t place, void* target) + { + this->create(reinterpret_cast(place), target); + } + + void detour::clear() + { + if (this->place_) + { + MH_RemoveHook(this->place_); + } + + this->place_ = nullptr; + this->original_ = nullptr; + } + + void* detour::get_original() const + { + return this->original_; + } + + void nop(void* place, const size_t length) + { + DWORD old_protect{}; + VirtualProtect(place, length, PAGE_EXECUTE_READWRITE, &old_protect); + + std::memset(place, 0x90, length); + + VirtualProtect(place, length, old_protect, &old_protect); + FlushInstructionCache(GetCurrentProcess(), place, length); + } + + void nop(const size_t place, const size_t length) + { + nop(reinterpret_cast(place), length); + } + + void copy(void* place, const void* data, const size_t length) + { + DWORD old_protect{}; + VirtualProtect(place, length, PAGE_EXECUTE_READWRITE, &old_protect); + + std::memmove(place, data, length); + + VirtualProtect(place, length, old_protect, &old_protect); + FlushInstructionCache(GetCurrentProcess(), place, length); + } + + void copy(const size_t place, const void* data, const size_t length) + { + copy(reinterpret_cast(place), data, length); + } + + bool is_relatively_far(const void* pointer, const void* data, int offset) + { + const int64_t diff = size_t(data) - (size_t(pointer) + offset); + const auto small_diff = int32_t(diff); + return diff != int64_t(small_diff); + } + + void call(void* pointer, void* data) + { + if (is_relatively_far(pointer, data)) + { + throw std::runtime_error("Too far away to create 32bit relative branch"); + } + + auto* patch_pointer = PBYTE(pointer); + set(patch_pointer, 0xE8); + set(patch_pointer + 1, int32_t(size_t(data) - (size_t(pointer) + 5))); + } + + void call(const size_t pointer, void* data) + { + return call(reinterpret_cast(pointer), data); + } + + void call(const size_t pointer, const size_t data) + { + return call(pointer, reinterpret_cast(data)); + } + + void set(std::uintptr_t address, std::vector&& bytes) + { + DWORD oldProtect = 0; + + auto* place = reinterpret_cast(address); + VirtualProtect(place, bytes.size(), PAGE_EXECUTE_READWRITE, &oldProtect); + memcpy(place, bytes.data(), bytes.size()); + VirtualProtect(place, bytes.size(), oldProtect, &oldProtect); + FlushInstructionCache(GetCurrentProcess(), place, bytes.size()); + } + + void set(std::uintptr_t address, void* buffer, size_t size) + { + DWORD oldProtect = 0; + + auto* place = reinterpret_cast(address); + VirtualProtect(place, size, PAGE_EXECUTE_READWRITE, &oldProtect); + memcpy(place, buffer, size); + VirtualProtect(place, size, oldProtect, &oldProtect); + FlushInstructionCache(GetCurrentProcess(), place, size); + } + + void jump(std::uintptr_t address, void* destination) + { + if (!address) return; + + std::uint8_t* bytes = new std::uint8_t[5]; + *bytes = 0xE9; + *reinterpret_cast(bytes + 1) = CalculateRelativeJMPAddress(address, destination); + + set(address, bytes, 5); + + delete[] bytes; + } + + void* assemble(const std::function& asm_function) + { + static asmjit::JitRuntime runtime; + + asmjit::CodeHolder code; + code.init(runtime.environment()); + + assembler a(&code); + + asm_function(a); + + void* result = nullptr; + runtime.add(&result, &code); + + return result; + } +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/hook.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/hook.hpp new file mode 100644 index 0000000..436bdc0 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/hook.hpp @@ -0,0 +1,177 @@ +#pragma once + +#define CalculateRelativeJMPAddress(X, Y) (((std::uintptr_t)Y - (std::uintptr_t)X) - 5) + +#include +#include + +using namespace asmjit::x86; + +namespace utils::hook +{ + class signature final + { + public: + struct container final + { + std::string signature; + std::string mask; + std::function callback; + }; + + signature(void* start, const size_t length) : start_(start), length_(length) + { + } + + signature(const DWORD start, const size_t length) : signature(reinterpret_cast(start), length) + { + } + + signature() : signature(0x400000, 0x800000) + { + } + + void process(); + void add(const container& container); + + private: + void* start_; + size_t length_; + std::vector signatures_; + }; + + class assembler : public Assembler + { + public: + using Assembler::Assembler; + using Assembler::call; + using Assembler::jmp; + + asmjit::Error call(void* target); + asmjit::Error jmp(void* target); + }; + + class detour + { + public: + detour() = default; + detour(void* place, void* target); + detour(size_t place, void* target); + ~detour(); + + detour(detour&& other) noexcept + { + this->operator=(std::move(other)); + } + + detour& operator= (detour&& other) noexcept + { + if (this != &other) + { + this->~detour(); + + this->place_ = other.place_; + this->original_ = other.original_; + + other.place_ = nullptr; + other.original_ = nullptr; + } + + return *this; + } + + detour(const detour&) = delete; + detour& operator= (const detour&) = delete; + + void enable() const; + void disable() const; + + void create(void* place, void* target); + void create(size_t place, void* target); + void clear(); + + template + T* get() const + { + return static_cast(this->get_original()); + } + + template + T invoke(Args... args) + { + return static_cast(this->get_original())(args...); + } + + template + T invoke_pascal(Args... args) + { + return static_cast(this->get_original())(args...); + } + + [[nodiscard]] void* get_original() const; + + private: + void* place_{}; + void* original_{}; + }; + + void nop(void* place, size_t length); + void nop(size_t place, size_t length); + + void copy(void* place, const void* data, size_t length); + void copy(size_t place, const void* data, size_t length); + + bool is_relatively_far(const void* pointer, const void* data, int offset = 5); + + void call(void* pointer, void* data); + void call(size_t pointer, void* data); + void call(size_t pointer, size_t data); + + void jump(std::uintptr_t address, void* destination); + + void* assemble(const std::function& asm_function); + + template + T extract(void* address) + { + const auto data = static_cast(address); + const auto offset = *reinterpret_cast(data); + return reinterpret_cast(data + offset + 4); + } + + template + T extract(const size_t address) + { + return extract(reinterpret_cast(address)); + } + + template + static void set(void* place, T value) + { + DWORD old_protect; + VirtualProtect(place, sizeof(T), PAGE_EXECUTE_READWRITE, &old_protect); + + *static_cast(place) = value; + + VirtualProtect(place, sizeof(T), old_protect, &old_protect); + FlushInstructionCache(GetCurrentProcess(), place, sizeof(T)); + } + + template + static void set(const size_t place, T value) + { + return set(reinterpret_cast(place), value); + } + + template + static T invoke(size_t func, Args... args) + { + return reinterpret_cast(func)(args...); + } + + template + static T invoke(void* func, Args... args) + { + return static_cast(func)(args...); + } +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/http.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/http.cpp new file mode 100644 index 0000000..42f8269 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/http.cpp @@ -0,0 +1,112 @@ +#ifdef _WIN32 +#include +#endif +#include "http.hpp" + +#include +#include + +#ifdef _WIN32 +#pragma comment(lib, "ws2_32.lib") +#endif + +namespace utils::http +{ + namespace + { + size_t write_callback(void* contents, const size_t size, const size_t nmemb, void* userp) noexcept + { + const auto buffer = static_cast(userp); + const auto total_size = size * nmemb; + try + { + buffer->append(static_cast(contents), total_size); + } + catch (...) + { + return 0; // Never unwind a C++ exception through libcurl. + } + return total_size; + } + + double timing_ms(CURL* curl, const CURLINFO info) + { + double seconds = 0; + curl_easy_getinfo(curl, info, &seconds); + return seconds * 1000.0; + } + } + + std::optional get_data(const std::string& url, const std::string& fields, + const headers& headers, const std::string& method, const timeouts& limits) + { + result response; + if (limits.connect_timeout_ms <= 0 || limits.timeout_ms <= 0) + { + response.code = CURLE_BAD_FUNCTION_ARGUMENT; + return response; + } + const std::unique_ptr handle(curl_easy_init(), curl_easy_cleanup); + const auto curl = handle.get(); + if (!curl) + { + return {}; + } + + std::unique_ptr header_list(nullptr, curl_slist_free_all); + + for (const auto& header : headers) + { + auto data = header.first + ": " + header.second; + const auto appended = curl_slist_append(header_list.get(), data.data()); + if (!appended) + { + response.code = CURLE_OUT_OF_MEMORY; + return response; + } + header_list.release(); + header_list.reset(appended); + } + + std::string buffer{}; + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list.get()); + curl_easy_setopt(curl, CURLOPT_URL, url.data()); + // Real native deadlines: a script-side wait limit cannot stop this transfer. + auto code = curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, limits.connect_timeout_ms); + if (code == CURLE_OK) + code = curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, limits.timeout_ms); + if (code != CURLE_OK) + { + response.code = code; + return response; + } + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); + + if (!fields.empty()) + { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, fields.data()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, static_cast(fields.size())); + } + + if (!method.empty()) + { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.data()); + } + + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); + + response.code = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.status); + response.dns_ms = timing_ms(curl, CURLINFO_NAMELOOKUP_TIME); + response.connect_ms = timing_ms(curl, CURLINFO_CONNECT_TIME); + response.tls_ms = timing_ms(curl, CURLINFO_APPCONNECT_TIME); + response.first_byte_ms = timing_ms(curl, CURLINFO_STARTTRANSFER_TIME); + response.total_ms = timing_ms(curl, CURLINFO_TOTAL_TIME); + if (response.code == CURLE_OK) + response.buffer = std::move(buffer); + return response; + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/http.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/http.hpp new file mode 100644 index 0000000..c7d91f1 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/http.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +#include + +namespace utils::http +{ + struct result + { + CURLcode code = CURLE_FAILED_INIT; + std::string buffer; + long status = 0; + // Cumulative milliseconds from the start of curl_easy_perform. + double dns_ms = 0; + double connect_ms = 0; + double tls_ms = 0; + double first_byte_ms = 0; + double total_ms = 0; + }; + + struct timeouts + { + long connect_timeout_ms = 5000; + long timeout_ms = 15000; + }; + + using headers = std::unordered_map; + + std::optional get_data(const std::string& url, const std::string& fields = {}, + const headers& headers = {}, const std::string& method = {}, const timeouts& limits = {}); +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/info_string.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/info_string.cpp new file mode 100644 index 0000000..836ef07 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/info_string.cpp @@ -0,0 +1,67 @@ +#include + +#include "info_string.hpp" +#include "string.hpp" + +namespace utils +{ + info_string::info_string(const std::string& buffer) + { + this->parse(buffer); + } + + void info_string::set(const std::string& key, const std::string& value) + { + this->key_value_pairs_[key] = value; + } + + void info_string::remove(const std::string& key) + { + this->key_value_pairs_.erase(key); + } + + std::string info_string::get(const std::string& key) const + { + const auto value = this->key_value_pairs_.find(key); + if (value != this->key_value_pairs_.end()) + { + return value->second; + } + + return {}; + } + + void info_string::parse(std::string buffer) + { + if (buffer[0] == '\\') + { + buffer = buffer.substr(1); + } + + const auto key_values = string::split(buffer, '\\'); + for (size_t i = 0; !key_values.empty() && i < (key_values.size() - 1); i += 2) + { + const auto& key = key_values[i]; + const auto& value = key_values[i + 1]; + + if (!this->key_value_pairs_.contains(key)) + { + this->key_value_pairs_[key] = value; + } + } + } + + std::string info_string::build() const + { + std::string info_string; + for (const auto& [key, val] : this->key_value_pairs_) + { + info_string.append("\\"); + info_string.append(key); + info_string.append("\\"); + info_string.append(val); + } + + return info_string; + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/info_string.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/info_string.hpp new file mode 100644 index 0000000..c1f09dd --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/info_string.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace utils +{ + class info_string + { + public: + info_string() = default; + info_string(const std::string& buffer); + + void set(const std::string& key, const std::string& value); + void remove(const std::string& key); + + [[nodiscard]] std::string get(const std::string& key) const; + [[nodiscard]] std::string build() const; + + private: + std::unordered_map key_value_pairs_{}; + + void parse(std::string buffer); + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/io.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/io.cpp new file mode 100644 index 0000000..ff034b1 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/io.cpp @@ -0,0 +1,136 @@ +#include +#include +#include "io.hpp" + +namespace utils::io +{ + bool remove_file(const std::string& file) + { + return DeleteFileA(file.data()) == TRUE; + } + + bool move_file(const std::string& src, const std::string& target) + { + return MoveFileA(src.data(), target.data()) == TRUE; + } + + bool file_exists(const std::string& file) + { + return std::ifstream(file).good(); + } + + bool write_file(const std::string& file, const std::string& data, const bool append) + { + const auto pos = file.find_last_of("/\\"); + if (pos != std::string::npos) + { + create_directory(file.substr(0, pos)); + } + + std::ofstream stream( + file, std::ios::binary | std::ofstream::out | (append ? std::ofstream::app : 0)); + + if (stream.is_open()) + { + stream.write(data.data(), data.size()); + stream.close(); + return true; + } + + return false; + } + + std::string read_file(const std::string& file) + { + std::string data; + read_file(file, &data); + return data; + } + + bool read_file(const std::string& file, std::string* data) + { + if (!data) return false; + data->clear(); + + if (file_exists(file)) + { + std::ifstream stream(file, std::ios::binary); + if (!stream.is_open()) return false; + + stream.seekg(0, std::ios::end); + const std::streamsize size = stream.tellg(); + stream.seekg(0, std::ios::beg); + + if (size > -1) + { + data->resize(static_cast(size)); + stream.read(const_cast(data->data()), size); + stream.close(); + return true; + } + } + + return false; + } + + size_t file_size(const std::string& file) + { + if (file_exists(file)) + { + std::ifstream stream(file, std::ios::binary); + + if (stream.good()) + { + stream.seekg(0, std::ios::end); + return static_cast(stream.tellg()); + } + } + + return 0; + } + + bool create_directory(const std::string& directory) + { + return std::filesystem::create_directories(directory); + } + + bool directory_exists(const std::string& directory) + { + return std::filesystem::is_directory(directory); + } + + bool directory_is_empty(const std::string& directory) + { + return std::filesystem::is_empty(directory); + } + + bool remove_directory(const std::string& directory, const bool recursive) + { + std::error_code ec; + + if (recursive) + { + std::uintmax_t n{ std::filesystem::remove_all(directory, ec) }; + return (n && (n != static_cast(-1))); // Why did they make it this way? + } + + return std::filesystem::remove(directory, ec); + } + + std::vector list_files(const std::string& directory) + { + std::vector files; + + for (auto& file : std::filesystem::directory_iterator(directory)) + { + files.push_back(file.path().generic_string()); + } + + return files; + } + + void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target) + { + std::filesystem::copy(src, target, std::filesystem::copy_options::overwrite_existing | std::filesystem::copy_options::recursive); + } +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/io.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/io.hpp new file mode 100644 index 0000000..fcabd94 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/io.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +namespace utils::io +{ + bool remove_file(const std::string& file); + bool move_file(const std::string& src, const std::string& target); + bool file_exists(const std::string& file); + bool write_file(const std::string& file, const std::string& data, bool append = false); + bool read_file(const std::string& file, std::string* data); + std::string read_file(const std::string& file); + size_t file_size(const std::string& file); + bool create_directory(const std::string& directory); + bool directory_exists(const std::string& directory); + bool directory_is_empty(const std::string& directory); + bool remove_directory(const std::string& directory, bool recursive = false); + std::vector list_files(const std::string& directory); + void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target); +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/memory.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/memory.cpp new file mode 100644 index 0000000..3809feb --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/memory.cpp @@ -0,0 +1,109 @@ +#include +#include "memory.hpp" + +namespace utils +{ + memory::allocator memory::mem_allocator_; + + memory::allocator::~allocator() + { + this->clear(); + } + + void memory::allocator::clear() + { + std::lock_guard _(this->mutex_); + + for (auto& data : this->pool_) + { + memory::free(data); + } + + this->pool_.clear(); + } + + void memory::allocator::free(void* data) + { + std::lock_guard _(this->mutex_); + + const auto j = std::find(this->pool_.begin(), this->pool_.end(), data); + if (j != this->pool_.end()) + { + memory::free(data); + this->pool_.erase(j); + } + } + + void memory::allocator::free(const void* data) + { + this->free(const_cast(data)); + } + + void* memory::allocator::allocate(const size_t length) + { + std::lock_guard _(this->mutex_); + + const auto data = memory::allocate(length); + this->pool_.push_back(data); + return data; + } + + bool memory::allocator::empty() const + { + return this->pool_.empty(); + } + + char* memory::allocator::duplicate_string(const std::string& string) + { + std::lock_guard _(this->mutex_); + + const auto data = memory::duplicate_string(string); + this->pool_.push_back(data); + return data; + } + + void* memory::allocate(const size_t length) + { + return calloc(length, 1); + } + + char* memory::duplicate_string(const std::string& string) + { + const auto new_string = allocate_array(string.size() + 1); + std::memcpy(new_string, string.data(), string.size()); + return new_string; + } + + void memory::free(void* data) + { + if (data) + { + ::free(data); + } + } + + void memory::free(const void* data) + { + free(const_cast(data)); + } + + bool memory::is_set(const void* mem, const char chr, const size_t length) + { + const auto mem_arr = static_cast(mem); + + for (size_t i = 0; i < length; ++i) + { + if (mem_arr[i] != chr) + { + return false; + } + } + + return true; + } + + memory::allocator* memory::get_allocator() + { + return &memory::mem_allocator_; + } +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/memory.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/memory.hpp new file mode 100644 index 0000000..6d6a8b5 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/memory.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +namespace utils +{ + class memory final + { + public: + class allocator final + { + public: + ~allocator(); + + void clear(); + + void free(void* data); + + void free(const void* data); + + void* allocate(size_t length); + + template + inline T* allocate() + { + return this->allocate_array(1); + } + + template + inline T* allocate_array(const size_t count = 1) + { + return static_cast(this->allocate(count * sizeof(T))); + } + + bool empty() const; + + char* duplicate_string(const std::string& string); + + private: + std::mutex mutex_; + std::vector pool_; + }; + + static void* allocate(size_t length); + + template + static inline T* allocate() + { + return allocate_array(1); + } + + template + static inline T* allocate_array(const size_t count = 1) + { + return static_cast(allocate(count * sizeof(T))); + } + + static char* duplicate_string(const std::string& string); + + static void free(void* data); + static void free(const void* data); + + static bool is_set(const void* mem, char chr, size_t length); + + static bool is_bad_read_ptr(const void* ptr); + static bool is_bad_code_ptr(const void* ptr); + static bool is_rdata_ptr(void* ptr); + + static allocator* get_allocator(); + + private: + static allocator mem_allocator_; + }; +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/nt.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/nt.cpp new file mode 100644 index 0000000..ebcd6b8 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/nt.cpp @@ -0,0 +1,250 @@ +#include +#include "nt.hpp" +#include "string.hpp" + +namespace utils::nt +{ + HMODULE library::current_handle_; + + library library::load(const std::string& name) + { + return library(LoadLibraryA(name.data())); + } + + library library::load(const std::filesystem::path& path) + { + return library::load(path.generic_string()); + } + + library library::get_by_address(void* address) + { + HMODULE handle = nullptr; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, static_cast(address), &handle); + return library(handle); + } + + void library::set_current_handle(HMODULE handle) + { + current_handle_ = handle; + } + + HMODULE library::get_current_handle() + { + return current_handle_; + } + + library::library() + { + this->module_ = GetModuleHandleA(nullptr); + } + + library::library(const std::string& name) + { + this->module_ = GetModuleHandleA(name.data()); + } + + library::library(const HMODULE handle) + { + this->module_ = handle; + } + + bool library::operator==(const library& obj) const + { + return this->module_ == obj.module_; + } + + library::operator bool() const + { + return this->is_valid(); + } + + library::operator HMODULE() const + { + return this->get_handle(); + } + + PIMAGE_NT_HEADERS library::get_nt_headers() const + { + if (!this->is_valid()) return nullptr; + return reinterpret_cast(this->get_ptr() + this->get_dos_header()->e_lfanew); + } + + PIMAGE_DOS_HEADER library::get_dos_header() const + { + return reinterpret_cast(this->get_ptr()); + } + + PIMAGE_OPTIONAL_HEADER library::get_optional_header() const + { + if (!this->is_valid()) return nullptr; + return &this->get_nt_headers()->OptionalHeader; + } + + std::vector library::get_section_headers() const + { + std::vector headers; + + auto nt_headers = this->get_nt_headers(); + auto section = IMAGE_FIRST_SECTION(nt_headers); + + for (uint16_t i = 0; i < nt_headers->FileHeader.NumberOfSections; ++i, ++section) + { + if (section) headers.push_back(section); + else OutputDebugStringA("There was an invalid section :O"); + } + + return headers; + } + + std::uint8_t* library::get_ptr() const + { + return reinterpret_cast(this->module_); + } + + void library::unprotect() const + { + if (!this->is_valid()) return; + + DWORD protection; + VirtualProtect(this->get_ptr(), this->get_optional_header()->SizeOfImage, PAGE_EXECUTE_READWRITE, + &protection); + } + + size_t library::get_relative_entry_point() const + { + if (!this->is_valid()) return 0; + return this->get_nt_headers()->OptionalHeader.AddressOfEntryPoint; + } + + void* library::get_entry_point() const + { + if (!this->is_valid()) return nullptr; + return this->get_ptr() + this->get_relative_entry_point(); + } + + bool library::is_valid() const + { + return this->module_ != nullptr && this->get_dos_header()->e_magic == IMAGE_DOS_SIGNATURE; + } + + std::string library::get_name() const + { + if (!this->is_valid()) return ""; + + auto path = this->get_path(); + const auto pos = path.find_last_of("/\\"); + if (pos == std::string::npos) return path; + + return path.substr(pos + 1); + } + + std::string library::get_path() const + { + if (!this->is_valid()) return ""; + + char name[MAX_PATH] = {0}; + GetModuleFileNameA(this->module_, name, sizeof name); + + return name; + } + + std::string library::get_folder() const + { + if (!this->is_valid()) return ""; + + const auto path = std::filesystem::path(this->get_path()); + return path.parent_path().generic_string(); + } + + void library::free() + { + if (this->is_valid()) + { + FreeLibrary(this->module_); + this->module_ = nullptr; + } + } + + HMODULE library::get_handle() const + { + return this->module_; + } + + void** library::get_iat_entry(const std::string& module_name, const std::string& proc_name) const + { + if (!this->is_valid()) return nullptr; + + const library other_module(module_name); + if (!other_module.is_valid()) return nullptr; + + auto* const target_function = other_module.get_proc(proc_name); + if (!target_function) return nullptr; + + auto* header = this->get_optional_header(); + if (!header) return nullptr; + + auto* import_descriptor = reinterpret_cast(this->get_ptr() + header->DataDirectory + [IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); + + while (import_descriptor->Name) + { + if (!_stricmp(reinterpret_cast(this->get_ptr() + import_descriptor->Name), module_name.data())) + { + auto* original_thunk_data = reinterpret_cast(import_descriptor-> + OriginalFirstThunk + this->get_ptr()); + auto* thunk_data = reinterpret_cast(import_descriptor->FirstThunk + this-> + get_ptr()); + + while (original_thunk_data->u1.AddressOfData) + { + const size_t ordinal_number = original_thunk_data->u1.AddressOfData & 0xFFFFFFF; + + if (ordinal_number > 0xFFFF) continue; + + if (GetProcAddress(other_module.module_, reinterpret_cast(ordinal_number)) == + target_function) + { + return reinterpret_cast(&thunk_data->u1.Function); + } + + ++original_thunk_data; + ++thunk_data; + } + + //break; + } + + ++import_descriptor; + } + + return nullptr; + } + + void raise_hard_exception() + { + int data = false; + const library ntdll("ntdll.dll"); + ntdll.invoke_pascal("RtlAdjustPrivilege", 19, true, false, &data); + ntdll.invoke_pascal("NtRaiseHardError", 0xC000007B, 0, nullptr, nullptr, 6, &data); + } + + std::string load_resource(const int id) + { + const auto self_handle = library::get_current_handle(); + const auto res = FindResource(self_handle, MAKEINTRESOURCE(id), RT_RCDATA); + if (res == nullptr) + { + return {}; + } + + const auto handle = LoadResource(self_handle, res); + if (handle == nullptr) + { + return {}; + } + + const auto str = LPSTR(LockResource(handle)); + const auto size = SizeofResource(self_handle, res); + return std::string{str, size}; + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/nt.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/nt.hpp new file mode 100644 index 0000000..4ba32c7 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/nt.hpp @@ -0,0 +1,112 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +// min and max is required by gdi, therefore NOMINMAX won't work +#ifdef max +#undef max +#endif + +#ifdef min +#undef min +#endif + +#include +#include +#include + +namespace utils::nt +{ + class library final + { + public: + static library load(const std::string& name); + static library load(const std::filesystem::path& path); + static library get_by_address(void* address); + + static void set_current_handle(HMODULE handle); + static HMODULE get_current_handle(); + + library(); + explicit library(const std::string& name); + explicit library(HMODULE handle); + + library(const library& a) : module_(a.module_) + { + } + + bool operator!=(const library& obj) const { return !(*this == obj); }; + bool operator==(const library& obj) const; + + operator bool() const; + operator HMODULE() const; + + void unprotect() const; + void* get_entry_point() const; + size_t get_relative_entry_point() const; + + bool is_valid() const; + std::string get_name() const; + std::string get_path() const; + std::string get_folder() const; + std::uint8_t* get_ptr() const; + void free(); + + HMODULE get_handle() const; + + template + T get_proc(const std::string& process) const + { + if (!this->is_valid()) T{}; + return reinterpret_cast(GetProcAddress(this->module_, process.data())); + } + + template + std::function get(const std::string& process) const + { + if (!this->is_valid()) return std::function(); + return static_cast(this->get_proc(process)); + } + + template + T invoke(const std::string& process, Args ... args) const + { + auto method = this->get(process); + if (method) return method(args...); + return T(); + } + + template + T invoke_pascal(const std::string& process, Args ... args) const + { + auto method = this->get(process); + if (method) return method(args...); + return T(); + } + + template + T invoke_this(const std::string& process, void* this_ptr, Args ... args) const + { + auto method = this->get(this_ptr, process); + if (method) return method(args...); + return T(); + } + + std::vector get_section_headers() const; + + PIMAGE_NT_HEADERS get_nt_headers() const; + PIMAGE_DOS_HEADER get_dos_header() const; + PIMAGE_OPTIONAL_HEADER get_optional_header() const; + + void** get_iat_entry(const std::string& module_name, const std::string& proc_name) const; + + private: + HMODULE module_; + static HMODULE current_handle_; + + }; + + __declspec(noreturn) void raise_hard_exception(); + std::string load_resource(int id); +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/string.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/string.cpp new file mode 100644 index 0000000..876fb64 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/string.cpp @@ -0,0 +1,181 @@ +#include +#include "string.hpp" +#include +#include +#include + +namespace utils::string +{ + const char* va(const char* fmt, ...) + { + static thread_local va_provider<8, 256> provider; + + va_list ap; + va_start(ap, fmt); + + const char* result = provider.get(fmt, ap); + + va_end(ap); + return result; + } + + std::vector split(const std::string& s, const char delim) + { + std::stringstream ss(s); + std::string item; + std::vector elems; + + while (std::getline(ss, item, delim)) + { + elems.push_back(item); // elems.push_back(std::move(item)); // if C++11 (based on comment from @mchiasson) + } + + return elems; + } + + std::string to_lower(std::string text) + { + std::transform(text.begin(), text.end(), text.begin(), [](const unsigned char input) + { + return static_cast(tolower(input)); + }); + + return text; + } + + std::string to_upper(std::string text) + { + std::transform(text.begin(), text.end(), text.begin(), [](const unsigned char input) + { + return static_cast(toupper(input)); + }); + + return text; + } + + bool starts_with(const std::string& text, const std::string& substring) + { + return text.find(substring) == 0; + } + + bool ends_with(const std::string& text, const std::string& substring) + { + if (substring.size() > text.size()) return false; + return std::equal(substring.rbegin(), substring.rend(), text.rbegin()); + } + + bool is_numeric(const std::string& text) + { + return !text.empty() && std::find_if(text.begin(), + text.end(), + [](unsigned char input) + { + return !std::isdigit(input); + }) == text.end(); + } + + std::string dump_hex(const std::string& data, const std::string& separator) + { + std::string result; + + for (unsigned int i = 0; i < data.size(); ++i) + { + if (i > 0) + { + result.append(separator); + } + + result.append(va("%02X", data[i] & 0xFF)); + } + + return result; + } + + std::string bin_to_hex(const std::string& data) + { + return dump_hex(data, ""); + } + + std::string hex_to_bin(const std::string& data) + { + std::string result; + + for (auto i = 0u; i < data.size(); i += 2) + { + const auto byte = data.substr(i, 2); + const auto value = static_cast(std::strtol(byte.data(), nullptr, 0x10)); + result += value; + } + + return result; + } + + std::string convert(const std::wstring& wstr) + { + std::string result; + result.reserve(wstr.size()); + + for (const auto& chr : wstr) + { + result.push_back(static_cast(chr)); + } + + return result; + } + + std::wstring convert(const std::string& str) + { + std::wstring result; + result.reserve(str.size()); + + for (const auto& chr : str) + { + result.push_back(static_cast(chr)); + } + + return result; + } + + std::string replace(std::string str, const std::string& from, const std::string& to) + { + if (from.empty()) + { + return str; + } + + size_t start_pos = 0; + while ((start_pos = str.find(from, start_pos)) != std::string::npos) + { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); + } + + return str; + } + + std::string get_timestamp() + { + tm ltime{}; + char timestamp[MAX_PATH] = { 0 }; + const auto time = _time64(nullptr); + + _localtime64_s(<ime, &time); + strftime(timestamp, sizeof(timestamp) - 1, "%Y-%m-%d-%H-%M-%S", <ime); + + return timestamp; + } + + std::string trim(const std::string& str, const std::string& whitespace) + { + const auto first = str.find_first_not_of(whitespace); + if (first == std::string::npos) + { + return {}; + } + + const auto last = str.find_last_not_of(whitespace); + const auto range = last - first + 1; + + return str.substr(first, range); + } +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/string.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/string.hpp new file mode 100644 index 0000000..4d5b382 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/string.hpp @@ -0,0 +1,105 @@ +#pragma once +#include "memory.hpp" +#include + +#ifndef ARRAYSIZE +template +size_t ARRAYSIZE(Type(&)[n]) { return n; } +#endif + +namespace utils::string +{ + template + class va_provider final + { + public: + static_assert(Buffers != 0 && MinBufferSize != 0, "Buffers and MinBufferSize mustn't be 0"); + + va_provider() : current_buffer_(0) + { + } + + char* get(const char* format, const va_list ap) + { + ++this->current_buffer_ %= ARRAYSIZE(this->string_pool_); + auto entry = &this->string_pool_[this->current_buffer_]; + + if (!entry->size || !entry->buffer) + { + throw std::runtime_error("String pool not initialized"); + } + + while (true) + { + const int res = vsnprintf_s(entry->buffer, entry->size, _TRUNCATE, format, ap); + if (res > 0) break; // Success + if (res == 0) return nullptr; // Error + + entry->double_size(); + } + + return entry->buffer; + } + + private: + class entry final + { + public: + explicit entry(const size_t _size = MinBufferSize) : size(_size), buffer(nullptr) + { + if (this->size < MinBufferSize) this->size = MinBufferSize; + this->allocate(); + } + + ~entry() + { + if (this->buffer) memory::get_allocator()->free(this->buffer); + this->size = 0; + this->buffer = nullptr; + } + + void allocate() + { + if (this->buffer) memory::get_allocator()->free(this->buffer); + this->buffer = memory::get_allocator()->allocate_array(this->size + 1); + } + + void double_size() + { + this->size *= 2; + this->allocate(); + } + + size_t size; + char* buffer; + }; + + size_t current_buffer_; + entry string_pool_[Buffers]; + }; + + const char* va(const char* fmt, ...); + + std::vector split(const std::string& s, char delim); + + std::string to_lower(std::string text); + std::string to_upper(std::string text); + bool starts_with(const std::string& text, const std::string& substring); + bool ends_with(const std::string& text, const std::string& substring); + bool is_numeric(const std::string& text); + + std::string dump_hex(const std::string& data, const std::string& separator = " "); + std::string bin_to_hex(const std::string& data); + std::string hex_to_bin(const std::string& data); + + void strip(const char* in, char* out, int max); + + std::string convert(const std::wstring& wstr); + std::wstring convert(const std::string& str); + + std::string replace(std::string str, const std::string& from, const std::string& to); + + std::string get_timestamp(); + + std::string trim(const std::string& str, const std::string& whitespace = " \t\n\r\f\v"); +} \ No newline at end of file diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/thread.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/thread.cpp new file mode 100644 index 0000000..1c20ff5 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/thread.cpp @@ -0,0 +1,89 @@ +#include +#include "thread.hpp" +#include "string.hpp" + +#include + +#include + +namespace utils::thread +{ + std::vector get_thread_ids() + { + auto* const h = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, GetCurrentProcessId()); + if (h == INVALID_HANDLE_VALUE) + { + return {}; + } + + const auto _ = gsl::finally([h]() + { + CloseHandle(h); + }); + + THREADENTRY32 entry{}; + entry.dwSize = sizeof(entry); + if (!Thread32First(h, &entry)) + { + return {}; + } + + std::vector ids{}; + + do + { + const auto check_size = entry.dwSize < FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + + sizeof(entry.th32OwnerProcessID); + entry.dwSize = sizeof(entry); + + if (check_size && entry.th32OwnerProcessID == GetCurrentProcessId()) + { + ids.emplace_back(entry.th32ThreadID); + } + } + while (Thread32Next(h, &entry)); + + return ids; + } + + void for_each_thread(const std::function& callback) + { + const auto ids = get_thread_ids(); + + for (const auto& id : ids) + { + auto* const thread = OpenThread(THREAD_ALL_ACCESS, FALSE, id); + if (thread != nullptr) + { + const auto _ = gsl::finally([thread]() + { + CloseHandle(thread); + }); + + callback(thread); + } + } + } + + void suspend_other_threads() + { + for_each_thread([](const HANDLE thread) + { + if (GetThreadId(thread) != GetCurrentThreadId()) + { + SuspendThread(thread); + } + }); + } + + void resume_other_threads() + { + for_each_thread([](const HANDLE thread) + { + if (GetThreadId(thread) != GetCurrentThreadId()) + { + ResumeThread(thread); + } + }); + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/thread.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/thread.hpp new file mode 100644 index 0000000..4d67c1a --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/thread.hpp @@ -0,0 +1,19 @@ +#pragma once +#include + +namespace utils::thread +{ + template + std::thread create_named_thread(const std::string& name, Args&&... args) + { + auto t = std::thread(std::forward(args)...); + set_name(t, name); + return t; + } + + std::vector get_thread_ids(); + void for_each_thread(const std::function& callback); + + void suspend_other_threads(); + void resume_other_threads(); +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/thread_pool.cpp b/t6/plugins/tws-t6-gsc-utils/src/utils/thread_pool.cpp new file mode 100644 index 0000000..092c89b --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/thread_pool.cpp @@ -0,0 +1,96 @@ +#include + +#include "thread_pool.hpp" + +namespace utils +{ + thread_pool::worker::worker() + { + } + + void thread_pool::worker::start(thread_pool& pool) + { + this->thread_ = std::thread([&] + { + this->loop(pool); + }); + } + + void thread_pool::worker::stop() + { + if (this->thread_.joinable()) + { + this->thread_.join(); + } + } + + void thread_pool::worker::loop(thread_pool& pool) + { + while (!pool.stopped_) + { + pool.run_job(); + } + } + + thread_pool::job_ptr thread_pool::pop_job() + { + thread_pool::job_ptr job = std::move(this->queue_.front()); + this->queue_.pop_front(); + return job; + } + + void thread_pool::run_job() + { + std::unique_lock lock(this->mutex_); + + this->event_.wait(lock, [&]() + { + return !this->queue_.empty() || this->stopped_; + }); + + if (this->stopped_ || this->queue_.empty()) + { + return; + } + + auto job = this->pop_job(); + lock.unlock(); + job->operator()(); + } + + thread_pool::thread_pool(const std::size_t num_workers) + { + this->initialize(num_workers); + } + + void thread_pool::initialize(const std::size_t num_workers) + { + for (auto i = 0u; i < num_workers; i++) + { + this->workers_.emplace_back(std::make_unique()); + } + } + + void thread_pool::start() + { + for (auto& worker : this->workers_) + { + worker->start(*this); + } + } + + void thread_pool::update() + { + + } + + void thread_pool::stop() + { + this->stopped_ = true; + this->event_.notify_all(); + for (auto& worker : this->workers_) + { + worker->stop(); + } + } +} diff --git a/t6/plugins/tws-t6-gsc-utils/src/utils/thread_pool.hpp b/t6/plugins/tws-t6-gsc-utils/src/utils/thread_pool.hpp new file mode 100644 index 0000000..9b9bcb2 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/src/utils/thread_pool.hpp @@ -0,0 +1,60 @@ +#pragma once + +namespace utils +{ + class thread_pool + { + public: + using job = std::function; + using job_ptr = std::unique_ptr; + + friend class worker; + class worker + { + public: + worker(); + + friend class thread_pool; + + void start(thread_pool& pool); + void stop(); + void loop(thread_pool& pool); + + private: + std::thread thread_; + + }; + + thread_pool() = default; + thread_pool(const std::size_t num_workers); + void initialize(const std::size_t num_workers); + + void start(); + void update(); + void stop(); + + template + void push(F&& job) + { + if (this->stopped_) + { + return; + } + + std::lock_guard lock(this->mutex_); + this->queue_.emplace_back(std::make_unique(std::forward(job))); + this->event_.notify_one(); + } + + private: + thread_pool::job_ptr pop_job(); + void run_job(); + + std::mutex mutex_; + std::atomic_bool stopped_; + std::deque queue_; + std::condition_variable event_; + std::deque> workers_; + + }; +} diff --git a/t6/plugins/tws-t6-gsc-utils/tools/premake5.exe b/t6/plugins/tws-t6-gsc-utils/tools/premake5.exe new file mode 100644 index 0000000..1a637aa Binary files /dev/null and b/t6/plugins/tws-t6-gsc-utils/tools/premake5.exe differ diff --git a/t6/plugins/tws-t6-gsc-utils/upstream.json b/t6/plugins/tws-t6-gsc-utils/upstream.json new file mode 100644 index 0000000..d8010e1 --- /dev/null +++ b/t6/plugins/tws-t6-gsc-utils/upstream.json @@ -0,0 +1,61 @@ +{ + "repository": "https://github.com/alicealys/t6-gsc-utils", + "commit": "57cd74db04ca646a74fe684307d90a80498df40f", + "dependencies": [ + { + "path": "deps/GSL", + "url": "https://github.com/microsoft/GSL.git", + "commit": "e64c97fc2cfc11992098bb38eda932de275e3f4d" + }, + { + "path": "deps/asmjit", + "url": "https://github.com/asmjit/asmjit.git", + "commit": "7e64eabca49c1a4d2cbfb06e4b9d3987bce7f7b3" + }, + { + "path": "deps/curl", + "url": "https://github.com/curl/curl.git", + "commit": "d2d48f21f3b0d4e4f7a77f73f95b30900d809ec8" + }, + { + "path": "deps/date", + "url": "https://github.com/HowardHinnant/date", + "commit": "cc4685a21e4a4fdae707ad1233c61bbaff241f93" + }, + { + "path": "deps/json", + "url": "https://github.com/nlohmann/json.git", + "commit": "58d6aa58316a84fc770bdd0409934cdfdf0697dc" + }, + { + "path": "deps/libtomcrypt", + "url": "https://github.com/libtom/libtomcrypt.git", + "commit": "f7e6519fae1e11ff5ff9d36c84101a673002133b" + }, + { + "path": "deps/libtommath", + "url": "https://github.com/libtom/libtommath.git", + "commit": "138309cfdc78978d5b86fc21f4d424a86c51e1eb" + }, + { + "path": "deps/minhook", + "url": "https://github.com/TsudaKageyu/minhook.git", + "commit": "0f25a2449b3cf878bcbdbf91b693c38149ecf029" + }, + { + "path": "deps/plutonium-sdk", + "url": "https://github.com/plutoniummod/plutonium-sdk.git", + "commit": "17e9a0a4d5e1133b50f879e3db07e97d1bf92e10" + }, + { + "path": "deps/sqlpp11", + "url": "https://github.com/rbock/sqlpp11.git", + "commit": "6cc2067ef5fd91612f31509384e78d36c74c7657" + }, + { + "path": "deps/zlib", + "url": "https://github.com/madler/zlib.git", + "commit": "21767c654d31d2dccdde4330529775c6c5fd5389" + } + ] +} diff --git a/t6/NOTES.md b/t6/scripts/NOTES.md similarity index 97% rename from t6/NOTES.md rename to t6/scripts/NOTES.md index 1e18bdf..323334c 100644 --- a/t6/NOTES.md +++ b/t6/scripts/NOTES.md @@ -39,7 +39,7 @@ death/laststand/intermission until active play. Other mods controlling the same invulnerability or weapon flags can still conflict and need live testing. The existing optional T5 dvars remain. Leaderboard adds the three settings -documented in the [shared README](../README.md#map-record-announcements). +documented in the [shared README](../../README.md#map-record-announcements). `common_config()` and `ensure_config()` are original T5 in-memory HUD helpers, not an external configuration system. @@ -98,7 +98,8 @@ The T5 and T6 implementations are not runtime-verified equivalents. In particula [T6 plugin v3.0.4](https://github.com/alicealys/t6-gsc-utils/blob/v3.0.4/src/component/http.cpp) expects `httpPost(url, body, headers)` and notifies only the response body; v3.1.1 expects `httpPost(url, options)` and also provides success/error values. -The current script targets v3.1.1. Switching the function name does not establish +The current script targets the [TWS build](../plugins/README.md) based on v3.1.1, +which appends diagnostics and enforces native HTTP deadlines. Switching the function name does not establish compatibility with an unknown installed DLL. Console markers identify `main`, `init`, round serialization, HTTP invocation, diff --git a/t6/README.md b/t6/scripts/README.md similarity index 90% rename from t6/README.md rename to t6/scripts/README.md index b9cfa87..07e192d 100644 --- a/t6/README.md +++ b/t6/scripts/README.md @@ -10,19 +10,19 @@ separate token config or database setup is required. | `tws_t6_hud.gsc` | Match/round timers, name, zombie count and health bar | Common | | `tws_t6_afk.gsc` | `.afk` / `.back`, protection, movement and weapon blocking | Common | | `tws_t6_lock.gsc` | Lock at round 20; clear password on game over | Common, HUD | -| `tws_t6_telemetry.gsc` | Async queued game reports (game_start/heartbeat/round_change/game_end) | t6-gsc-utils | +| `tws_t6_telemetry.gsc` | Async queued game reports (game_start/heartbeat/round_change/game_end) | tws-t6-gsc-utils | | `tws_t6_join.gsc` | `.join` chat command lets a spectator newcomer enter mid-match once | Common | -| `tws_t6_leaderboard.gsc` | Private map high-round record and every tied holder on connection | Common, t6-gsc-utils | +| `tws_t6_leaderboard.gsc` | Private map high-round record and every tied holder on connection | Common, tws-t6-gsc-utils | ## Install 1. Copy all seven `tws_t6_*.gsc` files into `Plutonium/storage/t6/scripts/zm/`. Update common along with leaderboard. -2. For telemetry or leaderboard, install [t6-gsc-utils v3.1.1](https://github.com/alicealys/t6-gsc-utils/releases/tag/v3.1.1) +2. For telemetry or leaderboard, install the [tws-t6-gsc-utils build](../plugins/README.md), based on upstream v3.1.1, in `Plutonium/plugins/` and restart the server process. The other five scripts do not require that plugin. 3. Ensure the API service's `EC_TELEMETRY_TOKEN` matches the token embedded in - the reporter; see [authentication setup](../README.md#telemetry-authentication-t5-and-t6). + the reporter; see [authentication setup](../../README.md#telemetry-authentication-t5-and-t6). 4. Restart the map to load script updates. If upgrading from the earlier leaderboard filename, **remove @@ -55,7 +55,7 @@ catalogue. An unknown mode/location is logged instead of guessing from the engin map. Use `tws_leaderboard_map_key` for a custom registered selection if needed. The plugin must expose `httpPost` (with the GET method override), `jsonParse`, -`typeof` and `formatString`. [Shared configuration, caching and validation](../README.md#map-record-announcements) +`typeof` and `formatString`. [Shared configuration, caching and validation](../../README.md#map-record-announcements) apply to both versions. All T6 script filenames use the `tws_t6_` prefix. ## Chat commands @@ -105,7 +105,7 @@ override when upgrading. The reporter token lives in `level.telemetry_token`. ## Round telemetry -Telemetry runs directly in GSC using t6-gsc-utils. Configure the exact Electric +Telemetry runs directly in GSC using tws-t6-gsc-utils. Configure the exact Electric Cherry registry name and full endpoint in your existing server configuration: ```cfg @@ -115,7 +115,7 @@ set tws_telemetry_url "https://ec.thewrightserver.net/api/telemetry" The script embeds the reporter token in `level.telemetry_token`; it must match the API service's `EC_TELEMETRY_TOKEN` environment variable. Follow the shared -[authentication setup](../README.md#telemetry-authentication-t5-and-t6). Every +[authentication setup](../../README.md#telemetry-authentication-t5-and-t6). Every request includes `Authorization: Bearer ` and `Content-Type: application/json`. Token rotation requires updating and redeploying both reporter scripts. Token and endpoint changes require a map restart. @@ -140,7 +140,12 @@ are reported as `0`. `score` is the player's current spendable `player.score`. Delivery is asynchronous with a sequenced FIFO queue and one in-flight request. The queue survives the game-end signal so the final snapshot drains. There is no automatic retry: the receiver may already have accepted the event. Transport -success reports the HTTP call finished, not that the API accepted it. Increase +success reports the HTTP call finished, not that the API accepted it. The TWS +plugin enforces 5-second connection and 15-second transfer deadlines, then +releases a timed-out request so the sender can advance. Telemetry logs HTTP +status and native timing diagnostics; non-2xx responses increment `http_rejected` +separately from transport failures. Older plugins trigger a warning that native +deadlines are unverified. Increase `tws_telemetry_log_level` to `2` for per-event capture/queue/response logging; set to `0` to silence non-error output. diff --git a/t6/tools/check.ps1 b/t6/scripts/tools/check.ps1 similarity index 100% rename from t6/tools/check.ps1 rename to t6/scripts/tools/check.ps1 diff --git a/t6/tws_t6_afk.gsc b/t6/scripts/tws_t6_afk.gsc similarity index 100% rename from t6/tws_t6_afk.gsc rename to t6/scripts/tws_t6_afk.gsc diff --git a/t6/tws_t6_common.gsc b/t6/scripts/tws_t6_common.gsc similarity index 100% rename from t6/tws_t6_common.gsc rename to t6/scripts/tws_t6_common.gsc diff --git a/t6/tws_t6_hud.gsc b/t6/scripts/tws_t6_hud.gsc similarity index 100% rename from t6/tws_t6_hud.gsc rename to t6/scripts/tws_t6_hud.gsc diff --git a/t6/tws_t6_join.gsc b/t6/scripts/tws_t6_join.gsc similarity index 100% rename from t6/tws_t6_join.gsc rename to t6/scripts/tws_t6_join.gsc diff --git a/t6/tws_t6_leaderboard.gsc b/t6/scripts/tws_t6_leaderboard.gsc similarity index 100% rename from t6/tws_t6_leaderboard.gsc rename to t6/scripts/tws_t6_leaderboard.gsc diff --git a/t6/tws_t6_lock.gsc b/t6/scripts/tws_t6_lock.gsc similarity index 100% rename from t6/tws_t6_lock.gsc rename to t6/scripts/tws_t6_lock.gsc diff --git a/t6/tws_t6_telemetry.gsc b/t6/scripts/tws_t6_telemetry.gsc similarity index 86% rename from t6/tws_t6_telemetry.gsc rename to t6/scripts/tws_t6_telemetry.gsc index 8c0dc69..30c1fb4 100644 --- a/t6/tws_t6_telemetry.gsc +++ b/t6/scripts/tws_t6_telemetry.gsc @@ -1,4 +1,4 @@ -/* tws_t6_telemetry.gsc - Zombies game reports via t6-gsc-utils v3.1.1. */ +/* tws_t6_telemetry.gsc - Zombies game reports via tws-t6-gsc-utils. */ #include maps\mp\_utility; #include common_scripts\utility; @@ -6,7 +6,7 @@ // Plutonium probes both entry points; runtime setup belongs in init. main() { - println("[tws_t6_telemetry] main reached (HTTP API target: t6-gsc-utils v3.1.1)"); + println("[tws_t6_telemetry] main reached (HTTP API target: tws-t6-gsc-utils with deadlines/diagnostics)"); } init() @@ -60,6 +60,8 @@ init() level.telemetry_sequence = 0; level.telemetry_completed = 0; level.telemetry_failed = 0; + level.telemetry_http_rejected = 0; + level.telemetry_missing_diagnostics_warned = false; level.telemetry_failure_streak = 0; level.telemetry_last_completed = -1; level.telemetry_last_capture = getTime(); @@ -310,6 +312,8 @@ telemetry_send(entry) options["method"] = "POST"; options["headers"] = headers; options["body"] = entry["body"]; + options["connect_timeout_ms"] = 5000; + options["timeout_ms"] = 15000; request = httpPost(level.telemetry_url, options); if (!isDefined(request)) @@ -321,9 +325,15 @@ telemetry_send(entry) } level.telemetry_sender_state = "waiting_http"; - request waittill("done", response, success, error); + request waittill("done", response, success, error, diagnostics); elapsed = getTime() - level.telemetry_request_started; level.telemetry_last_completed = getTime(); + timing = telemetry_http_diagnostics(diagnostics); + if (!isDefined(diagnostics) && !level.telemetry_missing_diagnostics_warned) + { + level.telemetry_missing_diagnostics_warned = true; + telemetry_log(0, "WARNING: plugin returned no HTTP diagnostics; native timeouts are unverified. Install the TWS plugin build and restart the server process"); + } // The plugin's success flag reports transport completion, not HTTP status. // No automatic retry: the receiver may already have accepted the event. @@ -331,7 +341,7 @@ telemetry_send(entry) { if (!isDefined(error)) error = "unknown transport error"; - telemetry_transport_failed(label + " elapsed_ms=" + elapsed + " error=" + telemetry_preview(error)); + telemetry_transport_failed(label + " elapsed_ms=" + elapsed + timing + " error=" + telemetry_preview(error)); } else { @@ -339,7 +349,18 @@ telemetry_send(entry) if (level.telemetry_failure_streak > 0) telemetry_log(0, "Transport recovered after " + level.telemetry_failure_streak + " failures; " + label); level.telemetry_failure_streak = 0; - telemetry_log(1, "Transport completed " + label + " elapsed_ms=" + elapsed + " (API acceptance unverified)"); + if (isDefined(diagnostics) && isDefined(diagnostics["http_status"])) + { + if (diagnostics["http_status"] < 200 || diagnostics["http_status"] >= 300) + { + level.telemetry_http_rejected++; + telemetry_log(0, "ERROR: HTTP rejected " + label + " elapsed_ms=" + elapsed + timing + " body=" + telemetry_preview(response)); + } + else + telemetry_log(1, "HTTP completed " + label + " elapsed_ms=" + elapsed + timing); + } + else + telemetry_log(1, "Transport completed " + label + " elapsed_ms=" + elapsed + timing + " (API acceptance unverified)"); if (!isDefined(response) || response == "") telemetry_log(0, "WARNING: empty API response for " + label); else @@ -350,6 +371,24 @@ telemetry_send(entry) level.telemetry_sender_state = "idle"; } +// The native plugin appends diagnostics while preserving the old done arguments. +// Curl timings are cumulative milestones, measured in real milliseconds. +telemetry_http_diagnostics(diagnostics) +{ + if (!isDefined(diagnostics)) + return " diagnostics=unavailable"; + + keys = strTok("http_status,curl_code,dns_ms,connect_ms,tls_ms,first_byte_ms,total_ms,worker_queue_ms,notify_ms,connect_timeout_ms,timeout_ms", ","); + detail = ""; + for (i = 0; i < keys.size; i++) + { + key = keys[i]; + if (isDefined(diagnostics[key])) + detail += " " + key + "=" + int(diagnostics[key]); + } + return detail; +} + telemetry_transport_failed(detail) { level.telemetry_failed++; @@ -399,7 +438,7 @@ telemetry_preview(value) } // Independent of producers and the HTTP wait; survives game over until drain. -// The plugin cannot cancel requests. Warn without opening overlapping POSTs +// Native requests have deadlines. Warn without opening overlapping POSTs // that could arrive out of order or exhaust its native worker pool. telemetry_watchdog() { @@ -416,7 +455,7 @@ telemetry_watchdog() telemetry_log(1, "Health started=" + level.telemetry_started + " ended=" + level.telemetry_ended + " sender=" + level.telemetry_sender_state + " capturing=" + level.telemetry_capture_event + " pending=" + pending + " queued_total=" + level.telemetry_sequence + - " completed=" + level.telemetry_completed + " failed=" + level.telemetry_failed + + " completed=" + level.telemetry_completed + " failed=" + level.telemetry_failed + " http_rejected=" + level.telemetry_http_rejected + " last_done_ms_ago=" + completion_age); if (isDefined(level.telemetry_inflight)) @@ -425,7 +464,7 @@ telemetry_watchdog() if (age >= 30000) telemetry_log(0, "WARNING: HTTP stalled " + telemetry_event_label(level.telemetry_inflight) + " elapsed_ms=" + age + " pending=" + pending + " stage=" + level.telemetry_sender_state + - "; waiting for plugin completion (no cancellation API)"); + "; native deadline exceeded; check plugin version, worker queue and completion delivery"); } else if (pending > 0) { diff --git a/tools/build_plugins.ps1 b/tools/build_plugins.ps1 new file mode 100644 index 0000000..e9361f4 --- /dev/null +++ b/tools/build_plugins.ps1 @@ -0,0 +1,26 @@ +param( + [ValidateSet('t5', 't6', 'all')] + [string]$Game = 'all', + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Release' +) +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent $PSScriptRoot +$games = if ($Game -eq 'all') { @('t5', 't6') } else { @($Game) } +foreach ($current in $games) { + & python (Join-Path $PSScriptRoot 'fetch_plugin_deps.py') $current + if ($LASTEXITCODE -ne 0) { throw "Dependency restore failed for $current" } + $plugin = Join-Path $repo "$current/plugins/tws-$current-gsc-utils" + Push-Location $plugin + try { + & .\tools\premake5.exe vs2022 + if ($LASTEXITCODE -ne 0) { throw "Project generation failed for $current" } + & msbuild /m /v:minimal "/p:Configuration=$Configuration" /p:PlatformTarget=x86 "build/tws-$current-gsc-utils.sln" + if ($LASTEXITCODE -ne 0) { throw "Build failed for $current" } + $binary = Join-Path $plugin "build/bin/$Configuration/tws-$current-gsc-utils.dll" + if (!(Test-Path -LiteralPath $binary)) { throw "Missing output: $binary" } + Write-Output "Built $binary" + } finally { + Pop-Location + } +} diff --git a/tools/check_leaderboard.mjs b/tools/check_leaderboard.mjs index 002479c..06ca141 100644 --- a/tools/check_leaderboard.mjs +++ b/tools/check_leaderboard.mjs @@ -42,7 +42,7 @@ function functions(source) { } function engine(game) { - const source = readFileSync(new URL(`${game}/tws_${game}_leaderboard.gsc`, root), 'utf8'); + const source = readFileSync(new URL(`${game}/scripts/tws_${game}_leaderboard.gsc`, root), 'utf8'); const all = functions(source); const excluded = new Set(['lb_worker', 'lb_welcome', 'lb_get', 'lb_collect', 'lb_log', 'lb_show_hud', 'lb_hud_help_visible', 'lb_hud_end_game', 'lb_hud_destroy', 'lb_chat_listener', 'lb_request', 'lb_hud_disconnect']); const table = []; @@ -90,7 +90,7 @@ for (const game of ['t5', 't6']) { const { context: c, source, all, dvars, responses, queries } = engine(game); const option = mapOption(game); check(`${game} exactly one connect listener across modules`, () => { - const common = readFileSync(new URL(`${game}/tws_${game}_common.gsc`, root), 'utf8'); + const common = readFileSync(new URL(`${game}/scripts/tws_${game}_common.gsc`, root), 'utf8'); assert.equal((common.match(/waittill\("connected"/g) ?? []).length, 1); assert(!source.includes('waittill("connected"')); assert(!all.get('lb_welcome').includes('spawned_player')); @@ -227,8 +227,8 @@ for (const game of ['t5', 't6']) { assert.equal(responses.length, 0, `${game} unused responses`); } check('paired data helpers stay identical', () => { - const t5 = functions(readFileSync(new URL('t5/tws_t5_leaderboard.gsc', root), 'utf8')); - const t6 = functions(readFileSync(new URL('t6/tws_t6_leaderboard.gsc', root), 'utf8')); + const t5 = functions(readFileSync(new URL('t5/scripts/tws_t5_leaderboard.gsc', root), 'utf8')); + const t6 = functions(readFileSync(new URL('t6/scripts/tws_t6_leaderboard.gsc', root), 'utf8')); for (const [name, fn] of t5) { if (!['lb_selection', 'lb_welcome', 'lb_get', 'lb_log'].includes(name)) { const normalized = t6.get(name).replaceAll('scripts\\zm\\tws_t6_common::', 'scripts\\sp\\tws_t5_common::'); diff --git a/tools/fetch_plugin_deps.py b/tools/fetch_plugin_deps.py new file mode 100644 index 0000000..4655140 --- /dev/null +++ b/tools/fetch_plugin_deps.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Restore the exact upstream dependency revisions for the vendored plugins.""" +import argparse +import json +from pathlib import Path +import subprocess + +ROOT = Path(__file__).resolve().parents[1] + + +def git(directory, *args, capture=False): + return subprocess.check_output(["git", "-C", str(directory), *args], text=True).strip() if capture else subprocess.check_call(["git", "-C", str(directory), *args]) + + +def restore(game, check_only): + plugin = ROOT / game / "plugins" / f"tws-{game}-gsc-utils" + lock = json.loads((plugin / "upstream.json").read_text()) + for dependency in lock["dependencies"]: + target = plugin / dependency["path"] + if not target.resolve().is_relative_to(plugin.resolve()): + raise RuntimeError(f"Dependency path leaves the plugin: {target}") + if not (target / ".git").exists(): + if check_only: + raise RuntimeError(f"Missing dependency: {target}") + if target.exists() and any(target.iterdir()): + raise RuntimeError(f"Refusing to replace nonempty directory: {target}") + target.mkdir(parents=True, exist_ok=True) + git(target, "init", "--quiet") + git(target, "remote", "add", "origin", dependency["url"]) + if git(target, "remote", "get-url", "origin", capture=True) != dependency["url"]: + raise RuntimeError(f"Unexpected dependency remote: {target}") + if git(target, "status", "--porcelain", capture=True): + raise RuntimeError(f"Dependency has local changes: {target}") + current = subprocess.run(["git", "-C", str(target), "rev-parse", "--verify", "HEAD"], capture_output=True, text=True) + if current.stdout.strip() != dependency["commit"]: + if check_only: + raise RuntimeError(f"Wrong dependency revision: {target}") + git(target, "fetch", "--depth", "1", "origin", dependency["commit"]) + git(target, "checkout", "--quiet", "--detach", dependency["commit"]) + print(f"OK {game}/{dependency['path']} {dependency['commit']}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("game", choices=["t5", "t6", "all"]) + parser.add_argument("--check", action="store_true", help="Verify existing checkouts without fetching or changing them") + args = parser.parse_args() + for game in ("t5", "t6") if args.game == "all" else (args.game,): + restore(game, args.check) diff --git a/tools/test_plugin_component.py b/tools/test_plugin_component.py new file mode 100644 index 0000000..51019e7 --- /dev/null +++ b/tools/test_plugin_component.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Compile actual HTTP component logic using host shims for engine interfaces.""" +import os +from pathlib import Path +import re +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[1] +with tempfile.TemporaryDirectory(prefix="tws-http-component-") as temp: + for game in ("t5", "t6"): + plugin = ROOT / game / "plugins" / f"tws-{game}-gsc-utils" / "src" + # Only includes are substituted; compile the entire component unchanged. + component = re.sub(r'^#include[^\n]*\n', '', (plugin / "component/http.cpp").read_text(), flags=re.MULTILINE) + source = Path(temp) / f"{game}.cpp" + source.write_text('#include "http_component_stub.hpp"\n' + component + (ROOT / "tools/tests/http_component_cases.cpp").read_text()) + executable = Path(temp) / game + subprocess.run([os.environ.get("CXX", "c++"), "-std=c++20", "-Wall", "-Wextra", "-Werror", "-fsanitize=address,undefined", + "-I", str(plugin / "utils"), "-I", str(ROOT / "tools/tests"), str(source), "-lcurl", "-o", str(executable)], check=True) + print(game, flush=True) + subprocess.run([str(executable)], check=True) diff --git a/tools/test_plugin_http.py b/tools/test_plugin_http.py new file mode 100644 index 0000000..abbff91 --- /dev/null +++ b/tools/test_plugin_http.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Compile both real HTTP utilities and exercise native deadlines on loopback.""" +import http.server +import json +import os +from pathlib import Path +import socketserver +import subprocess +import tempfile +import threading +import time + +ROOT = Path(__file__).resolve().parents[1] + + +class Handler(http.server.BaseHTTPRequestHandler): + def log_message(self, *_): + pass + + def do_POST(self): + body = self.rfile.read(int(self.headers.get("Content-Length", 0))) + assert body == b'{"event":"heartbeat"}' + assert self.headers.get("Content-Type") == "application/json" + assert self.headers.get("Authorization") == "Bearer test-only" + if self.path == "/stall": + time.sleep(2) + return + if self.path == "/partial": + self.send_response(200) + self.send_header("Content-Length", "100") + self.end_headers() + self.wfile.write(b"{") + self.wfile.flush() + time.sleep(2) + return + if self.path == "/slow": + time.sleep(0.1) + status = int(self.path[1:]) if self.path[1:].isdigit() else 200 + payload = b'{"ok":true}' if status == 200 else b'{"ok":false}' + self.send_response(status) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + +class StallTLS(socketserver.BaseRequestHandler): + def handle(self): + # Accept TCP but never finish TLS: the connection deadline must fire. + time.sleep(6) + + +class Server(socketserver.ThreadingTCPServer): + daemon_threads = True + + +def main(): + with Server(("127.0.0.1", 0), Handler) as http, Server(("127.0.0.1", 0), StallTLS) as tls, tempfile.TemporaryDirectory(prefix="tws-http-tests-") as temp: + for server in (http, tls): + threading.Thread(target=server.serve_forever, daemon=True).start() + try: + for game in ("t5", "t6"): + source = ROOT / game / "plugins" / f"tws-{game}-gsc-utils" / "src" / "utils" + executable = Path(temp) / game + subprocess.run([os.environ.get("CXX", "c++"), "-std=c++20", "-Wall", "-Wextra", "-Werror", "-I", str(source), + str(ROOT / "tools/tests/http_probe.cpp"), str(source / "http.cpp"), "-lcurl", "-o", str(executable)], check=True) + base = f"http://127.0.0.1:{http.server_address[1]}" + env = {**os.environ, "NO_PROXY": "*", "no_proxy": "*"} + + def probe(path, connect="1000", total="1000"): + url = path if "://" in path else base + path + completed = subprocess.run([str(executable), url, connect, total], env=env, capture_output=True, text=True, check=True, timeout=8) + return json.loads(completed.stdout) + + normal = probe("/200", "-", "-") + assert normal["curl_code"] == 0 and normal["http_status"] == 200 and normal["body_size"] == 11, normal + assert 0 <= normal["dns_ms"] <= normal["connect_ms"] <= normal["first_byte_ms"] <= normal["total_ms"], normal + for status in (401, 429, 500): + rejected = probe(f"/{status}") + assert rejected["curl_code"] == 0 and rejected["http_status"] == status and rejected["body_size"] == 12, rejected + for path, status in (("/stall", 0), ("/partial", 200)): + timed_out = probe(path, total="200") + assert timed_out["curl_code"] == 28 and timed_out["http_status"] == status, timed_out + assert 150 <= timed_out["total_ms"] < 1500 and timed_out["wall_ms"] < 1500, timed_out + assert timed_out["body_size"] == 0, timed_out + if status: assert timed_out["first_byte_ms"] > 0, timed_out + assert probe("/200")["curl_code"] == 0 # The next request completes after the stall. + slow = probe("/slow") + assert slow["curl_code"] == 0 and slow["first_byte_ms"] >= 90, slow + tls_url = f"https://127.0.0.1:{tls.server_address[1]}/" + stalled_connect = probe(tls_url, connect="150", total="1000") + assert stalled_connect["curl_code"] == 28 and 100 <= stalled_connect["total_ms"] < 800, stalled_connect + # Total timeout must also bound connection setup when it is shorter. + total_connect = probe(tls_url, connect="1000", total="150") + assert total_connect["curl_code"] == 28 and 100 <= total_connect["total_ms"] < 800, total_connect + for connect, total in (("0", "100"), ("100", "0"), ("-1", "100")): + invalid = probe("/200", connect, total) + assert invalid["curl_code"] == 43 and invalid["http_status"] == 0, invalid + print(f"PASS {game}: POST body/headers, HTTP errors, stalled/partial responses, recovery, TLS/connect and total deadlines, invalid limits", flush=True) + finally: + http.shutdown() + tls.shutdown() + + +if __name__ == "__main__": + main() diff --git a/tools/tests/http_component_cases.cpp b/tools/tests/http_component_cases.cpp new file mode 100644 index 0000000..7cf6ce0 --- /dev/null +++ b/tools/tests/http_component_cases.cpp @@ -0,0 +1,94 @@ +int main() +{ + using namespace scripting; + http::http_request_params_t params{}; + array options; + http::parse_request_options(params, options); + assert(params.limits.connect_timeout_ms == 5000 && params.limits.timeout_ms == 15000); + options["connect_timeout_ms"] = 120; + options["timeout_ms"] = 450; + options["method"] = "POST"; + options["body"] = "snapshot"; + array headers; + headers["Authorization"] = "Bearer test-only"; + options["headers"] = headers; + http::parse_request_options(params, options); + assert(params.limits.connect_timeout_ms == 120 && params.limits.timeout_ms == 450); + assert(params.method == "POST" && params.fields == "snapshot" && params.headers.at("Authorization") == "Bearer test-only"); + for (const auto* key : {"connect_timeout_ms", "timeout_ms"}) + { + for (script_value value : {script_value(0), script_value(-1), script_value(1.5), script_value("150")}) + { + array bad; + bad[key] = value; + bool rejected = false; + try { http::parse_request_options(params, bad); } + catch (const std::runtime_error&) { rejected = true; } + assert(rejected); + } + } + http::parse_request_options(params, options); + auto send = [&]() + { + notifications.clear(); + http::create_request(params); + http::check_requests(); + assert(notifications.empty()); // Incomplete work never notifies early. + scheduler::thread_pool.run(); + http::check_requests(); + assert(notifications.size() == 1 && notifications.front().size() == 4); + assert(utils::http::observed_limits.connect_timeout_ms == 120 && utils::http::observed_limits.timeout_ms == 450); + auto diagnostics = notifications.front()[3].as(); + assert(diagnostics["worker_queue_ms"].as() >= 0 && diagnostics["notify_ms"].as() >= 0); + assert(diagnostics["connect_timeout_ms"].as() == 120 && diagnostics["timeout_ms"].as() == 450); + assert(diagnostics.data->size() == 11); + http::check_requests(); + assert(notifications.size() == 1); // Exactly one completion. + return diagnostics; + }; + utils::http::result successful; + successful.code = CURLE_OK; + successful.status = 200; + successful.buffer = "accepted"; + successful.dns_ms = 1; + successful.connect_ms = 2; + successful.tls_ms = 3; + successful.first_byte_ms = 4; + successful.total_ms = 5; + utils::http::next_result = successful; + auto diagnostics = send(); + assert(notifications.front()[0].as() == "accepted" && notifications.front()[1].as()); + assert(notifications.front()[2].get_raw().type == game::SCRIPT_NONE); + assert(diagnostics["http_status"].as() == 200 && diagnostics["curl_code"].as() == 0); + assert(diagnostics["dns_ms"].as() == 1 && diagnostics["connect_ms"].as() == 2); + assert(diagnostics["tls_ms"].as() == 3 && diagnostics["first_byte_ms"].as() == 4 && diagnostics["total_ms"].as() == 5); + utils::http::next_result->status = 500; + assert(send()["http_status"].as() == 500 && notifications.front()[1].as()); + utils::http::next_result->code = CURLE_OPERATION_TIMEDOUT; + assert(send()["curl_code"].as() == 28 && !notifications.front()[1].as()); + assert(notifications.front()[0].get_raw().type == game::SCRIPT_NONE && notifications.front()[2].is()); + utils::http::next_result = std::nullopt; + send(); + assert(!notifications.front()[1].as()); + utils::http::throw_from_transfer = true; + send(); + assert(!notifications.front()[1].as()); + utils::http::throw_from_transfer = false; + utils::http::next_result = successful; + utils::http::next_result->buffer = std::string(0x5001, 'x'); + send(); + assert(notifications.front()[0].as().size() == 0x5000); + // A resumed waiter can enqueue enough work to reallocate the pending vector. + utils::http::next_result = successful; + notifications.clear(); + http::create_request(params); + scheduler::thread_pool.run(); + after_notify = [&]() { for (int i = 0; i < 100; ++i) http::create_request(params); }; + http::check_requests(); + after_notify = {}; + assert(http::requests.size() == 100 && notifications.size() == 1); + scheduler::thread_pool.run(); + http::check_requests(); + assert(http::requests.empty() && notifications.size() == 101); + puts("\nPASS component: option validation, metadata, compatible argument order, single completion on failures/exceptions, reentrant queue"); +} diff --git a/tools/tests/http_component_stub.hpp b/tools/tests/http_component_stub.hpp new file mode 100644 index 0000000..543d97d --- /dev/null +++ b/tools/tests/http_component_stub.hpp @@ -0,0 +1,121 @@ +// Minimal host interfaces for compiling the real component without the game VM. +#pragma once +#include "http.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace std::chrono_literals; + +namespace game { constexpr int SCRIPT_NONE = 0; } +namespace plugin { class plugin {}; } +class component_interface +{ +public: + virtual ~component_interface() = default; + virtual void on_startup(plugin::plugin*) {} + virtual void on_shutdown(plugin::plugin*) {} +}; +#define REGISTER_COMPONENT(...) + +namespace scripting +{ + class array; + class script_value; + using fields = std::map; + class script_value + { + public: + std::variant> data; + script_value() = default; + script_value(int v) : data(v) {} + script_value(bool v) : data(v) {} + script_value(double v) : data(v) {} + script_value(const char* v) : data(std::string(v)) {} + script_value(std::string v) : data(std::move(v)) {} + script_value(const array& v); + template bool is() const { return std::holds_alternative(data); } + template T as() const { return std::get(data); } + struct raw { int type; }; + raw get_raw() const { return {static_cast(data.index())}; } + std::string to_string() const { return as(); } + }; + class array + { + public: + std::shared_ptr data = std::make_shared(); + script_value& operator[](const script_value& key) const { return (*data)[key.as()]; } + std::vector get_keys() const + { + std::vector keys; + for (const auto& [key, value] : *data) keys.emplace_back(key); + return keys; + } + }; + inline script_value::script_value(const array& v) : data(v.data) {} + template <> inline bool script_value::is() const { return std::holds_alternative>(data); } + template <> inline array script_value::as() const + { + array result; + result.data = std::get>(data); + return result; + } + using variadic_args = std::vector; + class object + { + inline static unsigned next_id = 1; + unsigned id = next_id++; + public: + unsigned get_entity_id() const { return id; } + }; + inline std::vector> notifications; + inline std::function after_notify; + inline void notify(unsigned, const std::string& event, const std::vector& args) + { + assert(event == "done"); + notifications.push_back(args); + if (after_notify) after_notify(); + } + inline void on_shutdown(const std::function&) {} +} +namespace scheduler +{ + struct pool + { + std::vector> tasks; + void push(std::function f) { tasks.push_back(std::move(f)); } + void run() + { + auto work = std::move(tasks); + tasks.clear(); + for (auto& task : work) task(); + } + }; + inline pool thread_pool; + constexpr int server = 0; + inline void loop(const std::function&, int) {} +} +namespace gsc::function +{ + template void add(const char*, F) {} + template void add_multiple(F, Names...) {} +} +namespace utils::http +{ + inline bool throw_from_transfer = false; + inline std::optional next_result = result{}; + inline timeouts observed_limits; + inline std::optional get_data(const std::string&, const std::string&, const headers&, const std::string&, const timeouts& limits) + { + observed_limits = limits; + if (throw_from_transfer) throw std::runtime_error("simulated worker error"); + return next_result; + } +} diff --git a/tools/tests/http_probe.cpp b/tools/tests/http_probe.cpp new file mode 100644 index 0000000..3217d3f --- /dev/null +++ b/tools/tests/http_probe.cpp @@ -0,0 +1,26 @@ +#include "http.hpp" + +#include +#include +#include + +int main(int argc, char** argv) +{ + if (argc != 4 || curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) + return 2; + utils::http::timeouts limits; + // A dash exercises the defaults compiled into the actual plugin source. + if (std::string(argv[2]) != "-") limits.connect_timeout_ms = std::stol(argv[2]); + if (std::string(argv[3]) != "-") limits.timeout_ms = std::stol(argv[3]); + const auto started = std::chrono::steady_clock::now(); + const auto result = utils::http::get_data(argv[1], "{\"event\":\"heartbeat\"}", + {{"Content-Type", "application/json"}, {"Authorization", "Bearer test-only"}}, "POST", limits); + const auto wall_ms = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + if (!result) return 3; + std::cout << "{\"curl_code\":" << result->code << ",\"http_status\":" << result->status + << ",\"dns_ms\":" << result->dns_ms << ",\"connect_ms\":" << result->connect_ms + << ",\"tls_ms\":" << result->tls_ms << ",\"first_byte_ms\":" << result->first_byte_ms + << ",\"total_ms\":" << result->total_ms << ",\"wall_ms\":" << wall_ms + << ",\"body_size\":" << result->buffer.size() << "}\n"; + curl_global_cleanup(); +}