diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index bd7d6ed..b9590e3 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -19,7 +19,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/README.md b/README.md index 4663617..1741b47 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,175 @@ -# PLEASE DO NOT USE THIS, IT IS NOT COMPLETE AND IS AN ACTIVE WORK-IN-PROGRESS. ISSUES HAVE BEEN DISABLED FOR THIS REASON. +# 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 Core plugin for CS2 Surf Servers. This project is aimed to be fully open-source with the goal of uniting all of CS2 surf towards building the game mode. +
+
+ Center HUD Speedometer +

Different time formatting is available in the code base but not implemented for players to change it themselves. Refer to PlayerTimer.TimeFormatStyle in codebase

+ +
-# Goals +
+ Replays +

Currently only accessible through the !spec command and cycling the players. Different time formatting is available in the code base but not implemented for players to change it themselves. Refer to PlayerTimer.TimeFormatStyle in codebase

+

Replays are saved for all types of runs Map/Stage/Bonus (and future Styles) regardless if they are a World Record or just a Personal Best. No functionality is implemented for replaying PB replays yet, feel free to add and Pull Request it

+ +
+ + +
+ Chat Messages + +
+ +
+ Player Commands +

We recommend making binds using the Console commands, chat commands may flood the server and not always work.

+ + +
+
+ +# 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 - - [ ] MySQL database schema ([W.I.P Design Diagram](https://dbdiagram.io/d/CS2Surf-Timer-DB-Schema-6560b76b3be1495787ace4d2)) + - [X] MySQL database schema ([Design Diagram](https://dbdiagram.io/d/CS2Surf-Timer-DB-Schema-6560b76b3be1495787ace4d2)) - [ ] Plugin auto-create tables for easier setup? - [X] Base database class implementation -- [ ] Maps +- [X] Maps - [X] Implement map info object (DB) - - [ ] Zoning + - [X] Zoning - [X] Hook zones from map triggers - [X] Map start/end zones - [X] Stage zones @@ -23,45 +178,46 @@ Bold & Italics = being worked on. - [X] Support for stages/checkpoints - [X] Hook to their start/end zones - [X] Save/Compare checkpoint times - - [ ] Save Stage times + - [X] Save Stage times - [X] Support for bonuses - [X] Hook to their start/end zones - - [ ] Save Bonus times + - [X] Save Bonus times - [X] Start/End trigger touch hooks - [X] Load zone information automatically from standardised triggers: https://github.com/CS2Surf/Timer/wiki/CS2-Surf-Mapping - - [X] _**Support for stages (`/rs`, teleporting with `/s`)**_ - - [ ] _**Support for bonuses (`/rs`, teleporting with `/b #`)**_ - - [ ] _**Start/End touch hooks implemented for all zones**_ + - [X] Support for stages (`/rs`, teleporting with `/s`) + - [X] Support for bonuses (`/rs`, teleporting with `/b #`) + - [X] Start/End touch hooks implemented for all zones - [ ] Surf configs - [X] Server settings configuration - [ ] Plugin configuration - [X] Database configuration -- [ ] Timing +- [X] Timing - [X] Base timer class implementation - [X] Base timer HUD implementation - [X] Prespeed measurement and display - - [ ] Save/load times + - [X] Save/load times - [x] Map times - [x] Checkpoint times - - [ ] Stage times - - [ ] Bonus times + - [X] Stage times + - [X] Bonus times - [X] Practice Mode implementation - [ ] Announce records to Discord - [ ] Stretch goal: sub-tick timing - [ ] Player Data - [X] Base player class - - [ ] **_Player stat classes_** + - [ ] Player stat classes - [ ] Profile implementation (DB) - [ ] Points/Skill Groups (DB) - [ ] Player settings (DB) -- [x] Replays - Not tracking Stage/Bonus times but Replay functionality for them is there +- [x] Replays - [x] Personal Best - [x] Map Record - - [ ] Stage Record - - [ ] Bonus Record + - [X] Stage Record + - [X] Bonus Record - [x] World Record - [X] Map Record - - [ ] Stage Record - - [ ] Bonus Record + - [X] Stage Record + - [X] Bonus Record - [ ] Style implementation (SW, HSW, BW) - [ ] Paint (?) +- [ ] API Integration (Repo can be found [here](https://github.com/CS2Surf/CS2-Surf-API)) diff --git a/TODO b/TODO new file mode 100644 index 0000000..58070b4 --- /dev/null +++ b/TODO @@ -0,0 +1,65 @@ +- 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 + +- Double check the Start/End velocities for End Zone,Checkpoints, Stages, Bonuses + + Maps + + Checkpoints + - Stages + - 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 + +- Try and determine Left/Right side for Bonuses (surf_ace) +- Try and determine Left/Right side for Maps +- Try and determine Left/Right side for Stages +# Done ++ Hibernation fucks up the creation of `CurrentMap`, if nobody has joined the first map it was on and you change to another map, server segfaults. I guess a query is being held?? :think: + + Solution is to use `Server.NextWorldUpdate` as hibernation stops all `Timer`s :peepoHappy: ++ Introduce `async` for queries and some methods ++ Make Database spawn a new connection for each query to avoid `Connection in use` error + + Add notes to apply `using` blocks for certain methods for proper operation ++ Move **PlayerProfile** queries to the respective class ++ Move **Map** queries to the respective class ++ Total Completions for a map are wrong in HUD ++ Add `Situation` to each `ReplayFrame` and load it properly + + Fix `STAGE_EXIT_ZONE` situations - not triggered (logic in `TriggerEndTouch` is most likely bad) ++ Fix Map Replays + + Reload after setting a new WR + + Load if no replay existed before a new run + + Use the `ReplayFrameSituation` for determining where to cut replays + + Do not cut replay as soon as we enter end zone (determined by the AddTimer amount for `SaveMapTime`) ++ Double check the Checkpoints comparison messages + + 1st run: [CS2 Surf] CP [1]: 02.468 (1083) [PB: N/A (N/A) | WR: N/A (N/A)] + + 2nd run: [CS2 Surf] CP [1]: 02.421 (1128) [PB: +02.390 (+46) | WR: +02.390 (+46)] ++ Fix Stage replays + + Track the times even during Map Run on staged maps + + Include the pre-strafe + + Load all of them instead of overwriting and cycle (maybe make StageWR a Dictionary) + + Reload them after setting a new Stage WR + + Load if no replay existed before + + Use the `ReplayFrameSituation` for determining where to cut replays + + Save stage replays when `IsStageMode` is enabled + + Use seperate method + + Save the correct amount of `Ticks` for stage runs during map run + + Use seperate method + + Save the last stage time - triggered in `END_ZONE_ENTER` + + `IsStageMode` + + Use seperate method + + Map run + + Use seperate method + + When loading new stage replays the cycling of them does not work ++ First ever map run does not save Checkpoints, check if we wait to retrieve the MapTime_ID before trying to insert Checkpoints ++ Trim different type of replays based on situations + + Map Replays + + Stage Replays during Map runs + + Stage Replays in `IsStageMode` + + Bonus Replays ++ Replay bots are not being spawned on `Linear` maps + + Needs the CS2Fixes NAV mesh patch to work with our implementation \ No newline at end of file diff --git a/src/ST-API/Api.cs b/src/ST-API/Api.cs new file mode 100644 index 0000000..78a9fbe --- /dev/null +++ b/src/ST-API/Api.cs @@ -0,0 +1,41 @@ +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 new file mode 100644 index 0000000..82d3f73 --- /dev/null +++ b/src/ST-API/Comms.cs @@ -0,0 +1,85 @@ +using System.Net.Http.Json; + +namespace SurfTimer; + +internal class ApiMethod +{ + private ApiMethod() { } + + private static readonly HttpClient _client = new(); + private static readonly string base_addr = Config.ApiUrl; + + public static async Task GET(string url) + { + var uri = new Uri(base_addr + url); + +#if DEBUG + Console.WriteLine($"======= CS2 Surf DEBUG >> public static async Task GET -> BASE ADDR: {base_addr} | ENDPOINT: {url} | FULL: {uri.ToString()}"); +#endif + + using var response = await _client.GetAsync(uri); + + try + { + System.Console.WriteLine($"[API] GET {url} => {response.StatusCode}"); + if (response.StatusCode != System.Net.HttpStatusCode.OK) + { + Exception exception = new Exception("[API] GET - No data found"); + throw exception; + } + + return await response.Content.ReadFromJsonAsync(); + } + catch + { + Console.WriteLine("HTTP Response was invalid or could not be deserialised."); + return default; + } + } + + public static async Task POST(string url, T body) + { + 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); + + try + { + System.Console.WriteLine($"[API] POST {url} => {response.StatusCode}"); + response.EnsureSuccessStatusCode(); // BAD BAD BAD + return await response.Content.ReadFromJsonAsync(); + } + catch + { + Console.WriteLine("HTTP Response was invalid or could not be deserialised."); + return default; + } + } + + public static async Task PUT(string url, T body) + { + 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); + + try + { + System.Console.WriteLine($"[API] PUT {url} => {response.StatusCode}"); + response.EnsureSuccessStatusCode(); // BAD BAD BAD + return await response.Content.ReadFromJsonAsync(); + } + catch + { + Console.WriteLine("HTTP Response was invalid or could not be deserialised."); + return default; + } + } +} \ No newline at end of file diff --git a/src/ST-UTILS/ConVar.cs b/src/ST-API/ConVar.cs similarity index 91% rename from src/ST-UTILS/ConVar.cs rename to src/ST-API/ConVar.cs index 55206ba..c19eefc 100644 --- a/src/ST-UTILS/ConVar.cs +++ b/src/ST-API/ConVar.cs @@ -2,7 +2,7 @@ namespace SurfTimer; -internal class ConVarHelper +internal static class ConVarHelper { public static void RemoveCheatFlagFromConVar(string cv_name) { diff --git a/src/ST-UTILS/Compression.cs b/src/ST-API/JsonConverters.cs similarity index 83% rename from src/ST-UTILS/Compression.cs rename to src/ST-API/JsonConverters.cs index 25ff2b2..0e50238 100644 --- a/src/ST-UTILS/Compression.cs +++ b/src/ST-API/JsonConverters.cs @@ -6,6 +6,28 @@ namespace SurfTimer; +internal static class JsonSelp +{ + public static Dictionary ConstructJsonDictFromString(string str) + { + using (JsonDocument document = JsonDocument.Parse(str)) + { + // Access the root element + JsonElement root = document.RootElement; + + // Create a dictionary to store the parsed JSON data + Dictionary dictionary = new Dictionary(); + + foreach (JsonProperty property in root.EnumerateObject()) + { + dictionary[property.Name] = property.Value.ToString(); + } + + return dictionary; + } + } +} + internal class VectorConverter : JsonConverter { public override Vector Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -102,7 +124,7 @@ public override void Write(Utf8JsonWriter writer, QAngle value, JsonSerializerOp } } -internal class Compressor +internal static class Compressor { public static string Decompress(string input) { @@ -126,22 +148,20 @@ public static byte[] Decompress(byte[] input) source.Read(lengthBytes, 0, 4); var length = BitConverter.ToInt32(lengthBytes, 0); - using (var decompressionStream = new GZipStream(source, - CompressionMode.Decompress)) + 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) { - totalRead += bytesRead; + totalRead += bytesRead; } - return result; } } } - public static byte[] Compress(byte[] input) + public static byte[] Compress(byte[] input) { using (var result = new MemoryStream()) { @@ -158,4 +178,9 @@ public static byte[] Compress(byte[] input) return result.ToArray(); } } + + internal static string Decompress(byte v) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/ST-UTILS/Schema.cs b/src/ST-API/Schema.cs similarity index 100% rename from src/ST-UTILS/Schema.cs rename to src/ST-API/Schema.cs diff --git a/src/ST-API/Structures.cs b/src/ST-API/Structures.cs new file mode 100644 index 0000000..2b638df --- /dev/null +++ b/src/ST-API/Structures.cs @@ -0,0 +1,87 @@ +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 4bebc09..671a15e 100644 --- a/src/ST-Commands/MapCommands.cs +++ b/src/ST-Commands/MapCommands.cs @@ -19,11 +19,23 @@ 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}"; + if (CurrentMap.Stages > 1) - player.PrintToChat($"{PluginPrefix} {CurrentMap.Name} - {ChatColors.Green}Tier {CurrentMap.Tier}{ChatColors.Default} - Staged {ChatColors.Yellow}{CurrentMap.Stages} Stages{ChatColors.Default}"); + { + msg = string.Concat(msg, " - ", $"Stages {ChatColors.Yellow}{CurrentMap.Stages}{ChatColors.Default}"); + } else - player.PrintToChat($"{PluginPrefix} {CurrentMap.Name} - {ChatColors.Green}Tier {CurrentMap.Tier}{ChatColors.Default} - Linear {ChatColors.Yellow}{CurrentMap.Checkpoints} Checkpoints{ChatColors.Default}"); - return; + { + msg = string.Concat(msg, " - ", $"Linear {ChatColors.Yellow}{CurrentMap.TotalCheckpoints} Checkpoints{ChatColors.Default}"); + } + + if (CurrentMap.Bonuses > 0) + { + msg = string.Concat(msg, " - ", $"Bonuses {ChatColors.Yellow}{CurrentMap.Bonuses}"); + } + + player.PrintToChat(msg); } [ConsoleCommand("css_triggers", "List all valid zone triggers in the map.")] diff --git a/src/ST-Commands/PlayerCommands.cs b/src/ST-Commands/PlayerCommands.cs index 3e51527..3f6bb58 100644 --- a/src/ST-Commands/PlayerCommands.cs +++ b/src/ST-Commands/PlayerCommands.cs @@ -1,7 +1,6 @@ using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Commands; -using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Modules.Utils; @@ -13,75 +12,165 @@ public partial class SurfTimer [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void PlayerReset(CCSPlayerController? player, CommandInfo command) { - if (player == null) + if (player == null || player.Team == CsTeam.Spectator || player.Team == CsTeam.None) return; + Player oPlayer = playerList[player.UserId ?? 0]; + if (oPlayer.ReplayRecorder.IsSaving) + { + player.PrintToChat($"{Config.PluginPrefix} Please wait for your run to be saved before resetting."); + 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))); - return; } [ConsoleCommand("css_rs", "Reset back to the start of the stage or bonus you're in.")] [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void PlayerResetStage(CCSPlayerController? player, CommandInfo command) { - if (player == null) + if (player == null || player.Team == CsTeam.Spectator || player.Team == CsTeam.None) return; - // To-do: players[userid].Timer.Reset() -> teleport player - Player SurfPlayer = playerList[player.UserId ?? 0]; - if (SurfPlayer.Timer.Stage != 0 && CurrentMap.StageStartZone[SurfPlayer.Timer.Stage] != new Vector(0, 0, 0)) - Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StageStartZone[SurfPlayer.Timer.Stage], CurrentMap.StageStartZoneAngles[SurfPlayer.Timer.Stage], new Vector(0, 0, 0))); - else // Reset back to map start - Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StartZone, new QAngle(0, 0, 0), new Vector(0, 0, 0))); - return; + Player oPlayer = playerList[player.UserId ?? 0]; + if (oPlayer.ReplayRecorder.IsSaving) + { + player.PrintToChat($"{Config.PluginPrefix} Please wait for your run to be saved before resetting."); + 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))); + else // Reset back to map start + Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StartZone, new QAngle(0, 0, 0), new Vector(0, 0, 0))); + } + + 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))); + else // Reset back to map start + Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StartZone, new QAngle(0, 0, 0), new Vector(0, 0, 0))); + } } [ConsoleCommand("css_s", "Teleport to a stage")] + [ConsoleCommand("css_stage", "Teleport to a stage")] [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] public void PlayerGoToStage(CCSPlayerController? player, CommandInfo command) { - if (player == null) + if (player == null || player.Team == CsTeam.Spectator || player.Team == CsTeam.None) return; - int stage = Int32.Parse(command.ArgByIndex(1)) - 1; - if (stage > CurrentMap.Stages - 1 && CurrentMap.Stages > 0) - stage = CurrentMap.Stages - 1; + int stage; + try + { + stage = Int32.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 (command.ArgCount < 2 || stage <= 0) { - #if DEBUG - player.PrintToChat($"CS2 Surf DEBUG >> css_s >> Arg#: {command.ArgCount} >> Args: {Int32.Parse(command.ArgByIndex(1))}"); - #endif +#if DEBUG + player.PrintToChat($"CS2 Surf DEBUG >> css_stage >> Arg#: {command.ArgCount} >> Args: {Int32.Parse(command.ArgByIndex(1))}"); +#endif - player.PrintToChat($"{PluginPrefix} {ChatColors.Red}Invalid arguments. Usage: {ChatColors.Green}!s "); + player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Invalid arguments. Usage: {ChatColors.Green}!s "); return; } + else if (CurrentMap.Stages <= 0) { - player.PrintToChat($"{PluginPrefix} {ChatColors.Red}This map has no stages."); + player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}This map has no stages."); + return; + } + + else if (stage > CurrentMap.Stages) + { + player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Invalid stage provided, this map has {ChatColors.Green}{CurrentMap.Stages} stages."); return; } if (CurrentMap.StageStartZone[stage] != new Vector(0, 0, 0)) { - if (stage == 0) + playerList[player.UserId ?? 0].Timer.Reset(); + + if (stage == 1) + { Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StartZone, CurrentMap.StartZoneAngles, new Vector(0, 0, 0))); + } 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))); - - playerList[player.UserId ?? 0].Timer.Reset(); - playerList[player.UserId ?? 0].Timer.IsStageMode = true; + 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($"{PluginPrefix} {ChatColors.Red}Invalid stage provided. Usage: {ChatColors.Green}!s "); + player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Invalid stage provided. Usage: {ChatColors.Green}!s "); + } + + [ConsoleCommand("css_b", "Teleport to a bonus")] + [ConsoleCommand("css_bonus", "Teleport to a bonus")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] + public void PlayerGoToBonus(CCSPlayerController? player, CommandInfo command) + { + if (player == null || player.Team == CsTeam.Spectator || player.Team == CsTeam.None) + return; + + int bonus; + + // Check for argument count + if (command.ArgCount < 2) + { + 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)); + + if (CurrentMap.Bonuses <= 0) + { + player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}This map has no bonuses."); + return; + } + + else if (bonus > CurrentMap.Bonuses) + { + player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Invalid bonus provided, this map has {ChatColors.Green}{CurrentMap.Bonuses} bonuses."); + return; + } + + if (CurrentMap.BonusStartZone[bonus] != new Vector(0, 0, 0)) + { + 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))); + } + + else + player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Invalid bonus provided. Usage: {ChatColors.Green}!bonus "); } [ConsoleCommand("css_spec", "Moves a player automaticlly into spectator mode")] @@ -93,85 +182,251 @@ public void MovePlayerToSpectator(CCSPlayerController? player, CommandInfo comma player.ChangeTeam(CsTeam.Spectator); } + [ConsoleCommand("css_rank", "Show the current rank of the player for the style they are in")] + [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] + 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]}"); + } + + [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}"); + // } + } + /* ######################### - Reaplay Commands + Replay Commands ######################### */ [ConsoleCommand("css_replaybotpause", "Pause the replay bot playback")] [ConsoleCommand("css_rbpause", "Pause the replay bot playback")] public void PauseReplay(CCSPlayerController? player, CommandInfo command) { - if(player == null || player.Team != CsTeam.Spectator) + if (player == null || player.Team != CsTeam.Spectator) return; - foreach(ReplayPlayer rb in CurrentMap.ReplayBots) + foreach (ReplayPlayer rb in CurrentMap.ReplayManager.CustomReplays) { - if(!rb.IsPlayable || !rb.IsPlaying || !playerList[player.UserId ?? 0].IsSpectating(rb.Controller!)) + if (!rb.IsPlayable || !rb.IsPlaying || !playerList[player.UserId ?? 0].IsSpectating(rb.Controller!)) continue; - + rb.Pause(); } } - [ConsoleCommand("css_replaybotflip", "Flips the replay bot between Forward/Backward playback")] - [ConsoleCommand("css_rbflip", "Flips the replay bot between Forward/Backward playback")] - public void ReverseReplay(CCSPlayerController? player, CommandInfo command) + [ConsoleCommand("css_rbplay", "Start all replays from the start")] + public void PlayReplay(CCSPlayerController? player, CommandInfo command) { - if(player == null || player.Team != CsTeam.Spectator) + if (player == null || player.Team != CsTeam.Spectator) return; - foreach(ReplayPlayer rb in CurrentMap.ReplayBots) + Player oPlayer = playerList[player.UserId ?? 0]; + CurrentMap.ReplayManager.MapWR.ResetReplay(); + CurrentMap.ReplayManager.MapWR.Start(); + + CurrentMap.ReplayManager.StageWR?.ResetReplay(); + CurrentMap.ReplayManager.StageWR?.Start(); + + foreach (ReplayPlayer rb in CurrentMap.ReplayManager.CustomReplays) { - if(!rb.IsPlayable || !rb.IsPlaying || !playerList[player.UserId ?? 0].IsSpectating(rb.Controller!)) + if (!rb.IsPlayable || !rb.IsPlaying || !oPlayer.IsSpectating(rb.Controller!)) continue; - - rb.FrameTickIncrement *= -1; + + rb.Start(); } } - [ConsoleCommand("css_pbreplay", "Allows for replay of player's PB")] - public void PbReplay(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")] + public void ReverseReplay(CCSPlayerController? player, CommandInfo command) { - if(player == null) + if (player == null || player.Team != CsTeam.Spectator) return; - int maptime_id = playerList[player!.UserId ?? 0].Stats.PB[playerList[player.UserId ?? 0].Timer.Style].ID; - if (command.ArgCount > 1) + foreach (ReplayPlayer rb in CurrentMap.ReplayManager.CustomReplays) { - try - { - maptime_id = int.Parse(command.ArgByIndex(1)); - } - catch {} - } + if (!rb.IsPlayable || !rb.IsPlaying || !playerList[player.UserId ?? 0].IsSpectating(rb.Controller!)) + continue; - if(maptime_id == -1 || !CurrentMap.ConnectedMapTimes.Contains(maptime_id)) - { - player.PrintToChat($"{PluginPrefix} {ChatColors.Red}No time was found"); - return; - } - - for(int i = 0; i < CurrentMap.ReplayBots.Count; i++) - { - if(CurrentMap.ReplayBots[i].Stat_MapTimeID == maptime_id) - { - player.PrintToChat($"{PluginPrefix} {ChatColors.Red}A bot of this run already playing"); - return; - } + rb.FrameTickIncrement *= -1; } - - CurrentMap.ReplayBots = CurrentMap.ReplayBots.Prepend(new ReplayPlayer() { - Stat_MapTimeID = maptime_id, - Stat_Prefix = "PB" - }).ToList(); - - Server.NextFrame(() => { - Server.ExecuteCommand($"bot_quota {CurrentMap.ReplayBots.Count}"); - }); } - /* + // [ConsoleCommand("css_pbreplay", "Allows for replay of player's PB")] + // public void PbReplay(CCSPlayerController? player, CommandInfo command) + // { + // if(player == null) + // return; + + // int maptime_id = playerList[player!.UserId ?? 0].Stats.PB[playerList[player.UserId ?? 0].Timer.Style].ID; + // if (command.ArgCount > 1) + // { + // try + // { + // maptime_id = int.Parse(command.ArgByIndex(1)); + // } + // catch {} + // } + + // if(maptime_id == -1 || !CurrentMap.ConnectedMapTimes.Contains(maptime_id)) + // { + // player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}No time was found"); + // return; + // } + + // for(int i = 0; i < CurrentMap.ReplayBots.Count; i++) + // { + // if(CurrentMap.ReplayBots[i].MapTimeID == maptime_id) + // { + // player.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}A bot of this run already playing"); + // return; + // } + // } + + // CurrentMap.ReplayBots = CurrentMap.ReplayBots.Prepend(new ReplayPlayer() { + // Stat_MapTimeID = maptime_id, + // Stat_Prefix = "PB" + // }).ToList(); + + // Server.NextFrame(() => { + // Server.ExecuteCommand($"bot_quota {CurrentMap.ReplayBots.Count}"); + // }); + // } + + /* ######################## Saveloc Commands ######################## @@ -179,13 +434,13 @@ Saveloc Commands [ConsoleCommand("css_saveloc", "Save current player location to be practiced")] public void SavePlayerLocation(CCSPlayerController? player, CommandInfo command) { - if(player == null || !player.PawnIsAlive || !playerList.ContainsKey(player.UserId ?? 0)) + if (player == null || !player.PawnIsAlive || !playerList.ContainsKey(player.UserId ?? 0)) return; Player p = playerList[player.UserId ?? 0]; if (!p.Timer.IsRunning) { - p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}Cannot save location while not in run"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Cannot save location while not in run"); return; } @@ -193,74 +448,80 @@ public void SavePlayerLocation(CCSPlayerController? player, CommandInfo command) var player_angle = p.Controller.PlayerPawn.Value!.EyeAngles; var player_velocity = p.Controller.PlayerPawn.Value!.AbsVelocity; - p.SavedLocations.Add(new SavelocFrame { + 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), Tick = p.Timer.Ticks }); - p.CurrentSavedLocation = p.SavedLocations.Count-1; + p.CurrentSavedLocation = p.SavedLocations.Count - 1; - p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Green}Saved location! {ChatColors.Default} use !tele {p.SavedLocations.Count-1} to teleport to this location"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Green}Saved location! {ChatColors.Default} use !tele {p.SavedLocations.Count - 1} to teleport to this location"); } [ConsoleCommand("css_tele", "Teleport player to current saved location")] public void TeleportPlayerLocation(CCSPlayerController? player, CommandInfo command) { - if(player == null || !player.PawnIsAlive || !playerList.ContainsKey(player.UserId ?? 0)) + if (player == null || !player.PawnIsAlive || !playerList.ContainsKey(player.UserId ?? 0)) return; Player p = playerList[player.UserId ?? 0]; - if(p.SavedLocations.Count == 0) + if (p.SavedLocations.Count == 0) { - p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}No saved locations"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}No saved locations"); return; } - if(!p.Timer.IsRunning) + if (!p.Timer.IsRunning) p.Timer.Start(); if (!p.Timer.IsPracticeMode) { - p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}Timer now on practice"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Timer now on practice"); p.Timer.IsPracticeMode = true; } - if(command.ArgCount > 1) + if (command.ArgCount > 1) try { int tele_n = int.Parse(command.ArgByIndex(1)); if (tele_n < p.SavedLocations.Count) p.CurrentSavedLocation = tele_n; } - catch { } + catch + { + Exception exception = new("sum ting wong"); + throw exception; + } SavelocFrame location = p.SavedLocations[p.CurrentSavedLocation]; - Server.NextFrame(() => { + Server.NextFrame(() => + { p.Controller.PlayerPawn.Value!.Teleport(location.Pos, location.Ang, location.Vel); p.Timer.Ticks = location.Tick; }); - p.Controller.PrintToChat($"{PluginPrefix} Teleported #{p.CurrentSavedLocation}"); + p.Controller.PrintToChat($"{Config.PluginPrefix} Teleported #{p.CurrentSavedLocation}"); } [ConsoleCommand("css_teleprev", "Teleport player to previous saved location")] public void TeleportPlayerLocationPrev(CCSPlayerController? player, CommandInfo command) { - if(player == null || !player.PawnIsAlive || !playerList.ContainsKey(player.UserId ?? 0)) + if (player == null || !player.PawnIsAlive || !playerList.ContainsKey(player.UserId ?? 0)) return; Player p = playerList[player.UserId ?? 0]; - if(p.SavedLocations.Count == 0) + if (p.SavedLocations.Count == 0) { - p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}No saved locations"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}No saved locations"); return; } - if(p.CurrentSavedLocation == 0) + if (p.CurrentSavedLocation == 0) { - p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}Already at first location"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Already at first location"); } else { @@ -269,26 +530,26 @@ public void TeleportPlayerLocationPrev(CCSPlayerController? player, CommandInfo TeleportPlayerLocation(player, command); - p.Controller.PrintToChat($"{PluginPrefix} Teleported #{p.CurrentSavedLocation}"); + p.Controller.PrintToChat($"{Config.PluginPrefix} Teleported #{p.CurrentSavedLocation}"); } [ConsoleCommand("css_telenext", "Teleport player to next saved location")] public void TeleportPlayerLocationNext(CCSPlayerController? player, CommandInfo command) { - if(player == null || !player.PawnIsAlive || !playerList.ContainsKey(player.UserId ?? 0)) + if (player == null || !player.PawnIsAlive || !playerList.ContainsKey(player.UserId ?? 0)) return; Player p = playerList[player.UserId ?? 0]; - if(p.SavedLocations.Count == 0) + if (p.SavedLocations.Count == 0) { - p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}No saved locations"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}No saved locations"); return; } - if(p.CurrentSavedLocation == p.SavedLocations.Count-1) + if (p.CurrentSavedLocation == p.SavedLocations.Count - 1) { - p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}Already at last location"); + p.Controller.PrintToChat($"{Config.PluginPrefix} {ChatColors.Red}Already at last location"); } else { @@ -297,6 +558,6 @@ public void TeleportPlayerLocationNext(CCSPlayerController? player, CommandInfo TeleportPlayerLocation(player, command); - p.Controller.PrintToChat($"{PluginPrefix} Teleported #{p.CurrentSavedLocation}"); + p.Controller.PrintToChat($"{Config.PluginPrefix} Teleported #{p.CurrentSavedLocation}"); } } \ No newline at end of file diff --git a/src/ST-DB/DB.cs b/src/ST-DB/DB.cs index ca58143..1d6eea7 100644 --- a/src/ST-DB/DB.cs +++ b/src/ST-DB/DB.cs @@ -1,108 +1,123 @@ -namespace SurfTimer; +using MySqlConnector; -using System.Runtime.CompilerServices; -using CounterStrikeSharp.API; -using MySqlConnector; // https://dev.mysql.com/doc/connector-net/en/connector-net-connections-string.html +namespace SurfTimer; -// This will have functions for DB access and query sending internal class TimerDatabase { - private readonly MySqlConnection? _db; - private readonly string _connString = string.Empty; + private readonly string _connString; - public TimerDatabase() + public TimerDatabase(string connectionString) { - // Null'd + _connString = connectionString; } - - public TimerDatabase(string host, string database, string user, string password, int port, int timeout) + public void Dispose() { - this._connString = $"server={host};user={user};password={password};database={database};port={port};connect timeout={timeout};"; - this._db = new MySqlConnection(this._connString); - this._db.Open(); + Close(); } public void Close() { - if (this._db != null) - this._db!.Close(); + // Not needed } - public async Task Query(string query) + /// + /// Spawns a new connection to the database. + /// + /// DB Connection + private MySqlConnection GetConnection() { - return await Task.Run(async () => + var connection = new MySqlConnection(_connString); + try { - try - { - if (this._db == null) - { - throw new InvalidOperationException("Database connection is not open."); - } - - MySqlCommand cmd = new(query, this._db); - MySqlDataReader reader = await cmd.ExecuteReaderAsync(); + 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 reader; - } - catch (Exception ex) - { - Console.WriteLine($"Error executing query: {ex.Message}"); - throw; - } - }); + return connection; } - public async Task Write(string query) + /// + /// 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) { - return await Task.Run(async () => + try { - try - { - if (this._db == null) - { - throw new InvalidOperationException("Database connection is not open."); - } - - MySqlCommand cmd = new(query, this._db); - int rowsAffected = await cmd.ExecuteNonQueryAsync(); - - return rowsAffected; - } - catch (Exception ex) - { - Console.WriteLine($"Error executing write operation: {ex.Message}"); - throw; - } - }); + 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; + } } - public async Task Transaction(List commands) + /// + /// 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) { - if (this._db == null) + try { - throw new InvalidOperationException("Database connection is not open."); + using var connection = GetConnection(); + using var cmd = new MySqlCommand(query, connection); + return await cmd.ExecuteNonQueryAsync(); } - - using (var transaction = await this._db.BeginTransactionAsync()) + catch (Exception ex) { - try - { - foreach (var command in commands) - { - using (var cmd = new MySqlCommand(command, this._db, transaction)) - { - await cmd.ExecuteNonQueryAsync(); - } - } + Console.WriteLine($"Error executing write operation {query}: {ex.Message}"); + throw; + } + } - await transaction.CommitAsync(); - } - catch + /// + /// 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) { - await transaction.RollbackAsync(); - throw; // rethrow the exception to be handled by the caller + 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 a4fe6c5..283ce2f 100644 --- a/src/ST-Events/Players.cs +++ b/src/ST-Events/Players.cs @@ -13,34 +13,75 @@ public partial class SurfTimer public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) { var controller = @event.Userid; - if(!controller.IsValid || !controller.IsBot) + if (!controller.IsValid || !controller.IsBot || CurrentMap.ReplayManager.IsControllerConnectedToReplayPlayer(controller)) return HookResult.Continue; - for (int i = 0; i < CurrentMap.ReplayBots.Count; i++) + // Set the controller for the MapWR bot + if (!CurrentMap.ReplayManager!.MapWR.IsPlayable) { - if(CurrentMap.ReplayBots[i].IsPlayable) - continue; - - int repeats = -1; - if(CurrentMap.ReplayBots[i].Stat_Prefix == "PB") - repeats = 3; - - CurrentMap.ReplayBots[i].SetController(controller, repeats); - Server.PrintToChatAll($"{PluginPrefix} {ChatColors.Lime}Loading replay data..."); - AddTimer(2f, () => { - if(!CurrentMap.ReplayBots[i].IsPlayable) - return; - - CurrentMap.ReplayBots[i].Controller!.RemoveWeapons(); - - CurrentMap.ReplayBots[i].LoadReplayData(DB!); - - CurrentMap.ReplayBots[i].Start(); + CurrentMap.ReplayManager.MapWR.SetController(controller, -1); + CurrentMap.ReplayManager.MapWR.LoadReplayData(); + + AddTimer(1.5f, () => + { + CurrentMap.ReplayManager.MapWR.Controller!.RemoveWeapons(); + CurrentMap.ReplayManager.MapWR.Start(); + CurrentMap.ReplayManager.MapWR.FormatBotName(); + }); + + return HookResult.Continue; + } + + // Set the controller for the StageWR bot + if (CurrentMap.ReplayManager.StageWR != null && !CurrentMap.ReplayManager.StageWR.IsPlayable) + { + CurrentMap.ReplayManager.StageWR.SetController(controller, 3); + CurrentMap.ReplayManager.StageWR.LoadReplayData(repeat_count: 3); + + AddTimer(1.5f, () => + { + CurrentMap.ReplayManager.StageWR.Controller!.RemoveWeapons(); + CurrentMap.ReplayManager.StageWR.Start(); + CurrentMap.ReplayManager.StageWR.FormatBotName(); + }); + + return HookResult.Continue; + } + + // Spawn the BonusWR bot + if (CurrentMap.ReplayManager.BonusWR != null && !CurrentMap.ReplayManager.BonusWR.IsPlayable) + { + CurrentMap.ReplayManager.BonusWR.SetController(controller, 3); + CurrentMap.ReplayManager.BonusWR.LoadReplayData(); + + AddTimer(1.5f, () => + { + CurrentMap.ReplayManager.BonusWR.Controller!.RemoveWeapons(); + CurrentMap.ReplayManager.BonusWR.Start(); + CurrentMap.ReplayManager.BonusWR.FormatBotName(); }); - + return HookResult.Continue; } + // // Spawn the CustomReplays bot (for PB replays?) - T + // CurrentMap.ReplayManager.CustomReplays.ForEach(replay => + // { + // if (!replay.IsPlayable) + // { + // replay.SetController(controller, 3); + // replay.LoadReplayData(); + + // AddTimer(1.5f, () => { + // replay.Controller!.RemoveWeapons(); + // replay.Start(); + // replay.FormatBotName(); + // }); + + // return; + // } + // }); + return HookResult.Continue; } @@ -48,118 +89,46 @@ public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) public HookResult OnPlayerConnectFull(EventPlayerConnectFull @event, GameEventInfo info) { var player = @event.Userid; - #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> OnPlayerConnect -> {player.PlayerName} / {player.UserId} / {player.SteamID}"); - Console.WriteLine($"CS2 Surf DEBUG >> OnPlayerConnect -> {player.PlayerName} / {player.UserId} / Bot Diff: {player.PawnBotDifficulty}"); - #endif - if (player.IsBot || !player.IsValid) + string name = player.PlayerName; + string country = "XX"; + + // GeoIP + // Check if the IP is private before attempting GeoIP lookup + string ipAddress = player.IpAddress!.Split(":")[0]; + if (!IsPrivateIP(ipAddress)) { - return HookResult.Continue; + DatabaseReader geoipDB = new(Config.PluginPath + "data/GeoIP/GeoLite2-Country.mmdb"); + country = geoipDB.Country(ipAddress).Country.IsoCode ?? "XX"; + geoipDB.Dispose(); } else { - int dbID, joinDate, lastSeen, connections; - string name, country; - - // GeoIP - DatabaseReader geoipDB = new DatabaseReader(PluginPath + "data/GeoIP/GeoLite2-Country.mmdb"); - if (geoipDB.Country(player.IpAddress!.Split(":")[0]).Country.IsoCode is not null) - { - country = geoipDB.Country(player.IpAddress!.Split(":")[0]).Country.IsoCode!; - #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> OnPlayerConnect -> GeoIP -> {player.PlayerName} -> {player.IpAddress!.Split(":")[0]} -> {country}"); - #endif - } - else - country = "XX"; - geoipDB.Dispose(); + 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) + { + Exception ex = new("CS2 Surf ERROR >> OnPlayerConnect -> DB object is null, this shouldn't happen."); + throw ex; + } - if (DB == null) - throw new Exception("CS2 Surf ERROR >> OnPlayerConnect -> DB object is null, this shouldnt happen."); + // 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); - // Load player profile data from database (or create an entry if first time connecting) - Task dbTask = DB.Query($"SELECT * FROM `Player` WHERE `steam_id` = {player.SteamID} LIMIT 1;"); - MySqlDataReader playerData = dbTask.Result; - if (playerData.HasRows && playerData.Read()) - { - // Player exists in database - dbID = playerData.GetInt32("id"); - name = playerData.GetString("name"); - if (country == "XX" && playerData.GetString("country") != "XX") - country = playerData.GetString("country"); - joinDate = playerData.GetInt32("join_date"); - lastSeen = playerData.GetInt32("last_seen"); - connections = playerData.GetInt32("connections"); - playerData.Close(); - - #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> OnPlayerConnect -> Returning player {name} ({player.SteamID}) loaded from database with ID {dbID}"); - #endif - } - else - { - playerData.Close(); - // Player does not exist in database - name = player.PlayerName; - joinDate = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - lastSeen = joinDate; - connections = 1; - - // Write new player to database - Task newPlayerTask = DB.Write($@" - INSERT INTO `Player` (`name`, `steam_id`, `country`, `join_date`, `last_seen`, `connections`) - VALUES ('{MySqlHelper.EscapeString(name)}', {player.SteamID}, '{country}', {joinDate}, {lastSeen}, {connections}); - "); - int newPlayerTaskRows = newPlayerTask.Result; - if (newPlayerTaskRows != 1) - throw new Exception($"CS2 Surf ERROR >> OnPlayerConnect -> Failed to write new player to database, this shouldnt happen. Player: {name} ({player.SteamID})"); - newPlayerTask.Dispose(); - - // Get new player's database ID - Task newPlayerDataTask = DB.Query($"SELECT `id` FROM `Player` WHERE `steam_id` = {player.SteamID} LIMIT 1;"); - MySqlDataReader newPlayerData = newPlayerDataTask.Result; - if (newPlayerData.HasRows && newPlayerData.Read()) - { - #if DEBUG - // Iterate through data: - for (int i = 0; i < newPlayerData.FieldCount; i++) - { - Console.WriteLine($"CS2 Surf DEBUG >> OnPlayerConnect -> newPlayerData[{i}] = {newPlayerData.GetValue(i)}"); - } - #endif - dbID = newPlayerData.GetInt32("id"); - } - else - throw new Exception($"CS2 Surf ERROR >> OnPlayerConnect -> Failed to get new player's database ID after writing, this shouldnt happen. Player: {name} ({player.SteamID})"); - newPlayerData.Close(); - - #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> OnPlayerConnect -> New player {name} ({player.SteamID}) added to database with ID {dbID}"); - #endif - } - dbTask.Dispose(); - - // Create Player object and add to playerList - PlayerProfile Profile = new PlayerProfile(dbID, name, player.SteamID, country, joinDate, lastSeen, connections); - playerList[player.UserId ?? 0] = new Player(player, - new CCSPlayer_MovementServices(player.PlayerPawn.Value!.MovementServices!.Handle), - Profile, CurrentMap); - - #if DEBUG - Console.WriteLine($"=================================== SELECT * FROM `MapTimes` WHERE `player_id` = {playerList[player.UserId ?? 0].Profile.ID} AND `map_id` = {CurrentMap.ID};"); - #endif - - // To-do: hardcoded Style value - // Load MapTimes for the player's PB and their Checkpoints - playerList[player.UserId ?? 0].Stats.LoadMapTimesData(playerList[player.UserId ?? 0], DB); // Will reload PB and Checkpoints for the player for all styles - playerList[player.UserId ?? 0].Stats.LoadCheckpointsData(DB); // To-do: This really should go inside `LoadMapTimesData` imo cuz here we hardcoding load for Style 0 - - // Print join messages - Server.PrintToChatAll($"{PluginPrefix} {ChatColors.Green}{player.PlayerName}{ChatColors.Default} has connected from {ChatColors.Lime}{playerList[player.UserId ?? 0].Profile.Country}{ChatColors.Default}."); - Console.WriteLine($"[CS2 Surf] {player.PlayerName} has connected from {playerList[player.UserId ?? 0].Profile.Country}."); - return HookResult.Continue; - } + // 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 + + // 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}."); + + return HookResult.Continue; } [GameEventHandler] // Player Disconnect Event @@ -167,15 +136,30 @@ public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo { var player = @event.Userid; - for (int i = 0; i < CurrentMap.ReplayBots.Count; i++) - if (CurrentMap.ReplayBots[i].IsPlayable && CurrentMap.ReplayBots[i].Controller!.Equals(player) && CurrentMap.ReplayBots[i].Stat_MapTimeID != -1) - CurrentMap.ReplayBots[i].Reset(); + if (player == null) + { + Console.WriteLine($"CS2 Surf ERROR >> OnPlayerDisconnect -> Null ({player == null})"); + return HookResult.Continue; + } + + if (CurrentMap.ReplayManager.MapWR.Controller != null && CurrentMap.ReplayManager.MapWR.Controller.Equals(player) && CurrentMap.ReplayManager.MapWR.MapID != -1) + CurrentMap.ReplayManager.MapWR.Reset(); + + if (CurrentMap.ReplayManager.StageWR != null && CurrentMap.ReplayManager.StageWR.Controller != null && CurrentMap.ReplayManager.StageWR.Controller.Equals(player) && CurrentMap.ReplayManager.StageWR.MapID != -1) + CurrentMap.ReplayManager.StageWR.Reset(); + + if (CurrentMap.ReplayManager.BonusWR != null && CurrentMap.ReplayManager.BonusWR.Controller != null && CurrentMap.ReplayManager.BonusWR.Controller.Equals(player)) + CurrentMap.ReplayManager.BonusWR!.Reset(); + + for (int i = 0; i < CurrentMap.ReplayManager.CustomReplays.Count; i++) + if (CurrentMap.ReplayManager.CustomReplays[i].Controller != null && CurrentMap.ReplayManager.CustomReplays[i].Controller!.Equals(player)) + CurrentMap.ReplayManager.CustomReplays[i].Reset(); + if (player.IsBot || !player.IsValid) { return HookResult.Continue; } - else { if (DB == null) @@ -188,15 +172,7 @@ public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo else { // Update data in Player DB table - Task updatePlayerTask = DB.Write($@" - UPDATE `Player` SET country = '{playerList[player.UserId ?? 0].Profile.Country}', - `last_seen` = {(int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()}, `connections` = `connections` + 1 - WHERE `id` = {playerList[player.UserId ?? 0].Profile.ID} LIMIT 1; - "); - if (updatePlayerTask.Result != 1) - throw new Exception($"CS2 Surf ERROR >> OnPlayerDisconnect -> Failed to update player data in database. Player: {player.PlayerName} ({player.SteamID})"); - // Player disconnection to-do - updatePlayerTask.Dispose(); + playerList[player.UserId ?? 0].Profile.Update_Player_Profile(player.PlayerName).GetAwaiter().GetResult(); // Hold the thread until player data is updated // Remove player data from playerList playerList.Remove(player.UserId ?? 0); @@ -204,4 +180,30 @@ public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo 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 37703e4..2d59bb0 100644 --- a/src/ST-Events/Tick.cs +++ b/src/ST-Events/Tick.cs @@ -18,20 +18,73 @@ public void OnTick() // 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(); - if(cbq != CurrentMap.ReplayBots.Count) + + int replaybot_count = 1 + + (CurrentMap.ReplayManager.StageWR != null ? 1 : 0) + + (CurrentMap.ReplayManager.BonusWR != null ? 1 : 0) + + CurrentMap.ReplayManager.CustomReplays.Count; + + if (cbq != replaybot_count) { - bot_quota.SetValue(CurrentMap.ReplayBots.Count); + 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(); + + // Here we will load the NEXT stage replay from AllStageWR + if (CurrentMap.ReplayManager.StageWR?.RepeatCount == 0) + { + int next_stage; + if (CurrentMap.ReplayManager.AllStageWR[(CurrentMap.ReplayManager.StageWR.Stage % CurrentMap.Stages) + 1][0].MapTimeID == -1) + next_stage = 1; + else + next_stage = (CurrentMap.ReplayManager.StageWR.Stage % CurrentMap.Stages) + 1; + + 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(); + CurrentMap.ReplayManager.StageWR.Start(); } - for(int i = 0; i < CurrentMap!.ReplayBots.Count; i++) + if (CurrentMap.ReplayManager.BonusWR?.RepeatCount == 0) { - CurrentMap.ReplayBots[i].Tick(); - if (CurrentMap.ReplayBots[i].RepeatCount == 0) + int next_bonus; + if (CurrentMap.ReplayManager.AllBonusWR[(CurrentMap.ReplayManager.BonusWR.Stage % CurrentMap.Bonuses) + 1][0].MapTimeID == -1) + next_bonus = 1; + else + next_bonus = (CurrentMap.ReplayManager.BonusWR.Stage % CurrentMap.Bonuses) + 1; + + 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(); + CurrentMap.ReplayManager.BonusWR.Start(); + } + + for (int i = 0; i < CurrentMap.ReplayManager.CustomReplays.Count; i++) + { + if (CurrentMap.ReplayManager.CustomReplays[i].MapID != CurrentMap.ID) + CurrentMap.ReplayManager.CustomReplays[i].MapID = CurrentMap.ID; + + CurrentMap.ReplayManager.CustomReplays[i].Tick(); + if (CurrentMap.ReplayManager.CustomReplays[i].RepeatCount == 0) + { CurrentMap.KickReplayBot(i); + } } } } \ No newline at end of file diff --git a/src/ST-Events/TriggerEndTouch.cs b/src/ST-Events/TriggerEndTouch.cs index 65ddfc4..e685bd7 100644 --- a/src/ST-Events/TriggerEndTouch.cs +++ b/src/ST-Events/TriggerEndTouch.cs @@ -6,27 +6,38 @@ namespace SurfTimer; public partial class SurfTimer { - // Trigger end touch handler - CBaseTrigger_EndTouchFunc - // internal HookResult OnTriggerEndTouch(DynamicHook handler) + /// + /// Handler for trigger end touch hook - CBaseTrigger_EndTouchFunc. + /// + /// Sometimes this gets triggered when a player joins the server (for the 2nd time) so we assign `client` to `null` to bypass the error. + /// - T + /// internal HookResult OnTriggerEndTouch(CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, float delay) { - // CBaseTrigger trigger = handler.GetParam(0); CBaseTrigger trigger = new CBaseTrigger(caller.Handle); - // CBaseEntity entity = handler.GetParam(1); CBaseEntity entity = new CBaseEntity(activator.Handle); - CCSPlayerController client = new CCSPlayerController(new CCSPlayerPawn(entity.Handle).Controller.Value!.Handle); - if (!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 + CCSPlayerController client = null!; + + try { - return HookResult.Continue; + client = new CCSPlayerController(new CCSPlayerPawn(entity.Handle).Controller.Value!.Handle); + } + catch (System.Exception) + { + Console.WriteLine($"===================== [ERROR] OnTriggerEndTouch -> Could not assign `client` (name: {name})"); } + 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 + { + return HookResult.Continue; + } else { // Implement Trigger End Touch Here Player player = playerList[client.UserId ?? 0]; - #if DEBUG +#if DEBUG player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_EndTouchFunc -> {trigger.DesignerName} -> {trigger.Entity!.Name}"); - #endif +#endif if (trigger.Entity!.Name != null) { @@ -38,119 +49,147 @@ internal HookResult OnTriggerEndTouch(CEntityIOOutput output, string name, CEnti float velocity = (float)Math.Sqrt(velocity_x * velocity_x + velocity_y * velocity_y + velocity_z + velocity_z); // 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")) + if (trigger.Entity.Name.Contains("map_start") || + trigger.Entity.Name.Contains("s1_start") || + trigger.Entity.Name.Contains("stage1_start")) { - // 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 - } - // MAP START ZONE - if (!player.Timer.IsStageMode) + if (!player.Timer.IsStageMode && !player.Timer.IsBonusMode) { player.Timer.Start(); - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.START_RUN; - } - - /* Revisit - // Wonky Prespeed check - // To-do: make the teleportation a bit more elegant (method in a class or something) - if (velocity > 666.0) - { - player.Controller.PrintToChat( - $"{PluginPrefix} {ChatColors.Red}You are going too fast! ({velocity.ToString("0")} u/s)"); - player.Timer.Reset(); - if (CurrentMap.StartZone != new Vector(0,0,0)) - Server.NextFrame(() => player.Controller.PlayerPawn.Value!.Teleport(CurrentMap.StartZone, new QAngle(0,0,0), new Vector(0,0,0))); + 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 run - player.Stats.ThisRun.StartVelY = velocity_y; // Start pre speed for the run - player.Stats.ThisRun.StartVelZ = velocity_z; // Start pre speed for the run + 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 +#if DEBUG player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.LightRed}EndTouchFunc{ChatColors.Default} -> {ChatColors.Green}Map Start Zone"); - #endif +#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 +#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.Checkpoint.Count}"); - #endif + 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.Checkpoint.Count) + 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) { - var currentCheckpoint = player.Stats.ThisRun.Checkpoint[player.Timer.Checkpoint]; - #if DEBUG - Console.WriteLine($"currentCheckpoint.EndVelX {currentCheckpoint.EndVelX} - velocity_x {velocity_x}"); - Console.WriteLine($"currentCheckpoint.EndVelY {currentCheckpoint.EndVelY} - velocity_y {velocity_y}"); - Console.WriteLine($"currentCheckpoint.EndVelZ {currentCheckpoint.EndVelZ} - velocity_z {velocity_z}"); - Console.WriteLine($"currentCheckpoint.Attempts {currentCheckpoint.Attempts}"); - #endif - - // Update the values - currentCheckpoint.EndVelX = velocity_x; - currentCheckpoint.EndVelY = velocity_y; - currentCheckpoint.EndVelZ = velocity_z; - currentCheckpoint.EndTouch = player.Timer.Ticks; // To-do: what type of value we store in DB ? - currentCheckpoint.Attempts += 1; - // Assign the updated currentCheckpoint back to the list as `currentCheckpoint` is supposedly a copy of the original object - player.Stats.ThisRun.Checkpoint[player.Timer.Checkpoint] = currentCheckpoint; + 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 {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} - Prespeed: {velocity.ToString("0")} u/s"); + // Show Prespeed for Stages - will be enabled/disabled by the user? + player.Controller.PrintToCenter($"Stage {stage} - Prespeed: {velocity.ToString("0")} u/s"); } - else + else if (player.Timer.IsRunning) { - // Handle the case where the index is out of bounds +#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 +#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.Checkpoint.Count}"); - #endif + 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.Checkpoint.Count) + if (player.Timer.Checkpoint != 0 && player.Timer.Checkpoint <= player.Stats.ThisRun.Checkpoints.Count) { - var currentCheckpoint = player.Stats.ThisRun.Checkpoint[player.Timer.Checkpoint]; - #if DEBUG - Console.WriteLine($"currentCheckpoint.EndVelX {currentCheckpoint.EndVelX} - velocity_x {velocity_x}"); - Console.WriteLine($"currentCheckpoint.EndVelY {currentCheckpoint.EndVelY} - velocity_y {velocity_y}"); - Console.WriteLine($"currentCheckpoint.EndVelZ {currentCheckpoint.EndVelZ} - velocity_z {velocity_z}"); - #endif - - // Update the values - currentCheckpoint.EndVelX = velocity_x; - currentCheckpoint.EndVelY = velocity_y; - currentCheckpoint.EndVelZ = velocity_z; - currentCheckpoint.EndTouch = player.Timer.Ticks; // To-do: what type of value we store in DB ? - currentCheckpoint.Attempts += 1; - // Assign the updated currentCheckpoint back to the list as `currentCheckpoint` is supposedly a copy of the original object - player.Stats.ThisRun.Checkpoint[player.Timer.Checkpoint] = currentCheckpoint; +#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"); } - else + } + + // 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) { - // Handle the case where the index is out of bounds + 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 } } diff --git a/src/ST-Events/TriggerStartTouch.cs b/src/ST-Events/TriggerStartTouch.cs index c0af249..d73d394 100644 --- a/src/ST-Events/TriggerStartTouch.cs +++ b/src/ST-Events/TriggerStartTouch.cs @@ -7,167 +7,361 @@ namespace SurfTimer; public partial class SurfTimer { - // Trigger start touch handler - CBaseTrigger_StartTouchFunc - // internal HookResult OnTriggerStartTouch(DynamicHook handler) + /// + /// 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 = handler.GetParam(0); CBaseTrigger trigger = new CBaseTrigger(caller.Handle); - // CBaseEntity entity = handler.GetParam(1); CBaseEntity entity = new CBaseEntity(activator.Handle); CCSPlayerController client = new CCSPlayerController(new CCSPlayerPawn(entity.Handle).Controller.Value!.Handle); if (!client.IsValid || !client.PawnIsAlive || !playerList.ContainsKey((int)client.UserId!)) // !playerList.ContainsKey((int)client.UserId!) make sure to not check for user_id that doesnt exists { return HookResult.Continue; } - else + // 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)) { - // 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})"); - throw new Exception($"CS2 Surf ERROR >> OnTriggerStartTouch -> Init -> Player playerList does NOT contain client.UserId, this shouldn't happen. Player: {client.PlayerName} ({client.UserId})"); - // return HookResult.Continue; - } - // 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 + 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})"); + 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."); + 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; - if (trigger.Entity!.Name != null) + // Map end zones -- hook into map_end + if (trigger.Entity.Name == "map_end") { - // 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); - int style = player.Timer.Style; - - // Map end zones -- hook into map_end - if (trigger.Entity.Name == "map_end") - { - player.Controller.PrintToCenter($"Map End"); - // MAP END ZONE - if (player.Timer.IsRunning) - { - player.Timer.Stop(); - player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.END_RUN; + player.Controller.PrintToCenter($"Map End"); - 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 + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.END_ZONE_ENTER; + player.ReplayRecorder.MapSituations.Add(player.Timer.Ticks); - string PracticeString = ""; - if (player.Timer.IsPracticeMode) - PracticeString = $"({ChatColors.Grey}Practice{ChatColors.Default}) "; + 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 - // To-do: make Style (currently 0) be dynamic - if (player.Stats.PB[style].Ticks <= 0) // Player first ever PersonalBest for the map - { - Server.PrintToChatAll($"{PluginPrefix} {PracticeString}{player.Controller.PlayerName} finished the map in {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default} ({player.Timer.Ticks})!"); - } - else if (player.Timer.Ticks < player.Stats.PB[style].Ticks) // Player beating their existing PersonalBest for the map - { - Server.PrintToChatAll($"{PluginPrefix} {PracticeString}{ChatColors.Lime}{player.Profile.Name}{ChatColors.Default} beat their PB in {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default} (Old: {ChatColors.BlueGrey}{PlayerHUD.FormatTime(player.Stats.PB[style].Ticks)}{ChatColors.Default})!"); - } - else // Player did not beat their existing PersonalBest for the map - { - player.Controller.PrintToChat($"{PluginPrefix} {PracticeString}You finished the map in {ChatColors.Yellow}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default}!"); - return HookResult.Continue; // Exit here so we don't write to DB - } - if (DB == null) - throw new Exception("CS2 Surf ERROR >> OnTriggerStartTouch (Map end zone) -> DB object is null, this shouldn't happen."); + // 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}) "; - - player.Stats.PB[style].Ticks = player.Timer.Ticks; // Reload the run_time for the HUD and also assign for the DB query + 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 DEBUG - Console.WriteLine($@"CS2 Surf DEBUG >> OnTriggerStartTouch (Map end zone) -> + 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}, {style}, 0, 0, {player.Stats.ThisRun.Ticks}, + 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) { - AddTimer(1.5f, async () => { - player.Stats.ThisRun.SaveMapTime(player, DB); // Save the MapTime PB data - player.Stats.LoadMapTimesData(player, DB); // Load the MapTime PB data again (will refresh the MapTime ID for the Checkpoints query) - await player.Stats.ThisRun.SaveCurrentRunCheckpoints(player, DB); // Save this run's checkpoints - player.Stats.LoadCheckpointsData(DB); // Reload checkpoints for the run - we should really have this in `SaveMapTime` as well but we don't re-load PB data inside there so we need to do it here - CurrentMap.GetMapRecordAndTotals(DB); // Reload the Map record and totals for the HUD +#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[player.Timer.Style].Ticks || CurrentMap.WR[player.Timer.Style].ID == -1) + // 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, () => { - int WrIndex = CurrentMap.ReplayBots.Count-1; // As the ReplaysBot is set, WR Index will always be at the end of the List - AddTimer(2f, () => { - CurrentMap.ReplayBots[WrIndex].Stat_MapTimeID = CurrentMap.WR[player.Timer.Style].ID; - CurrentMap.ReplayBots[WrIndex].LoadReplayData(DB!); - CurrentMap.ReplayBots[WrIndex].ResetReplay(); + 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 DEBUG - player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.Lime}StartTouchFunc{ChatColors.Default} -> {ChatColors.Red}Map Stop Zone"); - #endif + 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); + }); + } } - // 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")) +#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.Checkpoint.Clear(); // I have the suspicion that the `Timer.Reset()` does not properly reset this object :thonk: + player.Stats.ThisRun.Checkpoints.Clear(); player.Controller.PrintToCenter($"Map Start ({trigger.Entity.Name})"); - #if DEBUG +#if DEBUG player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.Lime}StartTouchFunc{ChatColors.Default} -> {ChatColors.Green}Map Start Zone"); - // player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc -> KeyValues: {trigger.Entity.KeyValues3}"); - #endif +#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})"); - // 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) + 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) { - int stage = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value) - 1; - player.Timer.Stage = stage; + // 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}"); + } - #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.Checkpoint.Count <= stage}"); - #endif + player.Timer.Stage = stage; - // This should patch up re-triggering *player.Stats.ThisRun.Checkpoint.Count < stage* - if (player.Timer.IsRunning && !player.Timer.IsStageMode && player.Stats.ThisRun.Checkpoint.Count < 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) { - player.Timer.Checkpoint = stage; // Stage = Checkpoint when in a run on a Staged map + 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); + } - #if DEBUG - Console.WriteLine($"============== Initial entity value: {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} | Assigned to `stage`: {Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value) - 1}"); - Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Stage start zones) -> player.Stats.PB[{style}].Checkpoint.Count = {player.Stats.PB[style].Checkpoint.Count}"); - #endif + player.Timer.Checkpoint = stage - 1; // Stage = Checkpoint when in a run on a Staged map - // Print checkpoint message - player.HUD.DisplayCheckpointMessages(PluginPrefix); +#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 - // store the checkpoint in the player's current run checkpoints used for Checkpoint functionality - Checkpoint cp2 = new Checkpoint(stage, + // 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, @@ -175,33 +369,46 @@ internal HookResult OnTriggerStartTouch(CEntityIOOutput output, string name, CEn -1.0f, -1.0f, -1.0f, - -1.0f, - 0); - player.Stats.ThisRun.Checkpoint[stage] = cp2; + 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) +#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) { - int checkpoint = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value); - player.Timer.Checkpoint = 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 - // This should patch up re-triggering *player.Stats.ThisRun.Checkpoint.Count < checkpoint* - if (player.Timer.IsRunning && !player.Timer.IsStageMode && player.Stats.ThisRun.Checkpoint.Count < checkpoint) + if (player.Timer.IsRunning && player.ReplayRecorder.IsRecording) { - #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) - 1}"); - Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Checkpoint zones) -> player.Stats.PB[{style}].Checkpoint.Count = {player.Stats.PB[style].Checkpoint.Count}"); - #endif - - // Print checkpoint message - player.HUD.DisplayCheckpointMessages(PluginPrefix); + 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, @@ -211,18 +418,183 @@ internal HookResult OnTriggerStartTouch(CEntityIOOutput output, string name, CEn -1.0f, -1.0f, -1.0f, - -1.0f, - 0); - player.Stats.ThisRun.Checkpoint[checkpoint] = cp2; + 0, + 1); + player.Stats.ThisRun.Checkpoints[checkpoint] = cp2; + } + else + { + player.Stats.ThisRun.Checkpoints[checkpoint].Attempts++; } + } + +#if DEBUG + player.Controller.PrintToChat($"CS2 Surf DEBUG >> CBaseTrigger_{ChatColors.Lime}StartTouchFunc{ChatColors.Default} -> {ChatColors.LightBlue}Checkpoint {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} Zone"); +#endif + } + + // Bonus start zones -- hook into (b)onus#_start + else if (Regex.Match(trigger.Entity.Name, "^b([1-9][0-9]?|onus[1-9][0-9]?)_start$").Success) + { + int bonus = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value); + player.Timer.Bonus = bonus; + + player.Timer.Reset(); + player.Timer.IsBonusMode = true; + + + player.ReplayRecorder.Reset(); + player.ReplayRecorder.Start(); // Start replay recording + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.START_ZONE_ENTER; + player.ReplayRecorder.BonusSituations.Add(player.ReplayRecorder.Frames.Count); + Console.WriteLine($"START_ZONE_ENTER: player.ReplayRecorder.BonusSituations.Add({player.ReplayRecorder.Frames.Count})"); + + player.Controller.PrintToCenter($"Bonus Start ({trigger.Entity.Name})"); + +#if DEBUG + Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Bonus start zones) -> player.Timer.IsRunning: {player.Timer.IsRunning}"); + Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Bonus start zones) -> !player.Timer.IsBonusMode: {!player.Timer.IsBonusMode}"); +#endif + } + + // Bonus end zones -- hook into (b)onus#_end + else if (Regex.Match(trigger.Entity.Name, "^b([1-9][0-9]?|onus[1-9][0-9]?)_end$").Success && player.Timer.IsBonusMode && player.Timer.IsRunning) + { + // To-do: verify the bonus trigger being hit! + int bonus_idx = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value); + + player.Timer.Stop(); + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.END_ZONE_ENTER; + player.ReplayRecorder.BonusSituations.Add(player.Timer.Ticks); + + player.Stats.ThisRun.Ticks = player.Timer.Ticks; // End time for the run + player.Stats.ThisRun.EndVelX = velocity_x; // End pre speed for the run + player.Stats.ThisRun.EndVelY = velocity_y; // End pre speed for the run + player.Stats.ThisRun.EndVelZ = velocity_z; // End pre speed for the run - #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 + 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 + }); + } } } + } + return HookResult.Continue; + } - 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})!"); } } -} \ No newline at end of file +} + diff --git a/src/ST-Map/Map.cs b/src/ST-Map/Map.cs index c1fb166..6758c23 100644 --- a/src/ST-Map/Map.cs +++ b/src/ST-Map/Map.cs @@ -1,3 +1,5 @@ +using System.Data; +using System.Text.Json; using System.Text.RegularExpressions; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; @@ -6,45 +8,126 @@ namespace SurfTimer; -internal class Map +internal class Map { // 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;} = ""; - public string Author {get; set;} = ""; - public int Tier {get; set;} = 0; - public int Stages {get; set;} = 0; - public int Checkpoints {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; - public int TotalCompletions {get; set;} = 0; + 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] + /// + public Dictionary MapCompletions { get; set; } = new Dictionary(); + /// + /// Bonus Completion Count - Refer to as BonusCompletions[bonus#][style] + /// + public Dictionary[] BonusCompletions { get; set; } = new Dictionary[32]; + /// + /// Stage Completion Count - Refer to as StageCompletions[stage#][style] + /// + public Dictionary[] StageCompletions { get; set; } = new Dictionary[32]; + /// + /// Map World Record - Refer to as WR[style] + /// public Dictionary WR { get; set; } = new Dictionary(); + /// + /// Bonus World Record - Refer to as BonusWR[bonus#][style] + /// + public Dictionary[] BonusWR { get; set; } = new Dictionary[32]; + /// + /// Stage World Record - Refer to as StageWR[stage#][style] + /// + public Dictionary[] StageWR { get; set; } = new Dictionary[32]; + + /// + /// Not sure what this is for. + /// Guessing it's to do with Replays and the ability to play your PB replay. + /// + /// - T + /// public List ConnectedMapTimes { get; set; } = new List(); - public List ReplayBots { get; set; } = new List { new ReplayPlayer() }; // Zone Origin Information - // Map start/end zones - public Vector StartZone {get;} = new Vector(0,0,0); - public QAngle StartZoneAngles {get;} = new QAngle(0,0,0); - public Vector EndZone {get;} = new Vector(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(); - // 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 - // Map checkpoint zones - public Vector[] CheckpointStartZone {get;} = Enumerable.Repeat(0, 99).Select(x => new Vector(0,0,0)).ToArray(); + /* 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); + /* 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(); + /* 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 + /* Map Checkpoint zones */ + public Vector[] CheckpointStartZone { get; } = Enumerable.Repeat(0, 99).Select(x => new Vector(0, 0, 0)).ToArray(); + + public ReplayManager ReplayManager { get; set; } = null!; // Constructor - internal Map(string Name, TimerDatabase DB) + internal Map(string name) { // Set map name - this.Name = Name; - this.WR[0] = new PersonalBest(); // To-do: Implement styles + 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(); + } + } + + public static async Task CreateAsync(string name) + { + var map = new Map(name); + await map.InitializeAsync(); + return map; + } + + private async Task InitializeAsync() + { + // Load zones + Map_Load_Zones(); + Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> InitializeAsync -> Zones have been loaded."); + + // 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 + + await Get_Map_Info(); + + Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> InitializeAsync -> We got MapID = {ID} ({Name})"); + } + + /// + /// Loops through all the hookzones found in the map and loads the respective zones + /// + // 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() + { // Gathering zones from the map IEnumerable triggers = Utilities.FindAllEntitiesByDesignerName("trigger_multiple"); // Gathering info_teleport_destinations from the map @@ -54,17 +137,17 @@ internal Map(string Name, TimerDatabase DB) if (trigger.Entity!.Name != null) { // Map start zone - if (trigger.Entity!.Name.Contains("map_start") || - trigger.Entity!.Name.Contains("stage1_start") || - trigger.Entity!.Name.Contains("s1_start")) + if (trigger.Entity!.Name.Contains("map_start") || + trigger.Entity!.Name.Contains("stage1_start") || + trigger.Entity!.Name.Contains("s1_start")) { bool foundPlayerSpawn = false; // Track whether a player spawn is found foreach (CBaseEntity teleport in teleports) { - if (teleport.Entity!.Name != null && - (IsInZone(trigger.AbsOrigin!, trigger.Collision.BoundingRadius, teleport.AbsOrigin!) || - teleport.Entity!.Name.Contains("spawn_map_start") || - teleport.Entity!.Name.Contains("spawn_stage1_start") || + if (teleport.Entity!.Name != null && + (IsInZone(trigger.AbsOrigin!, trigger.Collision.BoundingRadius, teleport.AbsOrigin!) || + teleport.Entity!.Name.Contains("spawn_map_start") || + 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); @@ -81,25 +164,25 @@ internal Map(string Name, TimerDatabase DB) } // Map end zone - else if (trigger.Entity!.Name.Contains("map_end")) + else if (trigger.Entity!.Name.Contains("map_end")) { this.EndZone = new Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); } // Stage start zones - else if (Regex.Match(trigger.Entity.Name, "^s([1-9][0-9]?|tage[1-9][0-9]?)_start$").Success) + 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); - + // Find an info_destination_teleport inside this zone to grab angles from bool foundPlayerSpawn = false; // Track whether a player spawn is found foreach (CBaseEntity teleport in teleports) { - if (teleport.Entity!.Name != null && + 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 - 1] = new Vector(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z); - this.StageStartZoneAngles[stage - 1] = new QAngle(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z); + 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.Stages++; // Count stage zones for the map to populate DB foundPlayerSpawn = true; break; @@ -108,19 +191,20 @@ internal Map(string Name, TimerDatabase DB) if (!foundPlayerSpawn) { - this.StageStartZone[stage - 1] = new Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); + this.StageStartZone[stage] = new Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); + this.Stages++; } } // 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) + 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) - 1] = new Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); - this.Checkpoints++; // Might be useful to have this in DB entry + 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.TotalCheckpoints++; // Might be useful to have this in DB entry } - + // Bonus start zones - else if (Regex.Match(trigger.Entity.Name, "^b([1-9][0-9]?|onus[1-9][0-9]?)_start$").Success) + 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); @@ -128,11 +212,11 @@ internal Map(string Name, TimerDatabase DB) bool foundPlayerSpawn = false; // Track whether a player spawn is found foreach (CBaseEntity teleport in teleports) { - if (teleport.Entity!.Name != null && + 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 - 1] = new Vector(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z); - this.BonusStartZoneAngles[bonus - 1] = new QAngle(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z); + 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.Bonuses++; // Count bonus zones for the map to populate DB foundPlayerSpawn = true; break; @@ -141,189 +225,564 @@ internal Map(string Name, TimerDatabase DB) if (!foundPlayerSpawn) { - this.BonusStartZone[bonus - 1] = new Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); + this.BonusStartZone[bonus] = new Vector(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) + 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) - 1] = 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 Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z); } } } - if (this.Stages > 0) this.Stages++; // You did not count the stages right :( + + if (this.Stages > 0) // Account for stage 1, not counted above + 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}"); + } - // Gather map information OR create entry - Task reader = DB.Query($"SELECT * FROM Maps WHERE name='{MySqlHelper.EscapeString(Name)}'"); - MySqlDataReader mapData = reader.Result; - bool updateData = false; - 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) + /// + /// Inserts a new map entry in the database. + /// Automatically detects whether to use API Calls or MySQL query. + /// + internal async Task Insert_Map_Info() + { + if (Config.API.GetApiOnly()) // API Calls { - this.ID = mapData.GetInt32("id"); - this.Author = mapData.GetString("author") ?? "Unknown"; - this.Tier = mapData.GetInt32("tier"); - if (this.Stages != mapData.GetInt32("stages") || this.Bonuses != mapData.GetInt32("bonuses")) - updateData = true; - // this.Stages = mapData.GetInt32("stages"); // this should now be populated accordingly when looping through hookzones for the map - // this.Bonuses = mapData.GetInt32("bonuses"); // this should now be populated accordingly when looping through hookzones for the map - this.Ranked = mapData.GetBoolean("ranked"); - this.DateAdded = mapData.GetInt32("date_added"); - this.LastPlayed = mapData.GetInt32("last_played"); - updateData = true; - mapData.Close(); - } + 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, + }; - else - { - mapData.Close(); - Task writer = DB.Write($"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()})"); - int writerRows = writer.Result; - if (writerRows != 1) - throw new Exception($"CS2 Surf ERROR >> OnRoundStart -> new Map() -> Failed to write new map to database, this shouldn't happen. Map: {Name}"); - writer.Dispose(); + var postResponse = await ApiMethod.POST(Config.API.Endpoints.ENDPOINT_MAP_INSERT_INFO, inserted); - Task postWriteReader = DB.Query($"SELECT * FROM Maps WHERE name='{MySqlHelper.EscapeString(Name)}'"); - MySqlDataReader postWriteMapData = postWriteReader.Result; - if (postWriteMapData.HasRows && postWriteMapData.Read()) + // Check if the response is not null and get the last_id + if (postResponse != null) { - this.ID = postWriteMapData.GetInt32("id"); - this.Author = postWriteMapData.GetString("author"); - this.Tier = postWriteMapData.GetInt32("tier"); - // this.Stages = -1; // this should now be populated accordingly when looping through hookzones for the map - // this.Bonuses = -1; // this should now be populated accordingly when looping through hookzones for the map - this.Ranked = postWriteMapData.GetBoolean("ranked"); - this.DateAdded = postWriteMapData.GetInt32("date_added"); - this.LastPlayed = this.DateAdded; + 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; } - postWriteMapData.Close(); return; } + else // MySQL Queries + { + 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; + } - // Update the map's last played data in the DB - // Update last_played data or update last_played, stages, and bonuses data - string query = $"UPDATE Maps SET last_played={(int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()} WHERE id={this.ID}"; - if (updateData) query = $"UPDATE Maps SET last_played={(int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()}, stages={this.Stages}, bonuses={this.Bonuses} WHERE id={this.ID}"; - #if DEBUG - Console.WriteLine($"CS2 Surf ERROR >> OnRoundStart -> update Map() -> Update MapData: {query}"); - #endif - - Task updater = DB.Write(query); - int lastPlayedUpdateRows = updater.Result; - if (lastPlayedUpdateRows != 1) - throw new Exception($"CS2 Surf ERROR >> OnRoundStart -> update Map() -> Failed to update map in database, this shouldnt happen. Map: {Name} | was it 'big' update? {updateData}"); - updater.Dispose(); - - // Initiates getting the World Records for the map - GetMapRecordAndTotals(DB); // To-do: Implement styles - - this.ReplayBots[0].Stat_MapTimeID = this.WR[0].ID; // Sets WrIndex to WR maptime_id - if(this.Stages > 0) // If stages map adds bot - this.ReplayBots = this.ReplayBots.Prepend(new ReplayPlayer()).ToList(); - - if(this.Bonuses > 0) // If has bonuses adds bot - this.ReplayBots = this.ReplayBots.Prepend(new ReplayPlayer()).ToList(); + await Get_Map_Info(false); + } } - public void KickReplayBot(int index) + /// + /// 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() { - if (!this.ReplayBots[index].IsPlayable) - return; + if (Config.API.GetApiOnly()) // API Calls + { + 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() + }; - int? id_to_kick = this.ReplayBots[index].Controller!.UserId; - if(id_to_kick == null) - return; + _ = ApiMethod.PUT(Config.API.Endpoints.ENDPOINT_MAP_UPDATE_INFO, updated).Result; + } + else // MySQL Queries + { + // 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); + +#if DEBUG + Console.WriteLine($"CS2 Surf >> internal class Map -> internal async Task Update_Map_Info -> Update MapData: {updateQuery}"); +#endif - this.ReplayBots.RemoveAt(index); - Server.ExecuteCommand($"kickid {id_to_kick}; bot_quota {this.ReplayBots.Count}"); + 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; + } + } } - public bool IsInZone(Vector zoneOrigin, float zoneCollisionRadius, Vector spawnOrigin) + /// + /// Load map info data using MySQL Queries and update the info as well or create a new 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) { - if (spawnOrigin.X >= zoneOrigin.X - zoneCollisionRadius && spawnOrigin.X <= zoneOrigin.X + zoneCollisionRadius && - spawnOrigin.Y >= zoneOrigin.Y - zoneCollisionRadius && spawnOrigin.Y <= zoneOrigin.Y + zoneCollisionRadius && - spawnOrigin.Z >= zoneOrigin.Z - zoneCollisionRadius && spawnOrigin.Z <= zoneOrigin.Z + zoneCollisionRadius) - return true; - else - return false; + bool newMap = false; + + if (Config.API.GetApiOnly()) // API Calls + { + // 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; + } + } + else // MySQL queries + { + // 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; + } + } + } + + // This is a new map + if (newMap) + { + await Insert_Map_Info(); + 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 Get_Map_Record_Runs(); } - // Leaving this outside of the constructor for `Map` so we can call it to ONLY update the data when a new world record is set - internal void GetMapRecordAndTotals(TimerDatabase DB, int style = 0 ) // To-do: Implement styles + /// + /// 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() { - // Get map world records - Task reader = DB.Query($@" - SELECT MapTimes.*, Player.name - FROM MapTimes - JOIN Player ON MapTimes.player_id = Player.id - WHERE MapTimes.map_id = {this.ID} AND MapTimes.style = {style} - ORDER BY MapTimes.run_time ASC; - "); - MySqlDataReader mapWrData = reader.Result; - int totalRows = 0; - - if (mapWrData.HasRows) - { - // To-do: Implement bonuses WR - // To-do: Implement stages WR - this.ConnectedMapTimes.Clear(); - while (mapWrData.Read()) + int totalMapRuns = 0; + int totalStageRuns = 0; + int totalBonusRuns = 0; + this.ConnectedMapTimes.Clear(); + + int qType; + int qStage; + int qStyle; + + // Replay Stuff + JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = false, Converters = { new VectorConverter(), new QAngleConverter() } }; + + 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 + { + // 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))) { - if (totalRows == 0) // We are sorting by `run_time ASC` so the first row is always the fastest run for the map and style combo :) - { - this.WR[style].ID = mapWrData.GetInt32("id"); // WR ID for the Map and Style combo - this.WR[style].Ticks = mapWrData.GetInt32("run_time"); // Fastest run time (WR) for the Map and Style combo - this.WR[style].StartVelX = mapWrData.GetFloat("start_vel_x"); // Fastest run start velocity X for the Map and Style combo - this.WR[style].StartVelY = mapWrData.GetFloat("start_vel_y"); // Fastest run start velocity Y for the Map and Style combo - this.WR[style].StartVelZ = mapWrData.GetFloat("start_vel_z"); // Fastest run start velocity Z for the Map and Style combo - this.WR[style].EndVelX = mapWrData.GetFloat("end_vel_x"); // Fastest run end velocity X for the Map and Style combo - this.WR[style].EndVelY = mapWrData.GetFloat("end_vel_y"); // Fastest run end velocity Y for the Map and Style combo - this.WR[style].EndVelZ = mapWrData.GetFloat("end_vel_z"); // Fastest run end velocity Z for the Map and Style combo - this.WR[style].RunDate = mapWrData.GetInt32("run_date"); // Fastest run date for the Map and Style combo - this.WR[style].Name = mapWrData.GetString("name"); // Fastest run player name for the Map and Style combo + 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); + } + + // 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; + } + + // Console.WriteLine($"CS2 Surf DEBUG >> internal class Map -> internal async Task Get_Map_Record_Runs -> Map Completions for style {qStyle} {this.MapCompletions[qStyle]}"); + } } - this.ConnectedMapTimes.Add(mapWrData.GetInt32("id")); - totalRows++; } } - mapWrData.Close(); - this.TotalCompletions = totalRows; // Total completions for the map and style - this should maybe be added to PersonalBest class - // Get map world record checkpoints - if (totalRows != 0) + // Retrieve the checkpoints for each Style if it has been set. + foreach (int style in Config.Styles) { - Task cpReader = DB.Query($"SELECT * FROM `Checkpoints` WHERE `maptime_id` = {this.WR[style].ID};"); - MySqlDataReader cpWrData = cpReader.Result; - while (cpWrData.Read()) + // 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 DEBUG - Console.WriteLine($"cp {cpWrData.GetInt32("cp")} "); - Console.WriteLine($"run_time {cpWrData.GetFloat("run_time")} "); - Console.WriteLine($"sVelX {cpWrData.GetFloat("start_vel_x")} "); - Console.WriteLine($"sVelY {cpWrData.GetFloat("start_vel_y")} "); - #endif - - Checkpoint cp = new(cpWrData.GetInt32("cp"), - cpWrData.GetInt32("run_time"), // To-do: what type of value we use here? DB uses DECIMAL but `.Tick` is int??? - cpWrData.GetFloat("start_vel_x"), - cpWrData.GetFloat("start_vel_y"), - cpWrData.GetFloat("start_vel_z"), - cpWrData.GetFloat("end_vel_x"), - cpWrData.GetFloat("end_vel_y"), - cpWrData.GetFloat("end_vel_z"), - cpWrData.GetFloat("end_touch"), - cpWrData.GetInt32("attempts")); - cp.ID = cpWrData.GetInt32("cp"); - // To-do: cp.ID = calculate Rank # from DB - - this.WR[style].Checkpoint[cp.CP] = cp; - - #if DEBUG - Console.WriteLine($"======= CS2 Surf DEBUG >> internal void GetMapRecordAndTotals : Map -> Loaded WR CP {cp.CP} with RunTime {cp.Ticks} for MapTimeID {WR[0].ID} (MapId = {this.ID})."); - #endif +#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)}"); +#endif + await Get_Record_Run_Checkpoints(style); } - cpWrData.Close(); } } + + /// + /// 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. + /// + /// Type - 0 = Map, 1 = Bonus, 2 = Stage + /// 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) + { + JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = false, Converters = { new VectorConverter(), new QAngleConverter() } }; + + // Decompress the Base64 string + string json = Compressor.Decompress(replayFramesBase64); + + // Deserialize to List + List frames = JsonSerializer.Deserialize>(json, options)!; + + 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`"); + 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.Frames = frames; + this.ReplayManager.MapWR.MapTimeID = this.WR[style].ID; + this.ReplayManager.MapWR.MapID = this.ID; + this.ReplayManager.MapWR.Type = 0; + for (int i = 0; i < frames.Count; i++) // Load the situations for the replay + { + 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: + this.ReplayManager.MapWR.MapSituations.Add(i); + Console.WriteLine($"START_ZONE_EXIT: {i} | Situation {f.Situation}"); + break; + case ReplayFrameSituation.STAGE_ZONE_ENTER: + 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}"); + break; + case ReplayFrameSituation.CHECKPOINT_ZONE_ENTER: + this.ReplayManager.MapWR.CheckpointEnterSituations.Add(i); + Console.WriteLine($"CHECKPOINT_ZONE_ENTER: {i} | Situation {f.Situation}"); + break; + case ReplayFrameSituation.CHECKPOINT_ZONE_EXIT: + this.ReplayManager.MapWR.CheckpointExitSituations.Add(i); + Console.WriteLine($"CHECKPOINT_ZONE_EXIT: {i} | Situation {f.Situation}"); + 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}"); + 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) + 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`"); + // 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].MapTimeID = this.BonusWR[stage][style].ID; + this.ReplayManager.AllBonusWR[stage][style].Stage = stage; + this.ReplayManager.AllBonusWR[stage][style].Type = 1; + this.ReplayManager.AllBonusWR[stage][style].RecordRank = 1; + this.ReplayManager.AllBonusWR[stage][style].IsPlayable = true; // We set this to `true` else we overwrite it and need to call SetController method again + for (int i = 0; i < frames.Count; i++) + { + 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: + this.ReplayManager.AllBonusWR[stage][style].BonusSituations.Add(i); + break; + } + } + // 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 (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.MapTimeID = this.BonusWR[stage][style].ID; + this.ReplayManager.BonusWR.Stage = stage; + this.ReplayManager.BonusWR.Type = 1; + this.ReplayManager.BonusWR.RecordRank = 1; + } + 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) + 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`"); + // 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].MapTimeID = this.StageWR[stage][style].ID; + this.ReplayManager.AllStageWR[stage][style].Stage = stage; + this.ReplayManager.AllStageWR[stage][style].Type = 2; + this.ReplayManager.AllStageWR[stage][style].RecordRank = 1; + this.ReplayManager.AllStageWR[stage][style].IsPlayable = true; // We set this to `true` else we overwrite it and need to call SetController method again + for (int i = 0; i < frames.Count; i++) + { + ReplayFrame f = frames[i]; + switch (f.Situation) + { + case ReplayFrameSituation.STAGE_ZONE_ENTER: + this.ReplayManager.AllStageWR[stage][style].StageEnterSituations.Add(i); + break; + case ReplayFrameSituation.STAGE_ZONE_EXIT: + this.ReplayManager.AllStageWR[stage][style].StageExitSituations.Add(i); + break; + } + } + // 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 (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.MapTimeID = this.StageWR[stage][style].ID; + this.ReplayManager.StageWR.Stage = stage; + this.ReplayManager.StageWR.Type = 2; + this.ReplayManager.StageWR.RecordRank = 1; + } + break; + } + + // Start the new map replay if none existed until now + if (type == 0 && this.ReplayManager.MapWR != null && !this.ReplayManager.MapWR.IsPlaying) + { + 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(); + } + } + + public void KickReplayBot(int index) + { + if (!this.ReplayManager.CustomReplays[index].IsPlayable) + return; + + int? id_to_kick = this.ReplayManager.CustomReplays[index].Controller!.UserId; + if (id_to_kick == null) + return; + + this.ReplayManager.CustomReplays.RemoveAt(index); + Server.ExecuteCommand($"kickid {id_to_kick}; bot_quota {this.ReplayManager.CustomReplays.Count}"); + } + + public static bool IsInZone(Vector zoneOrigin, float zoneCollisionRadius, Vector spawnOrigin) + { + if (spawnOrigin.X >= zoneOrigin.X - zoneCollisionRadius && spawnOrigin.X <= zoneOrigin.X + zoneCollisionRadius && + spawnOrigin.Y >= zoneOrigin.Y - zoneCollisionRadius && spawnOrigin.Y <= zoneOrigin.Y + zoneCollisionRadius && + spawnOrigin.Z >= zoneOrigin.Z - zoneCollisionRadius && spawnOrigin.Z <= zoneOrigin.Z + zoneCollisionRadius) + return true; + else + return false; + } } \ No newline at end of file diff --git a/src/ST-Player/Player.cs b/src/ST-Player/Player.cs index 2bcfb83..cbbaed5 100644 --- a/src/ST-Player/Player.cs +++ b/src/ST-Player/Player.cs @@ -40,7 +40,7 @@ public Player(CCSPlayerController Controller, CCSPlayer_MovementServices Movemen } /// - /// Checks if current player is spcetating 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 39ab08e..41a3454 100644 --- a/src/ST-Player/PlayerHUD.cs +++ b/src/ST-Player/PlayerHUD.cs @@ -2,7 +2,7 @@ namespace SurfTimer; -internal class PlayerHUD +internal class PlayerHUD { private Player _player; @@ -44,10 +44,12 @@ public static string FormatTime(int ticks, PlayerTimer.TimeFormatStyle style = P { case PlayerTimer.TimeFormatStyle.Compact: return time.TotalMinutes < 1 - ? $"{time.Seconds:D1}.{millis:D3}" - : $"{time.Minutes:D1}:{time.Seconds:D1}.{millis:D3}"; + ? $"{time.Seconds:D2}.{millis:D3}" + : $"{time.Minutes:D1}:{time.Seconds:D2}.{millis:D3}"; case PlayerTimer.TimeFormatStyle.Full: - return $"{time.Hours:D2}:{time.Minutes:D2}:{time.Seconds:D2}.{millis:D3}"; + return time.TotalHours < 1 + ? $"{time.Minutes:D2}:{time.Seconds:D2}.{millis:D3}" + : $"{time.Hours:D2}:{time.Minutes:D2}:{time.Seconds:D2}.{millis:D3}"; case PlayerTimer.TimeFormatStyle.Verbose: return $"{time.Hours}h {time.Minutes}m {time.Seconds}s {millis}ms"; default: @@ -57,7 +59,7 @@ public static string FormatTime(int ticks, PlayerTimer.TimeFormatStyle style = P public void Display() { - if(!_player.Controller.IsValid) + if (!_player.Controller.IsValid) return; if (_player.Controller.PawnIsAlive) @@ -73,7 +75,14 @@ public void Display() else timerColor = "#2E9F65"; } - string timerModule = FormatHUDElementHTML("", FormatTime(_player.Timer.Ticks), timerColor); + + 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 - 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 @@ -82,17 +91,42 @@ public void Display() string velocityModule = FormatHUDElementHTML("Speed", velocity.ToString("0"), "#79d1ed") + " u/s"; // Rank Module string rankModule = FormatHUDElementHTML("Rank", $"N/A", "#7882dd"); - if (_player.Stats.PB[style].ID != -1 && _player.CurrMap.WR[style].ID != -1) + 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) { - rankModule = FormatHUDElementHTML("Rank", $"{_player.Stats.PB[style].Rank}/{_player.CurrMap.TotalCompletions}", "#7882dd"); + 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.CurrMap.WR[style].ID != -1) + else { - rankModule = FormatHUDElementHTML("Rank", $"-/{_player.CurrMap.TotalCompletions}", "#7882dd"); + 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"); } + // PB & WR Modules - string pbModule = FormatHUDElementHTML("PB", _player.Stats.PB[style].Ticks > 0 ? FormatTime(_player.Stats.PB[style].Ticks) : "N/A", "#7882dd"); // IMPLEMENT IN PlayerStats // To-do: make Style (currently 0) be dynamic - string wrModule = FormatHUDElementHTML("WR", _player.CurrMap.WR[style].Ticks > 0 ? FormatTime(_player.CurrMap.WR[style].Ticks) : "N/A", "#ffc61a"); // IMPLEMENT IN PlayerStats - This should be part of CurrentMap, not PlayerStats? + 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"); + } // Build HUD string hud = $"{timerModule}
{velocityModule}
{pbModule} | {rankModule}
{wrModule}"; @@ -102,17 +136,22 @@ public void Display() } else if (_player.Controller.Team == CsTeam.Spectator) { - for (int i = 0; i < _player.CurrMap.ReplayBots.Count; i++) - { - if(!_player.CurrMap.ReplayBots[i].IsPlayable || !_player.IsSpectating(_player.CurrMap.ReplayBots[i].Controller!)) - continue; - - string replayModule = $"{FormatHUDElementHTML("", "REPLAY", "red", "large")}"; + ReplayPlayer? spec_replay; - string nameModule = FormatHUDElementHTML($"{_player.CurrMap.ReplayBots[i].Stat_PlayerName}", $"{FormatTime(_player.CurrMap.ReplayBots[i].Stat_RunTime)}", "#ffd500"); + 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!)); - string elapsed_time = FormatHUDElementHTML("Time", $"{PlayerHUD.FormatTime(_player.CurrMap.ReplayBots[i].Stat_RunTick)}", "#7882dd"); - string hud = $"{replayModule}
{elapsed_time}
{nameModule}"; + 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}"; _player.Controller.PrintToCenterHtml(hud); } @@ -122,14 +161,16 @@ public void Display() /// /// Only calculates if the player has a PB, otherwise it will display N/A /// - /// - public void DisplayCheckpointMessages(string PluginPrefix) // To-do: PluginPrefix should be accessible in here without passing it as a parameter + public 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 currentTime = _player.Timer.Ticks; float currentSpeed = (float)Math.Sqrt(_player.Controller.PlayerPawn.Value!.AbsVelocity.X * _player.Controller.PlayerPawn.Value!.AbsVelocity.X @@ -140,43 +181,43 @@ public void DisplayCheckpointMessages(string PluginPrefix) // To-do: PluginPrefi 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 stage in this case but they will not have for the map as well + // 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 { - pbTime = _player.Stats.PB[style].Checkpoint[_player.Timer.Checkpoint].Ticks; - pbSpeed = (float)Math.Sqrt(_player.Stats.PB[style].Checkpoint[_player.Timer.Checkpoint].StartVelX * _player.Stats.PB[style].Checkpoint[_player.Timer.Checkpoint].StartVelX - + _player.Stats.PB[style].Checkpoint[_player.Timer.Checkpoint].StartVelY * _player.Stats.PB[style].Checkpoint[_player.Timer.Checkpoint].StartVelY - + _player.Stats.PB[style].Checkpoint[_player.Timer.Checkpoint].StartVelZ * _player.Stats.PB[style].Checkpoint[_player.Timer.Checkpoint].StartVelZ); - - #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [TIME] Got pbTime from _player.Stats.PB[{style}].Checkpoint[{_player.Timer.Checkpoint} = {pbTime}]"); - Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [SPEED] Got pbSpeed from _player.Stats.PB[{style}].Checkpoint[{_player.Timer.Checkpoint}] = {pbSpeed}"); - #endif + 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 } - #if DEBUG +#if DEBUG catch (System.Exception ex) - #else +#else catch (System.Exception) - #endif +#endif { - // Handle the exception gracefully without stopping the application + // 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 + +#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].Checkpoint.Count}"); - #endif + 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 +#if DEBUG Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> Starting PB difference calculation... (pbTime != -1)"); - #endif +#endif // Reset the string strPbDifference = ""; @@ -203,17 +244,17 @@ public void DisplayCheckpointMessages(string PluginPrefix) // To-do: PluginPrefi strPbDifference += ChatColors.Default + ")"; } - if (_player.CurrMap.WR[style].Ticks > 0) + if (SurfTimer.CurrentMap.WR[style].Ticks > 0) { // Calculate differences in WR (WR - Current) - #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> Starting WR difference calculation... (_player.CurrMap.WR[{style}].Ticks > 0)"); - #endif - - wrTime = _player.CurrMap.WR[style].Checkpoint[_player.Timer.Checkpoint].Ticks; - wrSpeed = (float)Math.Sqrt(_player.CurrMap.WR[style].Checkpoint[_player.Timer.Checkpoint].StartVelX * _player.CurrMap.WR[style].Checkpoint[_player.Timer.Checkpoint].StartVelX - + _player.CurrMap.WR[style].Checkpoint[_player.Timer.Checkpoint].StartVelY * _player.CurrMap.WR[style].Checkpoint[_player.Timer.Checkpoint].StartVelY - + _player.CurrMap.WR[style].Checkpoint[_player.Timer.Checkpoint].StartVelZ * _player.CurrMap.WR[style].Checkpoint[_player.Timer.Checkpoint].StartVelZ); +#if DEBUG + 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); // Reset the string strWrDifference = ""; @@ -242,17 +283,17 @@ public void DisplayCheckpointMessages(string PluginPrefix) // To-do: PluginPrefi // Print checkpoint message _player.Controller.PrintToChat( - $"{PluginPrefix} CP [{ChatColors.Yellow}{_player.Timer.Checkpoint}{ChatColors.Default}]: " + + $"{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}]"); - #if DEBUG +#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}"); - #endif +#endif } } diff --git a/src/ST-Player/PlayerProfile.cs b/src/ST-Player/PlayerProfile.cs index 3904e35..6f5930a 100644 --- a/src/ST-Player/PlayerProfile.cs +++ b/src/ST-Player/PlayerProfile.cs @@ -1,23 +1,120 @@ +using CounterStrikeSharp.API.Core; +using Microsoft.VisualBasic; + namespace SurfTimer; internal class PlayerProfile { - 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(int ID, string Name, ulong SteamID, string Country, int JoinDate, int LastSeen, int Connections) + 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 = "") + { + this.SteamID = steamId; + this.Name = name; + this.Country = 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 = "") + { + var profile = new PlayerProfile(steamId, name, country); + await profile.InitializeAsync(); + return profile; + } + + private async Task InitializeAsync() + { + await Get_Player_Profile(); + + Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerProfile -> InitializeAsync -> [{(Config.API.GetApiOnly() ? "API" : "DB")}] We got ProfileID = {this.ID} ({this.Name})"); + } + + /// + /// Retrieves all the data for the player from the database. + /// + public async Task Get_Player_Profile() + { + bool newPlayer = false; + + // 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 (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; + } + } + +#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}"); +#endif + if (newPlayer) + await Insert_Player_Profile(); + } + + /// + /// Insert new player information into the database. + /// Retrieves the ID of the newly created player. + /// + public async Task Insert_Player_Profile() + { + // 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) + { + Exception ex = new($"Error inserting new player profile for '{this.Name}' ({this.SteamID})"); + throw ex; + } + + 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}"); +#endif + } + + /// + /// Updates the information in the database for the player. Increments `connections` and changes nickname. + /// + /// Player Name + /// + public async Task Update_Player_Profile(string name) { - this.ID = ID; - this.Name = Name; - this.SteamID = SteamID; - this.Country = Country; - this.JoinDate = JoinDate; - this.LastSeen = LastSeen; - this.Connections = Connections; + 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})"); +#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}"); +#endif } } \ No newline at end of file diff --git a/src/ST-Player/PlayerStats/Checkpoint.cs b/src/ST-Player/PlayerStats/Checkpoint.cs index bf51165..85b6254 100644 --- a/src/ST-Player/PlayerStats/Checkpoint.cs +++ b/src/ST-Player/PlayerStats/Checkpoint.cs @@ -3,10 +3,10 @@ namespace SurfTimer; internal class Checkpoint : PersonalBest { public int CP { get; set; } - public float EndTouch { 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, float endTouch, int attempts) + 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???? diff --git a/src/ST-Player/PlayerStats/CurrentRun.cs b/src/ST-Player/PlayerStats/CurrentRun.cs index f6fd2d0..32150a0 100644 --- a/src/ST-Player/PlayerStats/CurrentRun.cs +++ b/src/ST-Player/PlayerStats/CurrentRun.cs @@ -5,7 +5,7 @@ namespace SurfTimer; /// internal class CurrentRun { - public Dictionary Checkpoint { get; set; } // Current RUN checkpoints tracker + 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 @@ -19,7 +19,7 @@ internal class CurrentRun // Constructor public CurrentRun() { - Checkpoint = new Dictionary(); + Checkpoints = new Dictionary(); Ticks = 0; StartVelX = 0.0f; StartVelY = 0.0f; @@ -32,7 +32,7 @@ public CurrentRun() public void Reset() { - Checkpoint.Clear(); + Checkpoints.Clear(); Ticks = 0; StartVelX = 0.0f; StartVelY = 0.0f; @@ -45,46 +45,152 @@ public void Reset() } /// - /// Saves the player's run to the database and reloads the data for the player. - /// NOTE: Not re-loading any data at this point as we need `LoadMapTimesData` to be called from here as well, otherwise we may not have the `this.ID` populated + /// Saves the player's run to the database. /// - public void SaveMapTime(Player player, TimerDatabase DB) + /// 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) { // Add entry in DB for the run - // To-do: add `type` + // PrintSituations(player); + string replay_frames = player.ReplayRecorder.TrimReplay(player, stage != 0 ? 2 : bonus != 0 ? 1 : 0, stage == SurfTimer.CurrentMap.Stages); + + Console.WriteLine($"CS2 Surf DEBUG >> internal class CurrentRun -> public async Task SaveMapTime -> Sending total of {replay_frames.Length} replay frames"); + if (Config.API.GetApiOnly()) + { + return; + } + else + { + await InsertMapTime(player, bonus, stage, run_ticks, replay_frames, true); + + if (stage != 0 || bonus != 0) + { + 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}"); + } + else + { + await SaveCurrentRunCheckpoints(player, true); // Save this run's checkpoints + } + + await player.CurrMap.Get_Map_Record_Runs(); // Reload the times for the Map + } + } + + public void PrintSituations(Player player) + { + Console.WriteLine($"========================== FOUND SITUATIONS =========================="); + for (int i = 0; i < player.ReplayRecorder.Frames.Count; i++) + { + ReplayFrame x = player.ReplayRecorder.Frames[i]; + switch (x.Situation) + { + case ReplayFrameSituation.START_ZONE_ENTER: + Console.WriteLine($"START_ZONE_ENTER: {i} | Situation {x.Situation}"); + break; + case ReplayFrameSituation.START_ZONE_EXIT: + Console.WriteLine($"START_ZONE_EXIT: {i} | Situation {x.Situation}"); + break; + case ReplayFrameSituation.STAGE_ZONE_ENTER: + Console.WriteLine($"STAGE_ZONE_ENTER: {i} | Situation {x.Situation}"); + break; + case ReplayFrameSituation.STAGE_ZONE_EXIT: + Console.WriteLine($"STAGE_ZONE_EXIT: {i} | Situation {x.Situation}"); + break; + case ReplayFrameSituation.CHECKPOINT_ZONE_ENTER: + Console.WriteLine($"CHECKPOINT_ZONE_ENTER: {i} | Situation {x.Situation}"); + break; + case ReplayFrameSituation.CHECKPOINT_ZONE_EXIT: + Console.WriteLine($"CHECKPOINT_ZONE_EXIT: {i} | Situation {x.Situation}"); + break; + case ReplayFrameSituation.END_ZONE_ENTER: + Console.WriteLine($"END_ZONE_ENTER: {i} | Situation {x.Situation}"); + break; + case ReplayFrameSituation.END_ZONE_EXIT: + Console.WriteLine($"END_ZONE_EXIT: {i} | Situation {x.Situation}"); + break; + } + } + Console.WriteLine($"========================== =========================="); + } + + /// + /// 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; - string replay_frames = player.ReplayRecorder.SerializeReplay(); - Task updatePlayerRunTask = DB.Write($@" - 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 ({player.Profile.ID}, {player.CurrMap.ID}, {style}, 0, 0, {player.Stats.ThisRun.Ticks}, - {player.Stats.ThisRun.StartVelX}, {player.Stats.ThisRun.StartVelY}, {player.Stats.ThisRun.StartVelZ}, {player.Stats.ThisRun.EndVelX}, {player.Stats.ThisRun.EndVelY}, {player.Stats.ThisRun.EndVelZ}, {(int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()}, '{replay_frames}') - 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); - "); - if (updatePlayerRunTask.Result <= 0) - throw new Exception($"CS2 Surf ERROR >> internal class PersonalBest -> SaveMapTime -> Failed to insert/update player run in database. Player: {player.Profile.Name} ({player.Profile.SteamID})"); - updatePlayerRunTask.Dispose(); + 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; + } - // Will have to LoadMapTimesData right here as well to get the ID of the run we just inserted - // this.SaveCurrentRunCheckpoints(player, DB); // Save checkpoints for this run - // this.LoadCheckpointsForRun(DB); // Re-Load checkpoints for this run + 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 - /// We need the correct `this.ID` to be populated before calling this method otherwise Query will fail /// - public async Task SaveCurrentRunCheckpoints(Player player, TimerDatabase DB) + /// 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.Checkpoint) + // foreach (var item in player.Stats.ThisRun.Checkpoints) + foreach (var item in this.Checkpoints) { int cp = item.Key; int ticks = item.Value!.Ticks; - int runTime = item.Value!.Ticks / 64; // Runtime in decimal + int endTouch = item.Value!.EndTouch; double startVelX = item.Value!.StartVelX; double startVelY = item.Value!.StartVelY; double startVelZ = item.Value!.StartVelZ; @@ -93,35 +199,33 @@ public async Task SaveCurrentRunCheckpoints(Player player, TimerDatabase DB) double endVelZ = item.Value!.EndVelZ; int attempts = item.Value!.Attempts; - #if DEBUG - Console.WriteLine($"CP: {cp} | MapTime ID: {player.Stats.PB[style].ID} | Time: {runTime} | Ticks: {ticks} | startVelX: {startVelX} | startVelY: {startVelY} | startVelZ: {startVelZ} | endVelX: {endVelX} | endVelY: {endVelY} | endVelZ: {endVelZ}"); - Console.WriteLine($@"CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> SaveCurrentRunCheckpoints -> - 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 ({player.Stats.PB[style].ID}, {cp}, {runTime}, {startVelX}, {startVelY}, {startVelZ}, {endVelX}, {endVelY}, {endVelZ}, {attempts}, {ticks}) 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); +#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 +#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 = $@" - 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 ({player.Stats.PB[style].ID}, {cp}, {runTime}, {startVelX}, {startVelY}, {startVelZ}, {endVelX}, {endVelY}, {endVelZ}, {attempts}, {ticks}) - 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); - "; + 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 DB.Transaction(commands); - player.Stats.ThisRun.Checkpoint.Clear(); + 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 2ee50bc..a689a00 100644 --- a/src/ST-Player/PlayerStats/PersonalBest.cs +++ b/src/ST-Player/PlayerStats/PersonalBest.cs @@ -1,14 +1,16 @@ namespace SurfTimer; -// To-do: make Style (currently 0) be dynamic -// To-do: add `Type` +/// +/// 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 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 Checkpoint { get; set; } - // public int Type { get; set; } + 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; } @@ -22,15 +24,199 @@ internal class PersonalBest // Constructor public PersonalBest() { - Ticks = -1; // To-do: what type of value we use here? DB uses DECIMAL but `.Tick` is int??? - Checkpoint = new Dictionary(); - // Type = type; + 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 = 0; + RunDate = -1; + } + + /// + /// 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() + { + if (this == null) + { +#if DEBUG + Console.WriteLine("CS2 Surf ERROR >> internal class PersonalBest -> public async Task PB_LoadCheckpointsData -> PersonalBest object is null."); +#endif + 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 + } + } + } + + // #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 + } + + /// + /// 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) + { + // Console.WriteLine($"CS2 Surf ERROR >> internal class PersonalBest -> public async Task PB_LoadPlayerSpecificMapTimeData -> QUERY:\n{string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_RUNTIME, player.Profile.ID, player.CurrMap.ID, 0, player.Timer.Style)}"); + // using (var results = await SurfTimer.DB.QueryAsync(string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_RUNTIME, player.Profile.ID, player.CurrMap.ID, 0, player.Timer.Style))) + if (this == null) + { +#if DEBUG + Console.WriteLine("CS2 Surf ERROR >> internal class PersonalBest -> public async Task PB_LoadPlayerSpecificMapTimeData -> PersonalBest object is null."); +#endif + + return; + } + + MySqlConnector.MySqlDataReader? results = null; + + // Console.WriteLine(string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_TYPE_RUNTIME, player.Profile.ID, SurfTimer.CurrentMap.ID, this.Type, player.Timer.Style)); + + if (this.ID == -1) + results = await SurfTimer.DB.QueryAsync(string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_TYPE_RUNTIME, player.Profile.ID, SurfTimer.CurrentMap.ID, this.Type, player.Timer.Style)); + else + results = await SurfTimer.DB.QueryAsync(string.Format(Config.MySQL.Queries.DB_QUERY_PB_GET_SPECIFIC_MAPTIME_DATA, this.ID)); + +#if DEBUG + Console.WriteLine($"----> public async Task PB_LoadPlayerSpecificMapTimeData -> this.ID {this.ID} "); + Console.WriteLine($"----> public async Task PB_LoadPlayerSpecificMapTimeData -> this.Ticks {this.Ticks} "); + Console.WriteLine($"----> public async Task PB_LoadPlayerSpecificMapTimeData -> this.RunDate {this.RunDate} "); +#endif + + if (results == null || !results.HasRows) + { + // #if DEBUG + 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 } } \ No newline at end of file diff --git a/src/ST-Player/PlayerStats/PlayerStats.cs b/src/ST-Player/PlayerStats/PlayerStats.cs index 48eed71..c0586c1 100644 --- a/src/ST-Player/PlayerStats/PlayerStats.cs +++ b/src/ST-Player/PlayerStats/PlayerStats.cs @@ -7,144 +7,238 @@ internal 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. - public int[,] StagePB { get; set; } = { { 0, 0 } }; // First dimension: style (0 = normal), second dimension: stage index - public int[,] StageRank { get; set; } = { { 0, 0 } }; // First dimension: style (0 = normal), second dimension: stage index - // - public Dictionary PB { get; set; } = new Dictionary(); - public CurrentRun ThisRun { get; set; } = new CurrentRun(); // This is a CurrenntRun object that tracks the data for the Player's current run - // Initialize PersonalBest for each `style` (e.g., 0 for normal) - this is a temporary solution - // Here we can loop through all available styles at some point and initialize them - public PlayerStats() - { - PB[0] = new PersonalBest(); - // Add more styles as needed - } + // /// + // /// 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 } }; /// - /// Loads the player's MapTimes data from the database along with `Rank` for the run. - /// `Checkpoints` are loaded separately because inside the while loop we cannot run queries. - /// This can populate all the `style` stats the player has for the map - currently only 1 style is supported + /// 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]; + /// + /// 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]; + /// + /// This object tracks data for the Player's current run. /// - public void LoadMapTimesData(Player player, TimerDatabase DB, int playerId = 0, int mapId = 0) + 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() { - Task dbTask2 = DB.Query($@" - 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`) AS `rank` FROM `MapTimes` AS mainquery - WHERE mainquery.`player_id` = {player.Profile.ID} AND mainquery.`map_id` = {player.CurrMap.ID}; - "); - MySqlDataReader playerStats = dbTask2.Result; - int style = 0; // To-do: implement styles - if (!playerStats.HasRows) + // Initialize MapPB for each style + foreach (int style in Config.Styles) { - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadMapTimesData -> No MapTimes data found for Player."); + PB[style] = new PersonalBest(); + PB[style].Type = 0; } - else + + int initialized = 0; + for (int i = 0; i < 32; i++) { - while (playerStats.Read()) - { - // Load data into PersonalBest object - // style = playerStats.GetInt32("style"); // Uncomment when style is implemented - PB[style].ID = playerStats.GetInt32("id"); - 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].Ticks = playerStats.GetInt32("run_time"); - PB[style].RunDate = playerStats.GetInt32("run_date"); - PB[style].Rank = playerStats.GetInt32("rank"); - - Console.WriteLine($"============== CS2 Surf DEBUG >> LoadMapTimesData -> PlayerID: {player.Profile.ID} | Rank: {PB[style].Rank} | ID: {PB[style].ID} | RunTime: {PB[style].Ticks} | SVX: {PB[style].StartVelX} | SVY: {PB[style].StartVelY} | SVZ: {PB[style].StartVelZ} | EVX: {PB[style].EndVelX} | EVY: {PB[style].EndVelY} | EVZ: {PB[style].EndVelZ} | Run Date (UNIX): {PB[style].RunDate}"); - #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadMapTimesData -> PlayerStats.PB (ID {PB[style].ID}) loaded from DB."); - #endif - } + 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++; } - playerStats.Close(); + Console.WriteLine($"====== INITIALIZED {initialized} STAGES AND BONUSES FOR PLAYERSTATS"); } - /// - /// Executes the DB query to get all the checkpoints and store them in the Checkpoint dictionary - /// - public void LoadCheckpointsData(TimerDatabase DB) + // API + public async void LoadMapTime(Player player, int style = 0) { - Task dbTask = DB.Query($"SELECT * FROM `Checkpoints` WHERE `maptime_id` = {PB[0].ID};"); - MySqlDataReader results = dbTask.Result; - if (PB[0] == null) + 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) { - #if DEBUG - Console.WriteLine("CS2 Surf ERROR >> internal class PlayerStats -> LoadCheckpointsData -> PersonalBest object is null."); - #endif - - results.Close(); + Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadMapTime -> No MapTime data found for Player."); return; } - if (PB[0].Checkpoint == null) + 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) { - #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadCheckpointsData -> PB Checkpoints list is not initialized."); - #endif + Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadMapTime -> No Checkpoints data found for Player."); + return; + } - PB[0].Checkpoint = new Dictionary(); // Initialize if null + foreach (var cp in player_maptime.checkpoints) + { + PB[style].Checkpoints[cp.cp] = new Checkpoint(cp.cp, cp.run_time, cp.start_vel_x, cp.start_vel_y, cp.start_vel_z, cp.end_vel_x, cp.end_vel_y, cp.end_vel_z, cp.end_touch, cp.attempts); } + } - #if DEBUG - Console.WriteLine($"this.Checkpoint.Count {PB[0].Checkpoint.Count} "); - Console.WriteLine($"this.ID {PB[0].ID} "); - Console.WriteLine($"this.Ticks {PB[0].Ticks} "); - Console.WriteLine($"this.RunDate {PB[0].RunDate} "); - #endif + // 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; + } - if (!results.HasRows) + foreach (API_MapTime mt in player_maptime) { - #if DEBUG - Console.WriteLine($"CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsData -> No checkpoints found for this mapTimeId {PB[0].ID}."); - #endif + 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; + } + } - results.Close(); + // 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; } - #if DEBUG - Console.WriteLine($"======== CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsData -> Checkpoints found for this mapTimeId"); - #endif - - while (results.Read()) + foreach (API_MapTime mt in player_maptime) { - #if DEBUG - Console.WriteLine($"cp {results.GetInt32("cp")} "); - Console.WriteLine($"run_time {results.GetFloat("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"), // To-do: what type of value we use here? DB uses DECIMAL but `.Tick` is int??? - 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.GetFloat("end_touch"), - results.GetInt32("attempts")); - cp.ID = results.GetInt32("cp"); - // To-do: cp.ID = calculate Rank # from DB - - PB[0].Checkpoint[cp.CP] = cp; - - #if DEBUG - Console.WriteLine($"======= CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsData -> Loaded CP {cp.CP} with RunTime {cp.Ticks}."); - #endif + 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; } - results.Close(); + } - #if DEBUG - Console.WriteLine($"======= CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsData -> Checkpoints loaded from DB. Count: {PB[0].Checkpoint.Count}"); - #endif + + /// + /// Loads the player's map time data from the database along with their ranks. For all types and styles (may not work correctly for Stages/Bonuses) + /// `Checkpoints` are loaded separately from another method in the `PresonalBest` class as it uses the unique `ID` for the run. + /// This 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) + { + using (var playerStats = await SurfTimer.DB.QueryAsync( + string.Format(Config.MySQL.Queries.DB_QUERY_PS_GET_ALL_RUNTIMES, player.Profile.ID, SurfTimer.CurrentMap.ID))) + { + // int style = player.Timer.Style; + int style; + if (!playerStats.HasRows) + { + 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()) + { + // Load data into each PersonalBest object + if (playerStats.GetInt32("type") == 1) // Bonus time + { +#if DEBUG + Console.WriteLine("DEBUG >> (func) LoadPlayerMapTimesData >> BonusPB"); +#endif + int bonusNum = playerStats.GetInt32("stage"); + style = playerStats.GetInt32("style"); // To-do: Uncomment when style is implemented + BonusPB[bonusNum][style].ID = playerStats.GetInt32("id"); + BonusPB[bonusNum][style].Ticks = playerStats.GetInt32("run_time"); + BonusPB[bonusNum][style].Type = playerStats.GetInt32("type"); + BonusPB[bonusNum][style].Rank = playerStats.GetInt32("rank"); + BonusPB[bonusNum][style].StartVelX = (float)playerStats.GetDouble("start_vel_x"); + BonusPB[bonusNum][style].StartVelY = (float)playerStats.GetDouble("start_vel_y"); + BonusPB[bonusNum][style].StartVelZ = (float)playerStats.GetDouble("start_vel_z"); + BonusPB[bonusNum][style].EndVelX = (float)playerStats.GetDouble("end_vel_x"); + BonusPB[bonusNum][style].EndVelY = (float)playerStats.GetDouble("end_vel_y"); + BonusPB[bonusNum][style].EndVelZ = (float)playerStats.GetDouble("end_vel_z"); + BonusPB[bonusNum][style].RunDate = playerStats.GetInt32("run_date"); + } + else if (playerStats.GetInt32("type") == 2) // Stage time + { +#if DEBUG + Console.WriteLine("DEBUG >> (func) LoadPlayerMapTimesData >> StagePB"); +#endif + int stageNum = playerStats.GetInt32("stage"); + style = playerStats.GetInt32("style"); // To-do: Uncomment when style is implemented + StagePB[stageNum][style].ID = playerStats.GetInt32("id"); + StagePB[stageNum][style].Ticks = playerStats.GetInt32("run_time"); + StagePB[stageNum][style].Type = playerStats.GetInt32("type"); + StagePB[stageNum][style].Rank = playerStats.GetInt32("rank"); + StagePB[stageNum][style].StartVelX = (float)playerStats.GetDouble("start_vel_x"); + StagePB[stageNum][style].StartVelY = (float)playerStats.GetDouble("start_vel_y"); + StagePB[stageNum][style].StartVelZ = (float)playerStats.GetDouble("start_vel_z"); + StagePB[stageNum][style].EndVelX = (float)playerStats.GetDouble("end_vel_x"); + StagePB[stageNum][style].EndVelY = (float)playerStats.GetDouble("end_vel_y"); + StagePB[stageNum][style].EndVelZ = (float)playerStats.GetDouble("end_vel_z"); + StagePB[stageNum][style].RunDate = playerStats.GetInt32("run_date"); + Console.WriteLine(@$"DEBUG >> (func) LoadPlayerMapTimesData >> StagePB Loaded: + StagePB[{stageNum}][{style}] = + Stage: {stageNum} | ID: {StagePB[stageNum][style].ID} | Ticks: {StagePB[stageNum][style].Ticks} | Rank: {StagePB[stageNum][style].Rank} | Type: {StagePB[stageNum][style].Type}"); + } + else // Map time + { +#if DEBUG + Console.WriteLine("DEBUG >> (func) LoadPlayerMapTimesData >> MapPB"); +#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}"); +#if DEBUG + Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> public async Task LoadPlayerMapTimesData -> PlayerStats.PB (ID {PB[style].ID}) loaded from DB."); +#endif + } + } } } \ No newline at end of file diff --git a/src/ST-Player/PlayerTimer.cs b/src/ST-Player/PlayerTimer.cs index 5878205..b87ef7d 100644 --- a/src/ST-Player/PlayerTimer.cs +++ b/src/ST-Player/PlayerTimer.cs @@ -10,18 +10,19 @@ internal class PlayerTimer // Modes public bool IsPracticeMode { get; set; } = false; // Practice mode toggle public bool IsStageMode { get; set; } = false; // Stage mode toggle + 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 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 // 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 - // Time Formatting + // Time Formatting - To-do: Move to player settings maybe? public enum TimeFormatStyle { Compact, @@ -39,7 +40,8 @@ public void Reset() this.IsPaused = false; this.IsPracticeMode = false; this.IsStageMode = false; - this.CurrentRunData.Reset(); + 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 f67de67..a5a8b11 100644 --- a/src/ST-Player/Replay/ReplayFrame.cs +++ b/src/ST-Player/Replay/ReplayFrame.cs @@ -1,24 +1,45 @@ namespace SurfTimer; using CounterStrikeSharp.API.Modules.Utils; -using CounterStrikeSharp.API.Core; internal enum ReplayFrameSituation { NONE, - START_RUN, - END_RUN, - TOUCH_CHECKPOINT, - START_STAGE, - END_STAGE + + STAGE_ZONE_ENTER, + STAGE_ZONE_EXIT, + + START_ZONE_ENTER, + START_ZONE_EXIT, + + END_ZONE_ENTER, + END_ZONE_EXIT, + + CHECKPOINT_ZONE_ENTER, + CHECKPOINT_ZONE_EXIT, + + // START_RUN, + // END_RUN, + // TOUCH_CHECKPOINT, + // START_STAGE, + // END_STAGE, + // ENTER_STAGE, } [Serializable] -internal class ReplayFrame +internal class ReplayFrame { - public Vector Pos { get; set; } = new Vector(0, 0, 0); - public QAngle Ang { get; set; } = new QAngle(0, 0, 0); - public uint Situation { get; set; } = (uint)ReplayFrameSituation.NONE; - public ulong Button { get; set; } + 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 MoveType_t MoveType { get; set; } + + public Vector GetPos() + { + return new Vector(this.pos[0], this.pos[1], this.pos[2]); + } + + public QAngle GetAng() + { + return new QAngle(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 new file mode 100644 index 0000000..7291846 --- /dev/null +++ b/src/ST-Player/Replay/ReplayManager.cs @@ -0,0 +1,101 @@ +using System.Text.Json; +using CounterStrikeSharp.API.Core; + +namespace SurfTimer; + +internal 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]; + /// + /// 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 List CustomReplays { get; set; } + + /// + /// + /// + /// ID of the map + /// Does the map have Stages + /// Does the map have Bonuses + /// Frames for the replay + /// Run time (Ticks) for the run + /// Name of the player + /// 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) + { + MapWR = new ReplayPlayer + { + Type = 0, + Stage = 0, + RecordRank = 1, + MapID = map_id, + Frames = frames, + RecordRunTime = run_time, + RecordPlayerName = playerName, + MapTimeID = map_time_id + }; + + if (staged) + { + // Initialize 32 Stages for each style + // TODO: Make the amount of stages dynamic + for (int i = 0; i < 32; i++) + { + AllStageWR[i] = new Dictionary(); + foreach (int x in Config.Styles) + { + AllStageWR[i][x] = new ReplayPlayer(); + } + } + StageWR = new ReplayPlayer(); + } + + if (bonused) + { + // Initialize 32 Stages for each style + // TODO: Make the amount of bonuses dynamic + for (int i = 0; i < 32; i++) + { + AllBonusWR[i] = new Dictionary(); + foreach (int x in Config.Styles) + { + AllBonusWR[i][x] = new ReplayPlayer(); + } + } + BonusWR = new ReplayPlayer(); + } + + CustomReplays = new List(); + } + + public bool IsControllerConnectedToReplayPlayer(CCSPlayerController controller) + { + if (this.MapWR.Controller?.Equals(controller) == true) + return true; + + if (this.StageWR?.Controller?.Equals(controller) == true) + return true; + + if (this.BonusWR?.Controller?.Equals(controller) == true) + return true; + + foreach (var replay in this.CustomReplays) + { + if (replay.Controller?.Equals(controller) == true) + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/ST-Player/Replay/ReplayPlayer.cs b/src/ST-Player/Replay/ReplayPlayer.cs index 98a44c4..6430f07 100644 --- a/src/ST-Player/Replay/ReplayPlayer.cs +++ b/src/ST-Player/Replay/ReplayPlayer.cs @@ -1,14 +1,15 @@ -using System.Text; -using System.Text.Json; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Utils; -using MySqlConnector; namespace SurfTimer; internal class ReplayPlayer { + /// + /// Enable or Disable the replay bots. + /// + public bool IsEnabled { get; set; } = Config.ReplaysEnabled; public bool IsPlaying { get; set; } = false; public bool IsPaused { get; set; } = false; public bool IsPlayable { get; set; } = false; @@ -16,16 +17,33 @@ internal class ReplayPlayer // Tracking for replay counting public int RepeatCount { get; set; } = -1; - // Stats for replay displaying - public string Stat_Prefix { get; set; } = "WR"; - public string Stat_PlayerName { get; set; } = "N/A"; - public int Stat_MapTimeID { get; set; } = -1; - public int Stat_RunTime { get; set; } = 0; - public bool Stat_IsRunning { get; set; } = false; - public int Stat_RunTick { get; set; } = 0; + public int MapID { get; set; } = -1; + public int MapTimeID { get; set; } = -1; + public int Type { get; set; } = -1; + public int Stage { get; set; } = -1; + + public int RecordRank { get; set; } = -1; // This is used to determine whether replay is for wr or for pb + public string RecordPlayerName { get; set; } = "N/A"; + public int RecordRunTime { get; set; } = -1; + public int ReplayCurrentRunTime { get; set; } = 0; + public bool IsReplayOutsideZone { get; set; } = false; // Tracking public List Frames { get; set; } = new List(); + public List StageEnterSituations { get; set; } = new List(); + public List StageExitSituations { get; set; } = new List(); + public List CheckpointEnterSituations { get; set; } = new List(); + public List CheckpointExitSituations { get; set; } = new List(); + /// + /// Indexes should always follow this pattern: START_ZONE_ENTER > START_ZONE_EXIT > END_ZONE_ENTER > END_ZONE_EXIT + /// Where END_ZONE_EXIT is not guaranteed + /// + public List MapSituations { get; set; } = new List(); + /// + /// Indexes should always follow this pattern: START_ZONE_ENTER > START_ZONE_EXIT > END_ZONE_ENTER > END_ZONE_EXIT + /// Where END_ZONE_EXIT is not guaranteed + /// + public List BonusSituations { get; set; } = new List(); // Playing public int CurrentFrameTick { get; set; } = 0; @@ -33,18 +51,18 @@ internal class ReplayPlayer public CCSPlayerController? Controller { get; set; } - public void ResetReplay() + public void ResetReplay() { this.CurrentFrameTick = 0; this.FrameTickIncrement = 1; - if(this.RepeatCount > 0) + if (this.RepeatCount > 0) this.RepeatCount--; - this.Stat_IsRunning = false; - this.Stat_RunTick = 0; + this.IsReplayOutsideZone = false; + this.ReplayCurrentRunTime = 0; } - public void Reset() + public void Reset() { this.IsPlaying = false; this.IsPaused = false; @@ -61,35 +79,44 @@ public void Reset() public void SetController(CCSPlayerController c, int repeat_count = -1) { this.Controller = c; - this.RepeatCount = repeat_count; + if (repeat_count != -1) + this.RepeatCount = repeat_count; this.IsPlayable = true; + + // Console.WriteLine($"===== public void SetController -> Set controller for {c.PlayerName}"); } - public void Start() + public void Start() { - if (!this.IsPlayable) + if (!this.IsPlayable || !this.IsEnabled) return; 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})"); } - public void Stop() + public void Stop() { 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})"); } - public void Pause() + public void Pause() { - if (!this.IsPlaying) + if (!this.IsPlaying || !this.IsEnabled) return; this.IsPaused = !this.IsPaused; - this.Stat_IsRunning = !this.Stat_IsRunning; + 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})"); } - public void Tick() + public void Tick() { - if (!this.IsPlaying || !this.IsPlayable || this.Frames.Count == 0) + if (this.MapID == -1 || !this.IsEnabled || !this.IsPlaying || !this.IsPlayable || this.Frames.Count == 0) return; ReplayFrame current_frame = this.Frames[this.CurrentFrameTick]; @@ -97,102 +124,105 @@ public void Tick() // SOME BLASHPEMY FOR YOU if (this.FrameTickIncrement >= 0) { - if (current_frame.Situation == (uint)ReplayFrameSituation.START_RUN) + if (current_frame.Situation == ReplayFrameSituation.START_ZONE_EXIT) { - this.Stat_IsRunning = true; - this.Stat_RunTick = 0; + this.IsReplayOutsideZone = true; + this.ReplayCurrentRunTime = 0; } - else if (current_frame.Situation == (uint)ReplayFrameSituation.END_RUN) + else if (current_frame.Situation == ReplayFrameSituation.END_ZONE_ENTER) { - this.Stat_IsRunning = false; + this.IsReplayOutsideZone = false; } } else { - if (current_frame.Situation == (uint)ReplayFrameSituation.START_RUN) + if (current_frame.Situation == ReplayFrameSituation.START_ZONE_EXIT) { - this.Stat_IsRunning = false; + this.IsReplayOutsideZone = false; } - else if (current_frame.Situation == (uint)ReplayFrameSituation.END_RUN) + else if (current_frame.Situation == ReplayFrameSituation.END_ZONE_ENTER) { - this.Stat_IsRunning = true; - this.Stat_RunTick = this.CurrentFrameTick - (64*2); // (64*2) counts for the 2 seconds before run actually starts + this.IsReplayOutsideZone = true; + this.ReplayCurrentRunTime = this.CurrentFrameTick - (64 * 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_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; + Vector velocity = (current_frame_pos - current_pos) * 64; if (is_on_ground) this.Controller.PlayerPawn.Value.MoveType = MoveType_t.MOVETYPE_WALK; else 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)); - else - this.Controller.PlayerPawn.Value.Teleport(new Vector(nint.Zero), current_frame.Ang, velocity); - + if ((current_pos - current_frame_pos).Length() > 200) + this.Controller.PlayerPawn.Value.Teleport(current_frame_pos, current_frame_ang, new Vector(nint.Zero)); + else + this.Controller.PlayerPawn.Value.Teleport(new Vector(nint.Zero), current_frame_ang, velocity); + if (!this.IsPaused) { this.CurrentFrameTick = Math.Max(0, this.CurrentFrameTick + this.FrameTickIncrement); - if (this.Stat_IsRunning) - this.Stat_RunTick = Math.Max(0, this.Stat_RunTick + this.FrameTickIncrement); + if (this.IsReplayOutsideZone) + this.ReplayCurrentRunTime = Math.Max(0, this.ReplayCurrentRunTime + this.FrameTickIncrement); } - if(this.CurrentFrameTick >= this.Frames.Count) + 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(TimerDatabase DB) + public void LoadReplayData(int repeat_count = -1) { - if (!this.IsPlayable) + if (!this.IsPlayable || !this.IsEnabled) return; - Task dbTask = DB.Query($@" - SELECT MapTimes.replay_frames, MapTimes.run_time, Player.name - FROM MapTimes - JOIN Player ON MapTimes.player_id = Player.id - WHERE MapTimes.id={this.Stat_MapTimeID} - "); + // 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}"); - MySqlDataReader mapTimeReplay = dbTask.Result; - if(!mapTimeReplay.HasRows) + if (this.MapID == -1) { - Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerReplay -> Load -> No replay data found for Player."); + 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."); + return; } - else - { - JsonSerializerOptions options = new JsonSerializerOptions {WriteIndented = false, Converters = { new VectorConverter(), new QAngleConverter() }}; - while(mapTimeReplay.Read()) - { - string json = Compressor.Decompress(Encoding.UTF8.GetString((byte[])mapTimeReplay[0])); - this.Frames = JsonSerializer.Deserialize>(json, options)!; - this.Stat_RunTime = mapTimeReplay.GetInt32("run_time"); - this.Stat_PlayerName = mapTimeReplay.GetString("name"); - } - FormatBotName(); - } - mapTimeReplay.Close(); - dbTask.Dispose(); + // 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}"); + this.ResetReplay(); + this.RepeatCount = repeat_count; } - private void FormatBotName() + public void FormatBotName() { - if (!this.IsPlayable) + if (!this.IsPlayable || !this.IsEnabled) return; + string prefix; + if (this.RecordRank == 1) + { + prefix = "WR"; + } + else + { + prefix = $"Rank #{this.RecordRank}"; + } + + if (this.Type == 1) + prefix = prefix + $"B {this.Stage}"; + else if (this.Type == 2) + prefix = prefix + $"CP {this.Stage}"; + SchemaString bot_name = new SchemaString(this.Controller!, "m_iszPlayerName"); - string replay_name = $"[{this.Stat_Prefix}] {this.Stat_PlayerName} | {PlayerHUD.FormatTime(this.Stat_RunTime)}"; - if(this.Stat_RunTime <= 0) - replay_name = $"[{this.Stat_Prefix}] {this.Stat_PlayerName}"; + 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"); diff --git a/src/ST-Player/Replay/ReplayRecorder.cs b/src/ST-Player/Replay/ReplayRecorder.cs index fd20ed5..61e0a8c 100644 --- a/src/ST-Player/Replay/ReplayRecorder.cs +++ b/src/ST-Player/Replay/ReplayRecorder.cs @@ -1,40 +1,63 @@ using System.Text.Json; using CounterStrikeSharp.API.Modules.Utils; - namespace SurfTimer; internal class ReplayRecorder { public bool IsRecording { get; set; } = false; + public bool IsSaving { get; set; } = false; public ReplayFrameSituation CurrentSituation { get; set; } = ReplayFrameSituation.NONE; public List Frames { get; set; } = new List(); + public List StageEnterSituations { get; set; } = new List(); + public List StageExitSituations { get; set; } = new List(); + public List CheckpointEnterSituations { get; set; } = new List(); + public List CheckpointExitSituations { get; set; } = new List(); + /// + /// Indexes should always follow this pattern: START_ZONE_ENTER > START_ZONE_EXIT > END_ZONE_ENTER > END_ZONE_EXIT + /// Where END_ZONE_EXIT is not guaranteed + /// + public List MapSituations { get; set; } = new List(); + /// + /// Indexes should always follow this pattern: START_ZONE_ENTER > START_ZONE_EXIT > END_ZONE_ENTER > END_ZONE_EXIT + /// Where END_ZONE_EXIT is not guaranteed + /// + public List BonusSituations { get; set; } = new List(); - public void Reset() + public void Reset() { this.IsRecording = false; this.Frames.Clear(); + this.StageEnterSituations.Clear(); + this.StageExitSituations.Clear(); + this.CheckpointEnterSituations.Clear(); + this.CheckpointExitSituations.Clear(); + this.MapSituations.Clear(); + this.BonusSituations.Clear(); + + Console.WriteLine($"===== ReplayRecorder -> Reset() -> Recording has been reset"); } - public void Start() + public void Start() { this.IsRecording = true; } - public void Stop() + public void Stop() { this.IsRecording = false; } - public void Tick(Player player) + public void Tick(Player player) { if (!this.IsRecording || player == null) return; - // Disabeling Recording if timer disabled - if (!player.Timer.IsEnabled) + // Disabling Recording if timer disabled + if (!player.Timer.IsEnabled) { this.Stop(); this.Reset(); + Console.WriteLine($"===== ReplayRecorder -> Tick() -> Recording has stopped and reset"); return; } @@ -43,15 +66,35 @@ public void Tick(Player player) 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; - - var frame = new ReplayFrame +/* + switch (this.CurrentSituation) { - Pos = new Vector(player_pos.X, player_pos.Y, player_pos.Z), - Ang = new QAngle(player_angle.X, player_angle.Y, player_angle.Z), - Situation = (uint)this.CurrentSituation, - Button = player_button, + 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 frame = new ReplayFrame + { + pos = [player_pos.X, player_pos.Y, player_pos.Z], + ang = [player_angle.X, player_angle.Y, player_angle.Z], + Situation = this.CurrentSituation, Flags = player_flags, - MoveType = player_move_type, }; this.Frames.Add(frame); @@ -62,8 +105,226 @@ public void Tick(Player player) public string SerializeReplay() { - JsonSerializerOptions options = new JsonSerializerOptions {WriteIndented = false, Converters = { new VectorConverter(), new QAngleConverter() }}; - string replay_frames = JsonSerializer.Serialize(Frames, options); + // JsonSerializerOptions options = new JsonSerializerOptions {WriteIndented = false, Converters = { new VectorConverter(), new QAngleConverter() }}; + // string replay_frames = JsonSerializer.Serialize(Frames, options); + string replay_frames = JsonSerializer.Serialize(Frames); + return Compressor.Compress(replay_frames); + } + + public string SerializeReplayPortion(int start_idx, int end_idx) // Not used anymore + { + // JsonSerializerOptions options = new JsonSerializerOptions {WriteIndented = false, Converters = { new VectorConverter(), new QAngleConverter() }}; + // string replay_frames = JsonSerializer.Serialize(Frames.GetRange(start_idx, end_idx), options); + string replay_frames = JsonSerializer.Serialize(Frames.GetRange(start_idx, end_idx)); return Compressor.Compress(replay_frames); } + + public void SetLastTickSituation(ReplayFrameSituation situation) + { + if (this.Frames.Count == 0) + return; + + this.Frames[this.Frames.Count - 2].Situation = situation; + } + + public string TrimReplay(Player player, int type = 0, bool lastStage = false) + { + this.IsSaving = true; + + List new_frames = new List(); + + if (this.Frames.Count == 0) + { + Console.WriteLine($"======== internal class ReplayRecorder -> public string TrimReplay -> There are no Frames for trimming"); + throw new Exception("There are no Frames available for trimming"); + } + switch (type) + { + 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; + } + + this.IsSaving = false; + string trimmed = JsonSerializer.Serialize(new_frames); + return Compressor.Compress(trimmed); + } + + 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--) + { + 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; + } + return 0; + } + + public int LastExitTick(int start_idx = 0) + { + if (start_idx == 0) + start_idx = this.Frames.Count - 1; + for (int i = start_idx; i > 0; i--) + { + 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 0; + } } \ No newline at end of file diff --git a/src/ST-UTILS/Config.cs b/src/ST-UTILS/Config.cs new file mode 100644 index 0000000..d972cb3 --- /dev/null +++ b/src/ST-UTILS/Config.cs @@ -0,0 +1,206 @@ +using System.Reflection; +using System.Text.Json; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Modules.Utils; + +namespace SurfTimer; + +public static class Config +{ + 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(); + + /// + /// Placeholder for amount of styles + /// + public static List Styles = new List { 0 }; // Add all supported style IDs + + public static bool ReplaysEnabled => true; + public static int ReplaysPre => 64; + // Helper class/methods for configuration loading + private static class ConfigLoader + { + private static readonly Dictionary _configDocuments = new(); + + public static JsonDocument GetConfigDocument(string configPath) + { + if (!_configDocuments.ContainsKey(configPath)) + { + var fullPath = Server.GameDirectory + configPath; + _configDocuments[configPath] = JsonDocument.Parse(File.ReadAllText(fullPath)); + } + return _configDocuments[configPath]; + } + } + + 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 + /// + /// A value containing the URL. + public static string GetApiUrl() + { + return ConfigDocument.RootElement.GetProperty("api_url").GetString()!; + } + + /// + /// Retrieves the `api_enabled` value from the configuration path + /// + /// A value for whether the API should be used. + public static bool GetApiOnly() + { + return ConfigDocument.RootElement.GetProperty("api_enabled").GetBoolean(); + } + + /// + /// Contains all the endpoints used by the API for the SurfTimer plugin. + /// + public static class Endpoints + { + // 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}"; + + // CurrentRun.cs + public const string ENDPOINT_CR_SAVE_STAGE_TIME = "/surftimer/savestagetime"; + } + } + + public static class MySQL + { + private const string DB_CONFIG_PATH = "/csgo/cfg/SurfTimer/database.json"; + + private static JsonDocument ConfigDocument => ConfigLoader.GetConfigDocument(DB_CONFIG_PATH); + + /// + /// Retrieves the connection details for connecting to the MySQL Database + /// + /// A connection + public static string GetConnectionString() + { + string host = ConfigDocument.RootElement.GetProperty("host").GetString()!; + string database = ConfigDocument.RootElement.GetProperty("database").GetString()!; + string user = ConfigDocument.RootElement.GetProperty("user").GetString()!; + string password = ConfigDocument.RootElement.GetProperty("password").GetString()!; + 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}"); + + 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/SurfTimer.cs b/src/SurfTimer.cs index b1344df..d0517a9 100644 --- a/src/SurfTimer.cs +++ b/src/SurfTimer.cs @@ -25,13 +25,10 @@ You should have received a copy of the GNU Affero General Public License #define DEBUG -using System.Text.Json; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Core.Attributes.Registration; -using CounterStrikeSharp.API.Modules.Memory; -using CounterStrikeSharp.API.Modules.Utils; namespace SurfTimer; @@ -40,26 +37,34 @@ namespace SurfTimer; public partial class SurfTimer : BasePlugin { // Metadata - public override string ModuleName => "CS2 SurfTimer"; + public override string ModuleName => $"CS2 {Config.PluginName}"; public override string ModuleVersion => "DEV-1"; public override string ModuleDescription => "Official Surf Timer by the CS2 Surf Initiative."; public override string ModuleAuthor => "The CS2 Surf Initiative - github.com/cs2surf"; - public string PluginPrefix => $"[{ChatColors.DarkBlue}CS2 Surf{ChatColors.Default}]"; // To-do: make configurable // Globals private Dictionary playerList = new Dictionary(); // This can probably be done way better, revisit - internal TimerDatabase? DB = new TimerDatabase(); - public string PluginPath = Server.GameDirectory + "/csgo/addons/counterstrikesharp/plugins/SurfTimer/"; - internal Map CurrentMap = null!; + 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!; /* ========== MAP START HOOKS ========== */ public void OnMapStart(string mapName) { // Initialise Map Object - // To-do: It seems like players connect very quickly and sometimes `CurrentMap` is null when it shouldn't be, lowered the timer ot 1.0 seconds for now - if ((CurrentMap == null || CurrentMap.Name != mapName) && mapName.Contains("surf_")) + if ((CurrentMap == null || !CurrentMap.Name.Equals(mapName)) && mapName.Contains("surf_")) { - AddTimer(1.0f, () => CurrentMap = new Map(mapName, DB!)); // Was 3 seconds, now 1 second + 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 } } @@ -88,30 +93,22 @@ public HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info) /* ========== PLUGIN LOAD ========== */ public override void Load(bool hotReload) { - // Load database config & spawn database object - try + // Check if we have connected to the Database + if (DB != null) { - JsonElement dbConfig = JsonDocument.Parse(File.ReadAllText(Server.GameDirectory + "/csgo/cfg/SurfTimer/database.json")).RootElement; - DB = new TimerDatabase(dbConfig.GetProperty("host").GetString()!, - dbConfig.GetProperty("database").GetString()!, - dbConfig.GetProperty("user").GetString()!, - dbConfig.GetProperty("password").GetString()!, - dbConfig.GetProperty("port").GetInt32(), - dbConfig.GetProperty("timeout").GetInt32()); Console.WriteLine("[CS2 Surf] Database connection established."); } - - catch (Exception e) + else { - Console.WriteLine($"[CS2 Surf] Error loading database config: {e.Message}"); + Console.WriteLine($"[CS2 Surf] Error connecting to the database."); // To-do: Abort plugin loading } Console.WriteLine(String.Format(" ____________ ____ ___\n" + " / ___/ __/_ | / __/_ ______/ _/\n" + "/ /___\\ \\/ __/ _\\ \\/ // / __/ _/ \n" - + "\\___/___/____/ /___/\\_,_/_/ /_/\n" - + $"[CS2 Surf] SurfTimer plugin loaded. Version: {ModuleVersion}" + + "\\___/___/____/ /___/\\_,_/_/ /_/\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" )); @@ -122,10 +119,6 @@ public override void Load(bool hotReload) // Tick listener RegisterListener(OnTick); - // StartTouch Hook -- DEPRECATE BROKEN CS2 7TH FEB 2024 - // VirtualFunctions.CBaseTrigger_StartTouchFunc.Hook(OnTriggerStartTouch, HookMode.Post); - // EndTouch Hook -- DEPRECATE BROKEN CS2 7TH FEB 2024 - // VirtualFunctions.CBaseTrigger_EndTouchFunc.Hook(OnTriggerEndTouch, HookMode.Post); HookEntityOutput("trigger_multiple", "OnStartTouch", OnTriggerStartTouch); HookEntityOutput("trigger_multiple", "OnEndTouch", OnTriggerEndTouch); diff --git a/src/SurfTimer.csproj b/src/SurfTimer.csproj index 87e253f..82404c6 100644 --- a/src/SurfTimer.csproj +++ b/src/SurfTimer.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable true @@ -13,9 +13,9 @@ - + - + diff --git a/src/bin/MaxMind.Db.dll b/src/bin/MaxMind.Db.dll new file mode 100644 index 0000000..dadb1f6 Binary files /dev/null and b/src/bin/MaxMind.Db.dll differ diff --git a/src/bin/MaxMind.GeoIP2.dll b/src/bin/MaxMind.GeoIP2.dll new file mode 100644 index 0000000..a665688 Binary files /dev/null and b/src/bin/MaxMind.GeoIP2.dll differ diff --git a/src/bin/MySqlConnector.dll b/src/bin/MySqlConnector.dll new file mode 100644 index 0000000..ea3237d Binary files /dev/null and b/src/bin/MySqlConnector.dll differ