Fix bomb prediction & Add netconport integration

* Bomb distance was previously fetched like in CBaseEntity::BodyTarget, but was switched to CBasePlayer::BodyTarget, which adds an amount of randomness to the damage, the more above or below the bomb a player is, the more randomness
* unitsDistanceMax will now keep the maximum distance the bomb calculates damage at, whereas unitsDistanceMin will have the minimum distance, generating a from-to value for damage
* Add min and max to bomb damage in the UI
* Added more summaries
* Bomb and player stroke are now thinner
* Fixed a random crash at startup when there were no NavAreas to loop over
* Added the functionality to set the current in-game point to either of the two points in the program, -netconport is needed for that and automatically added, if not there
* Added said netconport to the settings
* Added SteamUser class
* Added ability for VdfParser to find strings where quotes are escaped, since they were treated as normal quotes
* Add function to get the steam user that most recently logged into steam
This commit is contained in:
Mathias Lui 2022-12-10 00:27:23 +01:00
parent 3e94f4c622
commit c449cd1bf9
15 changed files with 1232 additions and 140 deletions

View file

@ -51,6 +51,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Windows.Compatibility" Version="6.0.0" /> <PackageReference Include="Microsoft.Windows.Compatibility" Version="6.0.0" />
<PackageReference Include="System.Management" Version="7.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="wndSettings.xaml"> <Page Update="wndSettings.xaml">

View file

@ -5,7 +5,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Damage_Calculator" xmlns:local="clr-namespace:Damage_Calculator"
mc:Ignorable="d" 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}" Style="{DynamicResource CustomWindowStyle}"
WindowStartupLocation="CenterScreen" Icon="27.ico" WindowStartupLocation="CenterScreen" Icon="27.ico"
WindowState="Maximized" WindowState="Maximized"
@ -29,7 +29,7 @@
<RowDefinition /> <RowDefinition />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="160" /> <ColumnDefinition Width="170" />
<ColumnDefinition /> <ColumnDefinition />
<ColumnDefinition Width="250" /> <ColumnDefinition Width="250" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
@ -56,7 +56,7 @@
<Rectangle x:Name="rectTop" VerticalAlignment="Top" Grid.Row="1" Height="1" Fill="White" Grid.ColumnSpan="3" /> <Rectangle x:Name="rectTop" VerticalAlignment="Top" Grid.Row="1" Height="1" Fill="White" Grid.ColumnSpan="3" />
<Rectangle x:Name="rectLeftSide" HorizontalAlignment="Left" Width="1" Grid.Row="1" Grid.Column="1" Fill="White" /> <Rectangle x:Name="rectLeftSide" HorizontalAlignment="Left" Width="1" Grid.Row="1" Grid.Column="1" Fill="White" />
<Rectangle x:Name="rectRightSide" HorizontalAlignment="Right" Width="1" Grid.Row="1" Grid.Column="1" Fill="White" /> <Rectangle x:Name="rectRightSide" HorizontalAlignment="Right" Width="1" Grid.Row="1" Grid.Column="1" Fill="White" />
<StackPanel x:Name="leftStackPanel" Margin="10,20,0,0" Grid.Row="1" VerticalAlignment="Top" HorizontalAlignment="Stretch"> <StackPanel x:Name="leftStackPanel" Margin="10,20,10,0" Grid.Row="1" VerticalAlignment="Top" HorizontalAlignment="Stretch">
<StackPanel> <StackPanel>
<StackPanel> <StackPanel>
<TextBlock x:Name="lblPlayerStance" Text="Player stance:" Visibility="Collapsed" FontSize="14" FontWeight="Bold" /> <TextBlock x:Name="lblPlayerStance" Text="Player stance:" Visibility="Collapsed" FontSize="14" FontWeight="Bold" />
@ -82,11 +82,23 @@
<TextBlock Text="Weapon used:" FontSize="14" FontWeight="Bold" /> <TextBlock Text="Weapon used:" FontSize="14" FontWeight="Bold" />
<ComboBox x:Name="comboWeapons" MinWidth="100" MaxWidth="200" HorizontalAlignment="Left" SelectionChanged="comboWeapons_SelectionChanged" /> <ComboBox x:Name="comboWeapons" MinWidth="100" MaxWidth="200" HorizontalAlignment="Left" SelectionChanged="comboWeapons_SelectionChanged" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,20,0,0"> <StackPanel x:Name="stackDamageFirearm" Margin="0,20,0,0">
<TextBlock Text="Resulting damage:" FontSize="14" FontWeight="Bold" /> <TextBlock Text="Resulting damage:" FontSize="14" FontWeight="Bold" />
<TextBlock x:Name="txtResult" Text="0" Foreground="IndianRed" FontSize="18" /> <TextBlock x:Name="txtResult" Text="0" Foreground="IndianRed" FontSize="18" />
<TextBlock x:Name="txtResultArmor" Text="0" Foreground="CadetBlue" FontSize="14" /> <TextBlock x:Name="txtResultArmor" Text="0" Foreground="CadetBlue" FontSize="14" />
</StackPanel> </StackPanel>
<StackPanel x:Name="stackDamageBomb" Visibility="Collapsed" Margin="0,20,0,0">
<TextBlock Text="Resulting damage" FontSize="14" FontWeight="Bold" />
<TextBlock Text="Min:" FontSize="12" Margin="0,10,0,0" />
<TextBlock x:Name="txtResultBombMin" Text="0" Foreground="IndianRed" FontSize="14" />
<TextBlock x:Name="txtResultArmorBombMin" Text="0" Foreground="CadetBlue" FontSize="12" />
<TextBlock Text="Likely:" FontSize="14" FontWeight="DemiBold" Margin="0,10,0,0" />
<TextBlock x:Name="txtResultBombMedian" Text="0" Foreground="IndianRed" FontSize="18" />
<TextBlock x:Name="txtResultArmorBombMedian" Text="0" Foreground="CadetBlue" FontSize="14" />
<TextBlock Text="Max:" FontSize="12" Margin="0,10,0,0" />
<TextBlock x:Name="txtResultBombMax" Text="0" Foreground="IndianRed" FontSize="14" />
<TextBlock x:Name="txtResultArmorBombMax" Text="0" Foreground="CadetBlue" FontSize="12" />
</StackPanel>
<StackPanel Margin="0,20,0,0"> <StackPanel Margin="0,20,0,0">
<TextBlock Text="Distance moved.." FontSize="14" FontWeight="Bold" /> <TextBlock Text="Distance moved.." FontSize="14" FontWeight="Bold" />
<StackPanel Margin="0,10,0,0"> <StackPanel Margin="0,10,0,0">
@ -102,6 +114,19 @@
<TextBlock x:Name="txtTimeCrouching" Text="None" TextWrapping="Wrap" /> <TextBlock x:Name="txtTimeCrouching" Text="None" TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<GroupBox Margin="0,20,0,0" Header="In-game">
<Grid>
<StackPanel x:Name="stackInGameDisconnected">
<Button x:Name="btnConnectToCsgo" Content="Connect to CS:GO" FontSize="10" Click="btnConnectToCsgo_Click" />
</StackPanel>
<StackPanel x:Name="stackInGameConnected" Visibility="Collapsed">
<Button x:Name="btnSetAsTargetPos" Content="Set as target/bomb position" Foreground="Red" FontSize="10" Click="btnSetAsTargetPos_Click" />
<Button x:Name="btnSetAsPlayerPos" Content="Set as player position" Foreground="Green" FontSize="10" Margin="0,5,0,0" Click="btnSetAsPlayerPos_Click" />
<Button x:Name="btnLoadCurrentMap" Content="Load current map" FontSize="10" Foreground="MediumSlateBlue" Margin="0,15,0,0" Click="btnLoadCurrentMap_Click" />
<Button x:Name="btnDisconnectFromCsgo" Content="Disconnect from CS:GO" Background="DarkRed" FontSize="10" Margin="0,15,0,0" Click="btnDisconnectFromCsgo_Click" />
</StackPanel>
</Grid>
</GroupBox>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<local:ZoomBorder x:Name="rightZoomBorder" Grid.Row="1" Grid.Column="1" Margin="10" ClipToBounds="True" SizeChanged="rightZoomBorder_SizeChanged"> <local:ZoomBorder x:Name="rightZoomBorder" Grid.Row="1" Grid.Column="1" Margin="10" ClipToBounds="True" SizeChanged="rightZoomBorder_SizeChanged">

View file

@ -19,6 +19,12 @@ using SteamShared.ZatVdfParser;
using System.Xml.Serialization; using System.Xml.Serialization;
using System.Globalization; using System.Globalization;
using System.Collections.ObjectModel; 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 namespace Damage_Calculator
{ {
@ -82,7 +88,17 @@ namespace Damage_Calculator
private Image ASiteIcon; private Image ASiteIcon;
private Image BSiteIcon; private Image BSiteIcon;
private double unitsDistance = -1; /// <summary>
/// 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.
/// </summary>
private double unitsDistanceMin = -1;
/// <summary>
/// The maximum distance that the bomb will calculate the damage at, in units.
/// -1 if not in bomb mode.
/// </summary>
private double unitsDistanceMax = -1;
/// <summary> /// <summary>
/// Gets or sets the currently loaded map. /// 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"); SteamShared.Globals.Settings.CsgoHelper.CsgoPath = SteamShared.Globals.Settings.SteamHelper.GetGamePathFromExactName("Counter-Strike: Global Offensive");
if (SteamShared.Globals.Settings.CsgoHelper.CsgoPath == null) 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(); this.Close();
return; return;
} }
@ -362,11 +378,18 @@ namespace Damage_Calculator
this.playerPoint = null; this.playerPoint = null;
this.connectingLine = null; this.connectingLine = null;
this.bombPoint = null; this.bombPoint = null;
this.unitsDistance = -1; this.unitsDistanceMin = -1;
this.unitsDistanceMax = -1;
this.textDistanceMetres.Text = "0"; this.textDistanceMetres.Text = "0";
this.textDistanceUnits.Text = "0"; this.textDistanceUnits.Text = "0";
this.txtResult.Text = "0"; this.txtResult.Text = "0";
this.txtResultArmor.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.txtTimeRunning.Text = "0";
this.txtTimeWalking.Text = "0"; this.txtTimeWalking.Text = "0";
this.txtTimeCrouching.Text = "0"; this.txtTimeCrouching.Text = "0";
@ -532,8 +555,8 @@ namespace Damage_Calculator
if (className == "info_hostage_spawn" || className == "hostage_entity") if (className == "info_hostage_spawn" || className == "hostage_entity")
{ {
// Entity is hostage spawn point (equivalent but latter is csgo specific) // 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(); var spawn = new PlayerSpawn(); // Technically not a player :D
spawn.Origin = this.stringToVector3(entityRootVdf["origin"]?.Value) ?? Vector3.Zero; spawn.Origin = this.stringToVector3(entityRootVdf["origin"]?.Value) ?? Vector3.Zero;
spawn.Angles = this.stringToVector3(entityRootVdf["angles"]?.Value) ?? Vector3.Zero; spawn.Angles = this.stringToVector3(entityRootVdf["angles"]?.Value) ?? Vector3.Zero;
spawn.Team = ePlayerTeam.CounterTerrorist; // Just for the colour spawn.Team = ePlayerTeam.CounterTerrorist; // Just for the colour
@ -635,7 +658,7 @@ namespace Damage_Calculator
circle.Fill = null; circle.Fill = null;
circle.Width = circle.Height = this.getPixelsFromUnits(150); circle.Width = circle.Height = this.getPixelsFromUnits(150);
circle.Stroke = new SolidColorBrush(strokeColour); circle.Stroke = new SolidColorBrush(strokeColour);
circle.StrokeThickness = 2; circle.StrokeThickness = 1;
circle.IsHitTestVisible = false; circle.IsHitTestVisible = false;
return circle; return circle;
@ -651,7 +674,7 @@ namespace Damage_Calculator
circle.Fill = new SolidColorBrush(fillColour); circle.Fill = new SolidColorBrush(fillColour);
circle.Width = circle.Height = this.getPixelsFromUnits(loadedMap.BombDamage * 3.5 * 2); // * 2 cause radius to width circle.Width = circle.Height = this.getPixelsFromUnits(loadedMap.BombDamage * 3.5 * 2); // * 2 cause radius to width
circle.Stroke = new SolidColorBrush(strokeColour); circle.Stroke = new SolidColorBrush(strokeColour);
circle.StrokeThickness = 3; circle.StrokeThickness = 1;
circle.IsHitTestVisible = false; circle.IsHitTestVisible = false;
return circle; return circle;
@ -863,10 +886,32 @@ namespace Damage_Calculator
} }
this.redrawLine = false; this.redrawLine = false;
// Update top right corner distance texts // 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); if (this.DrawMode == eDrawMode.Bomb)
this.textDistanceMetres.Text = Math.Round(this.unitsDistance / 39.37, 3).ToString(CultureInfo.InvariantCulture); {
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 // Recalculate and show damage
this.settings_Updated(null, null); this.settings_Updated(null, null);
@ -1027,7 +1072,16 @@ namespace Damage_Calculator
} }
} }
private double calculateDistanceInUnits() /// <summary>
/// Calculates the distance from the start to the end of the <see cref="connectingLine"/>.
/// Bomb distance adds a random factor, which is described as min and max.
/// </summary>
/// <param name="isMax">
/// If we calculate bomb damage, should we get the max possible distance?
/// Otherwise it will return the min possible distance.
/// </param>
/// <returns></returns>
private double calculateDistanceInUnits(bool isMax = false)
{ {
// left and right point for the X and Y coordinates (in pixels) so we gotta convert those // left and right point for the X and Y coordinates (in pixels) so we gotta convert those
Point[] points = this.connectingLine.Tag as Point[]; Point[] points = this.connectingLine.Tag as Point[];
@ -1073,31 +1127,35 @@ namespace Damage_Calculator
rightZ = this.playerPoint.Z; rightZ = this.playerPoint.Z;
} }
// Distance in shown pixels in 2D // Distance in units in 2D
double diffPixels2D = Math.Sqrt(Math.Pow(leftX - rightX, 2) + Math.Pow(leftY - rightY, 2)); double diffUnits2D = Math.Sqrt(Math.Pow(leftX - rightX, 2) + Math.Pow(leftY - rightY, 2));
double unitsDifference2D = this.getUnitsFromPixels(diffPixels2D);
if (this.DrawMode == eDrawMode.Bomb) 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) if (radioPlayerStanding.IsChecked == true)
rightZ += eyeLevelStanding; rightZ += isMax ? eyeLevelStanding * maxFactor : eyeLevelStanding * minFactor;
else if(radioPlayerCrouched.IsChecked == true) 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 // 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() private void calculateDistanceDuration()
{ {
double timeRunning = this.unitsDistance / this.selectedWeapon.RunningSpeed; double timeRunning = this.unitsDistanceMin / this.selectedWeapon.RunningSpeed;
double timeWalking = this.unitsDistance / (this.selectedWeapon.RunningSpeed * SteamShared.CsgoHelper.WalkModifier); double timeWalking = this.unitsDistanceMin / (this.selectedWeapon.RunningSpeed * SteamShared.CsgoHelper.WalkModifier);
double timeCrouching = this.unitsDistance / (this.selectedWeapon.RunningSpeed * SteamShared.CsgoHelper.DuckModifier); double timeCrouching = this.unitsDistanceMin / (this.selectedWeapon.RunningSpeed * SteamShared.CsgoHelper.DuckModifier);
this.txtTimeRunning.Text = getTimeStringFromSeconds(timeRunning); this.txtTimeRunning.Text = getTimeStringFromSeconds(timeRunning);
this.txtTimeWalking.Text = getTimeStringFromSeconds(timeWalking); this.txtTimeWalking.Text = getTimeStringFromSeconds(timeWalking);
@ -1125,7 +1183,7 @@ namespace Damage_Calculator
double absorbedDamageByArmor = 0; double absorbedDamageByArmor = 0;
bool wasArmorHit = false; bool wasArmorHit = false;
if (this.unitsDistance > this.selectedWeapon.MaxBulletRange) if (this.unitsDistanceMin > this.selectedWeapon.MaxBulletRange)
{ {
damage = 0; damage = 0;
txtResult.Text = txtResultArmor.Text = damage.ToString(); txtResult.Text = txtResultArmor.Text = damage.ToString();
@ -1133,7 +1191,7 @@ namespace Damage_Calculator
} }
// Range // 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) switch (this.selectedWeapon.DamageType)
{ {
@ -1199,22 +1257,45 @@ namespace Damage_Calculator
private void calculateAndUpdateBombDamage() 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 // 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 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; 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 // 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, the radius increases a bit // 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 bounding box of the player which is 32x32 units in the horizontal axes // 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 // Get mins and maxs of player hitbox
// Mins is X - 16, Y - 16, Z // 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 }; 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); float headHeight = (float)(this.radioPlayerCrouched.IsChecked == true ? heightCrouching : heightStanding);
// Maxs is X + 16, Y + 16, Z + head height // 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 }; 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) if (!playerIsInRange)
{ {
txtResult.Text = txtResultArmor.Text = "0"; hpDamage = 0;
armorDamage = 0;
return; return;
} }
// Now we can calculate the damage... // From player origin + offset, to the bomb origin
double flDistanceToLocalPlayer = distance;
const double damagePercentage = 1.0d;
// From player origin + eye level, to the bomb
double flDistanceToLocalPlayer = (double)this.unitsDistance;// ((c4bomb origin + viewoffset) - (localplayer origin + viewoffset))
// This defines the width of the curve, a smaller value gives a steeper curve and a faster falloff // This defines the width of the curve, a smaller value gives a steeper curve and a faster falloff
double fSigma = flBombRadius / 3.0d; double fSigma = flBombRadius / 3.0d;
double fGaussianFalloff = Math.Exp(-flDistanceToLocalPlayer * flDistanceToLocalPlayer / (2.0d * fSigma * fSigma)); 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; double flAdjustedDamageBeforeArmor = flAdjustedDamage;
if (chkArmorAny.IsChecked == true) if (chkArmorAny.IsChecked == true)
{ {
flAdjustedDamage = scaleDamageArmor(flAdjustedDamage, 100); flAdjustedDamage = scaleDamageArmor(flAdjustedDamage, armorValue);
wasArmorHit = true; armorDamage = flAdjustedDamage >= 1 ? Math.Ceiling((flAdjustedDamageBeforeArmor - flAdjustedDamage) / 2f) : 0;
} }
txtResult.Text = ((int)flAdjustedDamage).ToString(); hpDamage = flAdjustedDamage;
double roundedDamageToArmor = Math.Ceiling((flAdjustedDamageBeforeArmor - flAdjustedDamage) / 2f);
txtResultArmor.Text = (wasArmorHit && flAdjustedDamage >= 1 ? roundedDamageToArmor : 0).ToString();
} }
/// <summary>
/// Calculates the amount of damage that the bomb deals with a specific amount of armor.
/// </summary>
/// <param name="flDamage">The damage that was dealt to the player by the bomb.</param>
/// <param name="armor_value">The amount of armor that the player had.</param>
/// <returns>the amount of damage that is actually dealt.</returns>
double scaleDamageArmor(double flDamage, int armor_value) double scaleDamageArmor(double flDamage, int armor_value)
{ {
double flArmorRatio = 0.5d; double flArmorRatio = 0.5d;
@ -1311,6 +1391,14 @@ namespace Damage_Calculator
this.changeTheme(Globals.Settings.Theme); this.changeTheme(Globals.Settings.Theme);
} }
/// <summary>
/// Takes the given <see cref="NavArea"/> 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.
/// </summary>
/// <param name="x">The given X of point.</param>
/// <param name="y">The given Y of point.</param>
/// <param name="area">The area that serves as a height reference.</param>
/// <returns>the Z coordinate of the given point in respect to the <see cref="NavArea"/>.</returns>
private float getPointHeightInArea(float x, float y, NavArea area) private float getPointHeightInArea(float x, float y, NavArea area)
{ {
Vector3[][] groups = new Vector3[][] Vector3[][] groups = new Vector3[][]
@ -1356,7 +1444,7 @@ namespace Damage_Calculator
// Height to be displayed further down, depending on area chosen // Height to be displayed further down, depending on area chosen
float newZ = 0; float newZ = 0;
if (this.loadedMap.NavMesh?.Header?.NavAreas != null) if (this.loadedMap?.NavMesh?.Header?.NavAreas != null)
{ {
var navAreasFound = new List<NavArea>(); var navAreasFound = new List<NavArea>();
@ -1385,7 +1473,7 @@ namespace Damage_Calculator
// Or the amount of areas hovered over has changed. // 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 // 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; this.currentHeightLayer = 0;
else else
{ {
@ -1524,6 +1612,44 @@ namespace Damage_Calculator
} }
} }
/// <summary>
/// Gets the <see cref="NavArea"/> of the <see cref="loadedMap"/> 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.
/// </summary>
/// <param name="point">The point we want to partner with a NAV area.</param>
/// <returns>The <see cref="NavArea"/> belonging to the point.</returns>
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() private void fillWeaponInfo()
{ {
if(this.selectedWeapon != null) if(this.selectedWeapon != null)
@ -1556,7 +1682,334 @@ namespace Damage_Calculator
x.Text = placeholderText; 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 #region events
private void CsgoSocket_OnDisconnect(object sender, EventArgs e)
{
this.setPlayerConnected(false);
}
private void rightZoomBorder_SizeChanged(object sender, SizeChangedEventArgs e) private void rightZoomBorder_SizeChanged(object sender, SizeChangedEventArgs e)
{ {
if (rightZoomBorder.IsZoomed) if (rightZoomBorder.IsZoomed)
@ -1579,8 +2032,8 @@ namespace Damage_Calculator
this.DrawMode = eDrawMode.Shooting; this.DrawMode = eDrawMode.Shooting;
if (this.IsInitialized) if (this.IsInitialized)
{ {
this.stackArmorSeparated.Visibility = this.stackAreaHit.Visibility = this.stackWeaponUsed.Visibility = Visibility.Visible; this.stackArmorSeparated.Visibility = this.stackAreaHit.Visibility = this.stackWeaponUsed.Visibility = this.stackDamageFirearm.Visibility = Visibility.Visible;
this.stackPlayerStance.Visibility = this.lblPlayerStance.Visibility = this.chkArmorAny.Visibility = Visibility.Collapsed; 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; this.DrawMode = eDrawMode.Bomb;
if (this.IsInitialized) if (this.IsInitialized)
{ {
this.stackArmorSeparated.Visibility = this.stackAreaHit.Visibility = this.stackWeaponUsed.Visibility = Visibility.Collapsed; this.stackArmorSeparated.Visibility = this.stackAreaHit.Visibility = this.stackWeaponUsed.Visibility = this.stackDamageFirearm.Visibility = Visibility.Collapsed;
this.stackPlayerStance.Visibility = this.lblPlayerStance.Visibility = this.chkArmorAny.Visibility = Visibility.Visible; 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) private void mapImage_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{ {
if (this.DrawMode == eDrawMode.Shooting) this.setTargetOrBombPoint();
{
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();
}
} }
private void mapImage_MouseRightButtonUp(object sender, MouseButtonEventArgs e) private void mapImage_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
{ {
if (this.playerPoint == null) this.setPlayerPoint();
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();
} }
private void comboBoxMaps_SelectionChanged(object sender, SelectionChangedEventArgs e) private void comboBoxMaps_SelectionChanged(object sender, SelectionChangedEventArgs e)
@ -1808,6 +2183,50 @@ namespace Damage_Calculator
this.canvasReload(); 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 #endregion
} }

View file

@ -41,6 +41,10 @@ namespace Damage_Calculator
public bool ShowMapsMissingNav { get; set; } = true; public bool ShowMapsMissingNav { get; set; } = true;
public bool ShowMapsMissingAin { get; set; } = true; public bool ShowMapsMissingAin { get; set; } = true;
// OTHER
public ushort NetConPort { get; set; } = 2121;
public object Clone() public object Clone()
{ {
return this.MemberwiseClone(); return this.MemberwiseClone();

View file

@ -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);
}
}

View file

@ -70,7 +70,7 @@
<ext:IntegerUpDown x:Name="intCurrentMapCoordsOffsetY" Width="80" Margin="5,0,0,0" Value="0" Background="#222222" Foreground="White" /> <ext:IntegerUpDown x:Name="intCurrentMapCoordsOffsetY" Width="80" Margin="5,0,0,0" Value="0" Background="#222222" Foreground="White" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0"> <StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<Label Content="Current map multiplier override" /> <Label Content="Current map multiplier override:" />
<ext:SingleUpDown x:Name="intCurrentMapMultiplierOverride" Width="80" Margin="5,0,0,0" Value="0" Background="#222222" Foreground="White" /> <ext:SingleUpDown x:Name="intCurrentMapMultiplierOverride" Width="80" Margin="5,0,0,0" Value="0" Background="#222222" Foreground="White" />
<Label Content="0 = off, original =" Margin="5,0,0,0" /> <Label Content="0 = off, original =" Margin="5,0,0,0" />
<Label x:Name="txtCurrentMapMultiplier" Padding="0,5" Content="0" /> <Label x:Name="txtCurrentMapMultiplier" Padding="0,5" Content="0" />
@ -86,6 +86,15 @@
<CheckBox x:Name="mnuShowMapsMissingAin" Content="Show maps with missing AIN file" /> <CheckBox x:Name="mnuShowMapsMissingAin" Content="Show maps with missing AIN file" />
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<GroupBox Header="Misc" BorderBrush="Gray" Margin="0,10,0,0">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<Label Content="NetConPort:" />
<ext:UShortUpDown x:Name="ushortNetConPort" Width="80" Margin="5,0,0,0" Value="2121" Background="#222222" Foreground="White" />
<Label Content="non-zero, default = 2121" Margin="5,0,0,0" />
</StackPanel>
</StackPanel>
</GroupBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="10" Height="25"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="10" Height="25">
<Button x:Name="btnSave" Content="Save" Margin="0,0,5,0" Padding="10,0" Background="Green" Click="btnSave_Click" /> <Button x:Name="btnSave" Content="Save" Margin="0,0,5,0" Padding="10,0" Background="Green" Click="btnSave_Click" />
<Button x:Name="btnCancel" Content="Cancel" Padding="10,0" Click="btnCancel_Click" /> <Button x:Name="btnCancel" Content="Cancel" Padding="10,0" Click="btnCancel_Click" />

View file

@ -37,6 +37,9 @@ namespace Damage_Calculator
public wndSettings(SteamShared.Models.CsgoMap currentMap) public wndSettings(SteamShared.Models.CsgoMap currentMap)
{ {
InitializeComponent(); 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.currentMap = currentMap;
this.MaxHeight = System.Windows.SystemParameters.PrimaryScreenHeight; this.MaxHeight = System.Windows.SystemParameters.PrimaryScreenHeight;
this.settings = (Settings)Globals.Settings.Clone(); this.settings = (Settings)Globals.Settings.Clone();
@ -98,6 +101,9 @@ namespace Damage_Calculator
this.mnuShowMapsMissingBsp.IsChecked = this.settings.ShowMapsMissingBsp; this.mnuShowMapsMissingBsp.IsChecked = this.settings.ShowMapsMissingBsp;
this.mnuShowMapsMissingNav.IsChecked = this.settings.ShowMapsMissingNav; this.mnuShowMapsMissingNav.IsChecked = this.settings.ShowMapsMissingNav;
this.mnuShowMapsMissingAin.IsChecked = this.settings.ShowMapsMissingAin; this.mnuShowMapsMissingAin.IsChecked = this.settings.ShowMapsMissingAin;
// Other
this.ushortNetConPort.Value = this.settings.NetConPort;
} }
private void saveSettings() private void saveSettings()
@ -154,6 +160,9 @@ namespace Damage_Calculator
this.settings.ShowMapsMissingNav = (bool)this.mnuShowMapsMissingNav.IsChecked; this.settings.ShowMapsMissingNav = (bool)this.mnuShowMapsMissingNav.IsChecked;
this.settings.ShowMapsMissingAin = (bool)this.mnuShowMapsMissingAin.IsChecked; this.settings.ShowMapsMissingAin = (bool)this.mnuShowMapsMissingAin.IsChecked;
// Other
this.settings.NetConPort = this.ushortNetConPort.Value ?? this.settings.NetConPort;
Globals.Settings = this.settings; Globals.Settings = this.settings;
Globals.SaveSettings(); Globals.SaveSettings();
} }
@ -165,6 +174,12 @@ namespace Damage_Calculator
private void btnSave_Click(object sender, RoutedEventArgs e) 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.DialogResult = true; // Tell main window to reload with new settings
this.saveSettings(); this.saveSettings();
this.Close(); this.Close();

View file

@ -2,12 +2,15 @@
using SteamShared.ZatVdfParser; using SteamShared.ZatVdfParser;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Management;
namespace SteamShared namespace SteamShared
{ {
@ -19,6 +22,8 @@ namespace SteamShared
public static readonly float WalkModifier = 0.52f; public static readonly float WalkModifier = 0.52f;
public static readonly int GameID = 730;
/// <summary> /// <summary>
/// Gets the prefixes allowed for maps when using <see cref="GetMaps"/>. /// Gets the prefixes allowed for maps when using <see cref="GetMaps"/>.
/// </summary> /// </summary>
@ -48,7 +53,7 @@ namespace SteamShared
public CsgoHelper() public CsgoHelper()
{ {
// Nothing to do // Nothing to do, don't use this ctor, aside from before the program initialises.
} }
public CsgoHelper(string csgoPath) public CsgoHelper(string csgoPath)
@ -65,6 +70,25 @@ namespace SteamShared
return this.Validate(this.CsgoPath!); 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);
}
/// <summary> /// <summary>
/// Validates files and directories for CS:GO installed in the given path. /// Validates files and directories for CS:GO installed in the given path.
@ -90,7 +114,11 @@ namespace SteamShared
public List<CsgoMap> GetMaps() public List<CsgoMap> GetMaps()
{ {
List<string> 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<CsgoMap>();
List<string> mapTextFiles = Directory.GetFiles(mapOverviewsPath).ToList().Where(f => f.ToLower().EndsWith(".txt")).Where(f =>
this.mapFileNameValid(f)).ToList(); this.mapFileNameValid(f)).ToList();
List<CsgoMap> maps = new List<CsgoMap>(); List<CsgoMap> maps = new List<CsgoMap>();
@ -246,6 +274,35 @@ namespace SteamShared
return maps; return maps;
} }
/// <summary>
/// Gets the launch options of the specified Steam user.
/// </summary>
/// <param name="user">The Steam user of which to get the launch options.</param>
/// <returns>
/// The launch options,
/// or null if an error occurred, or if the passed <see cref="SteamUser.AbsoluteUserdataFolderPath"/> was wrong or <see langword="null"/>
/// </returns>
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<CsgoWeapon> GetWeapons() public List<CsgoWeapon> GetWeapons()
{ {
string filePath = Path.Combine(this.CsgoPath!, "csgo\\scripts\\items\\items_game.txt"); string filePath = Path.Combine(this.CsgoPath!, "csgo\\scripts\\items\\items_game.txt");

View file

@ -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
{
/// <summary>
/// State when first connecting.
/// </summary>
public enum CsgoSocketConnectResult { Success, WrongPassword, Failure }
/// <summary>
/// State when checking if a connection is still up.
/// </summary>
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<CsgoSocketConnectResult> 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<string?> 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<Vector3?> 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<string?> 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();
}
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
@ -12,6 +13,7 @@ namespace SteamShared
public static class Globals public static class Globals
{ {
public static Models.Settings Settings = new Models.Settings(); public static Models.Settings Settings = new Models.Settings();
public static readonly string ArgumentPattern = "[\\\"\"].+?[\\\"\"]|[^ ]+";
public static BitmapImage BitmapToImageSource(Bitmap src) 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) public static float Map(float s, float a1, float a2, float b1, float b2)
{ {
return b1 + (s - a1) * (b2 - b1) / (a2 - a1); return b1 + (s - a1) * (b2 - b1) / (a2 - a1);

View file

@ -11,5 +11,7 @@ namespace SteamShared.Models
public SteamHelper SteamHelper = new SteamHelper(); public SteamHelper SteamHelper = new SteamHelper();
public CsgoHelper CsgoHelper = new CsgoHelper(); public CsgoHelper CsgoHelper = new CsgoHelper();
public CsgoSocketConnection CsgoSocket = new CsgoSocketConnection();
} }
} }

View file

@ -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
{
/// <summary>
/// The name the user logs in with.
/// </summary>
public string? AccountName { get; set; }
/// <summary>
/// The persona name the user can change.
/// </summary>
public string? PersonaName { get; set; }
/// <summary>
/// The time of last login in unix time.
/// </summary>
public ulong LastLogin { get; set; }
/// <summary>
/// The long steam ID (starting with 7656...)
/// </summary>
public ulong SteamID64 { get; set; }
/// <summary>
/// The account ID, used for trade links or the userdata folder.
/// It's calculated from the SteamID64.
/// </summary>
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; }
}
}

View file

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using SteamShared.ZatVdfParser; using SteamShared.ZatVdfParser;
using Microsoft.Win32; using Microsoft.Win32;
using SteamShared.Models; using SteamShared.Models;
using System.Diagnostics;
namespace SteamShared namespace SteamShared
{ {
@ -14,6 +15,7 @@ namespace SteamShared
{ {
private string? steamPath; private string? steamPath;
private List<SteamLibrary>? steamLibraries; private List<SteamLibrary>? steamLibraries;
public List<SteamGame>? InstalledGames;
/// <summary> /// <summary>
/// Gets the absolute path to the Steam install directory. /// Gets the absolute path to the Steam install directory.
@ -142,6 +144,14 @@ namespace SteamShared
#endif #endif
} }
public void UpdateInstalledGames(bool force = false)
{
if (!force && this.InstalledGames != null)
return;
this.InstalledGames = this.GetInstalledGames();
}
public List<SteamGame>? GetInstalledGames() public List<SteamGame>? GetInstalledGames()
{ {
var steamLibraries = this.GetSteamLibraries(); var steamLibraries = this.GetSteamLibraries();
@ -230,6 +240,87 @@ namespace SteamShared
return null; return null;
} }
/// <summary>
/// Gets the most recently logged in Steam user, based on the "MostRecent" value.
/// </summary>
/// <returns>
/// The most recent logged in Steam user, or <see langword="null"/>, if none has been found or an error has occurred.
/// </returns>
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;
}
/// <summary>
/// Starts the given steam game, with the given additional arguments, if possible.
/// </summary>
/// <param name="gameID">The ID of the game.</param>
/// <param name="arguments">
/// 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.
/// </param>
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 #region Private Methods
private bool isAppManifestFile(string filePath) private bool isAppManifestFile(string filePath)
{ {

View file

@ -28,6 +28,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="6.0.0" /> <PackageReference Include="System.Drawing.Common" Version="6.0.0" />
<PackageReference Include="System.Management" Version="7.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="SourceConfig\test_source.cfg"> <None Update="SourceConfig\test_source.cfg">

View file

@ -41,6 +41,9 @@ namespace SteamShared.ZatVdfParser
} }
private void Parse(string filePathOrText, bool parseTextDirectly) private void Parse(string filePathOrText, bool parseTextDirectly)
{ {
if (!parseTextDirectly && !File.Exists(filePathOrText))
return;
Element? currentLevel = null; Element? currentLevel = null;
// Generate stream from string in case we want to read it directly, instead of using a file stream (boolean parameter) // 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; return;
line = line.Trim(); 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("//")) if (line.StartsWith("//"))
{ {
@ -104,6 +109,55 @@ namespace SteamShared.ZatVdfParser
} }
#endregion #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<string> splitStrings = new List<string>();
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 #region OPERATORS
public Element this[int key] public Element this[int key]
{ {