diff --git a/src/ST-Events/Players.cs b/src/ST-Events/Players.cs
index 6364feb..5d442ac 100644
--- a/src/ST-Events/Players.cs
+++ b/src/ST-Events/Players.cs
@@ -114,7 +114,7 @@ public HookResult OnPlayerConnect(EventPlayerConnectFull @event, GameEventInfo i
// To-do: hardcoded Style value
// Load MapTimes for the player's PB and their Checkpoints
playerList[player.UserId ?? 0].Stats.LoadMapTimesData(playerList[player.UserId ?? 0], DB); // Will reload PB and Checkpoints for the player for all styles
- playerList[player.UserId ?? 0].Stats.PB[0].Checkpoint[0].LoadCheckpointsForRun(DB); // To-do: This really should go inside `LoadMapTimesData` imo cuz here we hardcoding load for Style 0 - regardless of index for `Checkpoint[X]` it will load all checkpoints
+ playerList[player.UserId ?? 0].Stats.LoadCheckpointsData(DB); // To-do: This really should go inside `LoadMapTimesData` imo cuz here we hardcoding load for Style 0
// Print join messages
Server.PrintToChatAll($"{PluginPrefix} {ChatColors.Green}{player.PlayerName}{ChatColors.Default} has connected from {ChatColors.Lime}{playerList[player.UserId ?? 0].Profile.Country}{ChatColors.Default}.");
diff --git a/src/ST-Events/TriggerEndTouch.cs b/src/ST-Events/TriggerEndTouch.cs
index 244ca68..73ced38 100644
--- a/src/ST-Events/TriggerEndTouch.cs
+++ b/src/ST-Events/TriggerEndTouch.cs
@@ -87,11 +87,11 @@ internal HookResult OnTriggerEndTouch(DynamicHook handler)
#endif
// Update the values
- currentCheckpoint.CpEndVelX = velocity_x;
- currentCheckpoint.CpEndVelY = velocity_y;
- currentCheckpoint.CpEndVelZ = velocity_z;
- currentCheckpoint.CpEndTouch = player.Timer.Ticks; // To-do: what type of value we store in DB ?
- currentCheckpoint.CpAttempts += 1;
+ currentCheckpoint.EndVelX = velocity_x;
+ currentCheckpoint.EndVelY = velocity_y;
+ currentCheckpoint.EndVelZ = velocity_z;
+ currentCheckpoint.EndTouch = player.Timer.Ticks; // To-do: what type of value we store in DB ?
+ currentCheckpoint.Attempts += 1;
// Show Prespeed for stages - will be enabled/disabled by the user?
player.Controller.PrintToCenter($"Stage {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} - Prespeed: {velocity.ToString("0")} u/s");
@@ -121,11 +121,11 @@ internal HookResult OnTriggerEndTouch(DynamicHook handler)
#endif
// Update the values
- currentCheckpoint.CpEndVelX = velocity_x;
- currentCheckpoint.CpEndVelY = velocity_y;
- currentCheckpoint.CpEndVelZ = velocity_z;
- currentCheckpoint.CpEndTouch = player.Timer.Ticks; // To-do: what type of value we store in DB ?
- currentCheckpoint.CpAttempts += 1;
+ currentCheckpoint.EndVelX = velocity_x;
+ currentCheckpoint.EndVelY = velocity_y;
+ currentCheckpoint.EndVelZ = velocity_z;
+ currentCheckpoint.EndTouch = player.Timer.Ticks; // To-do: what type of value we store in DB ?
+ currentCheckpoint.Attempts += 1;
// Show Prespeed for stages - will be enabled/disabled by the user?
player.Controller.PrintToCenter($"Checkpoint {Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value} - Prespeed: {velocity.ToString("0")} u/s");
diff --git a/src/ST-Events/TriggerStartTouch.cs b/src/ST-Events/TriggerStartTouch.cs
index 7348c7b..a5ba595 100644
--- a/src/ST-Events/TriggerStartTouch.cs
+++ b/src/ST-Events/TriggerStartTouch.cs
@@ -86,10 +86,10 @@ internal HookResult OnTriggerStartTouch(DynamicHook handler)
#endif
// Add entry in DB for the run
- player.Stats.PB[0].SaveMapTime(player, DB); // Save the MapTime PB data
+ player.Stats.ThisRun.SaveMapTime(player, DB); // Save the MapTime PB data
player.Stats.LoadMapTimesData(player, DB); // Load the MapTime PB data again (will refresh the MapTime ID for the Checkpoints query)
- player.Stats.PB[0].Checkpoint[0].SaveCurrentRunCheckpoints(player, DB); // Save the Checkpoints PB data
- player.Stats.PB[0].Checkpoint[0].LoadCheckpointsForRun(DB); // Reload checkpoints for the run - we should really have this in `SaveMapTime` as well but we don't re-load PB data inside there so we need to do it here
+ player.Stats.ThisRun.SaveCurrentRunCheckpoints(player, DB); // Save this run's checkpoints
+ player.Stats.LoadCheckpointsData(DB); // Reload checkpoints for the run - we should really have this in `SaveMapTime` as well but we don't re-load PB data inside there so we need to do it here
CurrentMap.GetMapRecordAndTotals(DB); // Reload the Map record and totals for the HUD
}
diff --git a/src/ST-Map/Map.cs b/src/ST-Map/Map.cs
index 9a026b7..0ad0d13 100644
--- a/src/ST-Map/Map.cs
+++ b/src/ST-Map/Map.cs
@@ -56,14 +56,26 @@ internal Map(string Name, TimerDatabase DB)
trigger.Entity!.Name.Contains("stage1_start") ||
trigger.Entity!.Name.Contains("s1_start"))
{
- this.StartZone = new Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z);
+ bool foundPlayerSpawn = false; // Track whether a player spawn is found
foreach (CBaseEntity teleport in teleports)
{
- if (teleport.Entity!.Name != null && IsInZone(trigger.AbsOrigin!, trigger.Collision.BoundingRadius, teleport.AbsOrigin!))
+ if (teleport.Entity!.Name != null &&
+ (IsInZone(trigger.AbsOrigin!, trigger.Collision.BoundingRadius, teleport.AbsOrigin!) ||
+ teleport.Entity!.Name.Contains("spawn_map_start") ||
+ teleport.Entity!.Name.Contains("spawn_stage1_start") ||
+ teleport.Entity!.Name.Contains("spawn_s1_start")))
{
+ this.StartZone = new Vector(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z);
this.StartZoneAngles = new QAngle(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z);
+ foundPlayerSpawn = true;
+ break;
}
}
+
+ if (!foundPlayerSpawn)
+ {
+ this.StartZone = new Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z);
+ }
}
// Map end zone
@@ -75,17 +87,27 @@ internal Map(string Name, TimerDatabase DB)
// Stage start zones
else if (Regex.Match(trigger.Entity.Name, "^s([1-9][0-9]?|tage[1-9][0-9]?)_start$").Success)
{
- this.StageStartZone[Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value) - 1] = new Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z);
-
+ int stage = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value);
+
// Find an info_destination_teleport inside this zone to grab angles from
+ bool foundPlayerSpawn = false; // Track whether a player spawn is found
foreach (CBaseEntity teleport in teleports)
{
- if (teleport.Entity!.Name != null && IsInZone(trigger.AbsOrigin!, trigger.Collision.BoundingRadius, teleport.AbsOrigin!))
+ if (teleport.Entity!.Name != null &&
+ (IsInZone(trigger.AbsOrigin!, trigger.Collision.BoundingRadius, teleport.AbsOrigin!) || (Regex.Match(teleport.Entity.Name, "^spawn_s([1-9][0-9]?|tage[1-9][0-9]?)_start$").Success && Int32.Parse(Regex.Match(teleport.Entity.Name, "[0-9][0-9]?").Value) == stage)))
{
- this.StageStartZoneAngles[Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value) - 1] = new QAngle(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z);
+ this.StageStartZone[stage - 1] = new Vector(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z);
+ this.StageStartZoneAngles[stage - 1] = new QAngle(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z);
this.Stages++; // Count stage zones for the map to populate DB
+ foundPlayerSpawn = true;
+ break;
}
}
+
+ if (!foundPlayerSpawn)
+ {
+ this.StageStartZone[stage - 1] = new Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z);
+ }
}
// Checkpoint start zones (linear maps)
@@ -93,23 +115,32 @@ internal Map(string Name, TimerDatabase DB)
{
this.CheckpointStartZone[Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value) - 1] = new Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z);
this.Checkpoints++; // Might be useful to have this in DB entry
- // Do we need `info_destination_teleport` data for Checkpoint zones?
}
// Bonus start zones
else if (Regex.Match(trigger.Entity.Name, "^b([1-9][0-9]?|onus[1-9][0-9]?)_start$").Success)
{
- this.BonusStartZone[Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value) - 1] = new Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z);
+ int bonus = Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value);
// Find an info_destination_teleport inside this zone to grab angles from
+ bool foundPlayerSpawn = false; // Track whether a player spawn is found
foreach (CBaseEntity teleport in teleports)
{
- if (teleport.Entity!.Name != null && IsInZone(trigger.AbsOrigin!, trigger.Collision.BoundingRadius, teleport.AbsOrigin!))
+ if (teleport.Entity!.Name != null &&
+ (IsInZone(trigger.AbsOrigin!, trigger.Collision.BoundingRadius, teleport.AbsOrigin!) || (Regex.Match(teleport.Entity.Name, "^spawn_b([1-9][0-9]?|onus[1-9][0-9]?)_start$").Success && Int32.Parse(Regex.Match(teleport.Entity.Name, "[0-9][0-9]?").Value) == bonus)))
{
- this.BonusStartZoneAngles[Int32.Parse(Regex.Match(trigger.Entity.Name, "[0-9][0-9]?").Value) - 1] = new QAngle(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z);
+ this.BonusStartZone[bonus - 1] = new Vector(teleport.AbsOrigin!.X, teleport.AbsOrigin!.Y, teleport.AbsOrigin!.Z);
+ this.BonusStartZoneAngles[bonus - 1] = new QAngle(teleport.AbsRotation!.X, teleport.AbsRotation!.Y, teleport.AbsRotation!.Z);
this.Bonuses++; // Count bonus zones for the map to populate DB
+ foundPlayerSpawn = true;
+ break;
}
}
+
+ if (!foundPlayerSpawn)
+ {
+ this.BonusStartZone[bonus - 1] = new Vector(trigger.AbsOrigin!.X, trigger.AbsOrigin!.Y, trigger.AbsOrigin!.Z);
+ }
}
else if (Regex.Match(trigger.Entity.Name, "^b([1-9][0-9]?|onus[1-9][0-9]?)_end$").Success)
diff --git a/src/ST-Player/PlayerHUD.cs b/src/ST-Player/PlayerHUD.cs
index dfe64ff..321f512 100644
--- a/src/ST-Player/PlayerHUD.cs
+++ b/src/ST-Player/PlayerHUD.cs
@@ -119,10 +119,10 @@ public void DisplayCheckpointMessages(string PluginPrefix) // To-do: PluginPrefi
// Can check checkpoints count instead of try/catch
try
{
- pbTime = _player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].CpTicks;
- pbSpeed = (float)Math.Sqrt(_player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].CpStartVelX * _player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].CpStartVelX
- + _player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].CpStartVelY * _player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].CpStartVelY
- + _player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].CpStartVelZ * _player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].CpStartVelZ);
+ pbTime = _player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].Ticks;
+ pbSpeed = (float)Math.Sqrt(_player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].StartVelX * _player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].StartVelX
+ + _player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].StartVelY * _player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].StartVelY
+ + _player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].StartVelZ * _player.Stats.PB[0].Checkpoint[_player.Timer.Checkpoint].StartVelZ);
#if DEBUG
Console.WriteLine($"CS2 Surf DEBUG >> DisplayCheckpointMessages -> [TIME] Got pbTime from _player.Stats.PB[0].Checkpoint[{_player.Timer.Checkpoint} = {pbTime}]");
diff --git a/src/ST-Player/PlayerStats.cs b/src/ST-Player/PlayerStats.cs
deleted file mode 100644
index cc2435a..0000000
--- a/src/ST-Player/PlayerStats.cs
+++ /dev/null
@@ -1,330 +0,0 @@
-using MySqlConnector;
-
-namespace SurfTimer;
-
-///
-/// This class stores data for the current run.
-///
-internal class CurrentRun
-{
- public Dictionary Checkpoint { get; set; } // Current RUN checkpoints tracker
- public int Ticks { get; set; } // To-do: will be the last (any) zone end touch time
- public float StartVelX { get; set; } // This will store MAP START VELOCITY X
- public float StartVelY { get; set; } // This will store MAP START VELOCITY Y
- public float StartVelZ { get; set; } // This will store MAP START VELOCITY Z
- public float EndVelX { get; set; } // This will store MAP END VELOCITY X
- public float EndVelY { get; set; } // This will store MAP END VELOCITY Y
- public float EndVelZ { get; set; } // This will store MAP END VELOCITY Z
- public int RunDate { get; set; }
- // Add other properties as needed
-
- // Constructor
- public CurrentRun()
- {
- Checkpoint = new Dictionary();
- Ticks = 0;
- StartVelX = 0.0f;
- StartVelY = 0.0f;
- StartVelZ = 0.0f;
- EndVelX = 0.0f;
- EndVelY = 0.0f;
- EndVelZ = 0.0f;
- RunDate = 0;
- }
-
- public void Reset()
- {
- Checkpoint.Clear();
- Ticks = 0;
- StartVelX = 0.0f;
- StartVelY = 0.0f;
- StartVelZ = 0.0f;
- EndVelX = 0.0f;
- EndVelY = 0.0f;
- EndVelZ = 0.0f;
- RunDate = 0;
- // Reset other properties as needed
- }
-}
-
-internal class Checkpoint : PersonalBest
-{
- public int CP { get; set; }
- public int CpTicks { get; set; } // To-do: this was supposed to be the ticks but that is used for run_time for HUD????
- public float CpStartVelX { get; set; }
- public float CpStartVelY { get; set; }
- public float CpStartVelZ { get; set; }
- public float CpEndVelX { get; set; }
- public float CpEndVelY { get; set; }
- public float CpEndVelZ { get; set; }
- public float CpEndTouch { get; set; }
- public int CpAttempts { get; set; }
-
- public Checkpoint(int cp, int ticks, float startVelX, float startVelY, float startVelZ, float endVelX, float endVelY, float endVelZ, float endTouch, int attempts)
- {
- CP = cp;
- CpTicks = ticks; // To-do: this was supposed to be the ticks but that is used for run_time for HUD????
- CpStartVelX = startVelX;
- CpStartVelY = startVelY;
- CpStartVelZ = startVelZ;
- CpEndVelX = endVelX;
- CpEndVelY = endVelY;
- CpEndVelZ = endVelZ;
- CpEndTouch = endTouch;
- CpAttempts = attempts;
- }
-
- ///
- /// Executes the DB query to get all the checkpoints and store them in the Checkpoint dictionary
- ///
- public void LoadCheckpointsForRun(TimerDatabase DB)
- {
- Task dbTask = DB.Query($"SELECT * FROM `Checkpoints` WHERE `maptime_id` = {this.ID};");
- MySqlDataReader results = dbTask.Result;
- if (this == null)
- {
- #if DEBUG
- Console.WriteLine("CS2 Surf ERROR >> internal class Checkpoint : PersonalBest -> LoadCheckpointsForRun -> PersonalBest object is null.");
- #endif
-
- results.Close();
- return;
- }
-
- if (this.Checkpoint == null)
- {
- #if DEBUG
- Console.WriteLine($"CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsForRun -> Checkpoints list is not initialized.");
- #endif
-
- this.Checkpoint = new Dictionary(); // Initialize if null
- }
-
- #if DEBUG
- Console.WriteLine($"this.Checkpoint.Count {this.Checkpoint.Count} ");
- Console.WriteLine($"this.ID {this.ID} ");
- Console.WriteLine($"this.Ticks {this.Ticks} ");
- Console.WriteLine($"this.RunDate {this.RunDate} ");
- #endif
-
- if (!results.HasRows)
- {
- #if DEBUG
- Console.WriteLine($"CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsForRun -> No checkpoints found for this mapTimeId {this.ID}.");
- #endif
-
- results.Close();
- return;
- }
-
- #if DEBUG
- Console.WriteLine($"======== CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsForRun -> Checkpoints found for this mapTimeId");
- #endif
-
- while (results.Read())
- {
- #if DEBUG
- Console.WriteLine($"cp {results.GetInt32("cp")} ");
- Console.WriteLine($"run_time {results.GetFloat("run_time")} ");
- Console.WriteLine($"sVelX {results.GetFloat("start_vel_x")} ");
- Console.WriteLine($"sVelY {results.GetFloat("start_vel_y")} ");
- #endif
-
- Checkpoint cp = new(results.GetInt32("cp"),
- results.GetInt32("run_time"), // To-do: what type of value we use here? DB uses DECIMAL but `.Tick` is int???
- results.GetFloat("start_vel_x"),
- results.GetFloat("start_vel_y"),
- results.GetFloat("start_vel_z"),
- results.GetFloat("end_vel_x"),
- results.GetFloat("end_vel_y"),
- results.GetFloat("end_vel_z"),
- results.GetFloat("end_touch"),
- results.GetInt32("attempts"));
- cp.ID = results.GetInt32("cp");
- // To-do: cp.ID = calculate Rank # from DB
-
- Checkpoint[cp.CP] = cp;
-
- #if DEBUG
- Console.WriteLine($"======= CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsForRun -> Loaded CP {cp.CP} with RunTime {cp.Ticks}.");
- #endif
- }
- results.Close();
-
- #if DEBUG
- Console.WriteLine($"======= CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsForRun -> Checkpoints loaded from DB. Count: {Checkpoint.Count}");
- #endif
- }
-
- ///
- /// Saves the `CurrentRunCheckpoints` dictionary to the database
- /// We need the correct `this.ID` to be populated before calling this method otherwise Query will fail
- ///
- public void SaveCurrentRunCheckpoints(Player player, TimerDatabase DB) // To-do: Transactions? Player sometimes rubberbands for a bit here
- {
- // Loop through the checkpoints and insert/update them in the database for the run
- foreach (var item in player.Stats.ThisRun.Checkpoint)
- {
- int cp = item.Key;
- int ticks = item.Value.CpTicks;
- int runTime = item.Value.CpTicks / 64; // Runtime in decimal
- double startVelX = item.Value.CpStartVelX;
- double startVelY = item.Value.CpStartVelY;
- double startVelZ = item.Value.CpStartVelZ;
- double endVelX = item.Value.CpEndVelX;
- double endVelY = item.Value.CpEndVelY;
- double endVelZ = item.Value.CpEndVelZ;
- int attempts = item.Value.CpAttempts;
-
- #if DEBUG
- Console.WriteLine($"CP: {cp} | MapTime ID: {this.ID} | Time: {runTime} | Ticks: {ticks} | startVelX: {startVelX} | startVelY: {startVelY} | startVelZ: {startVelZ} | endVelX: {endVelX} | endVelY: {endVelY} | endVelZ: {endVelZ}");
- Console.WriteLine($"CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> SaveCurrentRunCheckpoints -> " +
- $"INSERT INTO `Checkpoints` " +
- $"(`maptime_id`, `cp`, `run_time`, `start_vel_x`, `start_vel_y`, `start_vel_z`, " +
- $"`end_vel_x`, `end_vel_y`, `end_vel_z`, `attempts`, `end_touch`) " +
- $"VALUES ({this.ID}, {cp}, {runTime}, {startVelX}, {startVelY}, {startVelZ}, {endVelX}, {endVelY}, {endVelZ}, {attempts}, {ticks}) ON DUPLICATE KEY UPDATE " +
- $"run_time=VALUES(run_time), start_vel_x=VALUES(start_vel_x), start_vel_y=VALUES(start_vel_y), start_vel_z=VALUES(start_vel_z), " +
- $"end_vel_x=VALUES(end_vel_x), end_vel_y=VALUES(end_vel_y), end_vel_z=VALUES(end_vel_z), attempts=VALUES(attempts), end_touch=VALUES(end_touch);");
- #endif
-
- // Insert/Update CPs to database
- // To-do: Transactions?
- // Check if the player has PB object initialized and if the player's character is currently active in the game
- if (this != null && player.Controller.PlayerPawn.Value != null)
- {
- Task newPbTask = DB.Write($"INSERT INTO `Checkpoints` " +
- $"(`maptime_id`, `cp`, `run_time`, `start_vel_x`, `start_vel_y`, `start_vel_z`, " +
- $"`end_vel_x`, `end_vel_y`, `end_vel_z`, `attempts`, `end_touch`) " +
- $"VALUES ({this.ID}, {cp}, {runTime}, {startVelX}, {startVelY}, {startVelZ}, {endVelX}, {endVelY}, {endVelZ}, {attempts}, {ticks}) " +
- $"ON DUPLICATE KEY UPDATE " +
- $"run_time=VALUES(run_time), start_vel_x=VALUES(start_vel_x), start_vel_y=VALUES(start_vel_y), start_vel_z=VALUES(start_vel_z), " +
- $"end_vel_x=VALUES(end_vel_x), end_vel_y=VALUES(end_vel_y), end_vel_z=VALUES(end_vel_z), attempts=VALUES(attempts), end_touch=VALUES(end_touch);");
- if (newPbTask.Result <= 0)
- throw new Exception($"CS2 Surf ERROR >> internal class Checkpoint : PersonalBest -> SaveCurrentRunCheckpoints -> Inserting Checkpoints. CP: {cp} | Name: {player.Profile.Name}");
- newPbTask.Dispose();
- }
- }
- player.Stats.ThisRun.Checkpoint.Clear();
- }
-
-}
-
-// To-do: make Style (currently 0) be dynamic
-// To-do: add `Type`
-internal class PersonalBest
-{
- public int ID { get; set; } = -1; // Exclude from constructor, retrieve from Database when loading/saving
- public int Ticks { get; set; }
- public int Rank { get; set; } = -1; // Exclude from constructor, retrieve from Database when loading/saving
- public Dictionary Checkpoint { get; set; }
- // public int Type { get; set; }
- public float StartVelX { get; set; }
- public float StartVelY { get; set; }
- public float StartVelZ { get; set; }
- public float EndVelX { get; set; }
- public float EndVelY { get; set; }
- public float EndVelZ { get; set; }
- public int RunDate { get; set; }
- // Add other properties as needed
-
- // Constructor
- public PersonalBest()
- {
- Ticks = -1; // To-do: what type of value we use here? DB uses DECIMAL but `.Tick` is int???
- Checkpoint = new Dictionary();
- // Type = type;
- StartVelX = -1.0f;
- StartVelY = -1.0f;
- StartVelZ = -1.0f;
- EndVelX = -1.0f;
- EndVelY = -1.0f;
- EndVelZ = -1.0f;
- RunDate = 0;
- }
-
- ///
- /// Saves the player's run to the database and reloads the data for the player.
- /// NOTE: Not re-loading any data at this point as we need `LoadMapTimesData` to be called from here as well, otherwise we may not have the `this.ID` populated
- ///
- public void SaveMapTime(Player player, TimerDatabase DB, int mapId = 0)
- {
- // Add entry in DB for the run
- // To-do: add `type`
- Task updatePlayerRunTask = DB.Write($"INSERT INTO `MapTimes` " +
- $"(`player_id`, `map_id`, `style`, `type`, `stage`, `run_time`, `start_vel_x`, `start_vel_y`, `start_vel_z`, `end_vel_x`, `end_vel_y`, `end_vel_z`, `run_date`) " +
- $"VALUES ({player.Profile.ID}, {player.CurrMap.ID}, 0, 0, 0, {this.Ticks}, " +
- $"{player.Stats.ThisRun.StartVelX}, {player.Stats.ThisRun.StartVelY}, {player.Stats.ThisRun.StartVelZ}, {player.Stats.ThisRun.EndVelX}, {player.Stats.ThisRun.EndVelY}, {player.Stats.ThisRun.EndVelZ}, {(int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()}) " +
- $"ON DUPLICATE KEY UPDATE run_time=VALUES(run_time), start_vel_x=VALUES(start_vel_x), start_vel_y=VALUES(start_vel_y), " +
- $"start_vel_z=VALUES(start_vel_z), end_vel_x=VALUES(end_vel_x), end_vel_y=VALUES(end_vel_y), end_vel_z=VALUES(end_vel_z), run_date=VALUES(run_date);");
- if (updatePlayerRunTask.Result <= 0)
- throw new Exception($"CS2 Surf ERROR >> internal class PersonalBest -> SaveMapTime -> Failed to insert/update player run in database. Player: {player.Profile.Name} ({player.Profile.SteamID})");
- updatePlayerRunTask.Dispose();
-
- // Will have to LoadMapTimesData right here as well to get the ID of the run we just inserted
- // this.SaveCurrentRunCheckpoints(player, DB); // Save checkpoints for this run
- // this.LoadCheckpointsForRun(DB); // Re-Load checkpoints for this run
- }
-}
-
-internal class PlayerStats
-{
- // To-Do: Each stat should be a class of its own, with its own methods and properties - easier to work with.
- // Temporarily, we store ticks + basic info so we can experiment
- // These account for future style support and a relevant index.
- public int[,] StagePB { get; set; } = { { 0, 0 } }; // First dimension: style (0 = normal), second dimension: stage index
- public int[,] StageRank { get; set; } = { { 0, 0 } }; // First dimension: style (0 = normal), second dimension: stage index
- //
-
- public Dictionary PB { get; set; } = new Dictionary();
- public CurrentRun ThisRun { get; set; } = new CurrentRun(); // This is a CurrenntRun object that tracks the data for the Player's current run
- // Initialize PersonalBest for each `style` (e.g., 0 for normal) - this is a temporary solution
- // Here we can loop through all available styles at some point and initialize them
- public PlayerStats()
- {
- PB[0] = new PersonalBest();
- // Add more styles as needed
- }
-
- ///
- /// Loads the player's MapTimes data from the database along with `Rank` for the run.
- /// `Checkpoints` are loaded separately because inside the while loop we cannot run queries.
- /// This can populate all the `style` stats the player has for the map - currently only 1 style is supported
- ///
- public void LoadMapTimesData(Player player, TimerDatabase DB, int playerId = 0, int mapId = 0)
- {
- Task dbTask2 = DB.Query($"SELECT mainquery.*, (SELECT COUNT(*) FROM `MapTimes` AS subquery " +
- $"WHERE subquery.`map_id` = mainquery.`map_id` AND subquery.`style` = mainquery.`style` " +
- $"AND subquery.`run_time` <= mainquery.`run_time`) AS `rank` FROM `MapTimes` AS mainquery " +
- $"WHERE mainquery.`player_id` = {player.Profile.ID} AND mainquery.`map_id` = {player.CurrMap.ID}; ");
- MySqlDataReader playerStats = dbTask2.Result;
- int style = 0; // To-do: implement styles
- if (!playerStats.HasRows)
- {
- Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadMapTimesData -> No MapTimes data found for Player.");
- }
- else
- {
- while (playerStats.Read())
- {
- // Load data into PersonalBest object
- // style = playerStats.GetInt32("style"); // Uncomment when style is implemented
- PB[style].ID = playerStats.GetInt32("id");
- PB[style].StartVelX = (float)playerStats.GetDouble("start_vel_x");
- PB[style].StartVelY = (float)playerStats.GetDouble("start_vel_y");
- PB[style].StartVelZ = (float)playerStats.GetDouble("start_vel_z");
- PB[style].EndVelX = (float)playerStats.GetDouble("end_vel_x");
- PB[style].EndVelY = (float)playerStats.GetDouble("end_vel_y");
- PB[style].EndVelZ = (float)playerStats.GetDouble("end_vel_z");
- PB[style].Ticks = playerStats.GetInt32("run_time");
- PB[style].RunDate = playerStats.GetInt32("run_date");
- PB[style].Rank = playerStats.GetInt32("rank");
-
- Console.WriteLine($"============== CS2 Surf DEBUG >> LoadMapTimesData -> PlayerID: {player.Profile.ID} | Rank: {PB[style].Rank} | ID: {PB[style].ID} | RunTime: {PB[style].Ticks} | SVX: {PB[style].StartVelX} | SVY: {PB[style].StartVelY} | SVZ: {PB[style].StartVelZ} | EVX: {PB[style].EndVelX} | EVY: {PB[style].EndVelY} | EVZ: {PB[style].EndVelZ} | Run Date (UNIX): {PB[style].RunDate}");
- #if DEBUG
- Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadMapTimesData -> PlayerStats.PB (ID {PB[style].ID}) loaded from DB.");
- #endif
- }
- }
- playerStats.Close();
- }
-}
\ No newline at end of file
diff --git a/src/ST-Player/PlayerStats/Checkpoint.cs b/src/ST-Player/PlayerStats/Checkpoint.cs
new file mode 100644
index 0000000..30921f5
--- /dev/null
+++ b/src/ST-Player/PlayerStats/Checkpoint.cs
@@ -0,0 +1,24 @@
+using MySqlConnector;
+
+namespace SurfTimer;
+
+internal class Checkpoint : PersonalBest
+{
+ public int CP { get; set; }
+ public float EndTouch { get; set; }
+ public int Attempts { get; set; }
+
+ public Checkpoint(int cp, int ticks, float startVelX, float startVelY, float startVelZ, float endVelX, float endVelY, float endVelZ, float endTouch, int attempts)
+ {
+ CP = cp;
+ Ticks = ticks; // To-do: this was supposed to be the ticks but that is used for run_time for HUD????
+ StartVelX = startVelX;
+ StartVelY = startVelY;
+ StartVelZ = startVelZ;
+ EndVelX = endVelX;
+ EndVelY = endVelY;
+ EndVelZ = endVelZ;
+ EndTouch = endTouch;
+ Attempts = attempts;
+ }
+}
\ No newline at end of file
diff --git a/src/ST-Player/PlayerStats/CurrentRun.cs b/src/ST-Player/PlayerStats/CurrentRun.cs
new file mode 100644
index 0000000..3ddf620
--- /dev/null
+++ b/src/ST-Player/PlayerStats/CurrentRun.cs
@@ -0,0 +1,122 @@
+namespace SurfTimer;
+
+///
+/// This class stores data for the current run.
+///
+internal class CurrentRun
+{
+ public Dictionary Checkpoint { get; set; } // Current RUN checkpoints tracker
+ public int Ticks { get; set; } // To-do: will be the last (any) zone end touch time
+ public float StartVelX { get; set; } // This will store MAP START VELOCITY X
+ public float StartVelY { get; set; } // This will store MAP START VELOCITY Y
+ public float StartVelZ { get; set; } // This will store MAP START VELOCITY Z
+ public float EndVelX { get; set; } // This will store MAP END VELOCITY X
+ public float EndVelY { get; set; } // This will store MAP END VELOCITY Y
+ public float EndVelZ { get; set; } // This will store MAP END VELOCITY Z
+ public int RunDate { get; set; }
+ // Add other properties as needed
+
+ // Constructor
+ public CurrentRun()
+ {
+ Checkpoint = new Dictionary();
+ Ticks = 0;
+ StartVelX = 0.0f;
+ StartVelY = 0.0f;
+ StartVelZ = 0.0f;
+ EndVelX = 0.0f;
+ EndVelY = 0.0f;
+ EndVelZ = 0.0f;
+ RunDate = 0;
+ }
+
+ public void Reset()
+ {
+ Checkpoint.Clear();
+ Ticks = 0;
+ StartVelX = 0.0f;
+ StartVelY = 0.0f;
+ StartVelZ = 0.0f;
+ EndVelX = 0.0f;
+ EndVelY = 0.0f;
+ EndVelZ = 0.0f;
+ RunDate = 0;
+ // Reset other properties as needed
+ }
+
+ ///
+ /// Saves the player's run to the database and reloads the data for the player.
+ /// NOTE: Not re-loading any data at this point as we need `LoadMapTimesData` to be called from here as well, otherwise we may not have the `this.ID` populated
+ ///
+ public void SaveMapTime(Player player, TimerDatabase DB, int mapId = 0) // To-do: Styles
+ {
+ // Add entry in DB for the run
+ // To-do: add `type`
+ Task updatePlayerRunTask = DB.Write($"INSERT INTO `MapTimes` " +
+ $"(`player_id`, `map_id`, `style`, `type`, `stage`, `run_time`, `start_vel_x`, `start_vel_y`, `start_vel_z`, `end_vel_x`, `end_vel_y`, `end_vel_z`, `run_date`) " +
+ $"VALUES ({player.Profile.ID}, {player.CurrMap.ID}, 0, 0, 0, {this.Ticks}, " +
+ $"{player.Stats.ThisRun.StartVelX}, {player.Stats.ThisRun.StartVelY}, {player.Stats.ThisRun.StartVelZ}, {player.Stats.ThisRun.EndVelX}, {player.Stats.ThisRun.EndVelY}, {player.Stats.ThisRun.EndVelZ}, {(int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()}) " +
+ $"ON DUPLICATE KEY UPDATE run_time=VALUES(run_time), start_vel_x=VALUES(start_vel_x), start_vel_y=VALUES(start_vel_y), " +
+ $"start_vel_z=VALUES(start_vel_z), end_vel_x=VALUES(end_vel_x), end_vel_y=VALUES(end_vel_y), end_vel_z=VALUES(end_vel_z), run_date=VALUES(run_date);");
+ if (updatePlayerRunTask.Result <= 0)
+ throw new Exception($"CS2 Surf ERROR >> internal class PersonalBest -> SaveMapTime -> Failed to insert/update player run in database. Player: {player.Profile.Name} ({player.Profile.SteamID})");
+ updatePlayerRunTask.Dispose();
+
+ // Will have to LoadMapTimesData right here as well to get the ID of the run we just inserted
+ // this.SaveCurrentRunCheckpoints(player, DB); // Save checkpoints for this run
+ // this.LoadCheckpointsForRun(DB); // Re-Load checkpoints for this run
+ }
+
+ ///
+ /// Saves the `CurrentRunCheckpoints` dictionary to the database
+ /// We need the correct `this.ID` to be populated before calling this method otherwise Query will fail
+ ///
+ public void SaveCurrentRunCheckpoints(Player player, TimerDatabase DB) // To-do: Transactions? Player sometimes rubberbands for a bit here
+ {
+ // Loop through the checkpoints and insert/update them in the database for the run
+ foreach (var item in player.Stats.ThisRun.Checkpoint)
+ {
+ int cp = item.Key;
+ int ticks = item.Value!.Ticks;
+ int runTime = item.Value!.Ticks / 64; // Runtime in decimal
+ double startVelX = item.Value!.StartVelX;
+ double startVelY = item.Value!.StartVelY;
+ double startVelZ = item.Value!.StartVelZ;
+ double endVelX = item.Value!.EndVelX;
+ double endVelY = item.Value!.EndVelY;
+ double endVelZ = item.Value!.EndVelZ;
+ int attempts = item.Value!.Attempts;
+
+ #if DEBUG
+ Console.WriteLine($"CP: {cp} | MapTime ID: {item.Value.ID} | Time: {runTime} | Ticks: {ticks} | startVelX: {startVelX} | startVelY: {startVelY} | startVelZ: {startVelZ} | endVelX: {endVelX} | endVelY: {endVelY} | endVelZ: {endVelZ}");
+ Console.WriteLine($"CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> SaveCurrentRunCheckpoints -> " +
+ $"INSERT INTO `Checkpoints` " +
+ $"(`maptime_id`, `cp`, `run_time`, `start_vel_x`, `start_vel_y`, `start_vel_z`, " +
+ $"`end_vel_x`, `end_vel_y`, `end_vel_z`, `attempts`, `end_touch`) " +
+ $"VALUES ({item.Value.ID}, {cp}, {runTime}, {startVelX}, {startVelY}, {startVelZ}, {endVelX}, {endVelY}, {endVelZ}, {attempts}, {ticks}) ON DUPLICATE KEY UPDATE " +
+ $"run_time=VALUES(run_time), start_vel_x=VALUES(start_vel_x), start_vel_y=VALUES(start_vel_y), start_vel_z=VALUES(start_vel_z), " +
+ $"end_vel_x=VALUES(end_vel_x), end_vel_y=VALUES(end_vel_y), end_vel_z=VALUES(end_vel_z), attempts=VALUES(attempts), end_touch=VALUES(end_touch);");
+ #endif
+
+ // Insert/Update CPs to database
+ // To-do: Transactions?
+ // Check if the player has PB object initialized and if the player's character is currently active in the game
+ if (item.Value != null && player.Controller.PlayerPawn.Value != null)
+ {
+ Task newPbTask = DB.Write($"INSERT INTO `Checkpoints` " +
+ $"(`maptime_id`, `cp`, `run_time`, `start_vel_x`, `start_vel_y`, `start_vel_z`, " +
+ $"`end_vel_x`, `end_vel_y`, `end_vel_z`, `attempts`, `end_touch`) " +
+ $"VALUES ({item.Value.ID}, {cp}, {runTime}, {startVelX}, {startVelY}, {startVelZ}, {endVelX}, {endVelY}, {endVelZ}, {attempts}, {ticks}) " +
+ $"ON DUPLICATE KEY UPDATE " +
+ $"run_time=VALUES(run_time), start_vel_x=VALUES(start_vel_x), start_vel_y=VALUES(start_vel_y), start_vel_z=VALUES(start_vel_z), " +
+ $"end_vel_x=VALUES(end_vel_x), end_vel_y=VALUES(end_vel_y), end_vel_z=VALUES(end_vel_z), attempts=VALUES(attempts), end_touch=VALUES(end_touch);");
+ if (newPbTask.Result <= 0)
+ throw new Exception($"CS2 Surf ERROR >> internal class Checkpoint : PersonalBest -> SaveCurrentRunCheckpoints -> Inserting Checkpoints. CP: {cp} | Name: {player.Profile.Name}");
+
+ newPbTask.Dispose();
+ }
+ }
+
+ player.Stats.ThisRun.Checkpoint.Clear();
+ }
+}
diff --git a/src/ST-Player/PlayerStats/PersonalBest.cs b/src/ST-Player/PlayerStats/PersonalBest.cs
new file mode 100644
index 0000000..181d126
--- /dev/null
+++ b/src/ST-Player/PlayerStats/PersonalBest.cs
@@ -0,0 +1,35 @@
+namespace SurfTimer;
+
+// To-do: make Style (currently 0) be dynamic
+// To-do: add `Type`
+internal class PersonalBest
+{
+ public int ID { get; set; } = -1; // Exclude from constructor, retrieve from Database when loading/saving
+ public int Ticks { get; set; }
+ public int Rank { get; set; } = -1; // Exclude from constructor, retrieve from Database when loading/saving
+ public Dictionary Checkpoint { get; set; }
+ // public int Type { get; set; }
+ public float StartVelX { get; set; }
+ public float StartVelY { get; set; }
+ public float StartVelZ { get; set; }
+ public float EndVelX { get; set; }
+ public float EndVelY { get; set; }
+ public float EndVelZ { get; set; }
+ public int RunDate { get; set; }
+ // Add other properties as needed
+
+ // Constructor
+ public PersonalBest()
+ {
+ Ticks = -1; // To-do: what type of value we use here? DB uses DECIMAL but `.Tick` is int???
+ Checkpoint = new Dictionary();
+ // Type = type;
+ StartVelX = -1.0f;
+ StartVelY = -1.0f;
+ StartVelZ = -1.0f;
+ EndVelX = -1.0f;
+ EndVelY = -1.0f;
+ EndVelZ = -1.0f;
+ RunDate = 0;
+ }
+}
\ No newline at end of file
diff --git a/src/ST-Player/PlayerStats/PlayerStats.cs b/src/ST-Player/PlayerStats/PlayerStats.cs
new file mode 100644
index 0000000..65dcc39
--- /dev/null
+++ b/src/ST-Player/PlayerStats/PlayerStats.cs
@@ -0,0 +1,148 @@
+using MySqlConnector;
+
+namespace SurfTimer;
+
+internal class PlayerStats
+{
+ // To-Do: Each stat should be a class of its own, with its own methods and properties - easier to work with.
+ // Temporarily, we store ticks + basic info so we can experiment
+ // These account for future style support and a relevant index.
+ public int[,] StagePB { get; set; } = { { 0, 0 } }; // First dimension: style (0 = normal), second dimension: stage index
+ public int[,] StageRank { get; set; } = { { 0, 0 } }; // First dimension: style (0 = normal), second dimension: stage index
+ //
+
+ public Dictionary PB { get; set; } = new Dictionary();
+ public CurrentRun ThisRun { get; set; } = new CurrentRun(); // This is a CurrenntRun object that tracks the data for the Player's current run
+ // Initialize PersonalBest for each `style` (e.g., 0 for normal) - this is a temporary solution
+ // Here we can loop through all available styles at some point and initialize them
+ public PlayerStats()
+ {
+ PB[0] = new PersonalBest();
+ // Add more styles as needed
+ }
+
+ ///
+ /// Loads the player's MapTimes data from the database along with `Rank` for the run.
+ /// `Checkpoints` are loaded separately because inside the while loop we cannot run queries.
+ /// This can populate all the `style` stats the player has for the map - currently only 1 style is supported
+ ///
+ public void LoadMapTimesData(Player player, TimerDatabase DB, int playerId = 0, int mapId = 0)
+ {
+ Task dbTask2 = DB.Query($"SELECT mainquery.*, (SELECT COUNT(*) FROM `MapTimes` AS subquery " +
+ $"WHERE subquery.`map_id` = mainquery.`map_id` AND subquery.`style` = mainquery.`style` " +
+ $"AND subquery.`run_time` <= mainquery.`run_time`) AS `rank` FROM `MapTimes` AS mainquery " +
+ $"WHERE mainquery.`player_id` = {player.Profile.ID} AND mainquery.`map_id` = {player.CurrMap.ID}; ");
+ MySqlDataReader playerStats = dbTask2.Result;
+ int style = 0; // To-do: implement styles
+ if (!playerStats.HasRows)
+ {
+ Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadMapTimesData -> No MapTimes data found for Player.");
+ }
+ else
+ {
+ while (playerStats.Read())
+ {
+ // Load data into PersonalBest object
+ // style = playerStats.GetInt32("style"); // Uncomment when style is implemented
+ PB[style].ID = playerStats.GetInt32("id");
+ PB[style].StartVelX = (float)playerStats.GetDouble("start_vel_x");
+ PB[style].StartVelY = (float)playerStats.GetDouble("start_vel_y");
+ PB[style].StartVelZ = (float)playerStats.GetDouble("start_vel_z");
+ PB[style].EndVelX = (float)playerStats.GetDouble("end_vel_x");
+ PB[style].EndVelY = (float)playerStats.GetDouble("end_vel_y");
+ PB[style].EndVelZ = (float)playerStats.GetDouble("end_vel_z");
+ PB[style].Ticks = playerStats.GetInt32("run_time");
+ PB[style].RunDate = playerStats.GetInt32("run_date");
+ PB[style].Rank = playerStats.GetInt32("rank");
+
+ Console.WriteLine($"============== CS2 Surf DEBUG >> LoadMapTimesData -> PlayerID: {player.Profile.ID} | Rank: {PB[style].Rank} | ID: {PB[style].ID} | RunTime: {PB[style].Ticks} | SVX: {PB[style].StartVelX} | SVY: {PB[style].StartVelY} | SVZ: {PB[style].StartVelZ} | EVX: {PB[style].EndVelX} | EVY: {PB[style].EndVelY} | EVZ: {PB[style].EndVelZ} | Run Date (UNIX): {PB[style].RunDate}");
+ #if DEBUG
+ Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadMapTimesData -> PlayerStats.PB (ID {PB[style].ID}) loaded from DB.");
+ #endif
+ }
+ }
+ playerStats.Close();
+ }
+
+ ///
+ /// Executes the DB query to get all the checkpoints and store them in the Checkpoint dictionary
+ ///
+ public void LoadCheckpointsData(TimerDatabase DB)
+ {
+ Task dbTask = DB.Query($"SELECT * FROM `Checkpoints` WHERE `maptime_id` = {PB[0].ID};");
+ MySqlDataReader results = dbTask.Result;
+ if (PB[0] == null)
+ {
+ #if DEBUG
+ Console.WriteLine("CS2 Surf ERROR >> internal class PlayerStats -> LoadCheckpointsData -> PersonalBest object is null.");
+ #endif
+
+ results.Close();
+ return;
+ }
+
+ if (PB[0].Checkpoint == null)
+ {
+ #if DEBUG
+ Console.WriteLine($"CS2 Surf DEBUG >> internal class PlayerStats -> LoadCheckpointsData -> PB Checkpoints list is not initialized.");
+ #endif
+
+ PB[0].Checkpoint = new Dictionary(); // Initialize if null
+ }
+
+ #if DEBUG
+ Console.WriteLine($"this.Checkpoint.Count {PB[0].Checkpoint.Count} ");
+ Console.WriteLine($"this.ID {PB[0].ID} ");
+ Console.WriteLine($"this.Ticks {PB[0].Ticks} ");
+ Console.WriteLine($"this.RunDate {PB[0].RunDate} ");
+ #endif
+
+ if (!results.HasRows)
+ {
+ #if DEBUG
+ Console.WriteLine($"CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsData -> No checkpoints found for this mapTimeId {PB[0].ID}.");
+ #endif
+
+ results.Close();
+ return;
+ }
+
+ #if DEBUG
+ Console.WriteLine($"======== CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsData -> Checkpoints found for this mapTimeId");
+ #endif
+
+ while (results.Read())
+ {
+ #if DEBUG
+ Console.WriteLine($"cp {results.GetInt32("cp")} ");
+ Console.WriteLine($"run_time {results.GetFloat("run_time")} ");
+ Console.WriteLine($"sVelX {results.GetFloat("start_vel_x")} ");
+ Console.WriteLine($"sVelY {results.GetFloat("start_vel_y")} ");
+ #endif
+
+ Checkpoint cp = new(results.GetInt32("cp"),
+ results.GetInt32("run_time"), // To-do: what type of value we use here? DB uses DECIMAL but `.Tick` is int???
+ results.GetFloat("start_vel_x"),
+ results.GetFloat("start_vel_y"),
+ results.GetFloat("start_vel_z"),
+ results.GetFloat("end_vel_x"),
+ results.GetFloat("end_vel_y"),
+ results.GetFloat("end_vel_z"),
+ results.GetFloat("end_touch"),
+ results.GetInt32("attempts"));
+ cp.ID = results.GetInt32("cp");
+ // To-do: cp.ID = calculate Rank # from DB
+
+ PB[0].Checkpoint[cp.CP] = cp;
+
+ #if DEBUG
+ Console.WriteLine($"======= CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsData -> Loaded CP {cp.CP} with RunTime {cp.Ticks}.");
+ #endif
+ }
+ results.Close();
+
+ #if DEBUG
+ Console.WriteLine($"======= CS2 Surf DEBUG >> internal class Checkpoint : PersonalBest -> LoadCheckpointsData -> Checkpoints loaded from DB. Count: {PB[0].Checkpoint.Count}");
+ #endif
+ }
+}
\ No newline at end of file