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 c9ddbf8..976eaf9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -# DEVELOPMENT IS NOT ACTIVE - -## EVERYONE IS WELCOME TO BUILD UP ON THIS PROJECT AND CONTRIBUTE. ISSUES ARE DISABLED FOR THE TIME BEING +# EVERYONE IS WELCOME TO BUILD UP ON THIS PROJECT AND CONTRIBUTE. ISSUES ARE DISABLED FOR THE TIME BEING ## Please join the Discord: https://discord.cs.surf # Timer @@ -162,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 @@ -207,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 @@ -222,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 58070b4..46f582e 100644 --- a/TODO +++ b/TODO @@ -1,11 +1,13 @@ -- Re-add the MySQL queries in code and make it switch between API and DB functions -- Map Time is NOT being saved with API +- 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 - Make configs generate themselves inside the `./configs/plugins/...` folder -- 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 ++ 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 - Double check the Start/End velocities for End Zone,Checkpoints, Stages, Bonuses + Maps @@ -14,7 +16,7 @@ - Try and get the Start/End velocity values from the Checkpoint object in Map Run + Bonuses -- Using `!spec` from any start zone and going back to CT starts your timer +- Using `!spec` from any start zone and going back to CT starts your timer - Will be in a PR - Try and determine Left/Right side for Bonuses (surf_ace) - Try and determine Left/Right side for Maps 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 544b7b4..c8686ca 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,3 +1,45 @@ { - + "prefix": "[{blue}Surf{bluegrey}Timer{default}]", + + "player_connected": "{green}{0}{default} has connected from {lime}{1}{default}.", + + /* 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}", + + /* 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 82d3f73..038eb95 100644 --- a/src/ST-API/Comms.cs +++ b/src/ST-API/Comms.cs @@ -1,85 +1,229 @@ 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, + }; - public static async Task GET(string url) + static ApiMethod() { - var uri = new Uri(base_addr + url); + _jsonOptions.Converters.Add(new ReplayFramesStringConverter()); + } -#if DEBUG - Console.WriteLine($"======= CS2 Surf DEBUG >> public static async Task GET -> BASE ADDR: {base_addr} | ENDPOINT: {url} | FULL: {uri.ToString()}"); -#endif + /// + /// 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(BaseAddress + url); + var _logger = SurfTimer.ServiceProvider.GetRequiredService>(); using var response = await _client.GetAsync(uri); try { - System.Console.WriteLine($"[API] GET {url} => {response.StatusCode}"); - if (response.StatusCode != System.Net.HttpStatusCode.OK) + 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) { - Exception exception = new Exception("[API] GET - No data found"); + _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}" + ); throw exception; } - return await response.Content.ReadFromJsonAsync(); + // Input the custom JsonSerializerOptions to handle ReplayFramesString conversion + return await response.Content.ReadFromJsonAsync(_jsonOptions); } - catch + catch (Exception ex) { - Console.WriteLine("HTTP Response was invalid or could not be deserialised."); + _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) + /// + /// 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); - -#if DEBUG - Console.WriteLine($"======= CS2 Surf DEBUG >> public static async Task POST -> BASE ADDR: {base_addr} | ENDPOINT: {url} | FULL: {uri.ToString()}"); -#endif - - using var response = await _client.PostAsJsonAsync(uri, body); + var uri = new Uri(BaseAddress + url); + var _logger = SurfTimer.ServiceProvider.GetRequiredService>(); try { - System.Console.WriteLine($"[API] POST {url} => {response.StatusCode}"); - response.EnsureSuccessStatusCode(); // BAD BAD BAD - return await response.Content.ReadFromJsonAsync(); + 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} | x-response-time-ms {ResponseTime}", + nameof(ApiMethod), + methodName, + url, + response.StatusCode, + responseTime + ); + + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(); + } + else + { + // Read response body to log what went wrong + var errorContent = await response.Content.ReadAsStringAsync(); + + _logger.LogWarning( + "[{ClassName}] {MethodName} -> POST {URL} failed with status {StatusCode}. Response body: {ResponseBody}", + nameof(ApiMethod), + methodName, + url, + response.StatusCode, + errorContent + ); + + return default; + } } - catch + catch (Exception ex) { - Console.WriteLine("HTTP Response was invalid or could not be deserialised."); + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> Exception during POST {URL}", + nameof(ApiMethod), + methodName, + url + ); + return default; } } - public static async Task PUT(string url, T body) + /// + /// 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); - -#if DEBUG - Console.WriteLine($"======= CS2 Surf DEBUG >> public static async Task PUT -> BASE ADDR: {base_addr} | ENDPOINT: {url} | FULL: {uri.ToString()}"); -#endif - - using var response = await _client.PutAsJsonAsync(uri, body); + var uri = new Uri(BaseAddress + url); + var _logger = SurfTimer.ServiceProvider.GetRequiredService>(); try { - System.Console.WriteLine($"[API] PUT {url} => {response.StatusCode}"); - response.EnsureSuccessStatusCode(); // BAD BAD BAD - return await response.Content.ReadFromJsonAsync(); + 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} | x-response-time-ms {ResponseTime}", + nameof(ApiMethod), + methodName, + url, + response.StatusCode, + responseTime + ); + + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(); + } + else + { + var errorContent = await response.Content.ReadAsStringAsync(); + + _logger.LogWarning( + "[{ClassName}] {MethodName} -> PUT {URL} failed with status {StatusCode}. Response body: {ResponseBody}", + nameof(ApiMethod), + methodName, + url, + response.StatusCode, + errorContent + ); + + return default; + } } - catch + catch (Exception ex) { - Console.WriteLine("HTTP Response was invalid or could not be deserialised."); + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> Exception during PUT {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 0e50238..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 VectorConverter : JsonConverter +internal class VectorTConverter : JsonConverter { - public override Vector 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 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS } } - return new Vector { X = x, Y = y, Z = z }; + return new VectorT { X = x, Y = y, Z = z }; } - public override void Write(Utf8JsonWriter writer, Vector 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 value, JsonSerializerOp } } -internal class QAngleConverter : JsonConverter +internal class QAngleTConverter : JsonConverter { - public override QAngle 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 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS } } - return new QAngle { X = X, Y = Y, Z = Z }; + return new QAngleT { X = X, Y = Y, Z = Z }; } - public override void Write(Utf8JsonWriter writer, QAngle 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 value, JsonSerializerOp 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 2b638df..0000000 --- a/src/ST-API/Structures.cs +++ /dev/null @@ -1,87 +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 int? 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"; -} - -internal class API_PlayerSurfProfile -{ - public string name { get; set; } = "N/A"; - public int 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; } -} \ No newline at end of file diff --git a/src/ST-Commands/MapCommands.cs b/src/ST-Commands/MapCommands.cs index 671a15e..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 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 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 3f6bb58..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; @@ -12,174 +13,232 @@ public partial class SurfTimer [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void PlayerReset(CCSPlayerController? player, CommandInfo command) { - if (player == null || player.Team == CsTeam.Spectator || player.Team == CsTeam.None) + if (player == null) return; + 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(); + } + ); + } + Player oPlayer = playerList[player.UserId ?? 0]; if (oPlayer.ReplayRecorder.IsSaving) { - player.PrintToChat($"{Config.PluginPrefix} Please wait for your run to be saved before resetting."); + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["reset_delay"]}"); return; } - // To-do: players[userid].Timer.Reset() -> teleport player + playerList[player.UserId ?? 0].Timer.Reset(); - if (CurrentMap.StartZone != new Vector(0, 0, 0)) - Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StartZone, new QAngle(0, 0, 0), new Vector(0, 0, 0))); + if (!CurrentMap.StartZone.IsZero()) + 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 || player.Team == CsTeam.Spectator || player.Team == CsTeam.None) + if (player == null) return; + 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(); + } + ); + } + Player oPlayer = playerList[player.UserId ?? 0]; if (oPlayer.ReplayRecorder.IsSaving) { - player.PrintToChat($"{Config.PluginPrefix} Please wait for your run to be saved before resetting."); + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["reset_delay"]}"); return; } if (oPlayer.Timer.IsBonusMode) { - if (oPlayer.Timer.Bonus != 0 && CurrentMap.BonusStartZone[oPlayer.Timer.Bonus] != new Vector(0, 0, 0)) - Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.BonusStartZone[oPlayer.Timer.Bonus], CurrentMap.BonusStartZoneAngles[oPlayer.Timer.Bonus], new Vector(0, 0, 0))); + if (oPlayer.Timer.Bonus != 0 && !CurrentMap.BonusStartZone[oPlayer.Timer.Bonus].IsZero()) + Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!, CurrentMap.BonusStartZone[oPlayer.Timer.Bonus])); else // Reset back to map start - Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StartZone, new QAngle(0, 0, 0), new Vector(0, 0, 0))); + Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!, CurrentMap.StartZone)); } - else { - if (oPlayer.Timer.Stage != 0 && CurrentMap.StageStartZone[oPlayer.Timer.Stage] != new Vector(0, 0, 0)) - Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StageStartZone[oPlayer.Timer.Stage], CurrentMap.StageStartZoneAngles[oPlayer.Timer.Stage], new Vector(0, 0, 0))); + 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(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StartZone, new QAngle(0, 0, 0), new Vector(0, 0, 0))); + 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 || player.Team == CsTeam.Spectator || player.Team == CsTeam.None) + if (player == null) return; - int stage; + short stage; try { - stage = Int32.Parse(command.ArgByIndex(1)); + stage = short.Parse(command.ArgByIndex(1)); } catch (System.Exception) { - player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Invalid arguments. Usage: {ChatColors.Green}!s "); - 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} {ChatColors.Red}Invalid arguments. Usage: {ChatColors.Green}!s "); + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_usage", + "!s "]}" + ); return; } - else if (CurrentMap.Stages <= 0) + if (CurrentMap.Stages <= 0) { - player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}This map has no stages."); + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["not_staged"]}"); return; } - else if (stage > CurrentMap.Stages) { - player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Invalid stage provided, this map has {ChatColors.Green}{CurrentMap.Stages} stages."); + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_stage_value", + CurrentMap.Stages]}" + ); return; } - if (CurrentMap.StageStartZone[stage] != new Vector(0, 0, 0)) + if (!CurrentMap.StageStartZone[stage].IsZero()) { 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(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StartZone, CurrentMap.StartZoneAngles, new Vector(0, 0, 0))); + Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!, CurrentMap.StartZone)); } else { playerList[player.UserId ?? 0].Timer.Stage = stage; - Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StageStartZone[stage], CurrentMap.StageStartZoneAngles[stage], new Vector(0, 0, 0))); + Server.NextFrame(() => Extensions.Teleport(player.PlayerPawn.Value!, CurrentMap.StageStartZone[stage])); playerList[player.UserId ?? 0].Timer.IsStageMode = true; } // 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} {ChatColors.Red}Invalid stage provided. Usage: {ChatColors.Green}!s "); + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_usage", + "!s "]}" + ); } [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 || player.Team == CsTeam.Spectator || player.Team == CsTeam.None) + if (player == null) return; int bonus; - // Check for argument count - if (command.ArgCount < 2) + try { - if (CurrentMap.Bonuses > 0) - bonus = 1; - else - { - player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Invalid arguments. Usage: {ChatColors.Green}!bonus "); - 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} {ChatColors.Red}This map has no bonuses."); + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["not_bonused"]}"); return; } - else if (bonus > CurrentMap.Bonuses) { - player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Invalid bonus provided, this map has {ChatColors.Green}{CurrentMap.Bonuses} bonuses."); + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_bonus_value", + CurrentMap.Bonuses]}" + ); return; } - if (CurrentMap.BonusStartZone[bonus] != new Vector(0, 0, 0)) + if (!CurrentMap.BonusStartZone[bonus].IsZero()) { playerList[player.UserId ?? 0].Timer.Reset(); playerList[player.UserId ?? 0].Timer.IsBonusMode = true; - Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.BonusStartZone[bonus], CurrentMap.BonusStartZoneAngles[bonus], new Vector(0, 0, 0))); - } + 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} {ChatColors.Red}Invalid bonus provided. Usage: {ChatColors.Green}!bonus "); + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["invalid_usage", + "!b "]}" + ); } [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")] @@ -189,142 +248,13 @@ public void PlayerRank(CCSPlayerController? player, CommandInfo command) if (player == null) return; - player.PrintToChat($"{Config.PluginPrefix} Your current rank for {ChatColors.Gold}{CurrentMap.Name}{ChatColors.Default} is {ChatColors.Green}{playerList[player.UserId ?? 0].Stats.PB[playerList[player.UserId ?? 0].Timer.Style].Rank}{ChatColors.Default} out of {ChatColors.Yellow}{playerList[player.UserId ?? 0].CurrMap.MapCompletions[playerList[player.UserId ?? 0].Timer.Style]}"); + int pRank = playerList[player.UserId ?? 0].Stats.PB[playerList[player.UserId ?? 0].Timer.Style].Rank; + int tRank = CurrentMap.MapCompletions[playerList[player.UserId ?? 0].Timer.Style]; + player.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["rank", + CurrentMap.Name!, pRank, tRank]}" + ); } - [ConsoleCommand("css_testx", "x")] - [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] - public void TestCmd(CCSPlayerController? player, CommandInfo command) - { - if (player == null) - return; - - 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} 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}{ChatColors.Orange}====== MAP ======"); - player.PrintToChat($"{Config.PluginPrefix} Map ID: {ChatColors.Green}{CurrentMap.ID}"); - player.PrintToChat($"{Config.PluginPrefix} Map Name: {ChatColors.Green}{CurrentMap.Name}"); - 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}{ChatColors.Purple}====== REPLAYS ======"); - player.PrintToChat($"{Config.PluginPrefix} .ReplayRecorder.Frames.Count: {ChatColors.Green}{oPlayer.ReplayRecorder.Frames.Count}"); - player.PrintToChat($"{Config.PluginPrefix} .ReplayRecorder.IsRecording: {ChatColors.Green}{oPlayer.ReplayRecorder.IsRecording}"); - player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.MapWR.RecordRunTime: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.RecordRunTime}"); - player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.MapWR.Frames.Count: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.Frames.Count}"); - player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.MapWR.IsPlayable: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.IsPlayable}"); - player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.MapWR.MapSituations.Count: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.MapSituations.Count}"); - player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.StageWR.RecordRunTime: {ChatColors.Green}{CurrentMap.ReplayManager.StageWR?.RecordRunTime}"); - player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.StageWR.Frames.Count: {ChatColors.Green}{CurrentMap.ReplayManager.StageWR?.Frames.Count}"); - player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.StageWR.IsPlayable: {ChatColors.Green}{CurrentMap.ReplayManager.StageWR?.IsPlayable}"); - player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.BonusWR.RecordRunTime: {ChatColors.Green}{CurrentMap.ReplayManager.BonusWR?.RecordRunTime}"); - 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}"); - // } - } /* ######################### @@ -333,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) @@ -348,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) @@ -371,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) @@ -432,15 +365,21 @@ 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 || !player.PawnIsAlive || !playerList.ContainsKey(player.UserId ?? 0)) + if (player == null) return; + if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) + { + player.ChangeTeam(CsTeam.CounterTerrorist); + player.Respawn(); + } Player p = playerList[player.UserId ?? 0]; if (!p.Timer.IsRunning) { - p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Cannot save location while not in run"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["saveloc_not_in_run"]}"); return; } @@ -450,27 +389,35 @@ public void SavePlayerLocation(CCSPlayerController? player, CommandInfo command) p.SavedLocations.Add(new SavelocFrame { - Pos = new Vector(player_pos.X, player_pos.Y, player_pos.Z), - Ang = new QAngle(player_angle.X, player_angle.Y, player_angle.Z), - Vel = new Vector(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; - p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Green}Saved location! {ChatColors.Default} use !tele {p.SavedLocations.Count - 1} to teleport to this location"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["saveloc_saved", + p.SavedLocations.Count - 1]}" + ); } [ConsoleCommand("css_tele", "Teleport player to current saved location")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void TeleportPlayerLocation(CCSPlayerController? player, CommandInfo command) { - if (player == null || !player.PawnIsAlive || !playerList.ContainsKey(player.UserId ?? 0)) + if (player == null) return; + if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) + { + player.ChangeTeam(CsTeam.CounterTerrorist); + player.Respawn(); + } Player p = playerList[player.UserId ?? 0]; if (p.SavedLocations.Count == 0) { - p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}No saved locations"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["saveloc_no_locations"]}"); return; } @@ -479,11 +426,12 @@ public void TeleportPlayerLocation(CCSPlayerController? player, CommandInfo comm if (!p.Timer.IsPracticeMode) { - p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Timer now on practice"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["saveloc_practice"]}"); p.Timer.IsPracticeMode = true; } if (command.ArgCount > 1) + { try { int tele_n = int.Parse(command.ArgByIndex(1)); @@ -495,33 +443,43 @@ public void TeleportPlayerLocation(CCSPlayerController? player, CommandInfo comm Exception exception = new("sum ting wong"); throw exception; } + } SavelocFrame location = p.SavedLocations[p.CurrentSavedLocation]; Server.NextFrame(() => - { - p.Controller.PlayerPawn.Value!.Teleport(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} Teleported #{p.CurrentSavedLocation}"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["saveloc_teleported", + p.CurrentSavedLocation]}" + ); } [ConsoleCommand("css_teleprev", "Teleport player to previous saved location")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void TeleportPlayerLocationPrev(CCSPlayerController? player, CommandInfo command) { - if (player == null || !player.PawnIsAlive || !playerList.ContainsKey(player.UserId ?? 0)) + if (player == null) return; + if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) + { + player.ChangeTeam(CsTeam.CounterTerrorist); + player.Respawn(); + } Player p = playerList[player.UserId ?? 0]; if (p.SavedLocations.Count == 0) { - p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}No saved locations"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["saveloc_no_locations"]}"); return; } if (p.CurrentSavedLocation == 0) { - p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Already at first location"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["saveloc_first"]}"); } else { @@ -530,26 +488,34 @@ public void TeleportPlayerLocationPrev(CCSPlayerController? player, CommandInfo TeleportPlayerLocation(player, command); - p.Controller.PrintToChat($"{Config.PluginPrefix} Teleported #{p.CurrentSavedLocation}"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["saveloc_teleported", + p.CurrentSavedLocation]}" + ); } [ConsoleCommand("css_telenext", "Teleport player to next saved location")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void TeleportPlayerLocationNext(CCSPlayerController? player, CommandInfo command) { - if (player == null || !player.PawnIsAlive || !playerList.ContainsKey(player.UserId ?? 0)) + if (player == null) return; + if (player.Team == CsTeam.Spectator || player.Team == CsTeam.None) + { + player.ChangeTeam(CsTeam.CounterTerrorist); + player.Respawn(); + } Player p = playerList[player.UserId ?? 0]; if (p.SavedLocations.Count == 0) { - p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}No saved locations"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["saveloc_no_locations"]}"); return; } if (p.CurrentSavedLocation == p.SavedLocations.Count - 1) { - p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Already at last location"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["saveloc_last"]}"); } else { @@ -558,6 +524,128 @@ public void TeleportPlayerLocationNext(CCSPlayerController? player, CommandInfo TeleportPlayerLocation(player, command); - p.Controller.PrintToChat($"{Config.PluginPrefix} Teleported #{p.CurrentSavedLocation}"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {LocalizationService.LocalizerNonNull["saveloc_teleported", + p.CurrentSavedLocation]}" + ); + } + + + + + /* + ######################## + 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]; + + 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) + return; + + Player oPlayer = playerList[player.UserId ?? 0]; + int style = oPlayer.Timer.Style; + + 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].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].RunTime)}"); + + + player.PrintToChat($"{Config.PluginPrefix}{ChatColors.Orange}====== MAP ======"); + player.PrintToChat($"{Config.PluginPrefix} Map ID: {ChatColors.Green}{CurrentMap.ID}"); + player.PrintToChat($"{Config.PluginPrefix} Map Name: {ChatColors.Green}{CurrentMap.Name}"); + 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[{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 ======"); + player.PrintToChat($"{Config.PluginPrefix} .ReplayRecorder.Frames.Count: {ChatColors.Green}{oPlayer.ReplayRecorder.Frames.Count}"); + player.PrintToChat($"{Config.PluginPrefix} .ReplayRecorder.IsRecording: {ChatColors.Green}{oPlayer.ReplayRecorder.IsRecording}"); + player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.MapWR.RecordRunTime: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.RecordRunTime}"); + player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.MapWR.Frames.Count: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.Frames.Count}"); + player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.MapWR.IsPlayable: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.IsPlayable}"); + player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.MapWR.MapSituations.Count: {ChatColors.Green}{CurrentMap.ReplayManager.MapWR.MapSituations.Count}"); + player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.StageWR.RecordRunTime: {ChatColors.Green}{CurrentMap.ReplayManager.StageWR?.RecordRunTime}"); + player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.StageWR.Frames.Count: {ChatColors.Green}{CurrentMap.ReplayManager.StageWR?.Frames.Count}"); + player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.StageWR.IsPlayable: {ChatColors.Green}{CurrentMap.ReplayManager.StageWR?.IsPlayable}"); + player.PrintToChat($"{Config.PluginPrefix} .ReplayManager.BonusWR.RecordRunTime: {ChatColors.Green}{CurrentMap.ReplayManager.BonusWR?.RecordRunTime}"); + 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}"); + } + + [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 1d6eea7..0000000 --- a/src/ST-DB/DB.cs +++ /dev/null @@ -1,123 +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 - public async Task WriteAsync(string query) - { - try - { - using var connection = GetConnection(); - using var cmd = new MySqlCommand(query, connection); - return await cmd.ExecuteNonQueryAsync(); - } - 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 283ce2f..ead08c4 100644 --- a/src/ST-Events/Players.cs +++ b/src/ST-Events/Players.cs @@ -2,8 +2,8 @@ using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Utils; -using MySqlConnector; using MaxMind.GeoIP2; +using Microsoft.Extensions.Logging; namespace SurfTimer; @@ -13,15 +13,21 @@ 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.", + controller.PlayerName + ); + // Set the controller for the MapWR bot - if (!CurrentMap.ReplayManager!.MapWR.IsPlayable) + if (!CurrentMap.ReplayManager!.MapWR.IsPlayable && controller.IsBot) { CurrentMap.ReplayManager.MapWR.SetController(controller, -1); CurrentMap.ReplayManager.MapWR.LoadReplayData(); + controller.SwitchTeam(CsTeam.Terrorist); + AddTimer(1.5f, () => { CurrentMap.ReplayManager.MapWR.Controller!.RemoveWeapons(); @@ -33,11 +39,13 @@ public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) } // Set the controller for the StageWR bot - if (CurrentMap.ReplayManager.StageWR != null && !CurrentMap.ReplayManager.StageWR.IsPlayable) + if (CurrentMap.ReplayManager.StageWR != null && !CurrentMap.ReplayManager.StageWR.IsPlayable && controller.IsBot) { 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(); @@ -49,11 +57,13 @@ public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) } // Spawn the BonusWR bot - if (CurrentMap.ReplayManager.BonusWR != null && !CurrentMap.ReplayManager.BonusWR.IsPlayable) + if (CurrentMap.ReplayManager.BonusWR != null && !CurrentMap.ReplayManager.BonusWR.IsPlayable && controller.IsBot) { CurrentMap.ReplayManager.BonusWR.SetController(controller, 3); CurrentMap.ReplayManager.BonusWR.LoadReplayData(); + controller.SwitchTeam(CsTeam.Terrorist); + AddTimer(1.5f, () => { CurrentMap.ReplayManager.BonusWR.Controller!.RemoveWeapons(); @@ -90,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"; @@ -106,28 +116,35 @@ 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."); Exception ex = new("CS2 Surf ERROR >> OnPlayerConnect -> DB object is null, this shouldn't happen."); 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); + + var p = new Player(player, movement, profile); - // 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 + // No lock - we use thread-safe method AddOrUpdate + playerList.AddOrUpdate(player.UserId ?? 0, p, (_, _) => p); - // Print join messages - Server.PrintToChatAll($"{Config.PluginPrefix} {ChatColors.Green}{name}{ChatColors.Default} has connected from {ChatColors.Lime}{playerList[player.UserId ?? 0].Profile.Country}{ChatColors.Default}."); - Console.WriteLine($"[CS2 Surf] {name} has connected from {playerList[player.UserId ?? 0].Profile.Country}."); + _ = 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; } @@ -138,7 +155,9 @@ public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo if (player == null) { - Console.WriteLine($"CS2 Surf ERROR >> OnPlayerDisconnect -> Null ({player == null})"); + _logger.LogError("OnPlayerDisconnect -> 'player' is NULL ({IsNull})", + player == null + ); return HookResult.Continue; } @@ -163,47 +182,30 @@ public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo else { if (DB == null) - throw new Exception("CS2 Surf ERROR >> OnPlayerDisconnect -> DB object is null, this shouldnt happen."); + { + _logger.LogCritical("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)) { - Console.WriteLine($"CS2 Surf ERROR >> OnPlayerDisconnect -> Player playerList does NOT contain player.UserId, this shouldn't happen. Player: {player.PlayerName} ({player.UserId})"); + _logger.LogError("OnPlayerDisconnect -> playerList does NOT contain player.UserId, this shouldn't happen. Player: {PlayerName} ({UserId})", + player.PlayerName, player.UserId + ); } else { - // Update data in Player DB table - playerList[player.UserId ?? 0].Profile.Update_Player_Profile(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 2d59bb0..05b0d5d 100644 --- a/src/ST-Events/Tick.cs +++ b/src/ST-Events/Tick.cs @@ -1,4 +1,5 @@ using CounterStrikeSharp.API.Modules.Cvars; +using CounterStrikeSharp.API.Modules.Entities.Constants; namespace SurfTimer; @@ -6,19 +7,22 @@ public partial class SurfTimer { public void OnTick() { + if (CurrentMap == null) + return; + foreach (var player in playerList.Values) { player.Timer.Tick(); player.ReplayRecorder.Tick(player); player.HUD.Display(); + if (player.Controller.Collision == null) continue; + if ((CollisionGroup)player.Controller.Collision.CollisionGroup == CollisionGroup.COLLISION_GROUP_DEBRIS) continue; + 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(); @@ -32,14 +36,17 @@ public void OnTick() { bot_quota.SetValue(replaybot_count); } - - // Console.WriteLine($"======== public void OnTick -> Got bot_quota {cbq} | Setting to bot_quota {replaybot_count}"); } CurrentMap.ReplayManager.MapWR.Tick(); CurrentMap.ReplayManager.StageWR?.Tick(); CurrentMap.ReplayManager.BonusWR?.Tick(); + if (CurrentMap.ReplayManager.MapWR.MapTimeID != -1) + { + CurrentMap.ReplayManager.MapWR.FormatBotName(); + } + // Here we will load the NEXT stage replay from AllStageWR if (CurrentMap.ReplayManager.StageWR?.RepeatCount == 0) { @@ -51,7 +58,6 @@ public void OnTick() CurrentMap.ReplayManager.AllStageWR[next_stage][0].Controller = CurrentMap.ReplayManager.StageWR.Controller; - // Console.WriteLine($"======== public void OnTick() -> Finished replay cycle for stage {CurrentMap.ReplayManager.StageWR.Stage}, changing to stage {next_stage}"); CurrentMap.ReplayManager.StageWR = CurrentMap.ReplayManager.AllStageWR[next_stage][0]; CurrentMap.ReplayManager.StageWR.LoadReplayData(repeat_count: 3); CurrentMap.ReplayManager.StageWR.FormatBotName(); @@ -68,7 +74,6 @@ public void OnTick() CurrentMap.ReplayManager.AllBonusWR[next_bonus][0].Controller = CurrentMap.ReplayManager.BonusWR.Controller; - // Console.WriteLine($"======== public void OnTick() -> Finished replay cycle for bonus {CurrentMap.ReplayManager.BonusWR.Stage}, changing to bonus {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 e685bd7..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,155 +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 - float velocity_x = player.Controller.PlayerPawn.Value!.AbsVelocity.X; - float velocity_y = player.Controller.PlayerPawn.Value!.AbsVelocity.Y; - float velocity_z = player.Controller.PlayerPawn.Value!.AbsVelocity.Z; - float velocity = (float)Math.Sqrt(velocity_x * velocity_x + velocity_y * velocity_y + velocity_z + velocity_z); + 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); - // Console.WriteLine($"START_ZONE_EXIT: player.ReplayRecorder.MapSituations.Add({player.ReplayRecorder.Frames.Count})"); - } - - // Prespeed display - player.Controller.PrintToCenter($"Prespeed: {velocity.ToString("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.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.ToString("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.ToString("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.ToString("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 d73d394..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,577 +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 - float velocity_x = player.Controller.PlayerPawn.Value!.AbsVelocity.X; - float velocity_y = player.Controller.PlayerPawn.Value!.AbsVelocity.Y; - float velocity_z = player.Controller.PlayerPawn.Value!.AbsVelocity.Z; - 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}{ChatColors.Lime}{player.Controller.PlayerName}{ChatColors.Default} has set a new {ChatColors.Yellow}Map{ChatColors.Default} record with a time of {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default}, beating the old record by {ChatColors.Green}-{PlayerHUD.FormatTime(timeImprove)}{ChatColors.Default}! (Previous: {ChatColors.BlueGrey}{PlayerHUD.FormatTime(CurrentMap.WR[pStyle].Ticks)}{ChatColors.Default})"); - } - else if (CurrentMap.WR[pStyle].ID == -1) // No record was set on the map - { - saveMapTime = true; - Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{ChatColors.Lime}{player.Controller.PlayerName}{ChatColors.Default} set the first {ChatColors.Yellow}Map{ChatColors.Default} record at {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default}!"); - } - else if (player.Stats.PB[pStyle].Ticks <= 0) // Player first ever PersonalBest for the map - { - saveMapTime = true; - player.Controller.PrintToChat($"{Config.PluginPrefix} {PracticeString}You finished the {ChatColors.Yellow}Map{ChatColors.Default} in {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default}, setting your new Personal Best!"); - } - 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}{ChatColors.Lime}{player.Controller.PlayerName}{ChatColors.Default} beat their {ChatColors.Yellow}Map{ChatColors.Default} Personal Best with a time of {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default}, improving by {ChatColors.Green}-{PlayerHUD.FormatTime(timeImprove)}{ChatColors.Default}! (Previous: {ChatColors.BlueGrey}{PlayerHUD.FormatTime(player.Stats.PB[pStyle].Ticks)}{ChatColors.Default})"); - } - else // Player did not beat their existing PersonalBest for the map nor the map record - { - player.Controller.PrintToChat($"{Config.PluginPrefix} {PracticeString}You finished the map in {ChatColors.Yellow}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default}!"); - } - - 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); - 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} {ChatColors.Yellow}Please try restarting again, replay is still being saved."); - } - } - - // 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 - - // 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) - { - 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 - } + ZoneType currentZone = GetZoneType(trigger.Entity.Name); - // 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) + switch (currentZone) { - // 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}{ChatColors.Lime}{player.Controller.PlayerName}{ChatColors.Default} has set a new {ChatColors.Yellow}Bonus {bonus_idx}{ChatColors.Default} record with a time of {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default}, beating the old record by {ChatColors.Green}-{PlayerHUD.FormatTime(timeImprove)}{ChatColors.Default}! (Previous: {ChatColors.BlueGrey}{PlayerHUD.FormatTime(CurrentMap.BonusWR[bonus_idx][pStyle].Ticks)}{ChatColors.Default})"); - } - else if (CurrentMap.BonusWR[bonus_idx][pStyle].ID == -1) // No Bonus record was set on the map - { - saveBonusTime = true; - Server.PrintToChatAll($"{Config.PluginPrefix} {PracticeString}{ChatColors.Lime}{player.Controller.PlayerName}{ChatColors.Default} set the first {ChatColors.Yellow}Bonus {bonus_idx}{ChatColors.Default} record at {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default}!"); - } - 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}You finished the {ChatColors.Yellow}Bonus {bonus_idx}{ChatColors.Default} in {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default}, setting your new Personal Best!"); - } - 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}{ChatColors.Lime}{player.Controller.PlayerName}{ChatColors.Default} beat their {ChatColors.Yellow}Bonus {bonus_idx}{ChatColors.Default} Personal Best with a time of {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default}, improving by {ChatColors.Green}-{PlayerHUD.FormatTime(timeImprove)}{ChatColors.Default}! (Previous: {ChatColors.BlueGrey}{PlayerHUD.FormatTime(player.Stats.PB[pStyle].Ticks)}{ChatColors.Default})"); - } - else // Player did not beat their existing personal best for the bonus - { - player.Controller.PrintToChat($"{Config.PluginPrefix} {PracticeString}You finished {ChatColors.Yellow}Bonus {bonus_idx}{ChatColors.Default} in {ChatColors.Yellow}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default}!"); - } - - // 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 - }); - } - } + // 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 + ); + 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 - ) - { - 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} {ChatColors.Lime}{player.Controller.PlayerName}{ChatColors.Default} has set a new {ChatColors.Yellow}Stage {stage}{ChatColors.Default} record with a time of {ChatColors.Gold}{PlayerHUD.FormatTime(stage_run_time)}{ChatColors.Default}, beating the old record by {ChatColors.Green}-{PlayerHUD.FormatTime(timeImprove)}{ChatColors.Default}! (Previous: {ChatColors.BlueGrey}{PlayerHUD.FormatTime(CurrentMap.StageWR[stage][pStyle].Ticks)}{ChatColors.Default})"); - } - else if (CurrentMap.StageWR[stage][pStyle].ID == -1) // No Stage record was set on the map - { - Server.PrintToChatAll($"{Config.PluginPrefix} {ChatColors.Lime}{player.Controller.PlayerName}{ChatColors.Default} set the first {ChatColors.Yellow}Stage {stage}{ChatColors.Default} record at {ChatColors.Gold}{PlayerHUD.FormatTime(stage_run_time)}{ChatColors.Default}!"); - } - 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} You finished {ChatColors.Yellow}Stage {stage}{ChatColors.Default} in {ChatColors.Gold}{PlayerHUD.FormatTime(stage_run_time)}{ChatColors.Default}, setting your new Personal Best!"); - } - 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} {ChatColors.Lime}{player.Controller.PlayerName}{ChatColors.Default} beat their {ChatColors.Yellow}Stage {stage}{ChatColors.Default} Personal Best with a time of {ChatColors.Gold}{PlayerHUD.FormatTime(stage_run_time)}{ChatColors.Default}, improving by {ChatColors.Green}-{PlayerHUD.FormatTime(timeImprove)}{ChatColors.Default}! (Previous: {ChatColors.BlueGrey}{PlayerHUD.FormatTime(player.Stats.StagePB[stage][pStyle].Ticks)}{ChatColors.Default})"); - } - - 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} You are behind the {ChatColors.Yellow}Stage {stage}{ChatColors.Default} record with a time of {ChatColors.Grey}{PlayerHUD.FormatTime(stage_run_time)}{ChatColors.Default}, missing the record by {ChatColors.Red}+{PlayerHUD.FormatTime(timeImprove)}{ChatColors.Default} ({ChatColors.Gold}{PlayerHUD.FormatTime(CurrentMap.StageWR[stage][pStyle].Ticks)}{ChatColors.Default})!"); - } - } } 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 6758c23..31f5f7f 100644 --- a/src/ST-Map/Map.cs +++ b/src/ST-Map/Map.cs @@ -1,26 +1,23 @@ -using System.Data; -using System.Text.Json; -using System.Text.RegularExpressions; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Utils; -using MySqlConnector; +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] /// @@ -28,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] /// @@ -40,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. @@ -56,68 +53,83 @@ internal class Map // Zone Origin Information /* Map Start/End zones */ - public Vector StartZone { get; set; } = new Vector(0, 0, 0); - public QAngle StartZoneAngles { get; set; } = new QAngle(0, 0, 0); - public Vector EndZone { get; set; } = new Vector(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[] StageStartZone { get; } = Enumerable.Repeat(0, 99).Select(x => new Vector(0, 0, 0)).ToArray(); - public QAngle[] StageStartZoneAngles { get; } = Enumerable.Repeat(0, 99).Select(x => new QAngle(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[] BonusStartZone { get; } = Enumerable.Repeat(0, 99).Select(x => new Vector(0, 0, 0)).ToArray(); // To-do: Implement bonuses - public QAngle[] BonusStartZoneAngles { get; } = Enumerable.Repeat(0, 99).Select(x => new QAngle(0, 0, 0)).ToArray(); // To-do: Implement bonuses - public Vector[] BonusEndZone { get; } = Enumerable.Repeat(0, 99).Select(x => new Vector(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[] CheckpointStartZone { get; } = Enumerable.Repeat(0, 99).Select(x => new Vector(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!; + private readonly ILogger _logger; + private readonly IDataAccessService _dataService; + // Constructor internal Map(string name) { + // Resolve the logger instance from the DI container + _logger = SurfTimer.ServiceProvider.GetRequiredService>(); + _dataService = SurfTimer.ServiceProvider.GetRequiredService(); + // Set map name this.Name = name; - // Initialize WR variables - foreach (int style in Config.Styles) - { - this.WR[style] = new PersonalBest(); - this.MapCompletions[style] = -1; - } - - 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(); - } + // Load zones + MapLoadZones(); + _logger.LogInformation("[{ClassName}] -> Zones have been loaded. | Bonuses: {Bonuses} | Stages: {Stages} | Checkpoints: {Checkpoints}", + nameof(Map), this.Bonuses, this.Stages, this.TotalCheckpoints + ); } - public static async Task CreateAsync(string name) + internal async Task InitializeAsync([CallerMemberName] string methodName = "") { - var map = new Map(name); - await map.InitializeAsync(); - return map; - } + // Initialize ReplayManager with placeholder values + this.ReplayManager = new ReplayManager(-1, this.Stages > 0, this.Bonuses > 0, null!); - private async Task InitializeAsync() - { - // Load zones - Map_Load_Zones(); - Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> InitializeAsync -> Zones have been loaded."); + // 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; - // 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 + foreach (int style in Config.Styles) + { + this.WR[style] = new PersonalBest { Type = 0 }; + this.MapCompletions[style] = 0; - await Get_Map_Info(); + 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++; + } + + 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++; + } + } - Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> InitializeAsync -> We got MapID = {ID} ({Name})"); + _logger.LogInformation("[{ClassName}] {MethodName} -> Initialized WR variables. | Bonuses: {Bonuses} | Stages: {Stages}", + nameof(Map), methodName, initBonuses, initStages + ); + + await LoadMapInfo(); } /// @@ -126,7 +138,7 @@ private async Task InitializeAsync() // 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() + internal void MapLoadZones([CallerMemberName] string methodName = "") { // Gathering zones from the map IEnumerable triggers = Utilities.FindAllEntitiesByDesignerName("trigger_multiple"); @@ -150,8 +162,8 @@ internal void Map_Load_Zones() teleport.Entity!.Name.Contains("spawn_stage1_start") || teleport.Entity!.Name.Contains("spawn_s1_start"))) { - this.StartZone = new Vector(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z); - this.StartZoneAngles = new QAngle(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; } @@ -159,14 +171,14 @@ internal void Map_Load_Zones() if (!foundPlayerSpawn) { - this.StartZone = new Vector(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(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 @@ -181,8 +193,8 @@ internal void Map_Load_Zones() 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(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z); - this.StageStartZoneAngles[stage] = new QAngle(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; @@ -191,7 +203,7 @@ internal void Map_Load_Zones() if (!foundPlayerSpawn) { - this.StageStartZone[stage] = new Vector(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++; } } @@ -199,7 +211,7 @@ internal void Map_Load_Zones() // 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(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 } @@ -215,8 +227,8 @@ internal void Map_Load_Zones() 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(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z); - this.BonusStartZoneAngles[bonus] = new QAngle(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; @@ -225,364 +237,233 @@ internal void Map_Load_Zones() if (!foundPlayerSpawn) { - this.BonusStartZone[bonus] = new Vector(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(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; - Console.WriteLine($"[CS2 Surf] Identifying start zone: {this.StartZone.X},{this.StartZone.Y},{this.StartZone.Z}\nIdentifying end zone: {this.EndZone.X},{this.EndZone.Y},{this.EndZone.Z}"); + } + + _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 + ); + + KillServerCommandEnts(); } /// /// Inserts a new map entry in the database. - /// Automatically detects whether to use API Calls or MySQL query. /// - internal async Task Insert_Map_Info() + internal async Task InsertMapInfo([CallerMemberName] string methodName = "") { - if (Config.API.GetApiOnly()) // API Calls + var mapInfo = new MapDto { - API_MapInfo inserted = new() - { - id = -1, // Shouldn't really use this at all at api side - name = Name, - author = "Unknown", - tier = this.Tier, - stages = this.Stages, - bonuses = this.Bonuses, - ranked = 0, - }; - - var postResponse = await ApiMethod.POST(Config.API.Endpoints.ENDPOINT_MAP_INSERT_INFO, inserted); - - // Check if the response is not null and get the last_id - if (postResponse != null) - { - Console.WriteLine($"======= CS2 Surf DEBUG API >> public async Task Insert_Map_Info -> New map '{Name}' inserted, got ID {postResponse.last_id}"); - this.ID = postResponse.last_id; - } + Name = this.Name!, + Author = "Unknown", // Or set appropriately + Tier = this.Tier, + Stages = this.Stages, + Bonuses = this.Bonuses, + Ranked = false + }; + + try + { + this.ID = await _dataService.InsertMapInfoAsync(mapInfo); - return; + _logger.LogInformation("[{ClassName}] {MethodName} -> Map '{Map}' inserted successfully with ID {ID}.", + nameof(Map), methodName, this.Name, this.ID + ); } - else // MySQL Queries + catch (Exception ex) { - int writerRows = await SurfTimer.DB.WriteAsync( - string.Format(Config.MySQL.Queries.DB_QUERY_MAP_INSERT_INFO, MySqlHelper.EscapeString(Name), "Unknown", this.Stages, this.Bonuses, 0, (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds())); - if (writerRows != 1) - { - Exception exception = new($"CS2 Surf ERROR >> internal class Map -> internal async Task Insert_Map_Info -> Failed to write new map to database, this shouldn't happen. Map: {Name}"); - throw exception; - } - - await Get_Map_Info(false); + _logger.LogCritical(ex, "[{ClassName}] {MethodName} -> Failed to insert map '{Map}'. Exception: {ExceptionMessage}", + nameof(Map), methodName, this.Name, ex.Message + ); + 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 Update_Map_Info() + internal async Task UpdateMapInfo([CallerMemberName] string methodName = "") { - if (Config.API.GetApiOnly()) // API Calls + var mapInfo = new MapDto { - API_MapInfo updated = new() - { - id = this.ID, - name = Name, - author = "Unknown", - tier = this.Tier, - stages = this.Stages, - bonuses = this.Bonuses, - ranked = 0, - last_played = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds() - }; - - _ = ApiMethod.PUT(Config.API.Endpoints.ENDPOINT_MAP_UPDATE_INFO, updated).Result; - } - else // MySQL Queries + Name = this.Name!, + Author = this.Author!, + Tier = this.Tier, + Stages = this.Stages, + Bonuses = this.Bonuses, + Ranked = this.Ranked, + LastPlayed = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds() + }; + + try { - // Update the map's last played data in the DB - string updateQuery = string.Format(Config.MySQL.Queries.DB_QUERY_MAP_UPDATE_INFO_FULL, - (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), this.Stages, this.Bonuses, this.ID); + await _dataService.UpdateMapInfoAsync(mapInfo, this.ID); #if DEBUG - Console.WriteLine($"CS2 Surf >> internal class Map -> internal async Task Update_Map_Info -> Update MapData: {updateQuery}"); + _logger.LogDebug("[{ClassName}] {MethodName} -> Updated map '{Map}' (ID: {ID}).", + nameof(Map), methodName, this.Name, this.ID + ); #endif - - int lastPlayedUpdateRows = await SurfTimer.DB.WriteAsync(updateQuery); - if (lastPlayedUpdateRows != 1) - { - Exception exception = new($"CS2 Surf ERROR >> internal class Map -> internal async Task Update_Map_Info -> Failed to update map in database, this shouldn't happen. Map: {Name}"); - throw exception; - } + } + catch (Exception ex) + { + _logger.LogCritical(ex, "[{ClassName}] {MethodName} -> Failed to update map '{Map}'. Exception Message: {ExceptionMessage}", + nameof(Map), methodName, this.Name, ex.Message + ); + 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 - internal async Task Get_Map_Info(bool updateData = true) + /// Should we run UPDATE query for the map + internal async Task LoadMapInfo(bool updateData = true, [CallerMemberName] string methodName = "") { bool newMap = false; - if (Config.API.GetApiOnly()) // API Calls + var mapInfo = await _dataService.GetMapInfoAsync(this.Name!); + + if (mapInfo != null) { - // Gather map information OR create entry - var mapinfo = await ApiMethod.GET(string.Format(Config.API.Endpoints.ENDPOINT_MAP_GET_INFO, Name)); - if (mapinfo != null) - { - this.ID = mapinfo.id; - this.Author = mapinfo.author; - this.Tier = mapinfo.tier; - this.Ranked = mapinfo.ranked == 1; - this.DateAdded = (int)mapinfo.date_added!; - this.LastPlayed = (int)mapinfo.last_played!; - } - else - { - newMap = true; - } + ID = mapInfo.ID; + Author = mapInfo.Author; + Tier = mapInfo.Tier; + Ranked = mapInfo.Ranked; + DateAdded = mapInfo.DateAdded; + LastPlayed = mapInfo.LastPlayed; } - else // MySQL queries + else { - // Gather map information OR create entry - using (var mapData = await SurfTimer.DB.QueryAsync( - string.Format(Config.MySQL.Queries.DB_QUERY_MAP_GET_INFO, MySqlHelper.EscapeString(Name)))) - { - if (mapData.HasRows && mapData.Read()) // In here we can check whether MapData in DB is the same as the newly extracted data, if not, update it (as hookzones may have changed on map updates) - { - this.ID = mapData.GetInt32("id"); - this.Author = mapData.GetString("author") ?? "Unknown"; - this.Tier = mapData.GetInt32("tier"); - this.Ranked = mapData.GetBoolean("ranked"); - this.DateAdded = mapData.GetInt32("date_added"); - this.LastPlayed = mapData.GetInt32("last_played"); - } - else - { - newMap = true; - } - } + newMap = true; } - // This is a new map if (newMap) { - await Insert_Map_Info(); + await InsertMapInfo(); return; } - // this.ReplayManager = new ReplayManager(this.ID, this.Stages > 0, this.Bonuses > 0); - - // Will skip updating the data in the case where we have just inserted a new map with MySQL Queries and called this method again in order to get the Map ID if (updateData) - await Update_Map_Info(); + await UpdateMapInfo(); - await Get_Map_Record_Runs(); + var stopwatch = Stopwatch.StartNew(); + await LoadMapRecordRuns(); + stopwatch.Stop(); + +#if DEBUG + _logger.LogDebug("[{ClassName}] {MethodName} -> Finished LoadMapRecordRuns in {Elapsed}ms | API = {API}", + nameof(Map), methodName, stopwatch.ElapsedMilliseconds, Config.API.GetApiOnly()); +#endif } /// /// 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 Get_Map_Record_Runs() + internal async Task LoadMapRecordRuns([CallerMemberName] string methodName = "") { - int totalMapRuns = 0; - int totalStageRuns = 0; - int totalBonusRuns = 0; - this.ConnectedMapTimes.Clear(); + //this.ConnectedMapTimes.Clear(); // This is for Custom Replays (PB replays?) - T - int qType; - int qStage; - int qStyle; + var runs = await _dataService.GetMapRecordRunsAsync(this.ID); - // Replay Stuff - JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = false, Converters = { new VectorConverter(), new QAngleConverter() } }; + _logger.LogInformation("[{ClassName}] {MethodName} -> Received {Length} runs from `GetMapRecordRunsAsync`", + nameof(Map), methodName, runs.Count + ); - if (Config.API.GetApiOnly()) // Need to update the query in API and re-do the assigning of data - { - // // var maptimes = await ApiMethod.GET(string.Format(Config.API.Endpoints.ENDPOINT_MAP_GET_RUNS, this.ID, style, type)); - // var maptimes = await ApiMethod.GET(string.Format(Config.API.Endpoints.ENDPOINT_MAP_GET_RUNS, this.ID, 0, 0)); - // if (maptimes == null) - // { - // Console.WriteLine($"======= CS2 Surf DEBUG API >> public async Task Get_Map_Record_Runs -> No map runs found for {this.Name} (MapID {this.ID} | Style {qStyle} | Type {qStyle})"); - // this.MapCompletions[qStyle] = 0; - // return; - // } - - // Console.WriteLine($"======= CS2 Surf DEBUG API >> public async Task Get_Map_Record_Runs -> Got {maptimes.Length} map runs for MapID {this.ID} (Style {qStyle} | Type {qStyle})"); - // // To-do: Implement bonuses WR - // // To-do: Implement stages WR - // foreach (var time in maptimes) - // { - // if (totalMapRuns == 0) // First row is always the fastest run for the map, style, type combo - // { - // this.WR[qStyle].ID = time.id; // WR ID for the Map and Style combo - // this.WR[qStyle].Ticks = time.run_time; // Fastest run time (WR) for the Map and Style combo - // this.WR[qStyle].StartVelX = time.start_vel_x; // Fastest run start velocity X for the Map and Style combo - // this.WR[qStyle].StartVelY = time.start_vel_y; // Fastest run start velocity Y for the Map and Style combo - // this.WR[qStyle].StartVelZ = time.start_vel_z; // Fastest run start velocity Z for the Map and Style combo - // this.WR[qStyle].EndVelX = time.end_vel_x; // Fastest run end velocity X for the Map and Style combo - // this.WR[qStyle].EndVelY = time.end_vel_y; // Fastest run end velocity Y for the Map and Style combo - // this.WR[qStyle].EndVelZ = time.end_vel_z; // Fastest run end velocity Z for the Map and Style combo - // this.WR[qStyle].RunDate = time.run_date; // Fastest run date for the Map and Style combo - // this.WR[qStyle].Name = time.name; // Fastest run player name for the Map and Style combo - // } - // this.ConnectedMapTimes.Add(time.id); - // totalMapRuns++; - // } - // // this.ConnectedMapTimes.Remove(this.WR[style].ID); // ?? - // // this.MapCompletions[style] = maptimes.Length; - } - else // MySQL Queries + foreach (var run in runs) { - // Get map world record - using (var mapWrData = await SurfTimer.DB.QueryAsync( - string.Format(Config.MySQL.Queries.DB_QUERY_MAP_GET_RECORD_RUNS_AND_COUNT, this.ID))) + switch (run.Type) { - if (mapWrData.HasRows) - { - while (mapWrData.Read()) - { - qType = mapWrData.GetInt32("type"); - qStage = mapWrData.GetInt32("stage"); - qStyle = mapWrData.GetInt32("style"); - - // Retrieve replay_frames as string from MySQL - string replayFramesBase64; - - // Option A: Try to get the string directly - try - { - replayFramesBase64 = mapWrData.GetString("replay_frames"); - } - catch (InvalidCastException) - { - // Option B: Get the data as byte[] and convert to string - byte[] replayFramesData = mapWrData.GetFieldValue("replay_frames"); - replayFramesBase64 = System.Text.Encoding.UTF8.GetString(replayFramesData); - } + case 0: // Map WR data and total completions + WR[run.Style].ID = run.ID; + WR[run.Style].RunTime = run.RunTime; + WR[run.Style].StartVelX = run.StartVelX; + WR[run.Style].StartVelY = run.StartVelY; + WR[run.Style].StartVelZ = run.StartVelZ; + WR[run.Style].EndVelX = run.EndVelX; + WR[run.Style].EndVelY = run.EndVelY; + WR[run.Style].EndVelZ = run.EndVelZ; + WR[run.Style].RunDate = run.RunDate; + WR[run.Style].Name = run.Name; + /// ConnectedMapTimes.Add(run.ID); + MapCompletions[run.Style] = run.TotalCount; + + SetReplayData(run.Type, run.Style, run.Stage, run.ReplayFrames!); + break; - // Populate parameters for all the MapTime rows found - switch (qType) - { - case 0: // Map WR data and total completions - this.WR[qStyle].ID = mapWrData.GetInt32("id"); - this.WR[qStyle].Ticks = mapWrData.GetInt32("run_time"); - this.WR[qStyle].StartVelX = mapWrData.GetFloat("start_vel_x"); - this.WR[qStyle].StartVelY = mapWrData.GetFloat("start_vel_y"); - this.WR[qStyle].StartVelZ = mapWrData.GetFloat("start_vel_z"); - this.WR[qStyle].EndVelX = mapWrData.GetFloat("end_vel_x"); - this.WR[qStyle].EndVelY = mapWrData.GetFloat("end_vel_y"); - this.WR[qStyle].EndVelZ = mapWrData.GetFloat("end_vel_z"); - this.WR[qStyle].RunDate = mapWrData.GetInt32("run_date"); - this.WR[qStyle].Name = mapWrData.GetString("name"); - totalMapRuns = mapWrData.GetInt32("total_count"); - this.ConnectedMapTimes.Add(mapWrData.GetInt32("id")); - this.MapCompletions[qStyle] = totalMapRuns; - - // Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal async Task Get_Map_Record_Runs -> [MapWR] Sending style {qStyle} to `ReplayManager`: Map ID {this.ID} | Stages {this.Stages > 0} | Bonuses {this.Bonuses > 0} | Run Time {this.WR[qStyle].Ticks} | Name {this.WR[qStyle].Name} | MapTime ID {this.WR[qStyle].ID}"); - - // Populate the ReplayManager for Map WR only if no replay exists or a new WR was set - if (this.ReplayManager.MapWR.MapID == -1 || this.WR[qStyle].Ticks < this.ReplayManager.MapWR.RecordRunTime) - { - Set_Replay_Data(qType, qStyle, qStage, replayFramesBase64); - } - break; - case 1: // Bonus WR data and total completions - this.BonusWR[qStage][qStyle].ID = mapWrData.GetInt32("id"); - this.BonusWR[qStage][qStyle].Ticks = mapWrData.GetInt32("run_time"); - this.BonusWR[qStage][qStyle].StartVelX = mapWrData.GetFloat("start_vel_x"); - this.BonusWR[qStage][qStyle].StartVelY = mapWrData.GetFloat("start_vel_y"); - this.BonusWR[qStage][qStyle].StartVelZ = mapWrData.GetFloat("start_vel_z"); - this.BonusWR[qStage][qStyle].EndVelX = mapWrData.GetFloat("end_vel_x"); - this.BonusWR[qStage][qStyle].EndVelY = mapWrData.GetFloat("end_vel_y"); - this.BonusWR[qStage][qStyle].EndVelZ = mapWrData.GetFloat("end_vel_z"); - this.BonusWR[qStage][qStyle].RunDate = mapWrData.GetInt32("run_date"); - this.BonusWR[qStage][qStyle].Name = mapWrData.GetString("name"); - totalBonusRuns = mapWrData.GetInt32("total_count"); - this.BonusCompletions[qStage][qStyle] = totalBonusRuns; - - // Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal async Task Get_Map_Record_Runs -> Got Bonus {qStage}"); - - // Populate the ReplayManager for all bonuses found and set the first bonus to replay - if (this.ReplayManager.BonusWR != null) - { - Set_Replay_Data(qType, qStyle, qStage, replayFramesBase64); - } - break; - case 2: // Stage WR data and total completions - this.StageWR[qStage][qStyle].ID = mapWrData.GetInt32("id"); - this.StageWR[qStage][qStyle].Ticks = mapWrData.GetInt32("run_time"); - this.StageWR[qStage][qStyle].StartVelX = mapWrData.GetFloat("start_vel_x"); - this.StageWR[qStage][qStyle].StartVelY = mapWrData.GetFloat("start_vel_y"); - this.StageWR[qStage][qStyle].StartVelZ = mapWrData.GetFloat("start_vel_z"); - this.StageWR[qStage][qStyle].EndVelX = mapWrData.GetFloat("end_vel_x"); - this.StageWR[qStage][qStyle].EndVelY = mapWrData.GetFloat("end_vel_y"); - this.StageWR[qStage][qStyle].EndVelZ = mapWrData.GetFloat("end_vel_z"); - this.StageWR[qStage][qStyle].RunDate = mapWrData.GetInt32("run_date"); - this.StageWR[qStage][qStyle].Name = mapWrData.GetString("name"); - totalStageRuns = mapWrData.GetInt32("total_count"); - this.StageCompletions[qStage][qStyle] = totalStageRuns; - - // Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal async Task Get_Map_Record_Runs -> [StageWR] Sending style {qStyle} to `ReplayManager.StageWR`: Map ID {this.ID} | Stages {this.Stages > 0} - {qStage} | Bonuses {this.Bonuses > 0} | Run Time {this.WR[qStyle].Ticks} | Name {this.WR[qStyle].Name} | MapTime ID {this.WR[qStyle].ID}"); - - // Populate the ReplayManager for all stages found and set the first stage to replay - if (this.ReplayManager.StageWR != null) - { - Set_Replay_Data(qType, qStyle, qStage, replayFramesBase64); - } - break; - } + case 1: // Bonus WR data and total completions + BonusWR[run.Stage][run.Style].ID = run.ID; + 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; + BonusWR[run.Stage][run.Style].EndVelX = run.EndVelX; + BonusWR[run.Stage][run.Style].EndVelY = run.EndVelY; + BonusWR[run.Stage][run.Style].EndVelZ = run.EndVelZ; + BonusWR[run.Stage][run.Style].RunDate = run.RunDate; + BonusWR[run.Stage][run.Style].Name = run.Name; + BonusCompletions[run.Stage][run.Style] = run.TotalCount; + + SetReplayData(run.Type, run.Style, run.Stage, run.ReplayFrames!); + break; - // Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal async Task Get_Map_Record_Runs -> Map Completions for style {qStyle} {this.MapCompletions[qStyle]}"); - } - } + case 2: // Stage WR data and total completions + StageWR[run.Stage][run.Style].ID = run.ID; + 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; + StageWR[run.Stage][run.Style].EndVelX = run.EndVelX; + StageWR[run.Stage][run.Style].EndVelY = run.EndVelY; + StageWR[run.Stage][run.Style].EndVelZ = run.EndVelZ; + StageWR[run.Stage][run.Style].RunDate = run.RunDate; + StageWR[run.Stage][run.Style].Name = run.Name; + StageCompletions[run.Stage][run.Style] = run.TotalCount; + + SetReplayData(run.Type, run.Style, run.Stage, run.ReplayFrames!); + break; } } - // Retrieve the checkpoints for each Style if it has been set. foreach (int style in Config.Styles) { - // if (this.MapCompletions[style] > 0 && this.WR[style].ID != -1 && this.WR[style].Ticks < this.ReplayManager.MapWR.RecordRunTime) // This should also reload Checkpoints if a new MapWR is set - if ( - this.MapCompletions[style] > 0 && this.WR[style].ID != -1 || - this.WR[style].ID != -1 && this.WR[style].Ticks < this.ReplayManager.MapWR.RecordRunTime - ) // This should also reload Checkpoints if a new MapWR is set + if (MapCompletions[style] > 0 && WR[style].ID != -1) { #if DEBUG - Console.WriteLine($"======= CS2 Surf DEBUG >> internal async Task Get_Map_Record_Runs : Map -> [{(Config.API.GetApiOnly() ? "API" : "DB")}] Loaded {this.MapCompletions[style]} runs (MapID {this.ID} | Style {style}). WR by {this.WR[style].Name} - {PlayerHUD.FormatTime(this.WR[style].Ticks)}"); + _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].RunTime) + ); #endif - await Get_Record_Run_Checkpoints(style); + + var stopwatch = Stopwatch.StartNew(); + await this.WR[style].LoadCheckpoints(); // Load the checkpoints for the WR and Style combo + stopwatch.Stop(); + + _logger.LogInformation("[{ClassName}] {MethodName} -> Finished WR.[{Style}].LoadCheckpoints() in {ElapsedMilliseconds}ms | API = {API}", + nameof(Map), methodName, style, stopwatch.ElapsedMilliseconds, Config.Api.GetApiOnly() + ); } } } - /// - /// Redirects to `PersonalBest.PB_LoadCheckpointsData()`. - /// 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].PB_LoadCheckpointsData(); - } - /// /// 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. @@ -591,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 Set_Replay_Data(int type, int style, int stage, string replayFramesBase64) + internal void SetReplayData(int type, int style, int stage, ReplayFramesString replayFramesBase64, [CallerMemberName] string methodName = "") { - JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = false, Converters = { new VectorConverter(), new QAngleConverter() } }; + 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)!; @@ -604,12 +485,14 @@ internal void Set_Replay_Data(int type, int style, int stage, string replayFrame switch (type) { case 0: // Map Replays - // Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal void Set_Replay_Data -> [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].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; @@ -619,49 +502,39 @@ internal void Set_Replay_Data(int type, int style, int stage, string replayFrame 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}"); - break; - case ReplayFrameSituation.STAGE_ZONE_EXIT: - this.ReplayManager.MapWR.StageExitSituations.Add(i); - Console.WriteLine($"STAGE_ZONE_EXIT: {i} | Situation {f.Situation}"); + /// Console.WriteLine($"STAGE_ZONE_ENTER: {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}"); + /// 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}"); - 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; - Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal void Set_Replay_Data -> [BonusWR] Adding run {this.BonusWR[stage][style].ID} {PlayerHUD.FormatTime(this.BonusWR[stage][style].Ticks)} (Ticks = {this.BonusWR[stage][style].Ticks}; Frames = {frames.Count}) to `ReplayManager.AllBonusWR`"); +#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].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; @@ -672,10 +545,7 @@ internal void Set_Replay_Data(int type, int style, int stage, string replayFrame 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; } @@ -683,13 +553,17 @@ internal void Set_Replay_Data(int type, int style, int stage, string replayFrame // Set the bonus to replay first if (this.ReplayManager.BonusWR != null && this.ReplayManager.BonusWR.MapID == -1) { - Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal void Set_Replay_Data -> [BonusWR] Setting first `ReplayManager.BonusWR` to bonus {stage}"); +#if DEBUG + _logger.LogDebug("[{ClassName}] {MethodName} -> [BonusWR] Setting first `ReplayManager.BonusWR` to bonus {stage}", + nameof(Map), methodName, stage + ); +#endif if (this.ReplayManager.BonusWR.IsPlaying) // Maybe only stop the replay if we are overwriting the current bonus being played? 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; @@ -698,14 +572,19 @@ internal void Set_Replay_Data(int type, int style, int stage, string replayFrame 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; - Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal void Set_Replay_Data -> [StageWR] Adding run {this.StageWR[stage][style].ID} {PlayerHUD.FormatTime(this.StageWR[stage][style].Ticks)} (Ticks = {this.StageWR[stage][style].Ticks}; Frames = {frames.Count}) to `ReplayManager.AllStageWR`"); +#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].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; @@ -727,13 +606,18 @@ internal void Set_Replay_Data(int type, int style, int stage, string replayFrame // Set the stage to replay first if (this.ReplayManager.StageWR != null && this.ReplayManager.StageWR.MapID == -1) { - Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal void Set_Replay_Data -> [StageWR] Setting first `ReplayManager.StageWR` to stage {stage}"); +#if DEBUG + _logger.LogDebug("[{ClassName}] {MethodName} -> [StageWR] Setting first `ReplayManager.StageWR` to stage {stage}", + nameof(Map), methodName, stage + ); +#endif + if (this.ReplayManager.StageWR.IsPlaying) // Maybe only stop the replay if we are overwriting the current stage being played? 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; @@ -743,24 +627,25 @@ internal void Set_Replay_Data(int type, int style, int stage, string replayFrame } // Start the new map replay if none existed until now - if (type == 0 && this.ReplayManager.MapWR != null && !this.ReplayManager.MapWR.IsPlaying) + Server.NextFrame(() => { - Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal void Set_Replay_Data -> [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 Set_Replay_Data -> [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 Set_Replay_Data -> [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) @@ -785,4 +670,16 @@ public static bool IsInZone(Vector zoneOrigin, float zoneCollisionRadius, Vector else return false; } + + private void KillServerCommandEnts([CallerMemberName] string methodName = "") + { + var pointServerCommands = Utilities.FindAllEntitiesByDesignerName("point_servercommand"); + + foreach (var servercmd in pointServerCommands) + { + if (servercmd == null) continue; + _logger.LogTrace("[{ClassName}] {MethodName} -> Killed point_servercommand ent: {ServerCMD}", nameof(Map), methodName, servercmd.Handle); + servercmd.Remove(); + } + } } \ No newline at end of file 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 41a3454..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,169 +70,484 @@ public static string FormatTime(int ticks, PlayerTimer.TimeFormatStyle style = P } } - public void Display() + /// + /// 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 + /// + 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) @@ -235,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) @@ -272,28 +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} CP [{ChatColors.Yellow}{playerCheckpoint}{ChatColors.Default}]: " + - $"{ChatColors.Yellow}{FormatTime(_player.Timer.Ticks)}{ChatColors.Default} " + - $"{ChatColors.Yellow}({currentSpeed.ToString("0")}){ChatColors.Default} " + - $"[PB: {strPbDifference} | " + - $"WR: {strWrDifference}]"); + $"{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 6f5930a..d9d0452 100644 --- a/src/ST-Player/PlayerProfile.cs +++ b/src/ST-Player/PlayerProfile.cs @@ -1,20 +1,24 @@ -using CounterStrikeSharp.API.Core; -using Microsoft.VisualBasic; +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; - - public PlayerProfile(ulong steamId, string name = "", string country = "") + private readonly ILogger _logger; + private readonly IDataAccessService _dataService; + + internal PlayerProfile(ulong steamId, string name = "", string country = "") { + // Resolve the logger instance from the DI container + _logger = SurfTimer.ServiceProvider.GetRequiredService>(); + _dataService = SurfTimer.ServiceProvider.GetRequiredService(); + + this.SteamID = steamId; this.Name = name; this.Country = country; @@ -22,99 +26,98 @@ 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() + internal async Task InitializeAsync([CallerMemberName] string methodName = "") { - await Get_Player_Profile(); + await GetPlayerProfile(); - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerProfile -> InitializeAsync -> [{(Config.API.GetApiOnly() ? "API" : "DB")}] We got ProfileID = {this.ID} ({this.Name})"); + _logger.LogTrace("[{ClassName}] {MethodName} -> InitializeAsync -> [{ConnType}] We got ProfileID {ProfileID} ({PlayerName})", + 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 Get_Player_Profile() + internal async Task GetPlayerProfile([CallerMemberName] string methodName = "") { - bool newPlayer = false; + var profile = await _dataService.GetPlayerProfileAsync(this.SteamID); - // Load player profile data from database - using (var playerData = await SurfTimer.DB.QueryAsync(string.Format(Config.MySQL.Queries.DB_QUERY_PP_GET_PROFILE, this.SteamID))) + if (profile != null) { - if (playerData.HasRows && playerData.Read()) - { - // Player exists in database - this.ID = playerData.GetInt32("id"); - this.Name = playerData.GetString("name"); - if (this.Country == "XX" && playerData.GetString("country") != "XX") - this.Country = playerData.GetString("country"); - this.JoinDate = playerData.GetInt32("join_date"); - this.LastSeen = playerData.GetInt32("last_seen"); - this.Connections = playerData.GetInt32("connections"); - } - else - { - newPlayer = true; - } + this.ID = profile.ID; + this.Name = profile.Name; + if (this.Country == "XX" && profile.Country != "XX") + this.Country = profile.Country; + this.JoinDate = profile.JoinDate; + this.LastSeen = profile.LastSeen; + this.Connections = profile.Connections; + } + else + { + await InsertPlayerProfile(); } #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerProfile -> InitializeAsync -> [{(Config.API.GetApiOnly() ? "API" : "DB")}] Returning player {this.Name} ({this.SteamID}) loaded from database with ID {this.ID}"); + _logger.LogDebug("[{ClassName}] {MethodName} -> GetPlayerProfile -> [{ConnType}] Loaded player {PlayerName} ({SteamID}) with ID {ProfileID}.", + nameof(PlayerProfile), methodName, Config.API.GetApiOnly() ? "API" : "DB", this.Name, this.SteamID, this.ID + ); #endif - if (newPlayer) - await Insert_Player_Profile(); } /// - /// 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 Insert_Player_Profile() + internal async Task InsertPlayerProfile([CallerMemberName] string methodName = "") { - // Player does not exist in database - int joinDate = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - int lastSeen = joinDate; - int connections = 1; - - // Write new player to database - int newPlayerRows = await SurfTimer.DB.WriteAsync(string.Format( - Config.MySQL.Queries.DB_QUERY_PP_INSERT_PROFILE, - MySqlConnector.MySqlHelper.EscapeString(this.Name), this.SteamID, this.Country, joinDate, lastSeen, connections)); - if (newPlayerRows != 1) + var profile = new PlayerProfileDto { - Exception ex = new($"Error inserting new player profile for '{this.Name}' ({this.SteamID})"); - throw ex; - } + SteamID = this.SteamID, + Name = this.Name!, + Country = this.Country! + }; + + this.ID = await _dataService.InsertPlayerProfileAsync(profile); - await Get_Player_Profile(); #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerProfile -> Insert_Player_Profile -> [{(Config.API.GetApiOnly() ? "API" : "DB")}] New player {this.Name} ({this.SteamID}) added to database with ID {this.ID}"); + _logger.LogDebug("[{ClassName}] {MethodName} -> InsertPlayerProfile -> [{ConnType}] New player {PlayerName} ({SteamID}) added with ID {ProfileID}.", + nameof(PlayerProfile), methodName, Config.API.GetApiOnly() ? "API" : "DB", this.Name, this.SteamID, this.ID + ); #endif } /// - /// 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 Update_Player_Profile(string name) + internal async Task UpdatePlayerProfile(string name, [CallerMemberName] string methodName = "") { - int updatePlayerTask = await SurfTimer.DB.WriteAsync(string.Format(Config.MySQL.Queries.DB_QUERY_PP_UPDATE_PROFILE, this.Country, (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), this.ID, name)); - if (updatePlayerTask != 1) - throw new Exception($"CS2 Surf ERROR >> internal class PlayerProfile -> Update_Player_Profile -> [{(Config.API.GetApiOnly() ? "API" : "DB")}] Failed to update player data in database. Player: {this.Name} ({this.SteamID})"); + this.Name = name; + var dto = new PlayerProfileDto + { + SteamID = this.SteamID, + Name = this.Name, + Country = this.Country! + }; + + await _dataService.UpdatePlayerProfileAsync(dto, this.ID); + #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerProfile -> Update_Player_Profile -> [{(Config.API.GetApiOnly() ? "API" : "DB")}] Updated player {name} ({this.SteamID}) in database. ID {this.ID}"); + _logger.LogDebug("[{ClassName}] {MethodName} -> UpdatePlayerProfile -> [{ConnType}] Updated player {PlayerName} ({SteamID}) with ID {ProfileID}.", + nameof(PlayerProfile), methodName, Config.API.GetApiOnly() ? "API" : "DB", this.Name, this.SteamID, this.ID + ); #endif } } \ No newline at end of file diff --git a/src/ST-Player/PlayerStats/Checkpoint.cs b/src/ST-Player/PlayerStats/Checkpoint.cs deleted file mode 100644 index 85b6254..0000000 --- a/src/ST-Player/PlayerStats/Checkpoint.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace SurfTimer; - -internal class Checkpoint : PersonalBest -{ - public int CP { get; set; } - public int EndTouch { get; set; } - public int Attempts { get; set; } - - 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; // To-do: this was supposed to be the ticks but that is used for run_time for HUD???? - StartVelX = startVelX; - StartVelY = startVelY; - StartVelZ = startVelZ; - EndVelX = endVelX; - EndVelY = endVelY; - EndVelZ = endVelZ; - EndTouch = endTouch; - Attempts = attempts; - } -} \ No newline at end of file diff --git a/src/ST-Player/PlayerStats/CurrentRun.cs b/src/ST-Player/PlayerStats/CurrentRun.cs index 32150a0..5bf1485 100644 --- a/src/ST-Player/PlayerStats/CurrentRun.cs +++ b/src/ST-Player/PlayerStats/CurrentRun.cs @@ -1,85 +1,201 @@ +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 +public class CurrentRun : RunStatsEntity { - public Dictionary Checkpoints { get; set; } // Current RUN checkpoints tracker - public int Ticks { get; set; } // To-do: will be the last (any) zone end touch time - public float StartVelX { get; set; } // This will store MAP START VELOCITY X - public float StartVelY { get; set; } // This will store MAP START VELOCITY Y - public float StartVelZ { get; set; } // This will store MAP START VELOCITY Z - public float EndVelX { get; set; } // This will store MAP END VELOCITY X - public float EndVelY { get; set; } // This will store MAP END VELOCITY Y - public float EndVelZ { get; set; } // This will store MAP END VELOCITY Z - public int RunDate { get; set; } - // Add other properties as needed - - // Constructor - public CurrentRun() - { - 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; - } + private readonly ILogger _logger; + private readonly IDataAccessService _dataService; - public void Reset() + public Dictionary Checkpoints { get; set; } + + internal CurrentRun() { - Checkpoints.Clear(); - Ticks = 0; - StartVelX = 0.0f; - StartVelY = 0.0f; - StartVelZ = 0.0f; - EndVelX = 0.0f; - EndVelY = 0.0f; - EndVelZ = 0.0f; - RunDate = 0; - // Reset other properties as needed + _logger = SurfTimer.ServiceProvider.GetRequiredService>(); + _dataService = SurfTimer.ServiceProvider.GetRequiredService(); + + 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) + internal async Task SaveMapTime(Player player, short bonus = 0, short stage = 0, int run_ticks = -1, [CallerMemberName] string methodName = "") { - // Add entry in DB for the run - // PrintSituations(player); - string replay_frames = player.ReplayRecorder.TrimReplay(player, stage != 0 ? 2 : bonus != 0 ? 1 : 0, stage == SurfTimer.CurrentMap.Stages); + string replay_frames = ""; + int style = player.Timer.Style; + int mapTimeId = 0; + short recType; - Console.WriteLine($"CS2 Surf DEBUG >> internal class CurrentRun -> public async Task SaveMapTime -> Sending total of {replay_frames.Length} replay frames"); - if (Config.API.GetApiOnly()) + if (stage != 0) { - return; + recType = 2; // Stage run + } + else if (bonus != 0) + { + recType = 1; // Bonus run } else { - await InsertMapTime(player, bonus, stage, run_ticks, replay_frames, true); + 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} serialized and compressed replay frames.", + nameof(CurrentRun), methodName, replay_frames.Length + ); + + var stopwatch = Stopwatch.StartNew(); + var mapTime = new MapTimeRunDataDto + { + PlayerID = player.Profile.ID, + MapID = SurfTimer.CurrentMap.ID, + Style = player.Timer.Style, + Type = recType, + Stage = stage != 0 ? stage : bonus, + 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, + ReplayFrames = replay_frames, + Checkpoints = this.Checkpoints + }; + + 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; + } - if (stage != 0 || bonus != 0) + if (mapTimeId <= 0) + mapTimeId = await _dataService.InsertMapTimeAsync(mapTime); + else + _ = await _dataService.UpdateMapTimeAsync(mapTime, mapTimeId); + + + // Reload the times for the map + await SurfTimer.CurrentMap.LoadMapRecordRuns(); + + _logger.LogTrace("[{ClassName}] {MethodName} -> Loading data for run {ID} with type {Type}.", + nameof(CurrentRun), methodName, mapTimeId, recType + ); + + // Reload the player PB time (could possibly be skipped as we have mapTimeId after inserting) + switch (recType) + { + 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() + ); + } + + /// + /// 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 + internal static async Task SaveStageTime(Player player, short stage = -1, int stage_run_time = -1, bool saveLastStage = false) + { +#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 + ) + { + if (stage_run_time < SurfTimer.CurrentMap.StageWR[stage][pStyle].RunTime) // Player beat the Stage WR { - Console.WriteLine($"CS2 Surf DEBUG >> internal class CurrentRun -> public async Task SaveMapTime -> Inserted an entry for {(stage != 0 ? "Stage" : "Bonus")} {(stage != 0 ? stage : bonus)} - {run_ticks}"); + 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)]}" + ); + } + else if (SurfTimer.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 + else if (player.Stats.StagePB[stage][pStyle] != null && player.Stats.StagePB[stage][pStyle].RunTime > stage_run_time) // Player beating their existing Stage personal best { - await SaveCurrentRunCheckpoints(player, true); // Save this run's checkpoints + 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.CurrMap.Get_Map_Record_Runs(); // Reload the times for the Map + player.ReplayRecorder.IsSaving = true; + + // Save stage run + await player.Stats.ThisRun.SaveMapTime(player, stage: stage, run_ticks: stage_run_time); // Save the Stage MapTime PB data + } + else if (stage_run_time > SurfTimer.CurrentMap.StageWR[stage][pStyle].RunTime && player.Timer.IsStageMode) // Player is behind the Stage WR for the map + { + 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)]}" + ); } } - 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++) @@ -113,119 +229,11 @@ public void PrintSituations(Player player) break; } } - Console.WriteLine($"========================== =========================="); + Console.WriteLine("========================== MapSituations: {0} | START_ZONE_ENTER = {1} | START_ZONE_EXIT = {2} | END_ZONE_ENTER = {3} ==========================", + player.ReplayRecorder.MapSituations.Count, + player.ReplayRecorder.MapSituations[0], player.ReplayRecorder.MapSituations[1], player.ReplayRecorder.MapSituations[2]); + Console.WriteLine("========================== Total Frames: {0} ==========================", player.ReplayRecorder.Frames.Count); } - /// - /// Saves the CurrentRun of the player to the database. Does NOT support Bonus entries yet. - /// - /// Player object - /// Bonus number - /// Stage number - /// Ticks for the run - /// Replay frames - /// Whether to reload the PersonalBest data for the Player. - /// - public async Task InsertMapTime(Player player, int bonus = 0, int stage = 0, int run_ticks = -1, string replay_frames = "", bool reloadData = false) - { - int playerId = player.Profile.ID; - int mapId = player.CurrMap.ID; - int style = player.Timer.Style; - int ticks = run_ticks == -1 ? player.Stats.ThisRun.Ticks : run_ticks; - // int ticks = player.Stats.ThisRun.Ticks; - int type = stage != 0 ? 2 : bonus != 0 ? 1 : 0; - float startVelX = player.Stats.ThisRun.StartVelX; - float startVelY = player.Stats.ThisRun.StartVelY; - float startVelZ = player.Stats.ThisRun.StartVelZ; - float endVelX = player.Stats.ThisRun.EndVelX; - float endVelY = player.Stats.ThisRun.EndVelY; - float endVelZ = player.Stats.ThisRun.EndVelZ; - - if (Config.API.GetApiOnly()) // API Calls - { - // API Insert map goes here - } - else // MySQL Queries - { - int updatePlayerRunTask = 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) - { - 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) - { - Console.WriteLine($"CS2 Surf DEBUG >> internal class CurrentRun -> public async Task InsertMapTime -> Will reload MapTime (Type {type}) data for '{player.Profile.Name}' (ID {player.Stats.PB[player.Timer.Style].ID}))"); - await player.Stats.PB[style].PB_LoadPlayerSpecificMapTimeData(player); // Load the Map MapTime PB data again (will refresh the MapTime ID for the Checkpoints query) - } - else if (reloadData && type == 1) - { - Console.WriteLine($"CS2 Surf DEBUG >> internal class CurrentRun -> public async Task InsertMapTime -> Will reload Bonus MapTime (Type {type}) data for '{player.Profile.Name}' (ID {player.Stats.BonusPB[bonus][style].ID}))"); - await player.Stats.BonusPB[bonus][style].PB_LoadPlayerSpecificMapTimeData(player); // Load the Bonus MapTime PB data again (will refresh the MapTime ID) - } - else if (reloadData && type == 2) - { - Console.WriteLine($"CS2 Surf DEBUG >> internal class CurrentRun -> public async Task InsertMapTime -> Will reload Stage MapTime (Type {type}) data for '{player.Profile.Name}' (ID {player.Stats.StagePB[stage][style].ID}))"); - await player.Stats.StagePB[stage][style].PB_LoadPlayerSpecificMapTimeData(player); // Load the Stage MapTime PB data again (will refresh the MapTime ID) - } - } - } - - /// - /// Saves the `CurrentRunCheckpoints` dictionary to the database - /// - /// Player object - /// Whether to reload the PersonalBest Checkpoints data for the Player. - public async Task SaveCurrentRunCheckpoints(Player player, bool reloadData = false) - { - Console.WriteLine($"CS2 Surf DEBUG >> internal class CurrentRun -> SaveCurrentRunCheckpoints -> Will send {player.Stats.ThisRun.Checkpoints.Count} ({this.Checkpoints.Count}) checkpoints to DB...."); - 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 player.Stats.ThisRun.Checkpoints) - 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; - -#if DEBUG - Console.WriteLine($"CP: {cp} | MapTime ID: {mapTimeId} | Time: {endTouch} | Ticks: {ticks} | startVelX: {startVelX} | startVelY: {startVelY} | startVelZ: {startVelZ} | endVelX: {endVelX} | endVelY: {endVelY} | endVelZ: {endVelZ}"); - Console.WriteLine($@"CS2 Surf DEBUG >> internal class CurrentRun -> SaveCurrentRunCheckpoints -> - {string.Format( - Config.MySQL.Queries.DB_QUERY_CR_INSERT_CP, - mapTimeId, cp, ticks, startVelX, startVelY, startVelZ, endVelX, endVelY, endVelZ, attempts, endTouch)} - "); -#endif - - // Insert/Update CPs to database - // Check if the player has PB object initialized and if the player's character is currently active in the game - 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); - } - } - await SurfTimer.DB.TransactionAsync(commands); - player.Stats.ThisRun.Checkpoints.Clear(); - - if (reloadData) - { - Console.WriteLine($"CS2 Surf DEBUG >> internal class CurrentRun -> public async Task SaveCurrentRunCheckpoints -> Will reload Checkpoints data for {player.Profile.Name} (ID {player.Stats.PB[player.Timer.Style].ID})"); - await player.Stats.PB[player.Timer.Style].PB_LoadCheckpointsData(); // Load the Checkpoints data again - } - } } + diff --git a/src/ST-Player/PlayerStats/PersonalBest.cs b/src/ST-Player/PlayerStats/PersonalBest.cs index a689a00..66be387 100644 --- a/src/ST-Player/PlayerStats/PersonalBest.cs +++ b/src/ST-Player/PlayerStats/PersonalBest.cs @@ -1,222 +1,97 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SurfTimer.Data; +using SurfTimer.Shared.Entities; +using System.Runtime.CompilerServices; + 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 +public class PersonalBest : MapTimeRunDataEntity { - public int ID { get; set; } = -1; // Exclude from constructor, retrieve from Database when loading/saving - public int Ticks { get; set; } - public int Rank { get; set; } = -1; // Exclude from constructor, retrieve from Database when loading/saving - public Dictionary Checkpoints { get; set; } - public int Type { get; set; } // Identifies bonus # - 0 for map time -> huh, why o_O? - 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 Name { get; set; } = ""; // This is used only for WRs - // Add other properties as needed + public Dictionary? Checkpoints { get; set; } + private readonly ILogger _logger; + private readonly IDataAccessService _dataService; - // Constructor - public PersonalBest() + internal PersonalBest() : base() { - Ticks = -1; - Checkpoints = new Dictionary(); - Type = -1; - StartVelX = -1.0f; - StartVelY = -1.0f; - StartVelZ = -1.0f; - EndVelX = -1.0f; - EndVelY = -1.0f; - EndVelZ = -1.0f; - RunDate = -1; + // Resolve the logger instance from the DI container + _logger = SurfTimer.ServiceProvider.GetRequiredService>(); + _dataService = SurfTimer.ServiceProvider.GetRequiredService(); } /// /// Loads the Checkpoint data for the given MapTime_ID. Used for loading player's personal bests and Map's world records. - /// Automatically detects whether to use API Calls or MySQL query. /// Bonus and Stage runs should NOT have any checkpoints. /// - public async Task PB_LoadCheckpointsData() + internal async Task LoadCheckpoints([CallerMemberName] string methodName = "") { - if (this == null) + var cps = await _dataService.LoadCheckpointsAsync(this.ID); + + // If nothing found, log and return + if (cps == null || cps.Count == 0) { -#if DEBUG - Console.WriteLine("CS2 Surf ERROR >> internal class PersonalBest -> public async Task PB_LoadCheckpointsData -> PersonalBest object is null."); -#endif + _logger.LogInformation( + "[{Class}] {Method} -> No checkpoints found for run {RunId}.", + nameof(PersonalBest), methodName, this.ID + ); return; } - if (this.Checkpoints == null) - { -#if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> internal class PersonalBest -> public async Task PB_LoadCheckpointsData -> PB Checkpoints list is not initialized."); -#endif - this.Checkpoints = new Dictionary(); // Initialize if null - } - if (Config.API.GetApiOnly()) // Load with API - { - var checkpoints = await ApiMethod.GET(string.Format(Config.API.Endpoints.ENDPOINT_MAP_GET_RUN_CPS, this.ID)); - if (checkpoints == null || checkpoints.Length == 0) - return; - - foreach (API_Checkpoint checkpoint in checkpoints) - { - Checkpoint cp = new Checkpoint - ( - checkpoint.cp, - checkpoint.run_time, - checkpoint.start_vel_x, - checkpoint.start_vel_y, - checkpoint.start_vel_z, - checkpoint.end_vel_x, - checkpoint.end_vel_y, - checkpoint.end_vel_z, - checkpoint.end_touch, - checkpoint.attempts - ); - cp.ID = checkpoint.cp; - - this.Checkpoints[cp.CP] = cp; - } - } - else // Load with MySQL - { - using (var results = await SurfTimer.DB.QueryAsync(string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_CPS, this.ID))) - { -#if DEBUG - Console.WriteLine($"this.Checkpoint.Count {this.Checkpoints.Count} "); - Console.WriteLine($"this.ID {this.ID} "); - Console.WriteLine($"this.Ticks {this.Ticks} "); - Console.WriteLine($"this.RunDate {this.RunDate} "); -#endif - - if (!results.HasRows) - { -#if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> internal class PersonalBest -> public async Task PB_LoadCheckpointsData -> No checkpoints found for this mapTimeId {this.ID}."); -#endif - - return; - } - -#if DEBUG - Console.WriteLine($"======== CS2 Surf DEBUG >> internal class PersonalBest -> public async Task PB_LoadCheckpointsData -> Checkpoints found for this mapTimeId"); -#endif - - while (results.Read()) - { -#if DEBUG - Console.WriteLine($"cp {results.GetInt32("cp")} "); - Console.WriteLine($"run_time {results.GetInt32("run_time")} "); - Console.WriteLine($"sVelX {results.GetFloat("start_vel_x")} "); - Console.WriteLine($"sVelY {results.GetFloat("start_vel_y")} "); -#endif - - Checkpoint cp = new(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")); - cp.ID = results.GetInt32("cp"); - // To-do: cp.ID = calculate Rank # from DB - - this.Checkpoints[cp.CP] = cp; - -#if DEBUG - Console.WriteLine($"======= CS2 Surf DEBUG >> internal class PersonalBest -> public async Task PB_LoadCheckpointsData -> Loaded CP {cp.CP} with RunTime {cp.Ticks}."); -#endif - } - } - } + this.Checkpoints = cps; - // #if DEBUG - Console.WriteLine($"======= CS2 Surf DEBUG >> internal class PersonalBest -> public async Task PB_LoadCheckpointsData -> [{(Config.API.GetApiOnly() ? "API" : "DB")}] {this.Checkpoints.Count} Checkpoints loaded from DB for run ID {this.ID}."); - // #endif + _logger.LogInformation( + "[{ClassName}] {MethodName} -> Loaded {Count} checkpoints for run {RunId}.", + nameof(PersonalBest), methodName, cps.Count, this.ID + ); } + /// /// Loads specific type/style MapTime data for the player (run without checkpoints) from the database for their personal best runs. /// Should be used to reload data from a specific `PersonalBest` object /// /// Player object - public async Task PB_LoadPlayerSpecificMapTimeData(Player player) + internal async Task 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) + var model = await _dataService.LoadPersonalBestRunAsync( + pbId: this.ID == -1 ? (int?)null : this.ID, + playerId: player.Profile.ID, + mapId: SurfTimer.CurrentMap.ID, + type: this.Type, + style: player.Timer.Style + ); + + // If nothing found, log and return + if (model == null) { -#if DEBUG - Console.WriteLine("CS2 Surf ERROR >> internal class PersonalBest -> public async Task PB_LoadPlayerSpecificMapTimeData -> PersonalBest object is null."); -#endif - + _logger.LogTrace( + "[{ClassName}] {MethodName} -> No personal best found for player {Player} (ID={Id} ; Type={Type}).", + nameof(PersonalBest), methodName, + player.Profile.Name, player.Profile.ID, this.Type + ); 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 - Console.WriteLine($"CS2 Surf DEBUG >> internal class PersonalBest -> public async Task PB_LoadPlayerSpecificMapTimeData -> No MapTime data found for '{player.Profile.Name}' ({player.Profile.ID}). (Null? {results == null})"); - // #endif - - return; - } - -#if DEBUG - Console.WriteLine($"======== CS2 Surf DEBUG >> internal class PersonalBest -> public async Task PB_LoadPlayerSpecificMapTimeData -> Found a run for '{player.Profile.Name}' ({player.Profile.ID})."); -#endif - - while (results.Read()) - { -#if DEBUG - Console.WriteLine($"cp {results.GetInt32("cp")} "); - Console.WriteLine($"run_time {results.GetInt32("run_time")} "); - Console.WriteLine($"sVelX {results.GetFloat("start_vel_x")} "); - Console.WriteLine($"sVelY {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 - Console.WriteLine($"======= CS2 Surf DEBUG >> internal class PersonalBest -> public async Task PB_LoadPlayerSpecificMapTimeData -> Loaded run (MapId = {this.ID}) by {player.Profile.Name} - {PlayerHUD.FormatTime(this.Ticks)}"); -#endif - } - - // #if DEBUG - Console.WriteLine($"======= CS2 Surf DEBUG >> internal class PersonalBest -> public async Task PB_LoadPlayerSpecificMapTimeData -> MapTime (Type: {this.Type}) loaded from DB. ID: {this.ID} for {player.Profile.Name}"); - // #endif + this.ID = model.ID; + this.RunTime = model.RunTime; + this.Rank = model.Rank; + this.StartVelX = model.StartVelX; + this.StartVelY = model.StartVelY; + this.StartVelZ = model.StartVelZ; + this.EndVelX = model.EndVelX; + this.EndVelY = model.EndVelY; + this.EndVelZ = model.EndVelZ; + this.RunDate = model.RunDate; + this.ReplayFrames = model.ReplayFrames; // Won't work with MySQL load? - Not tested + + _logger.LogDebug( + "[{ClassName}] {MethodName} -> Loaded PB run {RunId} for {Player}.", + nameof(PersonalBest), methodName, + this.ID, player.Profile.Name + ); } } \ No newline at end of file diff --git a/src/ST-Player/PlayerStats/PlayerStats.cs b/src/ST-Player/PlayerStats/PlayerStats.cs index c0586c1..fc55c05 100644 --- a/src/ST-Player/PlayerStats/PlayerStats.cs +++ b/src/ST-Player/PlayerStats/PlayerStats.cs @@ -1,244 +1,150 @@ -using MySqlConnector; +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(); - // 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() - { - // Initialize MapPB for each style - foreach (int style in Config.Styles) - { - PB[style] = new PersonalBest(); - PB[style].Type = 0; - } + private readonly ILogger _logger; + private readonly IDataAccessService _dataService; - 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; - - this.StagePB[i] = new Dictionary(); - this.StagePB[i][0] = new PersonalBest(); - this.StagePB[i][0].Type = 2; - initialized++; - } - Console.WriteLine($"====== INITIALIZED {initialized} STAGES AND BONUSES FOR PLAYERSTATS"); - } - // API - public async void LoadMapTime(Player player, int style = 0) + internal PlayerStats([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) - { - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadMapTime -> No MapTime data found for Player."); - return; - } + // Resolve the logger instance from the DI container + _logger = SurfTimer.ServiceProvider.GetRequiredService>(); + _dataService = SurfTimer.ServiceProvider.GetRequiredService(); - 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) - { - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadMapTime -> No Checkpoints data found for Player."); - return; - } + // 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; - foreach (var cp in player_maptime.checkpoints) + foreach (int style in Config.Styles) { - 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); - } - } + PB[style] = new PersonalBest { Type = 0 }; - // API - public async void LoadStageTime(Player player, int style = 0) - { - 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) - { - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadStageTime -> No MapTime data found for Player."); - 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 - public async void LoadBonusTime(Player player, int style = 0) - { - 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) - { - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadBonusTime -> No MapTime data found for Player."); - 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. + /// `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) + internal 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))) + var playerMapTimes = await _dataService.GetPlayerMapTimesAsync(player.Profile.ID, SurfTimer.CurrentMap.ID); + + if (!playerMapTimes.Any()) { - // int style = player.Timer.Style; - int style; - if (!playerStats.HasRows) - { - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> public async Task LoadPlayerMapTimesData -> No MapTimes data found for Player ({player.Profile.ID})."); - return; - } - while (playerStats.Read()) + _logger.LogTrace("[{ClassName}] {MethodName} -> No MapTimes data found for Player {PlayerName} (ID {PlayerID}).", + nameof(PlayerStats), methodName, player.Profile.Name, player.Profile.ID); + return; + } + + foreach (var mapTime in playerMapTimes) + { + int style = mapTime.Style; + switch (mapTime.Type) { - // Load data into each PersonalBest object - if (playerStats.GetInt32("type") == 1) // Bonus time - { + case 1: // Bonus time #if DEBUG - Console.WriteLine("DEBUG >> (func) LoadPlayerMapTimesData >> BonusPB"); + _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData >> BonusPB with ID {ID}", nameof(PlayerStats), methodName, mapTime.ID); #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 - { + BonusPB[mapTime.Stage][style].ID = mapTime.ID; + 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; + BonusPB[mapTime.Stage][style].StartVelY = mapTime.StartVelY; + BonusPB[mapTime.Stage][style].StartVelZ = mapTime.StartVelZ; + BonusPB[mapTime.Stage][style].EndVelX = mapTime.EndVelX; + BonusPB[mapTime.Stage][style].EndVelY = mapTime.EndVelY; + BonusPB[mapTime.Stage][style].EndVelZ = mapTime.EndVelZ; + BonusPB[mapTime.Stage][style].RunDate = mapTime.RunDate; + break; + + case 2: // Stage time #if DEBUG - Console.WriteLine("DEBUG >> (func) LoadPlayerMapTimesData >> StagePB"); + _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData >> StagePB with ID {ID}", nameof(PlayerStats), methodName, mapTime.ID); #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 - { + StagePB[mapTime.Stage][style].ID = mapTime.ID; + 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; + StagePB[mapTime.Stage][style].StartVelY = mapTime.StartVelY; + StagePB[mapTime.Stage][style].StartVelZ = mapTime.StartVelZ; + StagePB[mapTime.Stage][style].EndVelX = mapTime.EndVelX; + StagePB[mapTime.Stage][style].EndVelY = mapTime.EndVelY; + StagePB[mapTime.Stage][style].EndVelZ = mapTime.EndVelZ; + StagePB[mapTime.Stage][style].RunDate = mapTime.RunDate; + break; + + default: // Map time #if DEBUG - Console.WriteLine("DEBUG >> (func) LoadPlayerMapTimesData >> MapPB"); + _logger.LogDebug("[{ClassName}] {MethodName} -> LoadPlayerMapTimesData >> MapPB with ID {ID}", nameof(PlayerStats), methodName, mapTime.ID); #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].PB_LoadCheckpointsData(); - } - // 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}"); + PB[style].ID = mapTime.ID; + PB[style].RunTime = mapTime.RunTime; + PB[style].Type = mapTime.Type; + PB[style].Rank = mapTime.Rank; + PB[style].StartVelX = mapTime.StartVelX; + PB[style].StartVelY = mapTime.StartVelY; + PB[style].StartVelZ = mapTime.StartVelZ; + PB[style].EndVelX = mapTime.EndVelX; + 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; + } + #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> public async Task LoadPlayerMapTimesData -> PlayerStats.PB (ID {PB[style].ID}) loaded from DB."); + _logger.LogDebug("[{ClassName}] {MethodName} -> Loaded PB[{Style}] run {RunID} (Rank {Rank}) for '{PlayerName}' (ID {PlayerID}).", + nameof(PlayerStats), methodName, style, mapTime.ID, mapTime.Rank, player.Profile.Name, player.Profile.ID); #endif - } } } } \ No newline at end of file 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 a5a8b11..e5ce8fb 100644 --- a/src/ST-Player/Replay/ReplayFrame.cs +++ b/src/ST-Player/Replay/ReplayFrame.cs @@ -1,7 +1,8 @@ namespace SurfTimer; -using CounterStrikeSharp.API.Modules.Utils; -internal enum ReplayFrameSituation +using System; + +public enum ReplayFrameSituation { NONE, @@ -26,20 +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 Vector GetPos() + public VectorT GetPos() { - return new Vector(this.pos[0], this.pos[1], this.pos[2]); + return new VectorT(this.pos[0], this.pos[1], this.pos[2]); } - - public QAngle GetAng() + public QAngleT GetAng() { - return new QAngle(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 6430f07..5313245 100644 --- a/src/ST-Player/Replay/ReplayPlayer.cs +++ b/src/ST-Player/Replay/ReplayPlayer.cs @@ -1,10 +1,13 @@ 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. @@ -51,7 +54,16 @@ internal class ReplayPlayer public CCSPlayerController? Controller { get; set; } - public void ResetReplay() + private readonly ILogger _logger; + + // Constructor + internal ReplayPlayer() + { + // Resolve the logger instance from the DI container + _logger = SurfTimer.ServiceProvider.GetRequiredService>(); + } + + internal void ResetReplay() { this.CurrentFrameTick = 0; this.FrameTickIncrement = 1; @@ -62,7 +74,7 @@ public void ResetReplay() this.ReplayCurrentRunTime = 0; } - public void Reset() + internal void Reset() { this.IsPlaying = false; this.IsPaused = false; @@ -76,51 +88,69 @@ public void Reset() this.Controller = null; } - public void SetController(CCSPlayerController c, int repeat_count = -1) + internal void SetController(CCSPlayerController c, int repeat_count = -1, [CallerMemberName] string methodName = "") { this.Controller = c; if (repeat_count != -1) this.RepeatCount = repeat_count; this.IsPlayable = true; - // Console.WriteLine($"===== public void SetController -> Set controller for {c.PlayerName}"); + _logger.LogTrace("[{ClassName}] {MethodName} -> Set controller for {PlayerName}", + nameof(ReplayPlayer), methodName, c.PlayerName + ); } - public void Start() + internal void Start([CallerMemberName] string methodName = "") { if (!this.IsPlayable || !this.IsEnabled) return; + Server.NextFrame(() => + { + this.FormatBotName(); this.IsPlaying = true; - // Console.WriteLine($"CS2 Surf DEBUG >> internal class ReplayPlayer -> public void Start() -> Starting replay for run {this.MapTimeID} (Map ID {this.MapID}) - {this.RecordPlayerName} (Stage {this.Stage})"); +#if DEBUG + _logger.LogDebug("[{ClassName}] {MethodName} -> Starting replay for run {MapTimeID} (Map ID {MapID}) - {RecordPlayerName} (Stage {Stage})", + nameof(ReplayPlayer), methodName, this.MapTimeID, this.MapID, this.RecordPlayerName, this.Stage + ); +#endif + }); } - public void Stop() + internal void Stop([CallerMemberName] string methodName = "") { this.IsPlaying = false; - - // Console.WriteLine($"CS2 Surf DEBUG >> internal class ReplayPlayer -> public void Stop() -> Stopping replay for run {this.MapTimeID} (Map ID {this.MapID}) - {this.RecordPlayerName} (Stage {this.Stage})"); +#if DEBUG + _logger.LogDebug("[{ClassName}] {MethodName} -> Stopping replay for run {MapTimeID} (Map ID {MapID}) - {RecordPlayerName} (Stage {Stage})", + nameof(ReplayPlayer), methodName, this.MapTimeID, this.MapID, this.RecordPlayerName, this.Stage + ); +#endif } - public void Pause() + internal void Pause([CallerMemberName] string methodName = "") { if (!this.IsPlaying || !this.IsEnabled) return; this.IsPaused = !this.IsPaused; this.IsReplayOutsideZone = !this.IsReplayOutsideZone; - - // Console.WriteLine($"CS2 Surf DEBUG >> internal class ReplayPlayer -> public void Pause() -> Pausing replay for run {this.MapTimeID} (Map ID {this.MapID}) - {this.RecordPlayerName} (Stage {this.Stage})"); +#if DEBUG + _logger.LogDebug("[{ClassName}] {MethodName} -> Pausing replay for run {MapTimeID} (Map ID {MapID}) - {RecordPlayerName} (Stage {Stage})", + nameof(ReplayPlayer), methodName, this.MapTimeID, this.MapID, this.RecordPlayerName, this.Stage + ); +#endif } - public void Tick() + internal void Tick() { if (this.MapID == -1 || !this.IsEnabled || !this.IsPlaying || !this.IsPlayable || this.Frames.Count == 0) return; ReplayFrame current_frame = this.Frames[this.CurrentFrameTick]; + this.FormatBotName(); + // SOME BLASHPEMY FOR YOU if (this.FrameTickIncrement >= 0) { @@ -133,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 { @@ -143,18 +182,27 @@ 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 - var current_pos = this.Controller!.PlayerPawn.Value!.AbsOrigin!; + var current_pos = Controller!.PlayerPawn.Value!.AbsOrigin!.ToVector_t(); var current_frame_pos = current_frame.GetPos(); var current_frame_ang = current_frame.GetAng(); bool is_on_ground = (current_frame.Flags & (uint)PlayerFlags.FL_ONGROUND) != 0; - Vector 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; @@ -162,9 +210,9 @@ public void Tick() this.Controller.PlayerPawn.Value.MoveType = MoveType_t.MOVETYPE_NOCLIP; if ((current_pos - current_frame_pos).Length() > 200) - this.Controller.PlayerPawn.Value.Teleport(current_frame_pos, current_frame_ang, new Vector(nint.Zero)); + Extensions.Teleport(Controller.PlayerPawn.Value, current_frame_pos, current_frame_ang, null); else - this.Controller.PlayerPawn.Value.Teleport(new Vector(nint.Zero), current_frame_ang, velocity); + Extensions.Teleport(Controller.PlayerPawn.Value, null, current_frame_ang, velocity); if (!this.IsPaused) @@ -176,31 +224,40 @@ 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) + internal void LoadReplayData(int repeat_count = -1, [CallerMemberName] string methodName = "") { if (!this.IsPlayable || !this.IsEnabled) return; - // Console.WriteLine($"CS2 Surf DEBUG >> internal class ReplayPlayer -> [{(this.Type == 2 ? "Stage Replay" : this.Type == 1 ? "Bonus Replay" : "Map Replay")}] public void LoadReplayData -> We got MapID = {this.MapID}"); + string replayType = this.Type switch + { + 1 => "Bonus Replay", + 2 => "Stage Replay", + 0 => "Map Replay", + _ => "Unknown Type", + }; if (this.MapID == -1) { - Console.WriteLine($"CS2 Surf DEBUG >> internal class ReplayPlayer -> public void LoadReplayData -> [{(this.Type == 2 ? "Stage Replay" : this.Type == 1 ? "Bonus Replay" : "Map Replay")}] No replay data found for Player."); + _logger.LogWarning("[{ClassName}] {MethodName} -> [{Type}] No replay data found for Player. MapID {MapID} | MapTimeID {MapTimeID} | RecordPlayerName {RecordPlayerName}", + nameof(ReplayPlayer), methodName, replayType, this.MapID, this.MapTimeID, RecordPlayerName + ); return; } - // Console.WriteLine($"CS2 Surf DEBUG >> internal class ReplayPlayer -> public void LoadReplayData -> [{(this.Type == 2 ? "Stage Replay" : this.Type == 1 ? "Bonus Replay" : "Map Replay")}] Loaded replay data for Player '{this.RecordPlayerName}'. MapTime ID: {this.MapTimeID} | Repeat {repeat_count} | Frames {this.Frames.Count} | Ticks {this.RecordRunTime}"); + _logger.LogTrace("[{ClassName}] {MethodName} -> [{Type}] Loaded replay data for Player '{RecordPlayerName}' | MapTime ID: {MapTimeID} | Repeat {Repeat} | Frames {TotalFrames} | Ticks {RecordTicks}", + nameof(ReplayPlayer), methodName, replayType, this.RecordPlayerName, this.MapTimeID, repeat_count, this.Frames.Count, this.RecordRunTime + ); + this.ResetReplay(); this.RepeatCount = repeat_count; } - public void FormatBotName() + internal void FormatBotName([CallerMemberName] string methodName = "") { - if (!this.IsPlayable || !this.IsEnabled) + if (!this.IsPlayable || !this.IsEnabled || this.MapID == -1) return; string prefix; @@ -214,17 +271,24 @@ public void FormatBotName() } if (this.Type == 1) - prefix = prefix + $"B {this.Stage}"; + prefix += $"B {this.Stage}"; else if (this.Type == 2) - prefix = prefix + $"CP {this.Stage}"; + prefix += $"CP {this.Stage}"; 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 61e0a8c..180588b 100644 --- a/src/ST-Player/Replay/ReplayRecorder.cs +++ b/src/ST-Player/Replay/ReplayRecorder.cs @@ -1,9 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; using System.Text.Json; -using CounterStrikeSharp.API.Modules.Utils; + namespace SurfTimer; -internal class ReplayRecorder +public class ReplayRecorder { + private readonly ILogger _logger; + + internal ReplayRecorder() + { + // Resolve the logger instance from the DI container + _logger = SurfTimer.ServiceProvider.GetRequiredService>(); + } + public bool IsRecording { get; set; } = false; public bool IsSaving { get; set; } = false; public ReplayFrameSituation CurrentSituation { get; set; } = ReplayFrameSituation.NONE; @@ -23,7 +34,7 @@ internal class ReplayRecorder /// public List BonusSituations { get; set; } = new List(); - public void Reset() + internal void Reset([CallerMemberName] string methodName = "") { this.IsRecording = false; this.Frames.Clear(); @@ -34,61 +45,57 @@ public void Reset() this.MapSituations.Clear(); this.BonusSituations.Clear(); - Console.WriteLine($"===== ReplayRecorder -> Reset() -> Recording has been reset"); +#if DEBUG + _logger.LogDebug("[{ClassName}] {MethodName} -> Recording has been reset", + nameof(ReplayRecorder), methodName + ); +#endif } - public void Start() + internal void Start([CallerMemberName] string methodName = "") { this.IsRecording = true; + +#if DEBUG + _logger.LogDebug("[{ClassName}] {MethodName} -> Recording has been started", + nameof(ReplayRecorder), methodName + ); +#endif } - public void Stop() + internal void Stop([CallerMemberName] string methodName = "") { this.IsRecording = false; + +#if DEBUG + _logger.LogDebug("[{ClassName}] {MethodName} -> Recording has been stopped", + nameof(ReplayRecorder), methodName + ); +#endif } - public void Tick(Player player) + 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(); this.Reset(); - Console.WriteLine($"===== ReplayRecorder -> Tick() -> Recording has stopped and reset"); + _logger.LogTrace("[{ClassName}] {MethodName} -> Recording has stopped and reset for player {Name}", + nameof(ReplayRecorder), methodName, player.Profile.Name + ); return; } 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], @@ -103,228 +110,236 @@ public void Tick(Player player) this.CurrentSituation = ReplayFrameSituation.NONE; } - public string SerializeReplay() + internal string TrimReplay(Player player, short type = 0, bool lastStage = false, [CallerMemberName] string methodName = "") { - // 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); + this.IsSaving = true; + + List? trimmed_frames = new List(); + + _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 replay trimming for player {Name}", + nameof(ReplayRecorder), methodName, player.Profile.Name + ); + throw new InvalidOperationException("There are no Frames available for trimming"); + } + switch (type) + { + case 0: // Map Run + { + trimmed_frames = TrimMapRun(player); + break; + } + case 1: // Bonus Run + { + trimmed_frames = TrimBonusRun(player); + break; + } + case 2: // Stage Run + { + trimmed_frames = TrimStageRun(player, lastStage); + break; + } + } + + 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); } - public string SerializeReplayPortion(int start_idx, int end_idx) // Not used anymore + internal List? TrimMapRun(Player player, [CallerMemberName] string methodName = "") { - // 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); + List? new_frames = new List(); + + 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 {StartEnterIndex} | last start exit {StartExitIndex} | end enter {EndEnterIndex}", + nameof(ReplayRecorder), methodName, start_enter_index, start_exit_index, end_enter_index); + + 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; + } + + 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); + + _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 void SetLastTickSituation(ReplayFrameSituation situation) + internal List? TrimBonusRun(Player player, [CallerMemberName] string methodName = "") { - if (this.Frames.Count == 0) - return; + List? new_frames = new List(); - this.Frames[this.Frames.Count - 2].Situation = situation; + 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) + { + _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; + } } - public string TrimReplay(Player player, int type = 0, bool lastStage = false) + internal List? TrimStageRun(Player player, bool lastStage = false, [CallerMemberName] string methodName = "") { - this.IsSaving = true; + List? new_frames = new List(); - List new_frames = new List(); + int stage_end_index; + int stage_exit_index; + int stage_enter_index; - if (this.Frames.Count == 0) + int stage = player.Timer.Stage - 1; + + ReplayFrameSituation enterZone; + ReplayFrameSituation exitZone; + ReplayFrameSituation endZone; + + // Select the correct enums for trimming + if (stage == 1) { - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> There are no Frames for trimming"); - throw new Exception("There are no Frames available for trimming"); + _logger.LogDebug("Stage replay trimming will use START_ZONE_*"); + enterZone = ReplayFrameSituation.START_ZONE_ENTER; + exitZone = ReplayFrameSituation.START_ZONE_EXIT; + endZone = ReplayFrameSituation.STAGE_ZONE_ENTER; } - switch (type) + 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) { - case 0: // Trim Map replays - // Map/Bonus runs - 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); - - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> Trimming Map Run replay. Last start enter {start_enter_index}, last start exit {start_exit_index}, end enter {end_enter_index}"); - - if (start_enter_index != -1 && start_exit_index != -1 && end_enter_index != -1) - { - // Try different buffer sizes for start index - int startIndex; - if (start_exit_index - (Config.ReplaysPre * 2) >= start_enter_index) - startIndex = start_exit_index - (Config.ReplaysPre * 2); - else if (start_exit_index - Config.ReplaysPre >= start_enter_index) - startIndex = start_exit_index - Config.ReplaysPre; - else if (start_exit_index - (Config.ReplaysPre / 2) >= start_enter_index) - startIndex = start_exit_index - (Config.ReplaysPre / 2); - else - startIndex = start_enter_index; // fallback to minimum allowed - - // Try different buffer sizes for end index - int endIndex; - if (end_enter_index + (Config.ReplaysPre * 2) < Frames.Count) - endIndex = end_enter_index + (Config.ReplaysPre * 2); - else if (end_enter_index + Config.ReplaysPre < Frames.Count) - endIndex = end_enter_index + Config.ReplaysPre; - else if (end_enter_index + (Config.ReplaysPre / 2) < Frames.Count) - endIndex = end_enter_index + (Config.ReplaysPre / 2); - else - // endIndex = Frames.Count - 1; // fallback to maximum allowed - endIndex = end_enter_index; // fallback to maximum allowed - - // Get the range of frames - new_frames = Frames.GetRange(startIndex, endIndex - startIndex + 1); - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> Trimmed from {startIndex} to {endIndex} ({new_frames.Count}) - from total {this.Frames.Count}"); - } - break; - case 1: // Trim Bonus replays - // Bonus runs - int bonus_enter_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.START_ZONE_ENTER); - int bonus_exit_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.START_ZONE_EXIT); - int bonus_end_enter_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.END_ZONE_ENTER); - - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> Trimming Bonus Run replay. Last start enter {bonus_enter_index}, last start exit {bonus_exit_index}, end enter {bonus_end_enter_index}"); - - if (bonus_enter_index != -1 && bonus_exit_index != -1 && bonus_end_enter_index != -1) - { - // Try different buffer sizes for start index - int startIndex; - if (bonus_exit_index - (Config.ReplaysPre * 2) >= bonus_enter_index) - startIndex = bonus_exit_index - (Config.ReplaysPre * 2); - else if (bonus_exit_index - Config.ReplaysPre >= bonus_enter_index) - startIndex = bonus_exit_index - Config.ReplaysPre; - else if (bonus_exit_index - (Config.ReplaysPre / 2) >= bonus_enter_index) - startIndex = bonus_exit_index - (Config.ReplaysPre / 2); - else - startIndex = bonus_enter_index; // fallback to minimum allowed - - // Try different buffer sizes for end index - int endIndex; - if (bonus_end_enter_index + (Config.ReplaysPre * 2) < Frames.Count) - endIndex = bonus_end_enter_index + (Config.ReplaysPre * 2); - else if (bonus_end_enter_index + Config.ReplaysPre < Frames.Count) - endIndex = bonus_end_enter_index + Config.ReplaysPre; - else if (bonus_end_enter_index + (Config.ReplaysPre / 2) < Frames.Count) - endIndex = bonus_end_enter_index + (Config.ReplaysPre / 2); - else - // endIndex = Frames.Count - 1; // fallback to maximum allowed - endIndex = bonus_end_enter_index; // fallback to maximum allowed - - // Get the range of frames - new_frames = Frames.GetRange(startIndex, endIndex - startIndex + 1); - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> Trimmed from {startIndex} to {endIndex} ({new_frames.Count}) - from total {this.Frames.Count}"); - } - break; - case 2: // Trim Stage replays - int stage_end_index; - int stage_exit_index; - int stage_enter_index; - - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> Will trim Stage Run replay. Stage {player.Timer.Stage - 1}, available frames {Frames.Count}"); - - // Stage runs - if (lastStage) // Last stage - { - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> Stage replay trimming will use `STAGE_ZONE_X` + `END_ZONE_ENTER`"); - stage_end_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.END_ZONE_ENTER); // Last stage enter (finishing the stage) - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> stage_end_index = {stage_end_index}"); - stage_exit_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == ReplayFrameSituation.STAGE_ZONE_EXIT); // Exiting the previous stage zone (what we are looking for start of the stage run) - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> stage_exit_index = {stage_exit_index}"); - stage_enter_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == ReplayFrameSituation.STAGE_ZONE_ENTER); // Entering the previous stage zone (what we are looking for pre-speed trim) - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> stage_enter_index = {stage_enter_index}"); - } - else if (player.Timer.Stage - 1 > 1) // Not first stage - { - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> Stage replay trimming will use `STAGE_ZONE_X`"); - stage_end_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.STAGE_ZONE_ENTER); // Last stage enter (finishing the stage) - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> stage_end_index = {stage_end_index}"); - stage_exit_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == ReplayFrameSituation.STAGE_ZONE_EXIT); // Exiting the previous stage zone (what we are looking for start of the stage run) - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> stage_exit_index = {stage_exit_index}"); - stage_enter_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == ReplayFrameSituation.STAGE_ZONE_ENTER); // Entering the previous stage zone (what we are looking for pre-speed trim) - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> stage_enter_index = {stage_enter_index}"); - } - else // First stage is always the start of the map so we are looking for START_ZONE_X - { - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> Stage replay trimming will use `START_ZONE_X`"); - stage_end_index = Frames.FindLastIndex(f => f.Situation == ReplayFrameSituation.STAGE_ZONE_ENTER); // Last stage enter (finishing the stage) - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> stage_end_index = {stage_end_index}"); - stage_exit_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == ReplayFrameSituation.START_ZONE_EXIT); // Exiting the previous stage zone (what we are looking for start of the stage run) - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> stage_exit_index = {stage_exit_index}"); - stage_enter_index = Frames.FindLastIndex(stage_end_index - 1, f => f.Situation == ReplayFrameSituation.START_ZONE_ENTER); // Entering the previous stage zone (what we are looking for pre-speed trim) - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> stage_enter_index = {stage_enter_index}"); - } - - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> Trimming Stage Run replay. Stage {player.Timer.Stage - 1} enter {stage_enter_index}, stage exit {stage_exit_index}, stage end {stage_end_index}"); - - if (stage_enter_index != -1 && stage_exit_index != -1 && stage_end_index != -1) - { - // Try different buffer sizes for start index - int startIndex; - if (stage_exit_index - (Config.ReplaysPre * 2) >= stage_enter_index) - startIndex = stage_exit_index - (Config.ReplaysPre * 2); - else if (stage_exit_index - Config.ReplaysPre >= stage_enter_index) - startIndex = stage_exit_index - Config.ReplaysPre; - else if (stage_exit_index - (Config.ReplaysPre / 2) >= stage_enter_index) - startIndex = stage_exit_index - (Config.ReplaysPre / 2); - else - startIndex = stage_enter_index; // fallback to minimum allowed - - // Try different buffer sizes for end index - int endIndex; - if (stage_end_index + (Config.ReplaysPre * 2) < Frames.Count) - endIndex = stage_end_index + (Config.ReplaysPre * 2); - else if (stage_end_index + Config.ReplaysPre < Frames.Count) - endIndex = stage_end_index + Config.ReplaysPre; - else if (stage_end_index + (Config.ReplaysPre / 2) < Frames.Count) - endIndex = stage_end_index + (Config.ReplaysPre / 2); - else - // endIndex = Frames.Count - 1; // fallback to maximum allowed - endIndex = stage_end_index; // fallback to maximum allowed - - // Get the range of frames - new_frames = Frames.GetRange(startIndex, endIndex - startIndex + 1); - Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> Trimmed Stage replay from {startIndex} to {endIndex} ({new_frames.Count}) - from total {this.Frames.Count}"); - } - break; + _logger.LogDebug("This is the last stage, will end on END_ZONE_ENTER."); + endZone = ReplayFrameSituation.END_ZONE_ENTER; + stage += 1; } + } - this.IsSaving = false; - string trimmed = JsonSerializer.Serialize(new_frames); - return Compressor.Compress(trimmed); - } + _logger.LogInformation("[{ClassName}] {MethodName} -> Player is on Stage {Stage} and we are trimming replay for Stage {TrimmingStage}", + nameof(ReplayRecorder), methodName, player.Timer.Stage, stage + ); - public int LastEnterTick(int start_idx = 0) - { - if (start_idx == 0) - start_idx = this.Frames.Count - 1; - for (int i = start_idx; i > 0; i--) + 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) { - 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} -> 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; } - return 0; + + 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 static int CalculateStartIndex(int start_enter, int start_exit, int buffer) + { + if (start_exit - (buffer * 2) >= start_enter) + return start_exit - (buffer * 2); + else if (start_exit - buffer >= start_enter) + return start_exit - buffer; + else if (start_exit - (buffer / 2) >= start_enter) + return start_exit - (buffer / 2); + else + return start_enter; } - public int LastExitTick(int start_idx = 0) + private static int CalculateEndIndex(int end_enter, int totalFrames, int buffer) { - if (start_idx == 0) - start_idx = this.Frames.Count - 1; - for (int i = start_idx; i > 0; i--) + 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) { - 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; + return end_enter + (buffer / 2); } - return 0; + else + { + return end_enter; + } + } + + private List GetTrimmedFrames(int startIndex, int endIndex) + { + 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 a6bcd4c..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 Pos { get; set; } = new Vector(0, 0, 0); - public QAngle Ang { get; set; } = new QAngle(0, 0, 0); - public Vector Vel { get; set; } = new Vector(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 d972cb3..7585a36 100644 --- a/src/ST-UTILS/Config.cs +++ b/src/ST-UTILS/Config.cs @@ -1,26 +1,33 @@ +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 = $"[{ChatColors.DarkBlue}CS2 Surf{ChatColors.Default}]"; // To-do: make configurable - 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 readonly bool ReplaysEnabled = TimerSettings.GetReplaysEnabled(); + public static readonly int ReplaysPre = TimerSettings.GetReplaysPre(); - public static bool ReplaysEnabled => true; - public static int ReplaysPre => 64; // Helper class/methods for configuration loading private static class ConfigLoader { @@ -37,11 +44,31 @@ public static JsonDocument GetConfigDocument(string configPath) } } - public static class API + /// + /// Values from `timer_settings.json` + /// + private static class TimerSettings { - private const string API_CONFIG_PATH = "/csgo/cfg/SurfTimer/api_config.json"; + private const string TIMER_CONFIG_PATH = "/csgo/cfg/SurfTimer/timer_settings.json"; + private static JsonDocument ConfigDocument => + ConfigLoader.GetConfigDocument(TIMER_CONFIG_PATH); + + public static bool GetReplaysEnabled() + { + return ConfigDocument.RootElement.GetProperty("replays_enabled").GetBoolean(); + } + + public static int GetReplaysPre() + { + return ConfigDocument.RootElement.GetProperty("replays_pre").GetInt32(); + } + } - private static JsonDocument ConfigDocument => ConfigLoader.GetConfigDocument(API_CONFIG_PATH); + 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); /// /// Retrieves the `api_url` string from the configuration path @@ -66,29 +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_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 = "/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 = + "/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 = "/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 = + "/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()!; @@ -98,109 +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_RUNS = @" - SELECT MapTimes.*, Player.name - FROM MapTimes - JOIN Player ON MapTimes.player_id = Player.id - WHERE MapTimes.map_id = {0} AND MapTimes.style = {1} AND MapTimes.type = {2} - ORDER BY MapTimes.run_time ASC; - "; // Deprecated - 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, ranked, date_added, last_played) VALUES ('{0}', '{1}', {2}, {3}, {4}, {5}, {5})"; // "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; - "; - } } -} \ No newline at end of file +} diff --git a/src/ST-UTILS/Data/ApiDataAccessService.cs b/src/ST-UTILS/Data/ApiDataAccessService.cs new file mode 100644 index 0000000..e36f8f4 --- /dev/null +++ b/src/ST-UTILS/Data/ApiDataAccessService.cs @@ -0,0 +1,354 @@ +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SurfTimer.Shared.DTO; +using SurfTimer.Shared.Entities; + +namespace SurfTimer.Data +{ + public class ApiDataAccessService : IDataAccessService + { + private readonly ILogger _logger; + + /// + /// Add/load data using API calls. + /// + public ApiDataAccessService() + { + _logger = SurfTimer.ServiceProvider.GetRequiredService>(); + } + + public async Task PingAccessService([CallerMemberName] string 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; + } + + _logger.LogWarning( + "[{ClassName}] {MethodName} -> Unexpected response structure.", + nameof(ApiDataAccessService), + methodName + ); + return false; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> Failed to reach API.", + nameof(ApiDataAccessService), + methodName + ); + return false; + } + } + + /* 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; + } + + 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); + if (apiResult == null) + return null; + + _logger.LogInformation( + "[{ClassName}] {MethodName} -> LoadPersonalBestRunAsync -> Personal Best data", + nameof(ApiDataAccessService), + methodName + ); + + return apiResult; + } + + /* Map.cs */ + public async Task GetMapInfoAsync( + string mapName, + [CallerMemberName] string methodName = "" + ) + { + 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. MapID {MapId}", + nameof(ApiDataAccessService), + methodName, + mapInfo.ID + ); + + return mapInfo; + } + + return null; + } + + public async Task InsertMapInfoAsync( + MapDto mapInfo, + [CallerMemberName] string methodName = "" + ) + { + var postResponse = await ApiMethod.POST( + Config.Api.Endpoints.ENDPOINT_MAP_INSERT_INFO, + mapInfo + ); + + if (postResponse == null || postResponse.Id <= 0) + { + Exception ex = new($"API failed to insert map '{mapInfo.Name}'."); + throw ex; + } + + return postResponse.Id; + } + + public async Task UpdateMapInfoAsync( + MapDto mapInfo, + int mapId, + [CallerMemberName] string methodName = "" + ) + { + var response = await ApiMethod.PUT( + string.Format(Config.Api.Endpoints.ENDPOINT_MAP_UPDATE_INFO, mapId), + mapInfo + ); + if (response == null) + { + Exception ex = new($"API failed to update map '{mapInfo.Name}' (ID {mapId})."); + throw ex; + } + } + + public async Task> GetMapRecordRunsAsync( + int mapId, + [CallerMemberName] string methodName = "" + ) + { + var apiRuns = await ApiMethod.GET>( + string.Format(string.Format(Config.Api.Endpoints.ENDPOINT_MAP_GET_RUNS, mapId)) + ); + + return apiRuns!; + } + + /* PlayerProfile.cs */ + public async Task GetPlayerProfileAsync( + ulong steamId, + [CallerMemberName] string methodName = "" + ) + { + 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 for ProfileID = {ProfileID}", + nameof(ApiDataAccessService), + methodName, + player.ID + ); + + return player; + } + + _logger.LogWarning( + "[{ClassName}] {MethodName} -> GetPlayerProfileAsync -> No PlayerProfile data found for {SteamID}", + nameof(ApiDataAccessService), + methodName, + steamId + ); + return null; + } + + public async Task InsertPlayerProfileAsync( + PlayerProfileDto profile, + [CallerMemberName] string methodName = "" + ) + { + var postResponse = await ApiMethod.POST( + Config.Api.Endpoints.ENDPOINT_PP_INSERT_PROFILE, + profile + ); + + if (postResponse == null || postResponse.Id <= 0) + { + Exception ex = new($"API failed to insert Player Profile for '{profile.Name}'."); + throw ex; + } + + return postResponse.Id; + } + + public async Task UpdatePlayerProfileAsync( + PlayerProfileDto profile, + int playerId, + [CallerMemberName] string methodName = "" + ) + { + var response = await ApiMethod.PUT( + string.Format(Config.Api.Endpoints.ENDPOINT_PP_UPDATE_PROFILE, playerId), + profile + ); + + if (response == null) + { + 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 = "" + ) + { + var apiResponse = await ApiMethod.GET>( + string.Format(Config.Api.Endpoints.ENDPOINT_PS_GET_PLAYER_MAP_DATA, playerId, mapId) + ); + + if (apiResponse == null) + { + Exception ex = new( + $"API failed to GET MapTime entries for PlayerID '{playerId}' and MapID '{mapId}'." + ); + throw ex; + } + + _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( + MapTimeRunDataDto mapTime, + [CallerMemberName] string methodName = "" + ) + { + var postResponse = await ApiMethod.POST( + Config.Api.Endpoints.ENDPOINT_CR_SAVE_MAP_TIME, + mapTime + ); + + if (postResponse == null || postResponse.Inserted <= 0) + { + Exception ex = new( + $"API failed to insert MapTime for Player ID '{mapTime.PlayerID}' on Map ID '{mapTime.MapID}'." + ); + throw ex; + } + + _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 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 new file mode 100644 index 0000000..9bbe2b4 --- /dev/null +++ b/src/ST-UTILS/Data/IDataAccessService.cs @@ -0,0 +1,151 @@ +using System.Runtime.CompilerServices; +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 */ + /// + /// Retrieve Checkpoints table entries for a given run ID (map time). + /// Bonus and Stage runs should NOT have any checkpoints. + /// + /// 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 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. + /// + /// [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, + int type, + int style, + [CallerMemberName] string methodName = "" + ); + + /* Map.cs */ + /// + /// 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( + 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 */ + /// + /// 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( + PlayerProfileDto profile, + [CallerMemberName] string methodName = "" + ); + + /// + /// Updates Player table entry for the player through API or MySQL. + /// + /// Data to update in table + Task UpdatePlayerProfileAsync( + PlayerProfileDto profile, + int playerId, + [CallerMemberName] string methodName = "" + ); + + /* PlayerStats.cs */ + /// + /// 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( + MapTimeRunDataDto mapTime, + [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/MySqlDataAccessService.cs b/src/ST-UTILS/Data/MySqlDataAccessService.cs new file mode 100644 index 0000000..fc6b28c --- /dev/null +++ b/src/ST-UTILS/Data/MySqlDataAccessService.cs @@ -0,0 +1,528 @@ +using System.Runtime.CompilerServices; +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 +{ + public class MySqlDataAccessService : IDataAccessService + { + private readonly ILogger _logger; + + /// + /// Add/load data using MySQL connection and queries. + /// + public MySqlDataAccessService() + { + _logger = SurfTimer.ServiceProvider.GetRequiredService< + ILogger + >(); + } + + public async Task PingAccessService([CallerMemberName] string methodName = "") + { + try + { + var val = await SurfTimer.DB.QueryFirstOrDefaultAsync(Queries.DB_QUERY_PING); + + var reachable = val != 0; + + if (reachable) + { + _logger.LogInformation( + "[{ClassName}] {MethodName} -> PingAccessService -> MySQL is reachable", + nameof(MySqlDataAccessService), + methodName + ); + } + + return reachable; + } + catch (Exception ex) + { + _logger.LogCritical( + ex, + "[{ClassName}] {MethodName} -> PingAccessService -> MySQL is unreachable", + nameof(MySqlDataAccessService), + methodName + ); + return false; + } + } + + /* 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 = "" + ) + { + // 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 }; + } + + // Fetch a single row (or null). + var run = await SurfTimer.DB.QueryFirstOrDefaultAsync(sql, args); + + 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 + ); + return null; + } + + _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 run; + } + + /* Map.cs */ + public async Task GetMapInfoAsync( + string mapName, + [CallerMemberName] string methodName = "" + ) + { + var mapInfo = await SurfTimer.DB.QueryFirstOrDefaultAsync( + Queries.DB_QUERY_MAP_GET_INFO, + new { mapName } + ); + + if (mapInfo is not null) + { + _logger.LogInformation( + "[{ClassName}] {MethodName} -> GetMapInfoAsync -> Found MapInfo data (ID: {ID})", + nameof(MySqlDataAccessService), + methodName, + mapInfo.ID + ); + } + + return mapInfo; + } + + public async Task InsertMapInfoAsync( + MapDto mapInfo, + [CallerMemberName] string methodName = "" + ) + { + 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 (newId <= 0) + { + 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)newId; + } + + public async Task UpdateMapInfoAsync( + MapDto mapInfo, + int mapId, + [CallerMemberName] string methodName = "" + ) + { + 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, + } + ); + + if (rowsUpdated != 1) + { + Exception ex = new( + $"Failed to update map '{mapInfo.Name}' (ID {mapId}) in database. Rows updated: {rowsUpdated}" + ); + + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> UpdateMapInfoAsync -> {ErrorMessage}", + nameof(MySqlDataAccessService), + methodName, + ex.Message + ); + + throw ex; + } + } + + 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 = "" + ) + { + var playerData = await SurfTimer.DB.QueryFirstOrDefaultAsync( + Queries.DB_QUERY_PP_GET_PROFILE, + new { SteamID = steamId } + ); + + return playerData; + } + + public async Task InsertPlayerProfileAsync( + PlayerProfileDto profile, + [CallerMemberName] string methodName = "" + ) + { + int joinDate = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + 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, + } + ); + + if (lastId <= 0) + { + Exception ex = new( + $"Failed to insert new player '{profile.Name}' ({profile.SteamID}). LAST_INSERT_ID() was 0." + ); + + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> InsertPlayerProfileAsync -> {ErrorMessage}", + nameof(MySqlDataAccessService), + methodName, + ex.Message + ); + + throw ex; + } + + return (int)lastId; + } + + public async Task UpdatePlayerProfileAsync( + PlayerProfileDto profile, + int playerId, + [CallerMemberName] string methodName = "" + ) + { + int lastSeen = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + var rowsAffected = await SurfTimer.DB.ExecuteAsync( + Queries.DB_QUERY_PP_UPDATE_PROFILE, + new + { + Country = profile.Country, + LastSeen = lastSeen, + Name = profile.Name, + Id = playerId, + } + ); + + if (rowsAffected != 1) + { + Exception ex = new( + $"Failed to update player '{profile.Name}' ({profile.SteamID})." + ); + + _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( + MapTimeRunDataDto mapTime, + [CallerMemberName] string methodName = "" + ) + { + // 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 (mapTimeId <= 0) + { + Exception ex = new( + $"Failed to insert map time for PlayerId {mapTime.PlayerID}. LAST_INSERT_ID() was 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 }) + { + 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 + { + 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, + } + ); + + if (affectedRows <= 0) + { + Exception ex = new( + $"Failed to update map time for MapTimeId {mapTimeId}. affectedRows was {affectedRows}." + ); + + _logger.LogError( + ex, + "[{ClassName}] {MethodName} -> UpdateMapTimeAsync -> {ErrorMessage}", + nameof(MySqlDataAccessService), + methodName, + ex.Message + ); + + throw ex; + } + + _logger.LogInformation( + "[{ClassName}] {MethodName} -> UpdateMapTimeAsync -> Updated MapTimeId {MapTimeId} with {AffectedRows} affected rows.", + nameof(MySqlDataAccessService), + methodName, + mapTimeId, + affectedRows + ); + + // 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 new file mode 100644 index 0000000..1127fec --- /dev/null +++ b/src/ST-UTILS/Extensions.cs @@ -0,0 +1,161 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Entities.Constants; +using CounterStrikeSharp.API.Modules.Memory; +using CounterStrikeSharp.API.Modules.Utils; + + +namespace SurfTimer; + +unsafe static class Extensions +{ + public static void Teleport(this CBaseEntity entity, VectorT? position = null, QAngleT? angles = null, VectorT? velocity = null) + { + Guard.IsValidEntity(entity); + + void* pPos = null, pAng = null, pVel = null; + + // Structs are stored on the stack, GC should not break pointers. + + if (position.HasValue) + { + var pos = position.Value; // Remove nullable wrapper + pPos = &pos; + } + + if (angles.HasValue) + { + var ang = angles.Value; + pAng = ∠ + } + + if (velocity.HasValue) + { + var vel = velocity.Value; + pVel = &vel; + } + + VirtualFunction.CreateVoid(entity.Handle, GameData.GetOffset("CBaseEntity_Teleport"))(entity.Handle, (nint)pPos, + (nint)pAng, (nint)pVel); + } + + 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 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; + 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 new file mode 100644 index 0000000..ccd77e8 --- /dev/null +++ b/src/ST-UTILS/Injection.cs @@ -0,0 +1,73 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; +using SurfTimer.Data; + +namespace SurfTimer; + +public class Injection : IPluginServiceCollection +{ + private static readonly string LogDirectory = + $"{Server.GameDirectory}/csgo/addons/counterstrikesharp/logs"; + + 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); + + // Configure Serilog + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.Console() + .WriteTo.File( + path: filePath, + restrictedToMinimumLevel: LogEventLevel.Verbose, + rollingInterval: RollingInterval.Day + ) + .CreateLogger(); + + // Show the full path to the log file + Console.WriteLine($"[SurfTimer] Logging to file: {filePath}"); + Log.Information("[SurfTimer] Logging to file: {LogFile}", filePath); + + // Register Serilog as a logging provider for Microsoft.Extensions.Logging + serviceCollection.AddLogging(builder => + { + builder.ClearProviders(); + builder.AddSerilog(dispose: true); + }); + + // Register Dependencies + 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 + + serviceCollection.AddScoped(provider => + Config.Api.GetApiOnly() ? new ApiDataAccessService() : new MySqlDataAccessService() + ); + } +} + +/// +/// 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 void Init(IStringLocalizer localizer) + { + Localizer = localizer; + } +} 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/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/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 d0517a9..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,17 +33,35 @@ 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(120)] +[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 public override string ModuleName => $"CS2 {Config.PluginName}"; public override string ModuleVersion => "DEV-1"; @@ -43,33 +69,50 @@ public partial class SurfTimer : BasePlugin 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" - + $"[CS2 Surf] {Config.PluginName} {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" - ))); - - 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(); @@ -86,31 +129,74 @@ public HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info) ConVarHelper.RemoveCheatFlagFromConVar("bot_zombie"); Server.ExecuteCommand("execifexists SurfTimer/server_settings.cfg"); - Console.WriteLine("[CS2 Surf] Executed configuration: server_settings.cfg"); + _logger.LogTrace( + "[{Prefix}] Executed configuration: server_settings.cfg", + Config.PluginName + ); return HookResult.Continue; } /* ========== PLUGIN LOAD ========== */ public override void Load(bool hotReload) { - // Check if we have connected to the Database - if (DB != null) + LocalizationService.Init(Localizer); + + // === 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) { - Console.WriteLine("[CS2 Surf] 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 { - Console.WriteLine($"[CS2 Surf] Error connecting to the database."); - // To-do: Abort plugin loading + _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")}" + ); + throw exception; } - Console.WriteLine(String.Format(" ____________ ____ ___\n" - + " / ___/ __/_ | / __/_ ______/ _/\n" - + "/ /___\\ \\/ __/ _\\ \\/ // / __/ _/ \n" - + "\\___/___/____/ /___/\\_,_/_/ /_/\n" - + $"[CS2 Surf] {Config.PluginName} plugin loaded. Version: {ModuleVersion}\n" - + $"[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\n" - )); + _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 RegisterListener(OnMapStart); @@ -119,7 +205,6 @@ public override void Load(bool hotReload) // 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 82404c6..0000000 --- a/src/SurfTimer.csproj +++ /dev/null @@ -1,21 +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