├── Class1.cs ├── Commands ├── AdminCommands.cs ├── HudCommands.cs ├── SavelocCommands.cs ├── SurfGameplayCommands.cs ├── SurfStatsCommands.cs ├── UtilCommands.cs └── VIPCommands.cs ├── Components ├── MapZonesComponent.cs ├── PlayerComponent.cs ├── PlayerReplayComponent.cs └── PlayerTimerComponent.cs ├── Game └── SchemaString.cs ├── Infrastructure ├── Database.cs ├── Entity.cs ├── EntityManager.cs ├── EventManager.cs └── System.cs ├── Models ├── Checkpoint.cs ├── GlobalCfg.cs ├── MapCfg.cs ├── RecentRecords.cs ├── Record.cs ├── Replay.cs ├── Server.cs ├── SkillGroups.cs ├── V_Player.cs ├── V_RankedRecord.cs └── V_Replay.cs ├── Properties └── launchSettings.json ├── README.md ├── Systems ├── AdvertisingSystem.cs ├── CheckpointZoneSystem.cs ├── ClanTagSystem.cs ├── ConnectionSystem.cs ├── DiscordSystem.cs ├── EndZoneSystem.cs ├── HSWSurfSystem.cs ├── HookSystem.cs ├── HudSystem.cs ├── MapLoadSystem.cs ├── PlaytimeSystem.cs ├── ReplayPlaybackSystem.cs ├── ReplayRecorderSystem.cs ├── ServerSystem.cs ├── SidewaysSurfSystem.cs ├── StartZoneSystem.cs └── TurbomasterSystem.cs ├── Utils.cs ├── WST.csproj ├── WST.csproj.user ├── Workshop.cs ├── cssharp.sln ├── docker-compose.yaml ├── local_build.ps1 ├── schema.sql └── server.json /Commands/AdminCommands.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | using CounterStrikeSharp.API.Core; 3 | using CounterStrikeSharp.API.Core.Attributes.Registration; 4 | using CounterStrikeSharp.API.Modules.Commands; 5 | 6 | namespace WST; 7 | 8 | public partial class Wst 9 | { 10 | [ConsoleCommand("wst_set_tier", "Set map tier")] 11 | [CommandHelper(minArgs: 1, usage: "[tier]", whoCanExecute: CommandUsage.CLIENT_ONLY)] 12 | public void OnSetMapTierCommand(CCSPlayerController player, CommandInfo info) 13 | { 14 | if (player == null) 15 | { 16 | return; 17 | } 18 | 19 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 20 | var playerInfo = entity.GetComponent(); 21 | if (!playerInfo.PlayerStats.IsAdmin) 22 | { 23 | return; 24 | } 25 | 26 | var map = _entityManager.FindEntity(); 27 | var mapZones = map.GetComponent(); 28 | var route = mapZones.GetRoute(playerInfo.RouteKey); 29 | if (route == null) 30 | { 31 | return; 32 | } 33 | 34 | var tier = info.GetArg(1); 35 | if (!int.TryParse(tier, out var tierInt)) 36 | { 37 | return; 38 | } 39 | 40 | route.Tier = tierInt; 41 | _ = mapZones.Save(_database, Server.MapName); 42 | 43 | Server.PrintToChatAll( 44 | $" {CC.Main}[ADMIN] {CC.Secondary}{playerInfo.Name} {CC.White}set tier for {CC.Secondary}{route.Name} {CC.White}to {CC.Secondary}{tierInt}"); 45 | } 46 | 47 | [ConsoleCommand("wst_set_startpos", "Set map tier")] 48 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 49 | public void OnSetStartpos(CCSPlayerController player, CommandInfo info) 50 | { 51 | if (player == null) 52 | { 53 | return; 54 | } 55 | 56 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 57 | var playerInfo = entity.GetComponent(); 58 | if (!playerInfo.PlayerStats.IsAdmin) 59 | { 60 | return; 61 | } 62 | 63 | var map = _entityManager.FindEntity(); 64 | var mapZones = map.GetComponent(); 65 | var route = mapZones.GetRoute(playerInfo.RouteKey); 66 | if (route == null) 67 | { 68 | return; 69 | } 70 | 71 | route.StartPos = new ZoneVector 72 | { 73 | x = player.PlayerPawn.Value!.AbsOrigin.X, y = player.PlayerPawn.Value!.AbsOrigin.Y, 74 | z = player.PlayerPawn.Value!.AbsOrigin.Z 75 | }; 76 | route.StartAngles = new ZoneVector 77 | { 78 | x = player.PlayerPawn.Value.EyeAngles.X, y = player.PlayerPawn.Value.EyeAngles.Y, 79 | z = player.PlayerPawn.Value.EyeAngles.Z 80 | }; 81 | route.StartVelocity = new ZoneVector 82 | { 83 | x = player.PlayerPawn.Value.AbsVelocity.X, y = player.PlayerPawn.Value.AbsVelocity.Y, 84 | z = player.PlayerPawn.Value.AbsVelocity.Z 85 | }; 86 | 87 | _ = mapZones.Save(_database, Server.MapName); 88 | 89 | Server.PrintToChatAll( 90 | $" {CC.Main}[ADMIN] {CC.Secondary}{playerInfo.Name} {CC.White}update startpos for {CC.Secondary}{route.Name}"); 91 | } 92 | 93 | [ConsoleCommand("wst_add_cfg", "Add map CFG")] 94 | [CommandHelper(minArgs: 1, usage: "[cmd] [val]", whoCanExecute: CommandUsage.CLIENT_ONLY)] 95 | public void OnAddCfg(CCSPlayerController player, CommandInfo info) 96 | { 97 | if (player == null) 98 | { 99 | return; 100 | } 101 | 102 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 103 | var playerInfo = entity.GetComponent(); 104 | if (!playerInfo.PlayerStats.IsAdmin) 105 | { 106 | return; 107 | } 108 | 109 | var mapName = Server.MapName; 110 | 111 | var cmd = ""; 112 | for (var i = 1; i < info.ArgCount; i++) 113 | { 114 | cmd += info.GetArg(i) + " "; 115 | } 116 | 117 | cmd = cmd.Trim(); 118 | 119 | _ = _database.ExecuteAsync("INSERT INTO map_cfg (mapname, command) VALUES (@mapname, @command)", 120 | new { mapname = mapName, command = cmd }); 121 | 122 | Server.ExecuteCommand(cmd); 123 | 124 | Server.PrintToChatAll( 125 | $" {CC.Main}[ADMIN] {CC.Secondary}{playerInfo.Name} {CC.White}added map CFG {CC.Secondary}{cmd}"); 126 | } 127 | 128 | [ConsoleCommand("wst_show_cfg", "Shows map CFG")] 129 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 130 | public void OnShowCfg(CCSPlayerController player, CommandInfo info) 131 | { 132 | if (player == null) 133 | { 134 | return; 135 | } 136 | 137 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 138 | var playerInfo = entity.GetComponent(); 139 | if (!playerInfo.PlayerStats.IsAdmin) 140 | { 141 | return; 142 | } 143 | 144 | var mapName = Server.MapName; 145 | 146 | _ = Task.Run(async () => 147 | { 148 | var cfgs = await _database.QueryAsync("SELECT * FROM map_cfg WHERE mapname = @mapname", 149 | new { mapname = mapName }); 150 | 151 | Server.NextFrame(() => 152 | { 153 | foreach (var cfg in cfgs) 154 | { 155 | player.PrintToChat($" {CC.Main}[CFG] {CC.Secondary}{cfg.Command}"); 156 | } 157 | }); 158 | return; 159 | }); 160 | } 161 | 162 | 163 | 164 | [ConsoleCommand("wst_ent_debug", "Debug entites")] 165 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 166 | public void OnEntDebug(CCSPlayerController? player, CommandInfo command) 167 | { 168 | if (player == null) 169 | { 170 | return; 171 | } 172 | 173 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 174 | var playerInfo = entity.GetComponent(); 175 | if (!playerInfo.PlayerStats.IsAdmin) 176 | { 177 | return; 178 | } 179 | 180 | var debug = _entityManager.DebugEntities(); 181 | 182 | foreach (var d in debug) 183 | { 184 | Server.PrintToChatAll(d); 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /Commands/HudCommands.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | using CounterStrikeSharp.API.Core.Attributes.Registration; 3 | using CounterStrikeSharp.API.Modules.Commands; 4 | 5 | namespace WST; 6 | 7 | public partial class Wst 8 | { 9 | [ConsoleCommand("hidehud")] 10 | [ConsoleCommand("showhud")] 11 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 12 | public void OnHideHud(CCSPlayerController? player, CommandInfo command) 13 | { 14 | if (player == null) 15 | { 16 | return; 17 | } 18 | 19 | if (!player.IsValid || player.IsBot) 20 | { 21 | return; 22 | } 23 | 24 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 25 | var playerInfo = entity.GetComponent(); 26 | 27 | playerInfo.ShowHud = !playerInfo.ShowHud; 28 | 29 | player.PrintToChat($" {CC.White}Hud is now {CC.Secondary}{(playerInfo.ShowHud ? "visible" : "hidden")}"); 30 | } 31 | 32 | [ConsoleCommand("keys")] 33 | [ConsoleCommand("showkeys")] 34 | [ConsoleCommand("hidekeys")] 35 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 36 | public void OnShowKeys(CCSPlayerController? player, CommandInfo command) 37 | { 38 | if (player == null) 39 | { 40 | return; 41 | } 42 | 43 | if (!player.IsValid || player.IsBot) 44 | { 45 | return; 46 | } 47 | 48 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 49 | var playerInfo = entity.GetComponent(); 50 | 51 | playerInfo.ShowKeys = !playerInfo.ShowKeys; 52 | 53 | player.PrintToChat($" {CC.White}Keys is now {CC.Secondary}{(playerInfo.ShowHud ? "visible" : "hidden")}"); 54 | } 55 | 56 | [ConsoleCommand("customhud")] 57 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 58 | public void OnCustomHud(CCSPlayerController? player, CommandInfo command) 59 | { 60 | if (player == null) 61 | { 62 | return; 63 | } 64 | 65 | if (!player.IsValid || player.IsBot) 66 | { 67 | return; 68 | } 69 | 70 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 71 | var playerInfo = entity.GetComponent(); 72 | 73 | if (playerInfo.PlayerStats == null) 74 | { 75 | return; 76 | } 77 | 78 | var customHudExists = String.IsNullOrEmpty(playerInfo.PlayerStats.CustomHudUrl); 79 | if (customHudExists) 80 | { 81 | player.PrintToChat( 82 | $" {CC.White}You do not have a custom hud. Custom huds are a VIP only feature. Join the discord and message {CC.Secondary}will {CC.White}to get VIP."); 83 | return; 84 | } 85 | 86 | 87 | playerInfo.ShowCustomHud = !playerInfo.ShowCustomHud; 88 | 89 | player.PrintToChat( 90 | $" {CC.White}CustomHud is now {CC.Secondary}{(playerInfo.ShowHud ? "visible" : "hidden")} {CC.White}(It is always visible to spectators or in replays)"); 91 | } 92 | } -------------------------------------------------------------------------------- /Commands/SavelocCommands.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | using CounterStrikeSharp.API.Core; 3 | using CounterStrikeSharp.API.Core.Attributes.Registration; 4 | using CounterStrikeSharp.API.Modules.Commands; 5 | using CounterStrikeSharp.API.Modules.Utils; 6 | 7 | namespace WST; 8 | 9 | public partial class Wst 10 | { 11 | 12 | [ConsoleCommand("prac", "Prac Help")] 13 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 14 | public void OnPracCommand(CCSPlayerController? caller, CommandInfo command) 15 | { 16 | if (caller == null) 17 | { 18 | return; 19 | } 20 | 21 | // caller.PrintToChat($" {CC.White}Open your console (~) and type {CC.Secondary}bind sm_r{CC.White} to bind this command to a key"); 22 | 23 | // You can enter prac mode by first setting a saved location with sm_saveloc (while surfing in normal mode) 24 | // Then once you teleport to that saveloc with sm_tele your timer, position, and velocity will be restored 25 | // and you will be in prac mode. 26 | 27 | // To exit prac mode you can either type sm_r or sm_restart in console or you can type sm_saveloc to save 28 | 29 | caller.PrintToChat($" {CC.White}To enter prac mode you must first set a saveloc with {CC.Secondary}sm_saveloc"); 30 | caller.PrintToChat($" {CC.White}Then you can teleport to that saveloc with {CC.Secondary}sm_tele"); 31 | caller.PrintToChat($" {CC.White}To exit prac mode you can type {CC.Secondary}sm_r {CC.White}or enter the startzone"); 32 | caller.PrintToChat($" {CC.White}If you make a bad saveloc you can go back/forward checkpoints with {CC.Secondary}sm_prevloc {CC.White}and {CC.Secondary}sm_nextloc"); 33 | caller.PrintToChat($" {CC.White}To create saveloc binds type the following in console:"); 34 | caller.PrintToChat($" {CC.Secondary}bind sm_saveloc"); 35 | caller.PrintToChat($" {CC.Secondary}bind sm_tele"); 36 | 37 | } 38 | 39 | [ConsoleCommand("wst_cp", "Save location")] 40 | [ConsoleCommand("wst_saveloc", "Save location")] 41 | [ConsoleCommand("saveloc", "Save location")] 42 | [ConsoleCommand("sm_saveloc", "Save location")] 43 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 44 | public void OnSavelocCommand(CCSPlayerController player, CommandInfo info) 45 | { 46 | if (player == null) 47 | { 48 | return; 49 | } 50 | 51 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 52 | var playerInfo = entity.GetComponent(); 53 | 54 | var map = _entityManager.FindEntity(); 55 | var mapZones = map.GetComponent(); 56 | var route = mapZones.GetRoute(playerInfo.RouteKey); 57 | 58 | if (route == null) 59 | { 60 | return; 61 | } 62 | 63 | var playerPosition = new ZoneVector 64 | { 65 | x = player.PlayerPawn.Value!.AbsOrigin.X, y = player.PlayerPawn.Value!.AbsOrigin.Y, 66 | z = player.PlayerPawn.Value!.AbsOrigin.Z 67 | }; 68 | 69 | var startZoneTriggerInfo = route.Start.TriggerInfo; 70 | if (startZoneTriggerInfo.IsInside(playerPosition)) 71 | { 72 | StartPos(playerInfo.GetPlayer()); 73 | return; 74 | } 75 | 76 | var saveloc = new SavedLocations(); 77 | saveloc.SaveLocPosition = playerPosition; 78 | saveloc.Style = playerInfo.Style; 79 | Console.WriteLine("Loc style " + saveloc.Style); 80 | saveloc.RouteKey = playerInfo.RouteKey; 81 | 82 | saveloc.SaveLocAngles = new ZoneVector 83 | { 84 | x = player.PlayerPawn.Value.EyeAngles.X, y = player.PlayerPawn.Value.EyeAngles.Y, 85 | z = player.PlayerPawn.Value.EyeAngles.Z 86 | }; 87 | 88 | saveloc.SaveLocVelocity = new ZoneVector 89 | { 90 | x = player.PlayerPawn.Value.AbsVelocity.X, y = player.PlayerPawn.Value.AbsVelocity.Y, 91 | z = player.PlayerPawn.Value.AbsVelocity.Z 92 | }; 93 | 94 | var timer = entity.GetComponent(); 95 | if (timer != null && (saveloc.Style == "normal" || saveloc.Style == "prac")) 96 | { 97 | while (true) 98 | { 99 | var oneHourOfReplayFrames = 230400; // 10Mb 100 | var tenHoursOfReplayFrames = oneHourOfReplayFrames * 10; // 100Mb 101 | var replayFrameCount = 0; 102 | foreach (var locs in playerInfo.SavedLocations) 103 | { 104 | replayFrameCount += locs.Timer.Primary.Recording.Frames.Count; 105 | replayFrameCount += locs.Timer.Secondary?.Recording.Frames.Count ?? 0; 106 | } 107 | 108 | if (replayFrameCount > tenHoursOfReplayFrames) 109 | { 110 | playerInfo.SavedLocations.RemoveAt(0); 111 | player.PrintToChat($" {CC.White}You have reached the saveloc limit, removing oldest saveloc"); 112 | } 113 | else 114 | { 115 | break; 116 | } 117 | } 118 | saveloc.Timer = timer.DeepCopy(); 119 | } 120 | 121 | if (playerInfo.SavedLocations.Count > 100) 122 | { 123 | playerInfo.SavedLocations.RemoveAt(0); 124 | player.PrintToChat($" {CC.White}You have reached the saveloc limit, removing oldest saveloc"); 125 | } 126 | 127 | playerInfo.SavedLocations.Add(saveloc); 128 | 129 | var saveLocCount = playerInfo.SavedLocations.Count; 130 | playerInfo.CurrentSavelocIndex = saveLocCount - 1; 131 | 132 | player.PrintToChat( 133 | $" {CC.White}Saved location {CC.Secondary}#{saveLocCount}"); 134 | } 135 | 136 | [ConsoleCommand("wst_tele", "Tp to saved location")] 137 | [ConsoleCommand("tele", "Tp to saved location")] 138 | [ConsoleCommand("sm_tele", "Tp to saved location")] 139 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 140 | public void OnTeleCommand(CCSPlayerController player, CommandInfo info) 141 | { 142 | if (player == null) 143 | { 144 | return; 145 | } 146 | 147 | if (!player.NativeIsValidAliveAndNotABot()) 148 | { 149 | return; 150 | } 151 | 152 | GoToSaveloc(player); 153 | } 154 | 155 | 156 | 157 | public void GoToSaveloc(CCSPlayerController player) 158 | { 159 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 160 | var playerInfo = entity.GetComponent(); 161 | 162 | if (playerInfo.SavedLocations.Count == 0) 163 | { 164 | player.PrintToChat($" {CC.White}No saveloc's"); 165 | return; 166 | } 167 | 168 | var saveloc = playerInfo.SavedLocations[playerInfo.CurrentSavelocIndex]; 169 | if (saveloc == null) 170 | { 171 | player.PrintToChat($" {CC.White}No saveloc's"); 172 | return; 173 | } 174 | 175 | var pos = new Vector(saveloc.SaveLocPosition.x, saveloc.SaveLocPosition.y, saveloc.SaveLocPosition.z); 176 | var ang = new QAngle(saveloc.SaveLocAngles.x, saveloc.SaveLocAngles.y, 177 | saveloc.SaveLocAngles.z); 178 | var vel = new Vector(saveloc.SaveLocVelocity.x, saveloc.SaveLocVelocity.y, 179 | saveloc.SaveLocVelocity.z); 180 | 181 | 182 | entity.RemoveComponent(); 183 | 184 | var style = saveloc.Style; 185 | // only goto prac if you were in normal or prac style 186 | if (saveloc.Timer != null) 187 | { 188 | // if we are restoring a timer we always go to prac 189 | style = "prac"; 190 | entity.AddComponent(saveloc.Timer.DeepCopy()); 191 | } 192 | playerInfo.ChangeRoute(saveloc.RouteKey, style); 193 | 194 | if (playerInfo.Style == "lg") 195 | { 196 | player.PlayerPawn.Value!.GravityScale = 0.5f; 197 | } 198 | else 199 | { 200 | player.PlayerPawn.Value!.GravityScale = 1f; 201 | } 202 | 203 | 204 | playerInfo.InPrac = true; 205 | playerInfo.Teleporting = true; 206 | player.PlayerPawn.Value!.Teleport(pos, ang, vel); 207 | Console.WriteLine("TELE"); 208 | player.PrintToChat($" {CC.White}Teleported to saveloc {CC.Secondary}#{playerInfo.CurrentSavelocIndex + 1}"); 209 | 210 | Server.NextFrame(() => 211 | { 212 | Console.WriteLine("NOT TELE"); 213 | playerInfo.Teleporting = false; 214 | }); 215 | } 216 | 217 | [ConsoleCommand("wst_prevloc", "Tp to saved location")] 218 | [ConsoleCommand("prevloc", "Tp to saved location")] 219 | [ConsoleCommand("sm_prevloc", "Tp to saved location")] 220 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 221 | public void OnPrevLocCommand(CCSPlayerController player, CommandInfo info) 222 | { 223 | if (player == null) 224 | { 225 | return; 226 | } 227 | 228 | if (!player.NativeIsValidAliveAndNotABot()) 229 | { 230 | return; 231 | } 232 | 233 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 234 | var playerInfo = entity.GetComponent(); 235 | 236 | if (playerInfo.SavedLocations.Count == 0) 237 | { 238 | player.PrintToChat($" {CC.White}No saveloc's"); 239 | return; 240 | } 241 | 242 | // Remove the current loc 243 | playerInfo.SavedLocations.RemoveAt(playerInfo.CurrentSavelocIndex); 244 | 245 | playerInfo.CurrentSavelocIndex--; 246 | if (playerInfo.CurrentSavelocIndex < 0) 247 | { 248 | playerInfo.CurrentSavelocIndex = playerInfo.SavedLocations.Count - 1; 249 | } 250 | 251 | GoToSaveloc(player); 252 | } 253 | 254 | [ConsoleCommand("wst_backloc", "Tp to saved location")] 255 | [ConsoleCommand("backloc", "Tp to saved location")] 256 | [ConsoleCommand("sm_backloc", "Tp to saved location")] 257 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 258 | public void OnBackLocCommand(CCSPlayerController player, CommandInfo info) 259 | { 260 | if (player == null) 261 | { 262 | return; 263 | } 264 | 265 | if (!player.NativeIsValidAliveAndNotABot()) 266 | { 267 | return; 268 | } 269 | 270 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 271 | var playerInfo = entity.GetComponent(); 272 | 273 | if (playerInfo.SavedLocations.Count == 0) 274 | { 275 | player.PrintToChat($" {CC.White}No saveloc's"); 276 | return; 277 | } 278 | 279 | playerInfo.CurrentSavelocIndex--; 280 | if (playerInfo.CurrentSavelocIndex < 0) 281 | { 282 | playerInfo.CurrentSavelocIndex = playerInfo.SavedLocations.Count - 1; 283 | } 284 | 285 | GoToSaveloc(player); 286 | } 287 | 288 | [ConsoleCommand("wst_nextloc", "Tp to next saved location")] 289 | [ConsoleCommand("nextloc", "Tp to next saved location")] 290 | [ConsoleCommand("sm_nextloc", "Tp to next saved location")] 291 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 292 | public void OnNextLocCommand(CCSPlayerController player, CommandInfo info) 293 | { 294 | if (player == null) 295 | { 296 | return; 297 | } 298 | 299 | if (!player.NativeIsValidAliveAndNotABot()) 300 | { 301 | return; 302 | } 303 | 304 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 305 | var playerInfo = entity.GetComponent(); 306 | 307 | if (playerInfo.SavedLocations.Count == 0) 308 | { 309 | player.PrintToChat($" {CC.White}No saveloc's"); 310 | return; 311 | } 312 | 313 | // Increment the current index and wrap around if it exceeds the number of saved locations 314 | playerInfo.CurrentSavelocIndex = (playerInfo.CurrentSavelocIndex + 1) % playerInfo.SavedLocations.Count; 315 | 316 | GoToSaveloc(player); 317 | } 318 | } -------------------------------------------------------------------------------- /Commands/SurfGameplayCommands.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Core; 2 | using CounterStrikeSharp.API.Core.Attributes.Registration; 3 | using CounterStrikeSharp.API.Modules.Commands; 4 | using CounterStrikeSharp.API.Modules.Utils; 5 | 6 | namespace WST; 7 | 8 | public partial class Wst 9 | { 10 | [ConsoleCommand("r", "Restarts you back to the starting location of the current route")] 11 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 12 | public void OnRCommand(CCSPlayerController? caller, CommandInfo command) 13 | { 14 | if (caller == null) 15 | { 16 | return; 17 | } 18 | 19 | // Open your console and type `bind sm_r` to bind this command to a key 20 | // Restarting via chat commands is annoying for players on the server! 21 | caller.PrintToChat( 22 | $" {CC.White}Open your console (~) and type {CC.Secondary}bind sm_r{CC.White} to bind this command to a key"); 23 | Restart(caller); 24 | } 25 | 26 | [ConsoleCommand("sm_stuck", "Sets a custom start position for the current route")] 27 | [ConsoleCommand("wst_stuck", "Sets a custom start position for the current route")] 28 | [ConsoleCommand("stuck", "Teleports you to the start of a stage")] 29 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 30 | public void OnStuckCommand(CCSPlayerController? caller, CommandInfo command) 31 | { 32 | if (caller == null) 33 | { 34 | return; 35 | } 36 | 37 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 38 | var playerInfo = entity.GetComponent(); 39 | 40 | if (playerInfo.SecondaryRouteKey == null) 41 | { 42 | Restart(caller); 43 | return; 44 | } 45 | 46 | var map = _entityManager.FindEntity(); 47 | var mapZones = map.GetComponent(); 48 | var route = mapZones.GetRoute(playerInfo.SecondaryRouteKey); 49 | 50 | if (route == null) 51 | { 52 | Restart(caller); 53 | return; 54 | } 55 | 56 | var startPos = route.StartPos; 57 | var startAngles = route.StartAngles; 58 | 59 | var startPosVector = new Vector(startPos.x, startPos.y, startPos.z); 60 | var startPosAngles = new QAngle(startAngles.x, startAngles.y, startAngles.z); 61 | var startVelocityVector = new Vector(0, 0, 0); 62 | 63 | 64 | caller.PlayerPawn.Value!.MoveType = MoveType_t.MOVETYPE_WALK; 65 | caller.PlayerPawn.Value!.Teleport(startPosVector, startPosAngles, startVelocityVector); 66 | 67 | } 68 | 69 | [ConsoleCommand("sm_repeat", "Sets a custom start position for the current route")] 70 | [ConsoleCommand("wst_repeat", "Sets a custom start position for the current route")] 71 | [ConsoleCommand("repeat", "When your done with a route teleports you back to the start")] 72 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 73 | public void OnRepeatCommand(CCSPlayerController? caller, CommandInfo command) 74 | { 75 | if (caller == null) 76 | { 77 | return; 78 | } 79 | 80 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 81 | var playerInfo = entity.GetComponent(); 82 | 83 | playerInfo.RepeatMode = !playerInfo.RepeatMode; 84 | 85 | if (playerInfo.RepeatMode) 86 | { 87 | caller.PrintToChat($" {CC.White}Repeat mode {CC.Secondary}enabled"); 88 | } 89 | else 90 | { 91 | caller.PrintToChat($" {CC.White}Repeat mode {CC.Secondary}disabled"); 92 | } 93 | } 94 | 95 | [ConsoleCommand("normal", "Normal surf style")] 96 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 97 | public void OnNormalCommand(CCSPlayerController? caller, CommandInfo command) 98 | { 99 | if (caller == null) 100 | { 101 | return; 102 | } 103 | 104 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 105 | var playerInfo = entity.GetComponent(); 106 | 107 | playerInfo.Style = "normal"; 108 | 109 | 110 | Restart(caller); 111 | 112 | caller.PrintToChat($" {CC.White}Normal surf {CC.Main}enabled"); 113 | } 114 | 115 | [ConsoleCommand("sw", "Sideways surf style")] 116 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 117 | public void OnSwCommand(CCSPlayerController? caller, CommandInfo command) 118 | { 119 | if (caller == null) 120 | { 121 | return; 122 | } 123 | 124 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 125 | var playerInfo = entity.GetComponent(); 126 | 127 | playerInfo.Style = playerInfo.Style == "sw" ? "normal" : "sw"; 128 | 129 | 130 | Restart(caller); 131 | 132 | if (playerInfo.Style == "sw") 133 | { 134 | caller.PrintToChat($" {CC.White}Sideways surf {CC.Secondary}enabled"); 135 | } 136 | else 137 | { 138 | caller.PrintToChat($" {CC.White}Sideways surf {CC.Secondary}disabled"); 139 | } 140 | } 141 | 142 | [ConsoleCommand("hsw", "Half-Sideways surf style")] 143 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 144 | public void OnHswCommand(CCSPlayerController? caller, CommandInfo command) 145 | { 146 | if (caller == null) 147 | { 148 | return; 149 | } 150 | 151 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 152 | var playerInfo = entity.GetComponent(); 153 | 154 | playerInfo.Style = playerInfo.Style == "hsw" ? "normal" : "hsw"; 155 | 156 | 157 | Restart(caller); 158 | 159 | if (playerInfo.Style == "hsw") 160 | { 161 | caller.PrintToChat($" {CC.White}Half-Sideways surf {CC.Secondary}enabled"); 162 | } 163 | else 164 | { 165 | caller.PrintToChat($" {CC.White}Half-Sideways surf {CC.Secondary}disabled"); 166 | } 167 | } 168 | 169 | [ConsoleCommand("lg", "Low gravity style")] 170 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 171 | public void OnLgCommand(CCSPlayerController? caller, CommandInfo command) 172 | { 173 | if (caller == null) 174 | { 175 | return; 176 | } 177 | 178 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 179 | var playerInfo = entity.GetComponent(); 180 | 181 | playerInfo.Style = playerInfo.Style == "lg" ? "normal" : "lg"; 182 | 183 | 184 | Restart(caller); 185 | 186 | if (playerInfo.Style == "lg") 187 | { 188 | caller.PrintToChat($" {CC.White}Low gravity {CC.Main}enabled"); 189 | } 190 | else 191 | { 192 | caller.PrintToChat($" {CC.White}Low gravity {CC.Main}disabled"); 193 | } 194 | } 195 | 196 | [ConsoleCommand("tm", "Turbomaster Style")] 197 | [ConsoleCommand("turbo", "Turbomaster Style")] 198 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 199 | public void OnTmCommand(CCSPlayerController? caller, CommandInfo command) 200 | { 201 | if (caller == null) 202 | { 203 | return; 204 | } 205 | 206 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 207 | var playerInfo = entity.GetComponent(); 208 | 209 | playerInfo.Style = playerInfo.Style == "tm" ? "normal" : "tm"; 210 | 211 | 212 | Restart(caller); 213 | 214 | if (playerInfo.Style == "tm") 215 | { 216 | caller.PrintToChat($" {CC.White}Turbo {CC.Main}enabled"); 217 | } 218 | else 219 | { 220 | caller.PrintToChat($" {CC.White}Turbo {CC.Main}disabled"); 221 | } 222 | } 223 | 224 | [ConsoleCommand("sm_startpos", "Sets a custom start position for the current route")] 225 | [ConsoleCommand("wst_startpos", "Sets a custom start position for the current route")] 226 | [ConsoleCommand("startpos", "Sets a custom start position for the current route")] 227 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 228 | public void OnStartposCommand(CCSPlayerController? caller, CommandInfo command) 229 | { 230 | if (caller == null) 231 | { 232 | return; 233 | } 234 | 235 | StartPos(caller); 236 | } 237 | 238 | public void StartPos(CCSPlayerController caller) 239 | { 240 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 241 | var playerInfo = entity.GetComponent(); 242 | 243 | var map = _entityManager.FindEntity(); 244 | var mapZones = map.GetComponent(); 245 | var route = mapZones.GetRoute(playerInfo.RouteKey); 246 | 247 | if (route == null) 248 | { 249 | caller.PrintToChat($" {CC.White}You must be on a route to set the start position"); 250 | return; 251 | } 252 | 253 | var startZoneTriggerInfo = route.Start.TriggerInfo; 254 | 255 | if (startZoneTriggerInfo == null) 256 | { 257 | caller.PrintToChat($" {CC.White}Something went wrong"); 258 | return; 259 | } 260 | 261 | // check player is on ground 262 | const int FL_ONGROUND = 1 << 0; 263 | 264 | if ((caller.Pawn.Value.Flags & FL_ONGROUND) == 0) 265 | { 266 | caller.PrintToChat($" {CC.White}You must be on the ground to set the start position"); 267 | return; 268 | } 269 | 270 | 271 | var player = playerInfo.GetPlayer(); 272 | var playerPos = player.Pawn.Value.AbsOrigin; 273 | var playerAngle = player.PlayerPawn.Value.EyeAngles; 274 | 275 | var startPos = new ZoneVector(playerPos.X, playerPos.Y, playerPos.Z); 276 | var startAngles = new ZoneVector(playerAngle.X, playerAngle.Y, playerAngle.Z); 277 | 278 | if (!startZoneTriggerInfo.IsInside(startPos)) 279 | { 280 | caller.PrintToChat($" {CC.White}You must be inside the start zone to set the start position"); 281 | return; 282 | } 283 | 284 | playerInfo.CustomStartPos = startPos; 285 | playerInfo.CustomStartAng = startAngles; 286 | 287 | caller.PrintToChat($" {CC.White}Start position set to your current position"); 288 | } 289 | 290 | 291 | [ConsoleCommand("wst_r", "Restarts you back to the starting location of the current route")] 292 | [ConsoleCommand("sm_r", "Restarts you back to the starting location of the current route")] 293 | [ConsoleCommand("os_r", "Restarts you back to the starting location of the current route")] 294 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 295 | public void OnRestartCommand(CCSPlayerController? caller, CommandInfo command) 296 | { 297 | if (caller == null) 298 | { 299 | return; 300 | } 301 | 302 | Restart(caller); 303 | } 304 | 305 | public void Restart(CCSPlayerController? caller) 306 | { 307 | try 308 | { 309 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 310 | var playerInfo = entity.GetComponent(); 311 | entity.RemoveComponent(); 312 | entity.RemoveComponent(); 313 | 314 | var map = _entityManager.FindEntity(); 315 | var mapZones = map.GetComponent(); 316 | var route = mapZones.GetRoute(playerInfo.RouteKey); 317 | 318 | ZoneVector startPos; 319 | ZoneVector startAngles; 320 | ZoneVector startVelocity; 321 | 322 | if (playerInfo.CustomStartPos != null && playerInfo.CustomStartAng != null) 323 | { 324 | startPos = playerInfo.CustomStartPos; 325 | startAngles = playerInfo.CustomStartAng; 326 | startVelocity = new ZoneVector(0, 0, 0); 327 | } 328 | else 329 | { 330 | startPos = route.StartPos; 331 | startAngles = route.StartAngles; 332 | startVelocity = route.StartVelocity; 333 | } 334 | 335 | var startPosVector = new Vector(startPos.x, startPos.y, startPos.z); 336 | var startPosAngles = new QAngle(startAngles.x, startAngles.y, startAngles.z); 337 | var startVelocityVector = new Vector(startVelocity.x, startVelocity.y, startVelocity.z); 338 | 339 | if (playerInfo.Style == "prac") 340 | { 341 | playerInfo.Style = "normal"; 342 | } 343 | 344 | if (caller.TeamNum == (int)CsTeam.Spectator) 345 | { 346 | caller.PrintToChat(" You cannot restart while spectating, press M to join a team"); 347 | // caller.ChangeTeam(CsTeam.CounterTerrorist); 348 | // Server.NextFrame(() => 349 | // { 350 | // caller.Respawn(); 351 | // }); 352 | } 353 | else 354 | { 355 | if (playerInfo.Style == "lg") 356 | { 357 | caller.PlayerPawn.Value!.GravityScale = 0.5f; 358 | } 359 | else 360 | { 361 | caller.PlayerPawn.Value!.GravityScale = 1f; 362 | } 363 | 364 | caller.PlayerPawn.Value!.MoveType = MoveType_t.MOVETYPE_WALK; 365 | caller.PlayerPawn.Value!.Teleport(startPosVector, startPosAngles, startVelocityVector); 366 | } 367 | } 368 | catch (Exception e) 369 | { 370 | Console.WriteLine(e); 371 | } 372 | } 373 | } -------------------------------------------------------------------------------- /Commands/SurfStatsCommands.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | using CounterStrikeSharp.API.Core; 3 | using CounterStrikeSharp.API.Core.Attributes.Registration; 4 | using CounterStrikeSharp.API.Modules.Commands; 5 | using CounterStrikeSharp.API.Modules.Utils; 6 | 7 | namespace WST; 8 | 9 | public partial class Wst 10 | { 11 | [ConsoleCommand("surftop", "Prints the top players by points")] 12 | [ConsoleCommand("ptop", "Prints the top players by points")] 13 | public void OnSurfTopCommand(CCSPlayerController? caller, CommandInfo command) 14 | { 15 | if (caller == null) 16 | { 17 | return; 18 | } 19 | 20 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 21 | var playerInfo = entity.GetComponent(); 22 | var style = playerInfo.Style; 23 | 24 | _ = Task.Run(async () => 25 | { 26 | IEnumerable top10; 27 | if (style == "normal") 28 | { 29 | top10 = await _database.QueryAsync("SELECT * FROM v_players ORDER BY p_points DESC LIMIT 10"); 30 | } 31 | else if (style == "lg") 32 | { 33 | top10 = await _database.QueryAsync("SELECT * FROM v_players ORDER BY lg_points DESC LIMIT 10"); 34 | } 35 | else if (style == "tm") 36 | { 37 | top10 = await _database.QueryAsync("SELECT * FROM v_players ORDER BY tm_points DESC LIMIT 10"); 38 | } 39 | else if (style == "sw") 40 | { 41 | top10 = await _database.QueryAsync("SELECT * FROM v_players ORDER BY sw_points DESC LIMIT 10"); 42 | } 43 | else if (style == "hsw") 44 | { 45 | top10 = await _database.QueryAsync("SELECT * FROM v_players ORDER BY hsw_points DESC LIMIT 10"); 46 | } 47 | else if (style == "prac") 48 | { 49 | top10 = await _database.QueryAsync("SELECT * FROM v_players ORDER BY prac_points DESC LIMIT 10"); 50 | } 51 | else 52 | { 53 | throw new Exception("Unknown style"); 54 | } 55 | 56 | Server.NextFrame(() => 57 | { 58 | foreach (var player in top10) 59 | { 60 | SkillGroup skillGroup; 61 | if (style == "normal") 62 | { 63 | skillGroup = SkillGroups.GetSkillGroup(player.Rank, player.PPoints); 64 | } 65 | else if (style == "lg") 66 | { 67 | skillGroup = SkillGroups.GetSkillGroup(player.LgRank, player.LgPoints); 68 | } 69 | else if (style == "tm") 70 | { 71 | skillGroup = SkillGroups.GetSkillGroup(player.TmRank, player.TmPoints); 72 | } 73 | else if (style == "sw") 74 | { 75 | skillGroup = SkillGroups.GetSkillGroup(player.SwRank, player.SwPoints); 76 | } 77 | else if (style == "hsw") 78 | { 79 | skillGroup = SkillGroups.GetSkillGroup(player.HswRank, player.HswPoints); 80 | } 81 | else if (style == "prac") 82 | { 83 | skillGroup = SkillGroups.GetSkillGroup(player.PracRank, player.PracPoints); 84 | } 85 | else 86 | { 87 | throw new Exception("Unknown style"); 88 | } 89 | 90 | int rank = 0; 91 | if (style == "normal") 92 | { 93 | rank = player.Rank; 94 | } 95 | else if (style == "lg") 96 | { 97 | rank = player.LgRank; 98 | } 99 | else if (style == "tm") 100 | { 101 | rank = player.TmRank; 102 | } 103 | else if (style == "sw") 104 | { 105 | rank = player.SwRank; 106 | } 107 | else if (style == "hsw") 108 | { 109 | rank = player.HswRank; 110 | } 111 | else if (style == "prac") 112 | { 113 | rank = player.PracRank; 114 | } 115 | else 116 | { 117 | throw new Exception("Unknown style"); 118 | } 119 | 120 | int points = 0; 121 | if (style == "normal") 122 | { 123 | points = player.PPoints; 124 | } 125 | else if (style == "lg") 126 | { 127 | points = player.LgPoints; 128 | } 129 | else if (style == "tm") 130 | { 131 | points = player.TmPoints; 132 | } 133 | else if (style == "sw") 134 | { 135 | points = player.SwPoints; 136 | } 137 | else if (style == "hsw") 138 | { 139 | points = player.HswPoints; 140 | } 141 | else if (style == "prac") 142 | { 143 | points = player.PracPoints; 144 | } 145 | else 146 | { 147 | throw new Exception("Unknown style"); 148 | } 149 | 150 | caller.PrintToChat( 151 | $" {CC.Main}#{rank} {skillGroup.ChatColor}{player.Name} {CC.White} | {CC.Secondary}{points}{CC.White} points"); 152 | } 153 | }); 154 | return; 155 | }); 156 | } 157 | 158 | [ConsoleCommand("mtop", "Prints the top 10 times on the current route")] 159 | [ConsoleCommand("top", "Prints the top 10 times on the current route")] 160 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 161 | public void OnTopCommand(CCSPlayerController? caller, CommandInfo command) 162 | { 163 | if (caller == null) 164 | { 165 | return; 166 | } 167 | 168 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 169 | var playerInfo = entity.GetComponent(); 170 | var currentRoute = playerInfo.RouteKey; 171 | var mapName = Server.MapName; 172 | 173 | _ = Task.Run(async () => 174 | { 175 | var top10 = await _database.QueryAsync( 176 | "SELECT * FROM v_ranked_records WHERE mapname = @mapname and route = @route and style = @style ORDER BY position ASC LIMIT @n", 177 | new { mapname = mapName, route = currentRoute, n = 10, style = playerInfo.Style }); 178 | Server.NextFrame(() => 179 | { 180 | foreach (var record in top10) 181 | { 182 | caller.PrintToChat($" {CC.White}#{record.Position}" + record.FormatForChat()); 183 | } 184 | }); 185 | return; 186 | }); 187 | return; 188 | } 189 | 190 | [ConsoleCommand("mrank", "Prints your rank on the current route")] 191 | [ConsoleCommand("pr", "Prints your rank on the current route")] 192 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 193 | public void OnPrCommand(CCSPlayerController? caller, CommandInfo command) 194 | { 195 | if (caller == null) 196 | { 197 | return; 198 | } 199 | 200 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 201 | var playerInfo = entity.GetComponent(); 202 | 203 | var arg1 = command.GetArg(1); 204 | var arg2 = command.GetArg(2); 205 | 206 | var currentRoute = playerInfo.RouteKey; 207 | var mapName = Server.MapName; 208 | 209 | if (!string.IsNullOrEmpty(arg1)) 210 | { 211 | mapName = arg1; 212 | currentRoute = "main"; 213 | } 214 | 215 | if (!string.IsNullOrEmpty(arg2)) 216 | { 217 | currentRoute = arg2; 218 | } 219 | 220 | _ = Task.Run(async () => 221 | { 222 | Console.WriteLine(mapName + " " + currentRoute + " " + playerInfo.Style + " " + playerInfo.SteamId); 223 | var pr = await _database.QueryAsync( 224 | "SELECT * FROM v_ranked_records WHERE mapname = @mapname and route = @route and style = @style and steam_id = @steamid", 225 | new { mapname = mapName, route = currentRoute, steamid = playerInfo.SteamId, style = playerInfo.Style }); 226 | 227 | Server.NextFrame(() => 228 | { 229 | if (pr.Count() == 0) 230 | { 231 | caller.PrintToChat($" {CC.White}You do not have a rank on this route"); 232 | return; 233 | } 234 | 235 | var record = pr.First(); 236 | caller.PrintToChat(record.FormatForChat()); 237 | }); 238 | return; 239 | }); 240 | } 241 | 242 | [ConsoleCommand("rank", "Prints your current server rank")] 243 | [ConsoleCommand("pinfo", "Prints your current server rank")] 244 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 245 | public void OnRankCommand(CCSPlayerController? caller, CommandInfo command) 246 | { 247 | if (caller == null) 248 | { 249 | return; 250 | } 251 | 252 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 253 | var playerInfo = entity.GetComponent(); 254 | 255 | var rank = playerInfo.StyleRank; 256 | var totalPlayers = playerInfo.PlayerStats.TotalPlayers; 257 | var totalPoints = playerInfo.StylePoints; 258 | 259 | var skillGroup = playerInfo.SkillGroup.Name; 260 | 261 | var style = ""; 262 | if (playerInfo.Style != "normal") 263 | { 264 | style = $" {CC.White}Style: {CC.Secondary}{playerInfo.Style.ToUpper()} |"; 265 | } 266 | 267 | var rankString = $" {CC.White}Rank: {CC.Secondary}{rank}{CC.White}/{CC.Secondary}{totalPlayers}{CC.White} | Points: {CC.Secondary}{totalPoints}{CC.White} | Skill Group: {playerInfo.SkillGroup.ChatColor}{skillGroup}"; 268 | 269 | caller.PrintToChat(style + rankString); 270 | 271 | } 272 | 273 | [ConsoleCommand("wr", "Prints the current server record on the current would")] 274 | [ConsoleCommand("sr", "Prints the current server record on the current would")] 275 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 276 | public void OnWrCommand(CCSPlayerController? caller, CommandInfo command) 277 | { 278 | if (caller == null) 279 | { 280 | return; 281 | } 282 | 283 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 284 | var playerInfo = entity.GetComponent(); 285 | 286 | var arg1 = command.GetArg(1); 287 | var arg2 = command.GetArg(2); 288 | 289 | Console.WriteLine(arg1); 290 | Console.WriteLine(arg2); 291 | 292 | var currentRoute = playerInfo.RouteKey; 293 | var mapName = Server.MapName; 294 | 295 | if (!string.IsNullOrEmpty(arg1)) 296 | { 297 | mapName = arg1; 298 | currentRoute = "main"; 299 | } 300 | 301 | if (!string.IsNullOrEmpty(arg2)) 302 | { 303 | currentRoute = arg2; 304 | } 305 | 306 | _ = Task.Run(async () => 307 | { 308 | var wr = await _database.QueryAsync( 309 | "SELECT * FROM v_ranked_records WHERE mapname = @mapname and route = @route and style = @style and position = 1", 310 | new { mapname = mapName, route = currentRoute, style = playerInfo.Style }); 311 | 312 | Server.NextFrame(() => 313 | { 314 | if (wr.Count() == 0) 315 | { 316 | caller.PrintToChat($" {CC.White}There is no world record on this route"); 317 | return; 318 | } 319 | 320 | var record = wr.First(); 321 | caller.PrintToChat(record.FormatForChat()); 322 | }); 323 | return; 324 | }); 325 | } 326 | 327 | [ConsoleCommand("minfo", "Prints all the routes on the current map")] 328 | [ConsoleCommand("routes", "Prints all the routes on the current map")] 329 | public void OnRouteCommand(CCSPlayerController? caller, CommandInfo command) 330 | { 331 | if (caller == null) 332 | { 333 | return; 334 | } 335 | 336 | var map = _entityManager.FindEntity(); 337 | var mapZones = map.GetComponent(); 338 | 339 | caller.PrintToChat(" Available Routes:"); 340 | foreach (var route in mapZones.Routes) 341 | { 342 | var cmd = $"!{route.Key}"; 343 | var routeTier = route.Tier; 344 | var startVelocity = (new Vector(route.StartVelocity.x, route.StartVelocity.y, route.StartVelocity.z)) 345 | .Length2D(); 346 | caller.PrintToChat( 347 | $" {CC.Secondary}{cmd}{CC.White} | {CC.Secondary}{route.Name}{CC.White} | Tier: {CC.Secondary}{routeTier}{CC.White}"); 348 | } 349 | } 350 | 351 | [ConsoleCommand("points", "Prints the points for the current route")] 352 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 353 | public void OnPointsCommand(CCSPlayerController? caller, CommandInfo command) 354 | { 355 | if (caller == null) 356 | { 357 | return; 358 | } 359 | 360 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 361 | var playerInfo = entity.GetComponent(); 362 | var currentRoute = playerInfo.RouteKey; 363 | var mapName = Server.MapName; 364 | 365 | _ = Task.Run(async () => 366 | { 367 | var positions = new List { 1, 2, 3, 4, 5, 10, 50, 100, 500 }; 368 | var positionParameter = string.Join(",", positions); 369 | var points = await _database.QueryAsync( 370 | "SELECT * FROM v_ranked_records WHERE mapname = @mapname and route = @route and style = @style and position in (" + 371 | positionParameter + ")", 372 | new { mapname = mapName, route = currentRoute, style = playerInfo.Style }); 373 | 374 | 375 | Server.NextFrame(() => 376 | { 377 | foreach (var position in positions) 378 | { 379 | var record = points.FirstOrDefault(x => x.Position == position); 380 | if (record == null) 381 | { 382 | caller.PrintToChat($" {CC.White}#{CC.Secondary}{position}{CC.White}: no record"); 383 | continue; 384 | } 385 | 386 | caller.PrintToChat( 387 | $" {CC.White}#{CC.Secondary}{position}{CC.White}: {CC.Secondary}{record.Points}{CC.White} points"); 388 | } 389 | }); 390 | return; 391 | }); 392 | } 393 | 394 | [ConsoleCommand("cpr", "Checkpoints difference between your time and the current server record")] 395 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 396 | public void OnCprCommand(CCSPlayerController? caller, CommandInfo command) 397 | { 398 | var entity = _entityManager.FindEntity(caller!.Slot.ToString()); 399 | var playerInfo = entity.GetComponent(); 400 | var currentRoute = playerInfo.RouteKey; 401 | 402 | var map = _entityManager.FindEntity(); 403 | var mapZones = map.GetComponent(); 404 | 405 | var playerRecord = playerInfo.PR; 406 | var worldRecord = mapZones.GetWorldRecord(currentRoute, playerInfo.Style); 407 | 408 | if (playerRecord == null) 409 | { 410 | caller.PrintToChat($" {CC.White}You do not have a record on this route"); 411 | return; 412 | } 413 | 414 | if (worldRecord == null) 415 | { 416 | caller.PrintToChat($" {CC.White}There is no world record on this route"); 417 | return; 418 | } 419 | 420 | var playerCheckpoints = playerRecord.CheckpointsObject; 421 | var worldCheckpoints = worldRecord.CheckpointsObject; 422 | 423 | var playerCheckpointCount = playerCheckpoints.Count; 424 | var worldCheckpointCount = worldCheckpoints.Count; 425 | 426 | var checkpointCount = Math.Min(playerCheckpointCount, worldCheckpointCount); 427 | 428 | var checkpointTimeDifferences = new List { 0 }; 429 | var checkpointSpeedDifferences = new List { worldRecord.VelocityStartXY - playerRecord.VelocityStartXY }; 430 | for (var i = 0; i < checkpointCount; i++) 431 | { 432 | var playerCheckpoint = playerCheckpoints[i]; 433 | var worldCheckpoint = worldCheckpoints[i]; 434 | 435 | var checkpointDifference = worldCheckpoint.TimerTicks - playerCheckpoint.TimerTicks; 436 | checkpointTimeDifferences.Add(checkpointDifference); 437 | 438 | var playerCheckpointSpeed = worldCheckpoint.VelocityStartXY - playerCheckpoint.VelocityStartXY; 439 | checkpointSpeedDifferences.Add(playerCheckpointSpeed); 440 | } 441 | 442 | checkpointTimeDifferences.Add(worldRecord.Ticks - playerRecord.Ticks); 443 | checkpointSpeedDifferences.Add(worldRecord.VelocityEndXY - playerRecord.VelocityEndXY); 444 | 445 | 446 | var checkpointTimeDifferenceStrings = 447 | checkpointTimeDifferences.Select(x => Utils.FormatTimeWithPlusOrMinus(x)).ToList(); 448 | 449 | var checkpointSpeedDifferenceStrings = checkpointSpeedDifferences.Select(x => x.ToString("0.00")).ToList(); 450 | 451 | caller.PrintToChat($" {CC.White}CPR:"); 452 | for (var i = 0; i < checkpointCount; i++) 453 | { 454 | var checkpointTimeDifferenceString = checkpointTimeDifferenceStrings[i]; 455 | var checkpointSpeedDifferenceString = checkpointSpeedDifferenceStrings[i]; 456 | caller.PrintToChat( 457 | $" {CC.Main}#{i + 1}{CC.White}: {CC.Secondary}{checkpointTimeDifferenceString}{CC.White} | {CC.Secondary}{checkpointSpeedDifferenceString}{CC.White} u/s"); 458 | } 459 | } 460 | 461 | [ConsoleCommand("ranks", "Prints info about all ranks")] 462 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 463 | public void OnRanksCommand(CCSPlayerController? caller, CommandInfo command) 464 | { 465 | if (caller == null) 466 | { 467 | return; 468 | } 469 | 470 | var ranks = SkillGroups.Groups; 471 | 472 | caller.PrintToChat(" Available Ranks:"); 473 | foreach (var rank in ranks) 474 | { 475 | var idx = ranks.IndexOf(rank) + 1; 476 | var rankName = rank.Name; 477 | var rankColor = rank.ChatColor; 478 | var rankPoints = rank.Points; 479 | if (rankPoints == null) 480 | { 481 | var startPosition = rank.MinRank; 482 | var endPosition = rank.MaxRank; 483 | caller.PrintToChat( 484 | $" {CC.Main}#{idx} {rankColor}{rankName}{CC.White} | {CC.Secondary}{startPosition}{CC.White} - {CC.Secondary}{endPosition}{CC.White}"); 485 | } 486 | else 487 | { 488 | caller.PrintToChat( 489 | $" {CC.Main}#{idx} {rankColor}{rankName}{CC.White} | {CC.Secondary}{rankPoints}{CC.White} points"); 490 | } 491 | } 492 | } 493 | 494 | [ConsoleCommand("rr", "Recent Records")] 495 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 496 | public void OnRecentRecordsCommand(CCSPlayerController caller, CommandInfo info) 497 | { 498 | if (caller == null) 499 | { 500 | return; 501 | } 502 | 503 | 504 | _ = Task.Run(async () => 505 | { 506 | var recentRecords = await _database.QueryAsync( 507 | "SELECT * FROM recent_records ORDER BY created_at DESC LIMIT 10"); 508 | 509 | 510 | Server.NextFrame(() => 511 | { 512 | foreach (var record in recentRecords) 513 | { 514 | var mapName = record.MapName; 515 | var route = record.Route; 516 | var playerName = record.Name; 517 | var time = Utils.FormatTime(record.Ticks); 518 | var date = record.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss"); 519 | 520 | var message = 521 | $" {CC.Main}{mapName} {CC.White} | {CC.Secondary}{route} {CC.White} | {CC.Secondary}{playerName} {CC.White} | {CC.Secondary}{time} {CC.White}"; 522 | 523 | if (record.OldTicks.HasValue && record.OldName != null) 524 | { 525 | message += $"{CC.Main}(-{Utils.FormatTime(record.NewTicks - record.OldTicks.Value)})"; 526 | message += $" {CC.White}beating {CC.Secondary}{record.OldName} "; 527 | } 528 | 529 | caller.PrintToChat(message); 530 | } 531 | }); 532 | return; 533 | }); 534 | } 535 | 536 | public class IncompleteMap 537 | { 538 | public string Name { get; set; } 539 | public string Route { get; set; } 540 | } 541 | 542 | [ConsoleCommand("incomplete", "Prints incomplete maps to console")] 543 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 544 | public void OnIncompleteCommand(CCSPlayerController caller, CommandInfo info) 545 | { 546 | if (caller == null) 547 | { 548 | return; 549 | } 550 | 551 | var type = info.GetArg(1); 552 | if (String.IsNullOrEmpty(type)) 553 | { 554 | type = "main"; 555 | } 556 | 557 | var entity = _entityManager.FindEntity(caller!.Slot.ToString()); 558 | var playerInfo = entity.GetComponent(); 559 | 560 | _ = Task.Run(async () => 561 | { 562 | var incompleteMaps = await _database.QueryAsync( 563 | @"SELECT 564 | m.name, 565 | m.route 566 | FROM 567 | maps_2 m 568 | LEFT JOIN 569 | records r ON m.name = r.mapname AND m.route = r.route AND r.steam_id = @steamid and style = @style 570 | WHERE 571 | r.mapname IS NULL", new { steamid = playerInfo.SteamId, style = playerInfo.Style }); 572 | 573 | Server.NextFrame(() => 574 | { 575 | var maps = incompleteMaps.Where(x => x.Route == "main" || x.Route.StartsWith("boost")).ToList(); 576 | var bonuses = incompleteMaps.Where(x => x.Route.StartsWith("b")).ToList(); 577 | var stages = incompleteMaps.Where(x => x.Route.StartsWith("s")).ToList(); 578 | 579 | var toPrint = maps; 580 | if (type.StartsWith("b", StringComparison.InvariantCultureIgnoreCase)) 581 | { 582 | toPrint = bonuses; 583 | } 584 | else if (type.StartsWith("s", StringComparison.InvariantCultureIgnoreCase)) 585 | { 586 | toPrint = stages; 587 | } 588 | 589 | if (toPrint.Count == 0) 590 | { 591 | caller.PrintToChat($" {CC.White}You have completed all {type} routes"); 592 | return; 593 | } 594 | 595 | if (toPrint.Count >= 9) 596 | { 597 | // print first 8 to chat 598 | foreach (var map in toPrint.Take(8)) 599 | { 600 | caller.PrintToChat($" {CC.Main}{map.Name} {CC.White} | {CC.Secondary}{map.Route}"); 601 | } 602 | 603 | caller.PrintToChat($" The remaining {toPrint.Count - 8} {type} routes will be printed to console"); 604 | foreach (var map in toPrint.Skip(8)) 605 | { 606 | caller.PrintToConsole($" {map.Name}| {map.Route}"); 607 | } 608 | } 609 | else 610 | { 611 | foreach (var map in toPrint) 612 | { 613 | caller.PrintToChat($" {CC.Main}{map.Name} {CC.White} | {CC.Secondary}{map.Route}"); 614 | } 615 | } 616 | }); 617 | }); 618 | } 619 | } -------------------------------------------------------------------------------- /Commands/UtilCommands.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | using CounterStrikeSharp.API.Core; 3 | using CounterStrikeSharp.API.Core.Attributes.Registration; 4 | using CounterStrikeSharp.API.Modules.Commands; 5 | using CounterStrikeSharp.API.Modules.Utils; 6 | 7 | namespace WST; 8 | 9 | public partial class Wst 10 | { 11 | [ConsoleCommand("servers", "Prints servers")] 12 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 13 | public void OnServersCommand(CCSPlayerController? caller, CommandInfo command) 14 | { 15 | if (caller == null) 16 | { 17 | return; 18 | } 19 | 20 | _ = Task.Run(async () => 21 | { 22 | var gameServers = await _database.QueryAsync("select * from servers"); 23 | Server.NextFrame(() => 24 | { 25 | foreach (var server in gameServers) 26 | { 27 | if (server.IsPublic) 28 | { 29 | caller.PrintToChat( 30 | $" {CC.Main}{server.Hostname} {CC.White} | {CC.Secondary}{server.CurrentMap} | {CC.Secondary}{server.PlayerCount}/{server.TotalPlayers} | {CC.Secondary}connect {server.IP}"); 31 | } 32 | } 33 | }); 34 | }); 35 | } 36 | 37 | class StyleInfo 38 | { 39 | public string Command { get; set; } 40 | public string Name { get; set; } 41 | public string Explaination { get; set; } 42 | } 43 | 44 | [ConsoleCommand("style", "Prints styles")] 45 | [ConsoleCommand("styles", "Prints styles")] 46 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 47 | public void OnStylesCommand(CCSPlayerController? caller, CommandInfo command) 48 | { 49 | if (caller == null) 50 | { 51 | return; 52 | } 53 | 54 | var styles = new List 55 | { 56 | new StyleInfo {Command = "normal", Name = "Normal", Explaination = "Normal"}, 57 | new StyleInfo {Command = "turbo", Name = "Turbo", Explaination = "Left click to go faster, right click to go slow"}, 58 | new StyleInfo {Command = "lg", Name = "Low Gravity", Explaination = "Play the map with half of normal gravity"}, 59 | new StyleInfo {Command = "sw", Name = "Sideways Surf", Explaination = "Play the map with sideways surf (only use W and S)"}, 60 | new StyleInfo {Command = "hsw", Name = "Half-Sideways Surf", Explaination = "Play the map with HSW surf (hold two movement keys to surf half sideways ie W and A)"}, 61 | new StyleInfo {Command = "prac", Name = "Practice Mode/TAS", Explaination = "Use checkpoints to create the ultimate run"}, 62 | }; 63 | 64 | caller.PrintToChat(" Available Styles:"); 65 | foreach (var style in styles) 66 | { 67 | // var cmd = $"!{styles}"; 68 | // var routeTier = route.Tier; 69 | // var startVelocity = (new Vector(route.StartVelocity.x, route.StartVelocity.y, route.StartVelocity.z)) 70 | // .Length2D(); 71 | // caller.PrintToChat( 72 | // $" {CC.Secondary}{cmd}{CC.White} | {CC.Secondary}{route.Name}{CC.White} | Tier: {CC.Secondary}{routeTier}{CC.White}"); 73 | 74 | caller.PrintToChat( 75 | $" {CC.Secondary}!{style.Command}{CC.White} | {CC.Main}{style.Name}{CC.White} | {CC.White}{style.Explaination}"); 76 | } 77 | 78 | } 79 | 80 | [ConsoleCommand("help", "help")] 81 | [ConsoleCommand("commands", "comamnds")] 82 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 83 | public void OnHelpCommand(CCSPlayerController player, CommandInfo info) 84 | { 85 | var commands = new List 86 | { 87 | "r", "styles", "wr", "pr", "rank", "cpr", "top", "surftop", "replay", "routes", "points", "ranks", "help", "rr", 88 | "incomplete" 89 | }; 90 | var str = String.Join(", ", commands); 91 | player.PrintToChat($" Commands: {CC.Secondary}{str}"); 92 | } 93 | 94 | 95 | 96 | private bool IsInSpec(CCSPlayerController? caller) 97 | { 98 | if (caller == null) return false; 99 | return caller.TeamNum == (int)CsTeam.Spectator; 100 | } 101 | 102 | [ConsoleCommand("spec", "Moves you to spec.")] 103 | [ConsoleCommand("wst_spec", "Moves you to spec.")] 104 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 105 | public void OnSpecCommand(CCSPlayerController? caller, CommandInfo command) 106 | { 107 | if (caller == null) 108 | { 109 | return; 110 | } 111 | 112 | var entity = _entityManager.FindEntity(caller.Slot.ToString()); 113 | entity.RemoveComponent(); 114 | entity.RemoveComponent(); 115 | 116 | if (caller.TeamNum == (int)CsTeam.Spectator) 117 | { 118 | return; 119 | } 120 | 121 | if (caller.TeamNum == (int)CsTeam.Terrorist || caller.TeamNum == (int)CsTeam.CounterTerrorist) 122 | { 123 | caller.ChangeTeam(CsTeam.Spectator); 124 | } 125 | 126 | return; 127 | } 128 | } -------------------------------------------------------------------------------- /Commands/VIPCommands.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | using CounterStrikeSharp.API.Core; 3 | using CounterStrikeSharp.API.Core.Attributes.Registration; 4 | using CounterStrikeSharp.API.Modules.Commands; 5 | using CounterStrikeSharp.API.Modules.Utils; 6 | 7 | namespace WST; 8 | 9 | public partial class Wst 10 | { 11 | 12 | // admins can change map always 13 | // vips can only change map on vip server 14 | public bool HasMapChangePermission(bool isVip, bool isAdmin, bool isVipServer) 15 | { 16 | if (isAdmin || isVip) 17 | { 18 | return true; 19 | } 20 | 21 | return false; 22 | } 23 | 24 | 25 | [ConsoleCommand("map", "Change Map")] 26 | [ConsoleCommand("wst_map", "Change map")] 27 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 28 | public void OnChangeMap(CCSPlayerController player, CommandInfo info) 29 | { 30 | if (player == null) 31 | { 32 | return; 33 | } 34 | 35 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 36 | var playerInfo = entity.GetComponent(); 37 | 38 | if (!HasMapChangePermission(playerInfo.PlayerStats.IsVip, playerInfo.PlayerStats.IsAdmin, 39 | _currentGameServer.Vip)) 40 | { 41 | return; 42 | } 43 | 44 | 45 | var mapName = info.GetArg(1); 46 | 47 | 48 | var map = _workshopMaps.Where(x => x.Title.Equals(mapName, StringComparison.InvariantCultureIgnoreCase)) 49 | .ToList(); 50 | 51 | if (map.Count == 0) 52 | { 53 | player.PrintToChat($" {ChatColors.Purple}[VIP] {CC.White}map not found"); 54 | return; 55 | } 56 | 57 | var mapId = map.First().Publishedfileid; 58 | 59 | Server.ExecuteCommand($"host_workshop_map {mapId}"); 60 | } 61 | 62 | [ConsoleCommand("chattag", "Change Chat Tag")] 63 | [ConsoleCommand("wst_chattag", "Change chat tag")] 64 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 65 | public void OnChangeChatTag(CCSPlayerController player, CommandInfo info) 66 | { 67 | if (player == null) 68 | { 69 | return; 70 | } 71 | 72 | Console.WriteLine(info.GetCommandString); 73 | Console.WriteLine(info.ArgString); 74 | Console.WriteLine(info.GetArg(1)); 75 | 76 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 77 | var playerInfo = entity.GetComponent(); 78 | 79 | if (!playerInfo.PlayerStats.IsVip) 80 | { 81 | player.PrintToChat(NoVipMessage); 82 | return; 83 | } 84 | 85 | var chatTag = info.ArgString; 86 | 87 | if (string.IsNullOrEmpty(chatTag)) 88 | { 89 | playerInfo.PlayerStats.CustomTag = null; 90 | Task.Run(async () => 91 | { 92 | await _database.ExecuteAsync("UPDATE players SET custom_tag = NULL WHERE steam_id = @steamId", 93 | new { steamId = playerInfo.SteamId }); 94 | }); 95 | player.PrintToChat($" {ChatColors.Purple}[VIP] {CC.White}Chat tag reset"); 96 | return; 97 | } 98 | 99 | // check chattag incldues whitespace 100 | if (chatTag.Contains(" ")) 101 | { 102 | player.PrintToChat($" {ChatColors.Purple}[VIP] {CC.White}Chat tag cannot include whitespace"); 103 | return; 104 | } 105 | 106 | var withoutColor = Utils.RemoveColorNames(chatTag); 107 | if (withoutColor.Length > 15) 108 | { 109 | player.PrintToChat($" {ChatColors.Purple}[VIP] {CC.Secondary}{playerInfo.Name} {CC.White}chat tag cannot be longer than 15 characters"); 110 | return; 111 | } 112 | 113 | playerInfo.PlayerStats.CustomTag = chatTag; 114 | Task.Run(async () => 115 | { 116 | await _database.ExecuteAsync("UPDATE players SET custom_tag = @customTag WHERE steam_id = @steamId", 117 | new { customTag = chatTag, steamId = playerInfo.SteamId }); 118 | }); 119 | // player.PrintToChat($" {ChatColors.Purple}[VIP] {ChatColors.White}Chat color reset"); 120 | player.PrintToChat($" {ChatColors.Purple}[VIP] {CC.White}chat tag set to {Utils.ColorNamesToTags(chatTag)}"); 121 | 122 | } 123 | 124 | [ConsoleCommand("chatcolor", "Change Chat Color")] 125 | [ConsoleCommand("wst_chatcolor", "Change chat color")] 126 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 127 | public void OnChangeChatColor(CCSPlayerController player, CommandInfo info) 128 | { 129 | if (player == null) 130 | { 131 | return; 132 | } 133 | 134 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 135 | var playerInfo = entity.GetComponent(); 136 | 137 | if (!playerInfo.PlayerStats.IsVip) 138 | { 139 | player.PrintToChat(NoVipMessage); 140 | return; 141 | } 142 | 143 | var chatColor = info.ArgString; 144 | if (string.IsNullOrEmpty(chatColor)) 145 | { 146 | playerInfo.PlayerStats.ChatColor = null; 147 | Task.Run(async () => 148 | { 149 | await _database.ExecuteAsync("UPDATE players SET chat_color = NULL WHERE steam_id = @steamId", 150 | new { steamId = playerInfo.SteamId }); 151 | }); 152 | player.PrintToChat($" {ChatColors.Purple}[VIP] {ChatColors.White}Chat color reset"); 153 | return; 154 | } 155 | 156 | if (chatColor.Contains(" ")) 157 | { 158 | player.PrintToChat($" {ChatColors.Purple}[VIP] {CC.White}Chat tag cannot include whitespace"); 159 | return; 160 | } 161 | 162 | if (!Utils.IsStringAColorName(chatColor)) 163 | { 164 | player.PrintToChat($" {ChatColors.Purple}[VIP] {ChatColors.White}Invalid color, try !chatcolor {{LIME}}"); 165 | return; 166 | } 167 | 168 | playerInfo.PlayerStats.ChatColor = chatColor; 169 | Task.Run(async () => 170 | { 171 | await _database.ExecuteAsync("UPDATE players SET chat_color = @chatColor WHERE steam_id = @steamId", 172 | new { chatColor, steamId = playerInfo.SteamId }); 173 | }); 174 | player.PrintToChat($" {ChatColors.Purple}[VIP] {ChatColors.White}Chat color set to {Utils.ColorNamesToTags(chatColor)}{chatColor}"); 175 | } 176 | 177 | [ConsoleCommand("namecolor", "Change Name Color")] 178 | [ConsoleCommand("wst_namecolor", "Change Name color")] 179 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 180 | public void OnChangeNameColor(CCSPlayerController player, CommandInfo info) 181 | { 182 | if (player == null) 183 | { 184 | return; 185 | } 186 | 187 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 188 | var playerInfo = entity.GetComponent(); 189 | 190 | if (!playerInfo.PlayerStats.IsVip) 191 | { 192 | player.PrintToChat(NoVipMessage); 193 | return; 194 | } 195 | 196 | var nameColor = info.ArgString; 197 | if (string.IsNullOrEmpty(nameColor)) 198 | { 199 | playerInfo.PlayerStats.NameColor = null; 200 | Task.Run(async () => 201 | { 202 | await _database.ExecuteAsync("UPDATE players SET name_color = NULL WHERE steam_id = @steamId", 203 | new { steamId = playerInfo.SteamId }); 204 | }); 205 | player.PrintToChat($" {ChatColors.Purple}[VIP] {ChatColors.White}Name color reset"); 206 | return; 207 | } 208 | 209 | 210 | if (!Utils.IsStringAColorName(nameColor)) 211 | { 212 | player.PrintToChat($" {ChatColors.Purple}[VIP] {ChatColors.White}Invalid color, try !namecolor {{LIME}}"); 213 | return; 214 | } 215 | 216 | playerInfo.PlayerStats.NameColor = nameColor; 217 | Task.Run(async () => 218 | { 219 | await _database.ExecuteAsync("UPDATE players SET name_color = @nameColor WHERE steam_id = @steamId", 220 | new { nameColor, steamId = playerInfo.SteamId }); 221 | }); 222 | player.PrintToChat($" {ChatColors.Purple}[VIP] {ChatColors.White}Name color set to {Utils.ColorNamesToTags(nameColor)}{nameColor}"); 223 | } 224 | 225 | 226 | 227 | [ConsoleCommand("extend", "Extend map time")] 228 | [ConsoleCommand("wst_extend", "Extend map time")] 229 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 230 | public void OnExtendMap(CCSPlayerController player, CommandInfo info) 231 | { 232 | if (player == null) 233 | { 234 | return; 235 | } 236 | 237 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 238 | var playerInfo = entity.GetComponent(); 239 | if (!playerInfo.PlayerStats.IsVip) 240 | { 241 | player.PrintToChat(NoVipMessage); 242 | return; 243 | } 244 | 245 | Server.ExecuteCommand("c_cs2f_extend 15"); 246 | Server.ExecuteCommand("c_extend 15"); 247 | } 248 | 249 | [ConsoleCommand("replay", "Replay")] 250 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 251 | public void OnReplayCommand(CCSPlayerController player, CommandInfo info) 252 | { 253 | if (player == null) 254 | { 255 | return; 256 | } 257 | 258 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 259 | var playerInfo = entity.GetComponent(); 260 | 261 | var position = 1; 262 | 263 | var target = info.GetArg(1); 264 | Console.WriteLine(target); 265 | if (target != null) 266 | { 267 | if (target.StartsWith("@")) 268 | { 269 | var removeAt = target.Replace("@", ""); 270 | if (!int.TryParse(removeAt, out position)) 271 | { 272 | player.PrintToChat($" {CC.White}Invalid position"); 273 | return; 274 | } 275 | } 276 | 277 | } 278 | 279 | if (position != 1) 280 | { 281 | if (!playerInfo.PlayerStats.IsVip) 282 | { 283 | player.PrintToChat(NoVipMessage); 284 | return; 285 | } 286 | } 287 | 288 | _eventManager.Publish(new LoadReplayEvent { MapName = Server.MapName, PlayerSlot = player.Slot, Position = position }); 289 | } 290 | 291 | [ConsoleCommand("prreplay", "Your PR")] 292 | [CommandHelper(whoCanExecute: CommandUsage.CLIENT_ONLY)] 293 | public void OnPrReplayCommand(CCSPlayerController player, CommandInfo info) 294 | { 295 | if (player == null) 296 | { 297 | return; 298 | } 299 | 300 | var entity = _entityManager.FindEntity(player.Slot.ToString()); 301 | var playerInfo = entity.GetComponent(); 302 | if (!playerInfo.PlayerStats.IsVip) 303 | { 304 | player.PrintToChat(NoVipMessage); 305 | return; 306 | } 307 | 308 | _eventManager.Publish(new LoadReplayEvent { MapName = Server.MapName, PlayerSlot = player.Slot, Position = -1 }); 309 | } 310 | } -------------------------------------------------------------------------------- /Components/MapZonesComponent.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using CounterStrikeSharp.API.Modules.Utils; 4 | 5 | namespace WST; 6 | 7 | public class MapZone 8 | { 9 | public List Routes { get; set; } 10 | 11 | [JsonIgnore] 12 | public Route Main => Routes.First(x => x.Key == "main"); 13 | 14 | public Route? GetRoute(string key) 15 | { 16 | return Routes.FirstOrDefault(x => x.Key == key); 17 | } 18 | 19 | public List GetStageRoutes() 20 | { 21 | return Routes.Where(x => x.Key.StartsWith("s")).ToList(); 22 | } 23 | 24 | public List GetRouteKeys() 25 | { 26 | var keys = Routes.Select(x => x.Key).ToList(); 27 | return keys; 28 | } 29 | 30 | [JsonIgnore] 31 | public Dictionary> WorldRecords { get; set; } = new(); 32 | 33 | public async Task LoadRecords(Database db, string currentMap) 34 | { 35 | var records = (await db.QueryAsync("SELECT * FROM v_ranked_records WHERE mapname = @mapname and position = 1", 36 | new { mapname = currentMap })).ToList(); 37 | foreach (var record in records) 38 | { 39 | WorldRecords[record.Route] = WorldRecords.GetValueOrDefault(record.Route, new Dictionary()); 40 | WorldRecords[record.Route][record.Style] = record; 41 | } 42 | } 43 | 44 | 45 | 46 | public async Task Save(Database database, string mapName) 47 | { 48 | try 49 | { 50 | foreach (var routeData in this.Routes) 51 | { 52 | var routeJson = JsonSerializer.Serialize(routeData, 53 | new JsonSerializerOptions 54 | { WriteIndented = true, DictionaryKeyPolicy = JsonNamingPolicy.CamelCase }); 55 | 56 | await database.ExecuteAsync(@" 57 | INSERT INTO maps_2 (name, route, route_data) 58 | VALUES (@name, @route, CAST(@route_data as jsonb)) 59 | ON CONFLICT (name, route) 60 | DO UPDATE SET 61 | route_data = EXCLUDED.route_data", 62 | new { name = mapName, route = routeData.Key, route_data = routeJson }); 63 | 64 | Console.WriteLine($"Saved route {routeData.Key}"); 65 | 66 | } 67 | } 68 | catch (Exception e) 69 | { 70 | Console.WriteLine(e); 71 | } 72 | 73 | } 74 | 75 | public V_RankedRecord? GetWorldRecord(string route, string style) 76 | { 77 | if (WorldRecords.ContainsKey(route)) 78 | { 79 | if (WorldRecords[route].ContainsKey(style)) 80 | { 81 | return WorldRecords[route][style]; 82 | } 83 | } 84 | 85 | return null; 86 | } 87 | 88 | } 89 | 90 | public enum RouteType 91 | { 92 | stage, 93 | linear 94 | } 95 | 96 | public class Route 97 | { 98 | public string Key { get; set; } 99 | 100 | 101 | public string Name { get; set; } 102 | 103 | 104 | [JsonConverter(typeof(JsonStringEnumConverter))] 105 | public RouteType Type { get; set; } 106 | 107 | public ZoneVector StartPos { get; set; } 108 | 109 | public ZoneVector StartVelocity { get; set; } 110 | public ZoneVector StartAngles { get; set; } 111 | 112 | public float VelocityCapStartXY { get; set; } = 350; 113 | public Zone Start { get; set; } 114 | 115 | public Zone End { get; set; } 116 | 117 | public int Tier { get; set; } = 1; 118 | public List Checkpoints { get; set; } 119 | 120 | } 121 | 122 | public enum ZoneType 123 | { 124 | trigger, 125 | vector 126 | } 127 | 128 | public class ZoneVector 129 | { 130 | public ZoneVector() 131 | { 132 | 133 | } 134 | public ZoneVector(float x, float y, float z) 135 | { 136 | this.x = x; 137 | this.y = y; 138 | this.z = z; 139 | } 140 | 141 | public float x { get; set; } 142 | public float y { get; set; } 143 | public float z { get; set; } 144 | 145 | public Vector ToNativeVector() 146 | { 147 | return new Vector(x, y, z); 148 | } 149 | 150 | public QAngle ToNativeQAngle() 151 | { 152 | return new QAngle(x, y, z); 153 | } 154 | 155 | public string ToVectorString() 156 | { 157 | return $"{x} {y} {z}"; 158 | } 159 | } 160 | 161 | public class Zone 162 | { 163 | [JsonConverter(typeof(JsonStringEnumConverter))] 164 | public ZoneType Type { get; set; } 165 | 166 | public string? TargetName { get; set; } 167 | 168 | public ZoneVector v1 { get; set; } 169 | 170 | public ZoneVector v2 { get; set; } 171 | 172 | [JsonIgnore] 173 | public Wst.TriggerInfo TriggerInfo { get; set; } 174 | 175 | } -------------------------------------------------------------------------------- /Components/PlayerComponent.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using CounterStrikeSharp.API; 4 | using CounterStrikeSharp.API.Core; 5 | using CounterStrikeSharp.API.Modules.Utils; 6 | using WST.Game; 7 | 8 | namespace WST; 9 | 10 | public class SavedLocations 11 | { 12 | public ZoneVector SaveLocPosition { get; set; } 13 | public ZoneVector SaveLocAngles { get; set; } 14 | public ZoneVector SaveLocVelocity { get; set; } 15 | public string Style { get; set; } 16 | public string RouteKey { get; set; } 17 | public PlayerTimerComponent Timer { get; set; } = null; 18 | } 19 | 20 | public class PlayerComponent 21 | { 22 | public PlayerComponent(int slot, string steamId, string name, string style) 23 | { 24 | this.Slot = slot; 25 | this.SteamId = steamId; 26 | this.Name = name; 27 | this.Style = style; 28 | } 29 | public bool Teleporting { get; set; } 30 | public int Slot { get; set; } 31 | public string SteamId { get; set; } 32 | 33 | public string Name { get; set; } 34 | public string RouteKey { get; private set; } = "main"; 35 | 36 | public string Style { get; set; } = "normal"; 37 | public string? SecondaryRouteKey { get; set; } = null; 38 | 39 | public bool RepeatMode { get; set; } = false; 40 | 41 | public ZoneVector? CustomStartPos { get; set; } 42 | public ZoneVector? CustomStartAng { get; set; } 43 | 44 | public void ChangeRoute(string routeKey) 45 | { 46 | RouteKey = routeKey; 47 | Style = "normal"; 48 | SecondaryRouteKey = null; 49 | CustomStartPos = null; 50 | CustomStartAng = null; 51 | } 52 | 53 | public void ChangeRoute(string routeKey, string style) 54 | { 55 | RouteKey = routeKey; 56 | Style = style; 57 | SecondaryRouteKey = null; 58 | CustomStartPos = null; 59 | CustomStartAng = null; 60 | } 61 | 62 | public void ChangeSecondaryRoute(string routeKey) 63 | { 64 | SecondaryRouteKey = routeKey; 65 | } 66 | 67 | public Dictionary> PlayerRecords { get; set; } = new(); 68 | 69 | public List SavedLocations { get; set; } = new(); 70 | public int CurrentSavelocIndex { get; set; } = 0; 71 | public V_Player PlayerStats { get; set; } 72 | 73 | public bool InPrac { get; set; } = false; 74 | 75 | public bool ShowHud { get; set; } = true; 76 | 77 | public bool ShowKeys { get; set; } = false; 78 | 79 | public bool ShowCustomHud { get; set; } 80 | 81 | public V_RankedRecord? PR 82 | { 83 | get 84 | { 85 | if (PlayerRecords.ContainsKey(RouteKey)) 86 | { 87 | if (PlayerRecords[RouteKey].ContainsKey(Style)) 88 | { 89 | return PlayerRecords[RouteKey][Style]; 90 | } 91 | } 92 | 93 | return null; 94 | } 95 | } 96 | 97 | public int StylePoints 98 | { 99 | get 100 | { 101 | if (PlayerStats == null) 102 | { 103 | return 0; 104 | } 105 | 106 | if (Style == "normal") 107 | { 108 | return PlayerStats.PPoints; 109 | } 110 | else if (Style == "lg") 111 | { 112 | return PlayerStats.LgPoints; 113 | } 114 | else if (Style == "tm") 115 | { 116 | return PlayerStats.TmPoints; 117 | } 118 | else if (Style == "sw") 119 | { 120 | return PlayerStats.SwPoints; 121 | } 122 | else if (Style == "hsw") 123 | { 124 | return PlayerStats.HswPoints; 125 | } 126 | else if (Style == "prac") 127 | { 128 | return PlayerStats.PracPoints; 129 | } 130 | else 131 | { 132 | return 0; 133 | } 134 | } 135 | } 136 | 137 | public int StyleRank 138 | { 139 | get 140 | { 141 | if (PlayerStats == null) 142 | { 143 | return 0; 144 | } 145 | 146 | Console.WriteLine(Style); 147 | 148 | if (Style == "normal") 149 | { 150 | return PlayerStats.Rank; 151 | } 152 | else if (Style == "lg") 153 | { 154 | return PlayerStats.LgRank; 155 | } 156 | else if (Style == "tm") 157 | { 158 | return PlayerStats.TmRank; 159 | } 160 | else if (Style == "sw") 161 | { 162 | return PlayerStats.SwRank; 163 | } 164 | else if (Style == "hsw") 165 | { 166 | return PlayerStats.HswRank; 167 | } 168 | else if (Style == "prac") 169 | { 170 | return PlayerStats.PracRank; 171 | } 172 | else 173 | { 174 | return 0; 175 | } 176 | } 177 | } 178 | 179 | public void InMemoryUpdatePR(V_RankedRecord pr, string route, string style) 180 | { 181 | PlayerRecords[route] = PlayerRecords.GetValueOrDefault(route, new Dictionary()); 182 | PlayerRecords[route][style] = pr; 183 | } 184 | 185 | public string ChatRank() 186 | { 187 | if (PlayerStats != null && PlayerStats.CustomTag != null) 188 | { 189 | var x = Utils.RemoveColorNames(PlayerStats.CustomTag); 190 | return x; 191 | } 192 | return $"{SkillGroup.Name}"; 193 | } 194 | 195 | public string ChatRankFormatted() 196 | { 197 | if (PlayerStats != null && PlayerStats.CustomTag != null) 198 | { 199 | return $"{ChatColors.White}[{Utils.ColorNamesToTags(PlayerStats.CustomTag)}{ChatColors.White}]"; 200 | } 201 | 202 | if (Style == "normal") 203 | { 204 | return $"{ChatColors.White}[{SkillGroup.ChatColor}{SkillGroup.Name}{ChatColors.White}]"; 205 | } 206 | else 207 | { 208 | return $"{ChatColors.White}[{SkillGroup.ChatColor}{Style.ToUpper()}-{SkillGroup.Name}{ChatColors.White}{ChatColors.White}]"; 209 | 210 | } 211 | } 212 | 213 | public SkillGroup SkillGroup 214 | { 215 | get 216 | { 217 | return SkillGroups.GetSkillGroup(this.StyleRank, this.StylePoints); 218 | } 219 | } 220 | 221 | public async Task LoadStats(Database db) 222 | { 223 | Console.WriteLine(SteamId); 224 | var playerStats = 225 | (await db.QueryAsync("SELECT * FROM v_players WHERE steam_id = @steamid", 226 | new { steamid = SteamId })).FirstOrDefault(); 227 | if (playerStats == null) 228 | { 229 | Console.WriteLine("Player record is null"); 230 | return; 231 | } 232 | 233 | Console.WriteLine(JsonSerializer.Serialize(playerStats)); 234 | 235 | PlayerStats = playerStats; 236 | } 237 | 238 | public async Task LoadRecords(Database db, string currentMap) 239 | { 240 | var records = (await db.QueryAsync("SELECT * FROM v_ranked_records WHERE steam_id = @steamid and mapname = @mapname", 241 | new { steamid = SteamId, mapname = currentMap })).ToList(); 242 | foreach (var record in records) 243 | { 244 | PlayerRecords[record.Route] = PlayerRecords.GetValueOrDefault(record.Route, new Dictionary()); 245 | PlayerRecords[record.Route][record.Style] = record; 246 | } 247 | } 248 | 249 | public CCSPlayerController GetPlayer() 250 | { 251 | var player = Utilities.GetPlayerFromSlot(Slot); 252 | if (player == null) throw new Exception($"Player with slot {Slot} not found"); 253 | return player; 254 | } 255 | 256 | } -------------------------------------------------------------------------------- /Components/PlayerReplayComponent.cs: -------------------------------------------------------------------------------- 1 | namespace WST; 2 | 3 | public class PlayerReplayComponent 4 | { 5 | public PlayerReplayComponent(Replay replay, int tick, string route, string playerName, string style, string? customHudUrl) 6 | { 7 | Replay = replay; 8 | ReplayPlayback = new ReplayPlayback(replay); 9 | Tick = tick; 10 | Route = route; 11 | PlayerName = playerName; 12 | TotalTicks = replay.Frames.Count; 13 | CustomHudUrl = customHudUrl; 14 | Style = style; 15 | } 16 | 17 | // replay from DB 18 | public Replay? Replay { get; } 19 | 20 | // replay playback 21 | public ReplayPlayback ReplayPlayback { get; } 22 | 23 | public int Tick { get; set; } = 0; 24 | 25 | public string Route { get; set; } = ""; 26 | 27 | public string Style { get; set; } = ""; 28 | public string PlayerName { get; set; } = ""; 29 | 30 | public int TotalTicks { get; set; } = 0; 31 | 32 | public string? CustomHudUrl { get; set; } 33 | } -------------------------------------------------------------------------------- /Components/PlayerTimerComponent.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Utils; 2 | 3 | namespace WST; 4 | 5 | public class CheckpointTimerComponent 6 | { 7 | public int TimerTicks { get; set; } 8 | public float VelocityStartXY { get; set; } 9 | public float VelocityStartZ { get; set; } 10 | public float VelocityStartXYZ { get; set; } 11 | 12 | public CheckpointTimerComponent DeepCopy() 13 | { 14 | return new CheckpointTimerComponent 15 | { 16 | TimerTicks = this.TimerTicks, 17 | VelocityStartXY = this.VelocityStartXY, 18 | VelocityStartZ = this.VelocityStartZ, 19 | VelocityStartXYZ = this.VelocityStartXYZ 20 | }; 21 | } 22 | 23 | } 24 | public class TimerData 25 | { 26 | public int TimerTicks { get; set; } = 0; 27 | public float VelocityStartXY { get; set; } 28 | public float VelocityStartZ { get; set; } 29 | public float VelocityStartXYZ { get; set; } 30 | public float VelocityEndXY { get; set; } 31 | public float VelocityEndZ { get; set; } 32 | public float VelocityEndXYZ { get; set; } 33 | public List Checkpoints { get; set; } = new(); 34 | public Replay Recording { get; set; } = new(); 35 | public string RouteKey { get; set; } 36 | 37 | public TimerData DeepCopy() 38 | { 39 | var copy = new TimerData 40 | { 41 | TimerTicks = this.TimerTicks, 42 | VelocityStartXY = this.VelocityStartXY, 43 | VelocityStartZ = this.VelocityStartZ, 44 | VelocityStartXYZ = this.VelocityStartXYZ, 45 | VelocityEndXY = this.VelocityEndXY, 46 | VelocityEndZ = this.VelocityEndZ, 47 | VelocityEndXYZ = this.VelocityEndXYZ, 48 | Checkpoints = this.Checkpoints.Select(cp => cp.DeepCopy()).ToList(), 49 | RouteKey = this.RouteKey, 50 | Recording = this.Recording.DeepCopy() 51 | }; 52 | 53 | return copy; 54 | } 55 | } 56 | 57 | public class PlayerTimerComponent 58 | { 59 | public TimerData Primary { get; set; } = new(); 60 | 61 | public TimerData? Secondary { get; set; } = null; 62 | 63 | // turbo master shit 64 | public bool TmInSlowMo { get; set; } 65 | public float TmXYSpeedBeforeSlowMo { get; set; } 66 | public float TmZSpeedBeforeSlowMo { get; set; } 67 | 68 | public PlayerTimerComponent DeepCopy() 69 | { 70 | return new PlayerTimerComponent 71 | { 72 | Primary = this.Primary.DeepCopy(), 73 | Secondary = this.Secondary?.DeepCopy(), 74 | TmInSlowMo = this.TmInSlowMo, 75 | TmXYSpeedBeforeSlowMo = this.TmXYSpeedBeforeSlowMo, 76 | TmZSpeedBeforeSlowMo = this.TmZSpeedBeforeSlowMo 77 | }; 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /Game/SchemaString.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text; 3 | using CounterStrikeSharp.API; 4 | using CounterStrikeSharp.API.Modules.Memory; 5 | 6 | namespace WST.Game; 7 | 8 | public class SchemaString : NativeObject 9 | where SchemaClass : NativeObject 10 | { 11 | public SchemaString(SchemaClass instance, string member) 12 | : base(Schema.GetSchemaValue(instance.Handle, typeof(SchemaClass).Name!, member)) 13 | { } 14 | 15 | public unsafe void Set(string str) 16 | { 17 | byte[] bytes = GetStringBytes(str); 18 | 19 | for (int i = 0; i < bytes.Length; i++) 20 | { 21 | Unsafe.Write((void*)(Handle.ToInt64() + i), bytes[i]); 22 | } 23 | 24 | Unsafe.Write((void*)(Handle.ToInt64() + bytes.Length), 0); 25 | } 26 | 27 | private byte[] GetStringBytes(string str) 28 | { 29 | return Encoding.ASCII.GetBytes(str); 30 | } 31 | } -------------------------------------------------------------------------------- /Infrastructure/Database.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | using Npgsql; 3 | 4 | namespace WST; 5 | 6 | public class Database 7 | { 8 | private readonly string _connectionString; 9 | 10 | public Database() 11 | { 12 | DefaultTypeMap.MatchNamesWithUnderscores = true; 13 | // _connectionString = "Host=localhost;Username=wst;Password=password;Database=wst"; 14 | _connectionString = Environment.GetEnvironmentVariable("DATABASE_URL")!; 15 | } 16 | 17 | // Synchronous query method 18 | public IEnumerable Query(string sql, object? param = null) 19 | { 20 | using (var connection = new NpgsqlConnection(_connectionString)) 21 | { 22 | return connection.Query(sql, param); 23 | } 24 | } 25 | 26 | // Asynchronous query method 27 | public async Task> QueryAsync(string sql, object? param = null) 28 | { 29 | using (var connection = new NpgsqlConnection(_connectionString)) 30 | { 31 | return await connection.QueryAsync(sql, param); 32 | } 33 | } 34 | 35 | // Execute a command synchronously (INSERT, UPDATE, DELETE) 36 | public int Execute(string sql, object? param = null) 37 | { 38 | using (var connection = new NpgsqlConnection(_connectionString)) 39 | { 40 | return connection.Execute(sql, param); 41 | } 42 | } 43 | 44 | // Execute a command asynchronously (INSERT, UPDATE, DELETE) 45 | public async Task ExecuteAsync(string sql, object? param = null) 46 | { 47 | using (var connection = new NpgsqlConnection(_connectionString)) 48 | { 49 | return await connection.ExecuteAsync(sql, param); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /Infrastructure/Entity.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | using CounterStrikeSharp.API.Core; 3 | using CounterStrikeSharp.API.Modules.Utils; 4 | 5 | namespace WST; 6 | 7 | public class SurfEntity : Entity 8 | { 9 | public CCSPlayerController GetPlayer() 10 | { 11 | var player = Utilities.GetPlayerFromSlot(Int32.Parse(this.Id)); 12 | if (player == null) throw new Exception($"Player with slot {this.Id} not found"); 13 | return player; 14 | } 15 | } 16 | 17 | public class SurfPlayer : SurfEntity 18 | { 19 | } 20 | 21 | public class SurfBot : SurfEntity 22 | { 23 | } 24 | 25 | public class Map : Entity 26 | { 27 | } 28 | 29 | public class ReplayEntity : Entity 30 | { 31 | } 32 | 33 | public class Entity 34 | { 35 | public readonly Dictionary Components = new(); 36 | public string Id; 37 | 38 | public void AddComponent(T? component) 39 | { 40 | Components[typeof(T)] = component; 41 | } 42 | 43 | public T? GetComponent() 44 | { 45 | if (!HasComponent()) return default; 46 | return (T)Components[typeof(T)]!; 47 | } 48 | 49 | public bool HasComponent() 50 | { 51 | return Components.ContainsKey(typeof(T)); 52 | } 53 | 54 | public void RemoveComponent() 55 | { 56 | Components.Remove(typeof(T)); 57 | } 58 | } -------------------------------------------------------------------------------- /Infrastructure/EntityManager.cs: -------------------------------------------------------------------------------- 1 | namespace WST; 2 | 3 | public class EntityManager 4 | { 5 | private readonly List _entities = new(); 6 | private readonly List _systems = new(); 7 | 8 | public void AddEntity(Entity entity) 9 | { 10 | var existingEntity = _entities.Find(e => e.Id == entity.Id); 11 | if (existingEntity != null) 12 | { 13 | Console.WriteLine($"Entity with id {entity.Id} already exists, replacing"); 14 | _entities.Remove(existingEntity); 15 | } 16 | 17 | _entities.Add(entity); 18 | Console.WriteLine("Added entity id: " + entity.Id); 19 | PrintEntityIdsOnOneLine(); 20 | } 21 | 22 | // For Singleton entities like the map 23 | public T FindEntity() where T : Entity 24 | { 25 | var entity = _entities.Find(s => s is T); 26 | return (T)entity; 27 | } 28 | 29 | public T FindEntity(string id) where T : Entity 30 | { 31 | var e = _entities.Find(entity => entity is T && entity.Id == id); 32 | return (T)e; 33 | } 34 | 35 | public T FindEntityOrThrow(string id) where T : Entity 36 | { 37 | var e = FindEntity(id); 38 | if (e == null) throw new Exception($"Entity of type {typeof(T).Name} with id {id} not found"); 39 | return e; 40 | } 41 | 42 | public List Entities() where T : Entity 43 | { 44 | return _entities.OfType().ToList(); 45 | } 46 | 47 | 48 | public void RemoveEntity() where T : Entity 49 | { 50 | _entities.RemoveAll(entity => entity is T); 51 | Console.WriteLine("Removed all entities of type " + typeof(T).Name); 52 | PrintEntityIdsOnOneLine(); 53 | } 54 | 55 | public void RemoveEntity(string id) where T : Entity 56 | { 57 | 58 | _entities.RemoveAll(entity => entity is T && entity.Id == id); 59 | Console.WriteLine("Removed entity id: " + id); 60 | PrintEntityIdsOnOneLine(); 61 | } 62 | 63 | public void AddSystem(System system) 64 | { 65 | _systems.Add(system); 66 | } 67 | 68 | private void PrintEntityIdsOnOneLine() 69 | { 70 | Console.WriteLine("Entities: " + string.Join(", ", _entities.Select(e => e.Id))); 71 | } 72 | 73 | public List DebugEntities() 74 | { 75 | var result = new List(); 76 | Console.WriteLine("Entities:"); 77 | foreach (var entity in _entities) 78 | { 79 | var entityName = entity.GetType().Name; 80 | var components = string.Join(", ", entity.Components.Select(c => c.Key.Name)); 81 | Console.WriteLine($" {entityName} ({entity.Id}) [{components}]"); 82 | 83 | result.Add($"{entityName} ({entity.Id}) [{components}]"); 84 | 85 | } 86 | 87 | return result; 88 | } 89 | } -------------------------------------------------------------------------------- /Infrastructure/EventManager.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace WST; 4 | 5 | public class EventManager 6 | { 7 | private readonly Dictionary>> _asyncSubscribers = new(); 8 | private readonly Dictionary>> _syncSubscribers = new(); 9 | 10 | // Subscribe to async events 11 | public void SubscribeAsync(Func handler) where T : class 12 | { 13 | var type = typeof(T); 14 | if (!_asyncSubscribers.ContainsKey(type)) _asyncSubscribers[type] = new List>(); 15 | _asyncSubscribers[type].Add(async e => await handler(e as T)); 16 | } 17 | 18 | // Subscribe to sync events 19 | public void Subscribe(Action handler) where T : class 20 | { 21 | var type = typeof(T); 22 | if (!_syncSubscribers.ContainsKey(type)) _syncSubscribers[type] = new List>(); 23 | _syncSubscribers[type].Add(e => handler(e as T)); 24 | } 25 | 26 | public void Publish(T eventToPublish) where T : class 27 | { 28 | try 29 | { 30 | var type = typeof(T); 31 | if (_syncSubscribers.ContainsKey(type)) 32 | { 33 | var handlers = _syncSubscribers[type]; 34 | foreach (var handler in handlers) handler(eventToPublish); 35 | } 36 | 37 | if (_asyncSubscribers.ContainsKey(type)) 38 | { 39 | var handlers = _asyncSubscribers[type]; 40 | foreach (var handler in handlers) handler(eventToPublish); 41 | } 42 | 43 | if (type != typeof(OnTickEvent)) 44 | { 45 | Console.WriteLine($"Event: {type.Name}"); 46 | Console.WriteLine(JsonSerializer.Serialize(eventToPublish, new JsonSerializerOptions 47 | { 48 | WriteIndented = true 49 | })); 50 | } 51 | } 52 | catch (Exception e) 53 | { 54 | Console.WriteLine("WST_EVENT_ERROR:"); 55 | Console.WriteLine(e); 56 | var type = typeof(T); 57 | Console.WriteLine($"Event: {type.Name}"); 58 | Console.WriteLine(JsonSerializer.Serialize(eventToPublish, new JsonSerializerOptions 59 | { 60 | WriteIndented = true 61 | })); 62 | } 63 | 64 | } 65 | } -------------------------------------------------------------------------------- /Infrastructure/System.cs: -------------------------------------------------------------------------------- 1 | namespace WST; 2 | 3 | public abstract class System 4 | { 5 | protected System(EventManager eventManager, EntityManager entityManager, Database database) 6 | { 7 | EventManager = eventManager; 8 | EntityManager = entityManager; 9 | Database = database; 10 | } 11 | 12 | protected EventManager EventManager { get; } 13 | protected EntityManager EntityManager { get; } 14 | protected Database Database { get; } 15 | } -------------------------------------------------------------------------------- /Models/Checkpoint.cs: -------------------------------------------------------------------------------- 1 | namespace WST; 2 | 3 | public class Checkpoint 4 | { 5 | public int TimerTicks { get; set; } 6 | public float VelocityStartXY { get; set; } 7 | public float VelocityStartZ { get; set; } 8 | public float VelocityStartXYZ { get; set; } 9 | } -------------------------------------------------------------------------------- /Models/GlobalCfg.cs: -------------------------------------------------------------------------------- 1 | namespace WST; 2 | 3 | public class GlobalCfg 4 | { 5 | public int Id { get; set; } 6 | public string Command { get; set; } 7 | } -------------------------------------------------------------------------------- /Models/MapCfg.cs: -------------------------------------------------------------------------------- 1 | namespace WST; 2 | 3 | public class MapCfg 4 | { 5 | public int Id { get; set; } 6 | public string MapName { get; set; } 7 | public string Command { get; set; } 8 | } -------------------------------------------------------------------------------- /Models/RecentRecords.cs: -------------------------------------------------------------------------------- 1 | namespace WST; 2 | 3 | public class RecentRecord 4 | { 5 | public int Id { get; set; } 6 | public string SteamId { get; set; } 7 | public string MapName { get; set; } 8 | public string Route { get; set; } 9 | public int Ticks { get; set; } 10 | public string Name { get; set; } 11 | public string NewName { get; set; } 12 | public int NewTicks { get; set; } 13 | public string OldName { get; set; } 14 | public int? OldTicks { get; set; } // Nullable as the column is nullable 15 | public DateTime CreatedAt { get; set; } 16 | public string OldSteamId { get; set; } // Nullable as the column is nullable 17 | } -------------------------------------------------------------------------------- /Models/Record.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace WST; 4 | 5 | public class Record 6 | { 7 | public int Id { get; set; } // Maps to the 'id' column 8 | public string SteamId { get; set; } // Maps to the 'steam_id' column 9 | public string MapName { get; set; } // Maps to the 'mapname' column 10 | public string Route { get; set; } 11 | public int Ticks { get; set; } // Maps to the 'time' column 12 | public string Name { get; set; } // Maps to the 'name' column 13 | 14 | public float VelocityStartXY { get; set; } 15 | public float VelocityStartZ { get; set; } 16 | public float VelocityStartXYZ { get; set; } 17 | public float VelocityEndXY { get; set; } 18 | public float VelocityEndZ { get; set; } 19 | public float VelocityEndXYZ { get; set; } 20 | 21 | public string Checkpoints { get; set; } 22 | 23 | public List CheckpointsObject 24 | { 25 | get => JsonSerializer.Deserialize>(Checkpoints); 26 | } 27 | } -------------------------------------------------------------------------------- /Models/Replay.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using CounterStrikeSharp.API.Core; 4 | using CounterStrikeSharp.API.Modules.Utils; 5 | 6 | namespace WST; 7 | 8 | public class VectorConverter : JsonConverter 9 | { 10 | public override Vector Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | var obj = JsonSerializer.Deserialize>(ref reader, options); 13 | return new Vector(obj["x"], obj["y"], obj["z"]); 14 | } 15 | 16 | public override void Write(Utf8JsonWriter writer, Vector value, JsonSerializerOptions options) 17 | { 18 | writer.WriteStartObject(); 19 | writer.WriteNumber("x", value.X); 20 | writer.WriteNumber("y", value.Y); 21 | writer.WriteNumber("z", value.Z); 22 | writer.WriteEndObject(); 23 | } 24 | } 25 | 26 | public class QAngleConverter : JsonConverter 27 | { 28 | public override QAngle Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 29 | { 30 | var obj = JsonSerializer.Deserialize>(ref reader, options); 31 | return new QAngle(obj["x"], obj["y"], obj["z"]); 32 | } 33 | 34 | public override void Write(Utf8JsonWriter writer, QAngle value, JsonSerializerOptions options) 35 | { 36 | writer.WriteStartObject(); 37 | writer.WriteNumber("x", value.X); 38 | writer.WriteNumber("y", value.Y); 39 | writer.WriteNumber("z", value.Z); 40 | writer.WriteEndObject(); 41 | } 42 | } 43 | 44 | public class ReplayPlayback 45 | { 46 | public ReplayPlayback(Replay replay) 47 | { 48 | // convert to native 49 | foreach (var frame in replay.Frames) 50 | { 51 | var frameNative = new FrameTNative 52 | { 53 | Pos = frame.Pos.ToNativeVector(), 54 | Ang = frame.Ang.ToNativeQAngle(), 55 | Buttons = frame.Buttons, 56 | Flags = frame.Flags, 57 | MoveType = frame.MoveType 58 | }; 59 | Frames.Add(frameNative); 60 | } 61 | } 62 | 63 | public class FrameTNative 64 | { 65 | public Vector Pos { get; set; } 66 | public QAngle Ang { get; set; } 67 | public ulong Buttons { get; set; } 68 | public uint Flags { get; set; } 69 | public MoveType_t MoveType { get; set; } 70 | } 71 | 72 | public List Frames { get; set; } = new List(); 73 | } 74 | 75 | public class Replay 76 | { 77 | public List Frames { get; set; } = new List(); 78 | 79 | public byte[] Serialize() 80 | { 81 | var options = new JsonSerializerOptions 82 | { 83 | Converters = { new VectorConverter(), new QAngleConverter() } 84 | }; 85 | return JsonSerializer.SerializeToUtf8Bytes(this, options); 86 | } 87 | 88 | public string SerializeString() 89 | { 90 | var options = new JsonSerializerOptions 91 | { 92 | Converters = { new VectorConverter(), new QAngleConverter() } 93 | }; 94 | return JsonSerializer.Serialize(this, options); 95 | } 96 | 97 | public static Replay Deserialize(byte[] data) 98 | { 99 | var options = new JsonSerializerOptions 100 | { 101 | Converters = { new VectorConverter(), new QAngleConverter() } 102 | }; 103 | return JsonSerializer.Deserialize(data, options); 104 | } 105 | 106 | public static Replay DeserializeString(string data) 107 | { 108 | var options = new JsonSerializerOptions 109 | { 110 | Converters = { new VectorConverter(), new QAngleConverter() } 111 | }; 112 | return JsonSerializer.Deserialize(data, options); 113 | } 114 | 115 | public class FrameT 116 | { 117 | public ZoneVector Pos { get; set; } 118 | public ZoneVector Ang { get; set; } 119 | public ulong Buttons { get; set; } 120 | public uint Flags { get; set; } 121 | public MoveType_t MoveType { get; set; } 122 | } 123 | 124 | public Replay DeepCopy() 125 | { 126 | var copy = new Replay 127 | { 128 | Frames = this.Frames.Select(f => new FrameT 129 | { 130 | Pos = new ZoneVector(f.Pos.x, f.Pos.y, f.Pos.z), 131 | Ang = new ZoneVector(f.Ang.x, f.Ang.y, f.Ang.z), 132 | Buttons = f.Buttons, 133 | Flags = f.Flags, 134 | MoveType = f.MoveType 135 | }).ToList() 136 | }; 137 | 138 | return copy; 139 | } 140 | } 141 | 142 | -------------------------------------------------------------------------------- /Models/Server.cs: -------------------------------------------------------------------------------- 1 | namespace WST; 2 | 3 | public class GameServer 4 | { 5 | public string ServerId { get; set; } 6 | public string WorkshopCollection { get; set; } 7 | public string Hostname { get; set; } 8 | public bool IsPublic { get; set; } 9 | public string IP { get; set; } 10 | 11 | public string? CurrentMap { get; set; } 12 | 13 | public int PlayerCount { get; set; } 14 | 15 | public int TotalPlayers { get; set; } 16 | 17 | public string ShortName { get; set; } 18 | 19 | public bool Vip { get; set; } 20 | 21 | public string Style { get; set; } 22 | } -------------------------------------------------------------------------------- /Models/SkillGroups.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Utils; 2 | 3 | namespace WST; 4 | 5 | public class SkillGroup 6 | { 7 | public string Name { get; } 8 | public int? MinRank { get; } 9 | public int? MaxRank { get; } 10 | public int? Points { get; } 11 | 12 | public char ChatColor { get; } 13 | 14 | public SkillGroup(string name, int? minRank, int? maxRank, int? points, char chatColor) 15 | { 16 | Name = name; 17 | MinRank = minRank; 18 | MaxRank = maxRank; 19 | Points = points; 20 | ChatColor = chatColor; 21 | } 22 | } 23 | 24 | public static class SkillGroups 25 | { 26 | public static readonly List Groups = new List 27 | { 28 | new SkillGroup("King", 1, 1, null, ChatColors.Darkred), 29 | new SkillGroup("Godly", 2, 2, null, ChatColors.Green), 30 | new SkillGroup("Legendary", 3, 3, null, ChatColors.Magenta), 31 | new SkillGroup("Mythical", 4, 4, null, ChatColors.Yellow), 32 | new SkillGroup("Phantom", 5, 5, null, ChatColors.BlueGrey), 33 | new SkillGroup("Master", 6, 10, null, ChatColors.LightRed), 34 | new SkillGroup("Elite", 11, 25, null, ChatColors.Red), 35 | new SkillGroup("Veteran", 26, 50, null, ChatColors.DarkBlue), 36 | new SkillGroup("Pro", 51, 100, null, ChatColors.Blue), 37 | new SkillGroup("Expert", 101, 200, null, ChatColors.Purple), 38 | new SkillGroup("Ace", 201, 400, null, ChatColors.Orange), 39 | new SkillGroup("Exceptional", null, null, 1750, ChatColors.LightPurple), 40 | new SkillGroup("Skilled", null, null, 1500, ChatColors.LightBlue), 41 | new SkillGroup("Advanced", null, null, 1250, ChatColors.LightYellow), 42 | new SkillGroup("Casual", null, null, 1000, ChatColors.Grey), 43 | new SkillGroup("Average", null, null, 750, ChatColors.Olive), 44 | new SkillGroup("Rookie", null, null, 500, ChatColors.Lime), 45 | new SkillGroup("Beginner", null, null, 250, ChatColors.Silver), 46 | 47 | 48 | new SkillGroup("Unranked", null, null, 0, ChatColors.White) 49 | }; 50 | public static SkillGroup GetSkillGroup(int rank, int points) 51 | { 52 | if (points == 0 || rank == 0) 53 | { 54 | return Groups[^1]; 55 | } 56 | 57 | foreach (var group in Groups) 58 | { 59 | if (group.MinRank.HasValue && group.MaxRank.HasValue) 60 | { 61 | if (rank >= group.MinRank && rank <= group.MaxRank) 62 | { 63 | return group; 64 | } 65 | } 66 | else if (group.Points.HasValue && points >= group.Points) 67 | { 68 | return group; 69 | } 70 | } 71 | 72 | return Groups[^1]; 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /Models/V_Player.cs: -------------------------------------------------------------------------------- 1 | namespace WST; 2 | 3 | public class V_Player 4 | { 5 | public Int64 Id { get; set; } 6 | public string SteamId { get; set; } 7 | public string Name { get; set; } 8 | public bool IsAdmin { get; set; } 9 | public int PPoints { get; set; } 10 | public int LgPoints { get; set; } 11 | public int TmPoints { get; set; } 12 | public int SwPoints { get; set; } 13 | public int HswPoints { get; set; } 14 | public int PracPoints { get; set; } 15 | public int Rank { get; set; } 16 | 17 | public int LgRank { get; set; } 18 | 19 | public int TmRank { get; set; } 20 | public int SwRank { get; set; } 21 | public int HswRank { get; set; } 22 | public int PracRank { get; set; } 23 | public int TotalPlayers { get; set; } 24 | 25 | public string? CustomHudUrl { get; set; } 26 | 27 | public bool IsVip { get; set; } 28 | 29 | public string? CustomTag { get; set; } 30 | 31 | public string? ChatColor { get; set; } 32 | 33 | public string? NameColor { get; set; } 34 | } -------------------------------------------------------------------------------- /Models/V_RankedRecord.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace WST; 4 | 5 | public class V_RankedRecord 6 | { 7 | public int Id { get; set; } 8 | public string SteamId { get; set; } 9 | public string MapName { get; set; } 10 | public string Route { get; set; } 11 | public int Ticks { get; set; } 12 | public string Name { get; set; } 13 | public int Position { get; set; } 14 | public int TotalRecords { get; set; } 15 | 16 | public string Style { get; set; } 17 | 18 | public float VelocityStartXY { get; set; } 19 | public float VelocityStartZ { get; set; } 20 | public float VelocityStartXYZ { get; set; } 21 | public float VelocityEndXY { get; set; } 22 | public float VelocityEndZ { get; set; } 23 | public float VelocityEndXYZ { get; set; } 24 | 25 | public string Checkpoints { get; set; } 26 | 27 | public int ScaleFactor { get; set; } 28 | 29 | public int Tier { get; set; } 30 | 31 | public int BasicPoints { get; set; } 32 | public int BonusPoints { get; set; } 33 | 34 | public int Points 35 | { 36 | get => BasicPoints + BonusPoints; 37 | } 38 | 39 | 40 | 41 | public List CheckpointsObject 42 | { 43 | get => JsonSerializer.Deserialize>(Checkpoints); 44 | } 45 | 46 | public string FormatForChat() 47 | { 48 | var playerName = this.Name; 49 | if (playerName.Length > 20) 50 | { 51 | playerName = playerName.Substring(0, 20); 52 | } 53 | var time = Utils.FormatTime(this.Ticks); 54 | var rank = this.Position; 55 | var points = this.Points; 56 | 57 | var rankString = 58 | $"{CC.Secondary}{rank}{CC.White}/{CC.Secondary}{this.TotalRecords}{CC.White}"; 59 | 60 | return 61 | $" {CC.Main}{time}{CC.White} {CC.Secondary}{playerName}{CC.White} (rank {rankString}) {CC.Secondary}{points}{CC.White} points"; 62 | } 63 | } -------------------------------------------------------------------------------- /Models/V_Replay.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | using CounterStrikeSharp.API; 3 | 4 | namespace WST; 5 | 6 | public class V_ReplayWithPosition : V_Replay 7 | { 8 | public int ReplayPositionInRecords { get; set; } 9 | } 10 | 11 | public class V_Replay 12 | { 13 | public int Id { get; set; } 14 | public string SteamId { get; set; } 15 | public string MapName { get; set; } 16 | public string Route { get; set; } 17 | public int Ticks { get; set; } 18 | public byte[]? Replay { get; set; } 19 | public string PlayerName { get; set; } 20 | 21 | public string? CustomHudUrl { get; set; } 22 | 23 | public string? ReplayUrl { get; set; } 24 | 25 | public int Position { get; set; } // position in ranked_records 26 | public int TotalRecords { get; set; } 27 | 28 | public string Style { get; set; } = ""; 29 | 30 | public Task Delete(Database database) 31 | { 32 | return database.ExecuteAsync("DELETE FROM replays WHERE id = @id", new { id = Id }); 33 | } 34 | 35 | public static async Task GetReplay(Database database, string mapName, string route, string steamId, string style) 36 | { 37 | try 38 | { 39 | var result = await database.QueryAsync( 40 | "SELECT * FROM public.v_replays WHERE mapname = @mapname AND route = @route AND steam_id = @steamid and style = @style", 41 | new { mapname = mapName, route = route, steamid = steamId, style }); 42 | 43 | var res = result.FirstOrDefault(); 44 | 45 | if (res != null && res.ReplayUrl != null) 46 | { 47 | await JuiceReplayFromS3(res); 48 | } 49 | 50 | return res; 51 | } 52 | catch (Exception e) 53 | { 54 | Console.WriteLine(e); 55 | return null; 56 | } 57 | } 58 | 59 | public static async Task GetRecordReplay(Database database, string mapName, string route, int position, string style) 60 | { 61 | try 62 | { 63 | var result = await database.QueryAsync( 64 | @"WITH DesiredRank AS ( 65 | SELECT 66 | ticks, mapname, route, style 67 | FROM 68 | public.v_ranked_records 69 | WHERE 70 | position = @position 71 | AND mapname = @mapname 72 | AND route = @route 73 | and style = @style 74 | ), 75 | ClosestReplay AS ( 76 | SELECT 77 | r.*, 78 | ABS(r.ticks - DR.ticks) AS ticks_difference 79 | FROM 80 | public.v_replays r 81 | CROSS JOIN DesiredRank DR 82 | WHERE 83 | r.mapname = DR.mapname 84 | AND r.route = DR.route 85 | and r.style = DR.style 86 | ORDER BY 87 | ticks_difference ASC 88 | LIMIT 1 89 | ) 90 | SELECT 91 | CR.* 92 | FROM 93 | ClosestReplay CR;", 94 | new { mapname = mapName, route = route, position = position, style = style }); 95 | 96 | var res = result.FirstOrDefault(); 97 | 98 | if (res != null && res.ReplayUrl != null) 99 | { 100 | await JuiceReplayFromS3(res); 101 | } 102 | 103 | return res; 104 | } 105 | catch (Exception e) 106 | { 107 | Console.WriteLine(e); 108 | return null; 109 | } 110 | } 111 | 112 | private static async Task JuiceReplayFromS3(V_Replay res) 113 | { 114 | var supabase = Wst.Instance._supabaseClient; 115 | var replayBytes = await supabase.Storage.From("replays").Download(res.ReplayUrl.Replace("replays/", ""), null); 116 | if (replayBytes == null) 117 | { 118 | throw new Exception("Replay not found"); 119 | } 120 | 121 | using var compressedStream = new MemoryStream(replayBytes); 122 | using var decompressedStream = new MemoryStream(); 123 | using (var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress)) 124 | { 125 | gzipStream.CopyTo(decompressedStream); 126 | } 127 | 128 | var decompressedBytes = decompressedStream.ToArray(); 129 | 130 | res.Replay = decompressedBytes; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "WST": { 4 | "commandName": "Project" 5 | }, 6 | "Profile 1": { 7 | "commandName": "Executable", 8 | "executablePath": "powershell", 9 | "commandLineArgs": "C:\\dev\\wst\\cs2-surftimer\\cssharp\\local_build.ps1" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **OCE.SURF TIMER** 2 | 3 | I do not expect anyone to run this it lots of things are hardcoded for my server, however it may help the community build other timers. 4 | 5 | You should instead run: https://github.com/DEAFPS/SharpTimer OR https://github.com/CS2Surf/Timer 6 | 7 | Features: 8 | - Map, Stage, Bonus Timer 9 | - Replay system - main map, stage, bonus (!replay) 10 | - Points system (!points) 11 | - WRCP's (!s1) 12 | - Bonus records (!b1) 13 | - Map, stage and bonus tiers (!minfo) 14 | - Multiple map routes (!routes) 15 | - Player ranks - Godly, Legendary, Pro, Expert, Ace etc (!ranks) 16 | - Player top (!surftop) 17 | - Map, Stage and bonus top (!top) 18 | - Spec info/spectator hud 19 | - In game leaderboard (tab) shows your rank 20 | - Checkpoint record !cpr 21 | - Restart a stage !stuck 22 | - Recent records !rr 23 | - Show incomplete maps !incomplete 24 | - Show incomplete bonuses !incomplete b 25 | - Show incomplete stages !incomplete s 26 | - Repeat a stage in stage mode !repeat 27 | - Styles !turbo !sw !hsw !lg 28 | - Unique Turbo surf style: Left click accelerate, right click fall faster 29 | - Saveloc (and saveloc times are saved as TAS style) !prac 30 | - Cross server chat 31 | - Cross server WR annoucements 32 | - Cross server player join messages 33 | - Discord announcements 34 | 35 | VIP features: 36 | - Add a GIF to your hud 37 | - Add a custom colored chat rank (!chattag) 38 | - Change the color of your text (!chatcolor) 39 | - Change the color of your name (!namecolor) 40 | - Set a custom start position (!startpos) 41 | - View replays of top 100 times on any style/map (!replay @10) 42 | - Extend maps with (!extend) 43 | - Change map with (!map) 44 | - View your own replays on any style/map (!prreplay) 45 | - Change maps with (!map VIP server only) 46 | 47 | - ![image](https://github.com/ws-cs2/cs2-surftimer/assets/149922947/5b7f4203-636a-48c3-85e7-7e186c4f17bc) 48 | - ![image](https://github.com/ws-cs2/cs2-surftimer/assets/149922947/31b3962d-5b97-4326-bd79-2de0a8c8362a) 49 | 50 | 51 | ### How to install 52 | Coming soon 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Systems/AdvertisingSystem.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | 3 | namespace WST; 4 | 5 | 6 | public class OnAdvertisingTimerTickEvent {} 7 | public class AdvertisingSystem: System 8 | { 9 | 10 | private static readonly string[] _advertisements = 11 | { 12 | $" {CC.Main}[oce.surf] {CC.White}Finding this map too hard? Use {CC.Secondary}!servers{CC.White} to find an easier map or use {CC.Secondary}!styles{CC.White} to play on a easier style", 13 | $" {CC.Main}[oce.surf] {CC.White}Use {CC.Secondary}!styles {CC.White}to see fun styles (Turbo, Low Gravity, etc)", 14 | $" {CC.Main}[oce.surf] {CC.White}Bugged or cheated time? Report it on our discord {CC.Secondary}https://oce.surf/discord", 15 | $" {CC.Main}[oce.surf] {CC.White}Type {CC.Secondary}!help{CC.White} in chat for SurfTimer commands", 16 | $" {CC.Main}[oce.surf] {CC.White}Want to see how you stack up on the leaderboard? Try visiting our website {CC.Secondary}https://oce.surf", 17 | $" {CC.Main}[oce.surf] {CC.White}Type {CC.Secondary}!routes{CC.White} to see all the map stages and bonuses", 18 | $" {CC.Main}[oce.surf] {CC.White}Do {CC.Secondary}!lg{CC.White} to play the map in low gravity", 19 | $" {CC.Main}[oce.surf] {CC.White}Join our discord {CC.Secondary}https://oce.surf/discord", 20 | $" {CC.Main}[oce.surf] {CC.White}To see the top players by points type {CC.Secondary}!surftop{CC.White} in chat", 21 | $" {CC.Main}[oce.surf] {CC.White}To respawn type {CC.Secondary}!r{CC.White} in chat", 22 | $" {CC.Main}[oce.surf] {CC.White}Do {CC.Secondary}!turbo{CC.White} to play the map in Turbo mode (left click to go faster, right click to go slow)", 23 | $" {CC.Main}[oce.surf] {CC.White}Type {CC.Secondary}!replay{CC.White} in chat to see the best run on the current map", 24 | $" {CC.Main}[oce.surf] {CC.White}Want to support the server? All we ask is to {CC.Secondary}tell one friend{CC.White} about it and ask them to come play!", 25 | }; 26 | 27 | public int CurrentAdvertisementIndex { get; set; } = 0; 28 | 29 | 30 | public AdvertisingSystem(EventManager eventManager, EntityManager entityManager, Database database) : base(eventManager, entityManager, database) 31 | { 32 | EventManager.Subscribe(SendAdvertismentToServer); 33 | } 34 | 35 | public void SendAdvertismentToServer(OnAdvertisingTimerTickEvent e) 36 | { 37 | var advertisement = _advertisements[CurrentAdvertisementIndex % _advertisements.Length]; 38 | 39 | Server.NextFrame(() => 40 | { 41 | Server.PrintToChatAll(advertisement); 42 | }); 43 | 44 | 45 | CurrentAdvertisementIndex++; 46 | 47 | if (CurrentAdvertisementIndex >= _advertisements.Length) 48 | { 49 | CurrentAdvertisementIndex = 0; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /Systems/CheckpointZoneSystem.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using CounterStrikeSharp.API; 3 | using CounterStrikeSharp.API.Modules.Utils; 4 | 5 | namespace WST; 6 | 7 | public class CC 8 | { 9 | public static char Main = ChatColors.Olive; 10 | public static char Secondary = ChatColors.Yellow; 11 | public static char White = ChatColors.White; 12 | } 13 | 14 | public class CheckpointZoneSystem : System 15 | { 16 | public CheckpointZoneSystem(EventManager eventManager, EntityManager entityManager, Database database) 17 | : base(eventManager, entityManager, database) 18 | { 19 | EventManager.Subscribe(OnStartTouch); 20 | } 21 | 22 | public void OnStartTouch(OnStartTouchEvent e) 23 | { 24 | Entity entity = EntityManager.FindEntityOrThrow(e.PlayerSlot.ToString()); 25 | var playerInfo = entity.GetComponent(); 26 | if (playerInfo == null) return; 27 | var playerTimerComponent = entity.GetComponent(); 28 | if (playerTimerComponent == null) return; 29 | 30 | var map = EntityManager.FindEntity(); 31 | var mapZones = map.GetComponent(); 32 | if (mapZones == null) return; 33 | var route = mapZones.GetRoute(playerInfo.RouteKey); 34 | if (route == null) return; 35 | 36 | var checkpointZone = route.Checkpoints.FirstOrDefault(cp => cp.TargetName == e.TriggerName); 37 | 38 | 39 | if (checkpointZone != null) 40 | { 41 | var playerTimer = playerTimerComponent.Primary; 42 | var idx = route.Checkpoints.IndexOf(checkpointZone); 43 | 44 | // don't add the same checkpoint twice 45 | if (playerTimer.Checkpoints.Count > idx) 46 | { 47 | return; 48 | } 49 | 50 | var player = playerInfo.GetPlayer(); 51 | var velocity = new Vector(player.PlayerPawn.Value.AbsVelocity.X, player.PlayerPawn.Value.AbsVelocity.Y, player.PlayerPawn.Value.AbsVelocity.Z); 52 | 53 | var cp = new CheckpointTimerComponent 54 | { 55 | TimerTicks = playerTimer.TimerTicks, 56 | VelocityStartXY = velocity.Length2D(), 57 | VelocityStartZ = velocity.Z, 58 | VelocityStartXYZ = velocity.Length() 59 | }; 60 | playerTimer.Checkpoints.Add(cp); 61 | 62 | var time = Utils.FormatTime(playerTimer.TimerTicks); 63 | 64 | 65 | var velStartDisplay = Math.Round(cp.VelocityStartXY, 0); 66 | 67 | var chatString = new StringBuilder(); 68 | chatString.Append($" {CC.Secondary}⚡ {CC.White}CP [{CC.Secondary}{idx + 1}] {CC.Main}{time} {CC.White}({CC.Secondary}{velStartDisplay} u/s{CC.White})"); 69 | 70 | if (playerInfo.PR != null) 71 | { 72 | var prcp = playerInfo.PR.CheckpointsObject[idx]; 73 | if (prcp != null) 74 | { 75 | var timeDiff = cp.TimerTicks - prcp.TimerTicks; 76 | var timeDiffDisplay = Utils.FormatTime(timeDiff); 77 | 78 | if (timeDiff > 0) 79 | chatString.Append($" {CC.White}| PR: {CC.Main}+{timeDiffDisplay}"); 80 | else if (timeDiff < 0) 81 | chatString.Append($" {CC.White}| PR: {CC.Main}-{timeDiffDisplay}"); 82 | 83 | var diff = Math.Round(cp.VelocityStartXY - prcp.VelocityStartXY, 0); 84 | if (diff > 0) 85 | chatString.Append($" {CC.White}({CC.Secondary}+{diff} u/s{CC.White})"); 86 | else if (diff < 0) 87 | chatString.Append($" {CC.White}({CC.Secondary}{diff} u/s{CC.White})"); 88 | } 89 | } 90 | var mapWr = mapZones.GetWorldRecord(playerInfo.RouteKey, playerInfo.Style); 91 | if (mapWr != null) 92 | { 93 | var wrcp = mapWr.CheckpointsObject[idx]; 94 | if (wrcp != null) 95 | { 96 | var timeDiff = cp.TimerTicks - wrcp.TimerTicks; 97 | var timeDiffDisplay = Utils.FormatTime(timeDiff); 98 | 99 | if (timeDiff > 0) 100 | chatString.Append($" {CC.White}| WR: {CC.Main}+{timeDiffDisplay}"); 101 | else 102 | chatString.Append($" {CC.White}| WR: {CC.Main}-{timeDiffDisplay}"); 103 | 104 | var diff = Math.Round(cp.VelocityStartXY - wrcp.VelocityStartXY, 0); 105 | if (diff > 0) 106 | chatString.Append($" {CC.White}({CC.Secondary}+{diff} u/s{CC.White})"); 107 | else 108 | chatString.Append($" {CC.White}({CC.Secondary}{diff} u/s{CC.White})"); 109 | 110 | // WR: +0.000 (+0 u/s) 111 | // var centerChatString = ""; 112 | // if (timeDiff > 0) 113 | // centerChatString += $"WR: +{timeDiffDisplay}"; 114 | // else 115 | // centerChatString += $"WR: -{timeDiffDisplay}"; 116 | // 117 | // if (diff > 0) 118 | // centerChatString += $" (+{diff} u/s)"; 119 | // else 120 | // centerChatString += $" ({diff} u/s)"; 121 | // 122 | // player.PrintToCenter(centerChatString); 123 | } 124 | 125 | 126 | } 127 | 128 | 129 | player.PrintToChat(chatString.ToString()); 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /Systems/ClanTagSystem.cs: -------------------------------------------------------------------------------- 1 | namespace WST; 2 | 3 | public class UpdateClanTagEvent 4 | { 5 | public int PlayerSlot { get; set; } 6 | } 7 | public class ClanTagSystem : System 8 | { 9 | public ClanTagSystem(EventManager eventManager, EntityManager entityManager, Database database) 10 | : base(eventManager, entityManager, database) 11 | { 12 | EventManager.Subscribe(UpdateClanTag); 13 | } 14 | 15 | private void UpdateClanTag(UpdateClanTagEvent e) 16 | { 17 | var entity = EntityManager.FindEntity(e.PlayerSlot.ToString()); 18 | var playerInfo = entity.GetComponent(); 19 | if (playerInfo == null) return; 20 | 21 | var player = playerInfo.GetPlayer(); 22 | 23 | if (player == null || !player.IsValid || player.IsBot || player.IsHLTV || player.AuthorizedSteamID == null) return; 24 | 25 | player.Clan = $"[#{playerInfo.StyleRank}] {playerInfo.ChatRank()}"; 26 | } 27 | } -------------------------------------------------------------------------------- /Systems/ConnectionSystem.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | using CounterStrikeSharp.API.Core; 3 | using CounterStrikeSharp.API.Modules.Utils; 4 | using WST.Game; 5 | 6 | namespace WST; 7 | 8 | 9 | public class WST_EventPlayerConnect 10 | { 11 | public string SteamId { get; set; } 12 | public int Slot { get; set; } 13 | public string Name { get; set; } 14 | public string MapName { get; set; } 15 | public string Style { get; set; } 16 | 17 | } 18 | 19 | public class WST_EventPlayerDisconnect 20 | { 21 | public string SteamId { get; set; } 22 | public int Slot { get; set; } 23 | public string Name { get; set; } 24 | public string MapName { get; set; } 25 | 26 | 27 | 28 | public SurfPlayer Entity {get; set;} 29 | } 30 | 31 | public class WST_EventBotConnect 32 | { 33 | public int Slot { get; set; } 34 | } 35 | 36 | public class ConnectionSystem : System 37 | { 38 | public ConnectionSystem(EventManager eventManager, EntityManager entityManager, Database database) 39 | : base(eventManager, entityManager, database) 40 | { 41 | EventManager.Subscribe(OnPlayerConnect); 42 | EventManager.Subscribe(OnPlayerDisconnect); 43 | EventManager.Subscribe(OnBotConnect); 44 | } 45 | 46 | private void OnBotConnect(WST_EventBotConnect e) 47 | { 48 | var slot = e.Slot; 49 | var mapName = Server.MapName; 50 | 51 | EntityManager.AddEntity(new SurfBot() 52 | { 53 | Id = slot.ToString(), 54 | }); 55 | } 56 | 57 | private void OnPlayerConnect(WST_EventPlayerConnect e) 58 | { 59 | var steamid = e.SteamId; 60 | var slot = e.Slot; 61 | var name = e.Name; 62 | var mapName = Server.MapName; 63 | var style = e.Style; 64 | 65 | Task.Run(async () => 66 | { 67 | try 68 | { 69 | await Database.ExecuteAsync( 70 | "INSERT INTO players (steam_id, points, name) VALUES (@steamid, 0, @name) ON CONFLICT (steam_id) DO UPDATE SET name = EXCLUDED.name", 71 | new { steamid, name }); 72 | 73 | var playerComponent = new PlayerComponent(slot, steamid, name, style); 74 | await playerComponent.LoadStats(Database); 75 | await playerComponent.LoadRecords(Database, mapName); 76 | 77 | EntityManager.AddEntity(new SurfPlayer 78 | { 79 | Id = playerComponent.Slot.ToString(), 80 | Components = 81 | { 82 | { typeof(PlayerComponent), playerComponent } 83 | } 84 | }); 85 | 86 | 87 | Server.NextFrame(() => 88 | { 89 | var player = playerComponent.GetPlayer(); 90 | // player.Clan = playerComponent.SkillGroup.Name.ToUpper(); 91 | var existingName = player.PlayerName; 92 | 93 | var rank = playerComponent.StyleRank.ToString(); 94 | if (playerComponent.SkillGroup.Name.ToLower() == "unranked") 95 | { 96 | rank = "-"; 97 | } 98 | else 99 | { 100 | // var playerNameSchema = new SchemaString(player, "m_iszPlayerName"); 101 | // 102 | // 103 | // playerNameSchema.Set($"[#{rank}] {existingName}"); 104 | 105 | } 106 | 107 | // Utilities.SetStateChanged(player, "CCSPlayerController", "m_szClan"); 108 | // Utilities.SetStateChanged(player, "CBasePlayerController", "m_iszPlayerName"); 109 | //UpdatePlayerModel.UpdatePlayer(player); 110 | 111 | var pointsString = $"({ChatColors.Gold}{playerComponent.PlayerStats.PPoints}{ChatColors.White} points) " + 112 | $"(rank {ChatColors.Gold}{rank}{ChatColors.White}/{ChatColors.Gold}{playerComponent.PlayerStats.TotalPlayers}{ChatColors.White})"; 113 | 114 | // Will has joined the server (0 points) (rank 0/0) 115 | Server.PrintToChatAll( 116 | $" {playerComponent.ChatRankFormatted()} {ChatColors.Gold}{existingName}{ChatColors.White} has joined the server " + 117 | pointsString); 118 | 119 | 120 | // (MIXED-AU) Will has joined MIXED-AU on surf_cannonball (0 points) (rank 0/0) 121 | var globalMsg = 122 | $" {ChatColors.LightPurple}({Wst.Instance._currentGameServer.ShortName}) {playerComponent.ChatRankFormatted()}" + 123 | $" {ChatColors.Gold}{player.PlayerName}{ChatColors.White} has joined" + 124 | $" {ChatColors.LightPurple}{Wst.Instance._currentGameServer.ShortName}{ChatColors.White} on" + 125 | $" {ChatColors.LightPurple}{mapName} " + 126 | pointsString; 127 | _ = Wst.Instance._broadcast.Send("chat", new ChatBroadcast { Message = Utils.TagsToColorNames(globalMsg) }); 128 | 129 | Wst.Instance.AddTimer(2.0f, () => 130 | { 131 | EventManager.Publish(new UpdateClanTagEvent 132 | { 133 | PlayerSlot = slot 134 | }); 135 | }); 136 | 137 | Wst.Instance.AddTimer(5.0f, () => 138 | { 139 | var playerEntity = EntityManager.FindEntity(slot.ToString()); 140 | if (playerEntity == null) 141 | { 142 | return; 143 | } 144 | 145 | var player = playerEntity.GetPlayer(); 146 | if (player == null || !player.IsValid) 147 | { 148 | return; 149 | } 150 | 151 | if (style == "tm") 152 | { 153 | player.PrintToChat( 154 | $" {CC.Main}[oce.surf] {ChatColors.White}Welcome to {CC.Secondary}TURBO SURF."); 155 | player.PrintToChat( 156 | $" {CC.Main}[oce.surf] {CC.Secondary}HOLD LEFT CLICK{ChatColors.White} to go fast."); 157 | } 158 | 159 | }); 160 | }); 161 | } catch (Exception ex) 162 | { 163 | Console.WriteLine(ex); 164 | } 165 | }); 166 | } 167 | 168 | public void SetPlayerClanTag(int slot, string clanTag) 169 | { 170 | var player = Utilities.GetPlayerFromSlot(slot); 171 | if (player == null) return; 172 | player.Clan = clanTag; 173 | // Utilities.SetStateChanged(player, "CCSPlayerController", "m_szClan"); 174 | } 175 | 176 | private void OnPlayerDisconnect(WST_EventPlayerDisconnect e) 177 | { 178 | var entity = e.Entity; 179 | if (entity == null) 180 | { 181 | return; 182 | } 183 | 184 | var mapName = Server.MapName; 185 | var playerComponent = entity.GetComponent(); 186 | if (playerComponent != null) 187 | { 188 | Server.PrintToChatAll($" {playerComponent.ChatRankFormatted()} {ChatColors.Gold}{playerComponent.Name}{ChatColors.White} has left the server"); 189 | 190 | // (MIXED-AU) Will has left MIXED-AU on surf_cannonball 191 | var globalMsg = 192 | $" {ChatColors.LightPurple}({Wst.Instance._currentGameServer.ShortName}) {playerComponent.ChatRankFormatted()}" + 193 | $" {ChatColors.Gold}{playerComponent.Name}{ChatColors.White} has left " + 194 | $" {ChatColors.LightPurple}{Wst.Instance._currentGameServer.ShortName}{ChatColors.White} on" + 195 | $" {ChatColors.LightPurple}{mapName}"; 196 | 197 | _ = Wst.Instance._broadcast.Send("chat", new ChatBroadcast { Message = Utils.TagsToColorNames(globalMsg) }); 198 | return; 199 | } 200 | 201 | } 202 | } -------------------------------------------------------------------------------- /Systems/DiscordSystem.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | using System.Text.Json.Nodes; 4 | 5 | namespace WST; 6 | 7 | // { 8 | // "content": "**New World Record** | surf_evo - Main", 9 | // "embeds": [ 10 | // { 11 | // "color": null, 12 | // "fields": [ 13 | // { 14 | // "name": "Player", 15 | // "value": "Will", 16 | // "inline": true 17 | // }, 18 | // { 19 | // "name": "Time", 20 | // "value": "01:25:718 (-00:00:984)", 21 | // "inline": true 22 | // }, 23 | // { 24 | // "name": "Map Tier", 25 | // "value": "1", 26 | // "inline": true 27 | // } 28 | // ], 29 | // "footer": { 30 | // "text": "Server: hostname" 31 | // }, 32 | // "timestamp": "2024-01-02T11:00:00.000Z", 33 | // "image": { 34 | // "url": "https://raw.githubusercontent.com/Sayt123/SurfMapPics/Maps-and-bonuses/csgo/surf_mesa_aether.jpg" 35 | // } 36 | // } 37 | // ], 38 | // "attachments": [] 39 | // } 40 | 41 | public class EventPlayerWR 42 | { 43 | public string MapName { get; set; } 44 | public string RouteKey { get; set; } 45 | public string RouteName { get; set; } 46 | public string PlayerName { get; set; } 47 | public int Ticks { get; set; } 48 | public int? Diff { get; set; } 49 | public string Hostname { get; set; } 50 | public string MapTier { get; set; } 51 | 52 | public string Style { get; set; } 53 | } 54 | 55 | public class DiscordSystem : System 56 | { 57 | private readonly HttpClient _httpClient; 58 | 59 | private string MAP_WEBHOOK = Environment.GetEnvironmentVariable("DISCORD_MAP_WEBHOOK")!; 60 | private string STAGE_WEBHOOK = Environment.GetEnvironmentVariable("DISCORD_STAGE_WEBHOOK")!; 61 | private string BONUS_WEBHOOK = Environment.GetEnvironmentVariable("DISCORD_BONUS_WEBHOOK")!; 62 | private string STYLE_WEBHOOK = Environment.GetEnvironmentVariable("DISCORD_STYLE_WEBHOOK")!; 63 | 64 | public DiscordSystem(EventManager eventManager, EntityManager entityManager, Database database) 65 | : base(eventManager, entityManager, database) 66 | { 67 | EventManager.SubscribeAsync(OnPlayerRecord); 68 | _httpClient = new HttpClient(); 69 | } 70 | 71 | 72 | public async Task OnPlayerRecord(EventPlayerWR wr) 73 | { 74 | var mapName = wr.MapName; 75 | var routeKey = wr.RouteKey; 76 | var routeName = wr.RouteName; 77 | var playerName = wr.PlayerName; 78 | var style = wr.Style; 79 | 80 | 81 | var timeString = Utils.FormatTime(wr.Ticks); 82 | if (wr.Diff.HasValue) 83 | { 84 | timeString += $" (-{Utils.FormatTime(wr.Diff.Value)})"; 85 | } 86 | var hostname = wr.Hostname; 87 | var mapTier = wr.MapTier; 88 | 89 | 90 | var mapImg = $"https://raw.githubusercontent.com/Sayt123/SurfMapPics/Maps-and-bonuses/csgo/{mapName}.jpg"; 91 | // check if mapImg exists 92 | var mapImgExists = await _httpClient.GetAsync(mapImg); 93 | if (!mapImgExists.IsSuccessStatusCode) 94 | { 95 | mapImg = null; 96 | } 97 | 98 | var recordType = "Map"; 99 | var webhook = MAP_WEBHOOK; 100 | if (routeKey == "main" || routeKey == "boost") 101 | { 102 | recordType = "Map"; 103 | webhook = MAP_WEBHOOK; 104 | } else if (routeKey.StartsWith("b")) 105 | { 106 | recordType = "Bonus"; 107 | webhook = BONUS_WEBHOOK; 108 | } else if (routeKey.StartsWith("s")) 109 | { 110 | recordType = "Stage"; 111 | webhook = STAGE_WEBHOOK; 112 | } 113 | 114 | if (style != "normal") 115 | { 116 | // dont do stages for styles 117 | if (recordType == "Stage") 118 | { 119 | return; 120 | } 121 | 122 | if (style == "lg") 123 | { 124 | recordType = "Low Gravity"; 125 | } else if (style == "tm") 126 | { 127 | recordType = "Turbo"; 128 | } 129 | else if (style == "sw") 130 | { 131 | recordType = "Sideways"; 132 | } 133 | else if (style == "hsw") 134 | { 135 | recordType = "Half-Sideways"; 136 | } 137 | else if (style == "prac") 138 | { 139 | recordType = "Practice Mode/TAS"; 140 | } 141 | else 142 | { 143 | recordType = style; 144 | } 145 | 146 | webhook = STYLE_WEBHOOK; 147 | } 148 | 149 | var json = new JsonObject 150 | { 151 | ["content"] = $"**New {recordType} Record** | {mapName} - {routeName}", 152 | ["embeds"] = new JsonArray 153 | { 154 | new JsonObject 155 | { 156 | ["color"] = null, 157 | ["fields"] = new JsonArray 158 | { 159 | new JsonObject 160 | { 161 | ["name"] = "Player", 162 | ["value"] = playerName, 163 | ["inline"] = true 164 | }, 165 | new JsonObject 166 | { 167 | ["name"] = "Time", 168 | ["value"] = timeString, 169 | ["inline"] = true 170 | }, 171 | new JsonObject 172 | { 173 | ["name"] = "Map Tier", 174 | ["value"] = mapTier, 175 | ["inline"] = true 176 | } 177 | }, 178 | ["footer"] = new JsonObject 179 | { 180 | ["text"] = $"Server: {hostname}" 181 | }, 182 | ["timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), 183 | 184 | } 185 | }, 186 | ["attachments"] = new JsonArray() 187 | }; 188 | 189 | if (mapImg != null) 190 | { 191 | ((JsonObject)json["embeds"][0])["image"] = new JsonObject 192 | { 193 | ["url"] = mapImg 194 | }; 195 | } 196 | 197 | var content = new StringContent(json.ToString(), Encoding.UTF8, "application/json"); 198 | await _httpClient.PostAsync(webhook, content); 199 | // [Caff] beat the map World Record on < surf_beyer > with time < 01:18:36 [-00:00:04] > in the q3 Server on < = CS:S = > ]: 200 | var serverMessage = 201 | $" [{CC.Secondary}{playerName}{CC.White}] beat the Server Record on {CC.Secondary}{mapName}{CC.White} - {CC.Secondary}{routeName}{CC.White}"; 202 | if (style != "normal") 203 | { 204 | serverMessage += $" - {CC.Secondary}{recordType}{CC.White}"; 205 | } 206 | 207 | serverMessage += $" with time {CC.Main}{timeString}{CC.White} in the {CC.Secondary}{hostname}{CC.White} Server"; 208 | Wst.Instance._broadcast.Send("chat", new ChatBroadcast 209 | { 210 | Message = Utils.TagsToColorNames(serverMessage) 211 | }); 212 | } 213 | } -------------------------------------------------------------------------------- /Systems/HSWSurfSystem.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | using CounterStrikeSharp.API.Core; 3 | 4 | namespace WST; 5 | 6 | public class HSWSurfSystem : System 7 | { 8 | public const int FL_ONGROUND = 1 << 0; 9 | public HSWSurfSystem(EventManager eventManager, EntityManager entityManager, Database database) 10 | : base(eventManager, entityManager, database) 11 | { 12 | EventManager.Subscribe(OnTick); 13 | } 14 | 15 | private void OnTick(OnTickEvent e) 16 | { 17 | var entities = EntityManager.Entities(); 18 | 19 | foreach (var entity in entities) 20 | { 21 | var player = entity.GetPlayer(); 22 | if (!player.NativeIsValidAliveAndNotABot()) continue; 23 | var playerInfo = entity.GetComponent(); 24 | if (playerInfo == null) continue; 25 | if (playerInfo.Style != "hsw") continue; 26 | if (!player.Pawn.IsValid) continue; 27 | var buttons = (PlayerButtons) player.Pawn.Value.MovementServices.Buttons.ButtonStates[0]; 28 | var flags = player.Pawn.Value.Flags; 29 | var moveType = player.Pawn.Value.MoveType; 30 | 31 | var timer = entity.GetComponent(); 32 | if (timer == null) 33 | { 34 | continue; 35 | }; 36 | // 37 | // bool bForward = ((buttons & IN_FORWARD) > 0 && vel[0] >= 100.0); 38 | // bool bMoveLeft = ((buttons & IN_MOVELEFT) > 0 && vel[1] <= -100.0); 39 | // bool bBack = ((buttons & IN_BACK) > 0 && vel[0] <= -100.0); 40 | // bool bMoveRight = ((buttons & IN_MOVERIGHT) > 0 && vel[1] >= 100.0); 41 | // if (!g_bInStartZone[client] && !g_bInStageZone[client]) 42 | // { 43 | // if((bForward || bBack) && !(bMoveLeft || bMoveRight)) 44 | // { 45 | // vel[0] = 0.0; 46 | // buttons &= ~IN_FORWARD; 47 | // buttons &= ~IN_BACK; 48 | // } 49 | // if((bMoveLeft || bMoveRight) && !(bForward || bBack)) 50 | // { 51 | // vel[1] = 0.0; 52 | // buttons &= ~IN_MOVELEFT; 53 | // buttons &= ~IN_MOVERIGHT; 54 | // } 55 | // } 56 | 57 | 58 | // if on ground do nothing 59 | if ((flags & FL_ONGROUND) != 0) continue; 60 | // if on ladder do nothing 61 | if (moveType == MoveType_t.MOVETYPE_LADDER) continue; 62 | 63 | var currentSpeedXY = Math.Round(player.PlayerPawn.Value.AbsVelocity.Length2D()); 64 | // some grace period 65 | if (currentSpeedXY < 250) 66 | { 67 | continue; 68 | } 69 | 70 | // if ((buttons & PlayerButtons.Moveleft) != 0) 71 | // { 72 | // playerInfo.Style = "normal"; 73 | // player.PrintToChat($" Style set to {CC.Main}Normal {CC.Main}, A detected"); 74 | // } 75 | // 76 | // if ((buttons & PlayerButtons.Moveright) != 0) 77 | // { 78 | // playerInfo.Style = "normal"; 79 | // player.PrintToChat($" Style set to {CC.Main}Normal {CC.Main}, D detected"); 80 | // } 81 | 82 | var bForward = ((buttons & PlayerButtons.Forward) > 0); 83 | var bMoveLeft = ((buttons & PlayerButtons.Moveleft) > 0); 84 | var bBack = ((buttons & PlayerButtons.Back) > 0); 85 | var bMoveRight = ((buttons & PlayerButtons.Moveright) > 0); 86 | 87 | 88 | if ((bForward || bBack) && !(bMoveLeft || bMoveRight)) 89 | { 90 | player.PrintToChat($" Invalid keys detected for {CC.Main}HSW {CC.Main}, resetting"); 91 | Wst.Instance.Restart(player); 92 | continue; 93 | } 94 | if ((bMoveLeft || bMoveRight) && !(bForward || bBack)) 95 | { 96 | player.PrintToChat($" Invalid keys detected for {CC.Main}HSW {CC.Main}, resetting"); 97 | Wst.Instance.Restart(player); 98 | continue; 99 | } 100 | 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /Systems/HookSystem.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | 3 | namespace WST; 4 | 5 | public class HookSystem : System 6 | { 7 | public const int FL_ONGROUND = 1 << 0; 8 | public HookSystem(EventManager eventManager, EntityManager entityManager, Database database) 9 | : base(eventManager, entityManager, database) 10 | { 11 | EventManager.Subscribe(OnTick); 12 | } 13 | 14 | private void OnTick(OnTickEvent e) 15 | { 16 | var entities = EntityManager.Entities(); 17 | 18 | foreach (var entity in entities) 19 | { 20 | var player = entity.GetPlayer(); 21 | if (!player.NativeIsValidAliveAndNotABot()) continue; 22 | var playerInfo = entity.GetComponent(); 23 | if (playerInfo == null) continue; 24 | if (!player.Pawn.IsValid) continue; 25 | 26 | var buttons = (PlayerButtons) player.Pawn.Value.MovementServices.Buttons.ButtonStates[0]; 27 | var flags = player.Pawn.Value.Flags; 28 | var moveType = player.Pawn.Value.MoveType; 29 | 30 | var timer = entity.GetComponent(); 31 | if (timer != null) 32 | { 33 | entity.RemoveComponent(); 34 | }; 35 | // todo 36 | 37 | var isUse = PlayerButtons.Use; 38 | 39 | // todo need raycasting 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Systems/HudSystem.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using CounterStrikeSharp.API; 3 | using CounterStrikeSharp.API.Core; 4 | 5 | namespace WST; 6 | 7 | public class HudSystem : System 8 | { 9 | const string PRIMARY_COLOR = "#0079FF"; 10 | const string SECONDARY_COLOR = "#00DFA2"; 11 | const string TERTIARY_COLOR = "#F6FA70"; 12 | const string QUATERNARY_COLOR = "#FF0060"; 13 | const string WHITE = "#FFFFFF"; 14 | 15 | public HudSystem(EventManager eventManager, EntityManager entityManager, Database database) 16 | : base(eventManager, entityManager, database) 17 | { 18 | EventManager.Subscribe(Update); 19 | } 20 | 21 | private string FormatStyle(string style) 22 | { 23 | if (style == "normal") 24 | { 25 | return ""; 26 | } 27 | 28 | if (style == "tm") 29 | { 30 | style = "turbo"; 31 | } 32 | return $" ({style})"; 33 | } 34 | 35 | 36 | private ReplayHudData BuildReplayHudData(CCSPlayerController player, PlayerReplayComponent replay) 37 | { 38 | var speed = Math.Round(player.PlayerPawn.Value.AbsVelocity.Length2D()); 39 | return new ReplayHudData 40 | { 41 | Speed = speed, 42 | Ticks = replay.Tick, 43 | Route = replay.Route, 44 | ReplayPlayerName = TruncatePlayerName(replay.PlayerName), 45 | TotalTicks = replay.TotalTicks, 46 | CustomHudUrl = replay.CustomHudUrl, 47 | Buttons = replay.ReplayPlayback.Frames[replay.Tick].Buttons, 48 | Style = FormatStyle(replay.Style) 49 | }; 50 | } 51 | 52 | private RegularHudData BuildRegularHudData(CCSPlayerController player, PlayerComponent playerInfo, MapZone mapZones, PlayerTimerComponent? timerComponent, bool isSpec) 53 | { 54 | var speed = Math.Round(player.PlayerPawn.Value.AbsVelocity.Length2D()); 55 | var timer = timerComponent?.Primary; 56 | var hudData = new RegularHudData 57 | { 58 | Speed = speed, 59 | Ticks = timer?.TimerTicks ?? 0, 60 | Route = playerInfo.RouteKey, 61 | Style = FormatStyle(playerInfo.Style) 62 | }; 63 | 64 | if (playerInfo.PR != null) 65 | { 66 | hudData.PRTicks = playerInfo.PR.Ticks; 67 | hudData.PRPosition = playerInfo.PR.Position; 68 | hudData.PRTotalRecords = playerInfo.PR.TotalRecords; 69 | } 70 | 71 | var wr = mapZones.GetWorldRecord(playerInfo.RouteKey, playerInfo.Style); 72 | 73 | if (wr != null) 74 | { 75 | hudData.WRTicks = wr.Ticks; 76 | hudData.WRName = TruncatePlayerName(wr.Name); 77 | } 78 | 79 | if (isSpec) 80 | { 81 | // always show custom hud in spec 82 | hudData.CustomHudUrl = playerInfo.PlayerStats.CustomHudUrl; 83 | hudData.Buttons = player.Buttons; 84 | } 85 | else 86 | { 87 | // not specing so only show if on 88 | if (playerInfo.ShowCustomHud) 89 | { 90 | hudData.CustomHudUrl = playerInfo.PlayerStats.CustomHudUrl; 91 | } 92 | 93 | if (playerInfo.ShowKeys) 94 | { 95 | hudData.Buttons = player.Buttons; 96 | } 97 | } 98 | 99 | var lastCp = timer?.Checkpoints.LastOrDefault(); 100 | if (lastCp != null) 101 | { 102 | var lastCpIdx = timer.Checkpoints.IndexOf(lastCp); 103 | if (wr != null) 104 | { 105 | var wrCp = wr.CheckpointsObject[lastCpIdx]; 106 | if (wrCp != null) 107 | { 108 | var timeDiff = wrCp.TimerTicks - lastCp.TimerTicks; 109 | hudData.CpTicksDiff = timeDiff; 110 | } 111 | } 112 | } 113 | 114 | if (playerInfo.SecondaryRouteKey != null) 115 | { 116 | hudData.SecondaryRoute = playerInfo.SecondaryRouteKey; 117 | } 118 | 119 | return hudData; 120 | } 121 | 122 | private string RenderReplayHud(ReplayHudData replayHudData) 123 | { 124 | var speedLine = GenerateSpeedLine(replayHudData.Speed); 125 | var timeLine = GenerateTimeLine(replayHudData.Ticks, TERTIARY_COLOR); 126 | var replayLine = $" REPLAY
"; 127 | var routeLine = $" {replayHudData.Route}"; 128 | routeLine += replayHudData.Style; 129 | routeLine += $" | {replayHudData.ReplayPlayerName} | {Utils.FormatTime(replayHudData.TotalTicks)}
"; 130 | var customHud = replayHudData.CustomHudUrl != null ? $"
" : ""; 131 | var playerButtons = (PlayerButtons) replayHudData.Buttons; 132 | var buttonsLine = GenerateButtonsLine(playerButtons); 133 | return speedLine + timeLine + buttonsLine + routeLine + customHud; 134 | } 135 | 136 | private string RenderHud(RegularHudData hudData) 137 | { 138 | var speedLine = GenerateSpeedLine(hudData.Speed); 139 | var timeColor = hudData.Ticks == 0 ? QUATERNARY_COLOR : SECONDARY_COLOR; 140 | var timeLine = GenerateTimeLine(hudData.Ticks, timeColor); 141 | 142 | var routeLineString = hudData.Route; 143 | 144 | if (hudData.SecondaryRoute != null) 145 | { 146 | routeLineString += $" ({hudData.SecondaryRoute})"; 147 | } 148 | 149 | routeLineString += hudData.Style; 150 | 151 | if (hudData.PRTicks.HasValue && hudData.PRPosition.HasValue && hudData.PRTotalRecords.HasValue) 152 | { 153 | routeLineString += $" | PR: {Utils.FormatTime(hudData.PRTicks.Value)} ({hudData.PRPosition.Value}/{hudData.PRTotalRecords.Value})"; 154 | } 155 | 156 | if (hudData.WRTicks.HasValue && hudData.WRName != null) 157 | { 158 | routeLineString += $" | WR: {Utils.FormatTime(hudData.WRTicks.Value)}({hudData.WRName})"; 159 | } 160 | 161 | var routeLine = $" {routeLineString}
"; 162 | 163 | var customHud = hudData.CustomHudUrl != null ? $"
" : ""; 164 | 165 | var buttonsLine = hudData.Buttons.HasValue ? GenerateButtonsLine(hudData.Buttons.Value) : ""; 166 | 167 | var cpLine = hudData.CpTicksDiff.HasValue ? $" {Utils.FormatTimeWithPlusOrMinus(hudData.CpTicksDiff.Value)}
" : ""; 168 | 169 | return speedLine + timeLine + cpLine + buttonsLine + routeLine + customHud; 170 | } 171 | 172 | private string GenerateSpeedLine(double speed) 173 | { 174 | return $" {speed}
"; 175 | } 176 | 177 | private string GenerateTimeLine(int ticks, string color) 178 | { 179 | return $" {Utils.FormatTime(ticks)}
"; 180 | } 181 | 182 | private string GenerateButtonsLine(PlayerButtons playerButtons) 183 | { 184 | return $" {((playerButtons & PlayerButtons.Moveleft) != 0 ? "A" : "_")} " + 185 | $"{((playerButtons & PlayerButtons.Forward) != 0 ? "W" : "_")} " + 186 | $"{((playerButtons & PlayerButtons.Moveright) != 0 ? "D" : "_")} " + 187 | $"{((playerButtons & PlayerButtons.Back) != 0 ? "S" : "_")} " + 188 | $"{((playerButtons & PlayerButtons.Jump) != 0 ? "J" : "_")} " + 189 | $"{((playerButtons & PlayerButtons.Duck) != 0 ? "C" : "_")}
"; 190 | } 191 | 192 | private string TruncatePlayerName(string? name) 193 | { 194 | if (name == null) return ""; 195 | var newName = name.Replace("<", "").Replace(">", ""); 196 | // check string length 197 | if (newName.Length <= 10) return newName; 198 | // truncate string 199 | return newName.Substring(0, 10); 200 | } 201 | 202 | private class ReplayHudData 203 | { 204 | public double Speed { get; set; } 205 | public int Ticks { get; set; } 206 | public int TotalTicks { get; set; } 207 | public string Route { get; set; } 208 | 209 | public string Style { get; set; } 210 | public string ReplayPlayerName { get; set; } 211 | public string? CustomHudUrl { get; set; } 212 | public ulong Buttons { get; set; } 213 | } 214 | 215 | private class RegularHudData 216 | { 217 | public double Speed { get; set; } 218 | public int Ticks { get; set; } 219 | public string Route { get; set; } 220 | 221 | public string SecondaryRoute { get; set; } 222 | public int? PRTicks { get; set; } 223 | public int? PRPosition { get; set; } 224 | public int? PRTotalRecords { get; set; } 225 | public int? WRTicks { get; set; } 226 | public string? WRName { get; set; } 227 | public string? CustomHudUrl { get; set; } 228 | public PlayerButtons? Buttons { get; set; } 229 | 230 | public int? CpTicksDiff { get; set; } 231 | 232 | public string Style { get; set; } 233 | } 234 | 235 | 236 | private void Update(OnTickEvent e) 237 | { 238 | var entities = EntityManager.Entities(); 239 | var map = EntityManager.FindEntity(); 240 | if (map == null) 241 | { 242 | return; 243 | } 244 | var mapZones = map.GetComponent(); 245 | 246 | foreach (var entity in entities) 247 | { 248 | var player = entity.GetPlayer(); 249 | if (!player.NativeIsValidAliveAndNotABot()) continue; 250 | if (mapZones == null) continue; 251 | var playerInfo = entity.GetComponent(); 252 | if (playerInfo == null) continue; 253 | 254 | var replay = entity.GetComponent(); 255 | if (replay != null) 256 | { 257 | var replayHudData = BuildReplayHudData(player, replay); 258 | var replayHud = RenderReplayHud(replayHudData); 259 | TimerPrintHtml(player, playerInfo, replayHud); 260 | continue; 261 | } 262 | 263 | 264 | var timer = entity.GetComponent(); 265 | // note timer may be null and this is valid (startzones etc) 266 | // Todo: Shouldnt be in the hud lol 267 | if (timer != null) 268 | { 269 | timer.Primary.TimerTicks++; 270 | if (timer.Secondary != null) 271 | { 272 | timer.Secondary.TimerTicks++; 273 | } 274 | } 275 | 276 | var hudData = BuildRegularHudData(player, playerInfo, mapZones, timer, false); 277 | var hud = RenderHud(hudData); 278 | TimerPrintHtml(player, playerInfo, hud); 279 | } 280 | 281 | foreach (var entity in entities) 282 | { 283 | var playerInfo = entity.GetComponent(); 284 | var player = playerInfo.GetPlayer(); 285 | if (!player.IsValid) continue; 286 | 287 | // dont show spec hud to alive players 288 | if (player.PawnIsAlive) 289 | { 290 | continue; 291 | } 292 | 293 | var obsPawnHandle = player.ObserverPawn; 294 | if (!obsPawnHandle.IsValid) continue; 295 | var obsPawn = obsPawnHandle.Value; 296 | var observices = obsPawn.ObserverServices; 297 | if (observices == null) continue; 298 | var obsTargetHandle = observices.ObserverTarget; 299 | if (!obsTargetHandle.IsValid) continue; 300 | var target = obsTargetHandle.Value; 301 | if (target == null || !target.IsValid) continue; 302 | if (target.DesignerName != "player") continue; 303 | var targetPlayer = new CCSPlayerController(new CCSPlayerPawn(target.Handle).Controller.Value.Handle); 304 | var targetSlot = targetPlayer.Slot; 305 | // important: Could be a bot. 306 | var targetEntity = EntityManager.FindEntity(targetSlot.ToString()); 307 | if (targetEntity == null) continue; 308 | 309 | var targetPlayerReplay = targetEntity.GetComponent(); 310 | if (targetPlayerReplay != null) 311 | { 312 | var replayHudData = BuildReplayHudData(targetPlayer, targetPlayerReplay); 313 | var replayHud = RenderReplayHud(replayHudData); 314 | TimerPrintHtml(player, playerInfo, replayHud); 315 | continue; 316 | } 317 | 318 | var targetPlayerInfo = targetEntity.GetComponent(); 319 | if (targetPlayerInfo == null) continue; 320 | 321 | var targetTimer = targetEntity.GetComponent(); 322 | // note timer may be null and this is valid (startzones etc) 323 | var hudData = BuildRegularHudData(targetPlayer, targetPlayerInfo, mapZones, targetTimer, true); 324 | var hud = RenderHud(hudData); 325 | TimerPrintHtml(player, playerInfo, hud); 326 | } 327 | } 328 | 329 | public void TimerPrintHtml(CCSPlayerController player, PlayerComponent playerInfo, string hudContent) 330 | { 331 | if (!playerInfo.ShowHud) 332 | { 333 | return; 334 | } 335 | var @event = new EventShowSurvivalRespawnStatus(false) 336 | { 337 | LocToken = hudContent, 338 | Duration = 5, 339 | Userid = player 340 | }; 341 | @event.FireEvent(false); 342 | @event = null; 343 | } 344 | } -------------------------------------------------------------------------------- /Systems/MapLoadSystem.cs: -------------------------------------------------------------------------------- 1 | namespace WST; 2 | 3 | public class MapLoadSystem : System 4 | { 5 | public MapLoadSystem(EventManager eventManager, EntityManager entityManager, Database database) : base(eventManager, entityManager, database) 6 | { 7 | } 8 | 9 | 10 | } -------------------------------------------------------------------------------- /Systems/PlaytimeSystem.cs: -------------------------------------------------------------------------------- 1 | namespace WST; 2 | 3 | public class OnOneSecondEvent 4 | { 5 | } 6 | 7 | public class PlaytimeSystem : System 8 | { 9 | public PlaytimeSystem(EventManager eventManager, EntityManager entityManager, Database database) 10 | : base(eventManager, entityManager, database) 11 | { 12 | EventManager.Subscribe(OnEachSecond); 13 | } 14 | 15 | public void OnEachSecond(OnOneSecondEvent e) 16 | { 17 | 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /Systems/ReplayPlaybackSystem.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using CounterStrikeSharp.API; 3 | using CounterStrikeSharp.API.Core; 4 | using CounterStrikeSharp.API.Modules.Utils; 5 | using WST.Game; 6 | 7 | namespace WST; 8 | 9 | internal class LoadReplayEvent 10 | { 11 | public string MapName { get; set; } 12 | public int PlayerSlot { get; set; } 13 | public int Position { get; set; } 14 | } 15 | 16 | 17 | public class EventOnLoadBot 18 | { 19 | public string MapName { get; set; } 20 | public string Route { get; set; } 21 | public string Style { get; set; } 22 | } 23 | 24 | public class ReplayPlaybackSystem : System 25 | { 26 | public const int FL_ONGROUND = 1 << 0; 27 | public const int FL_DUCKING = 1 << 1; 28 | 29 | private Replay? _replay; 30 | 31 | public ReplayPlaybackSystem(EventManager eventManager, EntityManager entityManager, Database database) 32 | : base(eventManager, entityManager, database) 33 | { 34 | EventManager.Subscribe(Update); 35 | EventManager.SubscribeAsync(OnLoadReplay); 36 | EventManager.Subscribe(OnNewWr); 37 | EventManager.Subscribe(OnLoadBot); 38 | } 39 | 40 | private void OnLoadBot(EventOnLoadBot bot) 41 | { 42 | OnLoadReplayBot(bot.MapName, "main", bot.Style); 43 | } 44 | private void OnNewWr(EventPlayerWR wr) 45 | { 46 | if (wr.RouteKey != "main") 47 | { 48 | return; 49 | } 50 | 51 | if (wr.Style != Wst.Instance._currentGameServer.Style) 52 | { 53 | return; 54 | } 55 | 56 | OnLoadReplayBot(wr.MapName, "main", wr.Style); 57 | } 58 | 59 | private async Task OnLoadReplayBot(string mapName, string route, string style) 60 | { 61 | try 62 | { 63 | var firstRow = await V_Replay.GetRecordReplay(Database, mapName, route, 1, style); 64 | Server.NextFrame(() => 65 | { 66 | if (firstRow == null) 67 | { 68 | Server.PrintToChatAll($" {ChatColors.Gold}No replay found for {ChatColors.White}{mapName}"); 69 | return; 70 | } 71 | 72 | var replayDataBytes = firstRow.Replay; 73 | var replay = Replay.Deserialize(replayDataBytes); 74 | 75 | var replayComponent = new PlayerReplayComponent(replay, 0, firstRow.Route, firstRow.PlayerName, firstRow.Style, firstRow.CustomHudUrl); 76 | 77 | var entities = EntityManager.Entities(); 78 | if (entities.Count == 0) 79 | { 80 | Console.WriteLine("[OnLoadReplayBot] Could not find bot entity"); 81 | return; 82 | } 83 | if (entities.Count > 1) 84 | { 85 | Console.WriteLine("[OnLoadReplayBot] Found more than one bot entity"); 86 | return; 87 | } 88 | 89 | var entity = entities[0]; 90 | 91 | entity.AddComponent(replayComponent); 92 | 93 | var player = entity.GetPlayer(); 94 | if (player == null || !player.IsValid) 95 | { 96 | Console.WriteLine("[OnLoadReplayBot] Could not find bot player"); 97 | return; 98 | } 99 | var playerNameSchema = new SchemaString(player, "m_iszPlayerName"); 100 | if (playerNameSchema == null) 101 | { 102 | Console.WriteLine("[OnLoadReplayBot] Could not find bot player name schema"); 103 | return; 104 | } 105 | playerNameSchema.Set($"[WR] {firstRow.PlayerName} - {Utils.FormatTime(firstRow.Ticks)}"); 106 | Utilities.SetStateChanged(player, "CBasePlayerController", "m_iszPlayerName"); 107 | 108 | }); 109 | } 110 | catch (Exception ex) 111 | { 112 | Console.WriteLine(ex); 113 | } 114 | } 115 | 116 | private async Task OnLoadReplay(LoadReplayEvent e) 117 | { 118 | var player = EntityManager.FindEntity(e.PlayerSlot.ToString()); 119 | player.RemoveComponent(); 120 | 121 | var playerInfo = player.GetComponent(); 122 | var route = playerInfo.RouteKey; 123 | 124 | // shitty ass coding why am am i naming shit like this 125 | V_Replay? firstRow = null; 126 | // stupid hack -1 means self 127 | if (e.Position == -1) 128 | { 129 | firstRow = await V_Replay.GetReplay(Database, e.MapName, route, playerInfo.SteamId, playerInfo.Style); 130 | } 131 | else 132 | { 133 | firstRow = await V_Replay.GetRecordReplay(Database, e.MapName, route, e.Position, playerInfo.Style); 134 | } 135 | if (firstRow == null || firstRow.Replay == null) 136 | { 137 | Server.NextFrame(() => 138 | { 139 | var playerController = playerInfo.GetPlayer(); 140 | playerController.PrintToChat($" {CC.White}No {CC.Main}replay {CC.White}found for {CC.Secondary}{route} {CC.Main}on {CC.Secondary}{e.MapName}"); 141 | }); 142 | return; 143 | } 144 | 145 | var wrName = firstRow.PlayerName; 146 | var wrTicks = firstRow.Ticks; 147 | var customHudUrl = firstRow.CustomHudUrl; 148 | 149 | Server.NextFrame(() => 150 | { 151 | var replayDataBytes = firstRow.Replay; 152 | var replay = Replay.Deserialize(replayDataBytes); 153 | 154 | Console.WriteLine("Replay Frames: " + replay.Frames.Count); 155 | 156 | if (replay.Frames.Count == 0) 157 | { 158 | Console.WriteLine("Replay Frames is 0, deleting replay"); 159 | firstRow.Delete(Database); 160 | playerInfo.GetPlayer().PrintToChat($" {ChatColors.Red}Replay Error: ${CC.White} Attempting to clean up, please try !replay again."); 161 | return; 162 | } 163 | 164 | var desiredPosition = e.Position; 165 | var replayPosition = firstRow.Position; 166 | 167 | if (desiredPosition != -1 && desiredPosition != replayPosition) 168 | { 169 | playerInfo.GetPlayer().PrintToChat($" {CC.Main}[oce.surf] {CC.White}No replay found for position {CC.Secondary}{desiredPosition}/{firstRow.TotalRecords}{CC.White}, playing closest replay at position {CC.Secondary}{replayPosition}/{firstRow.TotalRecords}"); 170 | } 171 | 172 | playerInfo.GetPlayer().PrintToChat($" {CC.Main}[oce.surf] {CC.White}Type !main or !r to exit replay mode | Player: {CC.Secondary}{firstRow.PlayerName}{CC.White} | {CC.Main}{Utils.FormatTime(firstRow.Ticks)} {CC.White}({CC.Secondary}{replayPosition}/{firstRow.TotalRecords}{CC.White})"); 173 | 174 | // try 175 | // { 176 | // List velocities = new List(); 177 | // for (int i = 1; i < replay.Frames.Count; i++) 178 | // { 179 | // Vector displacement = replay.Frames[i].Pos - replay.Frames[i - 1].Pos; 180 | // 181 | // double timeInterval = 1.0 / 64; // Time per tick 182 | // 183 | // Console.WriteLine(timeInterval); 184 | // 185 | // var velocityX = displacement.X / timeInterval; 186 | // var velocityY = displacement.Y / timeInterval; 187 | // var velocityZ = displacement.Z / timeInterval; 188 | // 189 | // var velocityXY = Math.Sqrt(Math.Pow((double)velocityX, 2) + Math.Pow((double)velocityY, 2)); 190 | // velocities.Add(velocityXY); 191 | // 192 | // // Vector velocity = displacement / timeInterval; 193 | // // velocities.Add(velocity.Length2D()); 194 | // } 195 | // 196 | // // Write velocities to file as json in C:\tmp\velocities.json 197 | // var velocitiesJson = JsonSerializer.Serialize(velocities); 198 | // File.WriteAllText(@"C:\tmp\velocities.json", velocitiesJson); 199 | // } catch (Exception ex) 200 | // { 201 | // Console.WriteLine(ex); 202 | // } 203 | 204 | player.AddComponent(new PlayerReplayComponent(replay, 0, firstRow.Route, firstRow.PlayerName, firstRow.Style, firstRow.CustomHudUrl)); 205 | }); 206 | } 207 | 208 | 209 | private void Update(OnTickEvent e) 210 | { 211 | // bot OR player 212 | var entities = EntityManager.Entities(); 213 | foreach (var entity in entities) 214 | { 215 | var playerController = entity.GetPlayer(); 216 | var replayComponent = entity.GetComponent(); 217 | 218 | if (replayComponent == null) continue; 219 | 220 | var currentFrame = replayComponent.ReplayPlayback.Frames[replayComponent.Tick]; 221 | var currentPosition = playerController.PlayerPawn.Value.AbsOrigin!; 222 | 223 | 224 | // Calculate velocity 225 | var velocity = (currentFrame.Pos - currentPosition) * 64; 226 | 227 | var isOnGround = (currentFrame.Flags & FL_ONGROUND) != 0; 228 | var isDucking = (currentFrame.Flags & FL_DUCKING) != 0; 229 | 230 | if (isOnGround) 231 | playerController.PlayerPawn.Value.MoveType = MoveType_t.MOVETYPE_WALK; 232 | else 233 | playerController.PlayerPawn.Value.MoveType = MoveType_t.MOVETYPE_NOCLIP; 234 | 235 | // check if our current position is more than 200 units away from the replay position 236 | if (currentPosition.DistanceTo(currentFrame.Pos) > 300) 237 | playerController.PlayerPawn.Value.Teleport(currentFrame.Pos, currentFrame.Ang, new Vector(nint.Zero)); 238 | else 239 | playerController.PlayerPawn.Value.Teleport(new Vector(nint.Zero), currentFrame.Ang, velocity); 240 | 241 | 242 | replayComponent.Tick++; 243 | if (replayComponent.Tick >= replayComponent.ReplayPlayback.Frames.Count) replayComponent.Tick = 0; 244 | } 245 | } 246 | } -------------------------------------------------------------------------------- /Systems/ReplayRecorderSystem.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API.Modules.Utils; 2 | 3 | namespace WST; 4 | 5 | public class ReplayRecorderSystem : System 6 | { 7 | public ReplayRecorderSystem(EventManager eventManager, EntityManager entityManager, Database database) 8 | : base(eventManager, entityManager, database) 9 | { 10 | eventManager.Subscribe(Update); 11 | } 12 | 13 | private void Update(OnTickEvent e) 14 | { 15 | var entities = EntityManager.Entities(); 16 | foreach (var entity in entities) 17 | { 18 | var playerComponent = entity.GetComponent(); 19 | if (playerComponent == null) continue; 20 | var player = playerComponent.GetPlayer(); 21 | if (!player.NativeIsValidAliveAndNotABot()) continue; 22 | var timer = entity.GetComponent(); 23 | if (timer == null) continue; 24 | if (!player.Pawn.IsValid) continue; 25 | if (!player.PlayerPawn.IsValid) continue; 26 | 27 | var playerPos = player.Pawn.Value.AbsOrigin; 28 | var playerAngle = player.PlayerPawn.Value.EyeAngles; 29 | var buttons = player.Pawn.Value.MovementServices.Buttons.ButtonStates[0]; 30 | var flags = player.Pawn.Value.Flags; 31 | var moveType = player.Pawn.Value.MoveType; 32 | 33 | var frame = new Replay.FrameT 34 | { 35 | Pos = new ZoneVector(playerPos.X, playerPos.Y, playerPos.Z), 36 | Ang = new ZoneVector(playerAngle.X, playerAngle.Y, playerAngle.Z), 37 | Buttons = buttons, 38 | Flags = flags, 39 | MoveType = moveType 40 | }; 41 | 42 | timer.Primary.Recording.Frames.Add(frame); 43 | if (timer.Secondary != null) 44 | { 45 | timer.Secondary.Recording.Frames.Add(frame); 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Systems/ServerSystem.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using CounterStrikeSharp.API; 3 | 4 | namespace WST; 5 | 6 | public class OnServerStatusUpdateEvent 7 | { 8 | public string ServerId { get; set; } 9 | } 10 | 11 | public class PlayerJson 12 | { 13 | public string Name { get; set; } 14 | public string RouteKey { get; set; } 15 | public V_Player PlayerStats { get; set; } 16 | } 17 | 18 | public class ServerSystem : System 19 | { 20 | public ServerSystem(EventManager eventManager, EntityManager entityManager, Database database) 21 | : base(eventManager, entityManager, database) 22 | { 23 | EventManager.Subscribe(OnServerStatusUpdate); 24 | } 25 | 26 | private void OnServerStatusUpdate(OnServerStatusUpdateEvent e) 27 | { 28 | var surfPlayers = EntityManager.Entities(); 29 | var map = Server.MapName; 30 | var maxPlayers = Server.MaxPlayers; 31 | 32 | var playerComponents = new List(); 33 | foreach (var surfPlayer in surfPlayers) 34 | { 35 | var playerInfo = surfPlayer.GetComponent(); 36 | if (playerInfo == null) continue; 37 | 38 | var playerJson = new PlayerJson 39 | { 40 | Name = playerInfo.Name, 41 | RouteKey = playerInfo.RouteKey, 42 | PlayerStats = playerInfo.PlayerStats 43 | }; 44 | playerComponents.Add(playerJson); 45 | } 46 | 47 | var playerCount = playerComponents.Count; 48 | 49 | 50 | Task.Run(async () => 51 | { 52 | try 53 | { 54 | await Database.ExecuteAsync("update servers " + 55 | "set current_map = @current_map, " + 56 | "players = CAST(@players as jsonb), " + 57 | "player_count = @player_count, " + 58 | "total_players = @total_players " + 59 | "where server_id = @id", new 60 | { 61 | id = e.ServerId, 62 | current_map = map, 63 | player_count = playerCount, 64 | total_players = maxPlayers, 65 | players = JsonSerializer.Serialize(playerComponents, new JsonSerializerOptions 66 | { 67 | DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, 68 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 69 | }) 70 | }); 71 | } 72 | catch (Exception ex) 73 | { 74 | Console.WriteLine(ex); 75 | } 76 | }); 77 | } 78 | } -------------------------------------------------------------------------------- /Systems/SidewaysSurfSystem.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | using CounterStrikeSharp.API.Core; 3 | 4 | namespace WST; 5 | 6 | public class SidewaysSurfSystem : System 7 | { 8 | public const int FL_ONGROUND = 1 << 0; 9 | public SidewaysSurfSystem(EventManager eventManager, EntityManager entityManager, Database database) 10 | : base(eventManager, entityManager, database) 11 | { 12 | EventManager.Subscribe(OnTick); 13 | } 14 | 15 | private void OnTick(OnTickEvent e) 16 | { 17 | var entities = EntityManager.Entities(); 18 | 19 | foreach (var entity in entities) 20 | { 21 | var player = entity.GetPlayer(); 22 | if (!player.NativeIsValidAliveAndNotABot()) continue; 23 | var playerInfo = entity.GetComponent(); 24 | if (playerInfo == null) continue; 25 | if (playerInfo.Style != "sw") continue; 26 | if (!player.Pawn.IsValid) continue; 27 | var buttons = (PlayerButtons) player.Pawn.Value.MovementServices.Buttons.ButtonStates[0]; 28 | var flags = player.Pawn.Value.Flags; 29 | var moveType = player.Pawn.Value.MoveType; 30 | 31 | var timer = entity.GetComponent(); 32 | if (timer == null) 33 | { 34 | continue; 35 | }; 36 | 37 | // if on ground do nothing 38 | if ((flags & FL_ONGROUND) != 0) continue; 39 | // if on ladder do nothing 40 | if (moveType == MoveType_t.MOVETYPE_LADDER) continue; 41 | 42 | var currentSpeedXY = Math.Round(player.PlayerPawn.Value.AbsVelocity.Length2D()); 43 | // some grace period 44 | if (currentSpeedXY < 250) 45 | { 46 | continue; 47 | } 48 | 49 | if ((buttons & PlayerButtons.Moveleft) != 0) 50 | { 51 | // playerInfo.Style = "normal"; 52 | // player.PrintToChat($" Style set to {CC.Main}Normal {CC.Main}, A detected"); 53 | player.PrintToChat($" Invalid keys detected for {CC.Main}SW {CC.Main}, resetting"); 54 | Wst.Instance.Restart(player); 55 | continue; 56 | } 57 | 58 | if ((buttons & PlayerButtons.Moveright) != 0) 59 | { 60 | // playerInfo.Style = "normal"; 61 | // player.PrintToChat($" Style set to {CC.Main}Normal {CC.Main}, D detected"); 62 | player.PrintToChat($" Invalid keys detected for {CC.Main}SW {CC.Main}, resetting"); 63 | Wst.Instance.Restart(player); 64 | continue; 65 | } 66 | 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Systems/StartZoneSystem.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using CounterStrikeSharp.API.Core; 3 | using CounterStrikeSharp.API.Modules.Utils; 4 | 5 | namespace WST; 6 | 7 | public class StartZoneSystem : System 8 | { 9 | public StartZoneSystem(EventManager eventManager, EntityManager entityManager, Database database) 10 | : base(eventManager, entityManager, database) 11 | { 12 | EventManager.Subscribe(OnStartTouch); 13 | EventManager.Subscribe(OnEndTouch); 14 | } 15 | 16 | 17 | public void OnStartTouch(OnStartTouchEvent e) 18 | { 19 | Entity entity = EntityManager.FindEntityOrThrow(e.PlayerSlot.ToString()); 20 | var playerInfo = entity.GetComponent(); 21 | if (playerInfo == null) return; 22 | 23 | var map = EntityManager.FindEntity(); 24 | var mapZones = map.GetComponent(); 25 | if (mapZones == null) return; 26 | var route = mapZones.GetRoute(playerInfo.RouteKey); 27 | if (route == null) return; 28 | 29 | if (route.Start.TargetName == e.TriggerName) 30 | { 31 | // entering startzone 32 | entity.RemoveComponent(); 33 | if (playerInfo.Style == "prac") 34 | { 35 | playerInfo.Style = "normal"; 36 | } 37 | } 38 | 39 | // subroute 40 | if (playerInfo.RouteKey != "main") return; 41 | 42 | var stageRoutes = mapZones.GetStageRoutes(); 43 | var stageRoute = stageRoutes.FirstOrDefault(x => x.Start.TargetName == e.TriggerName); 44 | if (stageRoute == null) return; 45 | 46 | playerInfo.ChangeSecondaryRoute(stageRoute.Key); 47 | } 48 | 49 | public void OnEndTouch(OnEndTouchEvent e) 50 | { 51 | Console.WriteLine("ENDTOUCH"); 52 | Entity entity = EntityManager.FindEntityOrThrow(e.PlayerSlot.ToString()); 53 | var playerInfo = entity.GetComponent(); 54 | if (playerInfo == null) return; 55 | if (playerInfo.Teleporting) return; 56 | 57 | var player = playerInfo.GetPlayer(); 58 | 59 | var replay = entity.GetComponent(); 60 | if (replay != null) 61 | { 62 | return; 63 | } 64 | 65 | var map = EntityManager.FindEntity(); 66 | var mapZones = map.GetComponent(); 67 | if (mapZones == null) return; 68 | var route = mapZones.GetRoute(playerInfo.RouteKey); 69 | if (route == null) return; 70 | 71 | if (route.Start.TargetName == e.TriggerName) 72 | { 73 | // Standard we are starting a run 74 | entity.RemoveComponent(); 75 | 76 | var timerComponent = new PlayerTimerComponent(); 77 | var timer = timerComponent.Primary; 78 | var startVelocity = new Vector(player.PlayerPawn.Value.AbsVelocity.X, player.PlayerPawn.Value.AbsVelocity.Y, player.PlayerPawn.Value.AbsVelocity.Z); 79 | timer.VelocityStartXY = startVelocity.Length2D(); 80 | timer.VelocityStartZ = startVelocity.Z; 81 | timer.VelocityStartXYZ = startVelocity.Length(); 82 | timer.RouteKey = playerInfo.RouteKey; 83 | 84 | if (timer.VelocityStartXY > route.VelocityCapStartXY) 85 | { 86 | Utils.AdjustPlayerVelocity(player, 270); 87 | } 88 | 89 | entity.AddComponent(timerComponent); 90 | 91 | // Round to a whole number 92 | var velStartDisplay = Math.Round(timer.VelocityStartXYZ, 0); 93 | 94 | var chatString = new StringBuilder(); 95 | chatString.Append($" {CC.Secondary}⚡ {CC.White}Start: {CC.Secondary}{velStartDisplay} u/s"); 96 | 97 | if (playerInfo.PR != null) 98 | { 99 | var diff = Math.Round(timer.VelocityStartXYZ - playerInfo.PR.VelocityStartXYZ, 0); 100 | if (diff > 0) 101 | chatString.Append($" {CC.White}| PR: {CC.Secondary}+{diff} u/s"); 102 | else if (diff < 0) 103 | chatString.Append($" {CC.White}| PR: {CC.Secondary}{diff} u/s"); 104 | } 105 | var mapWr = mapZones.GetWorldRecord(playerInfo.RouteKey, playerInfo.Style); 106 | if (mapWr != null) 107 | { 108 | var diff = Math.Round(timer.VelocityStartXYZ - mapWr.VelocityStartXYZ, 0); 109 | if (diff > 0) 110 | chatString.Append($" {CC.White}| WR: {CC.Secondary}+{diff} u/s"); 111 | else if (diff < 0) 112 | chatString.Append($" {CC.White}| WR: {CC.Secondary}{diff} u/s"); 113 | } 114 | 115 | 116 | player.PrintToChat(chatString.ToString()); 117 | 118 | } 119 | 120 | // Ok we are LEAVING A ZONE. But is it potentially a (subroute)? 121 | // So for this to be a stage the following must be true. 122 | 123 | // 1. We are in the main route. 124 | // 2. Our timer is going 125 | // 3. The zone we just left is also the start zone of a stage route 126 | 127 | if (playerInfo.RouteKey != "main") return; 128 | var playerTimer = entity.GetComponent(); 129 | if (playerTimer == null) return; 130 | var stageRoutes = mapZones.GetStageRoutes(); 131 | var stageRoute = stageRoutes.FirstOrDefault(x => x.Start.TargetName == e.TriggerName); 132 | if (stageRoute == null) return; 133 | 134 | // Ok now we also know we are doing a stage route. 135 | // We have to now check if we are going below the stage route's velocity cap. 136 | // If so we can start the subroute timer 137 | 138 | // This is purely visual (pinfo.SecondaryRoute), its so we can show the correct route in the hud while still in the 139 | // startzone or even if we didnt start the timer. 140 | playerInfo.ChangeSecondaryRoute(stageRoute.Key); 141 | 142 | var velocity = new Vector(player.PlayerPawn.Value.AbsVelocity.X, player.PlayerPawn.Value.AbsVelocity.Y, player.PlayerPawn.Value.AbsVelocity.Z); 143 | var velocityXY = velocity.Length2D(); 144 | if (velocityXY < stageRoute.VelocityCapStartXY) 145 | { 146 | // Ok we are going slow enough to start the subroute timer. 147 | var secondaryTimer = new TimerData 148 | { 149 | VelocityStartXY = velocityXY, 150 | VelocityStartZ = velocity.Z, 151 | VelocityStartXYZ = velocity.Length(), 152 | RouteKey = stageRoute.Key 153 | }; 154 | playerTimer.Secondary = secondaryTimer; 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /Systems/TurbomasterSystem.cs: -------------------------------------------------------------------------------- 1 | using CounterStrikeSharp.API; 2 | 3 | namespace WST; 4 | 5 | public class TurbomasterSystem : System 6 | { 7 | public const int FL_ONGROUND = 1 << 0; 8 | public TurbomasterSystem(EventManager eventManager, EntityManager entityManager, Database database) 9 | : base(eventManager, entityManager, database) 10 | { 11 | EventManager.Subscribe(OnTick); 12 | } 13 | 14 | private void OnTick(OnTickEvent e) 15 | { 16 | var entities = EntityManager.Entities(); 17 | 18 | foreach (var entity in entities) 19 | { 20 | var player = entity.GetPlayer(); 21 | if (!player.NativeIsValidAliveAndNotABot()) continue; 22 | var playerInfo = entity.GetComponent(); 23 | if (playerInfo == null) continue; 24 | if (playerInfo.Style != "tm") continue; 25 | if (!player.Pawn.IsValid) continue; 26 | var buttons = (PlayerButtons) player.Pawn.Value.MovementServices.Buttons.ButtonStates[0]; 27 | var flags = player.Pawn.Value.Flags; 28 | var moveType = player.Pawn.Value.MoveType; 29 | 30 | // if on ground do nothing 31 | if ((flags & FL_ONGROUND) != 0) continue; 32 | 33 | 34 | // todo 35 | 36 | var attack = PlayerButtons.Attack; 37 | var attack2 = PlayerButtons.Attack2; 38 | 39 | // if ((buttons & PlayerButtons.Attack2) == 0 && timer.TmInSlowMo) 40 | // { 41 | // timer.TmInSlowMo = false; 42 | // player.PlayerPawn.Value.GravityScale = 1f; 43 | // var currentSpeedXY = Math.Round(player.PlayerPawn.Value.AbsVelocity.Length2D()); 44 | // var currentSpeedZ = Math.Round(player.PlayerPawn.Value.AbsVelocity.Z); 45 | // var targetSpeed = currentSpeedXY * 10; 46 | // Utils.AdjustPlayerVelocity(player, Math.Min((float) targetSpeed, timer.TmXYSpeedBeforeSlowMo)); 47 | // player.PlayerPawn.Value.AbsVelocity.Z = (float) Math.Min(timer.TmZSpeedBeforeSlowMo, currentSpeedZ * 10); 48 | // timer.TmXYSpeedBeforeSlowMo = 0; 49 | // timer.TmZSpeedBeforeSlowMo = 0; 50 | // continue; 51 | // } 52 | // 53 | // if ((buttons & PlayerButtons.Attack2) != 0 && !timer.TmInSlowMo) 54 | // { 55 | // timer.TmInSlowMo = true; 56 | // player.PlayerPawn.Value.GravityScale = 0.02f; 57 | // var currentSpeedXY = Math.Round(player.PlayerPawn.Value.AbsVelocity.Length2D()); 58 | // var currentSpeedZ = Math.Round(player.PlayerPawn.Value.AbsVelocity.Z); 59 | // timer.TmXYSpeedBeforeSlowMo = (float) currentSpeedXY; 60 | // timer.TmZSpeedBeforeSlowMo = (float) currentSpeedZ; 61 | // var targetSpeed = currentSpeedXY / 10; 62 | // Utils.AdjustPlayerVelocity(player, (float) targetSpeed); 63 | // player.PlayerPawn.Value.AbsVelocity.Z = (float) currentSpeedZ / 10; 64 | // continue; 65 | // } 66 | 67 | if ((buttons & PlayerButtons.Attack2) != 0) 68 | { 69 | var currentSpeedZ = player.PlayerPawn.Value.AbsVelocity.Z; 70 | 71 | var targetSpeed = currentSpeedZ - 15; 72 | player.PlayerPawn.Value.AbsVelocity.Z = targetSpeed; 73 | continue; 74 | } 75 | 76 | if ((buttons & PlayerButtons.Attack) != 0) 77 | { 78 | var currentSpeedXY = Math.Round(player.PlayerPawn.Value.AbsVelocity.Length2D()); 79 | if (currentSpeedXY < 10) 80 | { 81 | continue; 82 | } 83 | 84 | var targetSpeed = currentSpeedXY + 10; 85 | Utils.AdjustPlayerVelocity(player, (float) targetSpeed); 86 | continue; 87 | } 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /Utils.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Reflection; 3 | using CounterStrikeSharp.API.Core; 4 | using CounterStrikeSharp.API.Modules.Utils; 5 | 6 | namespace WST; 7 | 8 | public static class Utils 9 | { 10 | public static string NativeSteamId3(this CCSPlayerController controller) 11 | { 12 | var steamId64 = controller.SteamID; 13 | var steamId32 = (steamId64 - 76561197960265728).ToString(); 14 | var steamId3 = $"[U:1:{steamId32}]"; 15 | return steamId3; 16 | } 17 | 18 | public static string SteamId3ToSteamId64(string steamId3) 19 | { 20 | steamId3 = steamId3.Replace("[", ""); 21 | steamId3 = steamId3.Replace("]", ""); 22 | var steamId3Split = steamId3.Split(":"); 23 | var steamId32 = int.Parse(steamId3Split[2]); 24 | var steamId64 = steamId32 + 76561197960265728; 25 | return steamId64.ToString(); 26 | } 27 | 28 | public static bool NativeIsValidAliveAndNotABot(this CCSPlayerController? controller) 29 | { 30 | return controller != null && controller is { IsValid: true, IsBot: false, PawnIsAlive: true }; 31 | } 32 | 33 | // public static string FormatTime(decimal time) 34 | // { 35 | // var doubleTime = (double)time; 36 | // double abs = Math.Abs(doubleTime); 37 | // int minutes = (int)(abs / 60); 38 | // int seconds = (int)(abs - minutes * 60); 39 | // int milliseconds = (int)((abs - Math.Floor(abs)) * 1000); 40 | // return string.Format("{0:D2}:{1:D2}:{2:D3}", minutes, seconds, milliseconds); 41 | // } 42 | 43 | public static string FormatTime(int ticks) 44 | { 45 | var timeSpan = TimeSpan.FromSeconds(Math.Abs(ticks) / 64.0); 46 | 47 | // Format seconds with three decimal points 48 | var secondsWithMilliseconds = $"{timeSpan.Seconds:D2}.{Math.Abs(ticks) % 64 * (1000.0 / 64.0):000}"; 49 | 50 | return $"{timeSpan.Minutes:D2}:{secondsWithMilliseconds}"; 51 | } 52 | 53 | public static string FormatTimeWithPlusOrMinus(int ticks) 54 | { 55 | var timeSpan = TimeSpan.FromSeconds(Math.Abs(ticks) / 64.0); 56 | 57 | // Format seconds with three decimal points 58 | var secondsWithMilliseconds = $"{timeSpan.Seconds:D2}.{Math.Abs(ticks) % 64 * (1000.0 / 64.0):000}"; 59 | 60 | return $"{(ticks > 0 ? "-" : "+")}{timeSpan.Minutes:D2}:{secondsWithMilliseconds}"; 61 | } 62 | 63 | public static float DistanceTo(this Vector v1, Vector v2) 64 | { 65 | var dx = v1.X - v2.X; 66 | var dy = v1.Y - v2.Y; 67 | var dz = v1.Z - v2.Z; 68 | 69 | return (float)Math.Sqrt(dx * dx + dy * dy + dz * dz); 70 | } 71 | 72 | public static void AdjustPlayerVelocity(CCSPlayerController? player, float velocity) 73 | { 74 | if (!player.NativeIsValidAliveAndNotABot()) return; 75 | 76 | var currentX = player.PlayerPawn.Value.AbsVelocity.X; 77 | var currentY = player.PlayerPawn.Value.AbsVelocity.Y; 78 | var currentSpeed2D = Math.Sqrt(currentX * currentX + currentY * currentY); 79 | var normalizedX = currentX / currentSpeed2D; 80 | var normalizedY = currentY / currentSpeed2D; 81 | var adjustedX = normalizedX * velocity; // Adjusted speed limit 82 | var adjustedY = normalizedY * velocity; // Adjusted speed limit 83 | player.PlayerPawn.Value.AbsVelocity.X = (float)adjustedX; 84 | player.PlayerPawn.Value.AbsVelocity.Y = (float)adjustedY; 85 | } 86 | 87 | 88 | public static string ColorNamesToTags(string message) 89 | { 90 | string modifiedValue = message; 91 | foreach (FieldInfo field in typeof(ChatColors).GetFields()) 92 | { 93 | string pattern = $"{{{field.Name}}}"; 94 | if (message.Contains(pattern, StringComparison.OrdinalIgnoreCase)) 95 | { 96 | modifiedValue = modifiedValue.Replace(pattern, field.GetValue(null)!.ToString(), StringComparison.OrdinalIgnoreCase); 97 | } 98 | } 99 | return modifiedValue; 100 | } 101 | 102 | // reverse of ReplaceTags 103 | public static string TagsToColorNames(string message) 104 | { 105 | string modifiedValue = message; 106 | foreach (FieldInfo field in typeof(ChatColors).GetFields()) 107 | { 108 | string pattern = field.GetValue(null)!.ToString(); 109 | if (message.Contains(pattern, StringComparison.OrdinalIgnoreCase)) 110 | { 111 | modifiedValue = modifiedValue.Replace(pattern, $"{{{field.Name}}}", StringComparison.OrdinalIgnoreCase); 112 | } 113 | } 114 | return modifiedValue; 115 | } 116 | 117 | public static string RemoveColorNames(string message) 118 | { 119 | string modifiedValue = message; 120 | foreach (FieldInfo field in typeof(ChatColors).GetFields()) 121 | { 122 | string pattern = $"{{{field.Name}}}"; 123 | if (message.Contains(pattern, StringComparison.OrdinalIgnoreCase)) 124 | { 125 | modifiedValue = modifiedValue.Replace(pattern, "", StringComparison.OrdinalIgnoreCase); 126 | } 127 | } 128 | return modifiedValue; 129 | } 130 | 131 | public static bool IsStringAColorName(string message) 132 | { 133 | foreach (FieldInfo field in typeof(ChatColors).GetFields()) 134 | { 135 | string pattern = $"{{{field.Name}}}"; 136 | // THE STRING MUST BE EXACTLY THE SAME AS THE FIELD NAME 137 | // {GOLD} == TRUE 138 | // {Gold} == TRUE 139 | // {gold}cat == FALSE 140 | Console.WriteLine(pattern); 141 | Console.WriteLine(message); 142 | if (message.Equals(pattern, StringComparison.OrdinalIgnoreCase)) 143 | { 144 | return true; 145 | } 146 | } 147 | return false; 148 | } 149 | } -------------------------------------------------------------------------------- /WST.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | true 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ..\..\..\..\cs2\game\csgo\addons\counterstrikesharp\api\CounterStrikeSharp.API.dll 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /WST.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ProjectDebugger 5 | 6 | 7 | WST 8 | 9 | -------------------------------------------------------------------------------- /Workshop.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using CounterStrikeSharp.API; 4 | 5 | namespace WST; 6 | 7 | 8 | public partial class GetPublishedFileDetails 9 | { 10 | [JsonPropertyName("response")] 11 | public GetPublishedFileDetailsResponse Response { get; set; } 12 | } 13 | 14 | public partial class GetPublishedFileDetailsResponse 15 | { 16 | [JsonPropertyName("result")] 17 | public long Result { get; set; } 18 | 19 | [JsonPropertyName("resultcount")] 20 | public long Resultcount { get; set; } 21 | 22 | [JsonPropertyName("publishedfiledetails")] 23 | public WorkshopMapInfo[] Publishedfiledetails { get; set; } 24 | } 25 | 26 | public partial class WorkshopMapInfo 27 | { 28 | [JsonPropertyName("publishedfileid")] 29 | public string Publishedfileid { get; set; } 30 | 31 | [JsonPropertyName("result")] 32 | public long Result { get; set; } 33 | 34 | [JsonPropertyName("creator")] 35 | public string Creator { get; set; } 36 | 37 | [JsonPropertyName("creator_app_id")] 38 | public long CreatorAppId { get; set; } 39 | 40 | [JsonPropertyName("consumer_app_id")] 41 | public long ConsumerAppId { get; set; } 42 | 43 | [JsonPropertyName("filename")] 44 | public string Filename { get; set; } 45 | 46 | // [JsonPropertyName("file_size")] 47 | // public long FileSize { get; set; } 48 | 49 | [JsonPropertyName("file_url")] 50 | public string FileUrl { get; set; } 51 | 52 | [JsonPropertyName("hcontent_file")] 53 | public string HcontentFile { get; set; } 54 | 55 | [JsonPropertyName("preview_url")] 56 | public Uri PreviewUrl { get; set; } 57 | 58 | [JsonPropertyName("hcontent_preview")] 59 | public string HcontentPreview { get; set; } 60 | 61 | [JsonPropertyName("title")] 62 | public string Title { get; set; } 63 | 64 | [JsonPropertyName("description")] 65 | public string Description { get; set; } 66 | 67 | [JsonPropertyName("time_created")] 68 | public long TimeCreated { get; set; } 69 | 70 | [JsonPropertyName("time_updated")] 71 | public long TimeUpdated { get; set; } 72 | 73 | [JsonPropertyName("visibility")] 74 | public long Visibility { get; set; } 75 | 76 | [JsonPropertyName("banned")] 77 | public long Banned { get; set; } 78 | 79 | [JsonPropertyName("ban_reason")] 80 | public string BanReason { get; set; } 81 | 82 | [JsonPropertyName("subscriptions")] 83 | public long Subscriptions { get; set; } 84 | 85 | [JsonPropertyName("favorited")] 86 | public long Favorited { get; set; } 87 | 88 | [JsonPropertyName("lifetime_subscriptions")] 89 | public long LifetimeSubscriptions { get; set; } 90 | 91 | [JsonPropertyName("lifetime_favorited")] 92 | public long LifetimeFavorited { get; set; } 93 | 94 | [JsonPropertyName("views")] 95 | public long Views { get; set; } 96 | 97 | [JsonPropertyName("tags")] 98 | public Tag[] Tags { get; set; } 99 | } 100 | 101 | public partial class Tag 102 | { 103 | [JsonPropertyName("tag")] 104 | public string TagTag { get; set; } 105 | } 106 | 107 | class WorkshopResponse 108 | { 109 | public WorkshopResponseResponse response { get; set; } 110 | } 111 | 112 | class WorkshopResponseResponse 113 | { 114 | public int result { get; set; } 115 | public int resultcount { get; set; } 116 | public WorkshopResponseCollectionDetails[] collectiondetails { get; set; } 117 | } 118 | 119 | class WorkshopResponseCollectionDetails 120 | { 121 | public string publishedfileid { get; set; } 122 | public int result { get; set; } 123 | public WorkshopResponseChild[] children { get; set; } 124 | } 125 | 126 | class WorkshopResponseChild 127 | { 128 | public string publishedfileid { get; set; } 129 | public int sortorder { get; set; } 130 | public int filetype { get; set; } 131 | } 132 | 133 | 134 | 135 | // { 136 | // "publishedfileid": "3130141240", 137 | // "result": 1, 138 | // "creator": "76561198068046327", 139 | // "creator_app_id": 730, 140 | // "consumer_app_id": 730, 141 | // "filename": "", 142 | // "file_size": 144348554, 143 | // "file_url": "", 144 | // "hcontent_file": "3932512874937450163", 145 | // "preview_url": "https://steamuserimages-a.akamaihd.net/ugc/2268189945234926418/1336E1D5FFA66B520FE03AD91A228913761C3345/", 146 | // "hcontent_preview": "2268189945234926418", 147 | // "title": "surf_atrium", 148 | // "description": "Newly created linear map\nDifficulty: Tier 1\nType: Linear\n\nCredits: itsTetrix, Breezy\n\nThis map has zone triggers adhering to the CS2Surf naming convention.\n\nCommands:\nsv_cheats 1;\nsv_falldamage_scale 0;\nsv_accelerate 10;\nsv_airaccelerate 850;\nsv_gravity 850.0;\nsv_enablebunnyhopping 1;\nsv_autobunnyhopping 1;\nsv_staminamax 0;\nsv_staminajumpcost 0;\nsv_staminalandcost 0;\nsv_staminarecoveryrate 0;\nmp_respawn_on_death_ct 1;\nmp_respawn_on_death_t 1;\nmp_roundtime 60;\nmp_round_restart_delay 0;\nmp_team_intro_time 0;\nmp_freezetime 0;\ncl_firstperson_legs 0;\nmp_warmup_end;\nmp_restartgame 1;", 149 | // "time_created": 1704272581, 150 | // "time_updated": 1704670678, 151 | // "visibility": 0, 152 | // "banned": 0, 153 | // "ban_reason": "", 154 | // "subscriptions": 3608, 155 | // "favorited": 32, 156 | // "lifetime_subscriptions": 3858, 157 | // "lifetime_favorited": 34, 158 | // "views": 1487, 159 | // "tags": [ 160 | // { 161 | // "tag": "Cs2" 162 | // }, 163 | // { 164 | // "tag": "Map" 165 | // }, 166 | // { 167 | // "tag": "Custom" 168 | // } 169 | // ] 170 | // } 171 | 172 | 173 | public class Workshop 174 | { 175 | public static async Task> LoadMapPool(string workshopId) 176 | { 177 | using (var client = new HttpClient()) 178 | { 179 | Console.WriteLine("Loading map pool"); 180 | 181 | var request = new HttpRequestMessage(HttpMethod.Post, "https://api.steampowered.com/ISteamRemoteStorage/GetCollectionDetails/v1/"); 182 | 183 | // Set content 184 | var content = new FormUrlEncodedContent(new[] 185 | { 186 | new KeyValuePair("key", Environment.GetEnvironmentVariable("STEAM_API_KEY")!), 187 | new KeyValuePair("collectioncount", "1"), 188 | new KeyValuePair("publishedfileids[0]", workshopId) 189 | }); 190 | request.Content = content; 191 | 192 | // Send request 193 | HttpResponseMessage response = await client.SendAsync(request); 194 | 195 | // Read response as json 196 | var responseString = await response.Content.ReadAsStringAsync(); 197 | var responseJson = JsonSerializer.Deserialize(responseString); 198 | 199 | // Get map ids 200 | var mapIds = responseJson.response.collectiondetails[0].children.Select(x => x.publishedfileid).ToList(); 201 | 202 | Console.WriteLine($"Found {mapIds.Count} maps"); 203 | 204 | // Get maps 205 | var mapRequest = new HttpRequestMessage(HttpMethod.Post, "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/"); 206 | 207 | 208 | var nameValueCollection = new List>(); 209 | nameValueCollection.Add(new KeyValuePair("key", Environment.GetEnvironmentVariable("STEAM_API_KEY")!)); 210 | nameValueCollection.Add(new KeyValuePair("itemcount", mapIds.Count.ToString())); 211 | 212 | for (var i = 0; i < mapIds.Count; i++) 213 | { 214 | nameValueCollection.Add(new KeyValuePair($"publishedfileids[{i}]", mapIds[i])); 215 | } 216 | // Set content 217 | var mapContent = new FormUrlEncodedContent(nameValueCollection); 218 | mapRequest.Content = mapContent; 219 | 220 | // Send request 221 | HttpResponseMessage mapResponse = await client.SendAsync(mapRequest); 222 | 223 | // Read response as json 224 | var mapResponseString = await mapResponse.Content.ReadAsStringAsync(); 225 | var mapResponseJson = JsonSerializer.Deserialize(mapResponseString); 226 | 227 | // Get maps 228 | var maps = mapResponseJson.Response.Publishedfiledetails.ToList(); 229 | 230 | foreach (var map in maps) 231 | { 232 | Console.WriteLine(map.Title); 233 | } 234 | 235 | // Get map info 236 | return maps; 237 | } 238 | } 239 | } -------------------------------------------------------------------------------- /cssharp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WST", "WST.csproj", "{F7BF26CF-9B7C-4A58-B10D-078ADC609C45}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {F7BF26CF-9B7C-4A58-B10D-078ADC609C45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {F7BF26CF-9B7C-4A58-B10D-078ADC609C45}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {F7BF26CF-9B7C-4A58-B10D-078ADC609C45}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {F7BF26CF-9B7C-4A58-B10D-078ADC609C45}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {2F1CF78B-7F21-479E-AD6B-729C3E8D7621} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | wst-database: 5 | image: postgres:14 6 | environment: 7 | POSTGRES_DB: wst 8 | POSTGRES_USER: wst 9 | POSTGRES_PASSWORD: password 10 | volumes: 11 | - wst-database:/var/lib/postgresql/data 12 | ports: 13 | - "5432:5432" 14 | expose: 15 | - 5432 16 | 17 | volumes: 18 | wst-database: 19 | -------------------------------------------------------------------------------- /local_build.ps1: -------------------------------------------------------------------------------- 1 | # PowerShell script to build a .NET project and copy specific files 2 | 3 | # Navigate to the project directory, if necessary 4 | Set-Location "C:\dev\wst\cs2-surftimer\cssharp" 5 | 6 | # Run dotnet build 7 | dotnet build WST.csproj 8 | 9 | $command = "C:\cs2\game\bin\win64\cs2.exe" 10 | 11 | # Check if build was successful 12 | if ($LASTEXITCODE -eq 0) { 13 | # copy these files 14 | # Npgsql.dll 15 | # dapper.dll 16 | # WST.dll 17 | 18 | 19 | $sourcePath = ".\bin\Debug\net7.0" 20 | $destinationPath = "C:\cs2\game\csgo\addons\counterstrikesharp\plugins\WST" 21 | $cfgPath = "C:\cs2\game\csgo\cfg\WST" 22 | 23 | # Create the destination directory if it doesn't exist 24 | if (!(Test-Path -Path $destinationPath)) { 25 | New-Item -ItemType Directory -Force -Path $destinationPath 26 | } 27 | 28 | if (!(Test-Path -Path $cfgPath)) { 29 | New-Item -ItemType Directory -Force -Path $cfgPath 30 | } 31 | 32 | # Copy the files 33 | Copy-Item -Path ".\server.json" -Destination $cfgPath 34 | 35 | Copy-Item -Path $sourcePath"\Npgsql.dll" -Destination $destinationPath 36 | Copy-Item -Path $sourcePath"\Dapper.dll" -Destination $destinationPath 37 | Copy-Item -Path $sourcePath"\WST.dll" -Destination $destinationPath 38 | Copy-Item -Path $sourcePath"\Supabase.dll" -Destination $destinationPath 39 | Copy-Item -Path $sourcePath"\Supabase.Core.dll" -Destination $destinationPath 40 | Copy-Item -Path $sourcePath"\Supabase.Storage.dll" -Destination $destinationPath 41 | Copy-Item -Path $sourcePath"\Supabase.Gotrue.dll" -Destination $destinationPath 42 | Copy-Item -Path $sourcePath"\Supabase.Realtime.dll" -Destination $destinationPath 43 | Copy-Item -Path $sourcePath"\Supabase.Postgrest.dll" -Destination $destinationPath 44 | Copy-Item -Path $sourcePath"\Supabase.Functions.dll" -Destination $destinationPath 45 | Copy-Item -Path $sourcePath"\Newtonsoft.Json.dll" -Destination $destinationPath 46 | Copy-Item -Path $sourcePath"\MimeMapping.dll" -Destination $destinationPath 47 | Copy-Item -Path $sourcePath"\Websocket.Client.dll" -Destination $destinationPath 48 | Copy-Item -Path $sourcePath"\System.Reactive.dll" -Destination $destinationPath 49 | 50 | 51 | Write-Host "Files copied successfully." 52 | 53 | # check if THERE ARE TWO cs2 processes running 54 | $isTwo = Get-Process -Name cs2 | Measure-Object | Select-Object -ExpandProperty Count 55 | # if its not then start it 56 | if (!($isTwo -eq 2)) { 57 | #Start-Process -FilePath $command -ArgumentList "-dedicated +map de_dust2 +host_workshop_map 3129698096" 58 | Start-Process -FilePath $command -ArgumentList "-dedicated +map de_dust2 +game_type 3 +game_mode 0 +host_workshop_collection 3132701467" 59 | } 60 | 61 | } else { 62 | Write-Host "Build failed. Files were not copied." 63 | } 64 | 65 | -------------------------------------------------------------------------------- /server.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "dev" 3 | } --------------------------------------------------------------------------------