diff --git a/README.md b/README.md index 04093ad..123870d 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Bold & Italics = being worked on. - [ ] Profile implementation (DB) - [ ] Points/Skill Groups (DB) - [ ] Player settings (DB) -- [ ] Run replays +- [X] Run replays +- [X] Saveloc/Tele - [ ] Style implementation (SW, HSW, BW) - [ ] Paint (?) diff --git a/cfg/SurfTimer/server_settings.cfg b/cfg/SurfTimer/server_settings.cfg index 074b2f0..2ff13d0 100644 --- a/cfg/SurfTimer/server_settings.cfg +++ b/cfg/SurfTimer/server_settings.cfg @@ -30,7 +30,7 @@ sv_staminalandcost 0 sv_timebetweenducks 0 // Some replay bot shit (took so fucking long to debug) -bot_quota 1 // This is gonna be used to change the amount of bots allowed (per stages/bonuses/etc) when stages/bonuses/etc added +// bot_quota 1 No need for this, because the server handles it bot_quota_mode "normal" bot_join_after_player 1 bot_join_team CT diff --git a/src/ST-Commands/PlayerCommands.cs b/src/ST-Commands/PlayerCommands.cs index a480ae1..3e51527 100644 --- a/src/ST-Commands/PlayerCommands.cs +++ b/src/ST-Commands/PlayerCommands.cs @@ -47,7 +47,7 @@ public void PlayerGoToStage(CCSPlayerController? player, CommandInfo command) return; int stage = Int32.Parse(command.ArgByIndex(1)) - 1; - if (stage > CurrentMap.Stages - 1) + if (stage > CurrentMap.Stages - 1 && CurrentMap.Stages > 0) stage = CurrentMap.Stages - 1; // Must be 1 argument @@ -84,7 +84,6 @@ public void PlayerGoToStage(CCSPlayerController? player, CommandInfo command) player.PrintToChat($"{PluginPrefix} {ChatColors.Red}Invalid stage provided. Usage: {ChatColors.Green}!s "); } - // Test command [ConsoleCommand("css_spec", "Moves a player automaticlly into spectator mode")] public void MovePlayerToSpectator(CCSPlayerController? player, CommandInfo command) { @@ -94,31 +93,210 @@ public void MovePlayerToSpectator(CCSPlayerController? player, CommandInfo comma player.ChangeTeam(CsTeam.Spectator); } + /* + ######################### + Reaplay 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 - || CurrentMap.ReplayBot.Controller == null - || !CurrentMap.ReplayBot.IsPlaying - || CurrentMap.ReplayBot.Controller.Pawn.SerialNum != player.ObserverPawn.Value!.ObserverServices!.ObserverTarget.SerialNum) + if(player == null || player.Team != CsTeam.Spectator) return; - CurrentMap.ReplayBot.Pause(); + foreach(ReplayPlayer rb in CurrentMap.ReplayBots) + { + 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) { - if (player == null - || player.Team != CsTeam.Spectator - || CurrentMap.ReplayBot.Controller == null - || !CurrentMap.ReplayBot.IsPlaying - || CurrentMap.ReplayBot.Controller.Pawn.SerialNum != player.ObserverPawn.Value!.ObserverServices!.ObserverTarget.SerialNum) + if(player == null || player.Team != CsTeam.Spectator) + return; + + foreach(ReplayPlayer rb in CurrentMap.ReplayBots) + { + if(!rb.IsPlayable || !rb.IsPlaying || !playerList[player.UserId ?? 0].IsSpectating(rb.Controller!)) + continue; + + rb.FrameTickIncrement *= -1; + } + } + + [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($"{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; + } + } + + 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 + ######################## + */ + [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)) + return; + + Player p = playerList[player.UserId ?? 0]; + if (!p.Timer.IsRunning) + { + p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}Cannot save location while not in run"); + return; + } + + var player_pos = p.Controller.Pawn.Value!.AbsOrigin!; + var player_angle = p.Controller.PlayerPawn.Value!.EyeAngles; + var player_velocity = p.Controller.PlayerPawn.Value!.AbsVelocity; + + 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.Controller.PrintToChat($"{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)) return; - CurrentMap.ReplayBot.FrameTickIncrement *= -1; + Player p = playerList[player.UserId ?? 0]; + + if(p.SavedLocations.Count == 0) + { + p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}No saved locations"); + return; + } + + if(!p.Timer.IsRunning) + p.Timer.Start(); + + if (!p.Timer.IsPracticeMode) + { + p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}Timer now on practice"); + p.Timer.IsPracticeMode = true; + } + + if(command.ArgCount > 1) + try + { + int tele_n = int.Parse(command.ArgByIndex(1)); + if (tele_n < p.SavedLocations.Count) + p.CurrentSavedLocation = tele_n; + } + catch { } + SavelocFrame location = p.SavedLocations[p.CurrentSavedLocation]; + 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}"); + } + + [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)) + return; + + Player p = playerList[player.UserId ?? 0]; + + if(p.SavedLocations.Count == 0) + { + p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}No saved locations"); + return; + } + + if(p.CurrentSavedLocation == 0) + { + p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}Already at first location"); + } + else + { + p.CurrentSavedLocation--; + } + + TeleportPlayerLocation(player, command); + + p.Controller.PrintToChat($"{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)) + return; + + Player p = playerList[player.UserId ?? 0]; + + if(p.SavedLocations.Count == 0) + { + p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}No saved locations"); + return; + } + + if(p.CurrentSavedLocation == p.SavedLocations.Count-1) + { + p.Controller.PrintToChat($"{PluginPrefix} {ChatColors.Red}Already at last location"); + } + else + { + p.CurrentSavedLocation++; + } + + TeleportPlayerLocation(player, command); + + p.Controller.PrintToChat($"{PluginPrefix} Teleported #{p.CurrentSavedLocation}"); } } \ No newline at end of file diff --git a/src/ST-Events/Players.cs b/src/ST-Events/Players.cs index 3d7e17b..a2162a2 100644 --- a/src/ST-Events/Players.cs +++ b/src/ST-Events/Players.cs @@ -9,26 +9,36 @@ namespace SurfTimer; public partial class SurfTimer { - [GameEventHandler(HookMode.Post)] + [GameEventHandler] public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) { var controller = @event.Userid; - if(!controller.IsValid) + if(!controller.IsValid || !controller.IsBot) return HookResult.Continue; - if (controller.IsBot && CurrentMap.ReplayBot.Controller == null) + for (int i = 0; i < CurrentMap.ReplayBots.Count; i++) { - CurrentMap.ReplayBot.Controller = controller; - // CurrentMap.ReplayBot.Controller.PlayerName = $"[REPLAY] {CurrentMap.Name}"; + if(CurrentMap.ReplayBots[i].IsPlayable) + continue; - Server.PrintToChatAll($"{ChatColors.Lime} Loading replay data..."); // WHY COLORS NOT WORKING AHHHHH!!!!! + int repeats = -1; + if(CurrentMap.ReplayBots[i].Stat_Prefix == "PB") + repeats = 3; + + CurrentMap.ReplayBots[i].SetController(controller, repeats); + Server.PrintToChatAll($"{ChatColors.Lime} Loading replay data..."); AddTimer(2f, () => { - CurrentMap.ReplayBot.Controller.RemoveWeapons(); + if(!CurrentMap.ReplayBots[i].IsPlayable) + return; + + CurrentMap.ReplayBots[i].Controller!.RemoveWeapons(); - CurrentMap.ReplayBot.LoadReplayData(DB!, CurrentMap); + CurrentMap.ReplayBots[i].LoadReplayData(DB!); - CurrentMap.ReplayBot.Start(); + CurrentMap.ReplayBots[i].Start(); }); + + return HookResult.Continue; } return HookResult.Continue; @@ -157,8 +167,9 @@ public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo { var player = @event.Userid; - if (CurrentMap.ReplayBot.Controller != null&& CurrentMap.ReplayBot.Controller.Equals(player)) - CurrentMap.ReplayBot.Reset(); + for (int i = 0; i < CurrentMap.ReplayBots.Count; i++) + if (CurrentMap.ReplayBots[i].IsPlayable && CurrentMap.ReplayBots[i].Controller!.Equals(player)) + CurrentMap.ReplayBots[i].Reset(); if (player.IsBot || !player.IsValid) { diff --git a/src/ST-Events/Tick.cs b/src/ST-Events/Tick.cs index a7140cc..37703e4 100644 --- a/src/ST-Events/Tick.cs +++ b/src/ST-Events/Tick.cs @@ -1,5 +1,4 @@ -using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Utils; +using CounterStrikeSharp.API.Modules.Cvars; namespace SurfTimer; @@ -14,7 +13,25 @@ public void OnTick() player.HUD.Display(); } - // Replay BOT Ticks - CurrentMap?.ReplayBot.Tick(); // When CurrentMap null the ? operator will terminate safely the operation + if (CurrentMap == null) + return; + + // Need to disable maps from executing their cfgs. Currently idk how (But seriusly it a security issue) + ConVar? bot_quota = ConVar.Find("bot_quota"); + if (bot_quota != null) + { + int cbq = bot_quota.GetPrimitiveValue(); + if(cbq != CurrentMap.ReplayBots.Count) + { + bot_quota.SetValue(CurrentMap.ReplayBots.Count); + } + } + + for(int i = 0; i < CurrentMap!.ReplayBots.Count; i++) + { + CurrentMap.ReplayBots[i].Tick(); + if (CurrentMap.ReplayBots[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 b330ee3..b98ed13 100644 --- a/src/ST-Events/TriggerEndTouch.cs +++ b/src/ST-Events/TriggerEndTouch.cs @@ -2,7 +2,6 @@ using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Utils; using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; -using CounterStrikeSharp.API; namespace SurfTimer; @@ -45,11 +44,12 @@ internal HookResult OnTriggerEndTouch(DynamicHook handler) 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))); // Would like for someone to fact check the math :) + 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 player.Timer.Start(); + player.ReplayRecorder.CurrentSituation = ReplayFrameSituation.START_RUN; /* Revisit // Wonky Prespeed check diff --git a/src/ST-Events/TriggerStartTouch.cs b/src/ST-Events/TriggerStartTouch.cs index 2f7633c..3059d4e 100644 --- a/src/ST-Events/TriggerStartTouch.cs +++ b/src/ST-Events/TriggerStartTouch.cs @@ -51,24 +51,29 @@ internal HookResult OnTriggerStartTouch(DynamicHook handler) if (player.Timer.IsRunning) { 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.PB[style].Ticks <= 0) // Player first ever PersonalBest for the map { - Server.PrintToChatAll($"{PluginPrefix} {player.Controller.PlayerName} finished the map in {ChatColors.Gold}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default} ({player.Timer.Ticks})!"); + 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} {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})!"); + 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} You finished the map in {ChatColors.Yellow}{PlayerHUD.FormatTime(player.Timer.Ticks)}{ChatColors.Default}!"); + 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 } @@ -90,18 +95,26 @@ internal HookResult OnTriggerStartTouch(DynamicHook handler) #endif // Add entry in DB for the run - 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) - 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 - - // Replay - Add end buffer for replay - AddTimer(1.5f, () => player.ReplayRecorder.SaveReplayData(player, DB)); - AddTimer(2f, () => { - CurrentMap.ReplayBot.LoadReplayData(DB!, CurrentMap); - CurrentMap.ReplayBot.ResetReplay(); - }); + if(!player.Timer.IsPracticeMode) { + AddTimer(1.5f, () => { + 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) + 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 + }); + + // 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) + { + 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(); + }); + } + } } #if DEBUG diff --git a/src/ST-Map/Map.cs b/src/ST-Map/Map.cs index 272de00..c1fb166 100644 --- a/src/ST-Map/Map.cs +++ b/src/ST-Map/Map.cs @@ -21,7 +21,8 @@ internal class Map public int LastPlayed {get; set;} = 0; public int TotalCompletions {get; set;} = 0; public Dictionary WR { get; set; } = new Dictionary(); - public ReplayPlayer ReplayBot { get; set; } = new ReplayPlayer(); + public List ConnectedMapTimes { get; set; } = new List(); + public List ReplayBots { get; set; } = new List { new ReplayPlayer() }; // Zone Origin Information // Map start/end zones @@ -150,6 +151,7 @@ internal Map(string Name, TimerDatabase DB) } } } + if (this.Stages > 0) this.Stages++; // You did not count the stages right :( 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 @@ -215,6 +217,26 @@ internal Map(string Name, TimerDatabase DB) // 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(); + } + + public void KickReplayBot(int index) + { + if (!this.ReplayBots[index].IsPlayable) + return; + + int? id_to_kick = this.ReplayBots[index].Controller!.UserId; + if(id_to_kick == null) + return; + + this.ReplayBots.RemoveAt(index); + Server.ExecuteCommand($"kickid {id_to_kick}; bot_quota {this.ReplayBots.Count}"); } public bool IsInZone(Vector zoneOrigin, float zoneCollisionRadius, Vector spawnOrigin) @@ -245,6 +267,7 @@ FROM MapTimes { // To-do: Implement bonuses WR // To-do: Implement stages WR + 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 :) @@ -260,6 +283,7 @@ FROM MapTimes 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")); totalRows++; } } diff --git a/src/ST-Player/Player.cs b/src/ST-Player/Player.cs index 059d1cc..2bcfb83 100644 --- a/src/ST-Player/Player.cs +++ b/src/ST-Player/Player.cs @@ -12,6 +12,8 @@ internal class Player public PlayerStats Stats {get; set;} public PlayerHUD HUD {get; set;} public ReplayRecorder ReplayRecorder { get; set; } + public List SavedLocations { get; set; } + public int CurrentSavedLocation { get; set; } // Player information public PlayerProfile Profile {get; set;} @@ -30,8 +32,21 @@ public Player(CCSPlayerController Controller, CCSPlayer_MovementServices Movemen this.Timer = new PlayerTimer(); this.Stats = new PlayerStats(); this.ReplayRecorder = new ReplayRecorder(); + this.SavedLocations = new List(); + CurrentSavedLocation = 0; this.HUD = new PlayerHUD(this); this.CurrMap = CurrMap; } + + /// + /// Checks if current player is spcetating player

+ ///

+ public bool IsSpectating(CCSPlayerController p) + { + if(p == null || this.Controller == null || this.Controller.Team != CounterStrikeSharp.API.Modules.Utils.CsTeam.Spectator) + return false; + + return p.Pawn.SerialNum == this.Controller.ObserverPawn.Value!.ObserverServices!.ObserverTarget.SerialNum; + } } diff --git a/src/ST-Player/PlayerHUD.cs b/src/ST-Player/PlayerHUD.cs index dab8070..4ed6c2b 100644 --- a/src/ST-Player/PlayerHUD.cs +++ b/src/ST-Player/PlayerHUD.cs @@ -44,7 +44,7 @@ public static string FormatTime(int ticks, PlayerTimer.TimeFormatStyle style = P { case PlayerTimer.TimeFormatStyle.Compact: return time.TotalMinutes < 1 - ? $"{time.Seconds:D1}.{millis:D3}" + ? $"{time.Seconds:D1}:{millis:D3}" : $"{time.Minutes:D1}:{time.Seconds:D1}.{millis:D3}"; case PlayerTimer.TimeFormatStyle.Full: return $"{time.Hours:D2}:{time.Minutes:D2}:{time.Seconds:D2}.{millis:D3}"; @@ -102,13 +102,17 @@ public void Display() } else if (_player.Controller.Team == CsTeam.Spectator) { - if (_player.CurrMap.ReplayBot.Controller?.Pawn.SerialNum == _player.Controller.ObserverPawn.Value!.ObserverServices!.ObserverTarget.SerialNum) + for (int i = 0; i < _player.CurrMap.ReplayBots.Count; i++) { - // Replay HUD Modules + if(!_player.CurrMap.ReplayBots[i].IsPlayable || !_player.IsSpectating(_player.CurrMap.ReplayBots[i].Controller!)) + continue; + string replayModule = $"{FormatHUDElementHTML("", "REPLAY", "red", "large")}"; - string nameModule = FormatHUDElementHTML($"{_player.CurrMap.WR[_player.Timer.Style].Name}", $"{FormatTime(_player.CurrMap.WR[_player.Timer.Style].Ticks)}", "#ffd500"); - string elapsed_ticks = FormatHUDElementHTML("Tick", $"{_player.CurrMap.ReplayBot.CurrentFrameTick}/{_player.CurrMap.ReplayBot.Frames.Count}", "#7882dd"); - string hud = $"{replayModule}
{elapsed_ticks}
{nameModule}"; + + string nameModule = FormatHUDElementHTML($"{_player.CurrMap.ReplayBots[i].Stat_PlayerName}", $"{FormatTime(_player.CurrMap.ReplayBots[i].Stat_RunTime)}", "#ffd500"); + + string elapsed_time = FormatHUDElementHTML("Time", $"{PlayerHUD.FormatTime(_player.CurrMap.ReplayBots[i].Stat_RunTick)}", "#7882dd"); + string hud = $"{replayModule}
{elapsed_time}
{nameModule}"; _player.Controller.PrintToCenterHtml(hud); } diff --git a/src/ST-Player/PlayerStats/Checkpoint.cs b/src/ST-Player/PlayerStats/Checkpoint.cs index 30921f5..bf51165 100644 --- a/src/ST-Player/PlayerStats/Checkpoint.cs +++ b/src/ST-Player/PlayerStats/Checkpoint.cs @@ -1,5 +1,3 @@ -using MySqlConnector; - namespace SurfTimer; internal class Checkpoint : PersonalBest diff --git a/src/ST-Player/PlayerStats/CurrentRun.cs b/src/ST-Player/PlayerStats/CurrentRun.cs index ddacc1a..4609dd1 100644 --- a/src/ST-Player/PlayerStats/CurrentRun.cs +++ b/src/ST-Player/PlayerStats/CurrentRun.cs @@ -53,13 +53,14 @@ public void SaveMapTime(Player player, TimerDatabase DB) // Add entry in DB for the run // To-do: add `type` 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`) + (`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()}) + {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); + 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})"); diff --git a/src/ST-Player/Replay/ReplayFrame.cs b/src/ST-Player/Replay/ReplayFrame.cs index 18174b3..f67de67 100644 --- a/src/ST-Player/Replay/ReplayFrame.cs +++ b/src/ST-Player/Replay/ReplayFrame.cs @@ -2,12 +2,22 @@ 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 +} [Serializable] 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 uint Flags { get; set; } public MoveType_t MoveType { get; set; } diff --git a/src/ST-Player/Replay/ReplayPlayer.cs b/src/ST-Player/Replay/ReplayPlayer.cs index b258182..98a44c4 100644 --- a/src/ST-Player/Replay/ReplayPlayer.cs +++ b/src/ST-Player/Replay/ReplayPlayer.cs @@ -11,7 +11,18 @@ internal class ReplayPlayer { public bool IsPlaying { get; set; } = false; public bool IsPaused { get; set; } = false; - public bool IsOnRepeat { get; set; } = true; // Currently should always repeat + public bool IsPlayable { get; set; } = false; + + // 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; // Tracking public List Frames { get; set; } = new List(); @@ -26,12 +37,19 @@ public void ResetReplay() { this.CurrentFrameTick = 0; this.FrameTickIncrement = 1; + if(this.RepeatCount > 0) + this.RepeatCount--; + + this.Stat_IsRunning = false; + this.Stat_RunTick = 0; } public void Reset() { this.IsPlaying = false; this.IsPaused = false; + this.IsPlayable = false; + this.RepeatCount = -1; this.Frames.Clear(); @@ -40,13 +58,19 @@ public void Reset() this.Controller = null; } + public void SetController(CCSPlayerController c, int repeat_count = -1) + { + this.Controller = c; + this.RepeatCount = repeat_count; + this.IsPlayable = true; + } + public void Start() { - if (this.Controller == null) + if (!this.IsPlayable) return; this.IsPlaying = true; - this.Controller.Pawn.Value!.MoveType = MoveType_t.MOVETYPE_NOCLIP; } public void Stop() @@ -56,20 +80,50 @@ public void Stop() public void Pause() { - if (this.IsPlaying) - this.IsPaused = !this.IsPaused; + if (!this.IsPlaying) + return; + + this.IsPaused = !this.IsPaused; + this.Stat_IsRunning = !this.Stat_IsRunning; } public void Tick() { - if (!this.IsPlaying || this.Controller == null || this.Frames.Count == 0) + if (!this.IsPlaying || !this.IsPlayable || this.Frames.Count == 0) return; ReplayFrame current_frame = this.Frames[this.CurrentFrameTick]; - var current_pos = this.Controller.PlayerPawn.Value!.AbsOrigin!; + + // SOME BLASHPEMY FOR YOU + if (this.FrameTickIncrement >= 0) + { + if (current_frame.Situation == (uint)ReplayFrameSituation.START_RUN) + { + this.Stat_IsRunning = true; + this.Stat_RunTick = 0; + } + else if (current_frame.Situation == (uint)ReplayFrameSituation.END_RUN) + { + this.Stat_IsRunning = false; + } + } + else + { + if (current_frame.Situation == (uint)ReplayFrameSituation.START_RUN) + { + this.Stat_IsRunning = false; + } + else if (current_frame.Situation == (uint)ReplayFrameSituation.END_RUN) + { + this.Stat_IsRunning = true; + this.Stat_RunTick = 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!; bool is_on_ground = (current_frame.Flags & (uint)PlayerFlags.FL_ONGROUND) != 0; - bool is_ducking = (current_frame.Flags & (uint)PlayerFlags.FL_DUCKING) != 0; Vector velocity = (current_frame.Pos - current_pos) * 64; @@ -85,21 +139,28 @@ public void Tick() 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.CurrentFrameTick >= this.Frames.Count) this.ResetReplay(); } - public void LoadReplayData(TimerDatabase DB, Map current_map) + public void LoadReplayData(TimerDatabase DB) { - if (this.Controller == null) + if (!this.IsPlayable) return; - // TODO: make query for wr too + Task dbTask = DB.Query($@" - SELECT `replay_frames` FROM MapTimeReplay - WHERE `map_id`={current_map.ID} AND `maptime_id`={current_map.WR[0].ID} + 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} "); + MySqlDataReader mapTimeReplay = dbTask.Result; if(!mapTimeReplay.HasRows) { @@ -112,22 +173,28 @@ public void LoadReplayData(TimerDatabase DB, Map current_map) { 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(); - - FormatBotName(current_map); } - private void FormatBotName(Map current_map) + private void FormatBotName() { - if (this.Controller == null) + if (!this.IsPlayable) return; - SchemaString bot_name = new SchemaString(this.Controller, "m_iszPlayerName"); - // Revisit, FORMAT CORECTLLY - bot_name.Set($"[WR] {current_map.WR[0].Name} | {PlayerHUD.FormatTime(current_map.WR[0].Ticks)}"); - Utilities.SetStateChanged(this.Controller, "CBasePlayerController", "m_iszPlayerName"); + 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}"; + + bot_name.Set(replay_name); + Utilities.SetStateChanged(this.Controller!, "CBasePlayerController", "m_iszPlayerName"); } } \ No newline at end of file diff --git a/src/ST-Player/Replay/ReplayRecorder.cs b/src/ST-Player/Replay/ReplayRecorder.cs index dd610e0..fd20ed5 100644 --- a/src/ST-Player/Replay/ReplayRecorder.cs +++ b/src/ST-Player/Replay/ReplayRecorder.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Utils; namespace SurfTimer; @@ -7,6 +6,7 @@ namespace SurfTimer; internal class ReplayRecorder { public bool IsRecording { get; set; } = false; + public ReplayFrameSituation CurrentSituation { get; set; } = ReplayFrameSituation.NONE; public List Frames { get; set; } = new List(); public void Reset() @@ -48,32 +48,22 @@ public void Tick(Player player) { 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, Flags = player_flags, MoveType = player_move_type, }; this.Frames.Add(frame); + + // Every Situation should last for at most, 1 tick + this.CurrentSituation = ReplayFrameSituation.NONE; } - /// - /// [ player_id | maptime_id | replay_frames ] - /// @ Adding a replay data for a run (PB/WR) - /// @ Data saved can be accessed with `ReplayPlayer.LoadReplayData` - /// - public void SaveReplayData(Player player, TimerDatabase DB) + public string SerializeReplay() { JsonSerializerOptions options = new JsonSerializerOptions {WriteIndented = false, Converters = { new VectorConverter(), new QAngleConverter() }}; string replay_frames = JsonSerializer.Serialize(Frames, options); - string compressed_replay_frames = Compressor.Compress(replay_frames); - Task updatePlayerReplayTask = DB.Write($@" - INSERT INTO `MapTimeReplay` - (`player_id`, `maptime_id`, `map_id`, `replay_frames`) - VALUES ({player.Profile.ID}, {player.Stats.PB[0].ID}, {player.CurrMap.ID}, '{compressed_replay_frames}') - ON DUPLICATE KEY UPDATE replay_frames=VALUES(replay_frames) - "); - if (updatePlayerReplayTask.Result <= 0) - throw new Exception($"CS2 Surf ERROR >> internal class PlayerReplay -> SaveReplayData -> Failed to insert/update player run in database. Player: {player.Profile.Name} ({player.Profile.SteamID})"); - updatePlayerReplayTask.Dispose(); - } + return Compressor.Compress(replay_frames); + } } \ No newline at end of file diff --git a/src/ST-Player/Saveloc/SavelocFrame.cs b/src/ST-Player/Saveloc/SavelocFrame.cs new file mode 100644 index 0000000..a6bcd4c --- /dev/null +++ b/src/ST-Player/Saveloc/SavelocFrame.cs @@ -0,0 +1,11 @@ +using CounterStrikeSharp.API.Modules.Utils; + +namespace SurfTimer; + +internal class SavelocFrame +{ + public Vector Pos { get; set; } = new Vector(0, 0, 0); + public QAngle Ang { get; set; } = new QAngle(0, 0, 0); + public Vector Vel { get; set; } = new Vector(0, 0, 0); + public int Tick { get; set; } = 0; +} diff --git a/src/ST-UTILS/ConVar.cs b/src/ST-UTILS/ConVar.cs new file mode 100644 index 0000000..55206ba --- /dev/null +++ b/src/ST-UTILS/ConVar.cs @@ -0,0 +1,15 @@ +using CounterStrikeSharp.API.Modules.Cvars; + +namespace SurfTimer; + +internal class ConVarHelper +{ + public static void RemoveCheatFlagFromConVar(string cv_name) + { + ConVar? cv = ConVar.Find(cv_name); + if (cv == null || (cv.Flags & CounterStrikeSharp.API.ConVarFlags.FCVAR_CHEAT) == 0) + return; + + cv.Flags &= ~CounterStrikeSharp.API.ConVarFlags.FCVAR_CHEAT; + } +} \ No newline at end of file diff --git a/src/SurfTimer.cs b/src/SurfTimer.cs index 2816032..846a225 100644 --- a/src/SurfTimer.cs +++ b/src/SurfTimer.cs @@ -32,8 +32,6 @@ You should have received a copy of the GNU Affero General Public License using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Memory; using CounterStrikeSharp.API.Modules.Utils; -using CounterStrikeSharp.API.Modules.Timers; -using CounterStrikeSharp.API.Modules.Cvars; namespace SurfTimer; @@ -77,6 +75,11 @@ public HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info) { // Load cvars/other configs here // Execute server_settings.cfg + + ConVarHelper.RemoveCheatFlagFromConVar("bot_stop"); + ConVarHelper.RemoveCheatFlagFromConVar("bot_freeze"); + ConVarHelper.RemoveCheatFlagFromConVar("bot_zombie"); + Server.ExecuteCommand("execifexists SurfTimer/server_settings.cfg"); Console.WriteLine("[CS2 Surf] Executed configuration: server_settings.cfg"); return HookResult.Continue;