diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..927830c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,43 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +# .github/dependabot.yml +# Keep GitHub Actions up to date and open a SINGLE PR that aggregates all updates. + +version: 2 +updates: + - package-ecosystem: "github-actions" # manage versions of actions used in .github/workflows + directory: "/" # repo root (covers .github/workflows) + schedule: + interval: "weekly" # run once a week + day: "monday" # every Monday + time: "04:00" # at 04:00 local time + timezone: "Europe/Sofia" + target-branch: "main" # PRs will target this branch + open-pull-requests-limit: 10 # safety cap (not really needed with grouping) + commit-message: + prefix: "chore" # e.g., "chore: bump actions/*" + include: "scope" + groups: + all-actions: # SINGLE PR with everything inside + patterns: ["*"] # match all actions + update-types: ["major", "minor", "patch"] # include all types of bumps + + - package-ecosystem: "nuget" # manage NuGet packages in .csproj/.sln files + directory: "/" # Dependabot discovers projects recursively + schedule: + interval: "weekly" # run once a week + day: "monday" # every Monday + time: "04:00" # at 04:00 local time + timezone: "Europe/Sofia" + target-branch: "main" # PRs will target this branch + open-pull-requests-limit: 10 # safety cap (not really needed with grouping) + commit-message: + prefix: "chore" # e.g., "chore: bump MySqlConnector" + include: "scope" + groups: + all-nuget: # SINGLE PR with everything inside + patterns: ["*"] # match all packages + update-types: ["major", "minor", "patch"] # include all types of bumps diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..18d0b71 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,166 @@ +name: Release SurfTimer.Plugin + +on: + push: + tags: + - "v*.*.*" # auto trigger on tags like v1.2.3 + workflow_dispatch: + inputs: + tag: + description: "Tag to release (e.g., v1.2.3)" + required: true + default: "v0.0.0" + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Resolve release tag + id: vars + shell: bash + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + TAG="${{ github.event.inputs.tag }}" + else + TAG="${{ github.ref_name }}" + fi + if ! echo "$TAG" | grep -Eq '^v[0-9]'; then + echo "Tag must start with 'v' (e.g., v1.2.3). Got: $TAG" + exit 1 + fi + echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV + echo "tag=$TAG" >> $GITHUB_OUTPUT + + # Checkout plugin repo at the tag + - name: Checkout plugin repository (Timer) + uses: actions/checkout@v4 + with: + path: Timer + ref: ${{ env.RELEASE_TAG }} + + # Checkout SurfTimer.Shared as sibling folder (ProjectReference resolves via ../../SurfTimer.Shared) + - name: Checkout SurfTimer.Shared + uses: actions/checkout@v4 + with: + repository: tslashd/SurfTimer.Shared + path: SurfTimer.Shared + # If private: + # token: ${{ secrets.SHARED_REPO_PAT }} + # Optionally pin to a tag/commit: + # ref: vX.Y.Z + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + # From Timer/src, ProjectReference ../../SurfTimer.Shared/... resolves correctly + - name: Restore (plugin) + working-directory: Timer/src + run: dotnet restore SurfTimer.Plugin.csproj + + - name: Build (Release) + working-directory: Timer/src + run: dotnet build SurfTimer.Plugin.csproj -c Release --no-restore + + - name: Prepare package layout + id: prep + shell: bash + env: + OUT_ROOT: out + run: | + set -euo pipefail + + BIN="Timer/src/bin/Release/net8.0" + PKG="$OUT_ROOT" + ADDONS="$PKG/addons/SurfTimer.Plugin" + CFGDST="$PKG/cfg/SurfTimer" + + mkdir -p "$ADDONS/data/GeoIP" "$ADDONS/lang" "$CFGDST" + + echo "Build output listing (should contain only selected DLLs):" + ls -la "$BIN" + + # Required artifacts (these should exist thanks to your KeepOnlySelectedDlls target) + for f in SurfTimer.Plugin.dll SurfTimer.Shared.dll Dapper.dll MaxMind.Db.dll MaxMind.GeoIP2.dll MySqlConnector.dll; do + test -f "$BIN/$f" || { echo "Missing $f in $BIN"; exit 1; } + done + + # Copy all dlls that remain after your KeepOnlySelectedDlls pruning + cp -v "$BIN"/*.dll "$ADDONS/" + + # data/GeoIP + SRC_MMDB="Timer/data/GeoIP/GeoLite2-Country.mmdb" + test -f "$SRC_MMDB" || { echo "Missing $SRC_MMDB"; exit 1; } + cp -v "$SRC_MMDB" "$ADDONS/data/GeoIP/" + + # lang/en.json + SRC_LANG="Timer/lang/en.json" + test -f "$SRC_LANG" || { echo "Missing $SRC_LANG"; exit 1; } + cp -v "$SRC_LANG" "$ADDONS/lang/" + + # cfg/SurfTimer (copy entire folder) + test -d "Timer/cfg/SurfTimer" || { echo "Missing Timer/cfg/SurfTimer"; exit 1; } + cp -vr "Timer/cfg/SurfTimer/." "$CFGDST/" + + echo "PKG_PATH=$PKG" >> $GITHUB_OUTPUT + + - name: Create ZIP + shell: bash + env: + PKG_NAME: SurfTimer.Plugin-${{ env.RELEASE_TAG }} + run: | + cd out + # zip the *contents* so archive root is addons/ and cfg/ + zip -r "${PKG_NAME}.zip" addons cfg + sha256sum "${PKG_NAME}.zip" > "${PKG_NAME}.zip.sha256" + ls -la + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: SurfTimer.Plugin-${{ env.RELEASE_TAG }} + path: | + out/SurfTimer.Plugin-${{ env.RELEASE_TAG }}.zip + out/SurfTimer.Plugin-${{ env.RELEASE_TAG }}.zip.sha256 + + release: + runs-on: ubuntu-latest + needs: build + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: ./artifacts + + - name: Determine tag + id: vars + shell: bash + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + TAG="${{ github.event.inputs.tag }}" + else + TAG="${{ github.ref_name }}" + fi + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV + + - name: List artifacts + run: ls -R ./artifacts + + - name: Create GitHub Release and upload assets + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.vars.outputs.tag }} + name: SurfTimer.Plugin ${{ steps.vars.outputs.tag }} + draft: false + prerelease: ${{ contains(steps.vars.outputs.tag, '-rc') || contains(steps.vars.outputs.tag, '-beta') || contains(steps.vars.outputs.tag, '-alpha') }} + files: | + artifacts/SurfTimer.Plugin-${{ env.RELEASE_TAG }}/SurfTimer.Plugin-${{ env.RELEASE_TAG }}.zip + artifacts/SurfTimer.Plugin-${{ env.RELEASE_TAG }}/SurfTimer.Plugin-${{ env.RELEASE_TAG }}.zip.sha256 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 3485604..35e2280 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ src/bin/Debug/* src/bin/Release/* src/obj/* -src/SurfTimer.csproj \ No newline at end of file +src/SurfTimer.csproj +*.puml +out/uml/include/full.png \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 68c000f..c0ede26 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/src/SurfTimer.csproj", + "${workspaceFolder}/src/SurfTimer.Plugin.csproj", "/property:Configuration=Debug" ], "problemMatcher": "$msCompile" @@ -18,7 +18,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/src/SurfTimer.csproj", + "${workspaceFolder}/src/SurfTimer.Plugin.csproj", "/property:Configuration=Release" ], "problemMatcher": "$msCompile" diff --git a/CS2SurfTimer.sln b/CS2SurfTimer.sln index ab56b21..55b45d5 100644 --- a/CS2SurfTimer.sln +++ b/CS2SurfTimer.sln @@ -5,7 +5,11 @@ VisualStudioVersion = 17.5.002.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A9962DB7-AE8A-4370-B381-19529A91B7EC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SurfTimer", "src\SurfTimer.csproj", "{98841535-B479-49B7-8D35-03786D4C31B9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SurfTimer.Plugin", "src\SurfTimer.Plugin.csproj", "{98841535-B479-49B7-8D35-03786D4C31B9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurfTimer.Api", "..\SurfTimer.Api\SurfTimer.Api.csproj", "{F53C6067-0A4E-18EF-53CB-69AB97901241}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SurfTimer.Shared", "..\SurfTimer.Shared\SurfTimer.Shared.csproj", "{6FA80551-EAFE-9030-69AA-1CEE7BEC44FB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -17,6 +21,14 @@ Global {98841535-B479-49B7-8D35-03786D4C31B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {98841535-B479-49B7-8D35-03786D4C31B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {98841535-B479-49B7-8D35-03786D4C31B9}.Release|Any CPU.Build.0 = Release|Any CPU + {F53C6067-0A4E-18EF-53CB-69AB97901241}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F53C6067-0A4E-18EF-53CB-69AB97901241}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F53C6067-0A4E-18EF-53CB-69AB97901241}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F53C6067-0A4E-18EF-53CB-69AB97901241}.Release|Any CPU.Build.0 = Release|Any CPU + {6FA80551-EAFE-9030-69AA-1CEE7BEC44FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FA80551-EAFE-9030-69AA-1CEE7BEC44FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FA80551-EAFE-9030-69AA-1CEE7BEC44FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FA80551-EAFE-9030-69AA-1CEE7BEC44FB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 1741b47..976eaf9 100644 --- a/README.md +++ b/README.md @@ -160,11 +160,16 @@ Core plugin for CS2 Surf Servers. This project is aimed to be fully open-source
+## 🔗 Dependencies +- [`CounterStrikeSharp`](https://github.com/roflmuffin/CounterStrikeSharp) - **required** minimum version [v1.0.337](https://github.com/roflmuffin/CounterStrikeSharp/releases/tag/v1.0.337). +- [`SurfTimer.Shared`](https://github.com/tslashd/SurfTimer.Shared) – **required** shared library for DTOs, entities, and database integration. +- [`SurfTimer.Api`](https://github.com/tslashd/SurfTimer.Api) – *optional* REST API for faster, centralized communication with the database. + # Main list with tasks (more details can be found [here](https://github.com/CS2Surf/Timer/blob/dev/TODO)): *Note: This is not definitive/complete and simply serves as a reference for what we should try to achieve. Subject to change.* Bold & Italics = being worked on. - [ ] Database - - [X] MySQL database schema ([Design Diagram](https://dbdiagram.io/d/CS2Surf-Timer-DB-Schema-6560b76b3be1495787ace4d2)) + - [X] MySQL database schema ([Design Diagram](https://dbdiagram.io/d/Copy-of-CS2Surf-Timer-DB-Schema-6582e6e456d8064ca06328b9)) - [ ] Plugin auto-create tables for easier setup? - [X] Base database class implementation - [X] Maps @@ -205,12 +210,12 @@ Bold & Italics = being worked on. - [ ] Stretch goal: sub-tick timing - [ ] Player Data - [X] Base player class - - [ ] Player stat classes - - [ ] Profile implementation (DB) + - [X] Player stat classes + - [X] Profile implementation (DB) - [ ] Points/Skill Groups (DB) - [ ] Player settings (DB) - [x] Replays - - [x] Personal Best + - [x] Personal Best - Data for the PB replays is saved but no functionality to replay them yet is available - [x] Map Record - [X] Stage Record - [X] Bonus Record @@ -220,4 +225,4 @@ Bold & Italics = being worked on. - [X] Bonus Record - [ ] Style implementation (SW, HSW, BW) - [ ] Paint (?) -- [ ] API Integration (Repo can be found [here](https://github.com/CS2Surf/CS2-Surf-API)) +- [x] API Integration (Repo can be found [here](https://github.com/tslashd/SurfTimer.Api)) diff --git a/TODO b/TODO index 5675419..46f582e 100644 --- a/TODO +++ b/TODO @@ -1,11 +1,11 @@ - Replay Bot Scoreboard names NOT changing when setting new recs + Re-add the MySQL queries in code and make it switch between API and DB functions -- Map Time is NOT being saved with API ++ Map Time is NOT being saved with API - Make configs generate themselves inside the `./configs/plugins/...` folder -- Fix loading MapTimes for each type (stage, bonus, maps) - - API - - DB ++ Fix loading MapTimes for each type (stage, bonus, maps) + + API + + DB + Change `DB_QUERY_MAP_GET_RUNS` query with `DB_QUERY_MAP_GET_RECORD_RUNS_AND_COUNT` in API and edit code in plugin + Change `DB_QUERY_PB_GET_RUNTIME` query in API diff --git a/cfg/SurfTimer/api_config.json b/cfg/SurfTimer/api_config.json new file mode 100644 index 0000000..98449c5 --- /dev/null +++ b/cfg/SurfTimer/api_config.json @@ -0,0 +1,4 @@ +{ + "api_url": "API_URL_HERE", + "api_enabled": false +} \ No newline at end of file diff --git a/cfg/SurfTimer/timer_settings.json b/cfg/SurfTimer/timer_settings.json index 9e26dfe..77f73cb 100644 --- a/cfg/SurfTimer/timer_settings.json +++ b/cfg/SurfTimer/timer_settings.json @@ -1 +1,4 @@ -{} \ No newline at end of file +{ + "replays_enabled": true, + "replays_pre": 64 +} \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index feac428..c8686ca 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,39 +1,45 @@ { - "prefix": "[{white}asd{green}asd{red}asd{default}]", + "prefix": "[{blue}Surf{bluegrey}Timer{default}]", - "player_connected": "{green}{0}{default} has connected from {lime}{1}{default}.", + "player_connected": "{green}{0}{default} has connected from {lime}{1}{default}.", - /* PlayerCommands */ - "reset_delay": "Please wait for your run to be saved before resetting.", - "invalid_usage": "{red}Invalid arguments. {default}Usage: {green}{0}", - "invalid_stage_value": "{red}Invalid stage provided. {default}this map has {green}{0}{default} stages.", - "not_staged": "{red}This map has no stages.", - "invalid_bonus_value": "{red}Invalid bonus provided. {default}this map has {green}{0}{default} bonuses.", - "not_bonused": "{red}This map has no bonuses.", - "rank":"Your current rank for {gold}{0}{default} is {green}{1}{default} out of {yellow}{2}", - "saveloc_not_in_run": "{red}Cannot save location while not in run.", - "saveloc_saved": "{green}Saved location! {default}Use {green}!tele {0}{default} to teleport to this location.", - "saveloc_no_locations": "{red}No saved locations.", - "saveloc_practice": "{red}Timer now in Practice.", - "saveloc_teleported": "Teleported to #{green}{0}", - "saveloc_first": "{red}Already at first location", - "saveloc_last": "{red}Already at last location", + /* MapCommands */ + "map_info": "{yellow}{0}{default} | Tier - {1}{default} | Author - {yellow}{2}{default} | Ranked - {3}{default} | Added - {yellow}{4}{default}", + "map_info_linear": " | Linear - {yellow}{0} Checkpoints{default}", + "map_info_stages": " | Stages - {yellow}{0}{default}", + "map_info_bonuses": " | Bonuses - {yellow}{0}{default}", - /* TriggerStartTouch */ - "stagewr_set": "{lime}{0}{default} set the first {yellow}Stage {1}{default} record at {gold}{2}{default}!", - "stagewr_improved": "{lime}{0}{default} has set a new {yellow}Stage {1}{default} record with a time of {gold}{2}{default}, beating the old record by {green}-{3}{default}! (Previous: {bluegrey}{4})", - "stagewr_missed": "You are behind the {yellow}Stage {0}{default} record with a time of {grey}{1}{default}, missing the record by {red}+{2}{default} ({gold}{3}{default})!", - "stagepb_set": "You finished {yellow}Stage {0}{default} in {gold}{1}{default}, setting your new Personal Best!", - "stagepb_improved": "{lime}{0}{default} beat their {yellow}Stage {1}{default} Personal Best with a time of {gold}{2}{default}, improving by {green}-{3}{default}! (Previous: {bluegrey}{4})", - "mapwr_set": "{lime}{0}{default} set the first {yellow}Map{default} record at {gold}{1}{default}!", - "mapwr_improved": "{lime}{0}{default} has set a new {yellow}Map{default} record with a time of {gold}{1}{default}, beating the old record by {green}-{2}{default}! (Previous: {bluegrey}{3}{default})", - "mappb_set": "You finished the {yellow}Map{default} in {gold}{0}{default}, setting your new Personal Best!", - "mappb_improved": "{lime}{0}{default} beat their {yellow}Map{default} Personal Best with a time of {gold}{1}{default}, improving by {green}-{2}{default}! (Previous: {bluegrey}{3}{default})", - "mappb_missed": "You finished the map in {yellow}{0}{default}!", - "bonuswr_set": "{lime}{0}{default} set the first {yellow}Bonus {1}{default} record at {gold}{2}{default}!", - "bonuswr_improved": "{lime}{0}{default} has set a new {yellow}Bonus {1}{default} record with a time of {gold}{2}{default}, beating the old record by {green}-{3}{default}! (Previous: {bluegrey}{4})", - "bonuspb_set": "You finished {yellow}Bonus {0}{default} in {gold}{1}{default}, setting your new Personal Best!", - "bonuspb_improved": "{lime}{0}{default} beat their {yellow}Bonus {1}{default} Personal Best with a time of {gold}{2}{default}, improving by {green}-{3}{default}! (Previous: {bluegrey}{4})", - "bonuspb_missed": "You are behind the {yellow}Bonus {0}{default} record with a time of {grey}{1}{default}, missing the record by {red}+{2}{default} ({gold}{3}{default})!", - "checkpoint_message": "CP [{yellow}{0}{default}]: {yellow}{1}{default} {yellow}({2}){default} [PB: {3} | WR: {4}]", + /* PlayerCommands */ + "reset_delay": "Please wait for your run to be saved before resetting.", + "invalid_usage": "{red}Invalid arguments. {default}Usage: {green}{0}", + "invalid_stage_value": "{red}Invalid stage provided. {default}this map has {green}{0}{default} stages.", + "not_staged": "{red}This map has no stages.", + "invalid_bonus_value": "{red}Invalid bonus provided. {default}this map has {green}{0}{default} bonuses.", + "not_bonused": "{red}This map has no bonuses.", + "rank": "Your current rank for {gold}{0}{default} is {green}{1}{default} out of {yellow}{2}", + "saveloc_not_in_run": "{red}Cannot save location while not in run.", + "saveloc_saved": "{green}Saved location! {default}Use {green}!tele {0}{default} to teleport to this location.", + "saveloc_no_locations": "{red}No saved locations.", + "saveloc_practice": "{red}Timer now in Practice.", + "saveloc_teleported": "Teleported to #{green}{0}", + "saveloc_first": "{red}Already at first location", + "saveloc_last": "{red}Already at last location", + + /* TriggerStartTouch */ + "stagewr_set": "{lime}{0}{default} set the first {yellow}Stage {1}{default} record at {gold}{2}{default}!", + "stagewr_improved": "{lime}{0}{default} has set a new {yellow}Stage {1}{default} record with a time of {gold}{2}{default}, beating the old record by {green}-{3}{default}! (Previous: {bluegrey}{4})", + "stagewr_missed": "You are behind the {yellow}Stage {0}{default} record with a time of {grey}{1}{default}, missing the record by {red}+{2}{default} ({gold}{3}{default})!", + "stagepb_set": "You finished {yellow}Stage {0}{default} in {gold}{1}{default}, setting your new Personal Best!", + "stagepb_improved": "{lime}{0}{default} beat their {yellow}Stage {1}{default} Personal Best with a time of {gold}{2}{default}, improving by {green}-{3}{default}! (Previous: {bluegrey}{4})", + "mapwr_set": "{lime}{0}{default} set the first {yellow}Map{default} record at {gold}{1}{default}!", + "mapwr_improved": "{lime}{0}{default} has set a new {yellow}Map{default} record with a time of {gold}{1}{default}, beating the old record by {green}-{2}{default}! (Previous: {bluegrey}{3}{default})", + "mappb_set": "You finished the {yellow}Map{default} in {gold}{0}{default}, setting your new Personal Best!", + "mappb_improved": "{lime}{0}{default} beat their {yellow}Map{default} Personal Best with a time of {gold}{1}{default}, improving by {green}-{2}{default}! (Previous: {bluegrey}{3}{default})", + "mappb_missed": "You finished the map in {yellow}{0}{default}!", + "bonuswr_set": "{lime}{0}{default} set the first {yellow}Bonus {1}{default} record at {gold}{2}{default}!", + "bonuswr_improved": "{lime}{0}{default} has set a new {yellow}Bonus {1}{default} record with a time of {gold}{2}{default}, beating the old record by {green}-{3}{default}! (Previous: {bluegrey}{4})", + "bonuspb_set": "You finished {yellow}Bonus {0}{default} in {gold}{1}{default}, setting your new Personal Best!", + "bonuspb_improved": "{lime}{0}{default} beat their {yellow}Bonus {1}{default} Personal Best with a time of {gold}{2}{default}, improving by {green}-{3}{default}! (Previous: {bluegrey}{4})", + "bonuspb_missed": "You are behind the {yellow}Bonus {0}{default} record with a time of {grey}{1}{default}, missing the record by {red}+{2}{default} ({gold}{3}{default})!", + "checkpoint_message": "CP [{yellow}{0}{default}]: {yellow}{1}{default} {yellow}({2}){default} [PB: {3} | WR: {4}]" } \ No newline at end of file diff --git a/src/ST-API/Api.cs b/src/ST-API/Api.cs deleted file mode 100644 index 78a9fbe..0000000 --- a/src/ST-API/Api.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Net.Http.Json; - -namespace SurfTimer; - -internal class ApiCall -{ - public static async Task Api_Save_Stage_MapTime(Player player) - { - // This is a trick to record the time before the player exits the start zone - int last_exit_tick = player.ReplayRecorder.LastExitTick(); - int last_enter_tick = player.ReplayRecorder.LastEnterTick(); - - // player.Controller.PrintToChat($"CS2 Surf DEBUG >> OnTriggerStartTouch -> Last Exit Tick: {last_exit_tick} | Current Frame: {player.ReplayRecorder.Frames.Count}"); - - int stage_run_time = player.ReplayRecorder.Frames.Count - 1 - last_exit_tick; // Would like some check on this - int time_since_last_enter = player.ReplayRecorder.Frames.Count - 1 - last_enter_tick; - - int tt = -1; - if (last_exit_tick - last_enter_tick > 2 * 64) - tt = last_exit_tick - 2 * 64; - else - tt = last_enter_tick; - - API_CurrentRun stage_time = new() - { - player_id = player.Profile.ID, - map_id = player.CurrMap.ID, - style = player.Timer.Style, - type = 2, - stage = player.Timer.Stage - 1, - run_time = stage_run_time, - run_date = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - replay_frames = player.ReplayRecorder.SerializeReplayPortion(tt, time_since_last_enter) - - }; - - await ApiMethod.POST(Config.API.Endpoints.ENDPOINT_CR_SAVE_STAGE_TIME, stage_time); - // player.Stats.LoadStageTime(player); - // await CurrentMap.ApiGetMapRecordAndTotals(); // Reload the Map record and totals for the HUD - } -} \ No newline at end of file diff --git a/src/ST-API/Comms.cs b/src/ST-API/Comms.cs index 1a99a9f..038eb95 100644 --- a/src/ST-API/Comms.cs +++ b/src/ST-API/Comms.cs @@ -1,74 +1,131 @@ using System.Net.Http.Json; using System.Runtime.CompilerServices; +using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using SurfTimer.Shared.Entities; +using SurfTimer.Shared.JsonConverters; namespace SurfTimer; internal class ApiMethod { - private ApiMethod() { } - private static readonly HttpClient _client = new(); - private static readonly string base_addr = Config.ApiUrl; + private static readonly string BaseAddress = Config.ApiUrl; + + // Custom Converter for ReplayFramesString + private static readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNameCaseInsensitive = true, + }; + static ApiMethod() + { + _jsonOptions.Converters.Add(new ReplayFramesStringConverter()); + } + + /// + /// Executes a GET request to the specified URL and deserializes the response to type T. + /// + /// Type to deserialize response into + /// Relative URL to call + /// Deserialized T or null public static async Task GET(string url, [CallerMemberName] string methodName = "") { - var uri = new Uri(base_addr + url); + var uri = new Uri(BaseAddress + url); var _logger = SurfTimer.ServiceProvider.GetRequiredService>(); -#if DEBUG - Console.WriteLine($"======= CS2 Surf DEBUG >> public static async Task GET -> BASE ADDR: {base_addr} | ENDPOINT: {url} | FULL: {uri.ToString()}"); -#endif - using var response = await _client.GetAsync(uri); try { - _logger.LogInformation("[{ClassName}] {MethodName} -> GET {URL} => {StatusCode}", - nameof(ApiMethod), methodName, url, response.StatusCode + var responseTime = + response.Headers.TryGetValues("x-response-time-ms", out var values) + && !string.IsNullOrEmpty(values.FirstOrDefault()) + ? $"{values.First()}ms" + : "N/A"; + + _logger.LogInformation( + "[{ClassName}] {MethodName} -> GET {URL} => {StatusCode} | x-response-time-ms {ResponseTime}", + nameof(ApiMethod), + methodName, + url, + response.StatusCode, + responseTime ); if (response.StatusCode == System.Net.HttpStatusCode.NoContent) { - _logger.LogWarning("[{ClassName}] {MethodName} -> No data found {StatusCode}", - nameof(ApiMethod), methodName, response.StatusCode + _logger.LogWarning( + "[{ClassName}] {MethodName} -> No data found {StatusCode}", + nameof(ApiMethod), + methodName, + response.StatusCode ); return default; } else if (response.StatusCode != System.Net.HttpStatusCode.OK) { - Exception exception = new Exception($"[{nameof(ApiMethod)}] {methodName} -> Unexpected status code {response.StatusCode}"); + Exception exception = new Exception( + $"[{nameof(ApiMethod)}] {methodName} -> Unexpected status code {response.StatusCode}" + ); throw exception; } - return await response.Content.ReadFromJsonAsync(); + // Input the custom JsonSerializerOptions to handle ReplayFramesString conversion + return await response.Content.ReadFromJsonAsync(_jsonOptions); } catch (Exception ex) { - _logger.LogError(ex, "[{ClassName}] {MethodName} -> HTTP Response was invalid or could not be deserialised.", nameof(ApiMethod), methodName); + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> HTTP Response was invalid or could not be deserialised.", + nameof(ApiMethod), + methodName + ); return default; } } - public static async Task POST(string url, T body, [CallerMemberName] string methodName = "") + /// + /// Executes a POST request to the specified URL with the given body and returns the response. + /// + /// Type of the request body + /// Relative URL to call + /// Request body to send + /// PostResponseEntity or null + public static async Task POST( + string url, + T body, + [CallerMemberName] string methodName = "" + ) { - var uri = new Uri(base_addr + url); + var uri = new Uri(BaseAddress + url); var _logger = SurfTimer.ServiceProvider.GetRequiredService>(); try { using var response = await _client.PostAsJsonAsync(uri, body); + var responseTime = + response.Headers.TryGetValues("x-response-time-ms", out var values) + && !string.IsNullOrEmpty(values.FirstOrDefault()) + ? $"{values.First()}ms" + : "N/A"; + _logger.LogInformation( - "[{ClassName}] {MethodName} -> POST {URL} => {StatusCode}", - nameof(ApiMethod), methodName, url, response.StatusCode + "[{ClassName}] {MethodName} -> POST {URL} => {StatusCode} | x-response-time-ms {ResponseTime}", + nameof(ApiMethod), + methodName, + url, + response.StatusCode, + responseTime ); if (response.IsSuccessStatusCode) { - return await response.Content.ReadFromJsonAsync(); + return await response.Content.ReadFromJsonAsync(); } else { @@ -77,7 +134,11 @@ private ApiMethod() { } _logger.LogWarning( "[{ClassName}] {MethodName} -> POST {URL} failed with status {StatusCode}. Response body: {ResponseBody}", - nameof(ApiMethod), methodName, url, response.StatusCode, errorContent + nameof(ApiMethod), + methodName, + url, + response.StatusCode, + errorContent ); return default; @@ -88,30 +149,53 @@ private ApiMethod() { } _logger.LogError( ex, "[{ClassName}] {MethodName} -> Exception during POST {URL}", - nameof(ApiMethod), methodName, url + nameof(ApiMethod), + methodName, + url ); return default; } } - public static async Task PUT(string url, T body, [CallerMemberName] string methodName = "") + /// + /// Executes a PUT request to the specified URL with the given body and returns the response. + /// + /// Type of the request body + /// Relative URL to call + /// Request body to send + /// PostResponseEntity or null + public static async Task PUT( + string url, + T body, + [CallerMemberName] string methodName = "" + ) { - var uri = new Uri(base_addr + url); + var uri = new Uri(BaseAddress + url); var _logger = SurfTimer.ServiceProvider.GetRequiredService>(); try { using var response = await _client.PutAsJsonAsync(uri, body); + var responseTime = + response.Headers.TryGetValues("x-response-time-ms", out var values) + && !string.IsNullOrEmpty(values.FirstOrDefault()) + ? $"{values.First()}ms" + : "N/A"; + _logger.LogInformation( - "[{ClassName}] {MethodName} -> PUT {URL} => {StatusCode}", - nameof(ApiMethod), methodName, url, response.StatusCode + "[{ClassName}] {MethodName} -> PUT {URL} => {StatusCode} | x-response-time-ms {ResponseTime}", + nameof(ApiMethod), + methodName, + url, + response.StatusCode, + responseTime ); if (response.IsSuccessStatusCode) { - return await response.Content.ReadFromJsonAsync(); + return await response.Content.ReadFromJsonAsync(); } else { @@ -119,7 +203,11 @@ private ApiMethod() { } _logger.LogWarning( "[{ClassName}] {MethodName} -> PUT {URL} failed with status {StatusCode}. Response body: {ResponseBody}", - nameof(ApiMethod), methodName, url, response.StatusCode, errorContent + nameof(ApiMethod), + methodName, + url, + response.StatusCode, + errorContent ); return default; @@ -130,11 +218,12 @@ private ApiMethod() { } _logger.LogError( ex, "[{ClassName}] {MethodName} -> Exception during PUT {URL}", - nameof(ApiMethod), methodName, url + nameof(ApiMethod), + methodName, + url ); return default; } } - -} \ No newline at end of file +} diff --git a/src/ST-API/JsonConverters.cs b/src/ST-API/JsonConverters.cs index 377aacd..475a5bd 100644 --- a/src/ST-API/JsonConverters.cs +++ b/src/ST-API/JsonConverters.cs @@ -2,7 +2,6 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using CounterStrikeSharp.API.Modules.Utils; namespace SurfTimer; @@ -28,9 +27,9 @@ public static Dictionary ConstructJsonDictFromString(string str) } } -internal class Vector_tConverter : JsonConverter +internal class VectorTConverter : JsonConverter { - public override Vector_t Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override VectorT Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Ensure that the reader is positioned at the start of an object if (reader.TokenType != JsonTokenType.StartObject) @@ -63,10 +62,10 @@ public override Vector_t Read(ref Utf8JsonReader reader, Type typeToConvert, Jso } } - return new Vector_t { X = x, Y = y, Z = z }; + return new VectorT { X = x, Y = y, Z = z }; } - public override void Write(Utf8JsonWriter writer, Vector_t value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, VectorT value, JsonSerializerOptions options) { writer.WriteStartObject(); writer.WriteNumber("X", value.X); @@ -76,9 +75,9 @@ public override void Write(Utf8JsonWriter writer, Vector_t value, JsonSerializer } } -internal class QAngle_tConverter : JsonConverter +internal class QAngleTConverter : JsonConverter { - public override QAngle_t Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override QAngleT Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Ensure that the reader is positioned at the start of an object if (reader.TokenType != JsonTokenType.StartObject) @@ -111,10 +110,10 @@ public override QAngle_t Read(ref Utf8JsonReader reader, Type typeToConvert, Jso } } - return new QAngle_t { X = X, Y = Y, Z = Z }; + return new QAngleT { X = X, Y = Y, Z = Z }; } - public override void Write(Utf8JsonWriter writer, QAngle_t value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, QAngleT value, JsonSerializerOptions options) { writer.WriteStartObject(); writer.WriteNumber("X", value.X); @@ -126,11 +125,22 @@ public override void Write(Utf8JsonWriter writer, QAngle_t value, JsonSerializer internal static class Compressor { - public static string Decompress(string input) + public static byte[] Compress(byte[] input) { - byte[] compressed = Convert.FromBase64String(input); - byte[] decompressed = Decompress(compressed); - return Encoding.UTF8.GetString(decompressed); + using (var result = new MemoryStream()) + { + var lengthBytes = BitConverter.GetBytes(input.Length); + result.Write(lengthBytes, 0, 4); + + using (var compressionStream = new GZipStream(result, + CompressionMode.Compress)) + { + compressionStream.Write(input, 0, input.Length); + compressionStream.Flush(); + + } + return result.ToArray(); + } } public static string Compress(string input) @@ -145,42 +155,28 @@ public static byte[] Decompress(byte[] input) using (var source = new MemoryStream(input)) { byte[] lengthBytes = new byte[4]; - source.Read(lengthBytes, 0, 4); + int bytesRead = source.Read(lengthBytes, 0, 4); + if (bytesRead != 4) + throw new InvalidDataException("Failed to read the expected length prefix."); var length = BitConverter.ToInt32(lengthBytes, 0); using (var decompressionStream = new GZipStream(source, CompressionMode.Decompress)) { var result = new byte[length]; - int totalRead = 0, bytesRead; - while ((bytesRead = decompressionStream.Read(result, totalRead, length - totalRead)) > 0) + int totalRead = 0, read; + while ((read = decompressionStream.Read(result, totalRead, length - totalRead)) > 0) { - totalRead += bytesRead; + totalRead += read; } return result; } } } - public static byte[] Compress(byte[] input) - { - using (var result = new MemoryStream()) - { - var lengthBytes = BitConverter.GetBytes(input.Length); - result.Write(lengthBytes, 0, 4); - - using (var compressionStream = new GZipStream(result, - CompressionMode.Compress)) - { - compressionStream.Write(input, 0, input.Length); - compressionStream.Flush(); - - } - return result.ToArray(); - } - } - - internal static string Decompress(byte v) + public static string Decompress(string input) { - throw new NotImplementedException(); + byte[] compressed = Convert.FromBase64String(input); + byte[] decompressed = Decompress(compressed); + return Encoding.UTF8.GetString(decompressed); } } \ No newline at end of file diff --git a/src/ST-API/Schema.cs b/src/ST-API/Schema.cs index ba324df..3b79a01 100644 --- a/src/ST-API/Schema.cs +++ b/src/ST-API/Schema.cs @@ -1,6 +1,5 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Modules.Memory; - using System.Runtime.CompilerServices; using System.Text; diff --git a/src/ST-API/Structures.cs b/src/ST-API/Structures.cs deleted file mode 100644 index 79a4e04..0000000 --- a/src/ST-API/Structures.cs +++ /dev/null @@ -1,130 +0,0 @@ -namespace SurfTimer; - -// Map Info structure -internal class API_PostResponseData -{ - public int inserted { get; set; } - public float xtime { get; set; } - public int last_id { get; set; } - public List? trx { get; set; } -} - -internal class API_Checkpoint -{ - public int cp { get; set; } - public int run_time { get; set; } - public float start_vel_x { get; set; } - public float start_vel_y { get; set; } - public float start_vel_z { get; set; } - public float end_vel_x { get; set; } - public float end_vel_y { get; set; } - public float end_vel_z { get; set; } - public int end_touch { get; set; } - public int attempts { get; set; } -} - -internal class API_CurrentRun -{ - public int player_id { get; set; } - public int map_id { get; set; } - public int run_time { get; set; } - public float start_vel_x { get; set; } - public float start_vel_y { get; set; } - public float start_vel_z { get; set; } - public float end_vel_x { get; set; } - public float end_vel_y { get; set; } - public float end_vel_z { get; set; } - public int style { get; set; } = 0; - public int type { get; set; } = 0; - public int stage { get; set; } = 0; - public List? checkpoints { get; set; } = null; - public string replay_frames { get; set; } = ""; // This needs to be checked touroughly - public int? run_date { get; set; } = null; -} - -internal class API_MapInfo -{ - public int id { get; set; } = 0; - public string name { get; set; } = "N/A"; - public string author { get; set; } = "Unknown"; - public int tier { get; set; } = 0; - public int stages { get; set; } = 0; - public int bonuses { get; set; } = 0; - public int ranked { get; set; } = 0; - public int? date_added { get; set; } = null; - public int? last_played { get; set; } = null; -} - -internal class API_MapTime -{ - public int id { get; set; } - public int player_id { get; set; } - public int map_id { get; set; } - public int style { get; set; } = 0; - public int type { get; set; } = 0; - public int stage { get; set; } = 0; - public int run_time { get; set; } - public float start_vel_x { get; set; } - public float start_vel_y { get; set; } - public float start_vel_z { get; set; } - public float end_vel_x { get; set; } - public float end_vel_y { get; set; } - public float end_vel_z { get; set; } - public int run_date { get; set; } - public string replay_frames { get; set; } = ""; // This needs to be checked touroughly - public List? checkpoints { get; set; } = null; - public string name { get; set; } = "N/A"; - public int total_count { get; set; } -} - -internal class API_PlayerSurfProfile -{ - public int id { get; set; } - public string name { get; set; } = "N/A"; - public ulong steam_id { get; set; } - public string country { get; set; } = "N/A"; - public int join_date { get; set; } - public int last_seen { get; set; } - public int connections { get; set; } -} - -internal class API_PersonalBest -{ - public int id { get; set; } - public int player_id { get; set; } - public int map_id { get; set; } - public int style { get; set; } = 0; - public int type { get; set; } = 0; - public int stage { get; set; } = 0; - public int run_time { get; set; } - public float start_vel_x { get; set; } - public float start_vel_y { get; set; } - public float start_vel_z { get; set; } - public float end_vel_x { get; set; } - public float end_vel_y { get; set; } - public float end_vel_z { get; set; } - public int run_date { get; set; } - public string replay_frames { get; set; } = ""; // This needs to be checked touroughly - public List? checkpoints { get; set; } = null; - public string name { get; set; } = "N/A"; - public int rank { get; set; } -} - -internal class API_SaveMapTime -{ - public int player_id { get; set; } - public int map_id { get; set; } - public int run_time { get; set; } - public float start_vel_x { get; set; } - public float start_vel_y { get; set; } - public float start_vel_z { get; set; } - public float end_vel_x { get; set; } - public float end_vel_y { get; set; } - public float end_vel_z { get; set; } - public int style { get; set; } = 0; - public int type { get; set; } = 0; - public int stage { get; set; } = 0; - public List? checkpoints { get; set; } = null; - public string replay_frames { get; set; } = ""; - public int? run_date { get; set; } = null; -} diff --git a/src/ST-Commands/MapCommands.cs b/src/ST-Commands/MapCommands.cs index 14714d7..e2ac1a0 100644 --- a/src/ST-Commands/MapCommands.cs +++ b/src/ST-Commands/MapCommands.cs @@ -1,9 +1,11 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes.Registration; +using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Utils; -using CounterStrikeSharp.API.Modules.Admin; +using SurfTimer.Shared.DTO; +using System.Text.RegularExpressions; namespace SurfTimer; @@ -13,28 +15,169 @@ public partial class SurfTimer [ConsoleCommand("css_tier", "Display the current map tier.")] [ConsoleCommand("css_mapinfo", "Display the current map tier.")] [ConsoleCommand("css_mi", "Display the current map tier.")] + [ConsoleCommand("css_difficulty", "Display the current map tier.")] [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void MapTier(CCSPlayerController? player, CommandInfo command) { if (player == null) return; - string msg = $"{Config.PluginPrefix} {CurrentMap.Name} - Tier {ChatColors.Green}{CurrentMap.Tier}{ChatColors.Default} - Author {ChatColors.Yellow}{CurrentMap.Author}{ChatColors.Default} - Added {ChatColors.Yellow}{DateTimeOffset.FromUnixTimeSeconds(CurrentMap.DateAdded).DateTime.ToString("dd.MM.yyyy HH:mm")}{ChatColors.Default}"; + char rankedColor = CurrentMap.Ranked ? ChatColors.Green : ChatColors.Red; + string rankedStatus = CurrentMap.Ranked ? "Yes" : "No"; + + string msg = $"{Config.PluginPrefix} " + LocalizationService.LocalizerNonNull["map_info", + CurrentMap.Name!, + $"{Extensions.GetTierColor(CurrentMap.Tier)}{CurrentMap.Tier}", + CurrentMap.Author!, + $"{rankedColor}{rankedStatus}", + DateTimeOffset.FromUnixTimeSeconds(CurrentMap.DateAdded).DateTime.ToString("dd.MM.yyyy HH:mm") + ]; if (CurrentMap.Stages > 1) { - msg = string.Concat(msg, " - ", $"Stages {ChatColors.Yellow}{CurrentMap.Stages}{ChatColors.Default}"); + msg += LocalizationService.LocalizerNonNull["map_info_stages", CurrentMap.Stages]; } else { - msg = string.Concat(msg, " - ", $"Linear {ChatColors.Yellow}{CurrentMap.TotalCheckpoints} Checkpoints{ChatColors.Default}"); + msg += LocalizationService.LocalizerNonNull["map_info_linear", CurrentMap.TotalCheckpoints]; } if (CurrentMap.Bonuses > 0) { - msg = string.Concat(msg, " - ", $"Bonuses {ChatColors.Yellow}{CurrentMap.Bonuses}"); + msg += LocalizationService.LocalizerNonNull["map_info_bonuses", CurrentMap.Bonuses]; + } + + player.PrintToChat(msg); + } + + [ConsoleCommand("css_amt", "Set the Tier of the map.")] + [ConsoleCommand("css_addmaptier", "Set the Tier of the map.")] + [RequiresPermissions("@css/root")] + [CommandHelper(minArgs: 1, usage: " [1-8]", whoCanExecute: CommandUsage.CLIENT_ONLY)] + public void AddMapTier(CCSPlayerController? player, CommandInfo command) + { + if (player == null) + return; + + short tier; + try + { + tier = short.Parse(command.ArgByIndex(1)); + } + catch (System.Exception) + { + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_usage", + "!amt [1-8]"]}" + ); + return; + } + + if (tier > 8) + { + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_usage", + "!amt [1-8]"]}" + ); + return; + } + + var mapInfo = new MapDto + { + Name = CurrentMap.Name!, + Author = CurrentMap.Author!, + Tier = tier, + Stages = CurrentMap.Stages, + Bonuses = CurrentMap.Bonuses, + Ranked = CurrentMap.Ranked, + LastPlayed = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds() + }; + + CurrentMap.Tier = tier; + + Task.Run(async () => + { + await _dataService!.UpdateMapInfoAsync(mapInfo, CurrentMap.ID); + }); + + string msg = $"{Config.PluginPrefix} {ChatColors.Yellow}{CurrentMap.Name}{ChatColors.Default} - Set Tier to {Extensions.GetTierColor(CurrentMap.Tier)}{CurrentMap.Tier}{ChatColors.Default}."; + + player.PrintToChat(msg); + } + + [ConsoleCommand("css_amn", "Set the Name of the map author.")] + [ConsoleCommand("css_addmappername", "Set the Name of the map author.")] + [RequiresPermissions("@css/root")] + [CommandHelper(minArgs: 1, usage: "", whoCanExecute: CommandUsage.CLIENT_ONLY)] + public void AddMapAuthor(CCSPlayerController? player, CommandInfo command) + { + if (player == null) + return; + + string author = command.ArgString.Trim(); + + // Validate: letters, numbers, intervals, dashes and up to 50 symbols + if (string.IsNullOrWhiteSpace(author) || author.Length > 50 || !Regex.IsMatch(author, @"^[\w\s\-\.]+$")) + { + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_usage", + "!amn "]}" + ); + return; } + var mapInfo = new MapDto + { + Name = CurrentMap.Name!, + Author = author, + Tier = CurrentMap.Tier, + Stages = CurrentMap.Stages, + Bonuses = CurrentMap.Bonuses, + Ranked = CurrentMap.Ranked, + LastPlayed = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds() + }; + + CurrentMap.Author = author; + + Task.Run(async () => + { + await _dataService!.UpdateMapInfoAsync(mapInfo, CurrentMap.ID); + }); + + string msg = $"{Config.PluginPrefix} {ChatColors.Yellow}{CurrentMap.Name}{ChatColors.Default} - Set Author to {ChatColors.Green}{CurrentMap.Author}{ChatColors.Default}."; + + player.PrintToChat(msg); + } + + [ConsoleCommand("css_amr", "Set the Ranked option of the map.")] + [ConsoleCommand("css_addmapranked", "Set the Ranked option of the map.")] + [RequiresPermissions("@css/root")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] + public void AddMapRanked(CCSPlayerController? player, CommandInfo command) + { + if (player == null) + return; + + if (CurrentMap.Ranked) + CurrentMap.Ranked = false; + else + CurrentMap.Ranked = true; + + var mapInfo = new MapDto + { + Name = CurrentMap.Name!, + Author = CurrentMap.Author!, + Tier = CurrentMap.Tier, + Stages = CurrentMap.Stages, + Bonuses = CurrentMap.Bonuses, + Ranked = CurrentMap.Ranked, + LastPlayed = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds() + }; + + Task.Run(async () => + { + await _dataService!.UpdateMapInfoAsync(mapInfo, CurrentMap.ID); + }); + + string msg = $"{Config.PluginPrefix} {ChatColors.Yellow}{CurrentMap.Name}{ChatColors.Default} - Set Ranked to {(CurrentMap.Ranked ? ChatColors.Green : ChatColors.Red)}{CurrentMap.Ranked}{ChatColors.Default}."; + player.PrintToChat(msg); } @@ -59,9 +202,9 @@ public void Triggers(CCSPlayerController? player, CommandInfo command) player.PrintToChat($"Hooked Trigger -> Start -> {CurrentMap.StartZone} -> Angles {CurrentMap.StartZoneAngles}"); player.PrintToChat($"Hooked Trigger -> End -> {CurrentMap.EndZone}"); int i = 1; - foreach (Vector_t stage in CurrentMap.StageStartZone) + foreach (VectorT stage in CurrentMap.StageStartZone) { - if (stage.X == 0 && stage.Y == 0 && stage.Z == 0) + if (stage.IsZero()) continue; else { @@ -71,9 +214,9 @@ public void Triggers(CCSPlayerController? player, CommandInfo command) } i = 1; - foreach (Vector_t bonus in CurrentMap.BonusStartZone) + foreach (VectorT bonus in CurrentMap.BonusStartZone) { - if (bonus.X == 0 && bonus.Y == 0 && bonus.Z == 0) + if (bonus.IsZero()) continue; else { diff --git a/src/ST-Commands/PlayerCommands.cs b/src/ST-Commands/PlayerCommands.cs index cb790ff..5541076 100644 --- a/src/ST-Commands/PlayerCommands.cs +++ b/src/ST-Commands/PlayerCommands.cs @@ -3,6 +3,7 @@ using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Modules.Utils; +using CounterStrikeSharp.API.Modules.Admin; namespace SurfTimer; @@ -14,10 +15,20 @@ public void PlayerReset(CCSPlayerController? player, CommandInfo command) { if (player == null) return; + if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) { - player.ChangeTeam(CsTeam.CounterTerrorist); - player.Respawn(); + Server.NextFrame(() => // Weird CS2 bug that requires doing this twice to show the Joined X team in chat and not stay in limbo + { + player.ChangeTeam(CsTeam.CounterTerrorist); + player.Respawn(); + + player.ChangeTeam(CsTeam.Spectator); + + player.ChangeTeam(CsTeam.CounterTerrorist); + player.Respawn(); + } + ); } Player oPlayer = playerList[player.UserId ?? 0]; @@ -27,23 +38,35 @@ public void PlayerReset(CCSPlayerController? player, CommandInfo command) return; } - // oPlayer.ReplayRecorder.Reset(); - // To-do: players[userid].Timer.Reset() -> teleport player playerList[player.UserId ?? 0].Timer.Reset(); if (!CurrentMap.StartZone.IsZero()) - Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!,CurrentMap.StartZone)); + Server.NextFrame(() => + { + Extensions.Teleport(player.PlayerPawn.Value!, CurrentMap.StartZone); + } + ); } - [ConsoleCommand("css_rs", "Reset back to the start of the stage or bonus you're in.")] + [ConsoleCommand("css_rs", "Reset back to the start of the stage or bonus you were in.")] [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void PlayerResetStage(CCSPlayerController? player, CommandInfo command) { - if (player == null) + if (player == null) return; + if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) { - player.ChangeTeam(CsTeam.CounterTerrorist); - player.Respawn(); + Server.NextFrame(() => // Weird CS2 bug that requires doing this twice to show the Joined X team in chat and not stay in limbo + { + player.ChangeTeam(CsTeam.CounterTerrorist); + player.Respawn(); + + player.ChangeTeam(CsTeam.Spectator); + + player.ChangeTeam(CsTeam.CounterTerrorist); + player.Respawn(); + } + ); } Player oPlayer = playerList[player.UserId ?? 0]; @@ -57,37 +80,31 @@ public void PlayerResetStage(CCSPlayerController? player, CommandInfo command) if (oPlayer.Timer.IsBonusMode) { if (oPlayer.Timer.Bonus != 0 && !CurrentMap.BonusStartZone[oPlayer.Timer.Bonus].IsZero()) - Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value! , CurrentMap.BonusStartZone[oPlayer.Timer.Bonus])); + Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!, CurrentMap.BonusStartZone[oPlayer.Timer.Bonus])); else // Reset back to map start - Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!,CurrentMap.StartZone)); + Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!, CurrentMap.StartZone)); } - else { if (oPlayer.Timer.Stage != 0 && !CurrentMap.StageStartZone[oPlayer.Timer.Stage].IsZero()) Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!, CurrentMap.StageStartZone[oPlayer.Timer.Stage])); else // Reset back to map start - Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!,CurrentMap.StartZone)); + Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!, CurrentMap.StartZone)); } } [ConsoleCommand("css_s", "Teleport to a stage")] [ConsoleCommand("css_stage", "Teleport to a stage")] - [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] + [CommandHelper(minArgs: 1, usage: " [1/2/3]", whoCanExecute: CommandUsage.CLIENT_ONLY)] public void PlayerGoToStage(CCSPlayerController? player, CommandInfo command) { if (player == null) return; - if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) - { - player.ChangeTeam(CsTeam.CounterTerrorist); - player.Respawn(); - } - int stage; + short stage; try { - stage = Int32.Parse(command.ArgByIndex(1)); + stage = short.Parse(command.ArgByIndex(1)); } catch (System.Exception) { @@ -97,25 +114,11 @@ public void PlayerGoToStage(CCSPlayerController? player, CommandInfo command) return; } - // Must be 1 argument - if (command.ArgCount < 2 || stage <= 0) - { -#if DEBUG - player.PrintToChat($"CS2 Surf DEBUG >> css_stage >> Arg#: {command.ArgCount} >> Args: {Int32.Parse(command.ArgByIndex(1))}"); -#endif - - player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_usage", - "!s "]}" - ); - return; - } - - else if (CurrentMap.Stages <= 0) + if (CurrentMap.Stages <= 0) { player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["not_staged"]}"); return; } - else if (stage > CurrentMap.Stages) { player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_stage_value", @@ -128,6 +131,21 @@ public void PlayerGoToStage(CCSPlayerController? player, CommandInfo command) { playerList[player.UserId ?? 0].Timer.Reset(); + if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) + { + Server.NextFrame(() => // Weird CS2 bug that requires doing this twice to show the Joined X team in chat and not stay in limbo + { + player.ChangeTeam(CsTeam.CounterTerrorist); + player.Respawn(); + + player.ChangeTeam(CsTeam.Spectator); + + player.ChangeTeam(CsTeam.CounterTerrorist); + player.Respawn(); + } + ); + } + if (stage == 1) { Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!, CurrentMap.StartZone)); @@ -142,7 +160,6 @@ public void PlayerGoToStage(CCSPlayerController? player, CommandInfo command) // To-do: If you run this while you're in the start zone, endtouch for the start zone runs after you've teleported // causing the timer to start. This needs to be fixed. } - else player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_usage", "!s "]}" @@ -151,42 +168,31 @@ public void PlayerGoToStage(CCSPlayerController? player, CommandInfo command) [ConsoleCommand("css_b", "Teleport to a bonus")] [ConsoleCommand("css_bonus", "Teleport to a bonus")] - [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] + [CommandHelper(minArgs: 1, usage: " [1/2/3]", whoCanExecute: CommandUsage.CLIENT_ONLY)] public void PlayerGoToBonus(CCSPlayerController? player, CommandInfo command) { if (player == null) return; - if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) - { - player.ChangeTeam(CsTeam.CounterTerrorist); - player.Respawn(); - } int bonus; - // Check for argument count - if (command.ArgCount < 2) + try { - if (CurrentMap.Bonuses > 0) - bonus = 1; - else - { - player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_usage", - "!b "]}" - ); - return; - } - } - - else bonus = Int32.Parse(command.ArgByIndex(1)); + } + catch (System.Exception) + { + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_usage", + "!b "]}" + ); + return; + } if (CurrentMap.Bonuses <= 0) { player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["not_bonused"]}"); return; } - else if (bonus > CurrentMap.Bonuses) { player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_bonus_value", @@ -200,9 +206,23 @@ public void PlayerGoToBonus(CCSPlayerController? player, CommandInfo command) playerList[player.UserId ?? 0].Timer.Reset(); playerList[player.UserId ?? 0].Timer.IsBonusMode = true; + if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) + { + Server.NextFrame(() => // Weird CS2 bug that requires doing this twice to show the Joined X team in chat and not stay in limbo + { + player.ChangeTeam(CsTeam.CounterTerrorist); + player.Respawn(); + + player.ChangeTeam(CsTeam.Spectator); + + player.ChangeTeam(CsTeam.CounterTerrorist); + player.Respawn(); + } + ); + } + Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!, CurrentMap.BonusStartZone[bonus])); } - else player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_usage", "!b "]}" @@ -210,12 +230,15 @@ public void PlayerGoToBonus(CCSPlayerController? player, CommandInfo command) } [ConsoleCommand("css_spec", "Moves a player automaticlly into spectator mode")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void MovePlayerToSpectator(CCSPlayerController? player, CommandInfo command) { - if (player == null || player.Team == CsTeam.Spectator) + if (player == null) return; - player.ChangeTeam(CsTeam.Spectator); + Server.NextFrame(() => + player.ChangeTeam(CsTeam.Spectator) + ); } [ConsoleCommand("css_rank", "Show the current rank of the player for the style they are in")] @@ -226,9 +249,9 @@ public void PlayerRank(CCSPlayerController? player, CommandInfo command) return; int pRank = playerList[player.UserId ?? 0].Stats.PB[playerList[player.UserId ?? 0].Timer.Style].Rank; - int tRank = playerList[player.UserId ?? 0].CurrMap.MapCompletions[playerList[player.UserId ?? 0].Timer.Style]; + int tRank = CurrentMap.MapCompletions[playerList[player.UserId ?? 0].Timer.Style]; player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["rank", - CurrentMap.Name, pRank, tRank]}" + CurrentMap.Name!, pRank, tRank]}" ); } @@ -240,6 +263,7 @@ Replay Commands */ [ConsoleCommand("css_replaybotpause", "Pause the replay bot playback")] [ConsoleCommand("css_rbpause", "Pause the replay bot playback")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void PauseReplay(CCSPlayerController? player, CommandInfo command) { if (player == null || player.Team != CsTeam.Spectator) @@ -255,6 +279,7 @@ public void PauseReplay(CCSPlayerController? player, CommandInfo command) } [ConsoleCommand("css_rbplay", "Start all replays from the start")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void PlayReplay(CCSPlayerController? player, CommandInfo command) { if (player == null || player.Team != CsTeam.Spectator) @@ -278,6 +303,7 @@ public void PlayReplay(CCSPlayerController? player, CommandInfo command) [ConsoleCommand("css_replaybotflip", "Flips the replay bot between Forward/Backward playback")] [ConsoleCommand("css_rbflip", "Flips the replay bot between Forward/Backward playback")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void ReverseReplay(CCSPlayerController? player, CommandInfo command) { if (player == null || player.Team != CsTeam.Spectator) @@ -339,6 +365,7 @@ Saveloc Commands ######################## */ [ConsoleCommand("css_saveloc", "Save current player location to be practiced")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void SavePlayerLocation(CCSPlayerController? player, CommandInfo command) { if (player == null) @@ -362,9 +389,9 @@ public void SavePlayerLocation(CCSPlayerController? player, CommandInfo command) p.SavedLocations.Add(new SavelocFrame { - Pos = new Vector_t(player_pos.X, player_pos.Y, player_pos.Z), - Ang = new QAngle_t(player_angle.X, player_angle.Y, player_angle.Z), - Vel = new Vector_t(player_velocity.X, player_velocity.Y, player_velocity.Z), + Pos = new VectorT(player_pos.X, player_pos.Y, player_pos.Z), + Ang = new QAngleT(player_angle.X, player_angle.Y, player_angle.Z), + Vel = new VectorT(player_velocity.X, player_velocity.Y, player_velocity.Z), Tick = p.Timer.Ticks }); p.CurrentSavedLocation = p.SavedLocations.Count - 1; @@ -375,6 +402,7 @@ public void SavePlayerLocation(CCSPlayerController? player, CommandInfo command) } [ConsoleCommand("css_tele", "Teleport player to current saved location")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void TeleportPlayerLocation(CCSPlayerController? player, CommandInfo command) { if (player == null) @@ -403,6 +431,7 @@ public void TeleportPlayerLocation(CCSPlayerController? player, CommandInfo comm } if (command.ArgCount > 1) + { try { int tele_n = int.Parse(command.ArgByIndex(1)); @@ -414,12 +443,14 @@ public void TeleportPlayerLocation(CCSPlayerController? player, CommandInfo comm Exception exception = new("sum ting wong"); throw exception; } + } SavelocFrame location = p.SavedLocations[p.CurrentSavedLocation]; Server.NextFrame(() => - { - Extensions.Teleport(p.Controller.PlayerPawn.Value!, location.Pos, location.Ang, location.Vel); - p.Timer.Ticks = location.Tick; - }); + { + Extensions.Teleport(p.Controller.PlayerPawn.Value!, location.Pos, location.Ang, location.Vel); + p.Timer.Ticks = location.Tick; + } + ); p.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["saveloc_teleported", p.CurrentSavedLocation]}" @@ -427,6 +458,7 @@ public void TeleportPlayerLocation(CCSPlayerController? player, CommandInfo comm } [ConsoleCommand("css_teleprev", "Teleport player to previous saved location")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void TeleportPlayerLocationPrev(CCSPlayerController? player, CommandInfo command) { if (player == null) @@ -462,6 +494,7 @@ public void TeleportPlayerLocationPrev(CCSPlayerController? player, CommandInfo } [ConsoleCommand("css_telenext", "Teleport player to next saved location")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void TeleportPlayerLocationNext(CCSPlayerController? player, CommandInfo command) { if (player == null) @@ -506,78 +539,20 @@ TEST CMDS */ [ConsoleCommand("css_rx", "x")] [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] + [RequiresPermissions("@css/root")] public void TestSituationCmd(CCSPlayerController? player, CommandInfo command) { if (player == null) return; Player oPlayer = playerList[player.UserId ?? 0]; - int style = oPlayer.Timer.Style; - - oPlayer.Stats.ThisRun.PrintSituations(oPlayer); - } - - [ConsoleCommand("css_setpb", "xxxxx")] - [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] - public async void TestSetPb(CCSPlayerController? player, CommandInfo command) - { - if (player == null) - return; - - Player oPlayer = playerList[player.UserId ?? 0]; - int style = oPlayer.Timer.Style; - - await oPlayer.Stats.ThisRun.SaveMapTime(oPlayer, 0, 0, 6666, "TestSetPb"); - // oPlayer.Stats.ThisRun.PrintSituations(oPlayer); - - /* Test Time Saving *//* - if (methodName == "TestSetPb") - { - // 1. Dummy Checkpoint - var dummyCheckpoint = new Checkpoint - { - CP = 1, - Ticks = 1234, - EndTouch = 1, - StartVelX = 111.1f, - StartVelY = 222.2f, - StartVelZ = 333.3f, - EndVelX = 444.4f, - EndVelY = 555.5f, - EndVelZ = 666.6f, - Attempts = 2 - }; - - // 2. Dummy Dictionary за mapTime.Checkpoints - var dummyCheckpointsDict = new Dictionary - { - { dummyCheckpoint.CP, dummyCheckpoint } - }; - - mapTime = new MapTimeDataModel - { - PlayerId = player.Profile.ID, - MapId = player.CurrMap.ID, - Style = player.Timer.Style, - Type = 0, - Stage = stage != 0 ? stage : bonus, - Ticks = 666, - StartVelX = this.StartVelX, - StartVelY = this.StartVelY, - StartVelZ = this.StartVelZ, - EndVelX = this.EndVelX, - EndVelY = this.EndVelY, - EndVelZ = this.EndVelZ, - ReplayFramesBase64 = replay_frames, - Checkpoints = dummyCheckpointsDict - }; - } - /* END Test Time Saving */ + CurrentRun.PrintSituations(oPlayer); } [ConsoleCommand("css_testx", "x")] [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] + [RequiresPermissions("@css/root")] public void TestCmd(CCSPlayerController? player, CommandInfo command) { if (player == null) @@ -586,19 +561,17 @@ public void TestCmd(CCSPlayerController? player, CommandInfo command) Player oPlayer = playerList[player.UserId ?? 0]; int style = oPlayer.Timer.Style; - // player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Testing 'PB_LoadMapTimeData'"); player.PrintToChat($"{Config.PluginPrefix}{ChatColors.Lime}====== PLAYER ======"); player.PrintToChat($"{Config.PluginPrefix} Profile ID: {ChatColors.Green}{oPlayer.Profile.ID}"); player.PrintToChat($"{Config.PluginPrefix} Steam ID: {ChatColors.Green}{oPlayer.Profile.SteamID}"); - player.PrintToChat($"{Config.PluginPrefix} MapTime ID: {ChatColors.Green}{oPlayer.Stats.PB[style].ID} - {PlayerHUD.FormatTime(oPlayer.Stats.PB[style].Ticks)}"); + player.PrintToChat($"{Config.PluginPrefix} MapTime ID: {ChatColors.Green}{oPlayer.Stats.PB[style].ID} - {PlayerHud.FormatTime(oPlayer.Stats.PB[style].RunTime)}"); player.PrintToChat($"{Config.PluginPrefix} Stage: {ChatColors.Green}{oPlayer.Timer.Stage}"); player.PrintToChat($"{Config.PluginPrefix} IsStageMode: {ChatColors.Green}{oPlayer.Timer.IsStageMode}"); player.PrintToChat($"{Config.PluginPrefix} IsRunning: {ChatColors.Green}{oPlayer.Timer.IsRunning}"); player.PrintToChat($"{Config.PluginPrefix} Checkpoint: {ChatColors.Green}{oPlayer.Timer.Checkpoint}"); player.PrintToChat($"{Config.PluginPrefix} Bonus: {ChatColors.Green}{oPlayer.Timer.Bonus}"); player.PrintToChat($"{Config.PluginPrefix} Ticks: {ChatColors.Green}{oPlayer.Timer.Ticks}"); - player.PrintToChat($"{Config.PluginPrefix} StagePB ID: {ChatColors.Green}{oPlayer.Stats.StagePB[1][style].ID} - {PlayerHUD.FormatTime(oPlayer.Stats.StagePB[1][style].Ticks)}"); - // player.PrintToChat($"{Config.PluginPrefix} StagePB ID: {ChatColors.Green}{oPlayer.Stats.StagePB[style][1].ID} - {PlayerHUD.FormatTime(oPlayer.Stats.StagePB[style][1].Ticks)}"); + player.PrintToChat($"{Config.PluginPrefix} StagePB ID: {ChatColors.Green}{oPlayer.Stats.StagePB[1][style].ID} - {PlayerHud.FormatTime(oPlayer.Stats.StagePB[1][style].RunTime)}"); player.PrintToChat($"{Config.PluginPrefix}{ChatColors.Orange}====== MAP ======"); @@ -607,8 +580,8 @@ public void TestCmd(CCSPlayerController? player, CommandInfo command) player.PrintToChat($"{Config.PluginPrefix} Map Stages: {ChatColors.Green}{CurrentMap.Stages}"); player.PrintToChat($"{Config.PluginPrefix} Map Bonuses: {ChatColors.Green}{CurrentMap.Bonuses}"); player.PrintToChat($"{Config.PluginPrefix} Map Completions (Style: {ChatColors.Green}{style}{ChatColors.Default}): {ChatColors.Green}{CurrentMap.MapCompletions[style]}"); - player.PrintToChat($"{Config.PluginPrefix} .CurrentMap.WR[].Ticks: {ChatColors.Green}{CurrentMap.WR[style].Ticks}"); - player.PrintToChat($"{Config.PluginPrefix} .CurrentMap.WR[].Checkpoints.Count: {ChatColors.Green}{CurrentMap.WR[style].Checkpoints.Count}"); + player.PrintToChat($"{Config.PluginPrefix} CurrentMap.WR[{style}].Ticks: {ChatColors.Green}{CurrentMap.WR[style].RunTime}"); + player.PrintToChat($"{Config.PluginPrefix} CurrentMap.WR[{style}].Checkpoints.Count: {ChatColors.Green}{CurrentMap.WR[style].Checkpoints!.Count}"); player.PrintToChat($"{Config.PluginPrefix}{ChatColors.Purple}====== REPLAYS ======"); @@ -625,90 +598,54 @@ public void TestCmd(CCSPlayerController? player, CommandInfo command) player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.BonusWR.Frames.Count: {ChatColors.Green}{CurrentMap.ReplayManager.BonusWR?.Frames.Count}"); player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.BonusWR.IsPlayable: {ChatColors.Green}{CurrentMap.ReplayManager.BonusWR?.IsPlayable}"); player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.BonusWR.IsPlaying: {ChatColors.Green}{CurrentMap.ReplayManager.BonusWR?.IsPlaying}"); - - /* - for (int i = 1; i < SurfTimer.CurrentMap.Stages; i++) - { - player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.AllStageWR[{i}][0].RecordRunTime: {ChatColors.Green}{CurrentMap.ReplayManager.AllStageWR[i][0].RecordRunTime}"); - player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.AllStageWR[{i}][0].Frames.Count: {ChatColors.Green}{CurrentMap.ReplayManager.AllStageWR[i][0].Frames.Count}"); - player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.AllStageWR[{i}][0].IsPlayable: {ChatColors.Green}{CurrentMap.ReplayManager.AllStageWR[i][0].IsPlayable}"); - } - */ - - /* - for (int i = 0; i < CurrentMap.ReplayManager.MapWR.Frames.Count; i++) - { - ReplayFrame x = CurrentMap.ReplayManager.MapWR.Frames[i]; - - switch (x.Situation) - { - case ReplayFrameSituation.START_ZONE_ENTER: - player.PrintToChat($"Start Enter: {i} | Situation {x.Situation}"); - break; - case ReplayFrameSituation.START_ZONE_EXIT: - player.PrintToChat($"Start Exit: {i} | Situation {x.Situation}"); - break; - case ReplayFrameSituation.STAGE_ZONE_ENTER: - player.PrintToChat($"Stage Enter: {i} | Situation {x.Situation}"); - break; - case ReplayFrameSituation.STAGE_ZONE_EXIT: - player.PrintToChat($"Stage Exit: {i} | Situation {x.Situation}"); - break; - case ReplayFrameSituation.CHECKPOINT_ZONE_ENTER: - player.PrintToChat($"Checkpoint Enter: {i} | Situation {x.Situation}"); - break; - case ReplayFrameSituation.CHECKPOINT_ZONE_EXIT: - player.PrintToChat($"Checkpoint Exit: {i} | Situation {x.Situation}"); - break; - } - } - */ - // for (int i = 0; i < CurrentMap.ReplayManager.MapWR.MapSituations.Count; i++) - // { - // ReplayFrame x = CurrentMap.ReplayManager.MapWR.Frames[i]; - // switch (x.Situation) - // { - // case ReplayFrameSituation.START_ZONE_ENTER: - // player.PrintToChat($"START_ZONE_ENTER: {i} | Situation {x.Situation}"); - // break; - // case ReplayFrameSituation.START_ZONE_EXIT: - // player.PrintToChat($"START_ZONE_EXIT: {i} | Situation {x.Situation}"); - // break; - // case ReplayFrameSituation.STAGE_ZONE_ENTER: - // player.PrintToChat($"STAGE_ZONE_ENTER: {i} | Situation {x.Situation}"); - // break; - // case ReplayFrameSituation.STAGE_ZONE_EXIT: - // player.PrintToChat($"STAGE_ZONE_EXIT: {i} | Situation {x.Situation}"); - // break; - // case ReplayFrameSituation.CHECKPOINT_ZONE_ENTER: - // player.PrintToChat($"CHECKPOINT_ZONE_ENTER: {i} | Situation {x.Situation}"); - // break; - // case ReplayFrameSituation.CHECKPOINT_ZONE_EXIT: - // player.PrintToChat($"CHECKPOINT_ZONE_EXIT: {i} | Situation {x.Situation}"); - // break; - // } - // } - - // player.PrintToChat($"{Config.PluginPrefix} IsPlayable: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.IsPlayable}"); - // player.PrintToChat($"{Config.PluginPrefix} IsPlaying: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.IsPlaying}"); - // player.PrintToChat($"{Config.PluginPrefix} Player.IsSpectating: {ChatColors.Green}{oPlayer.IsSpectating(CurrentMap.ReplayManager.MapWR.Controller!)}"); - // player.PrintToChat($"{Config.PluginPrefix} Name & MapTimeID: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.RecordPlayerName} {CurrentMap.ReplayManager.MapWR.MapTimeID}"); - // player.PrintToChat($"{Config.PluginPrefix} ReplayCurrentRunTime: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.ReplayCurrentRunTime}"); - // player.PrintToChat($"{Config.PluginPrefix} RepeatCount: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.RepeatCount}"); - // player.PrintToChat($"{Config.PluginPrefix} IsReplayOutsideZone: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.IsReplayOutsideZone}"); - // player.PrintToChat($"{Config.PluginPrefix} CurrentFrameTick: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.CurrentFrameTick}"); - // player.PrintToChat($"{Config.PluginPrefix} ReplayRecorder.Frames.Length: {ChatColors.Green}{oPlayer.ReplayRecorder.Frames.Count}"); - - // if (CurrentMap.ReplayManager.StageWR != null) - // { - // player.PrintToChat($"{Config.PluginPrefix} ReplayManager.StageWR.MapTimeID - Stage: {ChatColors.Green}{CurrentMap.ReplayManager.StageWR.MapTimeID} - {CurrentMap.ReplayManager.StageWR.Stage}"); - // player.PrintToChat($"{Config.PluginPrefix} ReplayManager.StageWR.IsPlayable: {ChatColors.Green}{CurrentMap.ReplayManager.StageWR.IsPlayable}"); - // player.PrintToChat($"{Config.PluginPrefix} ReplayManager.StageWR.IsEnabled: {ChatColors.Green}{CurrentMap.ReplayManager.StageWR.IsEnabled}"); - // player.PrintToChat($"{Config.PluginPrefix} ReplayManager.StageWR.IsPaused: {ChatColors.Green}{CurrentMap.ReplayManager.StageWR.IsPaused}"); - // player.PrintToChat($"{Config.PluginPrefix} ReplayManager.StageWR.IsPlaying: {ChatColors.Green}{CurrentMap.ReplayManager.StageWR.IsPlaying}"); - // player.PrintToChat($"{Config.PluginPrefix} ReplayManager.StageWR.Controller Null?: {ChatColors.Green}{CurrentMap.ReplayManager.StageWR.Controller == null}"); - // } } - + [ConsoleCommand("css_ctest", "x")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_AND_SERVER)] + [RequiresPermissions("@css/root")] + public void ConsoleTestCmd(CCSPlayerController? player, CommandInfo command) + { + Console.WriteLine("====== MAP INFO ======"); + Console.WriteLine($"Map ID: {CurrentMap.ID}"); + Console.WriteLine($"Map Name: {CurrentMap.Name}"); + Console.WriteLine($"Map Author: {CurrentMap.Author}"); + Console.WriteLine($"Map Tier: {CurrentMap.Tier}"); + Console.WriteLine($"Map Stages: {CurrentMap.Stages}"); + Console.WriteLine($"Map Bonuses: {CurrentMap.Bonuses}"); + Console.WriteLine($"Map Completions: {CurrentMap.MapCompletions[0]}"); + + Console.WriteLine("====== MAP WR INFO ======"); + Console.WriteLine($"Map WR ID: {CurrentMap.WR[0].ID}"); + Console.WriteLine($"Map WR Name: {CurrentMap.WR[0].Name}"); + Console.WriteLine($"Map WR Type: {CurrentMap.WR[0].Type}"); + Console.WriteLine($"Map WR Rank: {CurrentMap.WR[0].Rank}"); + Console.WriteLine($"Map WR Checkpoints.Count: {CurrentMap.WR[0].Checkpoints?.Count}"); + Console.WriteLine($"Map WR ReplayFramesBase64.Length: {CurrentMap.WR[0].ReplayFrames?.ToString().Length}"); + Console.WriteLine($"Map WR ReplayFrames.Length: {CurrentMap.WR[0].ReplayFrames?.ToString().Length}"); + + Console.WriteLine("====== MAP StageWR INFO ======"); + Console.WriteLine($"Map Stage Completions ({CurrentMap.Stages} + 1): {CurrentMap.StageCompletions.Length}"); + Console.WriteLine($"Map StageWR ID: {CurrentMap.StageWR[1][0].ID}"); + Console.WriteLine($"Map StageWR Name: {CurrentMap.StageWR[1][0].Name}"); + Console.WriteLine($"Map StageWR Type: {CurrentMap.StageWR[1][0].Type}"); + Console.WriteLine($"Map StageWR Rank: {CurrentMap.StageWR[1][0].Rank}"); + Console.WriteLine($"Map StageWR ReplayFramesBase64.Length: {CurrentMap.StageWR[1][0].ReplayFrames?.ToString().Length}"); + Console.WriteLine($"Map StageWR ReplayFrames.Length: {CurrentMap.StageWR[1][0].ReplayFrames?.ToString().Length}"); + + Console.WriteLine($"Map Bonus Completions ({CurrentMap.Bonuses} + 1): {CurrentMap.BonusCompletions.Length}"); + + if (CurrentMap.Stages > 0) + { + for (int i = 1; i <= CurrentMap.Stages; i++) + { + Console.WriteLine($"========== Stage {i} =========="); + Console.WriteLine($"ID: {CurrentMap.StageWR[i][0].ID}"); + Console.WriteLine($"Name: {CurrentMap.StageWR[i][0].Name}"); + Console.WriteLine($"RunTime: {CurrentMap.StageWR[i][0].RunTime}"); + Console.WriteLine($"Type: {CurrentMap.StageWR[i][0].Type}"); + Console.WriteLine($"Rank: {CurrentMap.StageWR[i][0].Rank}"); + Console.WriteLine($"Stage Completions: {CurrentMap.StageCompletions[i][0]}"); + } + } + } } \ No newline at end of file diff --git a/src/ST-DB/DB.cs b/src/ST-DB/DB.cs deleted file mode 100644 index 879853d..0000000 --- a/src/ST-DB/DB.cs +++ /dev/null @@ -1,126 +0,0 @@ -using MySqlConnector; - -namespace SurfTimer; - -internal class TimerDatabase -{ - private readonly string _connString; - - public TimerDatabase(string connectionString) - { - _connString = connectionString; - } - - public void Dispose() - { - Close(); - } - - public void Close() - { - // Not needed - } - - /// - /// Spawns a new connection to the database. - /// - /// DB Connection - private MySqlConnection GetConnection() - { - var connection = new MySqlConnection(_connString); - try - { - connection.Open(); - } - catch (MySqlException mysqlEx) // Specifically catch MySQL-related exceptions - { - Console.WriteLine($"[CS2 Surf] MySQL error when connecting: {mysqlEx.Message}"); - throw new InvalidOperationException("Could not establish a connection to the database.", mysqlEx); // Wrap the original exception with additional context - } - catch (System.Exception ex) // Catch all other exceptions - { - Console.WriteLine($"[CS2 Surf] General error when connecting to the database: {ex.Message}"); - throw; // Re-throw the exception without wrapping it - } - - return connection; - } - - /// - /// Always encapsulate the block with `using` when calling this method. - /// That way we ensure the proper disposal of the `MySqlDataReader` when we are finished with it. - /// - /// SELECT query to execute - public async Task QueryAsync(string query) - { - try - { - var connection = GetConnection(); - var cmd = new MySqlCommand(query, connection); - return await cmd.ExecuteReaderAsync(System.Data.CommandBehavior.CloseConnection); - } - catch (Exception ex) - { - Console.WriteLine($"Error executing query {query}: {ex.Message}"); - throw; - } - } - - /// - /// Automatically disposes of the connection and command are disposed of after usage. - /// No need to encapsulate in `using` block. - /// - /// INSERT/UPDATE query to execute - /// rowsInserted, lastInsertedId - public async Task<(int rowsInserted, long lastInsertedId)> WriteAsync(string query) - { - try - { - using var connection = GetConnection(); - using var cmd = new MySqlCommand(query, connection); - var rows = await cmd.ExecuteNonQueryAsync(); - long lastId = cmd.LastInsertedId; // Retrieve the last ID inserted - return (rows, lastId); - } - catch (Exception ex) - { - Console.WriteLine($"Error executing write operation {query}: {ex.Message}"); - throw; - } - } - - /// - /// Begins a transaction and executes it on the database. - /// Used for inputting `Checkpoints` data after a run has been finished. - /// No need to encapsulate in a `using` block, method disposes of connection and data itself. - /// - /// INSERT/UPDATE queries to execute - public async Task TransactionAsync(List commands) - { - // Create a new connection and open it - using var connection = GetConnection(); - - // Begin a transaction on the connection - using var transaction = await connection.BeginTransactionAsync(); - - try - { - // Execute each command within the transaction - foreach (var commandText in commands) - { - using var cmd = new MySqlCommand(commandText, connection, transaction); - await cmd.ExecuteNonQueryAsync(); - } - - // Commit the transaction - await transaction.CommitAsync(); - } - catch - { - // Roll back the transaction if an error occurs - await transaction.RollbackAsync(); - throw; - } - // The connection and transaction are disposed here - } -} diff --git a/src/ST-Events/Players.cs b/src/ST-Events/Players.cs index 08306d2..ead08c4 100644 --- a/src/ST-Events/Players.cs +++ b/src/ST-Events/Players.cs @@ -1,6 +1,7 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes.Registration; +using CounterStrikeSharp.API.Modules.Utils; using MaxMind.GeoIP2; using Microsoft.Extensions.Logging; @@ -12,7 +13,7 @@ public partial class SurfTimer public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) { var controller = @event.Userid; - if (!controller.IsValid || !controller.IsBot || CurrentMap.ReplayManager.IsControllerConnectedToReplayPlayer(controller)) + if (!controller!.IsValid || !controller.IsBot || CurrentMap.ReplayManager.IsControllerConnectedToReplayPlayer(controller)) return HookResult.Continue; _logger.LogTrace("OnPlayerSpawn -> Player {Name} spawned.", @@ -25,6 +26,8 @@ public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) CurrentMap.ReplayManager.MapWR.SetController(controller, -1); CurrentMap.ReplayManager.MapWR.LoadReplayData(); + controller.SwitchTeam(CsTeam.Terrorist); + AddTimer(1.5f, () => { CurrentMap.ReplayManager.MapWR.Controller!.RemoveWeapons(); @@ -41,6 +44,8 @@ public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) CurrentMap.ReplayManager.StageWR.SetController(controller, 3); CurrentMap.ReplayManager.StageWR.LoadReplayData(repeat_count: 3); + controller.SwitchTeam(CsTeam.Terrorist); + AddTimer(1.5f, () => { CurrentMap.ReplayManager.StageWR.Controller!.RemoveWeapons(); @@ -57,6 +62,8 @@ public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) CurrentMap.ReplayManager.BonusWR.SetController(controller, 3); CurrentMap.ReplayManager.BonusWR.LoadReplayData(); + controller.SwitchTeam(CsTeam.Terrorist); + AddTimer(1.5f, () => { CurrentMap.ReplayManager.BonusWR.Controller!.RemoveWeapons(); @@ -93,13 +100,13 @@ public HookResult OnPlayerConnectFull(EventPlayerConnectFull @event, GameEventIn { var player = @event.Userid; - string name = player.PlayerName; - string country = "XX"; + string name = player!.PlayerName; + string country; // GeoIP // Check if the IP is private before attempting GeoIP lookup string ipAddress = player.IpAddress!.Split(":")[0]; - if (!IsPrivateIP(ipAddress)) + if (!Extensions.IsPrivateIP(ipAddress)) { DatabaseReader geoipDB = new(Config.PluginPath + "data/GeoIP/GeoLite2-Country.mmdb"); country = geoipDB.Country(ipAddress).Country.IsoCode ?? "XX"; @@ -109,9 +116,7 @@ public HookResult OnPlayerConnectFull(EventPlayerConnectFull @event, GameEventIn { country = "LL"; // Handle local IP appropriately } - // #if DEBUG - // Console.WriteLine($"CS2 Surf DEBUG >> OnPlayerConnectFull -> GeoIP -> {name} -> {player.IpAddress!.Split(":")[0]} -> {country}"); - // #endif + if (DB == null) { _logger.LogCritical("OnPlayerConnect -> DB object is null, this shouldn't happen."); @@ -119,22 +124,27 @@ public HookResult OnPlayerConnectFull(EventPlayerConnectFull @event, GameEventIn throw ex; } - // Create Player object and add to playerList - PlayerProfile Profile = PlayerProfile.CreateAsync(player.SteamID, name, country).GetAwaiter().GetResult(); - playerList[player.UserId ?? 0] = new Player(player, - new CCSPlayer_MovementServices(player.PlayerPawn.Value!.MovementServices!.Handle), - Profile, CurrentMap); + var profile = PlayerProfile.CreateAsync(player.SteamID, name, country).GetAwaiter().GetResult(); + var movement = new CCSPlayer_MovementServices(player.PlayerPawn.Value!.MovementServices!.Handle); - // Load MapTimes for the player's PB and their Checkpoints - playerList[player.UserId ?? 0].Stats.LoadPlayerMapTimesData(playerList[player.UserId ?? 0]).GetAwaiter().GetResult(); // Holds here until result is available + var p = new Player(player, movement, profile); - // Print join messages - Server.PrintToChatAll($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["player_connected", - name, country]}" - ); - _logger.LogTrace("[{Prefix}] {PlayerName} has connected from {Country}.", - Config.PluginName, name, playerList[player.UserId ?? 0].Profile.Country - ); + // No lock - we use thread-safe method AddOrUpdate + playerList.AddOrUpdate(player.UserId ?? 0, p, (_, _) => p); + + _ = p.Stats.LoadPlayerMapTimesData(p); + + // Go back to the Main Thread for chat message + Server.NextFrame(() => + { + // Print join messages + Server.PrintToChatAll($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["player_connected", + name, country]}" + ); + _logger.LogTrace("[{Prefix}] {PlayerName} has connected from {Country}.", + Config.PluginName, name, playerList[player.UserId ?? 0].Profile.Country + ); + }); return HookResult.Continue; } @@ -174,7 +184,8 @@ public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo if (DB == null) { _logger.LogCritical("OnPlayerDisconnect -> DB object is null, this shouldnt happen."); - throw new Exception("CS2 Surf ERROR >> OnPlayerDisconnect -> DB object is null, this shouldnt happen."); + Exception ex = new("CS2 Surf ERROR >> OnPlayerDisconnect -> DB object is null, this shouldnt happen."); + throw ex; } if (!playerList.ContainsKey(player.UserId ?? 0)) @@ -185,39 +196,16 @@ public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo } else { - // Update data in Player DB table - playerList[player.UserId ?? 0].Profile.UpdatePlayerProfile(player.PlayerName).GetAwaiter().GetResult(); // Hold the thread until player data is updated + int userId = player.UserId ?? 0; - // Remove player data from playerList - playerList.Remove(player.UserId ?? 0); + if (playerList.TryGetValue(userId, out var playerData)) + { + _ = playerData.Profile.UpdatePlayerProfile(player.PlayerName); + playerList.TryRemove(userId, out _); + } } return HookResult.Continue; } } - /// - /// Checks whether an IP is a local one. Allows testing the plugin in a local environment setup for GeoIP - /// - /// IP to check - /// True for Private IP - static bool IsPrivateIP(string ip) - { - var ipParts = ip.Split('.'); - int firstOctet = int.Parse(ipParts[0]); - int secondOctet = int.Parse(ipParts[1]); - - // 10.x.x.x range - if (firstOctet == 10) - return true; - - // 172.16.x.x to 172.31.x.x range - if (firstOctet == 172 && (secondOctet >= 16 && secondOctet <= 31)) - return true; - - // 192.168.x.x range - if (firstOctet == 192 && secondOctet == 168) - return true; - - return false; - } } \ No newline at end of file diff --git a/src/ST-Events/Tick.cs b/src/ST-Events/Tick.cs index fec417e..05b0d5d 100644 --- a/src/ST-Events/Tick.cs +++ b/src/ST-Events/Tick.cs @@ -7,6 +7,9 @@ public partial class SurfTimer { public void OnTick() { + if (CurrentMap == null) + return; + foreach (var player in playerList.Values) { player.Timer.Tick(); @@ -17,12 +20,9 @@ public void OnTick() player.Controller.SetCollisionGroup(CollisionGroup.COLLISION_GROUP_DEBRIS); } - if (CurrentMap == null) - return; - // Need to disable maps from executing their cfgs. Currently idk how (But seriusly it a security issue) ConVar? bot_quota = ConVar.Find("bot_quota"); - // Console.WriteLine($"======== public void OnTick -> bot_quota not null? {bot_quota != null}"); + if (bot_quota != null) { int cbq = bot_quota.GetPrimitiveValue(); @@ -36,8 +36,6 @@ public void OnTick() { bot_quota.SetValue(replaybot_count); } - - // _logger.LogInformation("public void OnTick -> Got bot_quota {cbq} | Setting to bot_quota {replaybot_count}", cbq, replaybot_count); } CurrentMap.ReplayManager.MapWR.Tick(); @@ -60,7 +58,6 @@ public void OnTick() CurrentMap.ReplayManager.AllStageWR[next_stage][0].Controller = CurrentMap.ReplayManager.StageWR.Controller; - // _logger.LogInformation("public void OnTick() -> Finished replay cycle for stage {Stage}, changing to stage {next_stage}", CurrentMap.ReplayManager.StageWR.Stage, next_stage); CurrentMap.ReplayManager.StageWR = CurrentMap.ReplayManager.AllStageWR[next_stage][0]; CurrentMap.ReplayManager.StageWR.LoadReplayData(repeat_count: 3); CurrentMap.ReplayManager.StageWR.FormatBotName(); @@ -77,7 +74,6 @@ public void OnTick() CurrentMap.ReplayManager.AllBonusWR[next_bonus][0].Controller = CurrentMap.ReplayManager.BonusWR.Controller; - // _logger.LogInformation("public void OnTick() -> Finished replay cycle for bonus {Bonus}, changing to bonus {next_bonus}", CurrentMap.ReplayManager.BonusWR.Stage, next_bonus); CurrentMap.ReplayManager.BonusWR = CurrentMap.ReplayManager.AllBonusWR[next_bonus][0]; CurrentMap.ReplayManager.BonusWR.LoadReplayData(repeat_count: 3); CurrentMap.ReplayManager.BonusWR.FormatBotName(); diff --git a/src/ST-Events/TriggerEndTouch.cs b/src/ST-Events/TriggerEndTouch.cs index 73da580..57a3e52 100644 --- a/src/ST-Events/TriggerEndTouch.cs +++ b/src/ST-Events/TriggerEndTouch.cs @@ -1,6 +1,5 @@ -using System.Text.RegularExpressions; using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Utils; +using Microsoft.Extensions.Logging; namespace SurfTimer; @@ -22,9 +21,11 @@ internal HookResult OnTriggerEndTouch(CEntityIOOutput output, string name, CEnti { client = new CCSPlayerController(new CCSPlayerPawn(entity.Handle).Controller.Value!.Handle); } - catch (System.Exception) + catch (Exception ex) { - Console.WriteLine($"===================== [ERROR] OnTriggerEndTouch -> Could not assign `client` (name: {name})"); + _logger.LogError(ex, "[{ClassName}] OnTriggerEndTouch -> Could not assign `client` (name: {Name}). Exception: {Exception}", + nameof(SurfTimer), name, ex.Message + ); } if (client == null || !client.IsValid || client.UserId == -1 || !client.PawnIsAlive || !playerList.ContainsKey((int)client.UserId!)) // `client.IsBot` throws error in server console when going to spectator? + !playerList.ContainsKey((int)client.UserId!) make sure to not check for user_id that doesnt exists @@ -41,153 +42,40 @@ internal HookResult OnTriggerEndTouch(CEntityIOOutput output, string name, CEnti if (trigger.Entity!.Name != null) { - // Get velocities for DB queries - // Get the velocity of the player - we will be using this values to compare and write to DB - Vector_t velocity = player.Controller.PlayerPawn.Value!.AbsVelocity.ToVector_t(); + ZoneType currentZone = GetZoneType(trigger.Entity.Name); - // Map start zones -- hook into map_start, (s)tage1_start - if (trigger.Entity.Name.Contains("map_start") || - trigger.Entity.Name.Contains("s1_start") || - trigger.Entity.Name.Contains("stage1_start")) + switch (currentZone) { - // MAP START ZONE - if (!player.Timer.IsStageMode && !player.Timer.IsBonusMode) - { - player.Timer.Start(); - player.Stats.ThisRun.Ticks = player.Timer.Ticks; - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.START_ZONE_EXIT; - player.ReplayRecorder.MapSituations.Add(player.ReplayRecorder.Frames.Count); - // player.Controller.PrintToChat($"{ChatColors.Red}START_ZONE_EXIT: player.ReplayRecorder.MapSituations.Add({player.ReplayRecorder.Frames.Count})"); - // Console.WriteLine($"START_ZONE_EXIT: player.ReplayRecorder.MapSituations.Add({player.ReplayRecorder.Frames.Count})"); - } - - // Prespeed display - player.Controller.PrintToCenter($"Prespeed: {velocity.velMag():0} u/s"); - player.Stats.ThisRun.StartVelX = velocity.X; // Start pre speed for the Map run - player.Stats.ThisRun.StartVelY = velocity.Y; // Start pre speed for the Map run - player.Stats.ThisRun.StartVelZ = velocity.Z; // Start pre speed for the Map run - -#if DEBUG - player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.LightRed}EndTouchFunc{ChatColors.Default} -> {ChatColors.Green}Map Start Zone"); -#endif - } - - // Map end zones -- hook into map_end - else if (trigger.Entity.Name == "map_end") - { - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.END_ZONE_EXIT; - } - - // Stage start zones -- hook into (s)tage#_start - else if (Regex.Match(trigger.Entity.Name, "^s([1-9][0-9]?|tage[1-9][0-9]?)_start$").Success) - { -#if DEBUG - player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.LightRed}EndTouchFunc{ChatColors.Default} -> {ChatColors.Yellow}Stage {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} Start Zone"); - Console.WriteLine($"===================== player.Timer.Checkpoint {player.Timer.Checkpoint} - player.Stats.ThisRun.Checkpoint.Count {player.Stats.ThisRun.Checkpoints.Count}"); -#endif - - int stage = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value); - - // Set replay situation - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.STAGE_ZONE_EXIT; - player.ReplayRecorder.StageExitSituations.Add(player.ReplayRecorder.Frames.Count); - player.Stats.ThisRun.Ticks = player.Timer.Ticks; - // Console.WriteLine($"STAGE_ZONE_EXIT: player.ReplayRecorder.StageExitSituations.Add({player.ReplayRecorder.Frames.Count})"); - - // Start the Stage timer - if (player.Timer.IsStageMode && player.Timer.Stage == stage) - { - player.Timer.Start(); - // player.Controller.PrintToChat($"{ChatColors.Green}Started{ChatColors.Default} Stage timer for stage {ChatColors.Green}{stage}{ChatColors.Default}"); - - // Show Prespeed for Stages - will be enabled/disabled by the user? - player.Controller.PrintToCenter($"Stage {stage} - Prespeed: {velocity.velMag().ToString("0")} u/s"); - } - else if (player.Timer.IsRunning) - { -#if DEBUG - Console.WriteLine($"currentCheckpoint.EndVelX {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelX} - velocity.X {velocity.X}"); - Console.WriteLine($"currentCheckpoint.EndVelY {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelY} - velocity.Y {velocity.Y}"); - Console.WriteLine($"currentCheckpoint.EndVelZ {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelZ} - velocity.Z {velocity.Z}"); - Console.WriteLine($"currentCheckpoint.Attempts {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].Attempts}"); -#endif - - // Update the Checkpoint object values - player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelX = velocity.X; - player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelY = velocity.Y; - player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelZ = velocity.Z; - player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndTouch = player.Timer.Ticks; - - // Show Prespeed for Checkpoints - will be enabled/disabled by the user? - player.Controller.PrintToCenter($"Checkpoint {player.Timer.Checkpoint} - Prespeed: {velocity.velMag():0} u/s"); - } - } - - // Checkpoint zones -- hook into "^map_c(p[1-9][0-9]?|heckpoint[1-9][0-9]?)$" map_c(heck)p(oint) - else if (Regex.Match(trigger.Entity.Name, "^map_c(p[1-9][0-9]?|heckpoint[1-9][0-9]?)$").Success) - { -#if DEBUG - player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.LightRed}EndTouchFunc{ChatColors.Default} -> {ChatColors.Yellow}Checkpoint {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} Start Zone"); - Console.WriteLine($"===================== player.Timer.Checkpoint {player.Timer.Checkpoint} - player.Stats.ThisRun.Checkpoint.Count {player.Stats.ThisRun.Checkpoints.Count}"); -#endif - - // This will populate the End velocities for the given Checkpoint zone (Stage = Checkpoint when in a Map Run) - if (player.Timer.Checkpoint != 0 && player.Timer.Checkpoint <= player.Stats.ThisRun.Checkpoints.Count) - { -#if DEBUG - Console.WriteLine($"currentCheckpoint.EndVelX {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelX} - velocity.X {velocity.X}"); - Console.WriteLine($"currentCheckpoint.EndVelY {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelY} - velocity.Y {velocity.Y}"); - Console.WriteLine($"currentCheckpoint.EndVelZ {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelZ} - velocity.Z {velocity.Z}"); -#endif - - if (player.Timer.IsRunning && player.ReplayRecorder.IsRecording) - { - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.CHECKPOINT_ZONE_EXIT; - player.ReplayRecorder.CheckpointExitSituations.Add(player.Timer.Ticks); - } - - // Update the Checkpoint object values - player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelX = velocity.X; - player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelY = velocity.Y; - player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelZ = velocity.Z; - player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndTouch = player.Timer.Ticks; - - // Show Prespeed for stages - will be enabled/disabled by the user? - player.Controller.PrintToCenter($"Checkpoint {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} - Prespeed: {velocity.velMag():0} u/s"); - } - } - - // Bonus start zones -- hook into (b)onus#_start - else if (Regex.Match(trigger.Entity.Name, "^b([1-9][0-9]?|onus[1-9][0-9]?)_start$").Success) - { -#if DEBUG - player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.LightRed}EndTouchFunc{ChatColors.Default} -> {ChatColors.Yellow}Bonus {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} Start Zone"); -#endif - - // Replay - if (player.ReplayRecorder.IsRecording) - { - // Saveing 2 seconds before leaving the start zone - player.ReplayRecorder.Frames.RemoveRange(0, Math.Max(0, player.ReplayRecorder.Frames.Count - (64 * 2))); // Todo make a plugin convar for the time saved before start of run - } - - // BONUS START ZONE - if (!player.Timer.IsStageMode && player.Timer.IsBonusMode) - { - player.Timer.Start(); - // Set the CurrentRunData values - player.Stats.ThisRun.Ticks = player.Timer.Ticks; - - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.START_ZONE_EXIT; - player.ReplayRecorder.BonusSituations.Add(player.ReplayRecorder.Frames.Count); - Console.WriteLine($"START_ZONE_EXIT: player.ReplayRecorder.BonusSituations.Add({player.ReplayRecorder.Frames.Count})"); - } - - // Prespeed display - player.Controller.PrintToCenter($"Prespeed: {velocity.velMag():0)} u/s"); - player.Stats.ThisRun.StartVelX = velocity.X; // Start pre speed for the Bonus run - player.Stats.ThisRun.StartVelY = velocity.Y; // Start pre speed for the Bonus run - player.Stats.ThisRun.StartVelZ = velocity.Z; // Start pre speed for the Bonus run + // Map end zones -- hook into map_end + case ZoneType.MapEnd: + EndTouchHandleMapEndZone(player); + break; + // Map start zones -- hook into map_start, (s)tage1_start + case ZoneType.MapStart: + EndTouchHandleMapStartZone(player); + break; + // Stage start zones -- hook into (s)tage#_start + case ZoneType.StageStart: + EndTouchHandleStageStartZone(player, trigger); + break; + // Map checkpoint zones -- hook into map_(c)heck(p)oint# + case ZoneType.Checkpoint: + EndTouchHandleCheckpointZone(player, trigger); + break; + // Bonus start zones -- hook into (b)onus#_start + case ZoneType.BonusStart: + EndTouchHandleBonusStartZone(player, trigger); + break; + // Bonus end zones -- hook into (b)onus#_end + case ZoneType.BonusEnd: + EndTouchHandleBonusEndZone(player); + break; + + default: + _logger.LogError("[{ClassName}] OnTriggerStartTouch -> Unknown MapZone detected in OnTriggerStartTouch. Name: {ZoneName}", + nameof(SurfTimer), trigger.Entity.Name + ); + break; } } diff --git a/src/ST-Events/TriggerStartTouch.cs b/src/ST-Events/TriggerStartTouch.cs index 7e3083d..121e667 100644 --- a/src/ST-Events/TriggerStartTouch.cs +++ b/src/ST-Events/TriggerStartTouch.cs @@ -1,7 +1,5 @@ -using System.Text.RegularExpressions; using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Utils; -using CounterStrikeSharp.API; +using Microsoft.Extensions.Logging; namespace SurfTimer; @@ -10,8 +8,6 @@ public partial class SurfTimer /// /// Handler for trigger start touch hook - CBaseTrigger_StartTouchFunc /// - /// CounterStrikeSharp.API.Core.HookResult - /// internal HookResult OnTriggerStartTouch(CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, float delay) { CBaseTrigger trigger = new CBaseTrigger(caller.Handle); @@ -24,607 +20,69 @@ internal HookResult OnTriggerStartTouch(CEntityIOOutput output, string name, CEn // To-do: Sometimes this triggers before `OnPlayerConnect` and `playerList` does not contain the player how is this possible :thonk: if (!playerList.ContainsKey(client.UserId ?? 0)) { - Console.WriteLine($"CS2 Surf ERROR >> OnTriggerStartTouch -> Init -> Player playerList does NOT contain client.UserId, this shouldn't happen. Player: {client.PlayerName} ({client.UserId})"); - Exception exception = new($"CS2 Surf ERROR >> OnTriggerStartTouch -> Init -> Player playerList does NOT contain client.UserId, this shouldn't happen. Player: {client.PlayerName} ({client.UserId})"); + _logger.LogCritical("[{ClassName}] OnTriggerStartTouch -> Player playerList does NOT contain client.UserId, this shouldn't happen. Player: {PlayerName} ({UserId})", + nameof(SurfTimer), client.PlayerName, client.UserId + ); + + Exception exception = new($"[{nameof(SurfTimer)}] OnTriggerStartTouch -> Init -> Player playerList does NOT contain client.UserId, this shouldn't happen. Player: {client.PlayerName} ({client.UserId})"); throw exception; } // Implement Trigger Start Touch Here Player player = playerList[client.UserId ?? 0]; + #if DEBUG player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc -> {trigger.DesignerName} -> {trigger.Entity!.Name}"); #endif if (DB == null) { - Exception exception = new Exception("CS2 Surf ERROR >> OnTriggerStartTouch (Map end zone) -> DB object is null, this shouldn't happen."); + _logger.LogCritical("[{ClassName}] OnTriggerStartTouch -> DB object is null, this shouldn't happen.", + nameof(SurfTimer) + ); + + Exception exception = new Exception($"[{nameof(SurfTimer)}] OnTriggerStartTouch -> DB object is null, this shouldn't happen."); throw exception; } if (trigger.Entity!.Name != null) { - // Get velocities for DB queries - // Get the velocity of the player - we will be using this values to compare and write to DB - Vector_t velocity = player.Controller.PlayerPawn.Value!.AbsVelocity.ToVector_t(); - int pStyle = player.Timer.Style; - - // Map end zones -- hook into map_end - if (trigger.Entity.Name == "map_end") - { - player.Controller.PrintToCenter($"Map End"); - - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.END_ZONE_ENTER; - player.ReplayRecorder.MapSituations.Add(player.Timer.Ticks); - - player.Stats.ThisRun.Ticks = player.Timer.Ticks; // End time for the Map run - player.Stats.ThisRun.EndVelX = velocity.X; // End speed for the Map run - player.Stats.ThisRun.EndVelY = velocity.Y; // End speed for the Map run - player.Stats.ThisRun.EndVelZ = velocity.Z; // End speed for the Map run - - - // MAP END ZONE - Map RUN - if (player.Timer.IsRunning && !player.Timer.IsStageMode) - { - player.Timer.Stop(); - bool saveMapTime = false; - string PracticeString = ""; - if (player.Timer.IsPracticeMode) - PracticeString = $"({ChatColors.Grey}Practice{ChatColors.Default}) "; - - if (player.Timer.Ticks < CurrentMap.WR[pStyle].Ticks) // Player beat the Map WR - { - saveMapTime = true; - int timeImprove = CurrentMap.WR[pStyle].Ticks - player.Timer.Ticks; - Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["mapwr_improved", - player.Controller.PlayerName, PlayerHUD.FormatTime(player.Timer.Ticks), PlayerHUD.FormatTime(timeImprove), PlayerHUD.FormatTime(CurrentMap.WR[pStyle].Ticks)]}" - ); - } - else if (CurrentMap.WR[pStyle].ID == -1) // No record was set on the map - { - saveMapTime = true; - Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["mapwr_set", - player.Controller.PlayerName, PlayerHUD.FormatTime(player.Timer.Ticks)]}" - ); - } - else if (player.Stats.PB[pStyle].Ticks <= 0) // Player first ever PersonalBest for the map - { - saveMapTime = true; - player.Controller.PrintToChat($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["mappb_set", - PlayerHUD.FormatTime(player.Timer.Ticks)]}" - ); - } - else if (player.Timer.Ticks < player.Stats.PB[pStyle].Ticks) // Player beating their existing PersonalBest for the map - { - saveMapTime = true; - int timeImprove = player.Stats.PB[pStyle].Ticks - player.Timer.Ticks; - Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["mappb_improved", - player.Controller.PlayerName, PlayerHUD.FormatTime(player.Timer.Ticks), PlayerHUD.FormatTime(timeImprove), PlayerHUD.FormatTime(player.Stats.PB[pStyle].Ticks)]}" - ); - } - else // Player did not beat their existing PersonalBest for the map nor the map record - { - player.Controller.PrintToChat($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["mappb_missed", - PlayerHUD.FormatTime(player.Timer.Ticks)]}" - ); - } - - if (saveMapTime) - { - player.ReplayRecorder.IsSaving = true; - AddTimer(1.0f, async () => - { - await player.Stats.ThisRun.SaveMapTime(player); // Save the MapTime PB data - }); - } - -#if DEBUG - Console.WriteLine($@"CS2 Surf DEBUG >> OnTriggerStartTouch (Map end zone) -> - ============== INSERT INTO `MapTimes` - (`player_id`, `map_id`, `style`, `type`, `stage`, `run_time`, `start_vel_x`, `start_vel_y`, `start_vel_z`, `end_vel_x`, `end_vel_y`, `end_vel_z`, `run_date`) - VALUES ({player.Profile.ID}, {CurrentMap.ID}, {pStyle}, 0, 0, {player.Stats.ThisRun.Ticks}, - {player.Stats.ThisRun.StartVelX}, {player.Stats.ThisRun.StartVelY}, {player.Stats.ThisRun.StartVelZ}, {velocity.X}, {velocity.Y}, {velocity.Z}, {(int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()}) - ON DUPLICATE KEY UPDATE run_time=VALUES(run_time), start_vel_x=VALUES(start_vel_x), start_vel_y=VALUES(start_vel_y), - start_vel_z=VALUES(start_vel_z), end_vel_x=VALUES(end_vel_x), end_vel_y=VALUES(end_vel_y), end_vel_z=VALUES(end_vel_z), run_date=VALUES(run_date); - "); -#endif - - // Add entry in DB for the run - if (!player.Timer.IsPracticeMode) - { - // Should we also save a last stage run? - if (CurrentMap.Stages > 0) - { - AddTimer(0.1f, () => - { - // This calculation is wrong unless we wait for a bit in order for the `END_ZONE_ENTER` to be available in the `Frames` object - int stage_run_time = player.ReplayRecorder.Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.END_ZONE_ENTER) - player.ReplayRecorder.Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.STAGE_ZONE_EXIT); - - // player.Controller.PrintToChat($"{Config.PluginPrefix} [LAST StageWR (Map RUN)] Sending to SaveStageTime: {player.Profile.Name}, {CurrentMap.Stages}, {stage_run_time}"); - SaveStageTime(player, CurrentMap.Stages, stage_run_time, true); - }); - } - - // This section checks if the PB is better than WR - if (player.Timer.Ticks < CurrentMap.WR[pStyle].Ticks || CurrentMap.WR[pStyle].ID == -1) - { - AddTimer(2f, () => - { - Console.WriteLine("CS2 Surf DEBUG >> OnTriggerStartTouch (Map end zone) -> WR/PB"); - CurrentMap.ReplayManager.MapWR.Start(); // Start the replay again - CurrentMap.ReplayManager.MapWR.FormatBotName(); - }); - } - - } - - // API - /* - // Add entry in DB for the run - if (!player.Timer.IsPracticeMode) { - API_CurrentRun? last_stage_time = null; - if (CurrentMap.Stages > 0) - { - int last_exit_tick = player.ReplayRecorder.LastExitTick(); - int last_enter_tick = player.ReplayRecorder.LastEnterTick(); - - int stage_run_time = player.ReplayRecorder.Frames.Count - 1 - last_exit_tick; // Would like some check on this - int time_since_last_enter = player.ReplayRecorder.Frames.Count - 1 - last_enter_tick; - - int tt = -1; - if (last_exit_tick - last_enter_tick > 2*64) - tt = last_exit_tick - 2*64; - else - tt = last_enter_tick; - - last_stage_time = new API_CurrentRun - { - player_id = player.Profile.ID, - map_id = player.CurrMap.ID, - style = style, - type = 2, - stage = CurrentMap.Stages, - run_time = stage_run_time, - run_date = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - replay_frames = player.ReplayRecorder.SerializeReplayPortion(tt, time_since_last_enter) - }; - } - AddTimer(1.5f, () => { - List checkpoints = new List(); - foreach (var cp in player.Stats.ThisRun.Checkpoint) - { - checkpoints.Add(new API_Checkpoint - { - cp = cp.Key, - run_time = cp.Value.Ticks, - start_vel_x = cp.Value.StartVelX, - start_vel_y = cp.Value.StartVelY, - start_vel_z = cp.Value.StartVelZ, - end_vel_x = cp.Value.EndVelX, - end_vel_y = cp.Value.EndVelY, - end_vel_z = cp.Value.EndVelZ, - end_touch = 0, // ????? - attempts = cp.Value.Attempts - }); - } - - API_CurrentRun map_time = new API_CurrentRun - { - player_id = player.Profile.ID, - map_id = player.CurrMap.ID, - style = style, - type = 0, - stage = 0, - run_time = player.Stats.ThisRun.Ticks, - run_date = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - checkpoints = checkpoints, - replay_frames = player.ReplayRecorder.SerializeReplay() - }; - - Task.Run(async () => { - System.Console.WriteLine("CS2 Surf DEBUG >> OnTriggerStartTouch (Map end zone) -> Saved map time"); - await ApiCall.POST("/surftimer/savemaptime", map_time); - - if (last_stage_time != null) - { - await ApiCall.POST("/surftimer/savestagetime", last_stage_time); - System.Console.WriteLine("CS2 Surf DEBUG >> OnTriggerStartTouch (Map end zone) -> Saved last stage time"); - player.Stats.LoadStageTime(player); - } - - player.Stats.LoadMapTime(player); - await CurrentMap.ApiGetMapRecordAndTotals(); // Reload the Map record and totals for the HUD - }); - }); - - // This section checks if the PB is better than WR - if(player.Timer.Ticks < CurrentMap.WR[pStyle].Ticks || CurrentMap.WR[pStyle].ID == -1) - { - AddTimer(2f, () => { - System.Console.WriteLine("CS2 Surf DEBUG >> OnTriggerStartTouch (Map end zone) -> WR PB"); - CurrentMap.ReplayManager.MapWR.LoadReplayData(); - - AddTimer(1.5f, () => { - CurrentMap.ReplayManager.MapWR.FormatBotName(); - }); - }); - } - } - */ - } - else if (player.Timer.IsStageMode) - { - player.Timer.Stop(); - - if (!player.Timer.IsPracticeMode) - { - AddTimer(0.1f, () => - { - // This calculation is wrong unless we wait for a bit in order for the `END_ZONE_ENTER` to be available in the `Frames` object - int stage_run_time = player.ReplayRecorder.Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.END_ZONE_ENTER) - player.ReplayRecorder.Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.STAGE_ZONE_EXIT); - - // player.Controller.PrintToChat($"{Config.PluginPrefix} [LAST StageWR (IsStageMode)] Sending to SaveStageTime: {player.Profile.Name}, {CurrentMap.Stages}, {stage_run_time}"); - SaveStageTime(player, CurrentMap.Stages, stage_run_time, true); - }); - } - } - -#if DEBUG - player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.Lime}StartTouchFunc{ChatColors.Default} -> {ChatColors.Red}Map Stop Zone"); -#endif - } - - // Map start zones -- hook into map_start, (s)tage1_start - else if (trigger.Entity.Name.Contains("map_start") || - trigger.Entity.Name.Contains("s1_start") || - trigger.Entity.Name.Contains("stage1_start") - ) - { - // We shouldn't start timer and reset data until MapTime has been saved - mostly concerns the Replays and trimming the correct parts - if (!player.ReplayRecorder.IsSaving) - { - player.ReplayRecorder.Reset(); // Start replay recording - player.ReplayRecorder.Start(); // Start replay recording - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.START_ZONE_ENTER; - - player.ReplayRecorder.MapSituations.Add(player.ReplayRecorder.Frames.Count); - // player.Controller.PrintToChat($"{ChatColors.Green}START_ZONE_ENTER: player.ReplayRecorder.MapSituations.Add({player.ReplayRecorder.Frames.Count})"); - // Console.WriteLine($"START_ZONE_ENTER: player.ReplayRecorder.MapSituations.Add({player.ReplayRecorder.Frames.Count})"); - player.Timer.Reset(); - player.Stats.ThisRun.Checkpoints.Clear(); - player.Controller.PrintToCenter($"Map Start ({trigger.Entity.Name})"); - -#if DEBUG - player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.Lime}StartTouchFunc{ChatColors.Default} -> {ChatColors.Green}Map Start Zone"); -#endif - } - else - { - player.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["reset_delay"]}"); - } - } - - // Stage start zones -- hook into (s)tage#_start - else if (Regex.Match(trigger.Entity.Name, "^s([1-9][0-9]?|tage[1-9][0-9]?)_start$").Success) - { - int stage = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value); - - if (!player.ReplayRecorder.IsRecording) - player.ReplayRecorder.Start(); - - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.STAGE_ZONE_ENTER; - player.ReplayRecorder.StageEnterSituations.Add(player.ReplayRecorder.Frames.Count); - Console.WriteLine($"STAGE_ZONE_ENTER: player.ReplayRecorder.StageEnterSituations.Add({player.ReplayRecorder.Frames.Count})"); - - bool failed_stage = false; - if (player.Timer.Stage == stage) - failed_stage = true; - - // Reset/Stop the Stage timer - // Save a Stage run when `IsStageMode` is active - (`stage - 1` to get the previous stage data) - if (player.Timer.IsStageMode) - { - // player.Controller.PrintToChat($"{Config.PluginPrefix} Player ticks higher than 0? {ChatColors.Yellow}{player.Timer.Ticks > 0}"); - // player.Controller.PrintToChat($"{Config.PluginPrefix} Player time is faster than StageWR time? {ChatColors.Yellow}{player.Timer.Ticks < CurrentMap.StageWR[stage - 1][style].Ticks}"); - // player.Controller.PrintToChat($"{Config.PluginPrefix} No StageWR Exists? {ChatColors.Yellow}{CurrentMap.StageWR[stage - 1][style].ID == -1}"); - // player.Controller.PrintToChat($"{Config.PluginPrefix} Not null? {ChatColors.Yellow}{player.Stats.StagePB[stage - 1][style] != null}"); - // player.Controller.PrintToChat($"{Config.PluginPrefix} Time faster than existing stage PB? {ChatColors.Yellow}{player.Stats.StagePB[stage - 1][style].Ticks > player.Timer.Ticks}"); - if (stage > 1 && !failed_stage && !player.Timer.IsPracticeMode) - { - int stage_run_time = player.Timer.Ticks; - // player.Controller.PrintToChat($"{Config.PluginPrefix} [StageWR (IsStageMode)] Sending to SaveStageTime: {player.Profile.Name}, {stage - 1}, {stage_run_time}"); - SaveStageTime(player, stage - 1, stage_run_time); - } - player.Timer.Reset(); - player.Timer.IsStageMode = true; - // player.Controller.PrintToChat($"{ChatColors.Red}Resetted{ChatColors.Default} Stage timer for stage {ChatColors.Green}{stage}"); - } - - player.Timer.Stage = stage; - -#if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Stage start zones) -> player.Timer.IsRunning: {player.Timer.IsRunning}"); - Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Stage start zones) -> !player.Timer.IsStageMode: {!player.Timer.IsStageMode}"); - Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Stage start zones) -> player.Stats.ThisRun.Checkpoint.Count <= stage: {player.Stats.ThisRun.Checkpoints.Count <= stage}"); -#endif - - // This should patch up re-triggering *player.Stats.ThisRun.Checkpoint.Count < stage* - if (player.Timer.IsRunning && !player.Timer.IsStageMode && player.Stats.ThisRun.Checkpoints.Count < stage) - { - // Save Stage MapTime during a Map run - if (stage > 1 && !failed_stage && !player.Timer.IsPracticeMode) - { - int stage_run_time = player.Timer.Ticks - player.Stats.ThisRun.Ticks; // player.Stats.ThisRun.Ticks should be the Tick we left the previous Stage zone - // player.Controller.PrintToChat($"{Config.PluginPrefix} [StageWR (Map RUN)] Sending to SaveStageTime: {player.Profile.Name}, {stage - 1}, {stage_run_time}"); - SaveStageTime(player, stage - 1, stage_run_time); - } - - player.Timer.Checkpoint = stage - 1; // Stage = Checkpoint when in a run on a Staged map - -#if DEBUG - Console.WriteLine($"============== Initial entity value: {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} | Assigned to `stage`: {stage} | player.Timer.Checkpoint: {stage - 1}"); - Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Stage start zones) -> player.Stats.PB[{pStyle}].Checkpoint.Count = {player.Stats.PB[pStyle].Checkpoints.Count}"); -#endif + ZoneType currentZone = GetZoneType(trigger.Entity.Name); - // Print checkpoint message - player.HUD.DisplayCheckpointMessages(); - - // store the checkpoint in the player's current run checkpoints used for Checkpoint functionality - if (!player.Stats.ThisRun.Checkpoints.ContainsKey(player.Timer.Checkpoint)) - { - Checkpoint cp2 = new Checkpoint(player.Timer.Checkpoint, - player.Timer.Ticks, - velocity.X, - velocity.Y, - velocity.Z, - -1.0f, - -1.0f, - -1.0f, - 0, - 1); - player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint] = cp2; - } - else - { - player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].Attempts++; - } - } - -#if DEBUG - player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.Lime}StartTouchFunc{ChatColors.Default} -> {ChatColors.Yellow}Stage {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} Start Zone"); -#endif - } - - // Map checkpoint zones -- hook into map_(c)heck(p)oint# - else if (Regex.Match(trigger.Entity.Name, "^map_c(p[1-9][0-9]?|heckpoint[1-9][0-9]?)$").Success) + switch (currentZone) { - int checkpoint = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value); - player.Timer.Checkpoint = checkpoint; - - // This should patch up re-triggering *player.Stats.ThisRun.Checkpoint.Count < checkpoint* - if (player.Timer.IsRunning && !player.Timer.IsStageMode && player.Stats.ThisRun.Checkpoints.Count < checkpoint) - { -#if DEBUG - Console.WriteLine($"============== Initial entity value: {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} | Assigned to `checkpoint`: {Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value)}"); - Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Checkpoint zones) -> player.Stats.PB[{pStyle}].Checkpoint.Count = {player.Stats.PB[pStyle].Checkpoints.Count}"); -#endif - - if (player.Timer.IsRunning && player.ReplayRecorder.IsRecording) - { - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.CHECKPOINT_ZONE_ENTER; - player.ReplayRecorder.CheckpointEnterSituations.Add(player.Timer.Ticks); - } - - // Print checkpoint message - player.HUD.DisplayCheckpointMessages(); - - if (!player.Stats.ThisRun.Checkpoints.ContainsKey(checkpoint)) - { - // store the checkpoint in the player's current run checkpoints used for Checkpoint functionality - Checkpoint cp2 = new Checkpoint(checkpoint, - player.Timer.Ticks, - velocity.X, - velocity.Y, - velocity.Z, - -1.0f, - -1.0f, - -1.0f, - 0, - 1); - player.Stats.ThisRun.Checkpoints[checkpoint] = cp2; - } - else - { - player.Stats.ThisRun.Checkpoints[checkpoint].Attempts++; - } - } - -#if DEBUG - player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.Lime}StartTouchFunc{ChatColors.Default} -> {ChatColors.LightBlue}Checkpoint {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} Zone"); -#endif - } - - // Bonus start zones -- hook into (b)onus#_start - else if (Regex.Match(trigger.Entity.Name, "^b([1-9][0-9]?|onus[1-9][0-9]?)_start$").Success) - { - int bonus = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value); - player.Timer.Bonus = bonus; - - player.Timer.Reset(); - player.Timer.IsBonusMode = true; - - - player.ReplayRecorder.Reset(); - player.ReplayRecorder.Start(); // Start replay recording - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.START_ZONE_ENTER; - player.ReplayRecorder.BonusSituations.Add(player.ReplayRecorder.Frames.Count); - Console.WriteLine($"START_ZONE_ENTER: player.ReplayRecorder.BonusSituations.Add({player.ReplayRecorder.Frames.Count})"); - - player.Controller.PrintToCenter($"Bonus Start ({trigger.Entity.Name})"); - -#if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Bonus start zones) -> player.Timer.IsRunning: {player.Timer.IsRunning}"); - Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Bonus start zones) -> !player.Timer.IsBonusMode: {!player.Timer.IsBonusMode}"); -#endif - } - - // Bonus end zones -- hook into (b)onus#_end - else if (Regex.Match(trigger.Entity.Name, "^b([1-9][0-9]?|onus[1-9][0-9]?)_end$").Success && player.Timer.IsBonusMode && player.Timer.IsRunning) - { - // To-do: verify the bonus trigger being hit! - int bonus_idx = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value); - - player.Timer.Stop(); - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.END_ZONE_ENTER; - player.ReplayRecorder.BonusSituations.Add(player.Timer.Ticks); - - player.Stats.ThisRun.Ticks = player.Timer.Ticks; // End time for the run - player.Stats.ThisRun.EndVelX = velocity.X; // End pre speed for the run - player.Stats.ThisRun.EndVelY = velocity.Y; // End pre speed for the run - player.Stats.ThisRun.EndVelZ = velocity.Z; // End pre speed for the run - - bool saveBonusTime = false; - string PracticeString = ""; - if (player.Timer.IsPracticeMode) - PracticeString = $"({ChatColors.Grey}Practice{ChatColors.Default}) "; - - if (player.Timer.Ticks < CurrentMap.BonusWR[bonus_idx][pStyle].Ticks) // Player beat the Bonus WR - { - saveBonusTime = true; - int timeImprove = CurrentMap.BonusWR[bonus_idx][pStyle].Ticks - player.Timer.Ticks; - Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["bonuswr_improved", - player.Controller.PlayerName, bonus_idx, PlayerHUD.FormatTime(player.Timer.Ticks), PlayerHUD.FormatTime(timeImprove), PlayerHUD.FormatTime(CurrentMap.BonusWR[bonus_idx][pStyle].Ticks)]}" - ); - } - else if (CurrentMap.BonusWR[bonus_idx][pStyle].ID == -1) // No Bonus record was set on the map - { - saveBonusTime = true; - Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["bonuswr_set", - player.Controller.PlayerName, bonus_idx, PlayerHUD.FormatTime(player.Timer.Ticks)]}" + // Map end zones -- hook into map_end + case ZoneType.MapEnd: + StartTouchHandleMapEndZone(player); + break; + // Map start zones -- hook into map_start, (s)tage1_start + case ZoneType.MapStart: + StartTouchHandleMapStartZone(player, trigger); + break; + // Stage start zones -- hook into (s)tage#_start + case ZoneType.StageStart: + StartTouchHandleStageStartZone(player, trigger); + break; + // Map checkpoint zones -- hook into map_(c)heck(p)oint# + case ZoneType.Checkpoint: + StartTouchHandleCheckpointZone(player, trigger); + break; + // Bonus start zones -- hook into (b)onus#_start + case ZoneType.BonusStart: + StartTouchHandleBonusStartZone(player, trigger); + break; + // Bonus end zones -- hook into (b)onus#_end + case ZoneType.BonusEnd: + StartTouchHandleBonusEndZone(player, trigger); + break; + + default: + _logger.LogError("[{ClassName}] OnTriggerStartTouch -> Unknown MapZone detected in OnTriggerStartTouch. Name: {ZoneName}", + nameof(SurfTimer), trigger.Entity.Name ); - } - else if (player.Stats.BonusPB[bonus_idx][pStyle].Ticks <= 0) // Player first ever PersonalBest for the bonus - { - saveBonusTime = true; - player.Controller.PrintToChat($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["bonuspb_set", - bonus_idx, PlayerHUD.FormatTime(player.Timer.Ticks)]}" - ); - } - else if (player.Timer.Ticks < player.Stats.BonusPB[bonus_idx][pStyle].Ticks) // Player beating their existing PersonalBest for the bonus - { - saveBonusTime = true; - int timeImprove = player.Stats.BonusPB[bonus_idx][pStyle].Ticks - player.Timer.Ticks; - Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["bonuspb_improved", - player.Controller.PlayerName, bonus_idx, PlayerHUD.FormatTime(player.Timer.Ticks), PlayerHUD.FormatTime(timeImprove), PlayerHUD.FormatTime(player.Stats.PB[pStyle].Ticks)]}" - ); - } - else // Player did not beat their existing personal best for the bonus - { - player.Controller.PrintToChat($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["bonuspb_missed", - bonus_idx, PlayerHUD.FormatTime(player.Timer.Ticks)]}" - ); - } - - // To-do: save to DB - if (!player.Timer.IsPracticeMode) - { - /* - AddTimer(1.5f, () => - { - API_CurrentRun bonus_time = new API_CurrentRun - { - player_id = player.Profile.ID, - map_id = player.CurrMap.ID, - style = pStyle, - type = 1, - stage = bonus_idx, - run_time = player.Stats.ThisRun.Ticks, - run_date = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - replay_frames = player.ReplayRecorder.SerializeReplay() - }; - - Task.Run(async () => - { - await ApiMethod.POST("/surftimer/savebonustime", bonus_time); - player.Stats.LoadBonusTime(player); - await CurrentMap.Get_Map_Record_Runs(); // Reload the Map record and totals for the HUD - // await CurrentMap.ApiGetMapRecordAndTotals(); // Reload the Map record and totals for the HUD - }); - }); - */ - if (saveBonusTime) - { - player.ReplayRecorder.IsSaving = true; - AddTimer(1.0f, async () => - { - await player.Stats.ThisRun.SaveMapTime(player, bonus: bonus_idx); // Save the Bonus MapTime data - }); - } - } + break; } } return HookResult.Continue; } - - /// - /// Deals with saving a Stage MapTime (Type 2) in the Database. - /// Should deal with `IsStageMode` runs, Stages during Map Runs and also Last Stage. - /// - /// Player object - /// Stage to save - /// Is it the last stage? - /// Run Time (Ticks) for the stage run - void SaveStageTime(Player player, int stage = -1, int stage_run_time = -1, bool saveLastStage = false) - { - // player.Controller.PrintToChat($"{Config.PluginPrefix} SaveStageTime received: {player.Profile.Name}, {stage}, {stage_run_time}, {saveLastStage}"); - int pStyle = player.Timer.Style; - if ( - stage_run_time < CurrentMap.StageWR[stage][pStyle].Ticks || - CurrentMap.StageWR[stage][pStyle].ID == -1 || - player.Stats.StagePB[stage][pStyle] != null && player.Stats.StagePB[stage][pStyle].Ticks > stage_run_time || - player.Stats.StagePB[stage][pStyle] != null && player.Stats.StagePB[stage][pStyle].ID == -1 - ) - { - if (stage_run_time < CurrentMap.StageWR[stage][pStyle].Ticks) // Player beat the Stage WR - { - int timeImprove = CurrentMap.StageWR[stage][pStyle].Ticks - stage_run_time; - Server.PrintToChatAll($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["stagewr_improved", - player.Controller.PlayerName, stage, PlayerHUD.FormatTime(stage_run_time), PlayerHUD.FormatTime(timeImprove), PlayerHUD.FormatTime(CurrentMap.StageWR[stage][pStyle].Ticks)]}" - ); - } - else if (CurrentMap.StageWR[stage][pStyle].ID == -1) // No Stage record was set on the map - { - Server.PrintToChatAll($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["stagewr_set", - player.Controller.PlayerName, stage, PlayerHUD.FormatTime(stage_run_time)]}" - ); - } - else if (player.Stats.StagePB[stage][pStyle] != null && player.Stats.StagePB[stage][pStyle].ID == -1) // Player first Stage personal best - { - player.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["stagepb_set", - stage, PlayerHUD.FormatTime(stage_run_time)]}" - ); - } - else if (player.Stats.StagePB[stage][pStyle] != null && player.Stats.StagePB[stage][pStyle].Ticks > stage_run_time) // Player beating their existing Stage personal best - { - int timeImprove = player.Stats.StagePB[stage][pStyle].Ticks - stage_run_time; - Server.PrintToChatAll($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["stagepb_improved", - player.Controller.PlayerName, stage, PlayerHUD.FormatTime(stage_run_time), PlayerHUD.FormatTime(timeImprove), PlayerHUD.FormatTime(player.Stats.StagePB[stage][pStyle].Ticks)]}" - ); - } - - player.ReplayRecorder.IsSaving = true; - AddTimer(1.0f, async () => - { - // Save stage run - Console.WriteLine($"==== OnTriggerStartTouch -> SaveStageTime -> [StageWR (IsStageMode? {player.Timer.IsStageMode} | Last? {saveLastStage})] Saving Stage {stage} ({stage}) time of {PlayerHUD.FormatTime(stage_run_time)} ({stage_run_time})"); - await player.Stats.ThisRun.SaveMapTime(player, stage: stage, run_ticks: stage_run_time); // Save the Stage MapTime PB data - }); - } - else if (stage_run_time > CurrentMap.StageWR[stage][pStyle].Ticks && player.Timer.IsStageMode) // Player is behind the Stage WR for the map - { - int timeImprove = stage_run_time - CurrentMap.StageWR[stage][pStyle].Ticks; - player.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["stagewr_missed", - stage, PlayerHUD.FormatTime(stage_run_time), PlayerHUD.FormatTime(timeImprove), PlayerHUD.FormatTime(CurrentMap.StageWR[stage][pStyle].Ticks)]}" - ); - } - } } diff --git a/src/ST-Events/ZoneEventHandlers.cs b/src/ST-Events/ZoneEventHandlers.cs new file mode 100644 index 0000000..e80c2fb --- /dev/null +++ b/src/ST-Events/ZoneEventHandlers.cs @@ -0,0 +1,582 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Utils; +using SurfTimer.Shared.Entities; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + +namespace SurfTimer; + +public partial class SurfTimer +{ + internal enum ZoneType + { + MapEnd, + MapStart, + StageStart, + Checkpoint, + BonusStart, + BonusEnd, + Unknown + } + + /// + /// Determines the zone type based on the entity name. + /// + /// Name of the entity. + /// ZoneType data + private static ZoneType GetZoneType(string entityName) + { + if (entityName == "map_end") + return ZoneType.MapEnd; + else if (entityName.Contains("map_start") || entityName.Contains("s1_start") || entityName.Contains("stage1_start")) + return ZoneType.MapStart; + else if (Regex.IsMatch(entityName, @"^s([1-9][0-9]?|tage[1-9][0-9]?)_start$")) + return ZoneType.StageStart; + else if (Regex.IsMatch(entityName, @"^map_c(p[1-9][0-9]?|heckpoint[1-9][0-9]?)$")) + return ZoneType.Checkpoint; + else if (Regex.IsMatch(entityName, @"^b([1-9][0-9]?|onus[1-9][0-9]?)_start$")) + return ZoneType.BonusStart; + else if (Regex.IsMatch(entityName, @"^b([1-9][0-9]?|onus[1-9][0-9]?)_end$")) + return ZoneType.BonusEnd; + + return ZoneType.Unknown; + } + + /* StartTouch */ + private void StartTouchHandleMapEndZone(Player player, [CallerMemberName] string methodName = "") + { + // Get velocities for DB queries + // Get the velocity of the player - we will be using this values to compare and write to DB + VectorT velocity = player.Controller.PlayerPawn.Value!.AbsVelocity.ToVector_t(); + int pStyle = player.Timer.Style; + + player.Controller.PrintToCenter($"Map End"); + + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.END_ZONE_ENTER; + player.ReplayRecorder.MapSituations.Add(player.Timer.Ticks); + + player.Stats.ThisRun.RunTime = player.Timer.Ticks; // End time for the Map run + player.Stats.ThisRun.EndVelX = velocity.X; // End speed for the Map run + player.Stats.ThisRun.EndVelY = velocity.Y; // End speed for the Map run + player.Stats.ThisRun.EndVelZ = velocity.Z; // End speed for the Map run + + + // MAP END ZONE - Map RUN + if (player.Timer.IsRunning && !player.Timer.IsStageMode) + { + player.Timer.Stop(); + bool saveMapTime = false; + string PracticeString = ""; + if (player.Timer.IsPracticeMode) + PracticeString = $"({ChatColors.Grey}Practice{ChatColors.Default}) "; + + if (player.Timer.Ticks < CurrentMap.WR[pStyle].RunTime) // Player beat the Map WR + { + saveMapTime = true; + int timeImprove = CurrentMap.WR[pStyle].RunTime - player.Timer.Ticks; + Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["mapwr_improved", + player.Controller.PlayerName, PlayerHud.FormatTime(player.Timer.Ticks), PlayerHud.FormatTime(timeImprove), PlayerHud.FormatTime(CurrentMap.WR[pStyle].RunTime)]}" + ); + } + else if (CurrentMap.WR[pStyle].ID == -1) // No record was set on the map + { + saveMapTime = true; + Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["mapwr_set", + player.Controller.PlayerName, PlayerHud.FormatTime(player.Timer.Ticks)]}" + ); + } + else if (player.Stats.PB[pStyle].RunTime <= 0) // Player first ever PersonalBest for the map + { + saveMapTime = true; + player.Controller.PrintToChat($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["mappb_set", + PlayerHud.FormatTime(player.Timer.Ticks)]}" + ); + } + else if (player.Timer.Ticks < player.Stats.PB[pStyle].RunTime) // Player beating their existing PersonalBest for the map + { + saveMapTime = true; + int timeImprove = player.Stats.PB[pStyle].RunTime - player.Timer.Ticks; + Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["mappb_improved", + player.Controller.PlayerName, PlayerHud.FormatTime(player.Timer.Ticks), PlayerHud.FormatTime(timeImprove), PlayerHud.FormatTime(player.Stats.PB[pStyle].RunTime)]}" + ); + } + else // Player did not beat their existing PersonalBest for the map nor the map record + { + player.Controller.PrintToChat($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["mappb_missed", + PlayerHud.FormatTime(player.Timer.Ticks)]}" + ); + } + + if (saveMapTime && !player.Timer.IsPracticeMode) + { + player.ReplayRecorder.IsSaving = true; + AddTimer(1.0f, async () => // This determines whether we will have frames for AFTER touch the endZone + { + await player.Stats.ThisRun.SaveMapTime(player); // Save the MapTime PB data + }); + } + + // Add entry in DB for the run + if (!player.Timer.IsPracticeMode) + { + // Should we also save a last stage run? + if (CurrentMap.Stages > 0) + { + AddTimer(1.0f, async () => // This determines whether we will have frames for AFTER touch the endZone + { + // This calculation is wrong unless we wait for a bit in order for the `END_ZONE_ENTER` to be available in the `Frames` object + int stage_run_time = player.ReplayRecorder.Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.END_ZONE_ENTER) - player.ReplayRecorder.Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.STAGE_ZONE_EXIT); + + await CurrentRun.SaveStageTime(player, CurrentMap.Stages, stage_run_time, true); + }); + } + + // This section checks if the PB is better than WR + if (player.Timer.Ticks < CurrentMap.WR[pStyle].RunTime || CurrentMap.WR[pStyle].ID == -1) + { + AddTimer(2f, () => + { + Console.WriteLine("CS2 Surf DEBUG >> OnTriggerStartTouch (Map end zone) -> WR/PB"); + CurrentMap.ReplayManager.MapWR.Start(); // Start the replay again + CurrentMap.ReplayManager.MapWR.FormatBotName(); + }); + } + + } + } + // MAP END ZONE - Stage RUN + else if (player.Timer.IsStageMode) + { + player.Timer.Stop(); + + if (!player.Timer.IsPracticeMode) + { + AddTimer(1.0f, async () => // This determines whether we will have frames for AFTER touch the endZone + { + // This calculation is wrong unless we wait for a bit in order for the `END_ZONE_ENTER` to be available in the `Frames` object + int stage_run_time = player.ReplayRecorder.Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.END_ZONE_ENTER) - player.ReplayRecorder.Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.STAGE_ZONE_EXIT); + + await CurrentRun.SaveStageTime(player, CurrentMap.Stages, stage_run_time, true); + }); + } + } + +#if DEBUG + player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.Lime}StartTouchFunc{ChatColors.Default} -> {ChatColors.Red}Map Stop Zone"); +#endif + } + + private static void StartTouchHandleMapStartZone(Player player, CBaseTrigger trigger, [CallerMemberName] string methodName = "") + { + // We shouldn't start timer and reset data until MapTime has been saved - mostly concerns the Replays and trimming the correct parts + if (!player.ReplayRecorder.IsSaving) + { + player.ReplayRecorder.Reset(); // Start replay recording + player.ReplayRecorder.Start(); // Start replay recording + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.START_ZONE_ENTER; + player.ReplayRecorder.MapSituations.Add(player.ReplayRecorder.Frames.Count); + player.Timer.Reset(); + player.Stats.ThisRun.Checkpoints.Clear(); + player.Controller.PrintToCenter($"Map Start ({trigger.Entity!.Name})"); + +#if DEBUG + player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.Lime}StartTouchFunc{ChatColors.Default} -> {ChatColors.Green}Map Start Zone"); +#endif + } + else + { + player.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["reset_delay"]}"); + } + } + + private void StartTouchHandleStageStartZone(Player player, CBaseTrigger trigger, [CallerMemberName] string methodName = "") + { + // Get velocities for DB queries + // Get the velocity of the player - we will be using this values to compare and write to DB + VectorT velocity = player.Controller.PlayerPawn.Value!.AbsVelocity.ToVector_t(); + short stage = short.Parse(Regex.Match(trigger.Entity!.Name, "[0-9][0-9]?").Value); + + if (!player.ReplayRecorder.IsRecording) + player.ReplayRecorder.Start(); + + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.STAGE_ZONE_ENTER; + player.ReplayRecorder.StageEnterSituations.Add(player.ReplayRecorder.Frames.Count); + + bool failed_stage = false; + if (player.Timer.Stage == stage) + failed_stage = true; + + // Reset/Stop the Stage timer + // Save a Stage run when `IsStageMode` is active - (`stage - 1` to get the previous stage data) + if (player.Timer.IsStageMode) + { + if (stage > 1 && !failed_stage && !player.Timer.IsPracticeMode) + { + int stage_run_time = player.Timer.Ticks; + AddTimer(1.0f, async () => // This determines whether we will have frames for AFTER touch the endZone + { + await CurrentRun.SaveStageTime(player, (short)(stage - 1), stage_run_time); + }); + } + player.Timer.Reset(); + player.Timer.IsStageMode = true; + } + + player.Timer.Stage = stage; + +#if DEBUG + Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Stage start zones) -> player.Timer.IsRunning: {player.Timer.IsRunning}"); + Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Stage start zones) -> !player.Timer.IsStageMode: {!player.Timer.IsStageMode}"); + Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Stage start zones) -> player.Stats.ThisRun.Checkpoint.Count <= stage: {player.Stats.ThisRun.Checkpoints.Count <= stage}"); +#endif + + // This should patch up re-triggering *player.Stats.ThisRun.Checkpoint.Count < stage* + if (player.Timer.IsRunning && !player.Timer.IsStageMode && player.Stats.ThisRun.Checkpoints.Count < stage) + { + // Save Stage MapTime during a Map run + if (stage > 1 && !failed_stage && !player.Timer.IsPracticeMode) + { + int stage_run_time = player.Timer.Ticks - player.Stats.ThisRun.RunTime; // player.Stats.ThisRun.RunTime should be the Tick we left the previous Stage zone + + AddTimer(1.0f, async () => // This determines whether we will have frames for AFTER touch the endZone + { + await CurrentRun.SaveStageTime(player, (short)(stage - 1), stage_run_time); + }); + } + + player.Timer.Checkpoint = (short)(stage - 1); // Stage = Checkpoint when in a run on a Staged map + +#if DEBUG + Console.WriteLine($"============== Initial entity value: {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} | Assigned to `stage`: {stage} | player.Timer.Checkpoint: {stage - 1}"); + Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Stage start zones) -> player.Stats.PB[{pStyle}].Checkpoint.Count = {player.Stats.PB[pStyle].Checkpoints.Count}"); +#endif + + // Print checkpoint message + player.HUD.DisplayCheckpointMessages(); + + // store the checkpoint in the player's current run checkpoints used for Checkpoint functionality + if (!player.Stats.ThisRun.Checkpoints.ContainsKey(player.Timer.Checkpoint)) + { + var cp2 = new CheckpointEntity(player.Timer.Checkpoint, + player.Timer.Ticks, + velocity.X, + velocity.Y, + velocity.Z, + -1.0f, + -1.0f, + -1.0f, + 0, + 1); + player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint] = cp2; + } + else + { + player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].Attempts++; + } + } + +#if DEBUG + player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.Lime}StartTouchFunc{ChatColors.Default} -> {ChatColors.Yellow}Stage {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} Start Zone"); +#endif + } + + private static void StartTouchHandleCheckpointZone(Player player, CBaseTrigger trigger, [CallerMemberName] string methodName = "") + { + // Get velocities for DB queries + // Get the velocity of the player - we will be using this values to compare and write to DB + VectorT velocity = player.Controller.PlayerPawn.Value!.AbsVelocity.ToVector_t(); + short checkpoint = short.Parse(Regex.Match(trigger.Entity!.Name, "[0-9][0-9]?").Value); + player.Timer.Checkpoint = checkpoint; + + // This should patch up re-triggering *player.Stats.ThisRun.Checkpoint.Count < checkpoint* + if (player.Timer.IsRunning && !player.Timer.IsStageMode && player.Stats.ThisRun.Checkpoints.Count < checkpoint) + { +#if DEBUG + int pStyle = player.Timer.Style; + Console.WriteLine($"============== Initial entity value: {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} | Assigned to `checkpoint`: {Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value)}"); + Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Checkpoint zones) -> player.Stats.PB[{pStyle}].Checkpoint.Count = {player.Stats.PB[pStyle].Checkpoints.Count}"); +#endif + + if (player.Timer.IsRunning && player.ReplayRecorder.IsRecording) + { + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.CHECKPOINT_ZONE_ENTER; + player.ReplayRecorder.CheckpointEnterSituations.Add(player.Timer.Ticks); + } + + // Print checkpoint message + player.HUD.DisplayCheckpointMessages(); + + if (!player.Stats.ThisRun.Checkpoints.ContainsKey(checkpoint)) + { + // store the checkpoint in the player's current run checkpoints used for Checkpoint functionality + var cp2 = new CheckpointEntity(checkpoint, + player.Timer.Ticks, + velocity.X, + velocity.Y, + velocity.Z, + -1.0f, + -1.0f, + -1.0f, + 0, + 1); + player.Stats.ThisRun.Checkpoints[checkpoint] = cp2; + } + else + { + player.Stats.ThisRun.Checkpoints[checkpoint].Attempts++; + } + } + +#if DEBUG + player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.Lime}StartTouchFunc{ChatColors.Default} -> {ChatColors.LightBlue}Checkpoint {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} Zone"); +#endif + } + + private static void StartTouchHandleBonusStartZone(Player player, CBaseTrigger trigger, [CallerMemberName] string methodName = "") + { + short bonus = short.Parse(Regex.Match(trigger.Entity!.Name, "[0-9][0-9]?").Value); + player.Timer.Bonus = bonus; + + player.Timer.Reset(); + player.Timer.IsBonusMode = true; + + + player.ReplayRecorder.Reset(); + player.ReplayRecorder.Start(); // Start replay recording + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.START_ZONE_ENTER; + player.ReplayRecorder.BonusSituations.Add(player.ReplayRecorder.Frames.Count); + Console.WriteLine($"START_ZONE_ENTER: player.ReplayRecorder.BonusSituations.Add({player.ReplayRecorder.Frames.Count})"); + + player.Controller.PrintToCenter($"Bonus Start ({trigger.Entity.Name})"); + +#if DEBUG + Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Bonus start zones) -> player.Timer.IsRunning: {player.Timer.IsRunning}"); + Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Bonus start zones) -> !player.Timer.IsBonusMode: {!player.Timer.IsBonusMode}"); +#endif + } + + private void StartTouchHandleBonusEndZone(Player player, CBaseTrigger trigger, [CallerMemberName] string methodName = "") + { + // Get velocities for DB queries + // Get the velocity of the player - we will be using this values to compare and write to DB + VectorT velocity = player.Controller.PlayerPawn.Value!.AbsVelocity.ToVector_t(); + int pStyle = player.Timer.Style; + // To-do: verify the bonus trigger being hit! + short bonus_idx = short.Parse(Regex.Match(trigger.Entity!.Name, "[0-9][0-9]?").Value); + + player.Timer.Stop(); + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.END_ZONE_ENTER; + player.ReplayRecorder.BonusSituations.Add(player.Timer.Ticks); + + player.Stats.ThisRun.RunTime = player.Timer.Ticks; // End time for the run + player.Stats.ThisRun.EndVelX = velocity.X; // End pre speed for the run + player.Stats.ThisRun.EndVelY = velocity.Y; // End pre speed for the run + player.Stats.ThisRun.EndVelZ = velocity.Z; // End pre speed for the run + + bool saveBonusTime = false; + string PracticeString = ""; + if (player.Timer.IsPracticeMode) + PracticeString = $"({ChatColors.Grey}Practice{ChatColors.Default}) "; + + if (player.Timer.Ticks < CurrentMap.BonusWR[bonus_idx][pStyle].RunTime) // Player beat the Bonus WR + { + saveBonusTime = true; + int timeImprove = CurrentMap.BonusWR[bonus_idx][pStyle].RunTime - player.Timer.Ticks; + Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["bonuswr_improved", + player.Controller.PlayerName, bonus_idx, PlayerHud.FormatTime(player.Timer.Ticks), PlayerHud.FormatTime(timeImprove), PlayerHud.FormatTime(CurrentMap.BonusWR[bonus_idx][pStyle].RunTime)]}" + ); + } + else if (CurrentMap.BonusWR[bonus_idx][pStyle].ID == -1) // No Bonus record was set on the map + { + saveBonusTime = true; + Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["bonuswr_set", + player.Controller.PlayerName, bonus_idx, PlayerHud.FormatTime(player.Timer.Ticks)]}" + ); + } + else if (player.Stats.BonusPB[bonus_idx][pStyle].RunTime <= 0) // Player first ever PersonalBest for the bonus + { + saveBonusTime = true; + player.Controller.PrintToChat($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["bonuspb_set", + bonus_idx, PlayerHud.FormatTime(player.Timer.Ticks)]}" + ); + } + else if (player.Timer.Ticks < player.Stats.BonusPB[bonus_idx][pStyle].RunTime) // Player beating their existing PersonalBest for the bonus + { + saveBonusTime = true; + int timeImprove = player.Stats.BonusPB[bonus_idx][pStyle].RunTime - player.Timer.Ticks; + Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["bonuspb_improved", + player.Controller.PlayerName, bonus_idx, PlayerHud.FormatTime(player.Timer.Ticks), PlayerHud.FormatTime(timeImprove), PlayerHud.FormatTime(player.Stats.PB[pStyle].RunTime)]}" + ); + } + else // Player did not beat their existing personal best for the bonus + { + player.Controller.PrintToChat($"{Config.PluginPrefix} {PracticeString}{LocalizationService.LocalizerNonNull["bonuspb_missed", + bonus_idx, PlayerHud.FormatTime(player.Timer.Ticks)]}" + ); + } + + if (!player.Timer.IsPracticeMode) + { + if (saveBonusTime) + { + player.ReplayRecorder.IsSaving = true; + AddTimer(1.0f, async () => // This determines whether we will have frames for AFTER touch the endZone + { + await player.Stats.ThisRun.SaveMapTime(player, bonus: bonus_idx); // Save the Bonus MapTime data + }); + } + } + } + + + /* EndTouch */ + private static void EndTouchHandleMapEndZone(Player player, [CallerMemberName] string methodName = "") + { + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.END_ZONE_EXIT; + } + + private static void EndTouchHandleMapStartZone(Player player, [CallerMemberName] string methodName = "") + { + VectorT velocity = player.Controller.PlayerPawn.Value!.AbsVelocity.ToVector_t(); + + // MAP START ZONE + if (!player.Timer.IsStageMode && !player.Timer.IsBonusMode) + { + player.Timer.Start(); + player.Stats.ThisRun.RunTime = player.Timer.Ticks; + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.START_ZONE_EXIT; + player.ReplayRecorder.MapSituations.Add(player.ReplayRecorder.Frames.Count); +#if DEBUG + player.Controller.PrintToChat($"{ChatColors.Red}START_ZONE_EXIT: player.ReplayRecorder.MapSituations.Add({player.ReplayRecorder.Frames.Count})"); + Console.WriteLine($"START_ZONE_EXIT: player.ReplayRecorder.MapSituations.Add({player.ReplayRecorder.Frames.Count})"); +#endif + } + + // Prespeed display + player.Controller.PrintToCenter($"Prespeed: {velocity.velMag():0} u/s"); + player.Stats.ThisRun.StartVelX = velocity.X; // Start pre speed for the Map run + player.Stats.ThisRun.StartVelY = velocity.Y; // Start pre speed for the Map run + player.Stats.ThisRun.StartVelZ = velocity.Z; // Start pre speed for the Map run + +#if DEBUG + player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.LightRed}EndTouchFunc{ChatColors.Default} -> {ChatColors.Green}Map Start Zone"); +#endif + } + + private static void EndTouchHandleStageStartZone(Player player, CBaseTrigger trigger, [CallerMemberName] string methodName = "") + { +#if DEBUG + player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.LightRed}EndTouchFunc{ChatColors.Default} -> {ChatColors.Yellow}Stage {Regex.Match(trigger.Entity!.Name, "[0-9][0-9]?").Value} Start Zone"); + Console.WriteLine($"===================== player.Timer.Checkpoint {player.Timer.Checkpoint} - player.Stats.ThisRun.Checkpoint.Count {player.Stats.ThisRun.Checkpoints.Count}"); +#endif + VectorT velocity = player.Controller.PlayerPawn.Value!.AbsVelocity.ToVector_t(); + int stage = Int32.Parse(Regex.Match(trigger.Entity!.Name, "[0-9][0-9]?").Value); + + // Set replay situation + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.STAGE_ZONE_EXIT; + player.ReplayRecorder.StageExitSituations.Add(player.ReplayRecorder.Frames.Count); + player.Stats.ThisRun.RunTime = player.Timer.Ticks; + + // Start the Stage timer + if (player.Timer.IsStageMode && player.Timer.Stage == stage) + { + player.Timer.Start(); + + // Show Prespeed for Stages - will be enabled/disabled by the user? + player.Controller.PrintToCenter($"Stage {stage} - Prespeed: {velocity.velMag().ToString("0")} u/s"); + } + else if (player.Timer.IsRunning) + { +#if DEBUG + Console.WriteLine($"currentCheckpoint.EndVelX {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelX} - velocity.X {velocity.X}"); + Console.WriteLine($"currentCheckpoint.EndVelY {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelY} - velocity.Y {velocity.Y}"); + Console.WriteLine($"currentCheckpoint.EndVelZ {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelZ} - velocity.Z {velocity.Z}"); + Console.WriteLine($"currentCheckpoint.Attempts {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].Attempts}"); +#endif + + // Update the Checkpoint object values + player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelX = velocity.X; + player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelY = velocity.Y; + player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelZ = velocity.Z; + player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndTouch = player.Timer.Ticks; + + // Show Prespeed for Checkpoints - will be enabled/disabled by the user? + player.Controller.PrintToCenter($"Checkpoint {player.Timer.Checkpoint} - Prespeed: {velocity.velMag():0} u/s"); + } + } + + private static void EndTouchHandleCheckpointZone(Player player, CBaseTrigger trigger, [CallerMemberName] string methodName = "") + { +#if DEBUG + player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.LightRed}EndTouchFunc{ChatColors.Default} -> {ChatColors.Yellow}Checkpoint {Regex.Match(trigger.Entity!.Name, "[0-9][0-9]?").Value} Start Zone"); + Console.WriteLine($"===================== player.Timer.Checkpoint {player.Timer.Checkpoint} - player.Stats.ThisRun.Checkpoint.Count {player.Stats.ThisRun.Checkpoints.Count}"); +#endif + VectorT velocity = player.Controller.PlayerPawn.Value!.AbsVelocity.ToVector_t(); + + // This will populate the End velocities for the given Checkpoint zone (Stage = Checkpoint when in a Map Run) + if (player.Timer.Checkpoint != 0 && player.Timer.Checkpoint <= player.Stats.ThisRun.Checkpoints.Count) + { +#if DEBUG + Console.WriteLine($"currentCheckpoint.EndVelX {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelX} - velocity.X {velocity.X}"); + Console.WriteLine($"currentCheckpoint.EndVelY {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelY} - velocity.Y {velocity.Y}"); + Console.WriteLine($"currentCheckpoint.EndVelZ {player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelZ} - velocity.Z {velocity.Z}"); +#endif + + if (player.Timer.IsRunning && player.ReplayRecorder.IsRecording) + { + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.CHECKPOINT_ZONE_EXIT; + player.ReplayRecorder.CheckpointExitSituations.Add(player.Timer.Ticks); + } + + // Update the Checkpoint object values + player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelX = velocity.X; + player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelY = velocity.Y; + player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndVelZ = velocity.Z; + player.Stats.ThisRun.Checkpoints[player.Timer.Checkpoint].EndTouch = player.Timer.Ticks; + + // Show Prespeed for stages - will be enabled/disabled by the user? + player.Controller.PrintToCenter($"Checkpoint {Regex.Match(trigger.Entity!.Name, "[0-9][0-9]?").Value} - Prespeed: {velocity.velMag():0} u/s"); + } + } + + private static void EndTouchHandleBonusStartZone(Player player, CBaseTrigger trigger, [CallerMemberName] string methodName = "") + { +#if DEBUG + player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.LightRed}EndTouchFunc{ChatColors.Default} -> {ChatColors.Yellow}Bonus {Regex.Match(trigger.Entity!.Name, "[0-9][0-9]?").Value} Start Zone"); +#endif + VectorT velocity = player.Controller.PlayerPawn.Value!.AbsVelocity.ToVector_t(); + + // Replay + if (player.ReplayRecorder.IsRecording) + { + // Saving 2 seconds before leaving the start zone + player.ReplayRecorder.Frames.RemoveRange(0, Math.Max(0, player.ReplayRecorder.Frames.Count - (Config.ReplaysPre * 2))); + } + + // BONUS START ZONE + if (!player.Timer.IsStageMode && player.Timer.IsBonusMode) + { + player.Timer.Start(); + // Set the CurrentRunData values + player.Stats.ThisRun.RunTime = player.Timer.Ticks; + + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.START_ZONE_EXIT; + player.ReplayRecorder.BonusSituations.Add(player.ReplayRecorder.Frames.Count); +#if DEBUG + Console.WriteLine($"START_ZONE_EXIT: player.ReplayRecorder.BonusSituations.Add({player.ReplayRecorder.Frames.Count})"); +#endif + } + + // Prespeed display + player.Controller.PrintToCenter($"Prespeed: {velocity.velMag():0)} u/s"); + player.Stats.ThisRun.StartVelX = velocity.X; // Start pre speed for the Bonus run + player.Stats.ThisRun.StartVelY = velocity.Y; // Start pre speed for the Bonus run + player.Stats.ThisRun.StartVelZ = velocity.Z; // Start pre speed for the Bonus run + } + + private static void EndTouchHandleBonusEndZone(Player player, [CallerMemberName] string methodName = "") + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/ST-Map/Map.cs b/src/ST-Map/Map.cs index 40e621e..31f5f7f 100644 --- a/src/ST-Map/Map.cs +++ b/src/ST-Map/Map.cs @@ -1,30 +1,23 @@ -using System.Data; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.RegularExpressions; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SurfTimer.Data; +using SurfTimer.Shared.DTO; +using SurfTimer.Shared.Entities; +using SurfTimer.Shared.Types; +using System.Data; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.RegularExpressions; namespace SurfTimer; -internal class Map +public class Map : MapEntity { - // Map information - public int ID { get; set; } = -1; // Can we use this to re-trigger retrieving map information from the database?? (all db IDs are auto-incremented) - public string Name { get; set; } = string.Empty; - public string Author { get; set; } = ""; - public int Tier { get; set; } = 0; - public int Stages { get; set; } = 0; public int TotalCheckpoints { get; set; } = 0; - public int Bonuses { get; set; } = 0; - public bool Ranked { get; set; } = false; - public int DateAdded { get; set; } = 0; - public int LastPlayed { get; set; } = 0; /// /// Map Completion Count - Refer to as MapCompletions[style] /// @@ -32,11 +25,11 @@ internal class Map /// /// Bonus Completion Count - Refer to as BonusCompletions[bonus#][style] /// - public Dictionary[] BonusCompletions { get; set; } = new Dictionary[32]; + public Dictionary[] BonusCompletions { get; set; } = Array.Empty>(); /// /// Stage Completion Count - Refer to as StageCompletions[stage#][style] /// - public Dictionary[] StageCompletions { get; set; } = new Dictionary[32]; + public Dictionary[] StageCompletions { get; set; } = Array.Empty>(); /// /// Map World Record - Refer to as WR[style] /// @@ -44,11 +37,11 @@ internal class Map /// /// Bonus World Record - Refer to as BonusWR[bonus#][style] /// - public Dictionary[] BonusWR { get; set; } = new Dictionary[32]; + public Dictionary[] BonusWR { get; set; } = Array.Empty>(); /// /// Stage World Record - Refer to as StageWR[stage#][style] /// - public Dictionary[] StageWR { get; set; } = new Dictionary[32]; + public Dictionary[] StageWR { get; set; } = Array.Empty>(); /// /// Not sure what this is for. @@ -60,18 +53,18 @@ internal class Map // Zone Origin Information /* Map Start/End zones */ - public Vector_t StartZone { get; set; } = new Vector_t(0, 0, 0); - public QAngle_t StartZoneAngles { get; set; } = new QAngle_t(0, 0, 0); - public Vector_t EndZone { get; set; } = new Vector_t(0, 0, 0); + public VectorT StartZone { get; set; } = new VectorT(0, 0, 0); + public QAngleT StartZoneAngles { get; set; } = new QAngleT(0, 0, 0); + public VectorT EndZone { get; set; } = new VectorT(0, 0, 0); /* Map Stage zones */ - public Vector_t[] StageStartZone { get; } = Enumerable.Repeat(0, 99).Select(x => new Vector_t(0, 0, 0)).ToArray(); - public QAngle_t[] StageStartZoneAngles { get; } = Enumerable.Repeat(0, 99).Select(x => new QAngle_t(0, 0, 0)).ToArray(); + public VectorT[] StageStartZone { get; } = Enumerable.Repeat(0, 99).Select(x => new VectorT(0, 0, 0)).ToArray(); + public QAngleT[] StageStartZoneAngles { get; } = Enumerable.Repeat(0, 99).Select(x => new QAngleT(0, 0, 0)).ToArray(); /* Map Bonus zones */ - public Vector_t[] BonusStartZone { get; } = Enumerable.Repeat(0, 99).Select(x => new Vector_t(0, 0, 0)).ToArray(); // To-do: Implement bonuses - public QAngle_t[] BonusStartZoneAngles { get; } = Enumerable.Repeat(0, 99).Select(x => new QAngle_t(0, 0, 0)).ToArray(); // To-do: Implement bonuses - public Vector_t[] BonusEndZone { get; } = Enumerable.Repeat(0, 99).Select(x => new Vector_t(0, 0, 0)).ToArray(); // To-do: Implement bonuses + public VectorT[] BonusStartZone { get; } = Enumerable.Repeat(0, 99).Select(x => new VectorT(0, 0, 0)).ToArray(); // To-do: Implement bonuses + public QAngleT[] BonusStartZoneAngles { get; } = Enumerable.Repeat(0, 99).Select(x => new QAngleT(0, 0, 0)).ToArray(); // To-do: Implement bonuses + public VectorT[] BonusEndZone { get; } = Enumerable.Repeat(0, 99).Select(x => new VectorT(0, 0, 0)).ToArray(); // To-do: Implement bonuses /* Map Checkpoint zones */ - public Vector_t[] CheckpointStartZone { get; } = Enumerable.Repeat(0, 99).Select(x => new Vector_t(0, 0, 0)).ToArray(); + public VectorT[] CheckpointStartZone { get; } = Enumerable.Repeat(0, 99).Select(x => new VectorT(0, 0, 0)).ToArray(); public ReplayManager ReplayManager { get; set; } = null!; @@ -88,54 +81,55 @@ internal Map(string name) // Set map name this.Name = name; + // Load zones + MapLoadZones(); + _logger.LogInformation("[{ClassName}] -> Zones have been loaded. | Bonuses: {Bonuses} | Stages: {Stages} | Checkpoints: {Checkpoints}", + nameof(Map), this.Bonuses, this.Stages, this.TotalCheckpoints + ); + } + + internal async Task InitializeAsync([CallerMemberName] string methodName = "") + { + // Initialize ReplayManager with placeholder values + this.ReplayManager = new ReplayManager(-1, this.Stages > 0, this.Bonuses > 0, null!); + // Initialize WR variables + this.StageWR = new Dictionary[this.Stages + 1]; // We do + 1 cause stages and bonuses start from 1, not from 0 + this.StageCompletions = new Dictionary[this.Stages + 1]; + this.BonusWR = new Dictionary[this.Bonuses + 1]; + this.BonusCompletions = new Dictionary[this.Bonuses + 1]; + int initStages = 0; + int initBonuses = 0; + foreach (int style in Config.Styles) { - this.WR[style] = new PersonalBest(); - this.MapCompletions[style] = -1; - } + this.WR[style] = new PersonalBest { Type = 0 }; + this.MapCompletions[style] = 0; - for (int i = 0; i < 32; i++) - { - this.BonusWR[i] = new Dictionary(); - this.BonusWR[i][0] = new PersonalBest(); - this.BonusWR[i][0].Type = 1; - this.BonusCompletions[i] = new Dictionary(); - - this.StageWR[i] = new Dictionary(); - this.StageWR[i][0] = new PersonalBest(); - this.StageWR[i][0].Type = 2; - this.StageCompletions[i] = new Dictionary(); - } - } + for (int i = 1; i <= this.Stages; i++) + { + this.StageWR[i] = new Dictionary(); + this.StageWR[i][style] = new PersonalBest { Type = 2 }; + this.StageCompletions[i] = new Dictionary(); + this.StageCompletions[i][style] = 0; + initStages++; + } - public static async Task CreateAsync(string name) - { - var map = new Map(name); - await map.InitializeAsync(); - return map; - } + for (int i = 1; i <= this.Bonuses; i++) + { + this.BonusWR[i] = new Dictionary(); + this.BonusWR[i][style] = new PersonalBest { Type = 1 }; + this.BonusCompletions[i] = new Dictionary(); + this.BonusCompletions[i][style] = 0; + initBonuses++; + } + } - private async Task InitializeAsync([CallerMemberName] string methodName = "") - { - // Load zones - Map_Load_Zones(); - _logger.LogInformation("[{ClassName}] {MethodName} -> Zones have been loaded. | Bonuses: {Bonuses} | Stages: {Stages} | Checkpoints: {Checkpoints}", - nameof(Map), methodName, this.Bonuses, this.Stages, this.TotalCheckpoints + _logger.LogInformation("[{ClassName}] {MethodName} -> Initialized WR variables. | Bonuses: {Bonuses} | Stages: {Stages}", + nameof(Map), methodName, initBonuses, initStages ); - // Initialize ReplayManager with placeholder values - // Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> InitializeAsync -> Initializing ReplayManager(-1, {this.Stages > 0}, false, null!)"); - this.ReplayManager = new ReplayManager(-1, this.Stages > 0, this.Bonuses > 0, null!); // Adjust values as needed - - // Start timing - var stopwatch = Stopwatch.StartNew(); await LoadMapInfo(); - stopwatch.Stop(); - - _logger.LogInformation("[{ClassName}] {MethodName} -> We got MapID = {ID} ({Name}) in {ElapsedMilliseconds}ms | API = {API}", - nameof(Map), methodName, ID, Name, stopwatch.ElapsedMilliseconds, Config.API.GetApiOnly() - ); } /// @@ -144,7 +138,7 @@ private async Task InitializeAsync([CallerMemberName] string methodName = "") // To-do: This loops through all the triggers. While that's great and comprehensive, some maps have two triggers with the exact same name, because there are two // for each side of the course (left and right, for example). We should probably work on automatically catching this. // Maybe even introduce a new naming convention? - internal void Map_Load_Zones([CallerMemberName] string methodName = "") + internal void MapLoadZones([CallerMemberName] string methodName = "") { // Gathering zones from the map IEnumerable triggers = Utilities.FindAllEntitiesByDesignerName("trigger_multiple"); @@ -168,8 +162,8 @@ internal void Map_Load_Zones([CallerMemberName] string methodName = "") teleport.Entity!.Name.Contains("spawn_stage1_start") || teleport.Entity!.Name.Contains("spawn_s1_start"))) { - this.StartZone = new Vector_t(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z); - this.StartZoneAngles = new QAngle_t(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z); + this.StartZone = new VectorT(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z); + this.StartZoneAngles = new QAngleT(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z); foundPlayerSpawn = true; break; } @@ -177,14 +171,14 @@ internal void Map_Load_Zones([CallerMemberName] string methodName = "") if (!foundPlayerSpawn) { - this.StartZone = new Vector_t(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); + this.StartZone = new VectorT(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); } } // Map end zone else if (trigger.Entity!.Name.Contains("map_end")) { - this.EndZone = new Vector_t(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); + this.EndZone = new VectorT(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); } // Stage start zones @@ -199,8 +193,8 @@ internal void Map_Load_Zones([CallerMemberName] string methodName = "") if (teleport.Entity!.Name != null && (IsInZone(trigger.AbsOrigin!, trigger.Collision.BoundingRadius, teleport.AbsOrigin!) || (Regex.Match(teleport.Entity.Name, "^spawn_s([1-9][0-9]?|tage[1-9][0-9]?)_start$").Success && Int32.Parse(Regex.Match(teleport.Entity.Name, "[0-9][0-9]?").Value) == stage))) { - this.StageStartZone[stage] = new Vector_t(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z); - this.StageStartZoneAngles[stage] = new QAngle_t(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z); + this.StageStartZone[stage] = new VectorT(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z); + this.StageStartZoneAngles[stage] = new QAngleT(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z); this.Stages++; // Count stage zones for the map to populate DB foundPlayerSpawn = true; break; @@ -209,7 +203,7 @@ internal void Map_Load_Zones([CallerMemberName] string methodName = "") if (!foundPlayerSpawn) { - this.StageStartZone[stage] = new Vector_t(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); + this.StageStartZone[stage] = new VectorT(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); this.Stages++; } } @@ -217,7 +211,7 @@ internal void Map_Load_Zones([CallerMemberName] string methodName = "") // Checkpoint start zones (linear maps) else if (Regex.Match(trigger.Entity.Name, "^map_c(p[1-9][0-9]?|heckpoint[1-9][0-9]?)$").Success) { - this.CheckpointStartZone[Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value)] = new Vector_t(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); + this.CheckpointStartZone[Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value)] = new VectorT(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); this.TotalCheckpoints++; // Might be useful to have this in DB entry } @@ -233,8 +227,8 @@ internal void Map_Load_Zones([CallerMemberName] string methodName = "") if (teleport.Entity!.Name != null && (IsInZone(trigger.AbsOrigin!, trigger.Collision.BoundingRadius, teleport.AbsOrigin!) || (Regex.Match(teleport.Entity.Name, "^spawn_b([1-9][0-9]?|onus[1-9][0-9]?)_start$").Success && Int32.Parse(Regex.Match(teleport.Entity.Name, "[0-9][0-9]?").Value) == bonus))) { - this.BonusStartZone[bonus] = new Vector_t(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z); - this.BonusStartZoneAngles[bonus] = new QAngle_t(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z); + this.BonusStartZone[bonus] = new VectorT(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z); + this.BonusStartZoneAngles[bonus] = new QAngleT(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z); this.Bonuses++; // Count bonus zones for the map to populate DB foundPlayerSpawn = true; break; @@ -243,22 +237,25 @@ internal void Map_Load_Zones([CallerMemberName] string methodName = "") if (!foundPlayerSpawn) { - this.BonusStartZone[bonus] = new Vector_t(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); + this.BonusStartZone[bonus] = new VectorT(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); this.Bonuses++; } } else if (Regex.Match(trigger.Entity.Name, "^b([1-9][0-9]?|onus[1-9][0-9]?)_end$").Success) { - this.BonusEndZone[Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value)] = new Vector_t(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); + this.BonusEndZone[Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value)] = new VectorT(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); } } } if (this.Stages > 0) // Account for stage 1, not counted above + { + this.TotalCheckpoints = this.Stages; // Stages are counted as Checkpoints on Staged maps during MAP runs this.Stages += 1; + } - _logger.LogTrace("[{ClassName}] {MethodName} -> Start zone: {StartZoneX},{StartZoneY},{StartZoneZ} | End zone: {EndZoneX},{EndZoneY},{EndZoneZ}", + _logger.LogTrace("[{ClassName}] {MethodName} -> Start zone: {StartZoneX}, {StartZoneY}, {StartZoneZ} | End zone: {EndZoneX}, {EndZoneY}, {EndZoneZ}", nameof(Map), methodName, this.StartZone.X, this.StartZone.Y, this.StartZone.Z, this.EndZone.X, this.EndZone.Y, this.EndZone.Z ); @@ -267,13 +264,12 @@ internal void Map_Load_Zones([CallerMemberName] string methodName = "") /// /// Inserts a new map entry in the database. - /// Automatically detects whether to use API Calls or MySQL query. /// internal async Task InsertMapInfo([CallerMemberName] string methodName = "") { - var mapInfo = new MapInfoDataModel + var mapInfo = new MapDto { - Name = this.Name, + Name = this.Name!, Author = "Unknown", // Or set appropriately Tier = this.Tier, Stages = this.Stages, @@ -283,43 +279,40 @@ internal async Task InsertMapInfo([CallerMemberName] string methodName = "") try { - // this.ID = await _dataService.InsertMapInfoAsync(mapInfo); - int mapId = await _dataService.InsertMapInfoAsync(mapInfo); - this.ID = mapId; + this.ID = await _dataService.InsertMapInfoAsync(mapInfo); + _logger.LogInformation("[{ClassName}] {MethodName} -> Map '{Map}' inserted successfully with ID {ID}.", nameof(Map), methodName, this.Name, this.ID ); } catch (Exception ex) { - _logger.LogCritical(ex, "[{ClassName}] {MethodName} -> Failed to insert map '{Map}'.", - nameof(Map), methodName, this.Name + _logger.LogCritical(ex, "[{ClassName}] {MethodName} -> Failed to insert map '{Map}'. Exception: {ExceptionMessage}", + nameof(Map), methodName, this.Name, ex.Message ); - throw; + throw new InvalidOperationException($"Failed to insert map '{Name}'. See inner exception for details.", ex); } } /// /// Updates last played, stages, bonuses for the map in the database. - /// Automatically detects whether to use API Calls or MySQL query. /// internal async Task UpdateMapInfo([CallerMemberName] string methodName = "") { - var mapInfo = new MapInfoDataModel + var mapInfo = new MapDto { - ID = this.ID, - Name = this.Name, - Author = "Unknown", // adjust as necessary + Name = this.Name!, + Author = this.Author!, Tier = this.Tier, Stages = this.Stages, Bonuses = this.Bonuses, - Ranked = false, + Ranked = this.Ranked, LastPlayed = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds() }; try { - await _dataService.UpdateMapInfoAsync(mapInfo); + await _dataService.UpdateMapInfoAsync(mapInfo, this.ID); #if DEBUG _logger.LogDebug("[{ClassName}] {MethodName} -> Updated map '{Map}' (ID: {ID}).", @@ -329,24 +322,23 @@ internal async Task UpdateMapInfo([CallerMemberName] string methodName = "") } catch (Exception ex) { - _logger.LogCritical(ex, "[{ClassName}] {MethodName} -> Failed to update map '{Map}'.", - nameof(Map), methodName, this.Name + _logger.LogCritical(ex, "[{ClassName}] {MethodName} -> Failed to update map '{Map}'. Exception Message: {ExceptionMessage}", + nameof(Map), methodName, this.Name, ex.Message ); - throw; + throw new InvalidOperationException($"Failed to update map '{Name}'. See inner exception for details.", ex); } } /// - /// Load map info data using MySQL Queries and update the info as well or create a new entry. + /// Load/update/create Map table entry. /// Loads the record runs for the map as well. - /// Automatically detects whether to use API Calls or MySQL query. /// - /// Should we run UPDATE query for the map + /// Should we run UPDATE query for the map internal async Task LoadMapInfo(bool updateData = true, [CallerMemberName] string methodName = "") { bool newMap = false; - var mapInfo = await _dataService.GetMapInfoAsync(this.Name); + var mapInfo = await _dataService.GetMapInfoAsync(this.Name!); if (mapInfo != null) { @@ -385,32 +377,24 @@ internal async Task LoadMapInfo(bool updateData = true, [CallerMemberName] strin /// Extracts Map, Bonus, Stage record runs and the total completions for each style. /// (NOT TESTED WITH MORE THAN 1 STYLE) /// For the Map WR it also gets the Checkpoints data. - /// Automatically detects whether to use API Calls or MySQL query. - /// TODO: Re-do the API with the new query and fix the API assign of values /// internal async Task LoadMapRecordRuns([CallerMemberName] string methodName = "") { - // int totalMapRuns = 0; - // int totalStageRuns = 0; - // int totalBonusRuns = 0; - this.ConnectedMapTimes.Clear(); - - int qType; - int qStage; - int qStyle; - - // Replay Stuff - JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = false, Converters = { new Vector_tConverter(), new QAngle_tConverter() } }; + //this.ConnectedMapTimes.Clear(); // This is for Custom Replays (PB replays?) - T var runs = await _dataService.GetMapRecordRunsAsync(this.ID); + _logger.LogInformation("[{ClassName}] {MethodName} -> Received {Length} runs from `GetMapRecordRunsAsync`", + nameof(Map), methodName, runs.Count + ); + foreach (var run in runs) { switch (run.Type) { case 0: // Map WR data and total completions WR[run.Style].ID = run.ID; - WR[run.Style].Ticks = run.RunTime; + WR[run.Style].RunTime = run.RunTime; WR[run.Style].StartVelX = run.StartVelX; WR[run.Style].StartVelY = run.StartVelY; WR[run.Style].StartVelZ = run.StartVelZ; @@ -419,16 +403,15 @@ internal async Task LoadMapRecordRuns([CallerMemberName] string methodName = "") WR[run.Style].EndVelZ = run.EndVelZ; WR[run.Style].RunDate = run.RunDate; WR[run.Style].Name = run.Name; - // totalMapRuns = run.TotalCount; - ConnectedMapTimes.Add(run.ID); + /// ConnectedMapTimes.Add(run.ID); MapCompletions[run.Style] = run.TotalCount; - SetReplayData(run.Type, run.Style, run.Stage, run.ReplayFramesBase64); + SetReplayData(run.Type, run.Style, run.Stage, run.ReplayFrames!); break; case 1: // Bonus WR data and total completions BonusWR[run.Stage][run.Style].ID = run.ID; - BonusWR[run.Stage][run.Style].Ticks = run.RunTime; + BonusWR[run.Stage][run.Style].RunTime = run.RunTime; BonusWR[run.Stage][run.Style].StartVelX = run.StartVelX; BonusWR[run.Stage][run.Style].StartVelY = run.StartVelY; BonusWR[run.Stage][run.Style].StartVelZ = run.StartVelZ; @@ -439,12 +422,12 @@ internal async Task LoadMapRecordRuns([CallerMemberName] string methodName = "") BonusWR[run.Stage][run.Style].Name = run.Name; BonusCompletions[run.Stage][run.Style] = run.TotalCount; - SetReplayData(run.Type, run.Style, run.Stage, run.ReplayFramesBase64); + SetReplayData(run.Type, run.Style, run.Stage, run.ReplayFrames!); break; case 2: // Stage WR data and total completions StageWR[run.Stage][run.Style].ID = run.ID; - StageWR[run.Stage][run.Style].Ticks = run.RunTime; + StageWR[run.Stage][run.Style].RunTime = run.RunTime; StageWR[run.Stage][run.Style].StartVelX = run.StartVelX; StageWR[run.Stage][run.Style].StartVelY = run.StartVelY; StageWR[run.Stage][run.Style].StartVelZ = run.StartVelZ; @@ -455,7 +438,7 @@ internal async Task LoadMapRecordRuns([CallerMemberName] string methodName = "") StageWR[run.Stage][run.Style].Name = run.Name; StageCompletions[run.Stage][run.Style] = run.TotalCount; - SetReplayData(run.Type, run.Style, run.Stage, run.ReplayFramesBase64); + SetReplayData(run.Type, run.Style, run.Stage, run.ReplayFrames!); break; } } @@ -466,7 +449,7 @@ internal async Task LoadMapRecordRuns([CallerMemberName] string methodName = "") { #if DEBUG _logger.LogDebug("[{ClassName}] {MethodName} -> LoadMapRecordRuns : Map -> [{DBorAPI}] Loaded {MapCompletions} runs (MapID {MapID} | Style {Style}). WR by {PlayerName} - {Time}", - nameof(Map), methodName, (Config.API.GetApiOnly() ? "API" : "DB"), this.MapCompletions[style], this.ID, style, this.WR[style].Name, PlayerHUD.FormatTime(this.WR[style].Ticks) + nameof(Map), methodName, Config.API.GetApiOnly() ? "API" : "DB", this.MapCompletions[style], this.ID, style, this.WR[style].Name, PlayerHUD.FormatTime(this.WR[style].RunTime) ); #endif @@ -475,22 +458,12 @@ internal async Task LoadMapRecordRuns([CallerMemberName] string methodName = "") stopwatch.Stop(); _logger.LogInformation("[{ClassName}] {MethodName} -> Finished WR.[{Style}].LoadCheckpoints() in {ElapsedMilliseconds}ms | API = {API}", - nameof(Map), methodName, style, stopwatch.ElapsedMilliseconds, Config.API.GetApiOnly() + nameof(Map), methodName, style, stopwatch.ElapsedMilliseconds, Config.Api.GetApiOnly() ); } } } - /// - /// Redirects to `PersonalBest.LoadCheckpoints()`. - /// Extracts all entries from Checkpoints table of the World Record for the given `style` - /// - /// Style to load - internal async Task Get_Record_Run_Checkpoints(int style = 0) - { - await this.WR[style].LoadCheckpoints(); - } - /// /// Sets the data for a replay that has been retrieved from MapTimes data. /// Also sets the first Stage replay if no replays existed for stages until now. @@ -499,12 +472,12 @@ internal async Task Get_Record_Run_Checkpoints(int style = 0) /// Style to add /// Stage to add /// Base64 encoded string for the replay_frames - internal void SetReplayData(int type, int style, int stage, string replayFramesBase64, [CallerMemberName] string methodName = "") + internal void SetReplayData(int type, int style, int stage, ReplayFramesString replayFramesBase64, [CallerMemberName] string methodName = "") { - JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = false, Converters = { new Vector_tConverter(), new QAngle_tConverter() } }; + JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = false, Converters = { new VectorTConverter(), new QAngleTConverter() } }; // Decompress the Base64 string - string json = Compressor.Decompress(replayFramesBase64); + string json = Compressor.Decompress(replayFramesBase64.ToString()); // Deserialize to List List frames = JsonSerializer.Deserialize>(json, options)!; @@ -512,15 +485,14 @@ internal void SetReplayData(int type, int style, int stage, string replayFramesB switch (type) { case 0: // Map Replays - // Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal void SetReplayData -> [MapWR] Setting run {this.WR[style].ID} {PlayerHUD.FormatTime(this.WR[style].Ticks)} (Ticks = {this.WR[style].Ticks}; Frames = {frames.Count}) to `ReplayManager.MapWR`"); _logger.LogTrace("[{ClassName}] {MethodName} -> SetReplayData -> [MapWR] Setting run {RunID} {RunTime} (Ticks = {RunTicks}; Frames = {TotalFrames})", - nameof(Map), methodName, this.WR[style].ID, PlayerHUD.FormatTime(this.WR[style].Ticks), this.WR[style].Ticks, frames.Count + nameof(Map), methodName, this.WR[style].ID, PlayerHud.FormatTime(this.WR[style].RunTime), this.WR[style].RunTime, frames.Count ); if (this.ReplayManager.MapWR.IsPlaying) this.ReplayManager.MapWR.Stop(); - this.ReplayManager.MapWR.RecordPlayerName = this.WR[style].Name; - this.ReplayManager.MapWR.RecordRunTime = this.WR[style].Ticks; + this.ReplayManager.MapWR.RecordPlayerName = this.WR[style].Name!; + this.ReplayManager.MapWR.RecordRunTime = this.WR[style].RunTime; this.ReplayManager.MapWR.Frames = frames; this.ReplayManager.MapWR.MapTimeID = this.WR[style].ID; this.ReplayManager.MapWR.MapID = this.ID; @@ -530,54 +502,39 @@ internal void SetReplayData(int type, int style, int stage, string replayFramesB ReplayFrame f = frames[i]; switch (f.Situation) { - case ReplayFrameSituation.START_ZONE_ENTER: - this.ReplayManager.MapWR.MapSituations.Add(i); - // Console.WriteLine($"START_ZONE_ENTER: {i} | Situation {f.Situation}"); - break; - case ReplayFrameSituation.START_ZONE_EXIT: + case ReplayFrameSituation.START_ZONE_ENTER or ReplayFrameSituation.START_ZONE_EXIT: this.ReplayManager.MapWR.MapSituations.Add(i); - // Console.WriteLine($"START_ZONE_EXIT: {i} | Situation {f.Situation}"); + /// Console.WriteLine($"START_ZONE_ENTER: {i} | Situation {f.Situation}"); break; - case ReplayFrameSituation.STAGE_ZONE_ENTER: + case ReplayFrameSituation.STAGE_ZONE_ENTER or ReplayFrameSituation.STAGE_ZONE_EXIT: this.ReplayManager.MapWR.StageEnterSituations.Add(i); - // Console.WriteLine($"STAGE_ZONE_ENTER: {i} | Situation {f.Situation}"); + /// Console.WriteLine($"STAGE_ZONE_ENTER: {i} | Situation {f.Situation}"); break; - case ReplayFrameSituation.STAGE_ZONE_EXIT: - this.ReplayManager.MapWR.StageExitSituations.Add(i); - // Console.WriteLine($"STAGE_ZONE_EXIT: {i} | Situation {f.Situation}"); - break; - case ReplayFrameSituation.CHECKPOINT_ZONE_ENTER: + case ReplayFrameSituation.CHECKPOINT_ZONE_ENTER or ReplayFrameSituation.CHECKPOINT_ZONE_EXIT: this.ReplayManager.MapWR.CheckpointEnterSituations.Add(i); - // Console.WriteLine($"CHECKPOINT_ZONE_ENTER: {i} | Situation {f.Situation}"); - break; - case ReplayFrameSituation.CHECKPOINT_ZONE_EXIT: - this.ReplayManager.MapWR.CheckpointExitSituations.Add(i); - // Console.WriteLine($"CHECKPOINT_ZONE_EXIT: {i} | Situation {f.Situation}"); + /// Console.WriteLine($"CHECKPOINT_ZONE_ENTER: {i} | Situation {f.Situation}"); break; - case ReplayFrameSituation.END_ZONE_ENTER: - // Console.WriteLine($"END_ZONE_ENTER: {i} | Situation {f.Situation}"); - break; - case ReplayFrameSituation.END_ZONE_EXIT: - // Console.WriteLine($"END_ZONE_EXIT: {i} | Situation {f.Situation}"); + case ReplayFrameSituation.END_ZONE_ENTER or ReplayFrameSituation.END_ZONE_EXIT: + /// Console.WriteLine($"END_ZONE_ENTER: {i} | Situation {f.Situation}"); break; } } break; case 1: // Bonus Replays // Skip if the same bonus run already exists - if (this.ReplayManager.AllBonusWR[stage][style].RecordRunTime == this.BonusWR[stage][style].Ticks) + if (this.ReplayManager.AllBonusWR[stage][style].RecordRunTime == this.BonusWR[stage][style].RunTime) break; #if DEBUG _logger.LogDebug("[{ClassName}] {MethodName} -> SetReplayData -> [BonusWR] Adding run {ID} {Time} (Ticks = {Ticks}; Frames = {Frames}) to `ReplayManager.AllBonusWR`", - nameof(Map), methodName, this.BonusWR[stage][style].ID, PlayerHUD.FormatTime(this.BonusWR[stage][style].Ticks), this.BonusWR[stage][style].Ticks, frames.Count + nameof(Map), methodName, this.BonusWR[stage][style].ID, PlayerHUD.FormatTime(this.BonusWR[stage][style].RunTime), this.BonusWR[stage][style].RunTime, frames.Count ); #endif // Add all stages found to a dictionary with their data this.ReplayManager.AllBonusWR[stage][style].MapID = this.ID; this.ReplayManager.AllBonusWR[stage][style].Frames = frames; - this.ReplayManager.AllBonusWR[stage][style].RecordRunTime = this.BonusWR[stage][style].Ticks; - this.ReplayManager.AllBonusWR[stage][style].RecordPlayerName = this.BonusWR[stage][style].Name; + this.ReplayManager.AllBonusWR[stage][style].RecordRunTime = this.BonusWR[stage][style].RunTime; + this.ReplayManager.AllBonusWR[stage][style].RecordPlayerName = this.BonusWR[stage][style].Name!; this.ReplayManager.AllBonusWR[stage][style].MapTimeID = this.BonusWR[stage][style].ID; this.ReplayManager.AllBonusWR[stage][style].Stage = stage; this.ReplayManager.AllBonusWR[stage][style].Type = 1; @@ -588,10 +545,7 @@ internal void SetReplayData(int type, int style, int stage, string replayFramesB ReplayFrame f = frames[i]; switch (f.Situation) { - case ReplayFrameSituation.START_ZONE_ENTER: - this.ReplayManager.AllBonusWR[stage][style].BonusSituations.Add(i); - break; - case ReplayFrameSituation.END_ZONE_EXIT: + case ReplayFrameSituation.START_ZONE_ENTER or ReplayFrameSituation.END_ZONE_EXIT: this.ReplayManager.AllBonusWR[stage][style].BonusSituations.Add(i); break; } @@ -608,8 +562,8 @@ internal void SetReplayData(int type, int style, int stage, string replayFramesB this.ReplayManager.BonusWR.Stop(); this.ReplayManager.BonusWR.MapID = this.ID; this.ReplayManager.BonusWR.Frames = frames; - this.ReplayManager.BonusWR.RecordRunTime = this.BonusWR[stage][style].Ticks; - this.ReplayManager.BonusWR.RecordPlayerName = this.BonusWR[stage][style].Name; + this.ReplayManager.BonusWR.RecordRunTime = this.BonusWR[stage][style].RunTime; + this.ReplayManager.BonusWR.RecordPlayerName = this.BonusWR[stage][style].Name!; this.ReplayManager.BonusWR.MapTimeID = this.BonusWR[stage][style].ID; this.ReplayManager.BonusWR.Stage = stage; this.ReplayManager.BonusWR.Type = 1; @@ -618,19 +572,19 @@ internal void SetReplayData(int type, int style, int stage, string replayFramesB break; case 2: // Stage Replays // Skip if the same stage run already exists - if (this.ReplayManager.AllStageWR[stage][style].RecordRunTime == this.StageWR[stage][style].Ticks) + if (this.ReplayManager.AllStageWR[stage][style].RecordRunTime == this.StageWR[stage][style].RunTime) break; #if DEBUG _logger.LogDebug("[{ClassName}] {MethodName} -> SetReplayData -> [StageWR] Adding run {ID} {Time} (Ticks = {Ticks}; Frames = {Frames}) to `ReplayManager.AllStageWR`", - nameof(Map), methodName, this.StageWR[stage][style].ID, PlayerHUD.FormatTime(this.StageWR[stage][style].Ticks), this.StageWR[stage][style].Ticks, frames.Count + nameof(Map), methodName, this.StageWR[stage][style].ID, PlayerHUD.FormatTime(this.StageWR[stage][style].RunTime), this.StageWR[stage][style].RunTime, frames.Count ); #endif // Add all stages found to a dictionary with their data this.ReplayManager.AllStageWR[stage][style].MapID = this.ID; this.ReplayManager.AllStageWR[stage][style].Frames = frames; - this.ReplayManager.AllStageWR[stage][style].RecordRunTime = this.StageWR[stage][style].Ticks; - this.ReplayManager.AllStageWR[stage][style].RecordPlayerName = this.StageWR[stage][style].Name; + this.ReplayManager.AllStageWR[stage][style].RecordRunTime = this.StageWR[stage][style].RunTime; + this.ReplayManager.AllStageWR[stage][style].RecordPlayerName = this.StageWR[stage][style].Name!; this.ReplayManager.AllStageWR[stage][style].MapTimeID = this.StageWR[stage][style].ID; this.ReplayManager.AllStageWR[stage][style].Stage = stage; this.ReplayManager.AllStageWR[stage][style].Type = 2; @@ -662,8 +616,8 @@ internal void SetReplayData(int type, int style, int stage, string replayFramesB this.ReplayManager.StageWR.Stop(); this.ReplayManager.StageWR.MapID = this.ID; this.ReplayManager.StageWR.Frames = frames; - this.ReplayManager.StageWR.RecordRunTime = this.StageWR[stage][style].Ticks; - this.ReplayManager.StageWR.RecordPlayerName = this.StageWR[stage][style].Name; + this.ReplayManager.StageWR.RecordRunTime = this.StageWR[stage][style].RunTime; + this.ReplayManager.StageWR.RecordPlayerName = this.StageWR[stage][style].Name!; this.ReplayManager.StageWR.MapTimeID = this.StageWR[stage][style].ID; this.ReplayManager.StageWR.Stage = stage; this.ReplayManager.StageWR.Type = 2; @@ -675,22 +629,23 @@ internal void SetReplayData(int type, int style, int stage, string replayFramesB // Start the new map replay if none existed until now Server.NextFrame(() => { - // Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal void SetReplayData -> [MapWR] ResetReplay() and Start()"); - this.ReplayManager.MapWR.ResetReplay(); - this.ReplayManager.MapWR.Start(); - } - else if (type == 1 && this.ReplayManager.BonusWR != null && !this.ReplayManager.BonusWR.IsPlaying) - { - // Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal void SetReplayData -> [BonusWR] ResetReplay() and Start() {stage}"); - this.ReplayManager.BonusWR.ResetReplay(); - this.ReplayManager.BonusWR.Start(); - } - else if (type == 2 && this.ReplayManager.StageWR != null && !this.ReplayManager.StageWR.IsPlaying) - { - // Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal void SetReplayData -> [StageWR] ResetReplay() and Start() {stage}"); - this.ReplayManager.StageWR.ResetReplay(); - this.ReplayManager.StageWR.Start(); + if (type == 0 && this.ReplayManager.MapWR != null && !this.ReplayManager.MapWR.IsPlaying) + { + this.ReplayManager.MapWR.ResetReplay(); + this.ReplayManager.MapWR.Start(); + } + else if (type == 1 && this.ReplayManager.BonusWR != null && !this.ReplayManager.BonusWR.IsPlaying) + { + this.ReplayManager.BonusWR.ResetReplay(); + this.ReplayManager.BonusWR.Start(); + } + else if (type == 2 && this.ReplayManager.StageWR != null && !this.ReplayManager.StageWR.IsPlaying) + { + this.ReplayManager.StageWR.ResetReplay(); + this.ReplayManager.StageWR.Start(); + } } + ); } public void KickReplayBot(int index) diff --git a/src/ST-Player/Player.cs b/src/ST-Player/Player.cs index cbbaed5..cdda246 100644 --- a/src/ST-Player/Player.cs +++ b/src/ST-Player/Player.cs @@ -1,7 +1,8 @@ -namespace SurfTimer; using CounterStrikeSharp.API.Core; -internal class Player +namespace SurfTimer; + +public class Player { // CCS requirements public CCSPlayerController Controller {get;} @@ -10,7 +11,7 @@ internal class Player // Timer-related properties public PlayerTimer Timer {get; set;} public PlayerStats Stats {get; set;} - public PlayerHUD HUD {get; set;} + public PlayerHud HUD {get; set;} public ReplayRecorder ReplayRecorder { get; set; } public List SavedLocations { get; set; } public int CurrentSavedLocation { get; set; } @@ -18,11 +19,8 @@ internal class Player // Player information public PlayerProfile Profile {get; set;} - // Map information - public Map CurrMap = null!; - // Constructor - public Player(CCSPlayerController Controller, CCSPlayer_MovementServices MovementServices, PlayerProfile Profile, Map CurrMap) + internal Player(CCSPlayerController Controller, CCSPlayer_MovementServices MovementServices, PlayerProfile Profile) { this.Controller = Controller; this.MovementServices = MovementServices; @@ -35,12 +33,11 @@ public Player(CCSPlayerController Controller, CCSPlayer_MovementServices Movemen this.SavedLocations = new List(); CurrentSavedLocation = 0; - this.HUD = new PlayerHUD(this); - this.CurrMap = CurrMap; + this.HUD = new PlayerHud(this); } /// - /// Checks if current player is spectating player

+ /// Checks if current player is spectating player ///

public bool IsSpectating(CCSPlayerController p) { diff --git a/src/ST-Player/PlayerHUD.cs b/src/ST-Player/PlayerHUD.cs index 7f28516..4da145a 100644 --- a/src/ST-Player/PlayerHUD.cs +++ b/src/ST-Player/PlayerHUD.cs @@ -2,16 +2,27 @@ namespace SurfTimer; -internal class PlayerHUD +public class PlayerHud { - private Player _player; - - public PlayerHUD(Player Player) + private readonly Player _player; + private readonly string TimerColor = "#4FC3F7"; + private readonly string TimerColorPractice = "#BA68C8"; + private readonly string TimerColorActive = "#43A047"; + private readonly string RankColorPb = "#7986CB"; + private readonly string RankColorWr = "#FFD700"; + private readonly string SpectatorColor = "#9E9E9E"; + + internal PlayerHud(Player Player) { _player = Player; } - private string FormatHUDElementHTML(string title, string body, string color, string size = "m") + private static string FormatHUDElementHTML( + string title, + string body, + string color, + string size = "m" + ) { if (title != "") { @@ -20,7 +31,6 @@ private string FormatHUDElementHTML(string title, string body, string color, str else return $"{title}: {body}"; } - else { if (size == "m") @@ -35,7 +45,10 @@ private string FormatHUDElementHTML(string title, string body, string color, str /// Unless specified differently, the default formatting will be `Compact`. /// Check for all formatting types. ///
- public static string FormatTime(int ticks, PlayerTimer.TimeFormatStyle style = PlayerTimer.TimeFormatStyle.Compact) + public static string FormatTime( + int ticks, + PlayerTimer.TimeFormatStyle style = PlayerTimer.TimeFormatStyle.Compact + ) { TimeSpan time = TimeSpan.FromSeconds(ticks / 64.0); int millis = (int)(ticks % 64 * (1000.0 / 64.0)); @@ -57,172 +70,484 @@ public static string FormatTime(int ticks, PlayerTimer.TimeFormatStyle style = P } } + /// + /// Build the timer module with appropriate prefix based on mode + /// + /// string timerModule + internal string BuildTimerWithPrefix() + { + // Timer Module + string timerColor = TimerColor; + + if (_player.Timer.IsRunning) + { + if (_player.Timer.IsPracticeMode) + timerColor = TimerColorPractice; + else + timerColor = TimerColorActive; + } + + string prefix = ""; + + if (_player.Timer.IsPracticeMode) + prefix += "[P] "; + + if (_player.Timer.IsBonusMode) + prefix += $"[B{_player.Timer.Bonus}] "; + else if (_player.Timer.IsStageMode) + prefix += $"[S{_player.Timer.Stage}] "; + + string timerModule = FormatHUDElementHTML( + "", + prefix + FormatTime(_player.Timer.Ticks), + timerColor + ); + + return timerModule; + } + + /// + /// Build the velocity module + /// + /// string velocityModule + internal string BuildVelocityModule() + { + float velocity = Extensions.GetVelocityFromController(_player.Controller); + string velocityModule = + FormatHUDElementHTML( + "Speed", + velocity.ToString("0"), + Extensions.GetSpeedColorGradient(velocity) + ) + " u/s"; + return velocityModule; + } + + /// + /// Build the rank module with appropriate values based on mode + /// + /// string rankModule + internal string BuildRankModule() + { + int style = _player.Timer.Style; + + // Rank Module + string rankModule = FormatHUDElementHTML("Rank", $"N/A", RankColorPb); + if (_player.Timer.IsBonusMode) + { + if ( + _player.Stats.BonusPB[_player.Timer.Bonus][style].ID != -1 + && SurfTimer.CurrentMap.BonusWR[_player.Timer.Bonus][style].ID != -1 + ) + rankModule = FormatHUDElementHTML( + "Rank", + $"{_player.Stats.BonusPB[_player.Timer.Bonus][style].Rank}/{SurfTimer.CurrentMap.BonusCompletions[_player.Timer.Bonus][style]}", + RankColorPb + ); + else if (SurfTimer.CurrentMap.BonusWR[_player.Timer.Bonus][style].ID != -1) + rankModule = FormatHUDElementHTML( + "Rank", + $"-/{SurfTimer.CurrentMap.BonusCompletions[_player.Timer.Bonus][style]}", + RankColorPb + ); + } + else if (_player.Timer.IsStageMode) + { + if ( + _player.Stats.StagePB[_player.Timer.Stage][style].ID != -1 + && SurfTimer.CurrentMap.StageWR[_player.Timer.Stage][style].ID != -1 + ) + rankModule = FormatHUDElementHTML( + "Rank", + $"{_player.Stats.StagePB[_player.Timer.Stage][style].Rank}/{SurfTimer.CurrentMap.StageCompletions[_player.Timer.Stage][style]}", + RankColorPb + ); + else if (SurfTimer.CurrentMap.StageWR[_player.Timer.Stage][style].ID != -1) + rankModule = FormatHUDElementHTML( + "Rank", + $"-/{SurfTimer.CurrentMap.StageCompletions[_player.Timer.Stage][style]}", + RankColorPb + ); + } + else + { + if (_player.Stats.PB[style].ID != -1 && SurfTimer.CurrentMap.WR[style].ID != -1) + rankModule = FormatHUDElementHTML( + "Rank", + $"{_player.Stats.PB[style].Rank}/{SurfTimer.CurrentMap.MapCompletions[style]}", + RankColorPb + ); + else if (SurfTimer.CurrentMap.WR[style].ID != -1) + rankModule = FormatHUDElementHTML( + "Rank", + $"-/{SurfTimer.CurrentMap.MapCompletions[style]}", + RankColorPb + ); + } + + return rankModule; + } + + /// + /// Build the PB module with appropriate values based on mode + /// + /// string pbModule + internal string BuildPbModule() + { + int style = _player.Timer.Style; + + // PB & WR Modules + string pbModule = FormatHUDElementHTML( + "PB", + _player.Stats.PB[style].RunTime > 0 + ? FormatTime(_player.Stats.PB[style].RunTime) + : "N/A", + RankColorPb + ); + + if (_player.Timer.Bonus > 0 && _player.Timer.IsBonusMode) // Show corresponding bonus values + { + pbModule = FormatHUDElementHTML( + "PB", + _player.Stats.BonusPB[_player.Timer.Bonus][style].RunTime > 0 + ? FormatTime(_player.Stats.BonusPB[_player.Timer.Bonus][style].RunTime) + : "N/A", + RankColorPb + ); + } + else if (_player.Timer.IsStageMode) // Show corresponding stage values + { + pbModule = FormatHUDElementHTML( + "PB", + _player.Stats.StagePB[_player.Timer.Stage][style].RunTime > 0 + ? FormatTime(_player.Stats.StagePB[_player.Timer.Stage][style].RunTime) + : "N/A", + RankColorPb + ); + } + + return pbModule; + } + + /// + /// Build the WR module with appropriate values based on mode + /// + /// string wrModule + internal string BuildWrModule() + { + int style = _player.Timer.Style; + + // WR Module + string wrModule = FormatHUDElementHTML( + "WR", + SurfTimer.CurrentMap.WR[style].RunTime > 0 + ? FormatTime(SurfTimer.CurrentMap.WR[style].RunTime) + : "N/A", + RankColorWr + ); + + if (_player.Timer.Bonus > 0 && _player.Timer.IsBonusMode) // Show corresponding bonus values + { + wrModule = FormatHUDElementHTML( + "WR", + SurfTimer.CurrentMap.BonusWR[_player.Timer.Bonus][style].RunTime > 0 + ? FormatTime(SurfTimer.CurrentMap.BonusWR[_player.Timer.Bonus][style].RunTime) + : "N/A", + RankColorWr + ); + } + else if (_player.Timer.IsStageMode) // Show corresponding stage values + { + wrModule = FormatHUDElementHTML( + "WR", + SurfTimer.CurrentMap.StageWR[_player.Timer.Stage][style].RunTime > 0 + ? FormatTime(SurfTimer.CurrentMap.StageWR[_player.Timer.Stage][style].RunTime) + : "N/A", + RankColorWr + ); + } + + return wrModule; + } + /// /// Displays the Center HUD for the client /// - public void Display() + internal void Display() { if (!_player.Controller.IsValid) return; if (_player.Controller.PawnIsAlive) { - int style = _player.Timer.Style; - // Timer Module - string timerColor = "#79d1ed"; + string timerModule = BuildTimerWithPrefix(); - if (_player.Timer.IsRunning) - { - if (_player.Timer.IsPracticeMode) - timerColor = "#F2C94C"; - else - timerColor = "#2E9F65"; - } - - string timerModule; - if (_player.Timer.IsBonusMode) - timerModule = FormatHUDElementHTML("", $"[B{_player.Timer.Bonus}] " + FormatTime(_player.Timer.Ticks), timerColor); - else if (_player.Timer.IsStageMode) - timerModule = FormatHUDElementHTML("", $"[S{_player.Timer.Stage}] " + FormatTime(_player.Timer.Ticks), timerColor); - else - timerModule = FormatHUDElementHTML("", FormatTime(_player.Timer.Ticks), timerColor); + // Velocity Module + string velocityModule = BuildVelocityModule(); - // Velocity Module - To-do: Make velocity module configurable (XY or XYZ velocity) - float velocity = (float)Math.Sqrt(_player.Controller.PlayerPawn.Value!.AbsVelocity.X * _player.Controller.PlayerPawn.Value!.AbsVelocity.X - + _player.Controller.PlayerPawn.Value!.AbsVelocity.Y * _player.Controller.PlayerPawn.Value!.AbsVelocity.Y - + _player.Controller.PlayerPawn.Value!.AbsVelocity.Z * _player.Controller.PlayerPawn.Value!.AbsVelocity.Z); - string velocityModule = FormatHUDElementHTML("Speed", velocity.ToString("0"), "#79d1ed") + " u/s"; // Rank Module - string rankModule = FormatHUDElementHTML("Rank", $"N/A", "#7882dd"); - if (_player.Timer.IsBonusMode) - { - if (_player.Stats.BonusPB[_player.Timer.Bonus][style].ID != -1 && SurfTimer.CurrentMap.BonusWR[_player.Timer.Bonus][style].ID != -1) - rankModule = FormatHUDElementHTML("Rank", $"{_player.Stats.BonusPB[_player.Timer.Bonus][style].Rank}/{SurfTimer.CurrentMap.BonusCompletions[_player.Timer.Bonus][style]}", "#7882dd"); - else if (SurfTimer.CurrentMap.BonusWR[_player.Timer.Bonus][style].ID != -1) - rankModule = FormatHUDElementHTML("Rank", $"-/{SurfTimer.CurrentMap.BonusCompletions[_player.Timer.Bonus][style]}", "#7882dd"); - } - else if (_player.Timer.IsStageMode) - { - if (_player.Stats.StagePB[_player.Timer.Stage][style].ID != -1 && SurfTimer.CurrentMap.StageWR[_player.Timer.Stage][style].ID != -1) - rankModule = FormatHUDElementHTML("Rank", $"{_player.Stats.StagePB[_player.Timer.Stage][style].Rank}/{SurfTimer.CurrentMap.StageCompletions[_player.Timer.Stage][style]}", "#7882dd"); - else if (SurfTimer.CurrentMap.StageWR[_player.Timer.Stage][style].ID != -1) - rankModule = FormatHUDElementHTML("Rank", $"-/{SurfTimer.CurrentMap.StageCompletions[_player.Timer.Stage][style]}", "#7882dd"); - } - else - { - if (_player.Stats.PB[style].ID != -1 && SurfTimer.CurrentMap.WR[style].ID != -1) - rankModule = FormatHUDElementHTML("Rank", $"{_player.Stats.PB[style].Rank}/{SurfTimer.CurrentMap.MapCompletions[style]}", "#7882dd"); - else if (SurfTimer.CurrentMap.WR[style].ID != -1) - rankModule = FormatHUDElementHTML("Rank", $"-/{SurfTimer.CurrentMap.MapCompletions[style]}", "#7882dd"); - } + string rankModule = BuildRankModule(); // PB & WR Modules - string pbModule = FormatHUDElementHTML("PB", _player.Stats.PB[style].Ticks > 0 ? FormatTime(_player.Stats.PB[style].Ticks) : "N/A", "#7882dd"); - string wrModule = FormatHUDElementHTML("WR", SurfTimer.CurrentMap.WR[style].Ticks > 0 ? FormatTime(SurfTimer.CurrentMap.WR[style].Ticks) : "N/A", "#ffc61a"); - - if (_player.Timer.Bonus > 0 && _player.Timer.IsBonusMode) // Show corresponding bonus values - { - pbModule = FormatHUDElementHTML("PB", _player.Stats.BonusPB[_player.Timer.Bonus][style].Ticks > 0 ? FormatTime(_player.Stats.BonusPB[_player.Timer.Bonus][style].Ticks) : "N/A", "#7882dd"); - wrModule = FormatHUDElementHTML("WR", SurfTimer.CurrentMap.BonusWR[_player.Timer.Bonus][style].Ticks > 0 ? FormatTime(SurfTimer.CurrentMap.BonusWR[_player.Timer.Bonus][style].Ticks) : "N/A", "#ffc61a"); - } - else if (_player.Timer.IsStageMode) // Show corresponding stage values - { - pbModule = FormatHUDElementHTML("PB", _player.Stats.StagePB[_player.Timer.Stage][style].Ticks > 0 ? FormatTime(_player.Stats.StagePB[_player.Timer.Stage][style].Ticks) : "N/A", "#7882dd"); - wrModule = FormatHUDElementHTML("WR", SurfTimer.CurrentMap.StageWR[_player.Timer.Stage][style].Ticks > 0 ? FormatTime(SurfTimer.CurrentMap.StageWR[_player.Timer.Stage][style].Ticks) : "N/A", "#ffc61a"); - } + string pbModule = BuildPbModule(); + string wrModule = BuildWrModule(); // Build HUD - string hud = $"{timerModule}
{velocityModule}
{pbModule} | {rankModule}
{wrModule}"; + string hud = + $"{timerModule}
{velocityModule}
{pbModule} | {rankModule}
{wrModule}"; // Display HUD _player.Controller.PrintToCenterHtml(hud); } else if (_player.Controller.Team == CsTeam.Spectator) { - ReplayPlayer? spec_replay; - - if (_player.IsSpectating(SurfTimer.CurrentMap.ReplayManager.MapWR.Controller!)) - spec_replay = SurfTimer.CurrentMap.ReplayManager.MapWR; - else if (_player.IsSpectating(SurfTimer.CurrentMap.ReplayManager.StageWR?.Controller!)) - spec_replay = SurfTimer.CurrentMap.ReplayManager.StageWR!; - else if (_player.IsSpectating(SurfTimer.CurrentMap.ReplayManager.BonusWR?.Controller!)) - spec_replay = SurfTimer.CurrentMap.ReplayManager.BonusWR!; - else - spec_replay = SurfTimer.CurrentMap.ReplayManager.CustomReplays.Find(x => _player.IsSpectating(x.Controller!)); + DisplaySpectatorHud(); + } + } - if (spec_replay != null) - { - string replayModule = $"{FormatHUDElementHTML("", "REPLAY", "red", "large")}"; - string nameModule = FormatHUDElementHTML($"{spec_replay.RecordPlayerName}", $"{FormatTime(spec_replay.RecordRunTime)}", "#ffd500"); - string hud = $"{replayModule}
{nameModule}"; + /// + /// Displays the Spectator HUD for the client if they are spectating a replay + /// + internal void DisplaySpectatorHud() + { + ReplayPlayer? specReplay; + string hud = string.Empty; - _player.Controller.PrintToCenterHtml(hud); - } + if (_player.IsSpectating(SurfTimer.CurrentMap.ReplayManager.MapWR.Controller!)) + { + specReplay = SurfTimer.CurrentMap.ReplayManager.MapWR; + hud = BuildMapWrModule(specReplay); + } + else if (_player.IsSpectating(SurfTimer.CurrentMap.ReplayManager.StageWR?.Controller!)) + { + specReplay = SurfTimer.CurrentMap.ReplayManager.StageWR!; + hud = BuildStageWrModule(specReplay); + } + else if (_player.IsSpectating(SurfTimer.CurrentMap.ReplayManager.BonusWR?.Controller!)) + { + specReplay = SurfTimer.CurrentMap.ReplayManager.BonusWR!; + hud = BuildBonusWrModule(specReplay); + } + else + { + specReplay = SurfTimer.CurrentMap.ReplayManager.CustomReplays.Find(x => + _player.IsSpectating(x.Controller!) + ); + if (specReplay != null) + hud = BuildCustomReplayModule(specReplay); + } + + if (!string.IsNullOrEmpty(hud)) + { + _player.Controller.PrintToCenterHtml(hud); } } /// + /// Build the Map WR module for the spectator HUD + /// + /// Replay data to use + internal string BuildMapWrModule(ReplayPlayer specReplay) + { + float velocity = Extensions.GetVelocityFromController(specReplay.Controller!); + string timerColor = specReplay.ReplayCurrentRunTime > 0 ? TimerColorActive : RankColorWr; + + string replayModule = FormatHUDElementHTML("", "Map WR Replay", SpectatorColor, "m"); + string nameModule = FormatHUDElementHTML("", $"{specReplay.RecordPlayerName}", RankColorWr); + string timeModule = FormatHUDElementHTML( + "", + $"{FormatTime(specReplay.ReplayCurrentRunTime)} / {FormatTime(specReplay.RecordRunTime)}", + timerColor + ); + string velocityModule = + FormatHUDElementHTML( + "Speed", + velocity.ToString("0"), + Extensions.GetSpeedColorGradient(velocity) + ) + " u/s"; + string cycleModule = FormatHUDElementHTML( + "Cycle", + $"{specReplay.RepeatCount}", + SpectatorColor, + "s" + ); + + return $"{replayModule}
{nameModule}
{timeModule}
{velocityModule}
{cycleModule}"; + } + + /// + /// Build the Stage WR module for the spectator HUD + /// + /// Replay data to use + internal string BuildStageWrModule(ReplayPlayer specReplay) + { + float velocity = Extensions.GetVelocityFromController(specReplay.Controller!); + string timerColor = specReplay.ReplayCurrentRunTime > 0 ? TimerColorActive : RankColorWr; + + string replayModule = FormatHUDElementHTML( + "", + $"Stage {specReplay.Stage} WR Replay", + SpectatorColor, + "m" + ); + string nameModule = FormatHUDElementHTML("", $"{specReplay.RecordPlayerName}", RankColorWr); + string timeModule = FormatHUDElementHTML( + "", + $"{FormatTime(specReplay.ReplayCurrentRunTime)} / {FormatTime(specReplay.RecordRunTime)}", + timerColor + ); + string velocityModule = + FormatHUDElementHTML( + "Speed", + velocity.ToString("0"), + Extensions.GetSpeedColorGradient(velocity) + ) + " u/s"; + string cycleModule = FormatHUDElementHTML( + "Cycle", + $"{specReplay.RepeatCount}", + SpectatorColor, + "s" + ); + + return $"{replayModule}
{nameModule}
{timeModule}
{velocityModule}
{cycleModule}"; + } + + /// + /// Build the Bonus WR module for the spectator HUD + /// + /// Replay data to use< + internal string BuildBonusWrModule(ReplayPlayer specReplay) + { + float velocity = Extensions.GetVelocityFromController(specReplay.Controller!); + string timerColor = specReplay.ReplayCurrentRunTime > 0 ? TimerColorActive : RankColorWr; + + string replayModule = FormatHUDElementHTML( + "", + $"Bonus {specReplay.Stage} WR Replay", + SpectatorColor, + "m" + ); + string nameModule = FormatHUDElementHTML("", $"{specReplay.RecordPlayerName}", RankColorWr); + string timeModule = FormatHUDElementHTML( + "", + $"{FormatTime(specReplay.ReplayCurrentRunTime)} / {FormatTime(specReplay.RecordRunTime)}", + timerColor + ); + string velocityModule = + FormatHUDElementHTML( + "Speed", + velocity.ToString("0"), + Extensions.GetSpeedColorGradient(velocity) + ) + " u/s"; + string cycleModule = FormatHUDElementHTML( + "Cycle", + $"{specReplay.RepeatCount}", + SpectatorColor, + "s" + ); + + return $"{replayModule}
{nameModule}
{timeModule}
{velocityModule}
{cycleModule}"; + } + + /// + /// Build the Custom Replay module for the spectator HUD + /// + /// Replay data to use< + internal string BuildCustomReplayModule(ReplayPlayer specReplay) + { + float velocity = Extensions.GetVelocityFromController(specReplay.Controller!); + string timerColor = specReplay.ReplayCurrentRunTime > 0 ? TimerColorActive : RankColorWr; + + string replayType; + switch (specReplay.Type) + { + case 0: + replayType = "Map PB Replay"; + break; + case 1: + replayType = $"Bonus {specReplay.Stage} PB Replay"; + break; + case 2: + replayType = $"Stage {specReplay.Stage} PB Replay"; + break; + default: + return ""; // Invalid type + } + + string replayModule = FormatHUDElementHTML("", replayType, SpectatorColor, "m"); + string nameModule = FormatHUDElementHTML("", $"{specReplay.RecordPlayerName}", RankColorWr); + string timeModule = FormatHUDElementHTML( + "", + $"{FormatTime(specReplay.ReplayCurrentRunTime)} / {FormatTime(specReplay.RecordRunTime)}", + timerColor + ); + string velocityModule = + FormatHUDElementHTML( + "Speed", + velocity.ToString("0"), + Extensions.GetSpeedColorGradient(velocity) + ) + " u/s"; + string cycleModule = FormatHUDElementHTML( + "Cycle", + $"{specReplay.RepeatCount}", + SpectatorColor, + "s" + ); + + return $"{replayModule}
{nameModule}
{timeModule}
{velocityModule}
{cycleModule}"; + } + + /// + /// Displays checkpoints comparison messages in player chat. /// Only calculates if the player has a PB, otherwise it will display N/A /// - public void DisplayCheckpointMessages() + internal void DisplayCheckpointMessages() { int pbTime; int wrTime = -1; float pbSpeed; float wrSpeed = -1.0f; int style = _player.Timer.Style; - int playerCheckpoint = _player.Timer.Checkpoint; - - // _player.Controller.PrintToChat($"{ChatColors.Blue}-> PlayerHUD{ChatColors.Default} => Style {ChatColors.Yellow}{style}{ChatColors.Default} | Checkpoint {playerCheckpoint} | WR Time Ticks {SurfTimer.CurrentMap.WR[style].Ticks} | Player Stage {_player.Timer.Stage} (CP {_player.Timer.Checkpoint}) | Player Ticks {_player.Timer.Ticks}"); - + int playerCurrentCheckpoint = _player.Timer.Checkpoint; int currentTime = _player.Timer.Ticks; - float currentSpeed = (float)Math.Sqrt(_player.Controller.PlayerPawn.Value!.AbsVelocity.X * _player.Controller.PlayerPawn.Value!.AbsVelocity.X - + _player.Controller.PlayerPawn.Value!.AbsVelocity.Y * _player.Controller.PlayerPawn.Value!.AbsVelocity.Y - + _player.Controller.PlayerPawn.Value!.AbsVelocity.Z * _player.Controller.PlayerPawn.Value!.AbsVelocity.Z); + float currentSpeed = Extensions.GetVelocityFromController(_player.Controller!); // Default values for the PB and WR differences in case no calculations can be made - string strPbDifference = $"{ChatColors.Grey}N/A{ChatColors.Default} ({ChatColors.Grey}N/A{ChatColors.Default})"; - string strWrDifference = $"{ChatColors.Grey}N/A{ChatColors.Default} ({ChatColors.Grey}N/A{ChatColors.Default})"; + string strPbDifference = + $"{ChatColors.Grey}N/A{ChatColors.Default} ({ChatColors.Grey}N/A{ChatColors.Default})"; + string strWrDifference = + $"{ChatColors.Grey}N/A{ChatColors.Default} ({ChatColors.Grey}N/A{ChatColors.Default})"; - // We need to try/catch this because the player might not have a PB for this checkpoint in this case but they will not have for the map as well - // Can check checkpoints count instead of try/catch - try + // Get PB checkpoint data if available + if (_player.Stats.PB[style].Checkpoints != null) { - pbTime = _player.Stats.PB[style].Checkpoints[playerCheckpoint].Ticks; - pbSpeed = (float)Math.Sqrt(_player.Stats.PB[style].Checkpoints[playerCheckpoint].StartVelX * _player.Stats.PB[style].Checkpoints[playerCheckpoint].StartVelX - + _player.Stats.PB[style].Checkpoints[playerCheckpoint].StartVelY * _player.Stats.PB[style].Checkpoints[playerCheckpoint].StartVelY - + _player.Stats.PB[style].Checkpoints[playerCheckpoint].StartVelZ * _player.Stats.PB[style].Checkpoints[playerCheckpoint].StartVelZ); - -#if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [TIME] Got pbTime from _player.Stats.PB[{style}].Checkpoint[{playerCheckpoint} = {pbTime}]"); - Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [SPEED] Got pbSpeed from _player.Stats.PB[{style}].Checkpoint[{playerCheckpoint}] = {pbSpeed}"); -#endif + pbTime = _player.Stats.PB[style].Checkpoints![playerCurrentCheckpoint].RunTime; + pbSpeed = (float) + Math.Sqrt( + _player.Stats.PB[style].Checkpoints![playerCurrentCheckpoint].StartVelX + * _player.Stats.PB[style].Checkpoints![playerCurrentCheckpoint].StartVelX + + _player.Stats.PB[style].Checkpoints![playerCurrentCheckpoint].StartVelY + * _player.Stats.PB[style].Checkpoints![playerCurrentCheckpoint].StartVelY + + _player.Stats.PB[style].Checkpoints![playerCurrentCheckpoint].StartVelZ + * _player.Stats.PB[style].Checkpoints![playerCurrentCheckpoint].StartVelZ + ); } -#if DEBUG - catch (System.Exception ex) -#else - catch (System.Exception) -#endif + else { - // Handle the exception gracefully without stopping // We assign default values to pbTime and pbSpeed pbTime = -1; // This determines if we will calculate differences or not!!! pbSpeed = 0.0f; - -#if DEBUG - Console.WriteLine($"CS2 Surf CAUGHT EXCEPTION >> DisplayCheckpointMessages -> An error occurred: {ex.Message}"); - Console.WriteLine($"CS2 Surf CAUGHT EXCEPTION >> DisplayCheckpointMessages -> An error occurred Player has no PB and therefore no Checkpoints | _player.Stats.PB[{style}].Checkpoint.Count = {_player.Stats.PB[style].Checkpoints.Count}"); -#endif } // Calculate differences in PB (PB - Current) if (pbTime != -1) { #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> Starting PB difference calculation... (pbTime != -1)"); + Console.WriteLine( + $"CS2 Surf DEBUG >> DisplayCheckpointMessages -> Starting PB difference calculation... (pbTime != -1)" + ); #endif // Reset the string - strPbDifference = ""; + strPbDifference = string.Empty; // Calculate the time difference if (pbTime - currentTime < 0.0) @@ -238,28 +563,46 @@ public void DisplayCheckpointMessages() // Calculate the speed difference if (pbSpeed - currentSpeed <= 0.0) { - strPbDifference += "(" + ChatColors.Green + "+" + ((pbSpeed - currentSpeed) * -1).ToString("0"); // We multiply by -1 to get the positive value + strPbDifference += + "(" + ChatColors.Green + "+" + ((pbSpeed - currentSpeed) * -1).ToString("0"); // We multiply by -1 to get the positive value } else if (pbSpeed - currentSpeed > 0.0) { - strPbDifference += "(" + ChatColors.Red + "-" + (pbSpeed - currentSpeed).ToString("0"); + strPbDifference += + "(" + ChatColors.Red + "-" + (pbSpeed - currentSpeed).ToString("0"); } strPbDifference += ChatColors.Default + ")"; } - if (SurfTimer.CurrentMap.WR[style].Ticks > 0) + if (SurfTimer.CurrentMap.WR[style].RunTime > 0) { // Calculate differences in WR (WR - Current) #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> Starting WR difference calculation... (SurfTimer.CurrentMap.WR[{style}].Ticks > 0)"); + Console.WriteLine( + $"CS2 Surf DEBUG >> DisplayCheckpointMessages -> Starting WR difference calculation... (SurfTimer.CurrentMap.WR[{style}].Ticks > 0)" + ); #endif - wrTime = SurfTimer.CurrentMap.WR[style].Checkpoints[playerCheckpoint].Ticks; - wrSpeed = (float)Math.Sqrt(SurfTimer.CurrentMap.WR[style].Checkpoints[playerCheckpoint].StartVelX * SurfTimer.CurrentMap.WR[style].Checkpoints[playerCheckpoint].StartVelX - + SurfTimer.CurrentMap.WR[style].Checkpoints[playerCheckpoint].StartVelY * SurfTimer.CurrentMap.WR[style].Checkpoints[playerCheckpoint].StartVelY - + SurfTimer.CurrentMap.WR[style].Checkpoints[playerCheckpoint].StartVelZ * SurfTimer.CurrentMap.WR[style].Checkpoints[playerCheckpoint].StartVelZ); + wrTime = SurfTimer.CurrentMap.WR[style].Checkpoints![playerCurrentCheckpoint].RunTime; + wrSpeed = (float) + Math.Sqrt( + SurfTimer.CurrentMap.WR[style].Checkpoints![playerCurrentCheckpoint].StartVelX + * SurfTimer.CurrentMap.WR[style].Checkpoints![playerCurrentCheckpoint].StartVelX + + SurfTimer.CurrentMap.WR[style].Checkpoints![playerCurrentCheckpoint].StartVelY + * SurfTimer + .CurrentMap + .WR[style] + .Checkpoints![playerCurrentCheckpoint] + .StartVelY + + SurfTimer.CurrentMap.WR[style].Checkpoints![playerCurrentCheckpoint].StartVelZ + * SurfTimer + .CurrentMap + .WR[style] + .Checkpoints![playerCurrentCheckpoint] + .StartVelZ + ); // Reset the string - strWrDifference = ""; + strWrDifference = string.Empty; // Calculate the WR time difference if (wrTime - currentTime < 0.0) @@ -275,25 +618,36 @@ public void DisplayCheckpointMessages() // Calculate the WR speed difference if (wrSpeed - currentSpeed <= 0.0) { - strWrDifference += "(" + ChatColors.Green + "+" + ((wrSpeed - currentSpeed) * -1).ToString("0"); // We multiply by -1 to get the positive value + strWrDifference += + "(" + ChatColors.Green + "+" + ((wrSpeed - currentSpeed) * -1).ToString("0"); // We multiply by -1 to get the positive value } else if (wrSpeed - currentSpeed > 0.0) { - strWrDifference += "(" + ChatColors.Red + "-" + (wrSpeed - currentSpeed).ToString("0"); + strWrDifference += + "(" + ChatColors.Red + "-" + (wrSpeed - currentSpeed).ToString("0"); } strWrDifference += ChatColors.Default + ")"; } // Print checkpoint message - _player.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["checkpoint_message", - playerCheckpoint, FormatTime(_player.Timer.Ticks), currentSpeed.ToString("0"), strPbDifference, strWrDifference]}" + _player.Controller.PrintToChat( + $"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["checkpoint_message", + playerCurrentCheckpoint, FormatTime(_player.Timer.Ticks), currentSpeed.ToString("0"), strPbDifference, strWrDifference]}" ); #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [TIME] PB: {pbTime} - CURR: {currentTime} = pbTime: {pbTime - currentTime}"); - Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [SPEED] PB: {pbSpeed} - CURR: {currentSpeed} = difference: {pbSpeed - currentSpeed}"); - Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [TIME] WR: {wrTime} - CURR: {currentTime} = difference: {wrTime - currentTime}"); - Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [SPEED] WR: {wrSpeed} - CURR: {currentSpeed} = difference: {wrSpeed - currentSpeed}"); + Console.WriteLine( + $"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [TIME] PB: {pbTime} - CURR: {currentTime} = pbTime: {pbTime - currentTime}" + ); + Console.WriteLine( + $"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [SPEED] PB: {pbSpeed} - CURR: {currentSpeed} = difference: {pbSpeed - currentSpeed}" + ); + Console.WriteLine( + $"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [TIME] WR: {wrTime} - CURR: {currentTime} = difference: {wrTime - currentTime}" + ); + Console.WriteLine( + $"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [SPEED] WR: {wrSpeed} - CURR: {currentSpeed} = difference: {wrSpeed - currentSpeed}" + ); #endif } } diff --git a/src/ST-Player/PlayerProfile.cs b/src/ST-Player/PlayerProfile.cs index 7f9124b..d9d0452 100644 --- a/src/ST-Player/PlayerProfile.cs +++ b/src/ST-Player/PlayerProfile.cs @@ -1,23 +1,18 @@ -using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SurfTimer.Data; +using SurfTimer.Shared.DTO; +using SurfTimer.Shared.Entities; +using System.Runtime.CompilerServices; namespace SurfTimer; -internal class PlayerProfile +public class PlayerProfile : PlayerProfileEntity { - public int ID { get; set; } = 0; - public string Name { get; set; } = ""; - public ulong SteamID { get; set; } = 0; - public string Country { get; set; } = ""; - public int JoinDate { get; set; } = 0; - public int LastSeen { get; set; } = 0; - public int Connections { get; set; } = 0; private readonly ILogger _logger; private readonly IDataAccessService _dataService; - public PlayerProfile(ulong steamId, string name = "", string country = "") + internal PlayerProfile(ulong steamId, string name = "", string country = "") { // Resolve the logger instance from the DI container _logger = SurfTimer.ServiceProvider.GetRequiredService>(); @@ -31,32 +26,31 @@ public PlayerProfile(ulong steamId, string name = "", string country = "") /// /// Deals with retrieving, creating and updating a Player's information in the database upon joining the server. - /// Automatically detects whether to use API Calls or Queries. /// /// Steam ID of the player /// Name of the player /// Country of the player /// PlayerProfile object - public static async Task CreateAsync(ulong steamId, string name = "", string country = "") + internal static async Task CreateAsync(ulong steamId, string name = "", string country = "") { var profile = new PlayerProfile(steamId, name, country); await profile.InitializeAsync(); return profile; } - private async Task InitializeAsync([CallerMemberName] string methodName = "") + internal async Task InitializeAsync([CallerMemberName] string methodName = "") { await GetPlayerProfile(); _logger.LogTrace("[{ClassName}] {MethodName} -> InitializeAsync -> [{ConnType}] We got ProfileID {ProfileID} ({PlayerName})", - nameof(PlayerProfile), methodName, Config.API.GetApiOnly() ? "API" : "DB", this.ID, this.Name + nameof(PlayerProfile), methodName, Config.Api.GetApiOnly() ? "API" : "DB", this.ID, this.Name ); } /// - /// Retrieves all the data for the player from the database. + /// Retrieves all the data for the player profile from the database. /// - public async Task GetPlayerProfile([CallerMemberName] string methodName = "") + internal async Task GetPlayerProfile([CallerMemberName] string methodName = "") { var profile = await _dataService.GetPlayerProfileAsync(this.SteamID); @@ -83,16 +77,16 @@ public async Task GetPlayerProfile([CallerMemberName] string methodName = "") } /// - /// Insert new player information into the database. + /// Insert new player profile information into the database. /// Retrieves the ID of the newly created player. /// - public async Task InsertPlayerProfile([CallerMemberName] string methodName = "") + internal async Task InsertPlayerProfile([CallerMemberName] string methodName = "") { - var profile = new PlayerProfileDataModel + var profile = new PlayerProfileDto { SteamID = this.SteamID, - Name = this.Name, - Country = this.Country + Name = this.Name!, + Country = this.Country! }; this.ID = await _dataService.InsertPlayerProfileAsync(profile); @@ -105,20 +99,20 @@ public async Task InsertPlayerProfile([CallerMemberName] string methodName = "") } /// - /// Updates the information in the database for the player. Increments `connections` and changes nickname. + /// Updates the information in the database for the player profile. Increments `connections` and changes nickname. /// /// Player Name - /// - public async Task UpdatePlayerProfile(string name, [CallerMemberName] string methodName = "") + internal async Task UpdatePlayerProfile(string name, [CallerMemberName] string methodName = "") { this.Name = name; - await _dataService.UpdatePlayerProfileAsync(new PlayerProfileDataModel + var dto = new PlayerProfileDto { - ID = this.ID, SteamID = this.SteamID, Name = this.Name, - Country = this.Country - }); + Country = this.Country! + }; + + await _dataService.UpdatePlayerProfileAsync(dto, this.ID); #if DEBUG _logger.LogDebug("[{ClassName}] {MethodName} -> UpdatePlayerProfile -> [{ConnType}] Updated player {PlayerName} ({SteamID}) with ID {ProfileID}.", diff --git a/src/ST-Player/PlayerStats/Checkpoint.cs b/src/ST-Player/PlayerStats/Checkpoint.cs deleted file mode 100644 index 6470561..0000000 --- a/src/ST-Player/PlayerStats/Checkpoint.cs +++ /dev/null @@ -1,30 +0,0 @@ -public class Checkpoint -{ - public int CP { get; set; } - public int Ticks { get; set; } - public float StartVelX { get; set; } - public float StartVelY { get; set; } - public float StartVelZ { get; set; } - public float EndVelX { get; set; } - public float EndVelY { get; set; } - public float EndVelZ { get; set; } - public int EndTouch { get; set; } - public int Attempts { get; set; } - public int ID { get; set; } - - public Checkpoint() { } - - public Checkpoint(int cp, int ticks, float startVelX, float startVelY, float startVelZ, float endVelX, float endVelY, float endVelZ, int endTouch, int attempts) - { - CP = cp; - Ticks = ticks; - StartVelX = startVelX; - StartVelY = startVelY; - StartVelZ = startVelZ; - EndVelX = endVelX; - EndVelY = endVelY; - EndVelZ = endVelZ; - EndTouch = endTouch; - Attempts = attempts; - } -} diff --git a/src/ST-Player/PlayerStats/CurrentRun.cs b/src/ST-Player/PlayerStats/CurrentRun.cs index 8ebd888..5bf1485 100644 --- a/src/ST-Player/PlayerStats/CurrentRun.cs +++ b/src/ST-Player/PlayerStats/CurrentRun.cs @@ -1,327 +1,201 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; +using CounterStrikeSharp.API; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SurfTimer.Data; +using SurfTimer.Shared.DTO; +using SurfTimer.Shared.Entities; +using System.Diagnostics; +using System.Runtime.CompilerServices; namespace SurfTimer; /// /// This class stores data for the current run. /// -internal class CurrentRun : RunStats +public class CurrentRun : RunStatsEntity { private readonly ILogger _logger; private readonly IDataAccessService _dataService; + public Dictionary Checkpoints { get; set; } - public CurrentRun() : base() + internal CurrentRun() { _logger = SurfTimer.ServiceProvider.GetRequiredService>(); _dataService = SurfTimer.ServiceProvider.GetRequiredService(); - } - public override void Reset() - { - base.Reset(); - // Reset other properties as needed (specific for this class) + Checkpoints = new Dictionary(); } /// - /// Saves the player's run to the database. + /// Saves the player's run to the database. + /// Supports all types of runs Map/Bonus/Stage. /// /// Player object /// Bonus number /// Stage number /// Ticks for the run - used for Stage and Bonus entries - public async Task SaveMapTime(Player player, int bonus = 0, int stage = 0, int run_ticks = -1, [CallerMemberName] string methodName = "") + internal async Task SaveMapTime(Player player, short bonus = 0, short stage = 0, int run_ticks = -1, [CallerMemberName] string methodName = "") { string replay_frames = ""; + int style = player.Timer.Style; + int mapTimeId = 0; + short recType; - /* Test Time Saving */ - if (methodName != "TestSetPb") - replay_frames = player.ReplayRecorder.TrimReplay(player, stage != 0 ? 2 : bonus != 0 ? 1 : 0, stage == SurfTimer.CurrentMap.Stages); + if (stage != 0) + { + recType = 2; // Stage run + } + else if (bonus != 0) + { + recType = 1; // Bonus run + } + else + { + recType = 0; // Map run + } + /// Test Time Saving: if (methodName != "TestSetPb") + replay_frames = player.ReplayRecorder.TrimReplay(player, recType, stage == SurfTimer.CurrentMap.Stages); - _logger.LogTrace("[{ClassName}] {MethodName} -> Sending total of {Frames} replay frames.", - nameof(CurrentRun), methodName, replay_frames.Length); + _logger.LogTrace("[{ClassName}] {MethodName} -> Sending total of {Frames} serialized and compressed replay frames.", + nameof(CurrentRun), methodName, replay_frames.Length + ); var stopwatch = Stopwatch.StartNew(); - int recType = stage != 0 ? 2 : bonus != 0 ? 1 : 0; - - var mapTime = new MapTimeDataModel + var mapTime = new MapTimeRunDataDto { - PlayerId = player.Profile.ID, - MapId = player.CurrMap.ID, + PlayerID = player.Profile.ID, + MapID = SurfTimer.CurrentMap.ID, Style = player.Timer.Style, Type = recType, Stage = stage != 0 ? stage : bonus, - Ticks = run_ticks == -1 ? this.Ticks : run_ticks, + RunTime = run_ticks == -1 ? this.RunTime : run_ticks, StartVelX = this.StartVelX, StartVelY = this.StartVelY, StartVelZ = this.StartVelZ, EndVelX = this.EndVelX, EndVelY = this.EndVelY, EndVelZ = this.EndVelZ, - ReplayFramesBase64 = replay_frames, - Checkpoints = this.Checkpoints // Test out + ReplayFrames = replay_frames, + Checkpoints = this.Checkpoints }; - /* - _logger.LogDebug( - "[{ClassName}] {MethodName} -> Sending data:\n" + - " PlayerId: {PlayerId}\n" + - " MapId: {MapId}\n" + - " Style: {Style}\n" + - " Type: {Type}\n" + - " Stage: {Stage}\n" + - " Ticks: {Ticks}\n" + - " StartVel: ({StartVelX}, {StartVelY}, {StartVelZ})\n" + - " EndVel: ({EndVelX}, {EndVelY}, {EndVelZ})\n" + - " ReplayFramesBase64: {ReplayFrames}\n" + - " Checkpoints: {CheckpointsCount}", - nameof(CurrentRun), methodName, - mapTime.PlayerId, - mapTime.MapId, - mapTime.Style, - mapTime.Type, - mapTime.Stage, - mapTime.Ticks, - mapTime.StartVelX, mapTime.StartVelY, mapTime.StartVelZ, - mapTime.EndVelX, mapTime.EndVelY, mapTime.EndVelZ, - mapTime.ReplayFramesBase64?.Length ?? 0, // log length to avoid dumping huge string - mapTime.Checkpoints?.Count ?? 0 - ); - */ + switch (recType) + { + case 0: + mapTimeId = player.Stats.PB[style].ID; + break; + case 1: + mapTimeId = player.Stats.BonusPB[bonus][style].ID; + break; + case 2: + mapTimeId = player.Stats.StagePB[stage][style].ID; + break; + } - await _dataService.InsertMapTimeAsync(mapTime); + if (mapTimeId <= 0) + mapTimeId = await _dataService.InsertMapTimeAsync(mapTime); + else + _ = await _dataService.UpdateMapTimeAsync(mapTime, mapTimeId); - if (recType == 0 && !Config.API.GetApiOnly()) - await SaveCurrentRunCheckpoints(player, true); - await player.CurrMap.LoadMapRecordRuns(); - await player.Stats.LoadPlayerMapTimesData(player); + // Reload the times for the map + await SurfTimer.CurrentMap.LoadMapRecordRuns(); - stopwatch.Stop(); - _logger.LogInformation("[{Class}] {Method} -> Finished SaveMapTime for '{Name}' in {Elapsed}ms | API = {API}", - nameof(CurrentRun), methodName, player.Profile.Name, stopwatch.ElapsedMilliseconds, Config.API.GetApiOnly() + _logger.LogTrace("[{ClassName}] {MethodName} -> Loading data for run {ID} with type {Type}.", + nameof(CurrentRun), methodName, mapTimeId, recType ); - } - /* - public async Task SaveMapTime(Player player, int bonus = 0, int stage = 0, int run_ticks = -1, [CallerMemberName] string methodName = "") + // Reload the player PB time (could possibly be skipped as we have mapTimeId after inserting) + switch (recType) { - string replay_frames = player.ReplayRecorder.TrimReplay(player, stage != 0 ? 2 : bonus != 0 ? 1 : 0, stage == SurfTimer.CurrentMap.Stages); - - _logger.LogTrace("[{ClassName}] {MethodName} -> SaveMapTime -> Sending total of {ReplayFramesTotal} replay frames.", - nameof(CurrentRun), methodName, replay_frames.Length - ); - - var stopwatch = Stopwatch.StartNew(); - - if (Config.API.GetApiOnly()) - { - return; - } - else - { - await InsertMapTime(player, bonus, stage, run_ticks, replay_frames, true); - - if (stage != 0 || bonus != 0) - { - _logger.LogTrace("[{ClassName}] {MethodName} -> Inserted an entry for {Type} {Number} - {Ticks}", - nameof(CurrentRun), methodName, (stage != 0 ? "Stage" : "Bonus"), (stage != 0 ? stage : bonus), run_ticks - ); - } - else - { - await SaveCurrentRunCheckpoints(player, true); // Save this run's checkpoints - } - - await player.CurrMap.LoadMapRecordRuns(); // Reload the times for the Map - } - - stopwatch.Stop(); - _logger.LogInformation("[{ClassName}] {MethodName} -> Finished SaveMapTime for player '{Name}' in {ElapsedMilliseconds}ms | API = {API}", - nameof(CurrentRun), methodName, player.Profile.Name, stopwatch.ElapsedMilliseconds, Config.API.GetApiOnly() - ); + case 0: + player.Stats.PB[player.Timer.Style].ID = mapTimeId; + await player.Stats.PB[player.Timer.Style].LoadPlayerSpecificMapTimeData(player); + break; + case 1: + player.Stats.BonusPB[bonus][player.Timer.Style].ID = mapTimeId; + await player.Stats.BonusPB[bonus][player.Timer.Style].LoadPlayerSpecificMapTimeData(player); + break; + case 2: + player.Stats.StagePB[stage][player.Timer.Style].ID = mapTimeId; + await player.Stats.StagePB[stage][player.Timer.Style].LoadPlayerSpecificMapTimeData(player); + break; } - */ + + stopwatch.Stop(); + _logger.LogInformation("[{Class}] {Method} -> Finished SaveMapTime for '{Name}' (ID {ID}) in {Elapsed}ms | API = {API}", + nameof(CurrentRun), methodName, player.Profile.Name, mapTimeId, stopwatch.ElapsedMilliseconds, Config.Api.GetApiOnly() + ); + } /// - /// Saves the CurrentRun of the player to the database. Does NOT support Bonus entries yet. + /// Deals with saving a Stage MapTime (Type 2) in the Database. + /// Should deal with `IsStageMode` runs, Stages during Map Runs and also Last Stage. /// - public async Task InsertMapTime(Player player, int bonus = 0, int stage = 0, int run_ticks = -1, string replay_frames = "", bool reloadData = false, [CallerMemberName] string methodName = "") + /// Player object + /// Stage to save + /// Is it the last stage? + /// Run Time (Ticks) for the stage run + internal static async Task SaveStageTime(Player player, short stage = -1, int stage_run_time = -1, bool saveLastStage = false) { - int playerId = player.Profile.ID; - int mapId = player.CurrMap.ID; - int style = player.Timer.Style; - int ticks = run_ticks == -1 ? this.Ticks : run_ticks; - int type = stage != 0 ? 2 : bonus != 0 ? 1 : 0; - float startVelX = this.StartVelX; - float startVelY = this.StartVelY; - float startVelZ = this.StartVelZ; - float endVelX = this.EndVelX; - float endVelY = this.EndVelY; - float endVelZ = this.EndVelZ; - - var stopwatch = Stopwatch.StartNew(); - - if (Config.API.GetApiOnly()) - { - // API Insert map goes here - } - else +#if DEBUG + _logger.LogTrace("[{Class}] -> SaveStageTime received: Name = {Name} | Stage = {Stage} | RunTime = {RunTime} | IsLastStage = {IsLastStage}", + nameof(CurrentRun), player.Profile.Name, stage, stage_run_time, saveLastStage + ); +#endif + int pStyle = player.Timer.Style; + if ( + stage_run_time < SurfTimer.CurrentMap.StageWR[stage][pStyle].RunTime || + SurfTimer.CurrentMap.StageWR[stage][pStyle].ID == -1 || + player.Stats.StagePB[stage][pStyle] != null && player.Stats.StagePB[stage][pStyle].RunTime > stage_run_time || + player.Stats.StagePB[stage][pStyle] != null && player.Stats.StagePB[stage][pStyle].ID == -1 + ) { - // int updatePlayerRunTask = await SurfTimer.DB.WriteAsync( - var (updatePlayerRunTask, lastId) = await SurfTimer.DB.WriteAsync( - string.Format(Config.MySQL.Queries.DB_QUERY_CR_INSERT_TIME, playerId, mapId, style, type, type == 2 ? stage : type == 1 ? bonus : 0, ticks, startVelX, startVelY, startVelZ, endVelX, endVelY, endVelZ, (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), replay_frames)); - if (updatePlayerRunTask <= 0) + if (stage_run_time < SurfTimer.CurrentMap.StageWR[stage][pStyle].RunTime) // Player beat the Stage WR { - _logger.LogError("[{ClassName}] {MethodName} -> InsertMapTime -> Failed to insert/update player run in database. Player: {Name} ({SteamID})", - nameof(CurrentRun), methodName, player.Profile.Name, player.Profile.SteamID + int timeImprove = SurfTimer.CurrentMap.StageWR[stage][pStyle].RunTime - stage_run_time; + Server.PrintToChatAll($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["stagewr_improved", + player.Controller.PlayerName, stage, PlayerHud.FormatTime(stage_run_time), PlayerHud.FormatTime(timeImprove), PlayerHud.FormatTime(SurfTimer.CurrentMap.StageWR[stage][pStyle].RunTime)]}" ); - Exception ex = new($"CS2 Surf ERROR >> internal class CurrentRun -> public async Task InsertMapTime -> Failed to insert/update player run in database. Player: {player.Profile.Name} ({player.Profile.SteamID})"); - throw ex; } - - if (reloadData && type == 0) + else if (SurfTimer.CurrentMap.StageWR[stage][pStyle].ID == -1) // No Stage record was set on the map { - _logger.LogInformation("[{ClassName}] {MethodName} -> InsertMapTime -> Will reload MapTime (Type {type}) data for '{Name}' (ID {MapTimeID}))", - nameof(CurrentRun), methodName, type, player.Profile.Name, player.Stats.PB[player.Timer.Style].ID + Server.PrintToChatAll($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["stagewr_set", + player.Controller.PlayerName, stage, PlayerHud.FormatTime(stage_run_time)]}" ); - await player.Stats.PB[style].LoadPlayerSpecificMapTimeData(player); // Load the Map MapTime PB data again (will refresh the MapTime ID for the Checkpoints query) } - else if (reloadData && type == 1) + else if (player.Stats.StagePB[stage][pStyle] != null && player.Stats.StagePB[stage][pStyle].ID == -1) // Player first Stage personal best { - _logger.LogInformation("[{ClassName}] {MethodName} -> InsertMapTime -> Will reload Bonus MapTime (Type {type}) data for '{Name}' (ID {MapTimeID}))", - nameof(CurrentRun), methodName, type, player.Profile.Name, player.Stats.BonusPB[bonus][style].ID + player.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["stagepb_set", + stage, PlayerHud.FormatTime(stage_run_time)]}" ); - await player.Stats.BonusPB[bonus][style].LoadPlayerSpecificMapTimeData(player); // Load the Bonus MapTime PB data again (will refresh the MapTime ID) } - else if (reloadData && type == 2) + else if (player.Stats.StagePB[stage][pStyle] != null && player.Stats.StagePB[stage][pStyle].RunTime > stage_run_time) // Player beating their existing Stage personal best { - _logger.LogInformation("[{ClassName}] {MethodName} -> InsertMapTime -> Will reload Stage MapTime (Type {type}) data for '{Name}' (ID {MapTimeID}))", - nameof(CurrentRun), methodName, type, player.Profile.Name, player.Stats.StagePB[stage][style].ID + int timeImprove = player.Stats.StagePB[stage][pStyle].RunTime - stage_run_time; + Server.PrintToChatAll($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["stagepb_improved", + player.Controller.PlayerName, stage, PlayerHud.FormatTime(stage_run_time), PlayerHud.FormatTime(timeImprove), PlayerHud.FormatTime(player.Stats.StagePB[stage][pStyle].RunTime)]}" ); - await player.Stats.StagePB[stage][style].LoadPlayerSpecificMapTimeData(player); // Load the Stage MapTime PB data again (will refresh the MapTime ID) } - } - - stopwatch.Stop(); - _logger.LogInformation("[{ClassName}] {MethodName} -> Finished InsertMapTime for player '{Name}' in {ElapsedMilliseconds}ms | API = {API}", - nameof(CurrentRun), methodName, player.Profile.Name, stopwatch.ElapsedMilliseconds, Config.API.GetApiOnly() - ); - } - - /// - /// Saves the `CurrentRunCheckpoints` dictionary to the database - /// API deals with this when sending a SaveMapTime of type 0, so we do not have an endpoint for it - /// - /// Player object - /// Whether to reload the PersonalBest Checkpoints data for the Player. - public async Task SaveCurrentRunCheckpoints(Player player, bool reloadData = false, [CallerMemberName] string methodName = "") - { - _logger.LogInformation("[{ClassName}] {MethodName} -> Saving {Count} checkpoints...", - nameof(CurrentRun), methodName, this.Checkpoints.Count); - - var stopwatch = Stopwatch.StartNew(); - - var checkpoints = this.Checkpoints.Select(cp => new Checkpoint - { - CP = cp.Key, - Ticks = cp.Value.Ticks, - EndTouch = cp.Value.EndTouch, - StartVelX = cp.Value.StartVelX, - StartVelY = cp.Value.StartVelY, - StartVelZ = cp.Value.StartVelZ, - EndVelX = cp.Value.EndVelX, - EndVelY = cp.Value.EndVelY, - EndVelZ = cp.Value.EndVelZ, - Attempts = cp.Value.Attempts - }); - - int mapTimeId = player.Stats.PB[player.Timer.Style].ID; - - // await _dataService.SaveRunCheckpointsAsync(mapTimeId, checkpoints); - - this.Checkpoints.Clear(); - - if (reloadData) - { - _logger.LogInformation("[{ClassName}] {MethodName} -> Reloading Checkpoints data for '{Name}' (ID {MapTimeID})", - nameof(CurrentRun), methodName, player.Profile.Name, mapTimeId); - await player.Stats.PB[player.Timer.Style].LoadCheckpoints(); - } - - stopwatch.Stop(); - _logger.LogInformation("[{ClassName}] {MethodName} -> Finished saving checkpoints for '{Name}' in {Elapsed}ms | API = {API}", - nameof(CurrentRun), methodName, player.Profile.Name, stopwatch.ElapsedMilliseconds, Config.API.GetApiOnly()); - } - - /* Delete after testing it using the ApiDataAccessService - public async Task SaveCurrentRunCheckpoints(Player player, bool reloadData = false, [CallerMemberName] string methodName = "") - { - _logger.LogInformation("[{ClassName}] {MethodName} -> SaveCurrentRunCheckpoints -> Will send {ThisRunCheckpoints} ({CheckpointsCount}) checkpoints to DB....", - nameof(CurrentRun), methodName, this.Checkpoints.Count, this.Checkpoints.Count - ); - var stopwatch = Stopwatch.StartNew(); - int style = player.Timer.Style; - int mapTimeId = player.Stats.PB[style].ID; - List commands = new List(); - // Loop through the checkpoints and insert/update them in the database for the run - foreach (var item in this.Checkpoints) - { - int cp = item.Key; - int ticks = item.Value!.Ticks; - int endTouch = item.Value!.EndTouch; - double startVelX = item.Value!.StartVelX; - double startVelY = item.Value!.StartVelY; - double startVelZ = item.Value!.StartVelZ; - double endVelX = item.Value!.EndVelX; - double endVelY = item.Value!.EndVelY; - double endVelZ = item.Value!.EndVelZ; - int attempts = item.Value!.Attempts; + player.ReplayRecorder.IsSaving = true; -#if DEBUG - _logger.LogDebug("[{ClassName}] {MethodName} -> SaveCurrentRunCheckpoints -> CP: {Checkpoint} | MapTime ID: {MapTimeID} | Time: {Time} | Ticks: {Ticks} | SVX {StartVelX} | SVY {StartVelY} | SVZ {StartVelZ} | EVX {EndVelX} | EVY {EndVelY} | EVZ {EndVelZ}", - nameof(CurrentRun), methodName, cp, mapTimeId, endTouch, ticks, startVelX, startVelY, startVelZ, endVelX, endVelY, endVelZ - ); - _logger.LogDebug("Query to send:\n{Query}", - string.Format(Config.MySQL.Queries.DB_QUERY_CR_INSERT_CP, - mapTimeId, cp, ticks, startVelX, startVelY, startVelZ, endVelX, endVelY, endVelZ, attempts, endTouch) - ); -#endif - - if (item.Value != null && player.Controller.PlayerPawn.Value != null) - { - string command = string.Format( - Config.MySQL.Queries.DB_QUERY_CR_INSERT_CP, - mapTimeId, cp, ticks, startVelX, startVelY, startVelZ, endVelX, endVelY, endVelZ, attempts, endTouch - ); - commands.Add(command); - } + // Save stage run + await player.Stats.ThisRun.SaveMapTime(player, stage: stage, run_ticks: stage_run_time); // Save the Stage MapTime PB data } - await SurfTimer.DB.TransactionAsync(commands); - this.Checkpoints.Clear(); - - if (reloadData) + else if (stage_run_time > SurfTimer.CurrentMap.StageWR[stage][pStyle].RunTime && player.Timer.IsStageMode) // Player is behind the Stage WR for the map { - _logger.LogInformation("[{ClassName}] {MethodName} -> SaveCurrentRunCheckpoints -> Will reload Checkpoints data for '{Name}' (ID {MapTimeID})", - nameof(CurrentRun), methodName, player.Profile.Name, player.Stats.PB[player.Timer.Style].ID + int timeImprove = stage_run_time - SurfTimer.CurrentMap.StageWR[stage][pStyle].RunTime; + player.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["stagewr_missed", + stage, PlayerHud.FormatTime(stage_run_time), PlayerHud.FormatTime(timeImprove), PlayerHud.FormatTime(SurfTimer.CurrentMap.StageWR[stage][pStyle].RunTime)]}" ); - await player.Stats.PB[player.Timer.Style].LoadCheckpoints(); // Load the Checkpoints data again } - - stopwatch.Stop(); - _logger.LogInformation("[{ClassName}] {MethodName} -> Finished SaveCurrentRunCheckpoints(reloadData = {reloadData}) for player '{Name}' in {ElapsedMilliseconds}ms | API = {API}", - nameof(CurrentRun), methodName, reloadData, player.Profile.Name, stopwatch.ElapsedMilliseconds, Config.API.GetApiOnly() - ); } -*/ - public void PrintSituations(Player player) + + public static void PrintSituations(Player player) { Console.WriteLine($"========================== FOUND SITUATIONS =========================="); for (int i = 0; i < player.ReplayRecorder.Frames.Count; i++) diff --git a/src/ST-Player/PlayerStats/PersonalBest.cs b/src/ST-Player/PlayerStats/PersonalBest.cs index b2155a3..66be387 100644 --- a/src/ST-Player/PlayerStats/PersonalBest.cs +++ b/src/ST-Player/PlayerStats/PersonalBest.cs @@ -1,7 +1,8 @@ -using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SurfTimer.Data; +using SurfTimer.Shared.Entities; +using System.Runtime.CompilerServices; namespace SurfTimer; @@ -9,18 +10,13 @@ namespace SurfTimer; /// As the PersonalBest object is being used for each different style, we shouldn't need a separate `Style` variable in here because each style entry will have unique ID in the Database /// and will therefore be a unique PersonalBest entry. ///
-internal class PersonalBest : RunStats +public class PersonalBest : MapTimeRunDataEntity { - public int ID { get; set; } = -1; // Exclude from constructor, retrieve from Database when loading/saving - public int Rank { get; set; } = -1; // Exclude from constructor, retrieve from Database when loading/saving - public int Type { get; set; } = -1; // Identifies bonus # - 0 for map time -> huh, why o_O? - public string Name { get; set; } = ""; // This is used only for WRs + public Dictionary? Checkpoints { get; set; } private readonly ILogger _logger; private readonly IDataAccessService _dataService; - // Add other properties as needed - // Constructor - public PersonalBest() : base() + internal PersonalBest() : base() { // Resolve the logger instance from the DI container _logger = SurfTimer.ServiceProvider.GetRequiredService>(); @@ -31,12 +27,11 @@ public PersonalBest() : base() /// Loads the Checkpoint data for the given MapTime_ID. Used for loading player's personal bests and Map's world records. /// Bonus and Stage runs should NOT have any checkpoints. /// - public async Task LoadCheckpoints([CallerMemberName] string methodName = "") + internal async Task LoadCheckpoints([CallerMemberName] string methodName = "") { - // 1) ask the data service for your checkpoints var cps = await _dataService.LoadCheckpointsAsync(this.ID); - // 2) if none, just return + // If nothing found, log and return if (cps == null || cps.Count == 0) { _logger.LogInformation( @@ -46,10 +41,8 @@ public async Task LoadCheckpoints([CallerMemberName] string methodName = "") return; } - // 3) otherwise assign this.Checkpoints = cps; - // 4) log how many you got _logger.LogInformation( "[{ClassName}] {MethodName} -> Loaded {Count} checkpoints for run {RunId}.", nameof(PersonalBest), methodName, cps.Count, this.ID @@ -62,9 +55,8 @@ public async Task LoadCheckpoints([CallerMemberName] string methodName = "") /// Should be used to reload data from a specific `PersonalBest` object /// /// Player object - public async Task LoadPlayerSpecificMapTimeData(Player player, [CallerMemberName] string methodName = "") + internal async Task LoadPlayerSpecificMapTimeData(Player player, [CallerMemberName] string methodName = "") { - // 1) call the data service, passing only the primitives: var model = await _dataService.LoadPersonalBestRunAsync( pbId: this.ID == -1 ? (int?)null : this.ID, playerId: player.Profile.ID, @@ -73,20 +65,19 @@ public async Task LoadPlayerSpecificMapTimeData(Player player, [CallerMemberName style: player.Timer.Style ); - // 2) if nothing found, log & return + // If nothing found, log and return if (model == null) { _logger.LogTrace( - "[{ClassName}] {MethodName} -> No personal best found for player {Player} (ID={Id}).", + "[{ClassName}] {MethodName} -> No personal best found for player {Player} (ID={Id} ; Type={Type}).", nameof(PersonalBest), methodName, - player.Profile.Name, player.Profile.ID + player.Profile.Name, player.Profile.ID, this.Type ); return; } - // 3) map back into your instance this.ID = model.ID; - this.Ticks = model.Ticks; + this.RunTime = model.RunTime; this.Rank = model.Rank; this.StartVelX = model.StartVelX; this.StartVelY = model.StartVelY; @@ -95,7 +86,7 @@ public async Task LoadPlayerSpecificMapTimeData(Player player, [CallerMemberName this.EndVelY = model.EndVelY; this.EndVelZ = model.EndVelZ; this.RunDate = model.RunDate; - // this.ReplayFrames = model.ReplayFrames; // Won't work with MySQL load? + this.ReplayFrames = model.ReplayFrames; // Won't work with MySQL load? - Not tested _logger.LogDebug( "[{ClassName}] {MethodName} -> Loaded PB run {RunId} for {Player}.", @@ -103,75 +94,4 @@ public async Task LoadPlayerSpecificMapTimeData(Player player, [CallerMemberName this.ID, player.Profile.Name ); } - - /* Delete after testing with API? - public async Task PB_LoadPlayerSpecificMapTimeData(Player player, [CallerMemberName] string methodName = "") - { - // Console.WriteLine($"CS2 Surf ERROR >> internal class PersonalBest -> public async Task PB_LoadPlayerSpecificMapTimeData -> QUERY:\n{string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_RUNTIME, player.Profile.ID, player.CurrMap.ID, 0, player.Timer.Style)}"); - // using (var results = await SurfTimer.DB.QueryAsync(string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_RUNTIME, player.Profile.ID, player.CurrMap.ID, 0, player.Timer.Style))) - if (this == null) - { - #if DEBUG - _logger.LogDebug("[{ClassName}] {MethodName} -> PB_LoadPlayerSpecificMapTimeData -> PersonalBest object is null.", - nameof(PersonalBest), methodName - ); - #endif - - return; - } - - MySqlConnector.MySqlDataReader? results = null; - - // Console.WriteLine(string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_TYPE_RUNTIME, player.Profile.ID, SurfTimer.CurrentMap.ID, this.Type, player.Timer.Style)); - - if (this.ID == -1) - results = await SurfTimer.DB.QueryAsync(string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_TYPE_RUNTIME, player.Profile.ID, SurfTimer.CurrentMap.ID, this.Type, player.Timer.Style)); - else - results = await SurfTimer.DB.QueryAsync(string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_SPECIFIC_MAPTIME_DATA, this.ID)); - - #if DEBUG - Console.WriteLine($"----> public async Task PB_LoadPlayerSpecificMapTimeData -> this.ID {this.ID} "); - Console.WriteLine($"----> public async Task PB_LoadPlayerSpecificMapTimeData -> this.Ticks {this.Ticks} "); - Console.WriteLine($"----> public async Task PB_LoadPlayerSpecificMapTimeData -> this.RunDate {this.RunDate} "); - #endif - - if (results == null || !results.HasRows) - { - // #if DEBUG - _logger.LogTrace("[{ClassName}] {MethodName} -> PB_LoadPlayerSpecificMapTimeData -> No MapTime data found for '{playerName}' ({playerID}). (Results Null? {IsNull})", - nameof(PersonalBest), methodName, player.Profile.Name, player.Profile.ID, results == null - ); - // #endif - - return; - } - - while (results.Read()) - { - #if DEBUG - _logger.LogDebug("[{ClassName}] {MethodName} -> PB_LoadPlayerSpecificMapTimeData -> Loading MapTime Run: RunID {RunID} | RunTicks {RunTicks} | StartVelX {StartVelX} | StartVelY {StartVelY}.", - nameof(PersonalBest), methodName, results.GetInt32("id"), results.GetInt32("run_time"), results.GetFloat("start_vel_x"), results.GetFloat("start_vel_y") - ); - #endif - - this.ID = results.GetInt32("id"); - this.Ticks = results.GetInt32("run_time"); - this.Rank = results.GetInt32("rank"); - this.StartVelX = (float)results.GetDouble("start_vel_x"); - this.StartVelY = (float)results.GetDouble("start_vel_y"); - this.StartVelZ = (float)results.GetDouble("start_vel_z"); - this.EndVelX = (float)results.GetDouble("end_vel_x"); - this.EndVelY = (float)results.GetDouble("end_vel_y"); - this.EndVelZ = (float)results.GetDouble("end_vel_z"); - this.RunDate = results.GetInt32("run_date"); - } - - // #if DEBUG - _logger.LogDebug("[{ClassName}] {MethodName} -> PB_LoadPlayerSpecificMapTimeData -> MapTime ID {ID} (Type: {Type}) loaded for '{PlayerName}' with time {RunTime}", - nameof(PersonalBest), methodName, this.ID, this.Type, player.Profile.Name, PlayerHUD.FormatTime(this.Ticks) - ); - // #endif - } - */ - } \ No newline at end of file diff --git a/src/ST-Player/PlayerStats/PlayerStats.cs b/src/ST-Player/PlayerStats/PlayerStats.cs index 3158cdc..fc55c05 100644 --- a/src/ST-Player/PlayerStats/PlayerStats.cs +++ b/src/ST-Player/PlayerStats/PlayerStats.cs @@ -1,183 +1,76 @@ -using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using SurfTimer.Data; +using System.Runtime.CompilerServices; namespace SurfTimer; -internal class PlayerStats +public class PlayerStats { - // To-Do: Each stat should be a class of its own, with its own methods and properties - easier to work with. - // Temporarily, we store ticks + basic info so we can experiment - // These account for future style support and a relevant index. - - // /// - // /// Stage Personal Best - Refer to as StagePB[style][stage#] - // /// To-do: DEPRECATE THIS WHEN IMPLEMENTING STAGES, FOLLOW NEW PB STRUCTURE - // /// - // public int[,] StagePB { get; set; } = { { 0, 0 } }; - // /// - // /// Stage Personal Best - Refer to as StageRank[style][stage#] - // /// To-do: DEPRECATE THIS WHEN IMPLEMENTING STAGES, FOLLOW NEW PB STRUCTURE - // /// - // public int[,] StageRank { get; set; } = { { 0, 0 } }; - /// /// Map Personal Best - Refer to as PB[style] /// public Dictionary PB { get; set; } = new Dictionary(); /// /// Bonus Personal Best - Refer to as BonusPB[bonus#][style] - /// Need to figure out a way to NOT hardcode to `32` but to total amount of bonuses /// - public Dictionary[] BonusPB { get; set; } = new Dictionary[32]; + public Dictionary[] BonusPB { get; set; } /// /// Stage Personal Best - Refer to as StagePB[stage#][style] - /// Need to figure out a way to NOT hardcode to `32` but to total amount of stages /// - public Dictionary[] StagePB { get; set; } = new Dictionary[32]; + public Dictionary[] StagePB { get; set; } /// /// This object tracks data for the Player's current run. /// public CurrentRun ThisRun { get; set; } = new CurrentRun(); + private readonly ILogger _logger; private readonly IDataAccessService _dataService; - - // Initialize PersonalBest for each `style` (e.g., 0 for normal) - // Here we can loop through all available styles at some point and initialize them - public PlayerStats([CallerMemberName] string methodName = "") + internal PlayerStats([CallerMemberName] string methodName = "") { // Resolve the logger instance from the DI container _logger = SurfTimer.ServiceProvider.GetRequiredService>(); _dataService = SurfTimer.ServiceProvider.GetRequiredService(); - // Initialize MapPB for each style - foreach (int style in Config.Styles) - { - PB[style] = new PersonalBest(); - PB[style].Type = 0; - } - - int initialized = 0; - for (int i = 0; i < 32; i++) - { - this.BonusPB[i] = new Dictionary(); - this.BonusPB[i][0] = new PersonalBest(); - this.BonusPB[i][0].Type = 1; + // Initialize PB variables + this.StagePB = new Dictionary[SurfTimer.CurrentMap.Stages + 1]; + this.BonusPB = new Dictionary[SurfTimer.CurrentMap.Bonuses + 1]; + int initStage = 0; + int initBonus = 0; - this.StagePB[i] = new Dictionary(); - this.StagePB[i][0] = new PersonalBest(); - this.StagePB[i][0].Type = 2; - initialized++; - } - _logger.LogTrace("[{ClassName}] {MethodName} -> PlayerStats -> Initialized {initialized} Stages and Bonuses", - nameof(PlayerStats), methodName, initialized - ); - } - - // API - Can be replaced with `ENDPOINT_MAP_GET_PB_BY_PLAYER` - public async void LoadMapTime(Player player, int style = 0, [CallerMemberName] string methodName = "") - { - var player_maptime = await ApiMethod.GET($"/surftimer/playerspecificdata?player_id={player.Profile.ID}&map_id={player.CurrMap.ID}&style={style}&type=0"); - if (player_maptime == null) - { - _logger.LogTrace("[{ClassName}] {MethodName} -> LoadMapTime -> No MapTime data found for Player {PlayerName} (ID {PlayerID}).", - nameof(PlayerStats), methodName, player.Profile.Name, player.Profile.ID - ); - return; - } - - PB[style].ID = player_maptime.id; - PB[style].Ticks = player_maptime.run_time; - PB[style].Type = player_maptime.type; - PB[style].StartVelX = player_maptime.start_vel_x; - PB[style].StartVelY = player_maptime.start_vel_y; - PB[style].StartVelZ = player_maptime.start_vel_z; - PB[style].EndVelX = player_maptime.end_vel_x; - PB[style].EndVelY = player_maptime.end_vel_y; - PB[style].EndVelZ = player_maptime.end_vel_z; - // PB[style].RunDate = player_maptime.run_date ?? 0; - PB[style].RunDate = player_maptime.run_date; - - if (player_maptime.checkpoints == null) + foreach (int style in Config.Styles) { - _logger.LogTrace("[{ClassName}] {MethodName} -> LoadMapTime -> No Checkpoints data found for Player {PlayerName} (ID {PlayerID}).", - nameof(PlayerStats), methodName, player.Profile.Name, player.Profile.ID - ); - return; - } + PB[style] = new PersonalBest { Type = 0 }; - foreach (var cp in player_maptime.checkpoints) - { - PB[style].Checkpoints[cp.cp] = new Checkpoint(cp.cp, cp.run_time, cp.start_vel_x, cp.start_vel_y, cp.start_vel_z, cp.end_vel_x, cp.end_vel_y, cp.end_vel_z, cp.end_touch, cp.attempts); - } - } - - // API - Can be replaced with `ENDPOINT_MAP_GET_PB_BY_PLAYER` - public async void LoadStageTime(Player player, int style = 0, [CallerMemberName] string methodName = "") - { - var player_maptime = await ApiMethod.GET($"/surftimer/playerspecificdata?player_id={player.Profile.ID}&map_id={player.CurrMap.ID}&style={style}&type=2"); - if (player_maptime == null) - { - _logger.LogTrace("[{ClassName}] {MethodName} -> LoadStageTime -> No MapTime data found for Player {PlayerName} (ID {PlayerID}).", - nameof(PlayerStats), methodName, player.Profile.Name, player.Profile.ID - ); - return; - } + for (int i = 1; i <= SurfTimer.CurrentMap.Stages; i++) + { + this.StagePB[i] = new Dictionary(); + this.StagePB[i][style] = new PersonalBest { Type = 2 }; + initStage++; + } - foreach (API_MapTime mt in player_maptime) - { - StagePB[mt.stage][style].ID = mt.id; - StagePB[mt.stage][style].Ticks = mt.run_time; - StagePB[mt.stage][style].Type = mt.type; - StagePB[mt.stage][style].StartVelX = mt.start_vel_x; - StagePB[mt.stage][style].StartVelY = mt.start_vel_y; - StagePB[mt.stage][style].StartVelZ = mt.start_vel_z; - StagePB[mt.stage][style].EndVelX = mt.end_vel_x; - StagePB[mt.stage][style].EndVelY = mt.end_vel_y; - StagePB[mt.stage][style].EndVelZ = mt.end_vel_z; - // StagePB[mt.stage][style].RunDate = mt.run_date ?? 0; - StagePB[mt.stage][style].RunDate = mt.run_date; + for (int i = 1; i <= SurfTimer.CurrentMap.Bonuses; i++) + { + this.BonusPB[i] = new Dictionary(); + this.BonusPB[i][style] = new PersonalBest { Type = 1 }; + initBonus++; + } } - } - // API - Can be replaced with `ENDPOINT_MAP_GET_PB_BY_PLAYER` - public async void LoadBonusTime(Player player, int style = 0, [CallerMemberName] string methodName = "") - { - var player_maptime = await ApiMethod.GET($"/surftimer/playerspecificdata?player_id={player.Profile.ID}&map_id={player.CurrMap.ID}&style={style}&type=1"); - if (player_maptime == null) - { - _logger.LogTrace("[{ClassName}] {MethodName} -> LoadBonusTime -> No MapTime data found for Player {PlayerName} (ID {PlayerID}).", - nameof(PlayerStats), methodName, player.Profile.Name, player.Profile.ID - ); - return; - } - foreach (API_MapTime mt in player_maptime) - { - BonusPB[mt.stage][style].ID = mt.id; - BonusPB[mt.stage][style].Ticks = mt.run_time; - BonusPB[mt.stage][style].Type = mt.type; - BonusPB[mt.stage][style].StartVelX = mt.start_vel_x; - BonusPB[mt.stage][style].StartVelY = mt.start_vel_y; - BonusPB[mt.stage][style].StartVelZ = mt.start_vel_z; - BonusPB[mt.stage][style].EndVelX = mt.end_vel_x; - BonusPB[mt.stage][style].EndVelY = mt.end_vel_y; - BonusPB[mt.stage][style].EndVelZ = mt.end_vel_z; - // BonusPB[mt.stage][style].RunDate = mt.run_date ?? 0; - BonusPB[mt.stage][style].RunDate = mt.run_date; - } + _logger.LogTrace("[{ClassName}] {MethodName} -> PlayerStats -> Initialized {StagesInitialized} Stages and {BonusesInitialized} Bonuses", + nameof(PlayerStats), methodName, initStage, initBonus + ); } - /// /// Loads the player's map time data from the database along with their ranks. For all types and styles (may not work correctly for Stages/Bonuses) /// `Checkpoints` are loaded separately from another method in the `PresonalBest` class as it uses the unique `ID` for the run. (This method calls it if needed) /// This populates all the `style` and `type` stats the player has for the map /// - public async Task LoadPlayerMapTimesData(Player player, int playerId = 0, int mapId = 0, [CallerMemberName] string methodName = "") + internal async Task LoadPlayerMapTimesData(Player player, int playerId = 0, int mapId = 0, [CallerMemberName] string methodName = "") { var playerMapTimes = await _dataService.GetPlayerMapTimesAsync(player.Profile.ID, SurfTimer.CurrentMap.ID); @@ -195,10 +88,10 @@ public async Task LoadPlayerMapTimesData(Player player, int playerId = 0, int ma { case 1: // Bonus time #if DEBUG - _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData >> BonusPB", nameof(PlayerStats), methodName); + _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData >> BonusPB with ID {ID}", nameof(PlayerStats), methodName, mapTime.ID); #endif BonusPB[mapTime.Stage][style].ID = mapTime.ID; - BonusPB[mapTime.Stage][style].Ticks = mapTime.RunTime; + BonusPB[mapTime.Stage][style].RunTime = mapTime.RunTime; BonusPB[mapTime.Stage][style].Type = mapTime.Type; BonusPB[mapTime.Stage][style].Rank = mapTime.Rank; BonusPB[mapTime.Stage][style].StartVelX = mapTime.StartVelX; @@ -212,10 +105,10 @@ public async Task LoadPlayerMapTimesData(Player player, int playerId = 0, int ma case 2: // Stage time #if DEBUG - _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData >> StagePB", nameof(PlayerStats), methodName); + _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData >> StagePB with ID {ID}", nameof(PlayerStats), methodName, mapTime.ID); #endif StagePB[mapTime.Stage][style].ID = mapTime.ID; - StagePB[mapTime.Stage][style].Ticks = mapTime.RunTime; + StagePB[mapTime.Stage][style].RunTime = mapTime.RunTime; StagePB[mapTime.Stage][style].Type = mapTime.Type; StagePB[mapTime.Stage][style].Rank = mapTime.Rank; StagePB[mapTime.Stage][style].StartVelX = mapTime.StartVelX; @@ -229,10 +122,10 @@ public async Task LoadPlayerMapTimesData(Player player, int playerId = 0, int ma default: // Map time #if DEBUG - _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData >> MapPB", nameof(PlayerStats), methodName); + _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData >> MapPB with ID {ID}", nameof(PlayerStats), methodName, mapTime.ID); #endif PB[style].ID = mapTime.ID; - PB[style].Ticks = mapTime.RunTime; + PB[style].RunTime = mapTime.RunTime; PB[style].Type = mapTime.Type; PB[style].Rank = mapTime.Rank; PB[style].StartVelX = mapTime.StartVelX; @@ -242,6 +135,7 @@ public async Task LoadPlayerMapTimesData(Player player, int playerId = 0, int ma PB[style].EndVelY = mapTime.EndVelY; PB[style].EndVelZ = mapTime.EndVelZ; PB[style].RunDate = mapTime.RunDate; + //SurfTimer.CurrentMap.ConnectedMapTimes.Add(mapTime.ID); // Needed for PB replays? await PB[style].LoadCheckpoints(); break; @@ -253,106 +147,4 @@ public async Task LoadPlayerMapTimesData(Player player, int playerId = 0, int ma #endif } } - - - /* - public async Task LoadPlayerMapTimesData(Player player, int playerId = 0, int mapId = 0, [CallerMemberName] string methodName = "") - { - using (var playerStats = await SurfTimer.DB.QueryAsync( - string.Format(Config.MySQL.Queries.DB_QUERY_PS_GET_ALL_RUNTIMES, player.Profile.ID, SurfTimer.CurrentMap.ID))) - { - // int style = player.Timer.Style; - int style; - if (!playerStats.HasRows) - { - _logger.LogTrace("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData -> No MapTimes data found for Player {PlayerName} (ID {PlayerID}).", - nameof(PlayerStats), methodName, player.Profile.Name, player.Profile.ID - ); - return; - } - while (playerStats.Read()) - { - // Load data into each PersonalBest object - if (playerStats.GetInt32("type") == 1) // Bonus time - { - #if DEBUG - _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData >> BonusPB", - nameof(PlayerStats), methodName - ); - #endif - int bonusNum = playerStats.GetInt32("stage"); - style = playerStats.GetInt32("style"); // To-do: Uncomment when style is implemented - BonusPB[bonusNum][style].ID = playerStats.GetInt32("id"); - BonusPB[bonusNum][style].Ticks = playerStats.GetInt32("run_time"); - BonusPB[bonusNum][style].Type = playerStats.GetInt32("type"); - BonusPB[bonusNum][style].Rank = playerStats.GetInt32("rank"); - BonusPB[bonusNum][style].StartVelX = (float)playerStats.GetDouble("start_vel_x"); - BonusPB[bonusNum][style].StartVelY = (float)playerStats.GetDouble("start_vel_y"); - BonusPB[bonusNum][style].StartVelZ = (float)playerStats.GetDouble("start_vel_z"); - BonusPB[bonusNum][style].EndVelX = (float)playerStats.GetDouble("end_vel_x"); - BonusPB[bonusNum][style].EndVelY = (float)playerStats.GetDouble("end_vel_y"); - BonusPB[bonusNum][style].EndVelZ = (float)playerStats.GetDouble("end_vel_z"); - BonusPB[bonusNum][style].RunDate = playerStats.GetInt32("run_date"); - } - else if (playerStats.GetInt32("type") == 2) // Stage time - { - #if DEBUG - _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData >> StagePB", - nameof(PlayerStats), methodName - ); - #endif - int stageNum = playerStats.GetInt32("stage"); - style = playerStats.GetInt32("style"); // To-do: Uncomment when style is implemented - StagePB[stageNum][style].ID = playerStats.GetInt32("id"); - StagePB[stageNum][style].Ticks = playerStats.GetInt32("run_time"); - StagePB[stageNum][style].Type = playerStats.GetInt32("type"); - StagePB[stageNum][style].Rank = playerStats.GetInt32("rank"); - StagePB[stageNum][style].StartVelX = (float)playerStats.GetDouble("start_vel_x"); - StagePB[stageNum][style].StartVelY = (float)playerStats.GetDouble("start_vel_y"); - StagePB[stageNum][style].StartVelZ = (float)playerStats.GetDouble("start_vel_z"); - StagePB[stageNum][style].EndVelX = (float)playerStats.GetDouble("end_vel_x"); - StagePB[stageNum][style].EndVelY = (float)playerStats.GetDouble("end_vel_y"); - StagePB[stageNum][style].EndVelZ = (float)playerStats.GetDouble("end_vel_z"); - StagePB[stageNum][style].RunDate = playerStats.GetInt32("run_date"); - // Console.WriteLine(@$"DEBUG >> (func) LoadPlayerMapTimesData >> StagePB Loaded: - // StagePB[{stageNum}][{style}] = - // Stage: {stageNum} | ID: {StagePB[stageNum][style].ID} | Ticks: {StagePB[stageNum][style].Ticks} | Rank: {StagePB[stageNum][style].Rank} | Type: {StagePB[stageNum][style].Type}" - // ); - } - else // Map time - { - #if DEBUG - _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData >> MapPB", - nameof(PlayerStats), methodName - ); - #endif - style = playerStats.GetInt32("style"); // To-do: Uncomment when style is implemented - PB[style].ID = playerStats.GetInt32("id"); - PB[style].Ticks = playerStats.GetInt32("run_time"); - PB[style].Type = playerStats.GetInt32("type"); - PB[style].Rank = playerStats.GetInt32("rank"); - PB[style].StartVelX = (float)playerStats.GetDouble("start_vel_x"); - PB[style].StartVelY = (float)playerStats.GetDouble("start_vel_y"); - PB[style].StartVelZ = (float)playerStats.GetDouble("start_vel_z"); - PB[style].EndVelX = (float)playerStats.GetDouble("end_vel_x"); - PB[style].EndVelY = (float)playerStats.GetDouble("end_vel_y"); - PB[style].EndVelZ = (float)playerStats.GetDouble("end_vel_z"); - PB[style].RunDate = playerStats.GetInt32("run_date"); - // Console.WriteLine(@$"DEBUG >> (func) LoadPlayerMapTimesData >> PB Loaded: - // PB[{style}] = - // ID: {PB[style].ID} | Ticks: {PB[style].Ticks} | Rank: {PB[style].Rank} | Type: {PB[style].Type}" - // ); - await this.PB[style].LoadCheckpoints(); - } - // Console.WriteLine($"============== CS2 Surf DEBUG >> internal class PlayerStats -> public async Task LoadPlayerMapTimesData -> PlayerID: {player.Profile.ID} | Rank: {PB[style].Rank} | ID: {PB[style].ID} | RunTime: {PB[style].Ticks} | SVX: {PB[style].StartVelX} | SVY: {PB[style].StartVelY} | SVZ: {PB[style].StartVelZ} | EVX: {PB[style].EndVelX} | EVY: {PB[style].EndVelY} | EVZ: {PB[style].EndVelZ} | Run Date (UNIX): {PB[style].RunDate}"); - #if DEBUG - _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData -> Loaded PB[{Style}] run {RunID} (Rank {Rank}) for '{PlayerName}' (ID {PlayerID}).", - nameof(PlayerStats), methodName, style, PB[style].ID, PB[style].Rank, player.Profile.Name, player.Profile.ID - ); - #endif - } - } - } - */ - } \ No newline at end of file diff --git a/src/ST-Player/PlayerStats/RunStats.cs b/src/ST-Player/PlayerStats/RunStats.cs deleted file mode 100644 index c60fce2..0000000 --- a/src/ST-Player/PlayerStats/RunStats.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace SurfTimer; - -public abstract class RunStats -{ - public Dictionary Checkpoints { get; set; } - public int Ticks { get; set; } - public float StartVelX { get; set; } - public float StartVelY { get; set; } - public float StartVelZ { get; set; } - public float EndVelX { get; set; } - public float EndVelY { get; set; } - public float EndVelZ { get; set; } - public int RunDate { get; set; } - public string ReplayFramesBase64 { get; set; } = ""; - - protected RunStats() - { - Checkpoints = new Dictionary(); - Ticks = 0; - StartVelX = 0.0f; - StartVelY = 0.0f; - StartVelZ = 0.0f; - EndVelX = 0.0f; - EndVelY = 0.0f; - EndVelZ = 0.0f; - RunDate = 0; - } - - // Shared Method - public virtual void Reset() - { - Checkpoints.Clear(); - Ticks = 0; - StartVelX = StartVelY = StartVelZ = 0.0f; - EndVelX = EndVelY = EndVelZ = 0.0f; - RunDate = 0; - } -} diff --git a/src/ST-Player/PlayerTimer.cs b/src/ST-Player/PlayerTimer.cs index b87ef7d..517adf6 100644 --- a/src/ST-Player/PlayerTimer.cs +++ b/src/ST-Player/PlayerTimer.cs @@ -1,6 +1,6 @@ namespace SurfTimer; -internal class PlayerTimer +public class PlayerTimer { // Status public bool IsEnabled { get; set; } = true; // Enable toggle for entire timer @@ -13,15 +13,17 @@ internal class PlayerTimer public bool IsBonusMode { get; set; } = false; // Bonus mode toggle // Tracking - public int Stage { get; set; } = 0; // Current stage tracker - public int Checkpoint {get; set;} = 0; // Current checkpoint tracker - // public CurrentRun CurrentRunData { get; set; } = new CurrentRun(); // Current RUN data tracker - public int Bonus { get; set; } = 0; // To-do: bonus implementation - Current bonus tracker - public int Style { get; set; } = 0; // To-do: functionality for player to change this value and the actual styles implementation - Current style tracker + public short Stage { get; set; } = 0; // Current stage tracker + public short Checkpoint { get; set; } = 0; // Current checkpoint tracker + public short Bonus { get; set; } = 0; // To-do: bonus implementation - Current bonus tracker + public short Style { get; set; } = 0; // To-do: functionality for player to change this value and the actual styles implementation - Current style tracker // Timing public int Ticks { get; set; } = 0; // To-do: sub-tick counting? This currently goes on OnTick, which is not sub-tick I believe? Needs investigating + /// + /// Different types of time formatting for chat and HUD + /// // Time Formatting - To-do: Move to player settings maybe? public enum TimeFormatStyle { @@ -41,7 +43,6 @@ public void Reset() this.IsPracticeMode = false; this.IsStageMode = false; this.IsBonusMode = false; - // this.CurrentRunData.Reset(); } public void Pause() diff --git a/src/ST-Player/Replay/ReplayFrame.cs b/src/ST-Player/Replay/ReplayFrame.cs index 2bf493e..e5ce8fb 100644 --- a/src/ST-Player/Replay/ReplayFrame.cs +++ b/src/ST-Player/Replay/ReplayFrame.cs @@ -1,11 +1,8 @@ namespace SurfTimer; using System; -using System.Numerics; -using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Utils; -internal enum ReplayFrameSituation +public enum ReplayFrameSituation { NONE, @@ -30,19 +27,19 @@ internal enum ReplayFrameSituation } [Serializable] -internal class ReplayFrame +public class ReplayFrame { public float[] pos { get; set; } = { 0, 0, 0 }; public float[] ang { get; set; } = { 0, 0, 0 }; public ReplayFrameSituation Situation { get; set; } = ReplayFrameSituation.NONE; - public uint Flags { get; set; } + public uint Flags { get; set; } - public Vector_t GetPos() + public VectorT GetPos() { - return new Vector_t(this.pos[0], this.pos[1], this.pos[2]); + return new VectorT(this.pos[0], this.pos[1], this.pos[2]); } - public QAngle_t GetAng() + public QAngleT GetAng() { - return new QAngle_t(this.ang[0], this.ang[1], this.ang[2]); + return new QAngleT(this.ang[0], this.ang[1], this.ang[2]); } } diff --git a/src/ST-Player/Replay/ReplayManager.cs b/src/ST-Player/Replay/ReplayManager.cs index 7291846..fe05a6d 100644 --- a/src/ST-Player/Replay/ReplayManager.cs +++ b/src/ST-Player/Replay/ReplayManager.cs @@ -1,28 +1,23 @@ -using System.Text.Json; using CounterStrikeSharp.API.Core; namespace SurfTimer; -internal class ReplayManager +public class ReplayManager { public ReplayPlayer MapWR { get; set; } public ReplayPlayer? BonusWR { get; set; } = null; public ReplayPlayer? StageWR { get; set; } = null; /// /// Contains all Stage records for all styles - Refer to as AllStageWR[stage#][style] - /// Need to figure out a way to NOT hardcode to `32` but to total amount of Stages /// - public Dictionary[] AllStageWR { get; set; } = new Dictionary[32]; + public Dictionary[] AllStageWR { get; set; } = Array.Empty>(); /// /// Contains all Bonus records for all styles - Refer to as AllBonusWR[bonus#][style] - /// Need to figure out a way to NOT hardcode to `32` but to total amount of Bonuses /// - public Dictionary[] AllBonusWR { get; set; } = new Dictionary[32]; + public Dictionary[] AllBonusWR { get; set; } = Array.Empty>(); public List CustomReplays { get; set; } - /// - /// - /// + /// ID of the map /// Does the map have Stages /// Does the map have Bonuses @@ -32,7 +27,7 @@ internal class ReplayManager /// ID of the run /// Style of the run /// Stage/Bonus of the run - public ReplayManager(int map_id, bool staged, bool bonused, List frames, int run_time = 0, string playerName = "", int map_time_id = -1, int style = 0, int stage = 0) + internal ReplayManager(int map_id, bool staged, bool bonused, List frames, int run_time = 0, string playerName = "", int map_time_id = -1, int style = 0, int stage = 0) { MapWR = new ReplayPlayer { @@ -48,9 +43,9 @@ public ReplayManager(int map_id, bool staged, bool bonused, List fr if (staged) { - // Initialize 32 Stages for each style - // TODO: Make the amount of stages dynamic - for (int i = 0; i < 32; i++) + this.AllStageWR = new Dictionary[SurfTimer.CurrentMap.Stages + 1]; + + for (int i = 1; i <= SurfTimer.CurrentMap.Stages; i++) { AllStageWR[i] = new Dictionary(); foreach (int x in Config.Styles) @@ -63,9 +58,9 @@ public ReplayManager(int map_id, bool staged, bool bonused, List fr if (bonused) { - // Initialize 32 Stages for each style - // TODO: Make the amount of bonuses dynamic - for (int i = 0; i < 32; i++) + this.AllBonusWR = new Dictionary[SurfTimer.CurrentMap.Bonuses + 1]; + + for (int i = 1; i <= SurfTimer.CurrentMap.Bonuses; i++) { AllBonusWR[i] = new Dictionary(); foreach (int x in Config.Styles) diff --git a/src/ST-Player/Replay/ReplayPlayer.cs b/src/ST-Player/Replay/ReplayPlayer.cs index 962c3ac..5313245 100644 --- a/src/ST-Player/Replay/ReplayPlayer.cs +++ b/src/ST-Player/Replay/ReplayPlayer.cs @@ -1,13 +1,13 @@ -using System.Runtime.CompilerServices; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; namespace SurfTimer; -internal class ReplayPlayer +public class ReplayPlayer { /// /// Enable or Disable the replay bots. @@ -63,7 +63,7 @@ internal ReplayPlayer() _logger = SurfTimer.ServiceProvider.GetRequiredService>(); } - public void ResetReplay() + internal void ResetReplay() { this.CurrentFrameTick = 0; this.FrameTickIncrement = 1; @@ -74,7 +74,7 @@ public void ResetReplay() this.ReplayCurrentRunTime = 0; } - public void Reset() + internal void Reset() { this.IsPlaying = false; this.IsPaused = false; @@ -88,7 +88,7 @@ public void Reset() this.Controller = null; } - public void SetController(CCSPlayerController c, int repeat_count = -1, [CallerMemberName] string methodName = "") + internal void SetController(CCSPlayerController c, int repeat_count = -1, [CallerMemberName] string methodName = "") { this.Controller = c; if (repeat_count != -1) @@ -100,7 +100,7 @@ public void SetController(CCSPlayerController c, int repeat_count = -1, [CallerM ); } - public void Start([CallerMemberName] string methodName = "") + internal void Start([CallerMemberName] string methodName = "") { if (!this.IsPlayable || !this.IsEnabled) return; @@ -118,7 +118,7 @@ public void Start([CallerMemberName] string methodName = "") }); } - public void Stop([CallerMemberName] string methodName = "") + internal void Stop([CallerMemberName] string methodName = "") { this.IsPlaying = false; #if DEBUG @@ -128,7 +128,7 @@ public void Stop([CallerMemberName] string methodName = "") #endif } - public void Pause([CallerMemberName] string methodName = "") + internal void Pause([CallerMemberName] string methodName = "") { if (!this.IsPlaying || !this.IsEnabled) return; @@ -142,7 +142,7 @@ public void Pause([CallerMemberName] string methodName = "") #endif } - public void Tick() + internal void Tick() { if (this.MapID == -1 || !this.IsEnabled || !this.IsPlaying || !this.IsPlayable || this.Frames.Count == 0) return; @@ -163,6 +163,15 @@ public void Tick() { this.IsReplayOutsideZone = false; } + else if (current_frame.Situation == ReplayFrameSituation.STAGE_ZONE_EXIT && this.Type == 2) + { + IsReplayOutsideZone = true; + ReplayCurrentRunTime = 0; + } + else if (current_frame.Situation == ReplayFrameSituation.STAGE_ZONE_ENTER && this.Type == 2) + { + IsReplayOutsideZone = false; + } } else { @@ -173,7 +182,16 @@ public void Tick() else if (current_frame.Situation == ReplayFrameSituation.END_ZONE_ENTER) { this.IsReplayOutsideZone = true; - this.ReplayCurrentRunTime = this.CurrentFrameTick - (64 * 2); // (64*2) counts for the 2 seconds before run actually starts + this.ReplayCurrentRunTime = this.CurrentFrameTick - (Config.ReplaysPre * 2); // (64*2) counts for the 2 seconds before run actually starts + } + else if (current_frame.Situation == ReplayFrameSituation.STAGE_ZONE_EXIT && this.Type == 2) + { + this.IsReplayOutsideZone = false; + } + else if (current_frame.Situation == ReplayFrameSituation.STAGE_ZONE_ENTER && this.Type == 2) + { + IsReplayOutsideZone = true; + this.ReplayCurrentRunTime = this.CurrentFrameTick - (Config.ReplaysPre * 2); // (64*2) counts for the 2 seconds before run actually starts } } // END OF BLASPHEMY @@ -184,7 +202,7 @@ public void Tick() bool is_on_ground = (current_frame.Flags & (uint)PlayerFlags.FL_ONGROUND) != 0; - Vector_t velocity = (current_frame_pos - current_pos) * 64; + VectorT velocity = (current_frame_pos - current_pos) * 64; if (is_on_ground) this.Controller.PlayerPawn.Value.MoveType = MoveType_t.MOVETYPE_WALK; @@ -194,7 +212,7 @@ public void Tick() if ((current_pos - current_frame_pos).Length() > 200) Extensions.Teleport(Controller.PlayerPawn.Value, current_frame_pos, current_frame_ang, null); else - Extensions.Teleport(Controller.PlayerPawn.Value, null , current_frame_ang, velocity); + Extensions.Teleport(Controller.PlayerPawn.Value, null, current_frame_ang, velocity); if (!this.IsPaused) @@ -206,32 +224,38 @@ public void Tick() if (this.CurrentFrameTick >= this.Frames.Count) this.ResetReplay(); - // if(RepeatCount != -1) // Spam City - // Console.WriteLine($"CS2 Surf DEBUG >> internal class ReplayPlayer -> Tick -> ====================> {this.RepeatCount} <===================="); } - public void LoadReplayData(int repeat_count = -1, [CallerMemberName] string methodName = "") + internal void LoadReplayData(int repeat_count = -1, [CallerMemberName] string methodName = "") { if (!this.IsPlayable || !this.IsEnabled) return; + string replayType = this.Type switch + { + 1 => "Bonus Replay", + 2 => "Stage Replay", + 0 => "Map Replay", + _ => "Unknown Type", + }; + if (this.MapID == -1) { _logger.LogWarning("[{ClassName}] {MethodName} -> [{Type}] No replay data found for Player. MapID {MapID} | MapTimeID {MapTimeID} | RecordPlayerName {RecordPlayerName}", - nameof(ReplayPlayer), methodName, (this.Type == 2 ? "Stage Replay" : this.Type == 1 ? "Bonus Replay" : this.Type == 0 ? "Map Replay" : "Unknown Type"), this.MapID, this.MapTimeID, RecordPlayerName + nameof(ReplayPlayer), methodName, replayType, this.MapID, this.MapTimeID, RecordPlayerName ); return; } _logger.LogTrace("[{ClassName}] {MethodName} -> [{Type}] Loaded replay data for Player '{RecordPlayerName}' | MapTime ID: {MapTimeID} | Repeat {Repeat} | Frames {TotalFrames} | Ticks {RecordTicks}", - nameof(ReplayPlayer), methodName, (this.Type == 2 ? "Stage Replay" : this.Type == 1 ? "Bonus Replay" : this.Type == 0 ? "Map Replay" : "Unknown Type"), this.RecordPlayerName, this.MapTimeID, repeat_count, this.Frames.Count, this.RecordRunTime + nameof(ReplayPlayer), methodName, replayType, this.RecordPlayerName, this.MapTimeID, repeat_count, this.Frames.Count, this.RecordRunTime ); this.ResetReplay(); this.RepeatCount = repeat_count; } - public void FormatBotName([CallerMemberName] string methodName = "") + internal void FormatBotName([CallerMemberName] string methodName = "") { if (!this.IsPlayable || !this.IsEnabled || this.MapID == -1) return; @@ -253,15 +277,18 @@ public void FormatBotName([CallerMemberName] string methodName = "") SchemaString bot_name = new SchemaString(this.Controller!, "m_iszPlayerName"); - string replay_name = $"[{prefix}] {this.RecordPlayerName} | {PlayerHUD.FormatTime(this.RecordRunTime)}"; + string replay_name = $"[{prefix}] {this.RecordPlayerName} | {PlayerHud.FormatTime(this.RecordRunTime)}"; if (this.RecordRunTime <= 0) replay_name = $"[{prefix}] {this.RecordPlayerName}"; bot_name.Set(replay_name); - Utilities.SetStateChanged(this.Controller!, "CBasePlayerController", "m_iszPlayerName"); - + Server.NextFrame(() => + Utilities.SetStateChanged(this.Controller!, "CBasePlayerController", "m_iszPlayerName") + ); +#if DEBUG // _logger.LogTrace("[{ClassName}] {MethodName} -> Changed replay bot name from '{OldName}' to '{NewName}'", // nameof(ReplayPlayer), methodName, bot_name, replay_name // ); +#endif } } \ No newline at end of file diff --git a/src/ST-Player/Replay/ReplayRecorder.cs b/src/ST-Player/Replay/ReplayRecorder.cs index 61bacee..180588b 100644 --- a/src/ST-Player/Replay/ReplayRecorder.cs +++ b/src/ST-Player/Replay/ReplayRecorder.cs @@ -1,15 +1,15 @@ -using System.Runtime.CompilerServices; -using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; +using System.Text.Json; namespace SurfTimer; -internal class ReplayRecorder +public class ReplayRecorder { private readonly ILogger _logger; - public ReplayRecorder() + internal ReplayRecorder() { // Resolve the logger instance from the DI container _logger = SurfTimer.ServiceProvider.GetRequiredService>(); @@ -34,7 +34,7 @@ public ReplayRecorder() /// public List BonusSituations { get; set; } = new List(); - public void Reset([CallerMemberName] string methodName = "") + internal void Reset([CallerMemberName] string methodName = "") { this.IsRecording = false; this.Frames.Clear(); @@ -52,7 +52,7 @@ public void Reset([CallerMemberName] string methodName = "") #endif } - public void Start([CallerMemberName] string methodName = "") + internal void Start([CallerMemberName] string methodName = "") { this.IsRecording = true; @@ -63,7 +63,7 @@ public void Start([CallerMemberName] string methodName = "") #endif } - public void Stop([CallerMemberName] string methodName = "") + internal void Stop([CallerMemberName] string methodName = "") { this.IsRecording = false; @@ -74,13 +74,12 @@ public void Stop([CallerMemberName] string methodName = "") #endif } - public void Tick(Player player, [CallerMemberName] string methodName = "") + internal void Tick(Player player, [CallerMemberName] string methodName = "") { if (!this.IsRecording || player == null) return; // Disabling Recording if timer disabled - // if (!player.Timer.IsEnabled) if (!player.Timer.IsEnabled && !player.ReplayRecorder.IsSaving) { this.Stop(); @@ -93,32 +92,10 @@ public void Tick(Player player, [CallerMemberName] string methodName = "") var player_pos = player.Controller.Pawn.Value!.AbsOrigin!; var player_angle = player.Controller.PlayerPawn.Value!.EyeAngles; - var player_button = player.Controller.Pawn.Value.MovementServices!.Buttons.ButtonStates[0]; var player_flags = player.Controller.Pawn.Value.Flags; - var player_move_type = player.Controller.Pawn.Value.MoveType; - /* - switch (this.CurrentSituation) - { - case ReplayFrameSituation.START_ZONE_ENTER: - player.Controller.PrintToChat($"Start Enter: {this.Frames.Count} | Situation {this.CurrentSituation}"); - break; - case ReplayFrameSituation.START_ZONE_EXIT: - player.Controller.PrintToChat($"Start Exit: {this.Frames.Count} | Situation {this.CurrentSituation}"); - break; - case ReplayFrameSituation.STAGE_ZONE_ENTER: - player.Controller.PrintToChat($"Stage Enter: {this.Frames.Count} | Situation {this.CurrentSituation}"); - break; - case ReplayFrameSituation.STAGE_ZONE_EXIT: - player.Controller.PrintToChat($"Stage Exit: {this.Frames.Count} | Situation {this.CurrentSituation}"); - break; - case ReplayFrameSituation.CHECKPOINT_ZONE_ENTER: - player.Controller.PrintToChat($"Checkpoint Enter: {this.Frames.Count} | Situation {this.CurrentSituation}"); - break; - case ReplayFrameSituation.CHECKPOINT_ZONE_EXIT: - player.Controller.PrintToChat($"Checkpoint Exit: {this.Frames.Count} | Situation {this.CurrentSituation}"); - break; - } - */ + /// var player_button = player.Controller.Pawn.Value.MovementServices!.Buttons.ButtonStates[0]; + /// var player_move_type = player.Controller.Pawn.Value.MoveType; + var frame = new ReplayFrame { pos = [player_pos.X, player_pos.Y, player_pos.Z], @@ -133,260 +110,203 @@ public void Tick(Player player, [CallerMemberName] string methodName = "") this.CurrentSituation = ReplayFrameSituation.NONE; } - public string SerializeReplay() - { - // JsonSerializerOptions options = new JsonSerializerOptions {WriteIndented = false, Converters = { new VectorConverter(), new QAngleConverter() }}; - // string replay_frames = JsonSerializer.Serialize(Frames, options); - string replay_frames = JsonSerializer.Serialize(Frames); - return Compressor.Compress(replay_frames); - } - - public string SerializeReplayPortion(int start_idx, int end_idx) // Not used anymore - { - // JsonSerializerOptions options = new JsonSerializerOptions {WriteIndented = false, Converters = { new VectorConverter(), new QAngleConverter() }}; - // string replay_frames = JsonSerializer.Serialize(Frames.GetRange(start_idx, end_idx), options); - string replay_frames = JsonSerializer.Serialize(Frames.GetRange(start_idx, end_idx)); - return Compressor.Compress(replay_frames); - } - - public void SetLastTickSituation(ReplayFrameSituation situation) - { - if (this.Frames.Count == 0) - return; - - this.Frames[this.Frames.Count - 2].Situation = situation; - } - - public string TrimReplay(Player player, int type = 0, bool lastStage = false, [CallerMemberName] string methodName = "") + internal string TrimReplay(Player player, short type = 0, bool lastStage = false, [CallerMemberName] string methodName = "") { this.IsSaving = true; - List new_frames = new List(); + List? trimmed_frames = new List(); - _logger.LogTrace(">>> [{ClassName}] {MethodName} -> Trimming replay for '{PlayerName}' | type = {type} | lastStage = {lastStage} ", + _logger.LogTrace(">>> [{ClassName}] {MethodName} -> Trimming replay for '{PlayerName}' | type = {Type} | lastStage = {LastStage} ", nameof(ReplayRecorder), methodName, player.Profile.Name, type, lastStage ); if (this.Frames.Count == 0) { - _logger.LogError("[{ClassName}] {MethodName} -> There are no Frames available for trimming for player {Name}", + _logger.LogError("[{ClassName}] {MethodName} -> There are no Frames available for replay trimming for player {Name}", nameof(ReplayRecorder), methodName, player.Profile.Name ); - throw new Exception("There are no Frames available for trimming"); + throw new InvalidOperationException("There are no Frames available for trimming"); } switch (type) { - case 0: // Map Run - { - var start_enter_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.START_ZONE_ENTER); - var start_exit_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.START_ZONE_EXIT); - var end_enter_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.END_ZONE_ENTER); - - _logger.LogInformation("[{ClassName}] {MethodName} -> Trimming Map Run replay. Last start enter {start_enter_index} | last start exit {start_exit_index} | end enter {end_enter_index}", - nameof(ReplayRecorder), methodName, start_enter_index, start_exit_index, end_enter_index); - - if (start_enter_index == -1) + case 0: // Map Run { - _logger.LogError("[{ClassName}] {MethodName} -> Player '{Name}' got '-1' for start_enter_index during Map replay trimming. Setting 'start_enter_index' to '0' | IsStageMode = {StageMode} | IsBonusMode = {BonusMode}", - nameof(ReplayRecorder), methodName, player.Profile.Name, player.Timer.IsStageMode, player.Timer.IsBonusMode - ); - start_enter_index = start_enter_index == -1 ? 0 : start_enter_index; - // start_exit_index = start_exit_index == -1 ? 0 : start_exit_index; + trimmed_frames = TrimMapRun(player); + break; } - - if (start_enter_index != -1 && start_exit_index != -1 && end_enter_index != -1) + case 1: // Bonus Run { - int startIndex = CalculateStartIndex(start_enter_index, start_exit_index, Config.ReplaysPre); - int endIndex = CalculateEndIndex(end_enter_index, Frames.Count, Config.ReplaysPre); - new_frames = GetTrimmedFrames(startIndex, endIndex); - - _logger.LogInformation("<<< [{ClassName}] {MethodName} -> Trimmed from {StartIndex} to {EndIndex} (new_frames = {NewFramesCount}) - from total {TotalFrames}", - nameof(ReplayRecorder), methodName, startIndex, endIndex, new_frames.Count, this.Frames.Count); + trimmed_frames = TrimBonusRun(player); + break; } - else + case 2: // Stage Run { - _logger.LogError("[{ClassName}] {MethodName} -> Got a '-1' value while trimming Map replay for '{Name}'. start_enter_index = {start_enter_index} | start_exit_index = {start_exit_index} | end_enter_index = {end_enter_index}", - nameof(ReplayRecorder), methodName, player.Profile.Name, start_enter_index, start_exit_index, end_enter_index - ); - } - break; - } - case 1: // Bonus Run - { - var bonus_enter_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.START_ZONE_ENTER); - var bonus_exit_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.START_ZONE_EXIT); - var bonus_end_enter_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.END_ZONE_ENTER); - _logger.LogInformation("[{ClassName}] {MethodName} -> Looking for Bonus Run replay trim indexes. Last start enter {bonus_enter_index}, last start exit {bonus_exit_index}, end enter {bonus_end_enter_index}", - nameof(ReplayRecorder), methodName, bonus_enter_index, bonus_exit_index, bonus_end_enter_index - ); - - if (bonus_enter_index == -1) - { - _logger.LogError("[{ClassName}] {MethodName} -> Player '{Name}' got '-1' for bonus_enter_index during Bonus ({BonusNumber}) replay trimming. Settinng 'bonus_enter_index' to '0'", - nameof(ReplayRecorder), methodName, player.Profile.Name, player.Timer.Bonus - ); - bonus_enter_index = 0; + trimmed_frames = TrimStageRun(player, lastStage); + break; } + } - if (bonus_enter_index != -1 && bonus_exit_index != -1 && bonus_end_enter_index != -1) - { - int startIndex = CalculateStartIndex(bonus_enter_index, bonus_exit_index, Config.ReplaysPre); - int endIndex = CalculateEndIndex(bonus_end_enter_index, Frames.Count, Config.ReplaysPre); - new_frames = GetTrimmedFrames(startIndex, endIndex); + this.IsSaving = false; + _logger.LogTrace("[{ClassName}] {MethodName} -> Sending total of {Frames} replay frames.", + nameof(CurrentRun), methodName, trimmed_frames?.Count + ); + var trimmed = JsonSerializer.Serialize(trimmed_frames); + return Compressor.Compress(trimmed); + } - _logger.LogInformation("<<< [{ClassName}] {MethodName} -> Trimmed Bonus replay from {startIndex} to {endIndex} ({new_frames}) - from total {OldFrames}", - nameof(ReplayRecorder), methodName, startIndex, endIndex, new_frames.Count, this.Frames.Count - ); - } - else - { - _logger.LogError("[{ClassName}] {MethodName} -> Got a '-1' value while trimming Bonus ({BonusNumber}) replay for '{Name}'. bonus_enter_index = {bonus_enter_index} | bonus_exit_index = {bonus_exit_index} | bonus_end_enter_index = {bonus_end_enter_index}", - nameof(ReplayRecorder), methodName, player.Timer.Bonus, player.Profile.Name, bonus_enter_index, bonus_exit_index, bonus_end_enter_index - ); - } - break; - } - case 2: // Stage Run - { - int stage_end_index; - int stage_exit_index; - int stage_enter_index; + internal List? TrimMapRun(Player player, [CallerMemberName] string methodName = "") + { + List? new_frames = new List(); - _logger.LogInformation("[{ClassName}] {MethodName} -> Looking for Stage Run replay trim indexes. Stage {Stage}, available frames {TotalFrames}", - nameof(ReplayRecorder), methodName, player.Timer.Stage - 1, Frames.Count - ); + var start_enter_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.START_ZONE_ENTER); + var start_exit_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.START_ZONE_EXIT); + var end_enter_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.END_ZONE_ENTER); - if (lastStage) - { - _logger.LogTrace("Stage replay trimming will use `STAGE_ZONE_X` + `END_ZONE_ENTER`"); - stage_end_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.END_ZONE_ENTER); - _logger.LogTrace("stage_end_index = {stage_end_index}", - stage_end_index - ); - stage_exit_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == ReplayFrameSituation.STAGE_ZONE_EXIT); - _logger.LogTrace("stage_exit_index = {stage_exit_index}", - stage_exit_index - ); - stage_enter_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == ReplayFrameSituation.STAGE_ZONE_ENTER); - _logger.LogTrace("stage_enter_index = {stage_enter_index}", - stage_enter_index - ); - } - else if (player.Timer.Stage - 1 > 1) - { - _logger.LogTrace("Stage replay trimming will use `STAGE_ZONE_X`"); - stage_end_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.STAGE_ZONE_ENTER); - _logger.LogTrace("stage_end_index = {stage_end_index}", - stage_end_index - ); - stage_exit_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == ReplayFrameSituation.STAGE_ZONE_EXIT); - _logger.LogTrace("stage_exit_index = {stage_exit_index}", - stage_exit_index - ); - stage_enter_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == ReplayFrameSituation.STAGE_ZONE_ENTER); - _logger.LogTrace("stage_enter_index = {stage_enter_index}", - stage_enter_index - ); - } - else if (player.Timer.Stage - 1 == -1) // Don't crash...? - { - _logger.LogError(">>>> Stage replay trimming will abort! <<<<"); - _logger.LogError("======== internal class ReplayRecorder -> public string TrimReplay -> this.IsSaving = {IsSaving}", this.IsSaving); - _logger.LogError("======== internal class ReplayRecorder -> public string TrimReplay -> player.Timer.IsRunning = {IsRunning}", player.Timer.IsRunning); - _logger.LogError("======== internal class ReplayRecorder -> public string TrimReplay -> player.Timer.IsEnabled = {IsEnabled}", player.Timer.IsEnabled); - _logger.LogError("======== internal class ReplayRecorder -> public string TrimReplay -> player.Timer.Stage = {Stage}", player.Timer.Stage); - _logger.LogError("======== internal class ReplayRecorder -> public string TrimReplay -> player.Timer.Ticks = {Ticks}", player.Timer.Ticks); - stage_enter_index = -1; - stage_end_index = -1; - stage_exit_index = -1; - } - else - { - _logger.LogInformation("Stage replay trimming will use `START_ZONE_X`"); - stage_end_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.STAGE_ZONE_ENTER); - _logger.LogTrace("stage_end_index = {stage_end_index}", - stage_end_index - ); - stage_exit_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == ReplayFrameSituation.START_ZONE_EXIT); - _logger.LogTrace("stage_exit_index = {stage_exit_index}", - stage_exit_index - ); - stage_enter_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == ReplayFrameSituation.START_ZONE_ENTER); - _logger.LogTrace("stage_enter_index = {stage_enter_index}", - stage_enter_index - ); - } - _logger.LogInformation("[{ClassName}] {MethodName} -> Trimming Stage Run replay. Stage {Stage}, enter {stage_enter_index}, stage exit {stage_exit_index}, stage end {stage_end_index}", - nameof(ReplayRecorder), methodName, player.Timer.Stage - 1, stage_enter_index, stage_exit_index, stage_end_index - ); - - if (stage_enter_index == -1) - { - _logger.LogError("[{ClassName}] {MethodName} -> Player '{Name}' got '-1' for stage_enter_index during Stage ({StageNumber}) replay trimming. Setting 'stage_enter_index' to '0'", - nameof(ReplayRecorder), methodName, player.Profile.Name, player.Timer.Stage - ); - stage_enter_index = 0; - } + _logger.LogInformation("[{ClassName}] {MethodName} -> Trimming Map Run replay. Last start enter {StartEnterIndex} | last start exit {StartExitIndex} | end enter {EndEnterIndex}", + nameof(ReplayRecorder), methodName, start_enter_index, start_exit_index, end_enter_index); - if (stage_enter_index != -1 && stage_exit_index != -1 && stage_end_index != -1) - { - int startIndex = CalculateStartIndex(stage_enter_index, stage_exit_index, Config.ReplaysPre); - int endIndex = CalculateEndIndex(stage_end_index, Frames.Count, Config.ReplaysPre); - new_frames = GetTrimmedFrames(startIndex, endIndex); + if (start_enter_index == -1) + { + _logger.LogError("[{ClassName}] {MethodName} -> Player '{Name}' got '-1' for start_enter_index during Map replay trimming. Setting 'start_enter_index' to '0' | IsStageMode = {StageMode} | IsBonusMode = {BonusMode}", + nameof(ReplayRecorder), methodName, player.Profile.Name, player.Timer.IsStageMode, player.Timer.IsBonusMode + ); + start_enter_index = start_enter_index == -1 ? 0 : start_enter_index; + } - _logger.LogInformation("<<< [{ClassName}] {MethodName} -> Trimmed Stage replay from {startIndex} to {endIndex} ({new_frames}) - from total {OldFrames}", - nameof(ReplayRecorder), methodName, startIndex, endIndex, new_frames.Count, this.Frames.Count - ); - } - else - { - _logger.LogError("[{ClassName}] {MethodName} -> Got a '-1' value while trimming Stage ({StageNumber}) replay for '{Name}'. stage_enter_index = {stage_enter_index} | stage_exit_index = {stage_exit_index} | stage_end_index = {stage_end_index}", - nameof(ReplayRecorder), methodName, player.Timer.Stage, player.Profile.Name, stage_enter_index, stage_exit_index, stage_end_index - ); - } - break; - } - } + if (start_enter_index != -1 && start_exit_index != -1 && end_enter_index != -1) + { + int startIndex = CalculateStartIndex(start_enter_index, start_exit_index, Config.ReplaysPre); + int endIndex = CalculateEndIndex(end_enter_index, Frames.Count, Config.ReplaysPre); + new_frames = GetTrimmedFrames(startIndex, endIndex); - this.IsSaving = false; - string trimmed = JsonSerializer.Serialize(new_frames); - return Compressor.Compress(trimmed); + _logger.LogDebug("<<< [{ClassName}] {MethodName} -> Trimmed from {StartIndex} to {EndIndex} (new_frames = {NewFramesCount}) - from total {TotalFrames}", + nameof(ReplayRecorder), methodName, startIndex, endIndex, new_frames.Count, this.Frames.Count); + + return new_frames; + } + else + { + _logger.LogError("[{ClassName}] {MethodName} -> Got a '-1' value while trimming Map replay for '{Name}'. start_enter_index = {StartEnterIndex} | start_exit_index = {StartExitIndex} | end_enter_index = {EndEnterIndex}", + nameof(ReplayRecorder), methodName, player.Profile.Name, start_enter_index, start_exit_index, end_enter_index + ); + + return new_frames; + } } - public int LastEnterTick(int start_idx = 0) + internal List? TrimBonusRun(Player player, [CallerMemberName] string methodName = "") { - if (start_idx == 0) - start_idx = this.Frames.Count - 1; - for (int i = start_idx; i > 0; i--) + List? new_frames = new List(); + + var bonus_enter_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.START_ZONE_ENTER); + var bonus_exit_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.START_ZONE_EXIT); + var bonus_end_enter_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.END_ZONE_ENTER); + _logger.LogInformation("[{ClassName}] {MethodName} -> Looking for Bonus Run replay trim indexes. Last start enter {BonusEnterIndex}, last start exit {BonusExitIndex}, end enter {BonusEndEnterIndex}", + nameof(ReplayRecorder), methodName, bonus_enter_index, bonus_exit_index, bonus_end_enter_index + ); + + if (bonus_enter_index == -1) { - if ( - this.Frames[i].Situation == ReplayFrameSituation.START_ZONE_ENTER || - this.Frames[i].Situation == ReplayFrameSituation.STAGE_ZONE_ENTER || - this.Frames[i].Situation == ReplayFrameSituation.CHECKPOINT_ZONE_ENTER || - this.Frames[i].Situation == ReplayFrameSituation.END_ZONE_ENTER - ) - return i; + _logger.LogError("[{ClassName}] {MethodName} -> Player '{Name}' got '-1' for bonus_enter_index during Bonus ({BonusNumber}) replay trimming. Setting 'bonus_enter_index' to '0'", + nameof(ReplayRecorder), methodName, player.Profile.Name, player.Timer.Bonus + ); + bonus_enter_index = 0; + } + + if (bonus_enter_index != -1 && bonus_exit_index != -1 && bonus_end_enter_index != -1) + { + int startIndex = CalculateStartIndex(bonus_enter_index, bonus_exit_index, Config.ReplaysPre); + int endIndex = CalculateEndIndex(bonus_end_enter_index, Frames.Count, Config.ReplaysPre); + new_frames = GetTrimmedFrames(startIndex, endIndex); + + _logger.LogDebug("<<< [{ClassName}] {MethodName} -> Trimmed Bonus replay from {StartIndex} to {EndIndex} ({NewFrames}) - from total {OldFrames}", + nameof(ReplayRecorder), methodName, startIndex, endIndex, new_frames.Count, this.Frames.Count + ); + + return new_frames; + } + else + { + _logger.LogError("[{ClassName}] {MethodName} -> Got a '-1' value while trimming Bonus ({BonusNumber}) replay for '{Name}'. bonus_enter_index = {BonusEnterIndex} | bonus_exit_index = {BonusExitIndex} | bonus_end_enter_index = {BonusEndEnterIndex}", + nameof(ReplayRecorder), methodName, player.Timer.Bonus, player.Profile.Name, bonus_enter_index, bonus_exit_index, bonus_end_enter_index + ); + + return new_frames; } - return 0; } - public int LastExitTick(int start_idx = 0) + internal List? TrimStageRun(Player player, bool lastStage = false, [CallerMemberName] string methodName = "") { - if (start_idx == 0) - start_idx = this.Frames.Count - 1; - for (int i = start_idx; i > 0; i--) + List? new_frames = new List(); + + int stage_end_index; + int stage_exit_index; + int stage_enter_index; + + int stage = player.Timer.Stage - 1; + + ReplayFrameSituation enterZone; + ReplayFrameSituation exitZone; + ReplayFrameSituation endZone; + + // Select the correct enums for trimming + if (stage == 1) { - if ( - this.Frames[i].Situation == ReplayFrameSituation.START_ZONE_EXIT || - this.Frames[i].Situation == ReplayFrameSituation.STAGE_ZONE_EXIT || - this.Frames[i].Situation == ReplayFrameSituation.CHECKPOINT_ZONE_EXIT || - this.Frames[i].Situation == ReplayFrameSituation.END_ZONE_EXIT - ) - return i; + _logger.LogDebug("Stage replay trimming will use START_ZONE_*"); + enterZone = ReplayFrameSituation.START_ZONE_ENTER; + exitZone = ReplayFrameSituation.START_ZONE_EXIT; + endZone = ReplayFrameSituation.STAGE_ZONE_ENTER; } - return 0; + else + { + _logger.LogDebug("Stage replay trimming will use STAGE_ZONE_*"); + enterZone = ReplayFrameSituation.STAGE_ZONE_ENTER; + exitZone = ReplayFrameSituation.STAGE_ZONE_EXIT; + endZone = ReplayFrameSituation.STAGE_ZONE_ENTER; + + // If it's the last stage we need to use END_ZONE_ENTER for trimming + if (lastStage) + { + _logger.LogDebug("This is the last stage, will end on END_ZONE_ENTER."); + endZone = ReplayFrameSituation.END_ZONE_ENTER; + stage += 1; + } + } + + _logger.LogInformation("[{ClassName}] {MethodName} -> Player is on Stage {Stage} and we are trimming replay for Stage {TrimmingStage}", + nameof(ReplayRecorder), methodName, player.Timer.Stage, stage + ); + + stage_end_index = Frames.FindLastIndex(f => f.Situation == endZone); + stage_exit_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == exitZone); + stage_enter_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == enterZone); + + _logger.LogInformation("[{ClassName}] {MethodName} -> Trimming Stage Run replay. Stage {Stage}, enter {EnterIndex}, exit {ExitIndex}, end {EndIndex}", + nameof(ReplayRecorder), methodName, stage, stage_enter_index, stage_exit_index, stage_end_index + ); + + if (stage_enter_index == -1 || stage_exit_index == -1 || stage_end_index == -1) + { + _logger.LogError("[{ClassName}] {MethodName} -> Could not find necessary frame indexes for trimming Stage {Stage} replay for player '{Name}'. ENTER: {Enter}, EXIT: {Exit}, END: {End}", + nameof(ReplayRecorder), methodName, stage, player.Profile.Name, + stage_enter_index, stage_exit_index, stage_end_index + ); + return new_frames; + } + + int startIndex = CalculateStartIndex(stage_enter_index, stage_exit_index, Config.ReplaysPre); + int endIndex = CalculateEndIndex(stage_end_index, Frames.Count, Config.ReplaysPre); + + new_frames = GetTrimmedFrames(startIndex, endIndex); + + _logger.LogInformation("<<< [{ClassName}] {MethodName} -> Trimmed Stage {Stage} replay from {Start} to {End} (Total Frames: {NewFrames})", + nameof(ReplayRecorder), methodName, stage, startIndex, endIndex, new_frames.Count + ); + + return new_frames; } - private int CalculateStartIndex(int start_enter, int start_exit, int buffer) + + private static int CalculateStartIndex(int start_enter, int start_exit, int buffer) { if (start_exit - (buffer * 2) >= start_enter) return start_exit - (buffer * 2); @@ -398,20 +318,28 @@ private int CalculateStartIndex(int start_enter, int start_exit, int buffer) return start_enter; } - private int CalculateEndIndex(int end_enter, int totalFrames, int buffer) + private static int CalculateEndIndex(int end_enter, int totalFrames, int buffer) { if (end_enter + (buffer * 2) < totalFrames) + { return end_enter + (buffer * 2); + } else if (end_enter + buffer < totalFrames) + { return end_enter + buffer; + } else if (end_enter + (buffer / 2) < totalFrames) + { return end_enter + (buffer / 2); + } else + { return end_enter; + } } private List GetTrimmedFrames(int startIndex, int endIndex) { - return Frames.GetRange(startIndex, endIndex - startIndex + 1); + return Frames.GetRange(startIndex, endIndex - startIndex + 1); } } \ No newline at end of file diff --git a/src/ST-Player/Saveloc/SavelocFrame.cs b/src/ST-Player/Saveloc/SavelocFrame.cs index 7fd9db6..e95cfb4 100644 --- a/src/ST-Player/Saveloc/SavelocFrame.cs +++ b/src/ST-Player/Saveloc/SavelocFrame.cs @@ -1,11 +1,9 @@ -using CounterStrikeSharp.API.Modules.Utils; - namespace SurfTimer; -internal class SavelocFrame +public class SavelocFrame { - public Vector_t Pos { get; set; } = new Vector_t(0, 0, 0); - public QAngle_t Ang { get; set; } = new QAngle_t(0, 0, 0); - public Vector_t Vel { get; set; } = new Vector_t(0, 0, 0); - public int Tick { get; set; } = 0; + public VectorT Pos { get; set; } = new VectorT(0, 0, 0); + public QAngleT Ang { get; set; } = new QAngleT(0, 0, 0); + public VectorT Vel { get; set; } = new VectorT(0, 0, 0); + public int Tick { get; set; } = 0; } diff --git a/src/ST-API/ConVar.cs b/src/ST-UTILS/ConVar.cs similarity index 100% rename from src/ST-API/ConVar.cs rename to src/ST-UTILS/ConVar.cs diff --git a/src/ST-UTILS/Config.cs b/src/ST-UTILS/Config.cs index 0d5067c..7585a36 100644 --- a/src/ST-UTILS/Config.cs +++ b/src/ST-UTILS/Config.cs @@ -1,26 +1,32 @@ +using System.Collections.Immutable; using System.Reflection; using System.Text.Json; using CounterStrikeSharp.API; -using CounterStrikeSharp.API.Modules.Utils; namespace SurfTimer; public static class Config { + public static readonly string PluginLogo = """ + + ____________ ____ ___ + / ___/ __/_ | / __/_ ______/ _/ + / /___\ \/ __/ _\ \/ // / __/ _/ + \___/___/____/ /___/\_,_/_/ /_/ + """; public static string PluginName => Assembly.GetExecutingAssembly().GetName().Name ?? ""; - public static string PluginPrefix = LocalizationService.LocalizerNonNull["prefix"]; - public static string PluginPath => $"{Server.GameDirectory}/csgo/addons/counterstrikesharp/plugins/{PluginName}/"; - public static string PluginSurfConfig = $"{Server.GameDirectory}/csgo/cfg/{PluginName}/{PluginName}.json"; - public static string ApiUrl => API.GetApiUrl(); - public static string DbConnectionString => MySQL.GetConnectionString(); + public static readonly string PluginPrefix = LocalizationService.LocalizerNonNull["prefix"]; + public static string PluginPath => + $"{Server.GameDirectory}/csgo/addons/counterstrikesharp/plugins/{PluginName}/"; + public static string ApiUrl => Api.GetApiUrl(); /// /// Placeholder for amount of styles /// - public static List Styles = new List { 0 }; // Add all supported style IDs + public static readonly ImmutableList Styles = [0]; // Add all supported style IDs - public static bool ReplaysEnabled => true; - public static int ReplaysPre => 64; + public static readonly bool ReplaysEnabled = TimerSettings.GetReplaysEnabled(); + public static readonly int ReplaysPre = TimerSettings.GetReplaysPre(); // Helper class/methods for configuration loading private static class ConfigLoader @@ -44,18 +50,25 @@ public static JsonDocument GetConfigDocument(string configPath) private static class TimerSettings { private const string TIMER_CONFIG_PATH = "/csgo/cfg/SurfTimer/timer_settings.json"; - private static JsonDocument ConfigDocument => ConfigLoader.GetConfigDocument(TIMER_CONFIG_PATH); + private static JsonDocument ConfigDocument => + ConfigLoader.GetConfigDocument(TIMER_CONFIG_PATH); - public static string GetPrefix() + public static bool GetReplaysEnabled() { - return ConfigDocument.RootElement.GetProperty("prefix").GetString()!; + return ConfigDocument.RootElement.GetProperty("replays_enabled").GetBoolean(); + } + + public static int GetReplaysPre() + { + return ConfigDocument.RootElement.GetProperty("replays_pre").GetInt32(); } } - public static class API + public static class Api { private const string API_CONFIG_PATH = "/csgo/cfg/SurfTimer/api_config.json"; - private static JsonDocument ConfigDocument => ConfigLoader.GetConfigDocument(API_CONFIG_PATH); + private static JsonDocument ConfigDocument => + ConfigLoader.GetConfigDocument(API_CONFIG_PATH); /// /// Retrieves the `api_url` string from the configuration path @@ -80,45 +93,49 @@ public static bool GetApiOnly() /// public static class Endpoints { + public const string ENDPOINT_PING = "/api/Utilities/ping/clientUnix={0}"; + // Map.cs related endpoints - public const string ENDPOINT_MAP_GET_INFO = "/surftimer/mapinfo?mapname={0}"; - public const string ENDPOINT_MAP_INSERT_INFO = "/surftimer/insertmap"; - public const string ENDPOINT_MAP_UPDATE_INFO = "/surftimer/updateMap"; - // public const string ENDPOINT_MAP_GET_RUNS = "/surftimer/maptotals?map_id={0}&style={1}"; - // public const string ENDPOINT_MAP_GET_RUNS = "/surftimer/maprunsdata?map_id={0}&style={1}&type={2}"; - public const string ENDPOINT_MAP_GET_RUNS = "/surftimer/maprunsdata?id={0}"; - public const string ENDPOINT_MAP_GET_RUN_CPS = "/surftimer/mapcheckpointsdata?maptime_id={0}"; + public const string ENDPOINT_MAP_GET_INFO = "/api/Map/mapName={0}"; + public const string ENDPOINT_MAP_INSERT_INFO = "/api/Map"; + public const string ENDPOINT_MAP_UPDATE_INFO = "/api/Map/mapId={0}"; + public const string ENDPOINT_MAP_GET_RUNS = "/api/Map/mapId={0}"; + public const string ENDPOINT_MAP_GET_RUN_CPS = + "/api/PersonalBest/checkpoints/mapTimeId={0}"; // CurrentRun.cs - public const string ENDPOINT_CR_SAVE_MAP_TIME = "/surftimer/savemaptime"; + public const string ENDPOINT_CR_SAVE_MAP_TIME = "/api/CurrentRun/saveMapTime"; + public const string ENDPOINT_CR_UPDATE_MAP_TIME = + "/api/CurrentRun/updateMapTime/mapTimeId={0}"; public const string ENDPOINT_CR_SAVE_STAGE_TIME = "/surftimer/savestagetime"; - // PersonalBest.cs - public const string ENDPOINT_MAP_GET_PB_BY_PLAYER = "/surftimer/runbyplayer?player_id={0}&map_id={1}&type={2}&style={3}"; - public const string ENDPOINT_MAP_GET_PB_BY_ID = "/surftimer/runbyid?run_id={0}"; - + public const string ENDPOINT_MAP_GET_PB_BY_PLAYER = + "/api/PersonalBest/playerId={0}&mapId={1}&type={2}&style={3}"; + public const string ENDPOINT_MAP_GET_PB_BY_ID = + "/api/PersonalBest/runById/mapTimeId={0}"; // PlayerProfile.cs - public const string ENDPOINT_PP_GET_PROFILE = "/surftimer/playersurfprofile/{0}"; - public const string ENDPOINT_PP_INSERT_PROFILE = "/surftimer/insertplayer"; - public const string ENDPOINT_PP_UPDATE_PROFILE = "/surftimer/updateplayerprofile"; + public const string ENDPOINT_PP_GET_PROFILE = "/api/PlayerProfile/steamId={0}"; + public const string ENDPOINT_PP_INSERT_PROFILE = "/api/PlayerProfile"; + public const string ENDPOINT_PP_UPDATE_PROFILE = "/api/PlayerProfile/playerId={0}"; // PlayerStats.cs - public const string ENDPOINT_PS_GET_PLAYER_MAP_DATA = "/surftimer/playermapdata?player_id={0}&map_id={1}"; - + public const string ENDPOINT_PS_GET_PLAYER_MAP_DATA = + "/api/PlayerStats/playerId={0}&mapId={1}"; } } - public static class MySQL + public static class MySql { private const string DB_CONFIG_PATH = "/csgo/cfg/SurfTimer/database.json"; - private static JsonDocument ConfigDocument => ConfigLoader.GetConfigDocument(DB_CONFIG_PATH); + private static JsonDocument ConfigDocument => + ConfigLoader.GetConfigDocument(DB_CONFIG_PATH); /// - /// Retrieves the connection details for connecting to the MySQL Database + /// Retrieves the connection details for connecting to the MySQL Database /// - /// A connection + /// A connection string public static string GetConnectionString() { string host = ConfigDocument.RootElement.GetProperty("host").GetString()!; @@ -128,145 +145,10 @@ public static string GetConnectionString() int port = ConfigDocument.RootElement.GetProperty("port").GetInt32()!; int timeout = ConfigDocument.RootElement.GetProperty("timeout").GetInt32()!; - string connString = $"server={host};user={user};password={password};database={database};port={port};connect timeout={timeout};"; - - // Console.WriteLine($"============= [CS2 Surf] Extracted connection string: {connString}"); + string connString = + $"Server={host};User={user};Password={password};Database={database};Port={port};Connect Timeout={timeout};Allow User Variables=true"; return connString; } - - /// - /// Contains all the queries used by MySQL for the SurfTimer plugin. - /// - public static class Queries - { - // Map.cs related queries - public const string DB_QUERY_MAP_GET_INFO = "SELECT * FROM Maps WHERE name='{0}';"; - public const string DB_QUERY_MAP_INSERT_INFO = "INSERT INTO Maps (name, author, tier, stages, bonuses, ranked, date_added, last_played) VALUES ('{0}', '{1}', {2}, {3}, {4}, {5}, {6}, {6})"; // "INSERT INTO Maps (name, author, tier, stages, ranked, date_added, last_played) VALUES ('{MySqlHelper.EscapeString(Name)}', 'Unknown', {this.Stages}, {this.Bonuses}, 0, {(int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()}, {(int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()})" - public const string DB_QUERY_MAP_UPDATE_INFO_FULL = "UPDATE Maps SET last_played={0}, stages={1}, bonuses={2} WHERE id={3};"; - public const string DB_QUERY_MAP_GET_RECORD_RUNS_AND_COUNT = @" - SELECT - ranked_times.* - FROM ( - SELECT - MapTimes.*, - Player.name, - ROW_NUMBER() OVER ( - PARTITION BY MapTimes.type, MapTimes.stage - ORDER BY MapTimes.run_time ASC - ) AS row_num, - COUNT(*) OVER (PARTITION BY MapTimes.type, MapTimes.stage) AS total_count - FROM MapTimes - JOIN Player ON MapTimes.player_id = Player.id - WHERE MapTimes.map_id = {0} - ) AS ranked_times - WHERE ranked_times.row_num = 1;"; - - - // PlayerStats.cs related queries - public const string DB_QUERY_PS_GET_ALL_RUNTIMES = @" - SELECT mainquery.*, (SELECT COUNT(*) FROM `MapTimes` AS subquery - WHERE subquery.`map_id` = mainquery.`map_id` AND subquery.`style` = mainquery.`style` - AND subquery.`run_time` <= mainquery.`run_time` AND subquery.`type` = mainquery.`type` AND subquery.`stage` = mainquery.`stage`) AS `rank` FROM `MapTimes` AS mainquery - WHERE mainquery.`player_id` = {0} AND mainquery.`map_id` = {1}; - "; // Deprecated - - // PersonalBest.cs related queries - public const string DB_QUERY_PB_GET_TYPE_RUNTIME = @" - SELECT mainquery.*, (SELECT COUNT(*) FROM `MapTimes` AS subquery - WHERE subquery.`map_id` = mainquery.`map_id` AND subquery.`style` = mainquery.`style` - AND subquery.`run_time` <= mainquery.`run_time` AND subquery.`type` = mainquery.`type` AND subquery.`stage` = mainquery.`stage`) AS `rank` FROM `MapTimes` AS mainquery - WHERE mainquery.`player_id` = {0} AND mainquery.`map_id` = {1} AND mainquery.`type` = {2} AND mainquery.`style` = {3}; - "; - public const string DB_QUERY_PB_GET_SPECIFIC_MAPTIME_DATA = @" - SELECT mainquery.*, (SELECT COUNT(*) FROM `MapTimes` AS subquery - WHERE subquery.`map_id` = mainquery.`map_id` AND subquery.`style` = mainquery.`style` - AND subquery.`run_time` <= mainquery.`run_time` AND subquery.`type` = mainquery.`type` AND subquery.`stage` = mainquery.`stage`) AS `rank` FROM `MapTimes` AS mainquery - WHERE mainquery.`id` = {0}; - "; - public const string DB_QUERY_PB_GET_CPS = "SELECT * FROM `Checkpoints` WHERE `maptime_id` = {0};"; - - // CurrentRun.cs related queries - public const string DB_QUERY_CR_INSERT_TIME = @" - INSERT INTO `MapTimes` - (`player_id`, `map_id`, `style`, `type`, `stage`, `run_time`, `start_vel_x`, `start_vel_y`, `start_vel_z`, `end_vel_x`, `end_vel_y`, `end_vel_z`, `run_date`, `replay_frames`) - VALUES ({0}, {1}, {2}, {3}, {4}, {5}, - {6}, {7}, {8}, {9}, {10}, {11}, {12}, '{13}') - ON DUPLICATE KEY UPDATE run_time=VALUES(run_time), start_vel_x=VALUES(start_vel_x), start_vel_y=VALUES(start_vel_y), - start_vel_z=VALUES(start_vel_z), end_vel_x=VALUES(end_vel_x), end_vel_y=VALUES(end_vel_y), end_vel_z=VALUES(end_vel_z), run_date=VALUES(run_date), replay_frames=VALUES(replay_frames); - "; - public const string DB_QUERY_CR_INSERT_CP = @" - INSERT INTO `Checkpoints` - (`maptime_id`, `cp`, `run_time`, `start_vel_x`, `start_vel_y`, `start_vel_z`, - `end_vel_x`, `end_vel_y`, `end_vel_z`, `attempts`, `end_touch`) - VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}) - ON DUPLICATE KEY UPDATE - run_time=VALUES(run_time), start_vel_x=VALUES(start_vel_x), start_vel_y=VALUES(start_vel_y), start_vel_z=VALUES(start_vel_z), - end_vel_x=VALUES(end_vel_x), end_vel_y=VALUES(end_vel_y), end_vel_z=VALUES(end_vel_z), attempts=VALUES(attempts), end_touch=VALUES(end_touch); - "; - - // ReplayPlayer.cs related queries - public const string DB_QUERY_RP_LOAD_REPLAY = @" - SELECT MapTimes.replay_frames, MapTimes.run_time, Player.name - FROM MapTimes - JOIN Player ON MapTimes.player_id = Player.id - WHERE MapTimes.id={0}; - "; - - // Players.cs related queries - public const string DB_QUERY_PP_GET_PROFILE = "SELECT * FROM `Player` WHERE `steam_id` = {0} LIMIT 1;"; - public const string DB_QUERY_PP_INSERT_PROFILE = @" - INSERT INTO `Player` (`name`, `steam_id`, `country`, `join_date`, `last_seen`, `connections`) - VALUES ('{0}', {1}, '{2}', {3}, {4}, {5}); - "; - public const string DB_QUERY_PP_UPDATE_PROFILE = @" - UPDATE `Player` SET country = '{0}', - `last_seen` = {1}, `connections` = `connections` + 1, `name` = '{3}' - WHERE `id` = {2} LIMIT 1; - "; - } } } - - - -/* - /// - /// Replaces color codes from strings to CS# ChatColors. - /// {white} -> {ChatColors.White} - /// - /// String to replace colors - /// with 'ChatColors' - private static string ReplaceColors(string message) - { - var replacements = new Dictionary - { - { "{default}", $"{ChatColors.Default}" }, - { "{red}", $"{ChatColors.Red}" }, - { "{white}", $"{ChatColors.White}" }, - { "{darkred}", $"{ChatColors.DarkRed}" }, - { "{green}", $"{ChatColors.Green}" }, - { "{lightyellow}", $"{ChatColors.LightYellow}" }, - { "{lightblue}", $"{ChatColors.LightBlue}" }, - { "{olive}", $"{ChatColors.Olive}" }, - { "{lime}", $"{ChatColors.Lime}" }, - { "{lightpurple}", $"{ChatColors.LightPurple}" }, - { "{purple}", $"{ChatColors.Purple}" }, - { "{grey}", $"{ChatColors.Grey}" }, - { "{yellow}", $"{ChatColors.Yellow}" }, - { "{gold}", $"{ChatColors.Gold}" }, - { "{silver}", $"{ChatColors.Silver}" }, - { "{blue}", $"{ChatColors.Blue}" }, - { "{darkblue}", $"{ChatColors.DarkBlue}" }, - { "{bluegrey}", $"{ChatColors.BlueGrey}" }, - { "{magenta}", $"{ChatColors.Magenta}" }, - { "{lightred}", $"{ChatColors.LightRed}" }, - { "{orange}", $"{ChatColors.Orange}" } - }; - - foreach (var replacement in replacements) - message = message.Replace(replacement.Key, replacement.Value); - - return message; - } -*/ \ No newline at end of file diff --git a/src/ST-UTILS/Data/ApiDataAccessService.cs b/src/ST-UTILS/Data/ApiDataAccessService.cs index 5d5bd6f..e36f8f4 100644 --- a/src/ST-UTILS/Data/ApiDataAccessService.cs +++ b/src/ST-UTILS/Data/ApiDataAccessService.cs @@ -1,6 +1,8 @@ using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using SurfTimer.Shared.DTO; +using SurfTimer.Shared.Entities; namespace SurfTimer.Data { @@ -8,415 +10,345 @@ public class ApiDataAccessService : IDataAccessService { private readonly ILogger _logger; + /// + /// Add/load data using API calls. + /// public ApiDataAccessService() { _logger = SurfTimer.ServiceProvider.GetRequiredService>(); } - /* PersonalBest.cs */ - /// - /// Loads the Checkpoint data for the given MapTime_ID. Used for loading player's personal bests and Map's world records. - /// Bonus and Stage runs should NOT have any checkpoints. - /// - public async Task> LoadCheckpointsAsync(int runId, [CallerMemberName] string methodName = "") + public async Task PingAccessService([CallerMemberName] string methodName = "") { - _logger.LogInformation("[{ClassName}] {MethodName} -> LoadCheckpointsAsync -> Using API data access service.", - nameof(ApiDataAccessService), methodName - ); + try + { + var response = await ApiMethod.GET>( + string.Format( + Config.Api.Endpoints.ENDPOINT_PING, + (double)DateTimeOffset.UtcNow.ToUnixTimeSeconds() + ) + ); + + if (response != null && response.ContainsKey("clientUnix")) + { + _logger.LogInformation( + "[{ClassName}] {MethodName} -> Success -> Client: {ClientUnix} | Server: {ServerUnix} | Latency: {LatencyS}s | Latency: {LatencyMS}ms", + nameof(ApiDataAccessService), + methodName, + response["clientUnix"], + response["serverUnix"], + response["latencySeconds"], + response["latencyMs"] + ); + return true; + } - var checkpoints = await ApiMethod - .GET( - string.Format(Config.API.Endpoints.ENDPOINT_MAP_GET_RUN_CPS, runId) + _logger.LogWarning( + "[{ClassName}] {MethodName} -> Unexpected response structure.", + nameof(ApiDataAccessService), + methodName ); - if (checkpoints == null || checkpoints.Length == 0) - return new Dictionary(); + return false; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> Failed to reach API.", + nameof(ApiDataAccessService), + methodName + ); + return false; + } + } - _logger.LogInformation("[{ClassName}] {MethodName} -> LoadCheckpointsAsync -> Found {Count} checkpoints.", - nameof(ApiDataAccessService), methodName, checkpoints.Length + /* PersonalBest.cs */ + public async Task> LoadCheckpointsAsync( + int runId, + [CallerMemberName] string methodName = "" + ) + { + var checkpoints = await ApiMethod.GET>( + string.Format(Config.Api.Endpoints.ENDPOINT_MAP_GET_RUN_CPS, runId) + ); + if (checkpoints == null || checkpoints.Count == 0) + return new Dictionary(); + + _logger.LogInformation( + "[{ClassName}] {MethodName} -> LoadCheckpointsAsync -> Found {Count} checkpoints for MapTimeId {MapTimeId}.", + nameof(ApiDataAccessService), + methodName, + checkpoints.Count, + runId ); - return checkpoints - .Select(cp => - { - var c = new Checkpoint( - cp.cp, - cp.run_time, - cp.start_vel_x, - cp.start_vel_y, - cp.start_vel_z, - cp.end_vel_x, - cp.end_vel_y, - cp.end_vel_z, - cp.end_touch, - cp.attempts - ); - c.ID = cp.cp; - return c; - }) - .ToDictionary(c => c.CP, c => c); + return checkpoints; } - public async Task LoadPersonalBestRunAsync( - int? pbId, int playerId, int mapId, int type, int style, [CallerMemberName] string methodName = "" + public async Task LoadPersonalBestRunAsync( + int? pbId, + int playerId, + int mapId, + int type, + int style, + [CallerMemberName] string methodName = "" ) { - string url = pbId == null || pbId == -1 - ? string.Format(Config.API.Endpoints.ENDPOINT_MAP_GET_PB_BY_PLAYER, - playerId, mapId, type, style) - : string.Format(Config.API.Endpoints.ENDPOINT_MAP_GET_PB_BY_ID, - pbId.Value); - - var apiResult = await ApiMethod.GET(url); + string url = + pbId == null || pbId == -1 + ? string.Format( + Config.Api.Endpoints.ENDPOINT_MAP_GET_PB_BY_PLAYER, + playerId, + mapId, + type, + style + ) + : string.Format(Config.Api.Endpoints.ENDPOINT_MAP_GET_PB_BY_ID, pbId.Value); + + var apiResult = await ApiMethod.GET(url); if (apiResult == null) return null; - _logger.LogInformation("[{ClassName}] {MethodName} -> LoadPersonalBestRunAsync -> Personal Best data", - nameof(ApiDataAccessService), methodName + _logger.LogInformation( + "[{ClassName}] {MethodName} -> LoadPersonalBestRunAsync -> Personal Best data", + nameof(ApiDataAccessService), + methodName ); - return new PersonalBestDataModel - { - ID = apiResult.id, - Ticks = apiResult.run_time, - Rank = apiResult.rank, - StartVelX = apiResult.start_vel_x, - StartVelY = apiResult.start_vel_y, - StartVelZ = apiResult.start_vel_z, - EndVelX = apiResult.end_vel_x, - EndVelY = apiResult.end_vel_y, - EndVelZ = apiResult.end_vel_z, - RunDate = apiResult.run_date, - ReplayFramesBase64 = apiResult.replay_frames - }; + return apiResult; } - /* Map.cs */ - public async Task GetMapInfoAsync(string mapName, [CallerMemberName] string methodName = "") + public async Task GetMapInfoAsync( + string mapName, + [CallerMemberName] string methodName = "" + ) { - var mapInfo = await ApiMethod.GET( - string.Format(Config.API.Endpoints.ENDPOINT_MAP_GET_INFO, mapName)); + var mapInfo = await ApiMethod.GET( + string.Format(Config.Api.Endpoints.ENDPOINT_MAP_GET_INFO, mapName) + ); if (mapInfo != null) { - _logger.LogInformation("[{ClassName}] {MethodName} -> GetMapInfoAsync -> Found MapInfo data", - nameof(ApiDataAccessService), methodName + _logger.LogInformation( + "[{ClassName}] {MethodName} -> GetMapInfoAsync -> Found MapInfo data. MapID {MapId}", + nameof(ApiDataAccessService), + methodName, + mapInfo.ID ); - return new MapInfoDataModel - { - ID = mapInfo.id, - Name = mapInfo.name, - Author = mapInfo.author, - Tier = mapInfo.tier, - Stages = mapInfo.stages, - Bonuses = mapInfo.bonuses, - Ranked = mapInfo.ranked == 1, - DateAdded = mapInfo.date_added ?? 0, - LastPlayed = mapInfo.last_played ?? 0 - }; + return mapInfo; } return null; } - public async Task InsertMapInfoAsync(MapInfoDataModel mapInfo, [CallerMemberName] string methodName = "") + public async Task InsertMapInfoAsync( + MapDto mapInfo, + [CallerMemberName] string methodName = "" + ) { - var apiMapInfo = new API_MapInfo - { - id = -1, // API-side will ignore or auto-increment - name = mapInfo.Name, - author = mapInfo.Author, - tier = mapInfo.Tier, - stages = mapInfo.Stages, - bonuses = mapInfo.Bonuses, - ranked = mapInfo.Ranked ? 1 : 0, - }; - - var postResponse = await ApiMethod.POST(Config.API.Endpoints.ENDPOINT_MAP_INSERT_INFO, apiMapInfo); - - if (postResponse == null || postResponse.last_id <= 0) + var postResponse = await ApiMethod.POST( + Config.Api.Endpoints.ENDPOINT_MAP_INSERT_INFO, + mapInfo + ); + + if (postResponse == null || postResponse.Id <= 0) { - throw new Exception($"API failed to insert map '{mapInfo.Name}'."); + Exception ex = new($"API failed to insert map '{mapInfo.Name}'."); + throw ex; } - return postResponse.last_id; + return postResponse.Id; } - public async Task UpdateMapInfoAsync(MapInfoDataModel mapInfo, [CallerMemberName] string methodName = "") + public async Task UpdateMapInfoAsync( + MapDto mapInfo, + int mapId, + [CallerMemberName] string methodName = "" + ) { - var apiMapInfo = new API_MapInfo - { - id = mapInfo.ID, - name = mapInfo.Name, - author = mapInfo.Author, - tier = mapInfo.Tier, - stages = mapInfo.Stages, - bonuses = mapInfo.Bonuses, - ranked = mapInfo.Ranked ? 1 : 0, - // last_played = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds() - }; - - var response = await ApiMethod.PUT(Config.API.Endpoints.ENDPOINT_MAP_UPDATE_INFO, apiMapInfo); + var response = await ApiMethod.PUT( + string.Format(Config.Api.Endpoints.ENDPOINT_MAP_UPDATE_INFO, mapId), + mapInfo + ); if (response == null) { - throw new Exception($"API failed to update map '{mapInfo.Name}' (ID {mapInfo.ID})."); + Exception ex = new($"API failed to update map '{mapInfo.Name}' (ID {mapId})."); + throw ex; } } - /// - /// Gets and loads all the record times for a given map ID - /// - /// ID of the map in DB - /// - public async Task> GetMapRecordRunsAsync(int mapId, [CallerMemberName] string methodName = "") + public async Task> GetMapRecordRunsAsync( + int mapId, + [CallerMemberName] string methodName = "" + ) { - // TODO: Re-do the API with the new query and fix the API assign of values - var apiRuns = await ApiMethod.GET( - string.Format(Config.API.Endpoints.ENDPOINT_MAP_GET_RUNS, mapId)); - - var runs = new List(); - - if (apiRuns != null) - { - foreach (var time in apiRuns) - { - runs.Add(new MapRecordRunDataModel - { - ID = time.id, - RunTime = time.run_time, - Type = time.type, // API currently returns only map times, needs rework - Stage = time.stage, - Style = time.style, // Fix this when updating API - Name = time.name, - StartVelX = (float)time.start_vel_x, - StartVelY = (float)time.start_vel_y, - StartVelZ = (float)time.start_vel_z, - EndVelX = (float)time.end_vel_x, - EndVelY = (float)time.end_vel_y, - EndVelZ = (float)time.end_vel_z, - RunDate = time.run_date, - TotalCount = time.total_count, // API should return total count, fix this as well - ReplayFramesBase64 = time.replay_frames // API should return this - }); - } - } + var apiRuns = await ApiMethod.GET>( + string.Format(string.Format(Config.Api.Endpoints.ENDPOINT_MAP_GET_RUNS, mapId)) + ); - return runs; + return apiRuns!; } - /* PlayerProfile.cs */ - public async Task GetPlayerProfileAsync(ulong steamId, [CallerMemberName] string methodName = "") + public async Task GetPlayerProfileAsync( + ulong steamId, + [CallerMemberName] string methodName = "" + ) { - // TODO: Implement API logic - // throw new NotImplementedException(); - - var player = await ApiMethod.GET( - string.Format(Config.API.Endpoints.ENDPOINT_PP_GET_PROFILE, steamId)); + var player = await ApiMethod.GET( + string.Format(Config.Api.Endpoints.ENDPOINT_PP_GET_PROFILE, steamId) + ); if (player != null) { - _logger.LogInformation("[{ClassName}] {MethodName} -> GetPlayerProfileAsync -> Found PlayerProfile data", - nameof(ApiDataAccessService), methodName + _logger.LogInformation( + "[{ClassName}] {MethodName} -> GetPlayerProfileAsync -> Found PlayerProfile data for ProfileID = {ProfileID}", + nameof(ApiDataAccessService), + methodName, + player.ID ); - return new PlayerProfileDataModel - { - ID = player.id, - // SteamID = steamId, - Name = player.name, - Country = player.country, - JoinDate = player.join_date, - LastSeen = player.last_seen, - Connections = player.connections - }; + + return player; } - _logger.LogWarning("[{ClassName}] {MethodName} -> GetPlayerProfileAsync -> No PlayerProfile data found for {SteamID}", - nameof(ApiDataAccessService), methodName, steamId + _logger.LogWarning( + "[{ClassName}] {MethodName} -> GetPlayerProfileAsync -> No PlayerProfile data found for {SteamID}", + nameof(ApiDataAccessService), + methodName, + steamId ); return null; } - public async Task InsertPlayerProfileAsync(PlayerProfileDataModel profile, [CallerMemberName] string methodName = "") + public async Task InsertPlayerProfileAsync( + PlayerProfileDto profile, + [CallerMemberName] string methodName = "" + ) { - // TODO: Implement API logic - // throw new NotImplementedException(); - int joinDate = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - - var apiPlayerProfileInfo = new API_PlayerSurfProfile - { - steam_id = profile.SteamID, - name = profile.Name, - country = profile.Country, - join_date = joinDate, - last_seen = joinDate, - connections = 1 - }; - - var postResponse = await ApiMethod.POST(Config.API.Endpoints.ENDPOINT_PP_INSERT_PROFILE, apiPlayerProfileInfo); + var postResponse = await ApiMethod.POST( + Config.Api.Endpoints.ENDPOINT_PP_INSERT_PROFILE, + profile + ); - if (postResponse == null || postResponse.last_id <= 0) + if (postResponse == null || postResponse.Id <= 0) { - throw new Exception($"API failed to insert Player Profile for '{profile.Name}'."); + Exception ex = new($"API failed to insert Player Profile for '{profile.Name}'."); + throw ex; } - return postResponse.last_id; + return postResponse.Id; } - public async Task UpdatePlayerProfileAsync(PlayerProfileDataModel profile, [CallerMemberName] string methodName = "") + public async Task UpdatePlayerProfileAsync( + PlayerProfileDto profile, + int playerId, + [CallerMemberName] string methodName = "" + ) { - // TODO: Implement API logic - // throw new NotImplementedException(); - int lastSeen = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var response = await ApiMethod.PUT( + string.Format(Config.Api.Endpoints.ENDPOINT_PP_UPDATE_PROFILE, playerId), + profile + ); - var apiPlayerProfileInfo = new API_PlayerSurfProfile - { - id = profile.ID, - steam_id = profile.SteamID, - name = profile.Name, - country = profile.Country, - join_date = profile.JoinDate, - last_seen = lastSeen, - connections = 1 - }; - - var response = await ApiMethod.PUT(Config.API.Endpoints.ENDPOINT_PP_UPDATE_PROFILE, apiPlayerProfileInfo); if (response == null) { - throw new Exception($"API failed to update Player Profile for '{apiPlayerProfileInfo.name}' (ID {apiPlayerProfileInfo.id})."); + Exception ex = new( + $"API failed to update Player Profile for '{profile.Name}' (ID {playerId})." + ); + throw ex; } } - /* PlayerStats.cs */ - public async Task> GetPlayerMapTimesAsync(int playerId, int mapId, [CallerMemberName] string methodName = "") + public async Task> GetPlayerMapTimesAsync( + int playerId, + int mapId, + [CallerMemberName] string methodName = "" + ) { - // TODO: Implement API logic - var mapTimes = new List(); - - var apiResponse = await ApiMethod.GET( - string.Format(Config.API.Endpoints.ENDPOINT_PS_GET_PLAYER_MAP_DATA, playerId, mapId) + var apiResponse = await ApiMethod.GET>( + string.Format(Config.Api.Endpoints.ENDPOINT_PS_GET_PLAYER_MAP_DATA, playerId, mapId) ); - if (apiResponse != null) + if (apiResponse == null) { - _logger.LogInformation("[{ClassName}] {MethodName} -> GetPlayerMapTimesAsync -> Found maptime data for PlayerID {PlayerID} and MapID {MapID}", - nameof(ApiDataAccessService), methodName, playerId, mapId + Exception ex = new( + $"API failed to GET MapTime entries for PlayerID '{playerId}' and MapID '{mapId}'." ); - - foreach (var time in apiResponse) - { - mapTimes.Add(new PlayerMapTimeDataModel - { - ID = time.id, - RunTime = time.run_time, - Type = time.type, - Stage = time.stage, - Style = time.style, - Rank = time.rank, - StartVelX = (float)time.start_vel_x, - StartVelY = (float)time.start_vel_y, - StartVelZ = (float)time.start_vel_z, - EndVelX = (float)time.end_vel_x, - EndVelY = (float)time.end_vel_y, - EndVelZ = (float)time.end_vel_z, - RunDate = time.run_date, - ReplayFramesBase64 = time.replay_frames - }); - } + throw ex; } - return mapTimes; - } - + _logger.LogInformation( + "[{ClassName}] {MethodName} -> GetPlayerMapTimesAsync -> Found maptime data for PlayerID {PlayerID} and MapID {MapID}", + nameof(ApiDataAccessService), + methodName, + playerId, + mapId + ); + return apiResponse; + } /* CurrentRun.cs */ - public async Task InsertMapTimeAsync(MapTimeDataModel mapTime, [CallerMemberName] string methodName = "") + public async Task InsertMapTimeAsync( + MapTimeRunDataDto mapTime, + [CallerMemberName] string methodName = "" + ) { - // Convert the Checkpoint object to the API_Checkpoint one - var runCheckpoints = mapTime.Checkpoints.Select(cp => new API_Checkpoint - { - cp = cp.Key, - run_time = cp.Value.Ticks, - end_touch = cp.Value.EndTouch, - start_vel_x = cp.Value.StartVelX, - start_vel_y = cp.Value.StartVelY, - start_vel_z = cp.Value.StartVelZ, - end_vel_x = cp.Value.EndVelX, - end_vel_y = cp.Value.EndVelY, - end_vel_z = cp.Value.EndVelZ, - attempts = cp.Value.Attempts - }).ToList(); - - var apiSaveMapTime = new API_SaveMapTime - { - player_id = mapTime.PlayerId, - map_id = mapTime.MapId, - run_time = mapTime.Ticks, - start_vel_x = mapTime.StartVelX, - start_vel_y = mapTime.StartVelY, - start_vel_z = mapTime.StartVelZ, - end_vel_x = mapTime.EndVelX, - end_vel_y = mapTime.EndVelY, - end_vel_z = mapTime.EndVelZ, - style = mapTime.Style, - type = mapTime.Type, - stage = mapTime.Stage, - replay_frames = mapTime.ReplayFramesBase64, - run_date = mapTime.RunDate, - checkpoints = runCheckpoints - }; - - /* - _logger.LogDebug( - "[{ClassName}] {MethodName} -> Converted and sending API_SaveMapTime:\n" + - " player_id: {PlayerId}\n" + - " map_id: {MapId}\n" + - " run_time: {RunTime}\n" + - " style: {Style}\n" + - " type: {Type}\n" + - " stage: {Stage}\n" + - " start_vel: ({StartVelX}, {StartVelY}, {StartVelZ})\n" + - " end_vel: ({EndVelX}, {EndVelY}, {EndVelZ})\n" + - " replay_frames: {ReplayFramesLength}\n" + - " checkpoints: {CheckpointsCount}\n" + - " run_date: {RunDate}", - nameof(CurrentRun), methodName, - apiSaveMapTime.player_id, - apiSaveMapTime.map_id, - apiSaveMapTime.run_time, - apiSaveMapTime.style, - apiSaveMapTime.type, - apiSaveMapTime.stage, - apiSaveMapTime.start_vel_x, apiSaveMapTime.start_vel_y, apiSaveMapTime.start_vel_z, - apiSaveMapTime.end_vel_x, apiSaveMapTime.end_vel_y, apiSaveMapTime.end_vel_z, - apiSaveMapTime.replay_frames?.Length ?? 0, - apiSaveMapTime.checkpoints?.Count ?? 0, - apiSaveMapTime.run_date ?? 0 - ); - */ - var postResponse = await ApiMethod.POST( - Config.API.Endpoints.ENDPOINT_CR_SAVE_MAP_TIME, - apiSaveMapTime + Config.Api.Endpoints.ENDPOINT_CR_SAVE_MAP_TIME, + mapTime ); - if (postResponse == null || postResponse.last_id <= 0) + if (postResponse == null || postResponse.Inserted <= 0) { - throw new Exception($"API failed to insert MapTime for Player ID '{mapTime.PlayerId}' on Map ID '{mapTime.MapId}'."); + Exception ex = new( + $"API failed to insert MapTime for Player ID '{mapTime.PlayerID}' on Map ID '{mapTime.MapID}'." + ); + throw ex; } - return postResponse.last_id; + _logger.LogDebug( + "[{ClassName}] {MethodName} -> Successfully inserted entry with id {ID} with type {Type}", + nameof(ApiDataAccessService), + methodName, + postResponse.Id, + mapTime.Type + ); + + return postResponse.Id; } - // public async Task SaveRunCheckpointsAsync(int mapTimeId, IEnumerable checkpoints, [CallerMemberName] string methodName = "") - // { - // // TODO: Implement API logic - // // throw new NotImplementedException(); + public async Task UpdateMapTimeAsync( + MapTimeRunDataDto mapTime, + int mapTimeId, + [CallerMemberName] string methodName = "" + ) + { + var postResponse = await ApiMethod.PUT( + string.Format(Config.Api.Endpoints.ENDPOINT_CR_UPDATE_MAP_TIME, mapTimeId), + mapTime + ); - // } + if (postResponse == null || postResponse.Inserted <= 0) + { + Exception ex = new( + $"API failed to update MapTime {mapTimeId} for Player ID '{mapTime.PlayerID}' on Map ID '{mapTime.MapID}'." + ); + throw ex; + } + _logger.LogDebug( + "[{ClassName}] {MethodName} -> Successfully updated MapTime entry {ID} with type {Type}", + nameof(ApiDataAccessService), + methodName, + mapTimeId, + mapTime.Type + ); + + return postResponse.Id; + } } } diff --git a/src/ST-UTILS/Data/IDataAccessService.cs b/src/ST-UTILS/Data/IDataAccessService.cs index 0efc60a..9bbe2b4 100644 --- a/src/ST-UTILS/Data/IDataAccessService.cs +++ b/src/ST-UTILS/Data/IDataAccessService.cs @@ -1,27 +1,44 @@ using System.Runtime.CompilerServices; -using CounterStrikeSharp.API.Modules.Entities; +using SurfTimer.Shared.DTO; +using SurfTimer.Shared.Entities; namespace SurfTimer.Data { + /// + /// Contains all methods for data retrieval or insertion by all access services (API, MySQL) + /// public interface IDataAccessService { + /// + /// Ping the Data Access Service. + /// + /// True for successful connection, False otherwise + Task PingAccessService([CallerMemberName] string methodName = ""); + /* PersonalBest.cs */ /// - /// Load all checkpoints for a given run ID (map time). - /// Returns an empty dictionary if none found. + /// Retrieve Checkpoints table entries for a given run ID (map time). + /// Bonus and Stage runs should NOT have any checkpoints. /// - Task> LoadCheckpointsAsync( + /// ID of the run from DB + /// Dictionary[int, CheckpointEntity] data or NULL if none found + Task> LoadCheckpointsAsync( int runId, [CallerMemberName] string methodName = "" ); /// - /// Load a personal-best run for a given player. - /// - If pbId is null or -1, load by playerId/mapId/type/style. - /// - If pbId has a value, load that specific run. - /// Returns null if not found. + /// Load a personal-best run for a given player from MapTime table through API or MySQL. + /// If pbId is null or -1, load by playerId/mapId/type/style. + /// If pbId has a value, load that specific run. /// - Task LoadPersonalBestRunAsync( + /// [Optional] ID of the run from DB. If present other arguments will be ignored + /// ID of the player from DB. If pbId is null or -1 + /// ID of the map from DB. If pbId is null or -1 + /// Run Type (0 = Map ; 1 = Bonus ; 2 = Stage). If pbId is null or -1 + /// If pbId is null or -1 + /// MapTimeRunDataEntity data or null if not found + Task LoadPersonalBestRunAsync( int? pbId, int playerId, int mapId, @@ -30,49 +47,105 @@ Task> LoadCheckpointsAsync( [CallerMemberName] string methodName = "" ); - /* Map.cs */ - Task GetMapInfoAsync( - string mapName, [CallerMemberName] string methodName = "" - ); - Task InsertMapInfoAsync( - MapInfoDataModel mapInfo, [CallerMemberName] string methodName = "" - ); + /// + /// Retrieves Map table entry for map through API or MySQL. + /// + /// Name of map + /// MapEntity data + Task GetMapInfoAsync(string mapName, [CallerMemberName] string methodName = ""); + + /// + /// Adds Map table entry for map through API or MySQL. + /// + /// Data to add in table + /// int mapId + Task InsertMapInfoAsync(MapDto mapInfo, [CallerMemberName] string methodName = ""); + + /// + /// Updates Map table entry for map through API or MySQL. + /// + /// Data to update in table Task UpdateMapInfoAsync( - MapInfoDataModel mapInfo, [CallerMemberName] string methodName = "" - ); - Task> GetMapRecordRunsAsync( - int mapId, [CallerMemberName] string methodName = "" + MapDto mapInfo, + int mapId, + [CallerMemberName] string methodName = "" ); + /// + /// Retrieves MapTime table record runs for given mapId through API or MySQL. + /// + /// ID from DB + /// List[MapTimeRunDataEntity] data + Task> GetMapRecordRunsAsync( + int mapId, + [CallerMemberName] string methodName = "" + ); /* PlayerProfile.cs */ - Task GetPlayerProfileAsync( - ulong steamId, [CallerMemberName] string methodName = "" + /// + /// Retrieve Player table entry for the player through API or MySQL. + /// + /// SteamID for the player + /// PlayerProfileEntity data + Task GetPlayerProfileAsync( + ulong steamId, + [CallerMemberName] string methodName = "" ); + + /// + /// Adds Player table entry for the player through API or MySQL. + /// + /// Data to add in table + /// int playerId given by DB Task InsertPlayerProfileAsync( - PlayerProfileDataModel profile, [CallerMemberName] string methodName = "" + PlayerProfileDto profile, + [CallerMemberName] string methodName = "" ); + + /// + /// Updates Player table entry for the player through API or MySQL. + /// + /// Data to update in table Task UpdatePlayerProfileAsync( - PlayerProfileDataModel profile, [CallerMemberName] string methodName = "" + PlayerProfileDto profile, + int playerId, + [CallerMemberName] string methodName = "" ); - /* PlayerStats.cs */ - Task> GetPlayerMapTimesAsync( - int playerId, int mapId, [CallerMemberName] string methodName = "" + /// + /// Retrieves ALL MapTime table entries for playerId and mapId combo through API or MySQL. + /// + /// ID from DB + /// ID from DB + /// List[MapTimeRunDataEntity] data + Task> GetPlayerMapTimesAsync( + int playerId, + int mapId, + [CallerMemberName] string methodName = "" ); - /* CurrentRun.cs */ + /// + /// Adds a MapTime table entry through API or MySQL. Deals with checkpoints for map runs of type 0 + /// + /// Data to insert/update in table + /// int mapTimeId given by DB Task InsertMapTimeAsync( - // Task InsertMapTimeAsync( - MapTimeDataModel mapTime, [CallerMemberName] string methodName = "" + MapTimeRunDataDto mapTime, + [CallerMemberName] string methodName = "" ); - /* Merged with InsertMapTimeAsync */ - // Task SaveRunCheckpointsAsync( - // int mapTimeId, IEnumerable checkpoints, [CallerMemberName] string methodName = "" - // ); + /// + /// Updates a MapTime table entry through API or MySQL. Deals with checkpoints for map runs of type 0 + /// + /// Data to update in table + /// int mapTimeId that was updated + Task UpdateMapTimeAsync( + MapTimeRunDataDto mapTime, + int mapTimeId, + [CallerMemberName] string methodName = "" + ); } } diff --git a/src/ST-UTILS/Data/Models.cs b/src/ST-UTILS/Data/Models.cs deleted file mode 100644 index 9456f19..0000000 --- a/src/ST-UTILS/Data/Models.cs +++ /dev/null @@ -1,66 +0,0 @@ -// File: Data/PersonalBestDataModel.cs -namespace SurfTimer.Data -{ - public class PersonalBestDataModel : RunStats - { - public int ID { get; set; } - public int Rank { get; set; } - } - - public class MapInfoDataModel - { - public int ID { get; set; } - public string Name { get; set; } = "N/A"; - public string Author { get; set; } = "Unknown"; - public int Tier { get; set; } - public int Stages { get; set; } - public int Bonuses { get; set; } - public bool Ranked { get; set; } - public int DateAdded { get; set; } - public int LastPlayed { get; set; } - } - - - public class MapRecordRunDataModel : RunStats - { - public int ID { get; set; } - public int RunTime { get; set; } - public int Type { get; set; } // 0 = Map, 1 = Bonus, 2 = Stage - public int Stage { get; set; } - public int Style { get; set; } - public string Name { get; set; } = ""; - public int TotalCount { get; set; } - // public string ReplayFramesBase64 { get; set; } = ""; - } - - - public class PlayerProfileDataModel - { - public int ID { get; set; } = 0; - public string Name { get; set; } = ""; - public ulong SteamID { get; set; } = 0; - public string Country { get; set; } = ""; - public int JoinDate { get; set; } = 0; - public int LastSeen { get; set; } = 0; - public int Connections { get; set; } = 0; - } - - public class PlayerMapTimeDataModel : RunStats - { - public int ID { get; set; } - public int RunTime { get; set; } - public int Type { get; set; } // 0 = Map, 1 = Bonus, 2 = Stage - public int Stage { get; set; } - public int Style { get; set; } - public int Rank { get; set; } - } - - public class MapTimeDataModel : RunStats - { - public int PlayerId { get; set; } - public int MapId { get; set; } - public int Style { get; set; } - public int Type { get; set; } // 0 = Map, 1 = Bonus, 2 = Stage - public int Stage { get; set; } - } -} diff --git a/src/ST-UTILS/Data/MySqlDataAccessService.cs b/src/ST-UTILS/Data/MySqlDataAccessService.cs index eb85e88..fc6b28c 100644 --- a/src/ST-UTILS/Data/MySqlDataAccessService.cs +++ b/src/ST-UTILS/Data/MySqlDataAccessService.cs @@ -1,8 +1,10 @@ -using MySqlConnector; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.DependencyInjection; using System.Runtime.CompilerServices; -using System.Data; +using Dapper; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SurfTimer.Shared.DTO; +using SurfTimer.Shared.Entities; +using SurfTimer.Shared.Sql; namespace SurfTimer.Data { @@ -10,369 +12,517 @@ public class MySqlDataAccessService : IDataAccessService { private readonly ILogger _logger; + /// + /// Add/load data using MySQL connection and queries. + /// public MySqlDataAccessService() { - _logger = SurfTimer.ServiceProvider.GetRequiredService>(); + _logger = SurfTimer.ServiceProvider.GetRequiredService< + ILogger + >(); } - /* PersonalBest.cs */ - /// - /// Loads the Checkpoint data for the given MapTime_ID. Used for loading player's personal bests and Map's world records. - /// Bonus and Stage runs should NOT have any checkpoints. - /// - public async Task> LoadCheckpointsAsync(int runId, [CallerMemberName] string methodName = "") + public async Task PingAccessService([CallerMemberName] string methodName = "") { - _logger.LogInformation("[{ClassName}] {MethodName} -> LoadCheckpointsAsync -> Using MySQL data access service.", - nameof(MySqlDataAccessService), methodName - ); - - var dict = new Dictionary(); - - using (var results = await SurfTimer.DB.QueryAsync( - string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_CPS, runId))) + try { - if (results == null || !results.HasRows) - return dict; + var val = await SurfTimer.DB.QueryFirstOrDefaultAsync(Queries.DB_QUERY_PING); + + var reachable = val != 0; - while (results.Read()) + if (reachable) { - var cp = new Checkpoint( - results.GetInt32("cp"), - results.GetInt32("run_time"), - results.GetFloat("start_vel_x"), - results.GetFloat("start_vel_y"), - results.GetFloat("start_vel_z"), - results.GetFloat("end_vel_x"), - results.GetFloat("end_vel_y"), - results.GetFloat("end_vel_z"), - results.GetInt32("end_touch"), - results.GetInt32("attempts") + _logger.LogInformation( + "[{ClassName}] {MethodName} -> PingAccessService -> MySQL is reachable", + nameof(MySqlDataAccessService), + methodName ); - cp.ID = results.GetInt32("cp"); - dict[cp.CP] = cp; } + + return reachable; } + catch (Exception ex) + { + _logger.LogCritical( + ex, + "[{ClassName}] {MethodName} -> PingAccessService -> MySQL is unreachable", + nameof(MySqlDataAccessService), + methodName + ); + return false; + } + } - _logger.LogInformation("[{ClassName}] {MethodName} -> LoadCheckpointsAsync -> Found {Count} checkpoints.", - nameof(MySqlDataAccessService), methodName, dict.Count + /* PersonalBest.cs */ + public async Task> LoadCheckpointsAsync( + int runId, + [CallerMemberName] string methodName = "" + ) + { + // Dapper handles mapping. + var rows = await SurfTimer.DB.QueryAsync( + Queries.DB_QUERY_PB_GET_CPS, + new { MapTimeID = runId } + ); + + // Key the dictionary by CP. + var dict = rows.ToDictionary(cp => (int)cp.CP); + + _logger.LogInformation( + "[{ClassName}] {MethodName} -> LoadCheckpointsAsync -> Found {Count} checkpoints.", + nameof(MySqlDataAccessService), + methodName, + dict.Count ); return dict; } - public async Task LoadPersonalBestRunAsync( - int? pbId, int playerId, int mapId, int type, int style, [CallerMemberName] string methodName = "" + public async Task LoadPersonalBestRunAsync( + int? pbId, + int playerId, + int mapId, + int type, + int style, + [CallerMemberName] string methodName = "" ) { - _logger.LogInformation("[{ClassName}] {MethodName} -> LoadPersonalBestRunAsync -> Using MySQL data access service.", - nameof(MySqlDataAccessService), methodName - ); + // Choose SQL and parameters based on whether a specific PB id is provided. + string sql; + object args; + + if (!pbId.HasValue || pbId == -1) + { + sql = Queries.DB_QUERY_PB_GET_TYPE_RUNTIME; + args = new + { + PlayerId = playerId, + MapId = mapId, + Type = type, + Style = style, + }; + } + else + { + sql = Queries.DB_QUERY_PB_GET_SPECIFIC_MAPTIME_DATA; + args = new { MapTimeId = pbId.Value }; + } - string sql = pbId == null || pbId == -1 - ? string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_TYPE_RUNTIME, - playerId, mapId, type, style) - : string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_SPECIFIC_MAPTIME_DATA, - pbId.Value); + // Fetch a single row (or null). + var run = await SurfTimer.DB.QueryFirstOrDefaultAsync(sql, args); - using var results = await SurfTimer.DB.QueryAsync(sql); - if (results == null || !results.HasRows) + if (run is null) { - _logger.LogInformation("[{ClassName}] {MethodName} -> LoadPersonalBestRunAsync -> No data found. PersonalBestID {PbID} | PlayerID {PlayerID} | MapID {MapID} | Type {Type} | Style {Style}", - nameof(MySqlDataAccessService), methodName, pbId, playerId, mapId, type, style + _logger.LogInformation( + "[{ClassName}] {MethodName} -> LoadPersonalBestRunAsync -> No data found. PersonalBestID {PbID} | PlayerID {PlayerID} | MapID {MapID} | Type {Type} | Style {Style}", + nameof(MySqlDataAccessService), + methodName, + pbId, + playerId, + mapId, + type, + style ); return null; } - // read the first (and only) row - await results.ReadAsync(); - - _logger.LogInformation("[{ClassName}] {MethodName} -> LoadPersonalBestRunAsync -> Found data for PersonalBestID {PbID} | PlayerID {PlayerID} | MapID {MapID} | Type {Type} | Style {Style}", - nameof(MySqlDataAccessService), methodName, pbId, playerId, mapId, type, style + _logger.LogInformation( + "[{ClassName}] {MethodName} -> LoadPersonalBestRunAsync -> Found data for PersonalBestID {PbID} | PlayerID {PlayerID} | MapID {MapID} | Type {Type} | Style {Style}", + nameof(MySqlDataAccessService), + methodName, + pbId, + playerId, + mapId, + type, + style ); - return new PersonalBestDataModel - { - ID = results.GetInt32("id"), - Ticks = results.GetInt32("run_time"), - Rank = results.GetInt32("rank"), - StartVelX = (float)results.GetDouble("start_vel_x"), - StartVelY = (float)results.GetDouble("start_vel_y"), - StartVelZ = (float)results.GetDouble("start_vel_z"), - EndVelX = (float)results.GetDouble("end_vel_x"), - EndVelY = (float)results.GetDouble("end_vel_y"), - EndVelZ = (float)results.GetDouble("end_vel_z"), - RunDate = results.GetInt32("run_date") - }; + return run; } - /* Map.cs */ - public async Task GetMapInfoAsync(string mapName, [CallerMemberName] string methodName = "") + public async Task GetMapInfoAsync( + string mapName, + [CallerMemberName] string methodName = "" + ) { - using var mapData = await SurfTimer.DB.QueryAsync( - string.Format(Config.MySQL.Queries.DB_QUERY_MAP_GET_INFO, MySqlHelper.EscapeString(mapName))); + var mapInfo = await SurfTimer.DB.QueryFirstOrDefaultAsync( + Queries.DB_QUERY_MAP_GET_INFO, + new { mapName } + ); - if (mapData.HasRows && mapData.Read()) + if (mapInfo is not null) { - _logger.LogInformation("[{ClassName}] {MethodName} -> GetMapInfoAsync -> Found MapInfo data", - nameof(MySqlDataAccessService), methodName + _logger.LogInformation( + "[{ClassName}] {MethodName} -> GetMapInfoAsync -> Found MapInfo data (ID: {ID})", + nameof(MySqlDataAccessService), + methodName, + mapInfo.ID ); - - return new MapInfoDataModel - { - ID = mapData.GetInt32("id"), - Name = mapName, - Author = mapData.GetString("author") ?? "Unknown", - Tier = mapData.GetInt32("tier"), - Ranked = mapData.GetBoolean("ranked"), - DateAdded = mapData.GetInt32("date_added"), - LastPlayed = mapData.GetInt32("last_played"), - }; } - return null; + return mapInfo; } - public async Task InsertMapInfoAsync(MapInfoDataModel mapInfo, [CallerMemberName] string methodName = "") + public async Task InsertMapInfoAsync( + MapDto mapInfo, + [CallerMemberName] string methodName = "" + ) { - // int rowsWritten = await SurfTimer.DB.WriteAsync( - var (rowsWritten, lastId) = await SurfTimer.DB.WriteAsync( - string.Format(Config.MySQL.Queries.DB_QUERY_MAP_INSERT_INFO, - MySqlHelper.EscapeString(mapInfo.Name), - MySqlHelper.EscapeString(mapInfo.Author), - mapInfo.Tier, - mapInfo.Stages, - mapInfo.Bonuses, - mapInfo.Ranked ? 1 : 0, - (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()) + var newId = await SurfTimer.DB.InsertAsync( + Queries.DB_QUERY_MAP_INSERT_INFO, + new + { + Name = mapInfo.Name, + Author = mapInfo.Author, + Tier = mapInfo.Tier, + Stages = mapInfo.Stages, + Bonuses = mapInfo.Bonuses, + Ranked = mapInfo.Ranked ? 1 : 0, + DateAdded = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + LastPlayed = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + } ); - if (rowsWritten != 1) + if (newId <= 0) { - throw new Exception($"Failed to insert new map '{mapInfo.Name}' into database."); + Exception ex = new( + $"Failed to insert new map '{mapInfo.Name}' into database. LAST_INSERT_ID() was 0." + ); + + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> InsertMapInfoAsync -> {ErrorMessage}", + nameof(MySqlDataAccessService), + methodName, + ex.Message + ); + + throw ex; } - return (int)lastId; + return (int)newId; } - public async Task UpdateMapInfoAsync(MapInfoDataModel mapInfo, [CallerMemberName] string methodName = "") + public async Task UpdateMapInfoAsync( + MapDto mapInfo, + int mapId, + [CallerMemberName] string methodName = "" + ) { - string updateQuery = string.Format( - Config.MySQL.Queries.DB_QUERY_MAP_UPDATE_INFO_FULL, - (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - mapInfo.Stages, - mapInfo.Bonuses, - mapInfo.ID + var rowsUpdated = await SurfTimer.DB.ExecuteAsync( + Queries.DB_QUERY_MAP_UPDATE_INFO_FULL, + new + { + LastPlayed = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + Stages = mapInfo.Stages, + Bonuses = mapInfo.Bonuses, + Author = mapInfo.Author, + Tier = mapInfo.Tier, + Ranked = mapInfo.Ranked ? 1 : 0, // TINYINT(1) + Id = mapId, + } ); - var (rowsUpdated, lastId) = await SurfTimer.DB.WriteAsync(updateQuery); if (rowsUpdated != 1) { - throw new Exception($"Failed to update map '{mapInfo.Name}' (ID {mapInfo.ID}) in database."); - } - } - - public async Task> GetMapRecordRunsAsync(int mapId, [CallerMemberName] string methodName = "") - { - var runs = new List(); - - using var results = await SurfTimer.DB.QueryAsync( - string.Format(Config.MySQL.Queries.DB_QUERY_MAP_GET_RECORD_RUNS_AND_COUNT, mapId)); - - if (results.HasRows) - { - while (results.Read()) - { - string replayFramesBase64; + Exception ex = new( + $"Failed to update map '{mapInfo.Name}' (ID {mapId}) in database. Rows updated: {rowsUpdated}" + ); - try - { - replayFramesBase64 = results.GetString("replay_frames"); - } - catch (InvalidCastException) - { - byte[] replayFramesData = results.GetFieldValue("replay_frames"); - replayFramesBase64 = System.Text.Encoding.UTF8.GetString(replayFramesData); - } + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> UpdateMapInfoAsync -> {ErrorMessage}", + nameof(MySqlDataAccessService), + methodName, + ex.Message + ); - runs.Add(new MapRecordRunDataModel - { - ID = results.GetInt32("id"), - RunTime = results.GetInt32("run_time"), - Type = results.GetInt32("type"), - Stage = results.GetInt32("stage"), - Style = results.GetInt32("style"), - Name = results.GetString("name"), - StartVelX = results.GetFloat("start_vel_x"), - StartVelY = results.GetFloat("start_vel_y"), - StartVelZ = results.GetFloat("start_vel_z"), - EndVelX = results.GetFloat("end_vel_x"), - EndVelY = results.GetFloat("end_vel_y"), - EndVelZ = results.GetFloat("end_vel_z"), - RunDate = results.GetInt32("run_date"), - TotalCount = results.GetInt32("total_count"), - ReplayFramesBase64 = replayFramesBase64 - }); - } + throw ex; } - - return runs; } + public async Task> GetMapRecordRunsAsync( + int mapId, + [CallerMemberName] string methodName = "" + ) + { + var runs = await SurfTimer.DB.QueryAsync( + Queries.DB_QUERY_MAP_GET_RECORD_RUNS_AND_COUNT, + new { Id = mapId } + ); + return runs.ToList(); + } /* PlayerProfile.cs */ - public async Task GetPlayerProfileAsync(ulong steamId, [CallerMemberName] string methodName = "") + public async Task GetPlayerProfileAsync( + ulong steamId, + [CallerMemberName] string methodName = "" + ) { - using var playerData = await SurfTimer.DB.QueryAsync( - string.Format(Config.MySQL.Queries.DB_QUERY_PP_GET_PROFILE, steamId)); - - if (playerData.HasRows && playerData.Read()) - { - return new PlayerProfileDataModel - { - ID = playerData.GetInt32("id"), - SteamID = steamId, - Name = playerData.GetString("name"), - Country = playerData.GetString("country"), - JoinDate = playerData.GetInt32("join_date"), - LastSeen = playerData.GetInt32("last_seen"), - Connections = playerData.GetInt32("connections") - }; - } + var playerData = await SurfTimer.DB.QueryFirstOrDefaultAsync( + Queries.DB_QUERY_PP_GET_PROFILE, + new { SteamID = steamId } + ); - return null; + return playerData; } - public async Task InsertPlayerProfileAsync(PlayerProfileDataModel profile, [CallerMemberName] string methodName = "") + public async Task InsertPlayerProfileAsync( + PlayerProfileDto profile, + [CallerMemberName] string methodName = "" + ) { int joinDate = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - var (rowsInserted, lastId) = await SurfTimer.DB.WriteAsync(string.Format( - Config.MySQL.Queries.DB_QUERY_PP_INSERT_PROFILE, - MySqlConnector.MySqlHelper.EscapeString(profile.Name), - profile.SteamID, - profile.Country, - joinDate, - joinDate, - 1)); - - if (rowsInserted != 1) - throw new Exception($"Failed to insert new player '{profile.Name}' ({profile.SteamID})."); - return (int)lastId; - } + var lastId = await SurfTimer.DB.InsertAsync( + Queries.DB_QUERY_PP_INSERT_PROFILE, + new + { + Name = profile.Name, + SteamID = profile.SteamID, + Country = profile.Country, + JoinDate = joinDate, + LastSeen = joinDate, + Connections = 1, + } + ); - public async Task UpdatePlayerProfileAsync(PlayerProfileDataModel profile, [CallerMemberName] string methodName = "") - { - int lastSeen = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - var (rowsUpdated, lastId) = await SurfTimer.DB.WriteAsync(string.Format( - Config.MySQL.Queries.DB_QUERY_PP_UPDATE_PROFILE, - profile.Country, - lastSeen, - profile.ID, - MySqlConnector.MySqlHelper.EscapeString(profile.Name))); + if (lastId <= 0) + { + Exception ex = new( + $"Failed to insert new player '{profile.Name}' ({profile.SteamID}). LAST_INSERT_ID() was 0." + ); - if (rowsUpdated != 1) - throw new Exception($"Failed to update player '{profile.Name}' ({profile.SteamID})."); - } + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> InsertPlayerProfileAsync -> {ErrorMessage}", + nameof(MySqlDataAccessService), + methodName, + ex.Message + ); + throw ex; + } + return (int)lastId; + } - /* PlayerStats.cs */ - public async Task> GetPlayerMapTimesAsync(int playerId, int mapId, [CallerMemberName] string methodName = "") + public async Task UpdatePlayerProfileAsync( + PlayerProfileDto profile, + int playerId, + [CallerMemberName] string methodName = "" + ) { - var mapTimes = new List(); - - using var results = await SurfTimer.DB.QueryAsync( - string.Format(Config.MySQL.Queries.DB_QUERY_PS_GET_ALL_RUNTIMES, playerId, mapId)); + int lastSeen = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - if (results.HasRows) - { - while (results.Read()) + var rowsAffected = await SurfTimer.DB.ExecuteAsync( + Queries.DB_QUERY_PP_UPDATE_PROFILE, + new { - mapTimes.Add(new PlayerMapTimeDataModel - { - ID = results.GetInt32("id"), - RunTime = results.GetInt32("run_time"), - Type = results.GetInt32("type"), - Stage = results.GetInt32("stage"), - Style = results.GetInt32("style"), - Rank = results.GetInt32("rank"), - StartVelX = (float)results.GetDouble("start_vel_x"), - StartVelY = (float)results.GetDouble("start_vel_y"), - StartVelZ = (float)results.GetDouble("start_vel_z"), - EndVelX = (float)results.GetDouble("end_vel_x"), - EndVelY = (float)results.GetDouble("end_vel_y"), - EndVelZ = (float)results.GetDouble("end_vel_z"), - RunDate = results.GetInt32("run_date") - }); + Country = profile.Country, + LastSeen = lastSeen, + Name = profile.Name, + Id = playerId, } - } + ); + + if (rowsAffected != 1) + { + Exception ex = new( + $"Failed to update player '{profile.Name}' ({profile.SteamID})." + ); - return mapTimes; + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> UpdatePlayerProfileAsync -> {ErrorMessage}", + nameof(MySqlDataAccessService), + methodName, + ex.Message + ); + + throw ex; + } } + /* PlayerStats.cs */ + public async Task> GetPlayerMapTimesAsync( + int playerId, + int mapId, + [CallerMemberName] string methodName = "" + ) + { + var mapTimes = await SurfTimer.DB.QueryAsync( + Queries.DB_QUERY_PS_GET_ALL_RUNTIMES, + new { PlayerId = playerId, MapId = mapId } + ); + // Convert IEnumerable to List + return mapTimes.ToList(); + } /* CurrentRun.cs */ - public async Task InsertMapTimeAsync(MapTimeDataModel mapTime, [CallerMemberName] string methodName = "") + public async Task InsertMapTimeAsync( + MapTimeRunDataDto mapTime, + [CallerMemberName] string methodName = "" + ) { - var (rowsInserted, lastId) = await SurfTimer.DB.WriteAsync( - string.Format( - Config.MySQL.Queries.DB_QUERY_CR_INSERT_TIME, - mapTime.PlayerId, - mapTime.MapId, - mapTime.Style, - mapTime.Type, - mapTime.Stage, - mapTime.Ticks, - mapTime.StartVelX, - mapTime.StartVelY, - mapTime.StartVelZ, - mapTime.EndVelX, - mapTime.EndVelY, - mapTime.EndVelZ, - mapTime.RunDate, - mapTime.ReplayFramesBase64) + // 1) Insert the run and get LAST_INSERT_ID() + var mapTimeId = await SurfTimer.DB.InsertAsync( + Queries.DB_QUERY_CR_INSERT_TIME, + new + { + PlayerId = mapTime.PlayerID, + MapId = mapTime.MapID, + Style = mapTime.Style, + Type = mapTime.Type, + Stage = mapTime.Stage, + RunTime = mapTime.RunTime, + StartVelX = mapTime.StartVelX, + StartVelY = mapTime.StartVelY, + StartVelZ = mapTime.StartVelZ, + EndVelX = mapTime.EndVelX, + EndVelY = mapTime.EndVelY, + EndVelZ = mapTime.EndVelZ, + RunDate = mapTime.RunDate, + ReplayFrames = mapTime.ReplayFrames, // assuming this matches your type handler + } ); - if (rowsInserted <= 0) + if (mapTimeId <= 0) { - throw new Exception($"Failed to insert map time for PlayerId {mapTime.PlayerId}."); - } + Exception ex = new( + $"Failed to insert map time for PlayerId {mapTime.PlayerID}. LAST_INSERT_ID() was 0." + ); - // Write the checkpoints after we have the `lastId` - if (mapTime.Checkpoints != null && mapTime.Checkpoints.Count > 0) + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> InsertMapTimeAsync -> {ErrorMessage}", + nameof(MySqlDataAccessService), + methodName, + ex.Message + ); + + throw ex; + } + // 2) Insert checkpoints in a single transaction (only for Type == 0) + if (mapTime.Type == 0 && mapTime.Checkpoints is { Count: > 0 }) { - var commands = new List(); - foreach (var cp in mapTime.Checkpoints.Values) + await SurfTimer.DB.TransactionAsync( + async (conn, tx) => + { + // Insert each checkpoint using the same transaction + foreach (var cp in mapTime.Checkpoints.Values) + { + await conn.ExecuteAsync( + Queries.DB_QUERY_CR_INSERT_CP, + new + { + MapTimeId = mapTimeId, + CP = cp.CP, + RunTime = cp.RunTime, + StartVelX = cp.StartVelX, + StartVelY = cp.StartVelY, + StartVelZ = cp.StartVelZ, + EndVelX = cp.EndVelX, + EndVelY = cp.EndVelY, + EndVelZ = cp.EndVelZ, + Attempts = cp.Attempts, + EndTouch = cp.EndTouch, + }, + tx + ); + } + } + ); + } + + return (int)mapTimeId; + } + + public async Task UpdateMapTimeAsync( + MapTimeRunDataDto mapTime, + int mapTimeId, + [CallerMemberName] string methodName = "" + ) + { + // 1) Update the run using it's ID + var affectedRows = await SurfTimer.DB.ExecuteAsync( + Queries.DB_QUERY_CR_UPDATE_TIME, + new { - commands.Add(string.Format( - Config.MySQL.Queries.DB_QUERY_CR_INSERT_CP, - lastId, cp.CP, cp.Ticks, cp.StartVelX, cp.StartVelY, cp.StartVelZ, - cp.EndVelX, cp.EndVelY, cp.EndVelZ, cp.Attempts, cp.EndTouch)); + RunTime = mapTime.RunTime, + StartVelX = mapTime.StartVelX, + StartVelY = mapTime.StartVelY, + StartVelZ = mapTime.StartVelZ, + EndVelX = mapTime.EndVelX, + EndVelY = mapTime.EndVelY, + EndVelZ = mapTime.EndVelZ, + RunDate = mapTime.RunDate, + ReplayFrames = mapTime.ReplayFrames, // assuming this matches your type handler + MapTimeId = mapTimeId, } - await SurfTimer.DB.TransactionAsync(commands); - } + ); + if (affectedRows <= 0) + { + Exception ex = new( + $"Failed to update map time for MapTimeId {mapTimeId}. affectedRows was {affectedRows}." + ); - return (int)lastId; - } + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> UpdateMapTimeAsync -> {ErrorMessage}", + nameof(MySqlDataAccessService), + methodName, + ex.Message + ); - // public async Task SaveRunCheckpointsAsync(int mapTimeId, IEnumerable checkpoints, [CallerMemberName] string methodName = "") - // { - // var commands = new List(); + throw ex; + } - // foreach (var cp in checkpoints) - // { - // commands.Add(string.Format( - // Config.MySQL.Queries.DB_QUERY_CR_INSERT_CP, - // mapTimeId, cp.CP, cp.Ticks, cp.StartVelX, cp.StartVelY, cp.StartVelZ, - // cp.EndVelX, cp.EndVelY, cp.EndVelZ, cp.Attempts, cp.EndTouch)); - // } + _logger.LogInformation( + "[{ClassName}] {MethodName} -> UpdateMapTimeAsync -> Updated MapTimeId {MapTimeId} with {AffectedRows} affected rows.", + nameof(MySqlDataAccessService), + methodName, + mapTimeId, + affectedRows + ); - // await SurfTimer.DB.TransactionAsync(commands); - // } + // 2) Insert checkpoints in a single transaction (only for Type == 0) + if (mapTime.Type == 0 && mapTime.Checkpoints is { Count: > 0 }) + { + await SurfTimer.DB.TransactionAsync( + async (conn, tx) => + { + // Insert each checkpoint using the same transaction + foreach (var cp in mapTime.Checkpoints.Values) + { + await conn.ExecuteAsync( + Queries.DB_QUERY_CR_INSERT_CP, + new + { + MapTimeId = mapTimeId, + CP = cp.CP, + RunTime = cp.RunTime, + StartVelX = cp.StartVelX, + StartVelY = cp.StartVelY, + StartVelZ = cp.StartVelZ, + EndVelX = cp.EndVelX, + EndVelY = cp.EndVelY, + EndVelZ = cp.EndVelZ, + Attempts = cp.Attempts, + EndTouch = cp.EndTouch, + }, + tx + ); + } + } + ); + } + return affectedRows; + } } } diff --git a/src/ST-UTILS/Extensions.cs b/src/ST-UTILS/Extensions.cs index 493b3b5..1127fec 100644 --- a/src/ST-UTILS/Extensions.cs +++ b/src/ST-UTILS/Extensions.cs @@ -9,7 +9,7 @@ namespace SurfTimer; unsafe static class Extensions { - public static void Teleport(this CBaseEntity entity, Vector_t? position = null, QAngle_t? angles = null, Vector_t? velocity = null) + public static void Teleport(this CBaseEntity entity, VectorT? position = null, QAngleT? angles = null, VectorT? velocity = null) { Guard.IsValidEntity(entity); @@ -39,19 +39,123 @@ public static void Teleport(this CBaseEntity entity, Vector_t? position = null, (nint)pAng, (nint)pVel); } - public static (Vector_t fwd, Vector_t right, Vector_t up) AngleVectors(this QAngle vec) => vec.ToQAngle_t().AngleVectors(); - public static void AngleVectors(this QAngle vec, out Vector_t fwd, out Vector_t right, out Vector_t up) => vec.ToQAngle_t().AngleVectors(out fwd, out right, out up); + public static (VectorT fwd, VectorT right, VectorT up) AngleVectors(this QAngle vec) => vec.ToQAngle_t().AngleVectors(); + public static void AngleVectors(this QAngle vec, out VectorT fwd, out VectorT right, out VectorT up) => vec.ToQAngle_t().AngleVectors(out fwd, out right, out up); - public static Vector_t ToVector_t(this Vector vec) => new(vec.Handle); - public static QAngle_t ToQAngle_t(this QAngle vec) => new(vec.Handle); + public static VectorT ToVector_t(this Vector vec) => new(vec.Handle); + public static QAngleT ToQAngle_t(this QAngle vec) => new(vec.Handle); public static void SetCollisionGroup(this CCSPlayerController controller, CollisionGroup collisionGroup) { - if (!controller.IsValid || controller.Collision==null) return; + if (!controller.IsValid || controller.Collision == null) return; controller.Collision.CollisionAttribute.CollisionGroup = (byte)collisionGroup; controller.Collision.CollisionGroup = (byte)collisionGroup; Utilities.SetStateChanged(controller, "CColisionProperity", "m_collisionGroup"); Utilities.SetStateChanged(controller, "CCollisionProperty", "m_collisionAttribute"); } + + + /// + /// Checks whether an IP is a local one. Allows testing the plugin in a local environment setup for GeoIP + /// + /// IP to check + /// True for Private IP + public static bool IsPrivateIP(string ip) + { + var ipParts = ip.Split('.'); + int firstOctet = int.Parse(ipParts[0]); + int secondOctet = int.Parse(ipParts[1]); + + // 10.x.x.x range + if (firstOctet == 10) + return true; + + // 172.16.x.x to 172.31.x.x range + if (firstOctet == 172 && (secondOctet >= 16 && secondOctet <= 31)) + return true; + + // 192.168.x.x range + if (firstOctet == 192 && secondOctet == 168) + return true; + + return false; + } + + /// + /// Asssigns a ChatColor to the given Tier value + /// + /// Map Tier up to 8 + /// Appropriate ChatColor value for the Tier + public static char GetTierColor(short tier) + { + return tier switch + { + 1 => ChatColors.Green, + 2 => ChatColors.Lime, + 3 => ChatColors.Yellow, + 4 => ChatColors.Orange, + 5 => ChatColors.LightRed, + 6 => ChatColors.DarkRed, + 7 => ChatColors.LightPurple, + 8 => ChatColors.Purple, + _ => ChatColors.White + }; + } + + /// + /// Color gradient for speed, based on a range of velocities. + /// + /// Velocity to determine color for + /// Minimum velocity + /// Maximum velocity + /// HEX value as string + public static string GetSpeedColorGradient(float velocity, float minSpeed = 240f, float maxSpeed = 4000f) + { + // Key colors (HEX -> RGB) + (int R, int G, int B)[] gradient = new (int, int, int)[] + { + (79, 195, 247), // blue #4FC3F7 + (46, 159, 101), // green #2E9F65 + (255, 255, 0), // yellow #FFFF00 + (255, 165, 0), // orange #FFA500 + (255, 0, 0) // red #FF0000 + }; + + // Limit velocity + velocity = Math.Clamp(velocity, minSpeed, maxSpeed); + + // Normalize velocity to 0..1 + float t = (velocity - minSpeed) / (maxSpeed - minSpeed); + + // Calculate which part of the gradient we are in + float scaledT = t * (gradient.Length - 1); + int index1 = (int)Math.Floor(scaledT); + int index2 = Math.Min(index1 + 1, gradient.Length - 1); + + float localT = scaledT - index1; + + // Linear interpolation between the two color points + int r = (int)(gradient[index1].R + (gradient[index2].R - gradient[index1].R) * localT); + int g = (int)(gradient[index1].G + (gradient[index2].G - gradient[index1].G) * localT); + int b = (int)(gradient[index1].B + (gradient[index2].B - gradient[index1].B) * localT); + + return $"#{r:X2}{g:X2}{b:X2}"; + } + + /// + /// Calculates the velocity of a given player controller + /// + /// Controller to calculate velocity for + /// float velocity + public static float GetVelocityFromController(CCSPlayerController controller) + { + var pawn = controller.PlayerPawn?.Value; + if (pawn == null) + return 0.0f; + + var vel = pawn.AbsVelocity; + return (float)Math.Sqrt(vel.X * vel.X + vel.Y * vel.Y + vel.Z * vel.Z); + } + } \ No newline at end of file diff --git a/src/ST-UTILS/Injection.cs b/src/ST-UTILS/Injection.cs index 7a370bf..ccd77e8 100644 --- a/src/ST-UTILS/Injection.cs +++ b/src/ST-UTILS/Injection.cs @@ -1,19 +1,20 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Localization; -using CounterStrikeSharp.API.Core; +using Microsoft.Extensions.Logging; using Serilog; using Serilog.Events; -using CounterStrikeSharp.API; using SurfTimer.Data; namespace SurfTimer; public class Injection : IPluginServiceCollection { - private static readonly string LogDirectory = $"{Server.GameDirectory}/csgo/addons/counterstrikesharp/logs"; + private static readonly string LogDirectory = + $"{Server.GameDirectory}/csgo/addons/counterstrikesharp/logs"; - public void ConfigureServices(IServiceCollection services) + public void ConfigureServices(IServiceCollection serviceCollection) { var fileName = $"log-SurfTimer-.txt"; // Date seems to be automatically appended so we leave it out var filePath = Path.Combine(LogDirectory, fileName); @@ -34,59 +35,39 @@ public void ConfigureServices(IServiceCollection services) Log.Information("[SurfTimer] Logging to file: {LogFile}", filePath); // Register Serilog as a logging provider for Microsoft.Extensions.Logging - services.AddLogging(builder => + serviceCollection.AddLogging(builder => { builder.ClearProviders(); builder.AddSerilog(dispose: true); }); // Register Dependencies - services.AddScoped(); // Multiple instances for different players - services.AddScoped(); // Multiple instances for different players - services.AddScoped(); // Multiple instances for different players - services.AddScoped(); // Multiple instances for different players - services.AddScoped(); // Multiple instances for different players - services.AddScoped(); // Multiple instances for different players - services.AddScoped(); // Multiple instances for different players - services.AddSingleton(); // Single instance for 1 Map object + serviceCollection.AddScoped(); // Multiple instances for different players + serviceCollection.AddScoped(); // Multiple instances for different players + serviceCollection.AddScoped(); // Multiple instances for different players + serviceCollection.AddScoped(); // Multiple instances for different players + serviceCollection.AddScoped(); // Multiple instances for different players + serviceCollection.AddScoped(); // Multiple instances for different players + serviceCollection.AddScoped(); // Multiple instances for different players + serviceCollection.AddSingleton(); // Single instance for 1 Map object - services.AddScoped(provider => - Config.API.GetApiOnly() - ? new ApiDataAccessService() - : new MySqlDataAccessService() + serviceCollection.AddScoped(provider => + Config.Api.GetApiOnly() ? new ApiDataAccessService() : new MySqlDataAccessService() ); } } -public class LocalizationService +/// +/// Handles translation files +/// +public static class LocalizationService { // Localizer as a Singleton public static IStringLocalizer? Localizer { get; private set; } - public static IStringLocalizer LocalizerNonNull - => Localizer!; + public static IStringLocalizer LocalizerNonNull => Localizer!; + public static void Init(IStringLocalizer localizer) { Localizer = localizer; } } - -/* -public class Injection : IPluginServiceCollection -{ - public void ConfigureServices(IServiceCollection serviceCollection) - { - // Register Logging - serviceCollection.AddLogging(builder => - { - builder.AddConsole(); - builder.SetMinimumLevel(LogLevel.Debug); - }); - - - // Register Dependencies - serviceCollection.AddScoped(); // Multiple instances for different players - serviceCollection.AddScoped(); // Multiple instances for different players - serviceCollection.AddSingleton(); // Single instance for 1 Map object - } -} -*/ \ No newline at end of file diff --git a/src/ST-UTILS/Structs/QAngleT.cs b/src/ST-UTILS/Structs/QAngleT.cs new file mode 100644 index 0000000..d0c1dfe --- /dev/null +++ b/src/ST-UTILS/Structs/QAngleT.cs @@ -0,0 +1,143 @@ +using CounterStrikeSharp.API.Core; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SurfTimer; + +public struct QAngleT : IAdditionOperators, + ISubtractionOperators, + IMultiplyOperators, + IDivisionOperators +{ + private float x, y, z; + + public float X + { + readonly get => x; + set => x = value; + } + + public float Y + { + readonly get => y; + set => y = value; + } + + public float Z + { + readonly get => z; + set => z = value; + } + + public const int SIZE = 3; + + public unsafe float this[int i] + { + readonly get + { + if (i < 0 || i > SIZE) + { + Exception ex = new IndexOutOfRangeException($"Index {i} is out of range for QAngleT. Valid range is 0 to {SIZE}."); + throw ex; + } + + fixed (void* ptr = &this) + { + return Unsafe.Read(Unsafe.Add(ptr, i)); + } + } + set + { + if (i < 0 || i > SIZE) + { + Exception ex = new IndexOutOfRangeException($"Index {i} is out of range for QAngleT. Valid range is 0 to {SIZE}."); + throw ex; + } + + fixed (void* ptr = &this) + { + Unsafe.Write(Unsafe.Add(ptr, i), value); + } + } + } + + public QAngleT() + { + } + + public unsafe QAngleT(nint ptr) : this(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef((void*)ptr), SIZE)) + { + } + + public QAngleT(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + public QAngleT(ReadOnlySpan values) + { + if (values.Length < SIZE) + { + throw new ArgumentOutOfRangeException(nameof(values)); + } + + this = Unsafe.ReadUnaligned(ref Unsafe.As(ref MemoryMarshal.GetReference(values))); + } + + public unsafe (VectorT fwd, VectorT right, VectorT up) AngleVectors() + { + VectorT fwd = default, right = default, up = default; + + nint pFwd = (nint)Unsafe.AsPointer(ref fwd); + nint pRight = (nint)Unsafe.AsPointer(ref right); + nint pUp = (nint)Unsafe.AsPointer(ref up); + + fixed (void* ptr = &this) + { + NativeAPI.AngleVectors((nint)ptr, pFwd, pRight, pUp); + } + + return (fwd, right, up); + } + + public unsafe void AngleVectors(out VectorT fwd, out VectorT right, out VectorT up) + { + fixed (void* ptr = &this, pFwd = &fwd, pRight = &right, pUp = &up) + { + NativeAPI.AngleVectors((nint)ptr, (nint)pFwd, (nint)pRight, (nint)pUp); + } + } + + public readonly override string ToString() + { + return $"{X:n2} {Y:n2} {Z:n2}"; + } + + public static QAngleT operator +(QAngleT a, QAngleT b) + { + return new QAngleT(a.X + b.X, a.Y + b.Y, a.Z + b.Z); + } + + public static QAngleT operator -(QAngleT a, QAngleT b) + { + return new QAngleT(a.X - b.X, a.Y - b.Y, a.Z - b.Z); + } + + public static QAngleT operator -(QAngleT a) + { + return new QAngleT(-a.X, -a.Y, -a.Z); + } + + public static QAngleT operator *(QAngleT a, float b) + { + return new QAngleT(a.X * b, a.Y * b, a.Z * b); + } + + public static QAngleT operator /(QAngleT a, float b) + { + return new QAngleT(a.X / b, a.Y / b, a.Z / b); + } +} \ No newline at end of file diff --git a/src/ST-UTILS/Structs/QAngle_t.cs b/src/ST-UTILS/Structs/QAngle_t.cs deleted file mode 100644 index f00c804..0000000 --- a/src/ST-UTILS/Structs/QAngle_t.cs +++ /dev/null @@ -1,123 +0,0 @@ -using CounterStrikeSharp.API.Core; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace SurfTimer; - -struct QAngle_t : IAdditionOperators, - ISubtractionOperators, - IMultiplyOperators, - IDivisionOperators -{ - public float X, Y, Z; - - public const int SIZE = 3; - - public unsafe float this[int i] - { - readonly get - { - if (i < 0 || i > SIZE) - { - throw new IndexOutOfRangeException(); - } - - fixed (void* ptr = &this) - { - return Unsafe.Read(Unsafe.Add(ptr, i)); - } - } - set - { - if (i < 0 || i > SIZE) - { - throw new IndexOutOfRangeException(); - } - - fixed (void* ptr = &this) - { - Unsafe.Write(Unsafe.Add(ptr, i), value); - } - } - } - - public QAngle_t() - { - } - - public unsafe QAngle_t(nint ptr) : this(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef((void*)ptr), SIZE)) - { - } - - public QAngle_t(float x, float y, float z) - { - X = x; - Y = y; - Z = z; - } - - public QAngle_t(ReadOnlySpan values) - { - if (values.Length < SIZE) - { - throw new ArgumentOutOfRangeException(nameof(values)); - } - - this = Unsafe.ReadUnaligned(ref Unsafe.As(ref MemoryMarshal.GetReference(values))); - } - - public unsafe (Vector_t fwd, Vector_t right, Vector_t up) AngleVectors() - { - Vector_t fwd = default, right = default, up = default; - - nint pFwd = (nint)Unsafe.AsPointer(ref fwd); - nint pRight = (nint)Unsafe.AsPointer(ref right); - nint pUp = (nint)Unsafe.AsPointer(ref up); - - fixed (void* ptr = &this) - { - NativeAPI.AngleVectors((nint)ptr, pFwd, pRight, pUp); - } - - return ( fwd, right, up ); - } - - public unsafe void AngleVectors(out Vector_t fwd, out Vector_t right, out Vector_t up) - { - fixed (void* ptr = &this, pFwd = &fwd, pRight = &right, pUp = &up) - { - NativeAPI.AngleVectors((nint)ptr, (nint)pFwd, (nint)pRight, (nint)pUp); - } - } - - public readonly override string ToString() - { - return $"{X:n2} {Y:n2} {Z:n2}"; - } - - public static QAngle_t operator +(QAngle_t a, QAngle_t b) - { - return new QAngle_t(a.X + b.X, a.Y + b.Y, a.Z + b.Z); - } - - public static QAngle_t operator -(QAngle_t a, QAngle_t b) - { - return new QAngle_t(a.X - b.X, a.Y - b.Y, a.Z - b.Z); - } - - public static QAngle_t operator -(QAngle_t a) - { - return new QAngle_t(-a.X, -a.Y, -a.Z); - } - - public static QAngle_t operator *(QAngle_t a, float b) - { - return new QAngle_t(a.X * b, a.Y * b, a.Z * b); - } - - public static QAngle_t operator /(QAngle_t a, float b) - { - return new QAngle_t(a.X / b, a.Y / b, a.Z / b); - } -} \ No newline at end of file diff --git a/src/ST-UTILS/Structs/VectorT.cs b/src/ST-UTILS/Structs/VectorT.cs new file mode 100644 index 0000000..05df527 --- /dev/null +++ b/src/ST-UTILS/Structs/VectorT.cs @@ -0,0 +1,144 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SurfTimer; + +public struct VectorT : IAdditionOperators, + ISubtractionOperators, + IMultiplyOperators, + IDivisionOperators +{ + private float x, y, z; + + public float X + { + readonly get => x; + set => x = value; + } + + public float Y + { + readonly get => y; + set => y = value; + } + + public float Z + { + readonly get => z; + set => z = value; + } + + public const int SIZE = 3; + + public unsafe float this[int i] + { + readonly get + { + if (i < 0 || i > SIZE) + { + Exception ex = new IndexOutOfRangeException($"Index {i} is out of range for VectorT. Valid range is higher than {SIZE}."); + throw ex; + } + + fixed (void* ptr = &this) + { + return Unsafe.Read(Unsafe.Add(ptr, i)); + } + } + set + { + if (i < 0 || i > SIZE) + { + Exception ex = new IndexOutOfRangeException($"Index {i} is out of range for VectorT. Valid range is higher than {SIZE}."); + throw ex; + } + + fixed (void* ptr = &this) + { + Unsafe.Write(Unsafe.Add(ptr, i), value); + } + } + } + + public VectorT() + { + } + + public unsafe VectorT(nint ptr) : this(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef((void*)ptr), SIZE)) + { + } + + public VectorT(float x, float y, float z) + { + this.x = x; + this.y = y; + this.z = z; + } + + public VectorT(ReadOnlySpan values) + { + if (values.Length < SIZE) + { + throw new ArgumentOutOfRangeException(nameof(values)); + } + + this = Unsafe.ReadUnaligned(ref Unsafe.As(ref MemoryMarshal.GetReference(values))); + } + + public readonly float Length() + { + return (float)Math.Sqrt(X * X + Y * Y + Z * Z); + } + + public readonly float Length2D() + { + return (float)Math.Sqrt(X * X + Y * Y); + } + + public readonly float velMag() + { + return (float)Math.Sqrt(X * X + Y * Y + Z + Z); + } + + public readonly bool IsZero(float tolerance = 0.0001f) + { + return Math.Abs(X) <= tolerance && Math.Abs(Y) <= tolerance && Math.Abs(Z) <= tolerance; + } + public void Scale(float scale) + { + X *= scale; + Y *= scale; + Z *= scale; + } + + public readonly override string ToString() + { + return $"{X:n2} {Y:n2} {Z:n2}"; + } + + public static VectorT operator +(VectorT a, VectorT b) + { + return new VectorT(a.X + b.X, a.Y + b.Y, a.Z + b.Z); + } + + public static VectorT operator -(VectorT a, VectorT b) + { + return new VectorT(a.X - b.X, a.Y - b.Y, a.Z - b.Z); + } + + public static VectorT operator -(VectorT a) + { + return new VectorT(-a.X, -a.Y, -a.Z); + } + + public static VectorT operator *(VectorT a, float b) + { + return new VectorT(a.X * b, a.Y * b, a.Z * b); + } + + public static VectorT operator /(VectorT a, float b) + { + return new VectorT(a.X / b, a.Y / b, a.Z / b); + } +} \ No newline at end of file diff --git a/src/ST-UTILS/Structs/Vector_t.cs b/src/ST-UTILS/Structs/Vector_t.cs deleted file mode 100644 index da8606a..0000000 --- a/src/ST-UTILS/Structs/Vector_t.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace SurfTimer; - -struct Vector_t : IAdditionOperators, - ISubtractionOperators, - IMultiplyOperators, - IDivisionOperators -{ - public float X, Y, Z; - - public const int SIZE = 3; - - public unsafe float this[int i] - { - readonly get - { - if (i < 0 || i > SIZE) - { - throw new IndexOutOfRangeException(); - } - - fixed (void* ptr = &this) - { - return Unsafe.Read(Unsafe.Add(ptr, i)); - } - } - set - { - if (i < 0 || i > SIZE) - { - throw new IndexOutOfRangeException(); - } - - fixed (void* ptr = &this) - { - Unsafe.Write(Unsafe.Add(ptr, i), value); - } - } - } - - public Vector_t() - { - } - - public unsafe Vector_t(nint ptr) : this(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef((void*)ptr), SIZE)) - { - } - - public Vector_t(float x, float y, float z) - { - X = x; - Y = y; - Z = z; - } - - public Vector_t(ReadOnlySpan values) - { - if (values.Length < SIZE) - { - throw new ArgumentOutOfRangeException(nameof(values)); - } - - this = Unsafe.ReadUnaligned(ref Unsafe.As(ref MemoryMarshal.GetReference(values))); - } - - public readonly float Length() - { - return (float)Math.Sqrt(X * X + Y * Y + Z * Z); - } - - public readonly float Length2D() - { - return (float)Math.Sqrt(X * X + Y * Y); - } - - public readonly float velMag() - { - return (float)Math.Sqrt(X * X + Y * Y + Z + Z ); - } - - public readonly bool IsZero(float tolerance = 0.0001f) - { - return Math.Abs(X) <= tolerance && Math.Abs(Y) <= tolerance && Math.Abs(Z) <= tolerance; - } - public void Scale(float scale) - { - X *= scale; - Y *= scale; - Z *= scale; - } - - public readonly override string ToString() - { - return $"{X:n2} {Y:n2} {Z:n2}"; - } - - public static Vector_t operator +(Vector_t a, Vector_t b) - { - return new Vector_t(a.X + b.X, a.Y + b.Y, a.Z + b.Z); - } - - public static Vector_t operator -(Vector_t a, Vector_t b) - { - return new Vector_t(a.X - b.X, a.Y - b.Y, a.Z - b.Z); - } - - public static Vector_t operator -(Vector_t a) - { - return new Vector_t(-a.X, -a.Y, -a.Z); - } - - public static Vector_t operator *(Vector_t a, float b) - { - return new Vector_t(a.X * b, a.Y * b, a.Z * b); - } - - public static Vector_t operator /(Vector_t a, float b) - { - return new Vector_t(a.X / b, a.Y / b, a.Z / b); - } -} \ No newline at end of file diff --git a/src/SurfTimer.Plugin.csproj b/src/SurfTimer.Plugin.csproj new file mode 100644 index 0000000..36ce64f --- /dev/null +++ b/src/SurfTimer.Plugin.csproj @@ -0,0 +1,45 @@ + + + net8.0 + enable + enable + true + + + true + + + + DEBUG + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SurfTimer.cs b/src/SurfTimer.cs index ec7ae41..cc7629c 100644 --- a/src/SurfTimer.cs +++ b/src/SurfTimer.cs @@ -1,11 +1,19 @@ /* - ___ _____ _________ ___ - ___ / _/ |/ / __/ _ \/ _ | - ___ _/ // / _// , _/ __ | - ___ /___/_/|_/_/ /_/|_/_/ |_| + ___ _____ ____________ ___ + ___ / _/ | / / ____/ __ \/ | + ___ / // |/ / /_ / /_/ / /| | + ___ _/ // /| / __/ / _, _/ ___ | + ___ /___/_/ |_/_/ /_/ |_/_/ |_| + + ___ ___________ __ ___ _____ __ ______ + ___ /_ __/ ___// / / | / ___// / / / __ \ + ___ / / \__ \/ / / /| | \__ \/ /_/ / / / / + ___ / / ___/ / /___/ ___ |___/ / __ / /_/ / + ___ /_/ /____/_____/_/ |_/____/_/ /_/_____/ Official Timer plugin for the CS2 Surf Initiative. Copyright (C) 2024 Liam C. (Infra) + Copyright (C) 2025 tslashd This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -25,26 +33,33 @@ You should have received a copy of the GNU Affero General Public License #define DEBUG +using System.Collections.Concurrent; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Core.Attributes.Registration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using SurfTimer.Data; +using SurfTimer.Shared.Data; +using SurfTimer.Shared.Data.MySql; namespace SurfTimer; // Gameplan: https://github.com/CS2Surf/Timer/tree/dev/README.md -[MinimumApiVersion(318)] +[MinimumApiVersion(337)] public partial class SurfTimer : BasePlugin { private readonly ILogger _logger; public static IServiceProvider ServiceProvider { get; private set; } = null!; + private readonly IDataAccessService? _dataService; // Inject ILogger and store IServiceProvider globally public SurfTimer(ILogger logger, IServiceProvider serviceProvider) { _logger = logger; ServiceProvider = serviceProvider; + _dataService = ServiceProvider.GetRequiredService(); } // Metadata @@ -54,33 +69,50 @@ public SurfTimer(ILogger logger, IServiceProvider serviceProvider) public override string ModuleAuthor => "The CS2 Surf Initiative - github.com/cs2surf"; // Globals - private Dictionary playerList = new Dictionary(); // This can probably be done way better, revisit - internal static TimerDatabase DB = new TimerDatabase(Config.MySQL.GetConnectionString()); // Initiate it with the correct connection string - // internal Map CurrentMap = null!; - internal static Map CurrentMap = null!; + private readonly ConcurrentDictionary playerList = new(); + internal static IDatabaseService DB { get; private set; } = null!; + public static Map CurrentMap { get; private set; } = null!; /* ========== MAP START HOOKS ========== */ public void OnMapStart(string mapName) { // Initialise Map Object - if ((CurrentMap == null || !CurrentMap.Name.Equals(mapName)) && mapName.Contains("surf_")) + if ((CurrentMap == null || CurrentMap.Name!.Equals(mapName)) && mapName.Contains("surf_")) { - Server.NextWorldUpdate(() => Console.WriteLine(String.Format(" ____________ ____ ___\n" - + " / ___/ __/_ | / __/_ ______/ _/\n" - + "/ /___\\ \\/ __/ _\\ \\/ // / __/ _/ \n" - + "\\___/___/____/ /___/\\_,_/_/ /_/\n" - + $"[{Config.PluginName}] v.{ModuleVersion} - loading map {mapName}.\n" - + $"[{Config.PluginName}] This software is licensed under the GNU Affero General Public License v3.0. See LICENSE for more information.\n" - + $"[{Config.PluginName}] ---> Source Code: https://github.com/CS2Surf/Timer\n" - + $"[{Config.PluginName}] ---> License Agreement: https://github.com/CS2Surf/Timer/blob/main/LICENSE\n" - ))); - - Server.NextWorldUpdate(async () => CurrentMap = await Map.CreateAsync(mapName)); // NextWorldUpdate runs even during server hibernation + _logger.LogInformation( + "[{Prefix}] New map {MapName} started. Initializing Map object.....", + Config.PluginName, + mapName + ); + + Server.NextWorldUpdateAsync(async () => // NextWorldUpdate runs even during server hibernation + { + _logger.LogInformation( + "{PluginLogo}\n" + + "[CS2 Surf] {PluginName} v.{ModuleVersion} - loading map {MapName}.\n" + + "[CS2 Surf] This software is licensed under the GNU Affero General Public License v3.0. See LICENSE for more information.\n" + + "[CS2 Surf] ---> Source Code: https://github.com/CS2Surf/Timer\n" + + "[CS2 Surf] ---> License Agreement: https://github.com/CS2Surf/Timer/blob/main/LICENSE\n", + Config.PluginLogo, + Config.PluginName, + ModuleVersion, + mapName + ); + + CurrentMap = new Map(mapName); + await CurrentMap.InitializeAsync(); + }); } } public void OnMapEnd() { + _logger.LogInformation( + "[{Prefix}] Map ({MapName}) ended. Cleaning up resources...", + Config.PluginName, + CurrentMap.Name + ); + // Clear/reset stuff here CurrentMap = null!; playerList.Clear(); @@ -97,7 +129,8 @@ public HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info) ConVarHelper.RemoveCheatFlagFromConVar("bot_zombie"); Server.ExecuteCommand("execifexists SurfTimer/server_settings.cfg"); - _logger.LogTrace("[{Prefix}] Executed configuration: server_settings.cfg", + _logger.LogTrace( + "[{Prefix}] Executed configuration: server_settings.cfg", Config.PluginName ); return HookResult.Continue; @@ -108,31 +141,61 @@ public override void Load(bool hotReload) { LocalizationService.Init(Localizer); - // Check if we have connected to the Database - if (DB != null) + // === Dapper bootstrap (snake_case mapping + type handlers) + DB init === + DapperBootstrapper.Init(); + var connString = Config.MySql.GetConnectionString(); + var factory = new MySqlConnectionStringFactory(connString); + DB = new DapperDatabaseService(factory); + + bool accessService = false; + + try + { + accessService = Task.Run(() => _dataService!.PingAccessService()) + .GetAwaiter() + .GetResult(); + } + catch (Exception ex) { - _logger.LogInformation("[{Prefix}] Database connection established.", + _logger.LogError( + ex, + "[{Prefix}] PingAccessService threw an exception.", Config.PluginName ); } + + if (accessService) + { + _logger.LogInformation( + "[{Prefix}] {AccessService} connection established.", + Config.PluginName, + Config.Api.GetApiOnly() ? "API" : "DB" + ); + } else { - _logger.LogCritical("[{Prefix}] Error connecting to the database.", - Config.PluginName + _logger.LogCritical( + "[{Prefix}] Error connecting to the {AccessService}.", + Config.PluginName, + Config.Api.GetApiOnly() ? "API" : "DB" + ); + + Exception exception = new( + $"[{Config.PluginName}] Error connecting to the {(Config.Api.GetApiOnly() ? "API" : "DB")}" ); - // To-do: Abort plugin loading + throw exception; } - _logger.LogInformation(""" - - ____________ ____ ___ - / ___/ __/_ | / __/_ ______/ _/ - / /___\ \/ __/ _\ \/ // / __/ _/ - \___/___/____/ /___/\_,_/_/ /_/ - [CS2 Surf] {PluginName} plugin loaded. Version: {ModuleVersion} - [CS2 Surf] This plugin is licensed under the GNU Affero General Public License v3.0. See LICENSE for more information. - Source code: https://github.com/CS2Surf/Timer - """, Config.PluginName, ModuleVersion + _logger.LogInformation( + """ + {PluginLogo} + [CS2 Surf] {PluginName} plugin loaded. Version: {ModuleVersion} + [CS2 Surf] This plugin is licensed under the GNU Affero General Public License v3.0. See LICENSE for more information. + Source code: https://github.com/CS2Surf/Timer + """, + Config.PluginLogo, + Config.PluginName, + ModuleVersion ); // Map Start Hook @@ -142,7 +205,6 @@ ____________ ____ ___ // Tick listener RegisterListener(OnTick); - HookEntityOutput("trigger_multiple", "OnStartTouch", OnTriggerStartTouch); HookEntityOutput("trigger_multiple", "OnEndTouch", OnTriggerEndTouch); } diff --git a/src/SurfTimer.csproj b/src/SurfTimer.csproj deleted file mode 100644 index 35c78ef..0000000 --- a/src/SurfTimer.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - net8.0 - enable - enable - true - - - - - DEBUG - - - - - - - - - - - - - diff --git a/src/bin/MaxMind.Db.dll b/src/bin/MaxMind.Db.dll deleted file mode 100644 index dadb1f6..0000000 Binary files a/src/bin/MaxMind.Db.dll and /dev/null differ diff --git a/src/bin/MaxMind.GeoIP2.dll b/src/bin/MaxMind.GeoIP2.dll deleted file mode 100644 index a665688..0000000 Binary files a/src/bin/MaxMind.GeoIP2.dll and /dev/null differ diff --git a/src/bin/MySqlConnector.dll b/src/bin/MySqlConnector.dll deleted file mode 100644 index ea3237d..0000000 Binary files a/src/bin/MySqlConnector.dll and /dev/null differ