diff --git a/DamageCalculator/DamageCalculator/DamageCalculator.csproj b/DamageCalculator/DamageCalculator/DamageCalculator.csproj
index 5fb23f2..9fd64ce 100644
--- a/DamageCalculator/DamageCalculator/DamageCalculator.csproj
+++ b/DamageCalculator/DamageCalculator/DamageCalculator.csproj
@@ -51,6 +51,7 @@
all
+
diff --git a/DamageCalculator/DamageCalculator/MainWindow.xaml b/DamageCalculator/DamageCalculator/MainWindow.xaml
index 2105daf..e2e6f80 100644
--- a/DamageCalculator/DamageCalculator/MainWindow.xaml
+++ b/DamageCalculator/DamageCalculator/MainWindow.xaml
@@ -5,7 +5,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Damage_Calculator"
mc:Ignorable="d"
- Title="CS:GO Damage Calculator" Height="566" Width="1030" MinHeight="700" MinWidth="1000"
+ Title="CS:GO Damage Calculator" Height="566" Width="1030" MinHeight="845" MinWidth="1000"
Style="{DynamicResource CustomWindowStyle}"
WindowStartupLocation="CenterScreen" Icon="27.ico"
WindowState="Maximized"
@@ -29,7 +29,7 @@
-
+
@@ -56,7 +56,7 @@
-
+
@@ -82,11 +82,23 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -102,6 +114,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DamageCalculator/DamageCalculator/MainWindow.xaml.cs b/DamageCalculator/DamageCalculator/MainWindow.xaml.cs
index 6316e5c..3e187eb 100644
--- a/DamageCalculator/DamageCalculator/MainWindow.xaml.cs
+++ b/DamageCalculator/DamageCalculator/MainWindow.xaml.cs
@@ -19,6 +19,12 @@ using SteamShared.ZatVdfParser;
using System.Xml.Serialization;
using System.Globalization;
using System.Collections.ObjectModel;
+using System.Text.RegularExpressions;
+using System.Diagnostics;
+using System.Web.Services.Description;
+using System.Net.Sockets;
+using SteamShared;
+using System.Reflection;
namespace Damage_Calculator
{
@@ -82,7 +88,17 @@ namespace Damage_Calculator
private Image ASiteIcon;
private Image BSiteIcon;
- private double unitsDistance = -1;
+ ///
+ /// The amount of distance in in-game units, that is drawn on the map.
+ /// If in bomb drawing mode, this will be the minimum distance that the bomb will calculate the damage at.
+ ///
+ private double unitsDistanceMin = -1;
+
+ ///
+ /// The maximum distance that the bomb will calculate the damage at, in units.
+ /// -1 if not in bomb mode.
+ ///
+ private double unitsDistanceMax = -1;
///
/// Gets or sets the currently loaded map.
@@ -103,7 +119,7 @@ namespace Damage_Calculator
SteamShared.Globals.Settings.CsgoHelper.CsgoPath = SteamShared.Globals.Settings.SteamHelper.GetGamePathFromExactName("Counter-Strike: Global Offensive");
if (SteamShared.Globals.Settings.CsgoHelper.CsgoPath == null)
{
- MessageBox.Show("Make sure you have installed CS:GO and Steam correctly.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ ShowMessage.Error("Make sure you have installed CS:GO and Steam correctly.");
this.Close();
return;
}
@@ -362,11 +378,18 @@ namespace Damage_Calculator
this.playerPoint = null;
this.connectingLine = null;
this.bombPoint = null;
- this.unitsDistance = -1;
+ this.unitsDistanceMin = -1;
+ this.unitsDistanceMax = -1;
this.textDistanceMetres.Text = "0";
this.textDistanceUnits.Text = "0";
this.txtResult.Text = "0";
this.txtResultArmor.Text = "0";
+ this.txtResultBombMin.Text = "0";
+ this.txtResultBombMedian.Text = "0";
+ this.txtResultBombMax.Text = "0";
+ this.txtResultArmorBombMin.Text = "0";
+ this.txtResultArmorBombMedian.Text = "0";
+ this.txtResultArmorBombMax.Text = "0";
this.txtTimeRunning.Text = "0";
this.txtTimeWalking.Text = "0";
this.txtTimeCrouching.Text = "0";
@@ -532,8 +555,8 @@ namespace Damage_Calculator
if (className == "info_hostage_spawn" || className == "hostage_entity")
{
- // Entity is hostage spawn point (equivalent but latter is csgo specific)
- var spawn = new PlayerSpawn();
+ // Entity is hostage spawn point (equivalent but "info_hostage_spawn" is CS:GO-specific, "hostage_entity" is the equivalent and also exists in CS:S)
+ var spawn = new PlayerSpawn(); // Technically not a player :D
spawn.Origin = this.stringToVector3(entityRootVdf["origin"]?.Value) ?? Vector3.Zero;
spawn.Angles = this.stringToVector3(entityRootVdf["angles"]?.Value) ?? Vector3.Zero;
spawn.Team = ePlayerTeam.CounterTerrorist; // Just for the colour
@@ -635,7 +658,7 @@ namespace Damage_Calculator
circle.Fill = null;
circle.Width = circle.Height = this.getPixelsFromUnits(150);
circle.Stroke = new SolidColorBrush(strokeColour);
- circle.StrokeThickness = 2;
+ circle.StrokeThickness = 1;
circle.IsHitTestVisible = false;
return circle;
@@ -651,7 +674,7 @@ namespace Damage_Calculator
circle.Fill = new SolidColorBrush(fillColour);
circle.Width = circle.Height = this.getPixelsFromUnits(loadedMap.BombDamage * 3.5 * 2); // * 2 cause radius to width
circle.Stroke = new SolidColorBrush(strokeColour);
- circle.StrokeThickness = 3;
+ circle.StrokeThickness = 1;
circle.IsHitTestVisible = false;
return circle;
@@ -863,10 +886,32 @@ namespace Damage_Calculator
}
this.redrawLine = false;
// Update top right corner distance texts
- this.unitsDistance = this.calculateDistanceInUnits();
+ this.unitsDistanceMin = this.calculateDistanceInUnits(isMax: false);
- this.textDistanceUnits.Text = Math.Round(this.unitsDistance, 3).ToString(CultureInfo.InvariantCulture);
- this.textDistanceMetres.Text = Math.Round(this.unitsDistance / 39.37, 3).ToString(CultureInfo.InvariantCulture);
+ if (this.DrawMode == eDrawMode.Bomb)
+ {
+ this.unitsDistanceMax = this.calculateDistanceInUnits(isMax: true);
+
+ if (this.unitsDistanceMin > this.unitsDistanceMax)
+ {
+ // The min and max will only be min and max if the player is above the bomb.
+ // If he's below the bomb, it will be swapped
+ (this.unitsDistanceMin, this.unitsDistanceMax) = (this.unitsDistanceMax, this.unitsDistanceMin);
+ }
+ }
+
+ if (this.unitsDistanceMax >= 0)
+ {
+ // We have a min and max value so show it like that as well: "0.000 - 0.000"
+ this.textDistanceUnits.Text = $"{Math.Round(this.unitsDistanceMin, 3).ToString(CultureInfo.InvariantCulture)} - {Math.Round(this.unitsDistanceMax, 3).ToString(CultureInfo.InvariantCulture)}";
+ this.textDistanceMetres.Text = $"{Math.Round(this.unitsDistanceMin / 39.37, 3).ToString(CultureInfo.InvariantCulture)} - {Math.Round(this.unitsDistanceMax / 39.37, 3).ToString(CultureInfo.InvariantCulture)}";
+ }
+ else
+ {
+ // We only have a "Min" distance, which is used for firearms. "0.000"
+ this.textDistanceUnits.Text = Math.Round(this.unitsDistanceMin, 3).ToString(CultureInfo.InvariantCulture);
+ this.textDistanceMetres.Text = Math.Round(this.unitsDistanceMin / 39.37, 3).ToString(CultureInfo.InvariantCulture);
+ }
// Recalculate and show damage
this.settings_Updated(null, null);
@@ -1027,7 +1072,16 @@ namespace Damage_Calculator
}
}
- private double calculateDistanceInUnits()
+ ///
+ /// Calculates the distance from the start to the end of the .
+ /// Bomb distance adds a random factor, which is described as min and max.
+ ///
+ ///
+ /// If we calculate bomb damage, should we get the max possible distance?
+ /// Otherwise it will return the min possible distance.
+ ///
+ ///
+ private double calculateDistanceInUnits(bool isMax = false)
{
// left and right point for the X and Y coordinates (in pixels) so we gotta convert those
Point[] points = this.connectingLine.Tag as Point[];
@@ -1073,31 +1127,35 @@ namespace Damage_Calculator
rightZ = this.playerPoint.Z;
}
- // Distance in shown pixels in 2D
- double diffPixels2D = Math.Sqrt(Math.Pow(leftX - rightX, 2) + Math.Pow(leftY - rightY, 2));
- double unitsDifference2D = this.getUnitsFromPixels(diffPixels2D);
-
+ // Distance in units in 2D
+ double diffUnits2D = Math.Sqrt(Math.Pow(leftX - rightX, 2) + Math.Pow(leftY - rightY, 2));
if (this.DrawMode == eDrawMode.Bomb)
{
- // Add the appropriate eye level
+ float minFactor = 0.7f;
+ float maxFactor = 1.0f;
+
+ // Add the appropriate height
+ // They use the middle of the oriented bounding box,
+ // which should be equal to the axis-aligned bounding box, but with additional yaw in the normal sense
+ // Since we already have the X-Y middle, we just add half of the height to get the Z-middle as well
if (radioPlayerStanding.IsChecked == true)
- rightZ += eyeLevelStanding;
+ rightZ += isMax ? eyeLevelStanding * maxFactor : eyeLevelStanding * minFactor;
else if(radioPlayerCrouched.IsChecked == true)
- rightZ += eyeLevelCrouching;
+ rightZ += isMax ? eyeLevelCrouching * maxFactor : eyeLevelCrouching * minFactor;
}
// Add Z height to calculation, unless a point has no area ID associated, then it stays 2D
- double diffDistance3D = Math.Sqrt(Math.Pow(diffPixels2D, 2) + Math.Pow(leftZ - rightZ, 2));
+ double diffUnits3D = Math.Sqrt(Math.Pow(diffUnits2D, 2) + Math.Pow(leftZ - rightZ, 2));
- return diffDistance3D;
+ return diffUnits3D;
}
private void calculateDistanceDuration()
{
- double timeRunning = this.unitsDistance / this.selectedWeapon.RunningSpeed;
- double timeWalking = this.unitsDistance / (this.selectedWeapon.RunningSpeed * SteamShared.CsgoHelper.WalkModifier);
- double timeCrouching = this.unitsDistance / (this.selectedWeapon.RunningSpeed * SteamShared.CsgoHelper.DuckModifier);
+ double timeRunning = this.unitsDistanceMin / this.selectedWeapon.RunningSpeed;
+ double timeWalking = this.unitsDistanceMin / (this.selectedWeapon.RunningSpeed * SteamShared.CsgoHelper.WalkModifier);
+ double timeCrouching = this.unitsDistanceMin / (this.selectedWeapon.RunningSpeed * SteamShared.CsgoHelper.DuckModifier);
this.txtTimeRunning.Text = getTimeStringFromSeconds(timeRunning);
this.txtTimeWalking.Text = getTimeStringFromSeconds(timeWalking);
@@ -1125,7 +1183,7 @@ namespace Damage_Calculator
double absorbedDamageByArmor = 0;
bool wasArmorHit = false;
- if (this.unitsDistance > this.selectedWeapon.MaxBulletRange)
+ if (this.unitsDistanceMin > this.selectedWeapon.MaxBulletRange)
{
damage = 0;
txtResult.Text = txtResultArmor.Text = damage.ToString();
@@ -1133,7 +1191,7 @@ namespace Damage_Calculator
}
// Range
- damage *= Math.Pow(this.selectedWeapon.DamageDropoff, double.Parse((this.unitsDistance / 500f).ToString()));
+ damage *= Math.Pow(this.selectedWeapon.DamageDropoff, double.Parse((this.unitsDistanceMin / 500f).ToString()));
switch (this.selectedWeapon.DamageType)
{
@@ -1199,22 +1257,45 @@ namespace Damage_Calculator
private void calculateAndUpdateBombDamage()
{
+ // Now we can calculate the damage...
+ double minDamage;
+ double minDamageArmor;
+ this.getBombDamage(this.unitsDistanceMax, armorValue: 100, out minDamage, out minDamageArmor);
+ double maxDamage;
+ double maxDamageArmor;
+ this.getBombDamage(this.unitsDistanceMin, armorValue: 100, out maxDamage, out maxDamageArmor);
+
+ txtResultBombMin.Text = ((int)minDamage).ToString();
+ txtResultBombMax.Text = ((int)maxDamage).ToString();
+ txtResultBombMedian.Text = ((int)((maxDamage + minDamage) / 2f)).ToString();
+
+ txtResultArmorBombMin.Text = minDamageArmor.ToString();
+ txtResultArmorBombMax.Text = maxDamageArmor.ToString();
+ txtResultArmorBombMedian.Text = ((maxDamageArmor + minDamageArmor) / 2f).ToString();
+ }
+
+ private void getBombDamage(double distance, int armorValue, out double hpDamage, out double armorDamage)
+ {
+ // out params
+ hpDamage = 0;
+ armorDamage = 0;
+
// This is the maximum damage, and the height of the bell curve
double flDamage = this.loadedMap.BombDamage; // 500 is hard-coded as the default, and also the default in the hammer editor, can be overridden as a map creator
double flBombRadius = flDamage * 3.5d;
- // First we need to check if the player is within the bomb radius, this isn't done with a circle, but with a box that has a side length of 2r
- // So basically it's the bounding box, so if you're not directly above, below, left or right of the bomb, the radius increases a bit
- // Also its calculated via the bounding box of the player which is 32x32 units in the horizontal axes
+ // First we need to check if the player is within the bomb radius, this isn't done with a sphere, but with a box that has a side length of 2r
+ // So basically it's the bounding box, so if you're not directly above, below, left or right of the bomb (as seen from above), the radius increases a bit.
+ // Also its calculated via the intersection with the bounding box of the player which is 32x32 units in the horizontal axes
// Get mins and maxs of player hitbox
// Mins is X - 16, Y - 16, Z
Vector3 playerMins = new Vector3 { X = (float)(this.playerPoint.X - (playerWidth / 2d)), Y = (float)(this.playerPoint.Y - (playerWidth / 2d)), Z = (float)this.playerPoint.Z };
-
- // Head height is not eye level, crouching is smaller, otherwise use standing height
+
+ // Head height is the eye level, crouching is smaller, otherwise use standing height
float headHeight = (float)(this.radioPlayerCrouched.IsChecked == true ? heightCrouching : heightStanding);
-
+
// Maxs is X + 16, Y + 16, Z + head height
Vector3 playerMaxs = new Vector3 { X = (float)(this.playerPoint.X + (playerWidth / 2d)), Y = (float)(this.playerPoint.Y + (playerWidth / 2d)), Z = (float)this.playerPoint.Z + headHeight };
@@ -1228,36 +1309,35 @@ namespace Damage_Calculator
if (!playerIsInRange)
{
- txtResult.Text = txtResultArmor.Text = "0";
+ hpDamage = 0;
+ armorDamage = 0;
return;
}
- // Now we can calculate the damage...
-
- const double damagePercentage = 1.0d;
-
- // From player origin + eye level, to the bomb
- double flDistanceToLocalPlayer = (double)this.unitsDistance;// ((c4bomb origin + viewoffset) - (localplayer origin + viewoffset))
+ // From player origin + offset, to the bomb origin
+ double flDistanceToLocalPlayer = distance;
// This defines the width of the curve, a smaller value gives a steeper curve and a faster falloff
double fSigma = flBombRadius / 3.0d;
double fGaussianFalloff = Math.Exp(-flDistanceToLocalPlayer * flDistanceToLocalPlayer / (2.0d * fSigma * fSigma));
- double flAdjustedDamage = flDamage * fGaussianFalloff * damagePercentage;
+ double flAdjustedDamage = flDamage * fGaussianFalloff;
- bool wasArmorHit = false;
double flAdjustedDamageBeforeArmor = flAdjustedDamage;
if (chkArmorAny.IsChecked == true)
{
- flAdjustedDamage = scaleDamageArmor(flAdjustedDamage, 100);
- wasArmorHit = true;
+ flAdjustedDamage = scaleDamageArmor(flAdjustedDamage, armorValue);
+ armorDamage = flAdjustedDamage >= 1 ? Math.Ceiling((flAdjustedDamageBeforeArmor - flAdjustedDamage) / 2f) : 0;
}
- txtResult.Text = ((int)flAdjustedDamage).ToString();
-
- double roundedDamageToArmor = Math.Ceiling((flAdjustedDamageBeforeArmor - flAdjustedDamage) / 2f);
- txtResultArmor.Text = (wasArmorHit && flAdjustedDamage >= 1 ? roundedDamageToArmor : 0).ToString();
+ hpDamage = flAdjustedDamage;
}
+ ///
+ /// Calculates the amount of damage that the bomb deals with a specific amount of armor.
+ ///
+ /// The damage that was dealt to the player by the bomb.
+ /// The amount of armor that the player had.
+ /// the amount of damage that is actually dealt.
double scaleDamageArmor(double flDamage, int armor_value)
{
double flArmorRatio = 0.5d;
@@ -1311,6 +1391,14 @@ namespace Damage_Calculator
this.changeTheme(Globals.Settings.Theme);
}
+ ///
+ /// Takes the given and the height of its 4 points,
+ /// and calculates what the height of the given point of (X,Y) must be, with linear interpolation and weights.
+ ///
+ /// The given X of point.
+ /// The given Y of point.
+ /// The area that serves as a height reference.
+ /// the Z coordinate of the given point in respect to the .
private float getPointHeightInArea(float x, float y, NavArea area)
{
Vector3[][] groups = new Vector3[][]
@@ -1356,7 +1444,7 @@ namespace Damage_Calculator
// Height to be displayed further down, depending on area chosen
float newZ = 0;
- if (this.loadedMap.NavMesh?.Header?.NavAreas != null)
+ if (this.loadedMap?.NavMesh?.Header?.NavAreas != null)
{
var navAreasFound = new List();
@@ -1385,7 +1473,7 @@ namespace Damage_Calculator
// Or the amount of areas hovered over has changed.
// In that case set it to the layer with the lowest Z difference to the previously selected one
- if (navAreasFound.Count == 1 || this.hoveredNavAreas.Count == 0)
+ if (navAreasFound.Count == 1 || this.hoveredNavAreas == null || this.hoveredNavAreas?.Count == 0)
this.currentHeightLayer = 0;
else
{
@@ -1524,6 +1612,44 @@ namespace Damage_Calculator
}
}
+ ///
+ /// Gets the of the that is closest to the height of the given point.
+ /// The point has to be inside of the X and Y bounds of the NAV area for it to be considered.
+ /// This makes it so the Z distance can be anything, as long as it's the closest of any area.
+ ///
+ /// The point we want to partner with a NAV area.
+ /// The belonging to the point.
+ private NavArea? getClosestNavAreaToPoint(Vector3 point)
+ {
+ if (this.loadedMap?.NavMesh?.Header?.NavAreas == null)
+ {
+ return null;
+ }
+
+ NavArea closestNavArea = null;
+ float closestDistance = float.MaxValue;
+
+ foreach(NavArea area in this.loadedMap.NavMesh.Header.NavAreas)
+ {
+ if (point.X < area.ActualNorthWestCorner.X || point.X > area.ActualNorthEastCorner.X
+ || point.Y < area.ActualSouthWestCorner.Y || point.Y > area.ActualNorthWestCorner.Y)
+ {
+ // Point is not in this NavArea's X and Y bounds
+ continue;
+ }
+
+ float currentZDistance = Math.Abs(area.MedianPosition.Z - point.Z);
+
+ if (currentZDistance < closestDistance)
+ {
+ closestDistance = currentZDistance;
+ closestNavArea = area;
+ }
+ }
+
+ return closestNavArea;
+ }
+
private void fillWeaponInfo()
{
if(this.selectedWeapon != null)
@@ -1556,7 +1682,334 @@ namespace Damage_Calculator
x.Text = placeholderText;
}
+ private void setPlayerConnected(bool connected)
+ {
+ if (connected)
+ {
+ this.stackInGameConnected.Visibility = Visibility.Visible;
+ this.stackInGameDisconnected.Visibility = Visibility.Collapsed;
+ }
+ else
+ {
+ this.stackInGameConnected.Visibility = Visibility.Collapsed;
+ this.stackInGameDisconnected.Visibility = Visibility.Visible;
+ }
+ }
+
+ private bool getNetConPort(string input, out ushort port)
+ {
+ int index = input.IndexOf("-netconport", StringComparison.InvariantCultureIgnoreCase);
+ port = 0;
+
+ if (index < 0)
+ return false;
+
+ input = input.Substring(index);
+ var foundArguments = Regex.Matches(input, SteamShared.Globals.ArgumentPattern, RegexOptions.IgnoreCase);
+
+ if (foundArguments.Count > 1)
+ {
+ string setPort = foundArguments[1].Value.Replace("\"","");
+ if (ushort.TryParse(setPort, out ushort telnetPort))
+ {
+ port = telnetPort;
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool getNetConPassword(string input, out string password)
+ {
+ int index = input.IndexOf("-netconpassword", StringComparison.InvariantCultureIgnoreCase);
+ password = string.Empty;
+
+ if (index < 0)
+ return false;
+
+ input = input.Substring(index);
+ var foundArguments = Regex.Matches(input, SteamShared.Globals.ArgumentPattern, RegexOptions.IgnoreCase);
+
+ if (foundArguments.Count > 1)
+ {
+ string setPassword = foundArguments[1].Value.Replace("\"", "");
+ if (!setPassword.StartsWith("-"))
+ {
+ password = setPassword;
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ private async void establishCsgoConnection()
+ {
+ if (SteamShared.Globals.Settings.CsgoSocket.IsConnecting)
+ {
+ ShowMessage.Error("We're currently trying to connect to CS:GO.\n\nThis should only be 10 seconds after starting to connect.\n\nIf CS:GO was not started yet, this time is 60 seconds.");
+ return;
+ }
+
+ var mostRecentUser = SteamShared.Globals.Settings.SteamHelper.GetMostRecentSteamUser();
+
+ if (mostRecentUser == null)
+ {
+ ShowMessage.Error("There was no user found that was marked as most recent.\n\nTry logging into Steam, if it's been a while.");
+ return;
+ }
+
+ var startOptions = SteamShared.Globals.Settings.CsgoHelper.GetLaunchOptions(mostRecentUser);
+
+ if (startOptions == null)
+ {
+ ShowMessage.Error($"There was an error while trying to get the CS:GO launch options for the user {mostRecentUser.PersonaName} ({mostRecentUser.AccountName}).");
+ return;
+ }
+
+ // Because the start options are passed either way, we just want to add what we want additionally
+ string additionalStartOptions = string.Empty;
+ bool needsAdditionalStartOptions = true;
+
+ // Set own password and port, if no others will be found
+ string passwordToUse = string.Empty; // We don't want a password
+ ushort portToUse = Globals.Settings.NetConPort;
+
+ // Check if CS:GO is running
+ (Process? csgo, string? curCsgoCmdLine) = SteamShared.Globals.Settings.CsgoHelper.GetRunningCsgo();
+
+ // CS:GO wasn't open, so either take the info from the start options or create our own
+ // We're gonna get those from the file that stores the current settings, because the game is not running
+ ushort foundPort = 0;
+ bool portWasFound = this.getNetConPort(csgo == null ? startOptions : curCsgoCmdLine, out foundPort);
+
+ string foundPassword = string.Empty;
+ bool passwordWasFound = this.getNetConPassword(csgo == null ? startOptions : curCsgoCmdLine, out foundPassword);
+
+ // Verify found port and password are valid
+ if (portWasFound && foundPort > 0)
+ {
+ portToUse = foundPort;
+ needsAdditionalStartOptions = false; // If at least a port has been found, we're happy
+ }
+ if (passwordWasFound && !string.IsNullOrWhiteSpace(foundPassword))
+ {
+ passwordToUse = foundPassword; // If we were told to use a password, then we'll do that
+ }
+
+ if (needsAdditionalStartOptions)
+ {
+ additionalStartOptions += $" -netconport {portToUse}";
+ }
+
+ if (csgo == null)
+ {
+ // CS:GO is not opened, just ask if the user wants to start it
+ if (MessageBox.Show("Do you want to start CS:GO now?", "Start CS:GO", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
+ {
+ SteamShared.Globals.Settings.SteamHelper.StartApp(SteamShared.CsgoHelper.GameID, additionalStartOptions);
+ ShowMessage.Info("CS:GO is currently attempting to start. Retry to connect when you're in-game.\n\nRetrying while on a map will load the corresponding map.");
+ return;
+ }
+ else
+ {
+ return;
+ }
+ }
+ else
+ {
+ // CS:GO is already running
+ if (needsAdditionalStartOptions)
+ {
+ // We need to restart the game, in order to set a port
+ if (MessageBox.Show("The game is already running and doesn't seem to have a connection enabled.\n\nDo you want to kill and restart CS:GO now?", "Restart CS:GO", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
+ {
+ // Wait for CS:GO to close here (with timeout!)
+ csgo.Kill();
+ csgo.WaitForExit(5000);
+
+ if (csgo.HasExited)
+ {
+ SteamShared.Globals.Settings.SteamHelper.StartApp(SteamShared.CsgoHelper.GameID, additionalStartOptions);
+
+ ShowMessage.Info("CS:GO is currently attempting to restart. Retry to connect when you're in-game.\n\nRetrying while on a map will load the corresponding map.");
+ return;
+ }
+ else
+ {
+ ShowMessage.Error("CS:GO has not closed after 5 seconds of waiting. Try again when the game has closed.");
+ return;
+ }
+ }
+ else
+ {
+ return;
+ }
+ }
+ }
+
+ var connectResult = await SteamShared.Globals.Settings.CsgoSocket.ConnectAsync(portToUse, null);
+ SteamShared.Globals.Settings.CsgoSocket.OnDisconnect += CsgoSocket_OnDisconnect;
+ if (connectResult == CsgoSocketConnectResult.Success)
+ {
+ // Connected
+ this.setPlayerConnected(true);
+
+ // Get map name and try to select it
+ await this.loadCurrentMap();
+ }
+ }
+
+ private async Task loadCurrentMap()
+ {
+ string currentMapName = await SteamShared.Globals.Settings.CsgoSocket.GetMapName();
+
+ if (currentMapName == null)
+ return;
+
+ foreach (var mapItem in comboBoxMaps.Items)
+ {
+ if (mapItem is ComboBoxItem item && item.Tag is CsgoMap map)
+ {
+ if (currentMapName.ToLower().Contains(map.MapFileName.ToLower()))
+ {
+ // We found the map in the list, that the player is currently on
+ comboBoxMaps.SelectedItem = mapItem;
+ break;
+ }
+ }
+ }
+ }
+
+
+ private void setTargetOrBombPoint(Vector3 givenCoords = null)
+ {
+ if (this.DrawMode == eDrawMode.Shooting)
+ {
+ if (this.targetPoint == null)
+ this.targetPoint = new MapPoint();
+
+ Point newPointPosPixels = givenCoords == null ? Mouse.GetPosition(this.mapCanvas) : this.getPointFromGameCoords(givenCoords.X, givenCoords.Y);
+ this.canvasRemove(this.targetPoint.Circle);
+
+ var circle = this.getPointEllipse(this.leftClickPointColour);
+
+ this.canvasAdd(circle);
+
+ this.targetPoint.PercentageX = newPointPosPixels.X * 100f / this.mapCanvas.ActualWidth;
+ this.targetPoint.PercentageY = newPointPosPixels.Y * 100f / this.mapCanvas.ActualHeight;
+ this.targetPoint.PercentageScale = circle.Width * 100f / this.mapCanvas.ActualWidth;
+ this.targetPoint.Z = givenCoords == null ? this.currentMouseCoord.Z : givenCoords.Z;
+ if (this.currentHeightLayer >= 0 && givenCoords == null)
+ {
+ // Associate area ID to see if we want just 2D distance in case one point has no area (and with that, Z value) with it
+ this.targetPoint.AssociatedAreaID = (int)this.hoveredNavAreas?[this.currentHeightLayer].ID;
+ }
+ else if (givenCoords != null)
+ {
+ NavArea closestArea = this.getClosestNavAreaToPoint(givenCoords);
+ if (closestArea != null)
+ {
+ this.targetPoint.AssociatedAreaID = (int)closestArea.ID;
+ }
+ }
+ else
+ this.targetPoint.AssociatedAreaID = -1;
+
+ this.targetPoint.Circle = circle;
+ this.redrawLine = true;
+
+ this.drawPointsAndConnectingLine();
+ }
+ else if (this.DrawMode == eDrawMode.Bomb)
+ {
+ if (this.bombPoint == null)
+ this.bombPoint = new MapPoint();
+
+ Point newPointPosPixels = givenCoords == null ? Mouse.GetPosition(this.mapCanvas) : this.getPointFromGameCoords(givenCoords.X, givenCoords.Y);
+ this.canvasRemove(this.bombPoint.Circle);
+
+ var circle = this.getBombEllipse(this.leftClickPointColour);
+
+ this.canvasAdd(circle);
+
+ this.bombPoint.PercentageX = newPointPosPixels.X * 100f / this.mapCanvas.ActualWidth;
+ this.bombPoint.PercentageY = newPointPosPixels.Y * 100f / this.mapCanvas.ActualHeight;
+ this.bombPoint.PercentageScale = circle.Width * 100f / this.mapCanvas.ActualWidth;
+ this.bombPoint.Z = givenCoords == null ? this.currentMouseCoord.Z : givenCoords.Z;
+ if (this.currentHeightLayer >= 0 && givenCoords == null)
+ {
+ // Associate area ID to see if we want just 2D distance in case one point has no area (and with that, Z value) with it
+ this.bombPoint.AssociatedAreaID = (int)this.hoveredNavAreas?[this.currentHeightLayer].ID;
+ }
+ else if (givenCoords != null)
+ {
+ NavArea closestArea = this.getClosestNavAreaToPoint(givenCoords);
+ if (closestArea != null)
+ {
+ this.bombPoint.AssociatedAreaID = (int)closestArea.ID;
+ }
+ }
+ else
+ this.bombPoint.AssociatedAreaID = -1;
+
+ this.bombPoint.Circle = circle;
+
+ this.redrawLine = true;
+
+ this.drawPointsAndConnectingLine();
+ }
+ }
+
+ private void setPlayerPoint(Vector3 givenCoords = null)
+ {
+ if (this.playerPoint == null)
+ this.playerPoint = new MapPoint();
+
+ Point newPointPosPixels = givenCoords == null ? Mouse.GetPosition(this.mapCanvas) : this.getPointFromGameCoords(givenCoords.X, givenCoords.Y);
+ this.canvasRemove(this.playerPoint.Circle);
+
+ var circle = this.getPointEllipse(this.rightClickPointColour);
+
+ this.canvasAdd(circle);
+
+ this.playerPoint.PercentageX = newPointPosPixels.X * 100f / this.mapCanvas.ActualWidth;
+ this.playerPoint.PercentageY = newPointPosPixels.Y * 100f / this.mapCanvas.ActualHeight;
+ this.playerPoint.PercentageScale = circle.Width * 100f / this.mapCanvas.ActualWidth;
+ this.playerPoint.Z = givenCoords == null ? this.currentMouseCoord.Z : givenCoords.Z;
+
+ if (this.currentHeightLayer >= 0 && givenCoords == null)
+ {
+ // Associate area ID to see if we want just 2D distance in case one point has no area (and with that, Z value) with it
+ this.playerPoint.AssociatedAreaID = (int)this.hoveredNavAreas?[this.currentHeightLayer].ID;
+ }
+ else if (givenCoords != null)
+ {
+ NavArea closestArea = this.getClosestNavAreaToPoint(givenCoords);
+ if(closestArea != null)
+ {
+ this.playerPoint.AssociatedAreaID = (int)closestArea.ID;
+ }
+ }
+ else
+ this.playerPoint.AssociatedAreaID = -1;
+
+ this.playerPoint.Circle = circle;
+
+ this.redrawLine = true;
+
+ this.drawPointsAndConnectingLine();
+ }
+
+
+
#region events
+ private void CsgoSocket_OnDisconnect(object sender, EventArgs e)
+ {
+ this.setPlayerConnected(false);
+ }
+
private void rightZoomBorder_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (rightZoomBorder.IsZoomed)
@@ -1579,8 +2032,8 @@ namespace Damage_Calculator
this.DrawMode = eDrawMode.Shooting;
if (this.IsInitialized)
{
- this.stackArmorSeparated.Visibility = this.stackAreaHit.Visibility = this.stackWeaponUsed.Visibility = Visibility.Visible;
- this.stackPlayerStance.Visibility = this.lblPlayerStance.Visibility = this.chkArmorAny.Visibility = Visibility.Collapsed;
+ this.stackArmorSeparated.Visibility = this.stackAreaHit.Visibility = this.stackWeaponUsed.Visibility = this.stackDamageFirearm.Visibility = Visibility.Visible;
+ this.stackPlayerStance.Visibility = this.lblPlayerStance.Visibility = this.chkArmorAny.Visibility = this.stackDamageBomb.Visibility = Visibility.Collapsed;
}
}
@@ -1590,8 +2043,8 @@ namespace Damage_Calculator
this.DrawMode = eDrawMode.Bomb;
if (this.IsInitialized)
{
- this.stackArmorSeparated.Visibility = this.stackAreaHit.Visibility = this.stackWeaponUsed.Visibility = Visibility.Collapsed;
- this.stackPlayerStance.Visibility = this.lblPlayerStance.Visibility = this.chkArmorAny.Visibility = Visibility.Visible;
+ this.stackArmorSeparated.Visibility = this.stackAreaHit.Visibility = this.stackWeaponUsed.Visibility = this.stackDamageFirearm.Visibility = Visibility.Collapsed;
+ this.stackPlayerStance.Visibility = this.lblPlayerStance.Visibility = this.chkArmorAny.Visibility = this.stackDamageBomb.Visibility = Visibility.Visible;
}
}
@@ -1602,90 +2055,12 @@ namespace Damage_Calculator
private void mapImage_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
- if (this.DrawMode == eDrawMode.Shooting)
- {
- if (this.targetPoint == null)
- this.targetPoint = new MapPoint();
-
- Point mousePos = Mouse.GetPosition(this.mapCanvas);
- this.canvasRemove(this.targetPoint.Circle);
-
- var circle = this.getPointEllipse(this.leftClickPointColour);
-
- this.canvasAdd(circle);
-
- this.targetPoint.PercentageX = mousePos.X * 100f / this.mapCanvas.ActualWidth;
- this.targetPoint.PercentageY = mousePos.Y * 100f / this.mapCanvas.ActualHeight;
- this.targetPoint.PercentageScale = circle.Width * 100f / this.mapCanvas.ActualWidth;
- this.targetPoint.Z = this.currentMouseCoord.Z;
- if(this.currentHeightLayer >= 0)
- // Associate area ID to see if we want just 2D distance in case one point has no area (and with that, Z value) with it
- this.targetPoint.AssociatedAreaID = (int)this.hoveredNavAreas[this.currentHeightLayer].ID;
- else
- this.targetPoint.AssociatedAreaID = -1;
-
- this.targetPoint.Circle = circle;
- this.redrawLine = true;
-
- this.drawPointsAndConnectingLine();
- }
- else if (this.DrawMode == eDrawMode.Bomb)
- {
- if (this.bombPoint == null)
- this.bombPoint = new MapPoint();
-
- Point mousePos = Mouse.GetPosition(this.mapCanvas);
- this.canvasRemove(this.bombPoint.Circle);
-
- var circle = this.getBombEllipse(this.leftClickPointColour);
-
- this.canvasAdd(circle);
-
- this.bombPoint.PercentageX = mousePos.X * 100f / this.mapCanvas.ActualWidth;
- this.bombPoint.PercentageY = mousePos.Y * 100f / this.mapCanvas.ActualHeight;
- this.bombPoint.PercentageScale = circle.Width * 100f / this.mapCanvas.ActualWidth;
- this.bombPoint.Z = this.currentMouseCoord.Z;
- if (this.currentHeightLayer >= 0)
- // Associate area ID to see if we want just 2D distance in case one point has no area (and with that, Z value) with it
- this.bombPoint.AssociatedAreaID = (int)this.hoveredNavAreas[this.currentHeightLayer].ID;
- else
- this.bombPoint.AssociatedAreaID = -1;
-
- this.bombPoint.Circle = circle;
-
- this.redrawLine = true;
-
- this.drawPointsAndConnectingLine();
- }
+ this.setTargetOrBombPoint();
}
private void mapImage_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
- if (this.playerPoint == null)
- this.playerPoint = new MapPoint();
-
- Point mousePos = Mouse.GetPosition(this.mapCanvas);
- this.canvasRemove(this.playerPoint.Circle);
-
- var circle = this.getPointEllipse(this.rightClickPointColour);
-
- this.canvasAdd(circle);
-
- this.playerPoint.PercentageX = mousePos.X * 100f / this.mapCanvas.ActualWidth;
- this.playerPoint.PercentageY = mousePos.Y * 100f / this.mapCanvas.ActualHeight;
- this.playerPoint.PercentageScale = circle.Width * 100f / this.mapCanvas.ActualWidth;
- this.playerPoint.Z = this.currentMouseCoord.Z;
- if (this.currentHeightLayer >= 0)
- // Associate area ID to see if we want just 2D distance in case one point has no area (and with that, Z value) with it
- this.playerPoint.AssociatedAreaID = (int)this.hoveredNavAreas[this.currentHeightLayer].ID;
- else
- this.playerPoint.AssociatedAreaID = -1;
-
- this.playerPoint.Circle = circle;
-
- this.redrawLine = true;
-
- this.drawPointsAndConnectingLine();
+ this.setPlayerPoint();
}
private void comboBoxMaps_SelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -1808,6 +2183,50 @@ namespace Damage_Calculator
this.canvasReload();
}
}
+
+ private void btnConnectToCsgo_Click(object sender, RoutedEventArgs e)
+ {
+ this.establishCsgoConnection();
+ }
+
+ private async void btnSetAsTargetPos_Click(object sender, RoutedEventArgs e)
+ {
+ // Get player position
+ Vector3 playerPos = await SteamShared.Globals.Settings.CsgoSocket.GetPlayerPosition();
+
+ if (playerPos == null)
+ {
+ ShowMessage.Error("No valid in-game position was found.\n\nMake sure you're not on a server that you're not the admin on.");
+ return;
+ }
+
+ this.setTargetOrBombPoint(playerPos);
+ }
+
+ private async void btnSetAsPlayerPos_Click(object sender, RoutedEventArgs e)
+ {
+ // Get player position
+ Vector3 playerPos = await SteamShared.Globals.Settings.CsgoSocket.GetPlayerPosition();
+
+ if (playerPos == null)
+ {
+ ShowMessage.Error("No valid in-game position was found.\n\nMake sure you're not on a server that you're not the admin on.");
+ return;
+ }
+
+ this.setPlayerPoint(playerPos);
+ }
+
+ private async void btnLoadCurrentMap_Click(object sender, RoutedEventArgs e)
+ {
+ await this.loadCurrentMap();
+ }
+
+ private async void btnDisconnectFromCsgo_Click(object sender, RoutedEventArgs e)
+ {
+ await SteamShared.Globals.Settings.CsgoSocket.DisconnectAsync();
+ this.setPlayerConnected(false);
+ }
#endregion
}
diff --git a/DamageCalculator/DamageCalculator/Settings.cs b/DamageCalculator/DamageCalculator/Settings.cs
index 1954a56..d3a13de 100644
--- a/DamageCalculator/DamageCalculator/Settings.cs
+++ b/DamageCalculator/DamageCalculator/Settings.cs
@@ -41,6 +41,10 @@ namespace Damage_Calculator
public bool ShowMapsMissingNav { get; set; } = true;
public bool ShowMapsMissingAin { get; set; } = true;
+ // OTHER
+
+ public ushort NetConPort { get; set; } = 2121;
+
public object Clone()
{
return this.MemberwiseClone();
diff --git a/DamageCalculator/DamageCalculator/ShowMessage.cs b/DamageCalculator/DamageCalculator/ShowMessage.cs
new file mode 100644
index 0000000..70fc793
--- /dev/null
+++ b/DamageCalculator/DamageCalculator/ShowMessage.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace Damage_Calculator
+{
+ internal static class ShowMessage
+ {
+ public static void Error(string message) => MessageBox.Show(message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ public static void Warning(string message) => MessageBox.Show(message, "Warning", MessageBoxButton.OK, MessageBoxImage.Warning);
+ public static void Info(string message) => MessageBox.Show(message, "Information", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+}
diff --git a/DamageCalculator/DamageCalculator/wndSettings.xaml b/DamageCalculator/DamageCalculator/wndSettings.xaml
index f67a161..b971e45 100644
--- a/DamageCalculator/DamageCalculator/wndSettings.xaml
+++ b/DamageCalculator/DamageCalculator/wndSettings.xaml
@@ -70,7 +70,7 @@
-
+
@@ -86,6 +86,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/DamageCalculator/DamageCalculator/wndSettings.xaml.cs b/DamageCalculator/DamageCalculator/wndSettings.xaml.cs
index 6a3c2c9..60f8d83 100644
--- a/DamageCalculator/DamageCalculator/wndSettings.xaml.cs
+++ b/DamageCalculator/DamageCalculator/wndSettings.xaml.cs
@@ -37,6 +37,9 @@ namespace Damage_Calculator
public wndSettings(SteamShared.Models.CsgoMap currentMap)
{
InitializeComponent();
+ this.ushortNetConPort.Minimum = 1; // Because we will have TCP, Port 0 is reserved
+ this.ushortNetConPort.Maximum = ushort.MaxValue; // Maximum port number, should already be implied, but I want to make sure
+
this.currentMap = currentMap;
this.MaxHeight = System.Windows.SystemParameters.PrimaryScreenHeight;
this.settings = (Settings)Globals.Settings.Clone();
@@ -98,6 +101,9 @@ namespace Damage_Calculator
this.mnuShowMapsMissingBsp.IsChecked = this.settings.ShowMapsMissingBsp;
this.mnuShowMapsMissingNav.IsChecked = this.settings.ShowMapsMissingNav;
this.mnuShowMapsMissingAin.IsChecked = this.settings.ShowMapsMissingAin;
+
+ // Other
+ this.ushortNetConPort.Value = this.settings.NetConPort;
}
private void saveSettings()
@@ -154,6 +160,9 @@ namespace Damage_Calculator
this.settings.ShowMapsMissingNav = (bool)this.mnuShowMapsMissingNav.IsChecked;
this.settings.ShowMapsMissingAin = (bool)this.mnuShowMapsMissingAin.IsChecked;
+ // Other
+ this.settings.NetConPort = this.ushortNetConPort.Value ?? this.settings.NetConPort;
+
Globals.Settings = this.settings;
Globals.SaveSettings();
}
@@ -165,6 +174,12 @@ namespace Damage_Calculator
private void btnSave_Click(object sender, RoutedEventArgs e)
{
+ if (this.ushortNetConPort.Value == null)
+ {
+ ShowMessage.Error("Please set the NetConPort to a custom value, or to the default.");
+ return;
+ }
+
this.DialogResult = true; // Tell main window to reload with new settings
this.saveSettings();
this.Close();
diff --git a/SteamShared/SteamShared/SteamShared/CsgoHelper.cs b/SteamShared/SteamShared/SteamShared/CsgoHelper.cs
index c2ae908..6d9eb2d 100644
--- a/SteamShared/SteamShared/SteamShared/CsgoHelper.cs
+++ b/SteamShared/SteamShared/SteamShared/CsgoHelper.cs
@@ -2,12 +2,15 @@
using SteamShared.ZatVdfParser;
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using System.Management;
namespace SteamShared
{
@@ -19,6 +22,8 @@ namespace SteamShared
public static readonly float WalkModifier = 0.52f;
+ public static readonly int GameID = 730;
+
///
/// Gets the prefixes allowed for maps when using .
///
@@ -48,7 +53,7 @@ namespace SteamShared
public CsgoHelper()
{
- // Nothing to do
+ // Nothing to do, don't use this ctor, aside from before the program initialises.
}
public CsgoHelper(string csgoPath)
@@ -65,6 +70,25 @@ namespace SteamShared
return this.Validate(this.CsgoPath!);
}
+ public (Process?,string?) GetRunningCsgo()
+ {
+ // This is used as the normal process handle
+ Process? csgoProcess = Process.GetProcessesByName("csgo").FirstOrDefault(p => Globals.ComparePaths(p.MainModule?.FileName!, this.CsgoPath!));
+
+ // And this is used for grabbing the command line arguments the process was started with
+ string? cmdLine = string.Empty;
+
+ var mgmtClass = new ManagementClass("Win32_Process");
+ foreach (ManagementObject o in mgmtClass.GetInstances())
+ {
+ if (o["Name"].Equals("csgo.exe"))
+ {
+ cmdLine = o["CommandLine"].ToString();
+ }
+ }
+
+ return (csgoProcess, cmdLine);
+ }
///
/// Validates files and directories for CS:GO installed in the given path.
@@ -90,7 +114,11 @@ namespace SteamShared
public List GetMaps()
{
- List mapTextFiles = Directory.GetFiles(System.IO.Path.Combine(this.CsgoPath!, "csgo\\resource\\overviews")).ToList().Where(f => f.ToLower().EndsWith(".txt")).Where(f =>
+ string mapOverviewsPath = System.IO.Path.Combine(this.CsgoPath!, "csgo", "resource", "overviews");
+ if (!Directory.Exists(mapOverviewsPath))
+ return new List();
+
+ List mapTextFiles = Directory.GetFiles(mapOverviewsPath).ToList().Where(f => f.ToLower().EndsWith(".txt")).Where(f =>
this.mapFileNameValid(f)).ToList();
List maps = new List();
@@ -246,6 +274,35 @@ namespace SteamShared
return maps;
}
+ ///
+ /// Gets the launch options of the specified Steam user.
+ ///
+ /// The Steam user of which to get the launch options.
+ ///
+ /// The launch options,
+ /// or null if an error occurred, or if the passed was wrong or
+ ///
+ public string? GetLaunchOptions(SteamUser user)
+ {
+ if (user.AbsoluteUserdataFolderPath == null)
+ return null;
+
+ string localUserCfgPath = Path.Combine(user.AbsoluteUserdataFolderPath, "config", "localconfig.vdf");
+
+ if (localUserCfgPath == null)
+ return null;
+
+ VDFFile localConfigVdf = new VDFFile(localUserCfgPath);
+
+ string? launchOptions = localConfigVdf?["UserLocalConfigStore"]?["Software"]?["Valve"]?["Steam"]?["apps"]?["730"]?["LaunchOptions"]?.Value;
+
+ if (launchOptions == null)
+ return null;
+
+ // If there are " escaped like this \" we want them to be unescaped
+ return Regex.Replace(launchOptions, "\\\\(.)", "$1");
+ }
+
public List GetWeapons()
{
string filePath = Path.Combine(this.CsgoPath!, "csgo\\scripts\\items\\items_game.txt");
diff --git a/SteamShared/SteamShared/SteamShared/CsgoSocketConnection.cs b/SteamShared/SteamShared/SteamShared/CsgoSocketConnection.cs
new file mode 100644
index 0000000..f4790bb
--- /dev/null
+++ b/SteamShared/SteamShared/SteamShared/CsgoSocketConnection.cs
@@ -0,0 +1,315 @@
+using SteamShared.Models;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Net.Sockets;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace SteamShared
+{
+ ///
+ /// State when first connecting.
+ ///
+ public enum CsgoSocketConnectResult { Success, WrongPassword, Failure }
+
+ ///
+ /// State when checking if a connection is still up.
+ ///
+ public enum SocketConnectionState { Connected, Disconnected, Blocking }
+
+ public class CsgoSocketConnection : IDisposable
+ {
+ private Socket? socket = null;
+ private DateTime lastCommandExecute = DateTime.MinValue;
+ private readonly TimeSpan commandTimeout = TimeSpan.FromSeconds(1); // Actual timeout is a bit lower
+
+ public event EventHandler OnDisconnect;
+
+ public bool IsConnecting { get; private set; }
+
+ public async Task ConnectAsync(ushort port, string? password)
+ {
+ bool usePassword = !String.IsNullOrWhiteSpace(password);
+ if (socket == null)
+ this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+
+ try
+ {
+ IAsyncResult connectResult = socket.BeginConnect("localhost", port, null, null); // We wanna wait for the connection
+
+ // Wait for timeout before closing connection
+ this.IsConnecting = true;
+ bool success = connectResult.AsyncWaitHandle.WaitOne(10_000, exitContext: true);
+ this.IsConnecting = false;
+
+ if (this.socket.Connected)
+ {
+ this.socket.EndConnect(connectResult);
+ }
+ else
+ {
+ // We couldn't connect in the given time
+ this.socket.Close();
+ return CsgoSocketConnectResult.Failure;
+ }
+ }
+ catch (Exception e)
+ {
+ return CsgoSocketConnectResult.Failure;
+ }
+
+ if (usePassword)
+ {
+ // A password was specified (not by us) so we gotta deal with this shit
+ await this.executeCommand("PASS " + password, 0); // Actual bytes might be for example 2
+ }
+
+ return CsgoSocketConnectResult.Success;
+ }
+
+ public async Task DisconnectAsync()
+ {
+ if (socket == null)
+ return;
+
+ await this.socket.DisconnectAsync(false);
+ this.OnDisconnect(this, null!);
+ this.socket = null;
+ }
+
+ private string stripTrailingNullBytes(string text)
+ {
+ string result = string.Empty;
+ for (int i = 0; i < text.Length; i++)
+ {
+ if (text[i] == 0)
+ {
+ return result;
+ }
+ result += text[i];
+ }
+ return result;
+ }
+
+ public async Task GetMapName()
+ {
+ string? response = await this.executeCommand("host_map", 2048);
+
+ if (response == null)
+ return null;
+
+ // Example response: "host_map" = "de_tuscan.bsp" ( def. "" )
+ // This can be changed by the user, regardless of sv_cheats, at least on their own server
+ // By default it will result to the map's file name that's been loaded
+
+ int indexStart = response.LastIndexOf("host_map");
+
+ if (indexStart < 0)
+ return null;
+
+ response = response.Substring(indexStart); // Here our output starts
+
+ var valuesSplit = response.Split('=');
+
+ if (valuesSplit.Length < 2)
+ return null;
+
+ var foundArgument = Regex.Match(valuesSplit[1], Globals.ArgumentPattern, RegexOptions.IgnoreCase);
+
+ if (!foundArgument.Success)
+ return null;
+
+ // Only take everything until the last dot, to erase the file name, if existent
+ int indexOfLastDot = foundArgument.Value.LastIndexOf('.');
+ string mapName = foundArgument.Value.Substring(0, indexOfLastDot < 0 ? foundArgument.Value.Length : indexOfLastDot);
+
+ return mapName;
+ }
+
+ public async Task GetPlayerPosition()
+ {
+ string? response = await this.executeCommand("getpos_exact", 2048, isCheat: true);
+
+ if (response == null)
+ return null;
+
+ // Example response: setpos_exact 0.000000 0.000000 0.000000;setang_exact 0.000000 0.000000 0.000000
+ // It might only send everything until the first ; is hit, but we don't want the angles here anyways.
+ // If we later also want the orientation then one could maybe execute Receive() twice
+ // It might also contain text before it, so we wanna find the beginning of the output first
+
+ int indexStart = response.LastIndexOf("setpos_exact");
+
+ if (indexStart < 0)
+ return null;
+
+ response = response.Substring(indexStart); // Here our output starts
+
+ var valuesSplit = response.Split(';');
+
+ if (valuesSplit.Length < 2)
+ return null;
+
+ // Get position
+ var positionSplit = valuesSplit[0].Split(' ');
+
+ if(positionSplit.Length != 4)
+ return null;
+
+ var pos = new Vector3();
+ if(float.TryParse(positionSplit[1], NumberStyles.Float, CultureInfo.InvariantCulture, out float x ))
+ {
+ pos.X = x;
+ }
+ if (float.TryParse(positionSplit[2], NumberStyles.Float, CultureInfo.InvariantCulture, out float y))
+ {
+ pos.Y = y;
+ }
+ if (float.TryParse(positionSplit[3], NumberStyles.Float, CultureInfo.InvariantCulture, out float z))
+ {
+ pos.Z = z;
+ }
+
+ return pos;
+ }
+
+ private async Task executeCommand(string message, int expectedBytes, bool isCheat = false, bool isRepeat = false)
+ {
+ if (Globals.Settings.CsgoHelper.GetRunningCsgo().Item1 == null)
+ {
+ // CS:GO isn't running (anymore)
+ await this.DisconnectAsync();
+ return null;
+ }
+
+ if (socket == null)
+ return null;
+
+ byte[] messageBytes = Encoding.UTF8.GetBytes(message + "\r\n");
+
+ Debug.WriteLine("command is: " + message);
+
+ bool repeatCommand = false;
+ string answer = string.Empty;
+
+ TimeSpan timeSinceLastCommand = DateTime.Now - lastCommandExecute;
+ if (timeSinceLastCommand < commandTimeout)
+ {
+ Debug.WriteLine("timeout. sleeping for " + (commandTimeout - timeSinceLastCommand).Milliseconds + "ms");
+ // Sleep for the remaining timeout
+ await Task.Delay(commandTimeout - timeSinceLastCommand);
+ }
+
+ try
+ {
+ Debug.WriteLine("Sending actual command...");
+ // Send the actual command
+ await socket.SendAsync(messageBytes, SocketFlags.None);
+ }
+ catch { return null; }
+
+ // Save this for the CS:GO ConCommand timeout
+ this.lastCommandExecute = DateTime.Now;
+
+ byte[] buffer = new byte[expectedBytes];
+ socket.ReceiveTimeout = 1000; // 1 second
+
+ int parts = 0;
+ answer = string.Empty;
+ try
+ {
+ Debug.WriteLine("Receiving actual command...");
+
+ do
+ {
+ int receivedBytes = this.socket.Receive(buffer, SocketFlags.None);
+ parts++;
+
+ string partAnswer = Encoding.UTF8.GetString(buffer, 0, receivedBytes);
+
+ Debug.WriteLine($"Received response (Part {parts}): \n" + partAnswer);
+
+ answer += partAnswer;
+
+ // We will most likely not get all of the response in one go, so we'll read until we get a completely empty response and concat everything
+ } while (buffer[0] != 0);
+ }
+ catch
+ {
+ if (string.IsNullOrEmpty(answer))
+ return null;
+ }
+
+ Debug.WriteLine($"Complete response in {parts} parts:\n" + answer);
+
+ if (isCheat && !isRepeat)
+ {
+ repeatCommand = this.cheatCommandNeedsElevation(answer);
+
+ if (repeatCommand)
+ {
+ // The game told us we used a cheat command and need to have sv_cheats set to 1
+ Debug.WriteLine("Trying to execute sv_cheats 1...");
+ await this.executeCommand("sv_cheats 1", 0);
+ Debug.WriteLine("Executed sv_cheats 1. Now re-executing previous command...");
+ return await this.executeCommand(message, expectedBytes, isCheat, isRepeat: true);
+ }
+ }
+
+ return answer;
+ }
+
+ private bool cheatCommandNeedsElevation(string command)
+ {
+ return command.Contains("Can't use cheat command");
+ }
+
+ private SocketConnectionState checkConnected()
+ {
+ if (socket == null)
+ return SocketConnectionState.Disconnected;
+
+ var connectionState = SocketConnectionState.Disconnected;
+
+ bool blockingState = socket.Blocking;
+ try
+ {
+ byte[] tmp = new byte[1];
+
+ socket.Blocking = false;
+ socket.Send(tmp, 0, 0);
+
+ connectionState = SocketConnectionState.Connected;
+ }
+ catch (SocketException e)
+ {
+ // 10035 == WSAEWOULDBLOCK
+ if (e.NativeErrorCode.Equals(10035))
+ {
+ // Still Connected, but the Send would block
+ connectionState = SocketConnectionState.Blocking;
+ }
+ else
+ {
+ connectionState = SocketConnectionState.Disconnected;
+ }
+ }
+ finally
+ {
+ socket.Blocking = blockingState;
+ }
+
+ return connectionState;
+ }
+
+ public void Dispose()
+ {
+ this.socket?.Dispose();
+ }
+ }
+}
diff --git a/SteamShared/SteamShared/SteamShared/Globals.cs b/SteamShared/SteamShared/SteamShared/Globals.cs
index 7063ba2..f096fba 100644
--- a/SteamShared/SteamShared/SteamShared/Globals.cs
+++ b/SteamShared/SteamShared/SteamShared/Globals.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
+using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media.Imaging;
@@ -12,6 +13,7 @@ namespace SteamShared
public static class Globals
{
public static Models.Settings Settings = new Models.Settings();
+ public static readonly string ArgumentPattern = "[\\\"\"].+?[\\\"\"]|[^ ]+";
public static BitmapImage BitmapToImageSource(Bitmap src)
{
@@ -42,6 +44,36 @@ namespace SteamShared
}
}
+ public static bool ComparePaths(string path1, string path2)
+ {
+ // If it's a file that exists, remove the name from either to get the directory
+ if(File.Exists(path1))
+ path1 = Path.GetDirectoryName(path1)!;
+ if (File.Exists(path2))
+ path1 = Path.GetDirectoryName(path2)!;
+
+ if (path1 == null && path2 == null)
+ // They're technically the same
+ return true;
+ else if (path1 == null || path2 == null)
+ return false;
+
+ // Take care of back and forward slashes
+ path1 = Path.GetFullPath(path1);
+ path2 = Path.GetFullPath(path2);
+
+ // Add another temp folder at the back and get the name of its parent directory,
+ // thus removing that temp folder again,
+ // basically getting rid of a trailing \\ at the end, if existent
+ path1 = Path.Combine(path1, "temp");
+ path2 = Path.Combine(path2, "temp");
+
+ path1 = Path.GetDirectoryName(path1)?.ToLower()!;
+ path2 = Path.GetDirectoryName(path2)?.ToLower()!;
+
+ return path1 == path2;
+ }
+
public static float Map(float s, float a1, float a2, float b1, float b2)
{
return b1 + (s - a1) * (b2 - b1) / (a2 - a1);
diff --git a/SteamShared/SteamShared/SteamShared/Models/Settings.cs b/SteamShared/SteamShared/SteamShared/Models/Settings.cs
index d185fa0..88c169e 100644
--- a/SteamShared/SteamShared/SteamShared/Models/Settings.cs
+++ b/SteamShared/SteamShared/SteamShared/Models/Settings.cs
@@ -11,5 +11,7 @@ namespace SteamShared.Models
public SteamHelper SteamHelper = new SteamHelper();
public CsgoHelper CsgoHelper = new CsgoHelper();
+
+ public CsgoSocketConnection CsgoSocket = new CsgoSocketConnection();
}
}
diff --git a/SteamShared/SteamShared/SteamShared/Models/SteamUser.cs b/SteamShared/SteamShared/SteamShared/Models/SteamUser.cs
new file mode 100644
index 0000000..33f2546
--- /dev/null
+++ b/SteamShared/SteamShared/SteamShared/Models/SteamUser.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SteamShared.Models
+{
+ public class SteamUser
+ {
+ ///
+ /// The name the user logs in with.
+ ///
+ public string? AccountName { get; set; }
+
+ ///
+ /// The persona name the user can change.
+ ///
+ public string? PersonaName { get; set; }
+
+ ///
+ /// The time of last login in unix time.
+ ///
+ public ulong LastLogin { get; set; }
+
+ ///
+ /// The long steam ID (starting with 7656...)
+ ///
+ public ulong SteamID64 { get; set; }
+
+ ///
+ /// The account ID, used for trade links or the userdata folder.
+ /// It's calculated from the SteamID64.
+ ///
+ public ulong AccountID
+ {
+ get
+ {
+ // Steps I take to calculate this:
+ // The account ID is the lowest 32 bits (half of the total bits),
+ // 1 << 32 will result in one bigger than we need, with no correct bitmask (only one 1).
+ // Subtracting 1 will give us all 1s where we need them :).
+ // Casting it to e.g. a long is necessary, because otherwise it would overflow into some dumb shit and be wrong.
+ // The bitwise-& is done last
+ return this.SteamID64 & ((long)1 << 32) - 1;
+ }
+ }
+
+ public string? AbsoluteUserdataFolderPath { get; set; }
+ }
+}
diff --git a/SteamShared/SteamShared/SteamShared/SteamHelper.cs b/SteamShared/SteamShared/SteamShared/SteamHelper.cs
index 982c49d..5721846 100644
--- a/SteamShared/SteamShared/SteamShared/SteamHelper.cs
+++ b/SteamShared/SteamShared/SteamShared/SteamHelper.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using SteamShared.ZatVdfParser;
using Microsoft.Win32;
using SteamShared.Models;
+using System.Diagnostics;
namespace SteamShared
{
@@ -14,6 +15,7 @@ namespace SteamShared
{
private string? steamPath;
private List? steamLibraries;
+ public List? InstalledGames;
///
/// Gets the absolute path to the Steam install directory.
@@ -142,6 +144,14 @@ namespace SteamShared
#endif
}
+ public void UpdateInstalledGames(bool force = false)
+ {
+ if (!force && this.InstalledGames != null)
+ return;
+
+ this.InstalledGames = this.GetInstalledGames();
+ }
+
public List? GetInstalledGames()
{
var steamLibraries = this.GetSteamLibraries();
@@ -230,6 +240,87 @@ namespace SteamShared
return null;
}
+ ///
+ /// Gets the most recently logged in Steam user, based on the "MostRecent" value.
+ ///
+ ///
+ /// The most recent logged in Steam user, or , if none has been found or an error has occurred.
+ ///
+ public SteamUser? GetMostRecentSteamUser()
+ {
+ string steamPath = this.SteamPath;
+
+ if (steamPath == null)
+ return null;
+
+ string usersFilePath = Path.Combine(steamPath, "config", "loginusers.vdf");
+
+ if (!File.Exists(usersFilePath))
+ return null;
+
+ VDFFile vdf = new VDFFile(usersFilePath);
+
+ var users = vdf?["users"]?.Children;
+
+ if (users == null)
+ return null;
+
+ SteamUser? mostRecentUser = null;
+
+ foreach (var user in users)
+ {
+ if (int.TryParse(user["MostRecent"]?.Value, out int mostRecent) && Convert.ToBoolean(mostRecent))
+ {
+ // We found a "most recent" user
+ mostRecentUser = new SteamUser();
+
+ if(ulong.TryParse(user.Name, out ulong steamID64))
+ {
+ mostRecentUser.SteamID64 = steamID64;
+ }
+
+ mostRecentUser.AccountName = user["AccountName"].Value;
+ mostRecentUser.PersonaName = user["PersonaName"].Value;
+
+ if (ulong.TryParse(user["Timestamp"].Value, out ulong lastLoginUnixTime))
+ {
+ mostRecentUser.LastLogin = lastLoginUnixTime;
+ }
+
+ mostRecentUser.AbsoluteUserdataFolderPath = Path.Combine(steamPath, "userdata", mostRecentUser.AccountID.ToString());
+ }
+ }
+
+ return mostRecentUser;
+ }
+
+ ///
+ /// Starts the given steam game, with the given additional arguments, if possible.
+ ///
+ /// The ID of the game.
+ ///
+ /// The arguments passed to that game.
+ /// Note, that the default arguments set by the user in the UI are also passed to the app, these are just additional.
+ ///
+ public void StartApp(int gameID, string arguments)
+ {
+ string? steamPath = this.SteamPath;
+ this.UpdateInstalledGames(); // Won't force update, if already set
+
+ SteamGame? gameToStart = this.InstalledGames?.FirstOrDefault(game => game.AppId == gameID);
+
+ if (steamPath == null || gameToStart == null)
+ return;
+
+ var startInfo = new ProcessStartInfo();
+ startInfo.UseShellExecute = false; // Make double sure
+ startInfo.CreateNoWindow = false;
+ startInfo.FileName = Path.Combine(steamPath, "steam.exe");
+ startInfo.Arguments = $"-applaunch {gameID}" + (String.IsNullOrWhiteSpace(arguments) ? string.Empty : $" {arguments}");
+
+ Process.Start(startInfo);
+ }
+
#region Private Methods
private bool isAppManifestFile(string filePath)
{
diff --git a/SteamShared/SteamShared/SteamShared/SteamShared.csproj b/SteamShared/SteamShared/SteamShared/SteamShared.csproj
index 7ddbd8c..443b512 100644
--- a/SteamShared/SteamShared/SteamShared/SteamShared.csproj
+++ b/SteamShared/SteamShared/SteamShared/SteamShared.csproj
@@ -28,6 +28,7 @@
+
diff --git a/SteamShared/SteamShared/SteamShared/ZatVdfParser/VdfFile.cs b/SteamShared/SteamShared/SteamShared/ZatVdfParser/VdfFile.cs
index 0c570e5..57e34cf 100644
--- a/SteamShared/SteamShared/SteamShared/ZatVdfParser/VdfFile.cs
+++ b/SteamShared/SteamShared/SteamShared/ZatVdfParser/VdfFile.cs
@@ -41,6 +41,9 @@ namespace SteamShared.ZatVdfParser
}
private void Parse(string filePathOrText, bool parseTextDirectly)
{
+ if (!parseTextDirectly && !File.Exists(filePathOrText))
+ return;
+
Element? currentLevel = null;
// Generate stream from string in case we want to read it directly, instead of using a file stream (boolean parameter)
@@ -59,7 +62,9 @@ namespace SteamShared.ZatVdfParser
return;
line = line.Trim();
- string[] parts = line.Split('"');
+ // We don't want to split if " is escaped with \
+ // If " is preceeded by an even number of \, it will get split
+ string[] parts = splitEscaped(line, '"', '\\');
if (line.StartsWith("//"))
{
@@ -104,6 +109,55 @@ namespace SteamShared.ZatVdfParser
}
#endregion
+ #region Private methods
+
+ private string[] splitEscaped(string text, char delimiter, char escapeCharacter)
+ {
+ // Example text with delimiter " and escape character \ would be:
+ // "MyPassword" "My password is \"1234\""
+ // ^ ^ ^ ^ << Splits are marked
+
+ List splitStrings = new List();
+
+ bool escaped = false;
+ string currentSection = string.Empty;
+
+ for (int i = 0; i < text.Length; i++)
+ {
+ if (text[i] == delimiter)
+ {
+ if (!escaped)
+ {
+ splitStrings.Add(currentSection);
+ currentSection = string.Empty;
+
+ if (i + 1 == text.Length)
+ // The last char in the string is a delimiter, so add that section now, because we won't iterate over it later
+ splitStrings.Add(currentSection);
+
+ continue;
+ }
+
+ escaped = false;
+ }
+
+ if (text[i] == escapeCharacter)
+ escaped = !escaped;
+
+ currentSection += text[i];
+
+ if (i + 1 == text.Length)
+ {
+ // This was the last character, but wasn't a delimiter
+ splitStrings.Add(currentSection);
+ }
+ }
+
+ return splitStrings.ToArray();
+ }
+
+ #endregion
+
#region OPERATORS
public Element this[int key]
{