diff --git a/README.md b/README.md index 4663617..23a3900 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,11 @@ Bold & Italics = being worked on. - [ ] 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 #`)**_ + - [X] Support for stages (`/rs`, teleporting with `/s`) + - [X] Support for bonuses (`/rs`, teleporting with `/b #`) - [ ] _**Start/End touch hooks implemented for all zones**_ - [ ] Surf configs - [X] Server settings configuration @@ -44,7 +44,7 @@ Bold & Italics = being worked on. - [x] Map times - [x] Checkpoint times - [ ] Stage times - - [ ] Bonus times + - [X] Bonus times - [X] Practice Mode implementation - [ ] Announce records to Discord - [ ] Stretch goal: sub-tick timing diff --git a/cfg/SurfTimer/server_settings.cfg b/cfg/SurfTimer/server_settings.cfg index b67b24f..c1741e9 100644 --- a/cfg/SurfTimer/server_settings.cfg +++ b/cfg/SurfTimer/server_settings.cfg @@ -76,5 +76,6 @@ mp_warmup_end mp_warmuptime 0 sv_holiday_mode 0 sv_party_mode 0 +sv_hibernate_when_empty 0 sv_cheats 0 \ No newline at end of file diff --git a/src/ST-Commands/PlayerCommands.cs b/src/ST-Commands/PlayerCommands.cs index 3e51527..89a5850 100644 --- a/src/ST-Commands/PlayerCommands.cs +++ b/src/ST-Commands/PlayerCommands.cs @@ -32,43 +32,62 @@ public void PlayerResetStage(CCSPlayerController? player, CommandInfo command) // 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))); + + if (SurfPlayer.Timer.IsBonusMode) + { + if (SurfPlayer.Timer.Bonus != 0 && CurrentMap.BonusStartZone[SurfPlayer.Timer.Bonus] != new Vector(0, 0, 0)) + Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.BonusStartZone[SurfPlayer.Timer.Bonus], CurrentMap.BonusStartZoneAngles[SurfPlayer.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 (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; } [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) return; - int stage = Int32.Parse(command.ArgByIndex(1)) - 1; - if (stage > CurrentMap.Stages - 1 && CurrentMap.Stages > 0) - stage = CurrentMap.Stages - 1; + int stage = Int32.Parse(command.ArgByIndex(1)); // 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))}"); + 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 "); return; } + else if (CurrentMap.Stages <= 0) { player.PrintToChat($"{PluginPrefix} {ChatColors.Red}This map has no stages."); return; } + else if (stage > CurrentMap.Stages) + { + player.PrintToChat($"{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) + if (stage == 1) Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StartZone, CurrentMap.StartZoneAngles, new Vector(0, 0, 0))); else Server.NextFrame(() => player.PlayerPawn.Value!.Teleport(CurrentMap.StageStartZone[stage], CurrentMap.StageStartZoneAngles[stage], new Vector(0, 0, 0))); @@ -84,6 +103,55 @@ public void PlayerGoToStage(CCSPlayerController? player, CommandInfo command) player.PrintToChat($"{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) + return; + + int bonus; + + // Check for argument count + if (command.ArgCount < 2) + { + if (CurrentMap.Bonuses > 0) + bonus = 1; + else + { + player.PrintToChat($"{PluginPrefix} {ChatColors.Red}Invalid arguments. Usage: {ChatColors.Green}!bonus "); + return; + } + } + + else + bonus = Int32.Parse(command.ArgByIndex(1)); + + if (CurrentMap.Bonuses <= 0) + { + player.PrintToChat($"{PluginPrefix} {ChatColors.Red}This map has no bonuses."); + return; + } + + else if (bonus > CurrentMap.Bonuses) + { + player.PrintToChat($"{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($"{PluginPrefix} {ChatColors.Red}Invalid bonus provided. Usage: {ChatColors.Green}!bonus "); + } + [ConsoleCommand("css_spec", "Moves a player automaticlly into spectator mode")] public void MovePlayerToSpectator(CCSPlayerController? player, CommandInfo command) { diff --git a/src/ST-Events/TriggerEndTouch.cs b/src/ST-Events/TriggerEndTouch.cs index 65ddfc4..5118cb3 100644 --- a/src/ST-Events/TriggerEndTouch.cs +++ b/src/ST-Events/TriggerEndTouch.cs @@ -6,8 +6,11 @@ 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 + /// + /// CounterStrikeSharp.API.Core.HookResult + /// internal HookResult OnTriggerEndTouch(CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, float delay) { // CBaseTrigger trigger = handler.GetParam(0); @@ -50,7 +53,7 @@ internal HookResult OnTriggerEndTouch(CEntityIOOutput output, string name, CEnti } // MAP START ZONE - if (!player.Timer.IsStageMode) + if (!player.Timer.IsStageMode && !player.Timer.IsBonusMode) { player.Timer.Start(); player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.START_RUN; @@ -103,7 +106,7 @@ internal HookResult OnTriggerEndTouch(CEntityIOOutput output, string name, CEnti 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.EndTouch = player.Timer.Ticks; 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; @@ -139,7 +142,7 @@ internal HookResult OnTriggerEndTouch(CEntityIOOutput output, string name, CEnti 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.EndTouch = player.Timer.Ticks; 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; @@ -152,6 +155,27 @@ internal HookResult OnTriggerEndTouch(CEntityIOOutput output, string name, CEnti // Handle the case where the index is out of bounds } } + + // 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 + + // BONUS START ZONE + if (!player.Timer.IsStageMode && player.Timer.IsBonusMode) + { + player.Timer.Start(); + // To-do: bonus replay + } + + // 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 + } } return HookResult.Continue; diff --git a/src/ST-Events/TriggerStartTouch.cs b/src/ST-Events/TriggerStartTouch.cs index c0af249..4ebc09c 100644 --- a/src/ST-Events/TriggerStartTouch.cs +++ b/src/ST-Events/TriggerStartTouch.cs @@ -7,8 +7,11 @@ 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); @@ -97,7 +100,7 @@ internal HookResult OnTriggerStartTouch(CEntityIOOutput output, string name, CEn #endif // Add entry in DB for the run - if(!player.Timer.IsPracticeMode) { + 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) @@ -144,7 +147,7 @@ internal HookResult OnTriggerStartTouch(CEntityIOOutput output, string name, CEn // 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) - 1; + int stage = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value); player.Timer.Stage = stage; #if DEBUG @@ -156,10 +159,10 @@ internal HookResult OnTriggerStartTouch(CEntityIOOutput output, string name, CEn // 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) { - player.Timer.Checkpoint = stage; // Stage = Checkpoint when in a run on a Staged map + player.Timer.Checkpoint = stage - 1; // Stage = Checkpoint when in a run on a Staged map #if DEBUG - Console.WriteLine($"============== Initial entity value: {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} | Assigned to `stage`: {Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value) - 1}"); + 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)}"); Console.WriteLine($"CS2 Surf DEBUG >> CBaseTrigger_StartTouchFunc (Stage start zones) -> player.Stats.PB[{style}].Checkpoint.Count = {player.Stats.PB[style].Checkpoint.Count}"); #endif @@ -167,7 +170,7 @@ internal HookResult OnTriggerStartTouch(CEntityIOOutput output, string name, CEn player.HUD.DisplayCheckpointMessages(PluginPrefix); // store the checkpoint in the player's current run checkpoints used for Checkpoint functionality - Checkpoint cp2 = new Checkpoint(stage, + Checkpoint cp2 = new Checkpoint(player.Timer.Checkpoint, player.Timer.Ticks, velocity_x, velocity_y, @@ -177,7 +180,7 @@ internal HookResult OnTriggerStartTouch(CEntityIOOutput output, string name, CEn -1.0f, -1.0f, 0); - player.Stats.ThisRun.Checkpoint[stage] = cp2; + player.Stats.ThisRun.Checkpoint[player.Timer.Checkpoint] = cp2; } #if DEBUG @@ -195,7 +198,7 @@ internal HookResult OnTriggerStartTouch(CEntityIOOutput output, string name, CEn if (player.Timer.IsRunning && !player.Timer.IsStageMode && player.Stats.ThisRun.Checkpoint.Count < checkpoint) { #if DEBUG - Console.WriteLine($"============== Initial entity value: {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} | Assigned to `checkpoint`: {Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value) - 1}"); + 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[{style}].Checkpoint.Count = {player.Stats.PB[style].Checkpoint.Count}"); #endif @@ -220,6 +223,86 @@ internal HookResult OnTriggerStartTouch(CEntityIOOutput output, string name, CEn 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) + { + // We only want this working if they're in bonus mode, ignore otherwise. + if (player.Timer.IsBonusMode) + { + player.ReplayRecorder.Start(); // Start replay recording + + player.Timer.Reset(); + player.Timer.IsBonusMode = true; + int bonus = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value); + player.Timer.Bonus = bonus; + + 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) + { + // We only want this working if they're in bonus mode, ignore otherwise. + if (player.Timer.IsBonusMode && player.Timer.IsRunning) + { + // To-do: verify the bonus trigger being hit! + int bonus = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value); + if (bonus != player.Timer.Bonus) + { + // Exit hook as this end zone is not relevant to the player's current bonus + return HookResult.Continue; + } + + player.Timer.Stop(); + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.END_RUN; + + 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 + + string PracticeString = ""; + if (player.Timer.IsPracticeMode) + PracticeString = $"({ChatColors.Grey}Practice{ChatColors.Default}) "; + + // To-do: make Style (currently 0) be dynamic + if (player.Stats.BonusPB[bonus][style].Ticks <= 0) // Player first ever PB for the bonus + { + Server.PrintToChatAll($"{PluginPrefix} {PracticeString}{player.Controller.PlayerName} finished bonus {bonus} in {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default} ({player.Timer.Ticks})!"); + } + else if (player.Timer.Ticks < player.Stats.BonusPB[bonus][style].Ticks) // Player beating their existing PB for the bonus + { + Server.PrintToChatAll($"{PluginPrefix} {PracticeString}{ChatColors.Lime}{player.Profile.Name}{ChatColors.Default} beat their bonus {bonus} PB in {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default} (Old: {ChatColors.BlueGrey}{PlayerHUD.FormatTime(player.Stats.BonusPB[bonus][style].Ticks)}{ChatColors.Default})!"); + } + else // Player did not beat their existing personal best for the bonus + { + player.Controller.PrintToChat($"{PluginPrefix} {PracticeString}You finished bonus {bonus} 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 (Bonus end zone) -> DB object is null, this shouldn't happen."); + + player.Stats.BonusPB[bonus][style].Ticks = player.Timer.Ticks; // Reload the run_time for the HUD and also assign for the DB query + + // To-do: save to DB + if (!player.Timer.IsPracticeMode) + { + AddTimer(1.5f, () => { + player.Stats.ThisRun.SaveMapTime(player, DB, bonus); // Save the bonus time PB data + player.Stats.LoadMapTimesData(player, DB); // Load the MapTime PB data again (will refresh the MapTime ID for the Checkpoints query) + CurrentMap.GetMapRecordAndTotals(DB); // Reload the Map record and totals for the HUD + }); + } + } + } } return HookResult.Continue; diff --git a/src/ST-Map/Map.cs b/src/ST-Map/Map.cs index c1fb166..8155bac 100644 --- a/src/ST-Map/Map.cs +++ b/src/ST-Map/Map.cs @@ -19,8 +19,22 @@ internal class Map public bool Ranked {get; set;} = false; public int DateAdded {get; set;} = 0; public int LastPlayed {get; set;} = 0; - public int TotalCompletions {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]; + /// + /// 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]; public List ConnectedMapTimes { get; set; } = new List(); public List ReplayBots { get; set; } = new List { new ReplayPlayer() }; @@ -40,11 +54,23 @@ internal class Map public Vector[] CheckpointStartZone {get;} = Enumerable.Repeat(0, 99).Select(x => new Vector(0,0,0)).ToArray(); // Constructor + // 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 Map(string Name, TimerDatabase DB) { // Set map name this.Name = Name; + + // Initialize WR variables this.WR[0] = new PersonalBest(); // To-do: Implement styles + for (int i = 0; i < 32; i++) + { + this.BonusWR[i] = new Dictionary(); + this.BonusWR[i][0] = new PersonalBest(); // To-do: Implement styles + this.BonusCompletions[i] = new Dictionary(); + } + // Gathering zones from the map IEnumerable triggers = Utilities.FindAllEntitiesByDesignerName("trigger_multiple"); // Gathering info_teleport_destinations from the map @@ -98,8 +124,8 @@ internal Map(string Name, TimerDatabase DB) 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,14 +134,15 @@ 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) { - 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.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.Checkpoints++; // Might be useful to have this in DB entry } @@ -131,8 +158,8 @@ internal Map(string Name, TimerDatabase DB) 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,17 +168,20 @@ 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) { - 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 @@ -190,8 +220,6 @@ internal Map(string Name, TimerDatabase DB) 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; @@ -254,14 +282,14 @@ internal void GetMapRecordAndTotals(TimerDatabase DB, int style = 0 ) // To-do: { // Get map world records Task reader = DB.Query($@" - SELECT MapTimes.*, Player.name + SELECT MapTimes.*, MIN(MapTimes.run_time) AS minimum, Player.name FROM MapTimes JOIN Player ON MapTimes.player_id = Player.id WHERE MapTimes.map_id = {this.ID} AND MapTimes.style = {style} + GROUP BY MapTimes.type ORDER BY MapTimes.run_time ASC; "); MySqlDataReader mapWrData = reader.Result; - int totalRows = 0; if (mapWrData.HasRows) { @@ -270,10 +298,26 @@ FROM MapTimes this.ConnectedMapTimes.Clear(); while (mapWrData.Read()) { - 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 :) - { + if (mapWrData.GetInt32("type") > 0) + { + this.BonusWR[mapWrData.GetInt32("type")][style].ID = mapWrData.GetInt32("id"); // WR ID for the Map and Style combo + this.BonusWR[mapWrData.GetInt32("type")][style].Ticks = mapWrData.GetInt32("run_time"); // Fastest run time (WR) for the Map and Style combo + this.BonusWR[mapWrData.GetInt32("type")][style].Type = mapWrData.GetInt32("type"); // Bonus type (0 = map, 1+ = bonus index) + this.BonusWR[mapWrData.GetInt32("type")][style].StartVelX = mapWrData.GetFloat("start_vel_x"); // Fastest run start velocity X for the Map and Style combo + this.BonusWR[mapWrData.GetInt32("type")][style].StartVelY = mapWrData.GetFloat("start_vel_y"); // Fastest run start velocity Y for the Map and Style combo + this.BonusWR[mapWrData.GetInt32("type")][style].StartVelZ = mapWrData.GetFloat("start_vel_z"); // Fastest run start velocity Z for the Map and Style combo + this.BonusWR[mapWrData.GetInt32("type")][style].EndVelX = mapWrData.GetFloat("end_vel_x"); // Fastest run end velocity X for the Map and Style combo + this.BonusWR[mapWrData.GetInt32("type")][style].EndVelY = mapWrData.GetFloat("end_vel_y"); // Fastest run end velocity Y for the Map and Style combo + this.BonusWR[mapWrData.GetInt32("type")][style].EndVelZ = mapWrData.GetFloat("end_vel_z"); // Fastest run end velocity Z for the Map and Style combo + this.BonusWR[mapWrData.GetInt32("type")][style].RunDate = mapWrData.GetInt32("run_date"); // Fastest run date for the Map and Style combo + this.BonusWR[mapWrData.GetInt32("type")][style].Name = mapWrData.GetString("name"); // Fastest run player name for the Map and Style combo + } + + else + { 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].Type = mapWrData.GetInt32("type"); // Bonus type (0 = map, 1+ = bonus index) 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 @@ -282,16 +326,43 @@ FROM MapTimes 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 + + this.ConnectedMapTimes.Add(mapWrData.GetInt32("id")); } - 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 + + // Count completions + Task completionStats = DB.Query($@" + SELECT MapTimes.type, COUNT(*) as count + FROM MapTimes + WHERE MapTimes.map_id = {this.ID} + GROUP BY type; + "); + MySqlDataReader completionStatsResult = completionStats.Result; + + if (completionStatsResult.HasRows) + { + while (completionStatsResult.Read()) + { + if (completionStatsResult.GetInt32("type") > 0) + { + // To-do: bonus completion counts + this.BonusCompletions[completionStatsResult.GetInt32("type")][style] = completionStatsResult.GetInt32("count"); + } + + else + { + // Total completions for the map and style - this should maybe be added to PersonalBest class + this.MapCompletions[style] = completionStatsResult.GetInt32("count"); + } + } + } + completionStatsResult.Close(); // Get map world record checkpoints - if (totalRows != 0) + if (this.MapCompletions[style] != 0) { Task cpReader = DB.Query($"SELECT * FROM `Checkpoints` WHERE `maptime_id` = {this.WR[style].ID};"); MySqlDataReader cpWrData = cpReader.Result; @@ -299,20 +370,20 @@ FROM MapTimes { #if DEBUG Console.WriteLine($"cp {cpWrData.GetInt32("cp")} "); - Console.WriteLine($"run_time {cpWrData.GetFloat("run_time")} "); + Console.WriteLine($"run_time {cpWrData.GetInt32("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.GetInt32("run_time"), 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("end_touch"), cpWrData.GetInt32("attempts")); cp.ID = cpWrData.GetInt32("cp"); // To-do: cp.ID = calculate Rank # from DB 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..57d80a4 100644 --- a/src/ST-Player/PlayerHUD.cs +++ b/src/ST-Player/PlayerHUD.cs @@ -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: @@ -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,31 @@ 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) { - rankModule = FormatHUDElementHTML("Rank", $"{_player.Stats.PB[style].Rank}/{_player.CurrMap.TotalCompletions}", "#7882dd"); + if (_player.Stats.BonusPB[_player.Timer.Bonus][style].ID != -1 && _player.CurrMap.BonusWR[_player.Timer.Bonus][style].ID != -1) + rankModule = FormatHUDElementHTML("Rank", $"{_player.Stats.BonusPB[_player.Timer.Bonus][style].Rank}/{_player.CurrMap.BonusCompletions[_player.Timer.Bonus][style]}", "#7882dd"); + else if (_player.CurrMap.BonusWR[_player.Timer.Bonus][style].ID != -1) + rankModule = FormatHUDElementHTML("Rank", $"-/{_player.CurrMap.BonusCompletions[_player.Timer.Bonus][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 && _player.CurrMap.WR[style].ID != -1) + rankModule = FormatHUDElementHTML("Rank", $"{_player.Stats.PB[style].Rank}/{_player.CurrMap.MapCompletions[style]}", "#7882dd"); + else if (_player.CurrMap.WR[style].ID != -1) + rankModule = FormatHUDElementHTML("Rank", $"-/{_player.CurrMap.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"); // 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"); // To-do: make Style (currently 0) be dynamic + + 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"); // To-do: make Style (currently 0) be dynamic + wrModule = FormatHUDElementHTML("WR", _player.CurrMap.BonusWR[_player.Timer.Bonus][style].Ticks > 0 ? FormatTime(_player.CurrMap.BonusWR[_player.Timer.Bonus][style].Ticks) : "N/A", "#ffc61a"); // To-do: make Style (currently 0) be dynamic + } // Build HUD string hud = $"{timerModule}
{velocityModule}
{pbModule} | {rankModule}
{wrModule}"; diff --git a/src/ST-Player/PlayerStats/CurrentRun.cs b/src/ST-Player/PlayerStats/CurrentRun.cs index f6fd2d0..96a83c4 100644 --- a/src/ST-Player/PlayerStats/CurrentRun.cs +++ b/src/ST-Player/PlayerStats/CurrentRun.cs @@ -45,10 +45,9 @@ 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) + public void SaveMapTime(Player player, TimerDatabase DB, int bonus = 0) { // Add entry in DB for the run // To-do: add `type` @@ -57,7 +56,7 @@ public void SaveMapTime(Player player, TimerDatabase DB) 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}, + VALUES ({player.Profile.ID}, {player.CurrMap.ID}, {style}, {bonus}, 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); diff --git a/src/ST-Player/PlayerStats/PersonalBest.cs b/src/ST-Player/PlayerStats/PersonalBest.cs index 2ee50bc..fc45e5a 100644 --- a/src/ST-Player/PlayerStats/PersonalBest.cs +++ b/src/ST-Player/PlayerStats/PersonalBest.cs @@ -1,14 +1,12 @@ namespace SurfTimer; -// To-do: make Style (currently 0) be dynamic -// To-do: add `Type` 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 int Type { get; set; } // Identifies bonus # - 0 for map time public float StartVelX { get; set; } public float StartVelY { get; set; } public float StartVelZ { get; set; } @@ -22,9 +20,9 @@ internal class PersonalBest // Constructor public PersonalBest() { - Ticks = -1; // To-do: what type of value we use here? DB uses DECIMAL but `.Tick` is int??? + Ticks = -1; Checkpoint = new Dictionary(); - // Type = type; + Type = 0; StartVelX = -1.0f; StartVelY = -1.0f; StartVelZ = -1.0f; diff --git a/src/ST-Player/PlayerStats/PlayerStats.cs b/src/ST-Player/PlayerStats/PlayerStats.cs index 48eed71..77c0185 100644 --- a/src/ST-Player/PlayerStats/PlayerStats.cs +++ b/src/ST-Player/PlayerStats/PlayerStats.cs @@ -7,31 +7,55 @@ 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 - // + /// + /// Stage Personal Best - Refer to as StagePB[style][stage#] + /// To-do: DEPRECATE THIS WHEN IMPLEMENTING STAGES, FOLLOW NEW PB STRUCTURE + /// + public int[,] StagePB { get; set; } = { { 0, 0 } }; + /// + /// Stage Personal Best - Refer to as StageRank[style][stage#] + /// To-do: DEPRECATE THIS WHEN IMPLEMENTING STAGES, FOLLOW NEW PB STRUCTURE + /// + public int[,] StageRank { get; set; } = { { 0, 0 } }; + + /// + /// Map Personal Best - Refer to as PB[style] + /// public Dictionary PB { get; set; } = new Dictionary(); - public CurrentRun ThisRun { get; set; } = new CurrentRun(); // This is a CurrenntRun object that tracks the data for the Player's current run + /// + /// Bonus Personal Best - Refer to as BonusPB[bonus#][style] + /// + public Dictionary[] BonusPB { get; set; } = new Dictionary[32]; + /// + /// This object tracks data for the Player's current run. + /// + public CurrentRun ThisRun { get; set; } = new CurrentRun(); + // Initialize PersonalBest for each `style` (e.g., 0 for normal) - 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(); + for (int i = 0; i < 32; i++) + { + BonusPB[i] = new Dictionary(); + BonusPB[i][0] = new PersonalBest(); + } // Add more styles as needed } /// - /// 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 + /// Loads the player's map time data from the database along with their ranks. /// + // `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 public void LoadMapTimesData(Player player, TimerDatabase DB, int playerId = 0, int mapId = 0) { 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 + AND subquery.`run_time` <= mainquery.`run_time` AND subquery.`type` = mainquery.`type`) AS `rank` FROM `MapTimes` AS mainquery WHERE mainquery.`player_id` = {player.Profile.ID} AND mainquery.`map_id` = {player.CurrMap.ID}; "); MySqlDataReader playerStats = dbTask2.Result; @@ -45,17 +69,38 @@ public void LoadMapTimesData(Player player, TimerDatabase DB, int playerId = 0, 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"); + if (playerStats.GetInt32("type") > 0) // Bonus time + { + int bonus = playerStats.GetInt32("type"); + // style = playerStats.GetInt32("style"); // To-do: Uncomment when style is implemented + BonusPB[bonus][style].ID = playerStats.GetInt32("id"); + BonusPB[bonus][style].Ticks = playerStats.GetInt32("run_time"); + BonusPB[bonus][style].Type = playerStats.GetInt32("type"); + BonusPB[bonus][style].Rank = playerStats.GetInt32("rank"); + BonusPB[bonus][style].StartVelX = (float)playerStats.GetDouble("start_vel_x"); + BonusPB[bonus][style].StartVelY = (float)playerStats.GetDouble("start_vel_y"); + BonusPB[bonus][style].StartVelZ = (float)playerStats.GetDouble("start_vel_z"); + BonusPB[bonus][style].EndVelX = (float)playerStats.GetDouble("end_vel_x"); + BonusPB[bonus][style].EndVelY = (float)playerStats.GetDouble("end_vel_y"); + BonusPB[bonus][style].EndVelZ = (float)playerStats.GetDouble("end_vel_z"); + BonusPB[bonus][style].RunDate = playerStats.GetInt32("run_date"); + } + + else // Map time + { + // 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($"============== 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 @@ -67,7 +112,7 @@ public void LoadMapTimesData(Player player, TimerDatabase DB, int playerId = 0, } /// - /// Executes the DB query to get all the checkpoints and store them in the Checkpoint dictionary + /// Loads the player's checkpoint data from the database for their personal best run. /// public void LoadCheckpointsData(TimerDatabase DB) { @@ -117,20 +162,20 @@ public void LoadCheckpointsData(TimerDatabase DB) { #if DEBUG Console.WriteLine($"cp {results.GetInt32("cp")} "); - Console.WriteLine($"run_time {results.GetFloat("run_time")} "); + 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"), // To-do: what type of value we use here? DB uses DECIMAL but `.Tick` is int??? + 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.GetFloat("end_touch"), + results.GetInt32("end_touch"), results.GetInt32("attempts")); cp.ID = results.GetInt32("cp"); // To-do: cp.ID = calculate Rank # from DB diff --git a/src/ST-Player/PlayerTimer.cs b/src/ST-Player/PlayerTimer.cs index 5878205..d2d9939 100644 --- a/src/ST-Player/PlayerTimer.cs +++ b/src/ST-Player/PlayerTimer.cs @@ -10,6 +10,7 @@ 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 @@ -21,7 +22,7 @@ internal class PlayerTimer // 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,6 +40,7 @@ public void Reset() this.IsPaused = false; this.IsPracticeMode = false; this.IsStageMode = false; + this.IsBonusMode = false; this.CurrentRunData.Reset(); } diff --git a/src/SurfTimer.cs b/src/SurfTimer.cs index b1344df..3f3daf4 100644 --- a/src/SurfTimer.cs +++ b/src/SurfTimer.cs @@ -57,9 +57,19 @@ 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) == false) && mapName.Contains("surf_")) { AddTimer(1.0f, () => CurrentMap = new Map(mapName, DB!)); // Was 3 seconds, now 1 second + + AddTimer(3.0f, () =>Console.WriteLine(String.Format(" ____________ ____ ___\n" + + " / ___/ __/_ | / __/_ ______/ _/\n" + + "/ /___\\ \\/ __/ _\\ \\/ // / __/ _/ \n" + + "\\___/___/____/ /___/\\_,_/_/ /_/\n" + + $"[CS2 Surf] SurfTimer {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" + ))); } } @@ -111,7 +121,7 @@ public override void Load(bool hotReload) + " / ___/ __/_ | / __/_ ______/ _/\n" + "/ /___\\ \\/ __/ _\\ \\/ // / __/ _/ \n" + "\\___/___/____/ /___/\\_,_/_/ /_/\n" - + $"[CS2 Surf] SurfTimer plugin loaded. Version: {ModuleVersion}" + + $"[CS2 Surf] SurfTimer 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" ));