mirror of
https://github.com/MathiasLui/CSGO-Projects.git
synced 2025-05-06 22:01:18 +00:00
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:
parent
3e94f4c622
commit
c449cd1bf9
15 changed files with 1232 additions and 140 deletions
|
@ -51,6 +51,7 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Windows.Compatibility" Version="6.0.0" />
|
||||
<PackageReference Include="System.Management" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="wndSettings.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 @@
|
|||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="160" />
|
||||
<ColumnDefinition Width="170" />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="250" />
|
||||
</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="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" />
|
||||
<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>
|
||||
<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" />
|
||||
<ComboBox x:Name="comboWeapons" MinWidth="100" MaxWidth="200" HorizontalAlignment="Left" SelectionChanged="comboWeapons_SelectionChanged" />
|
||||
</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 x:Name="txtResult" Text="0" Foreground="IndianRed" FontSize="18" />
|
||||
<TextBlock x:Name="txtResultArmor" Text="0" Foreground="CadetBlue" FontSize="14" />
|
||||
</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">
|
||||
<TextBlock Text="Distance moved.." FontSize="14" FontWeight="Bold" />
|
||||
<StackPanel Margin="0,10,0,0">
|
||||
|
@ -102,6 +114,19 @@
|
|||
<TextBlock x:Name="txtTimeCrouching" Text="None" TextWrapping="Wrap" />
|
||||
</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>
|
||||
<local:ZoomBorder x:Name="rightZoomBorder" Grid.Row="1" Grid.Column="1" Margin="10" ClipToBounds="True" SizeChanged="rightZoomBorder_SizeChanged">
|
||||
|
|
|
@ -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;
|
||||
/// <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>
|
||||
/// 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()
|
||||
/// <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
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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 flArmorRatio = 0.5d;
|
||||
|
@ -1311,6 +1391,14 @@ namespace Damage_Calculator
|
|||
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)
|
||||
{
|
||||
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<NavArea>();
|
||||
|
||||
|
@ -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
|
|||
}
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
16
DamageCalculator/DamageCalculator/ShowMessage.cs
Normal file
16
DamageCalculator/DamageCalculator/ShowMessage.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -70,7 +70,7 @@
|
|||
<ext:IntegerUpDown x:Name="intCurrentMapCoordsOffsetY" Width="80" Margin="5,0,0,0" Value="0" Background="#222222" Foreground="White" />
|
||||
</StackPanel>
|
||||
<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" />
|
||||
<Label Content="0 = off, original =" Margin="5,0,0,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" />
|
||||
</StackPanel>
|
||||
</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">
|
||||
<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" />
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the prefixes allowed for maps when using <see cref="GetMaps"/>.
|
||||
/// </summary>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates files and directories for CS:GO installed in the given path.
|
||||
|
@ -90,7 +114,11 @@ namespace SteamShared
|
|||
|
||||
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();
|
||||
|
||||
List<CsgoMap> maps = new List<CsgoMap>();
|
||||
|
@ -246,6 +274,35 @@ namespace SteamShared
|
|||
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()
|
||||
{
|
||||
string filePath = Path.Combine(this.CsgoPath!, "csgo\\scripts\\items\\items_game.txt");
|
||||
|
|
315
SteamShared/SteamShared/SteamShared/CsgoSocketConnection.cs
Normal file
315
SteamShared/SteamShared/SteamShared/CsgoSocketConnection.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -11,5 +11,7 @@ namespace SteamShared.Models
|
|||
public SteamHelper SteamHelper = new SteamHelper();
|
||||
|
||||
public CsgoHelper CsgoHelper = new CsgoHelper();
|
||||
|
||||
public CsgoSocketConnection CsgoSocket = new CsgoSocketConnection();
|
||||
}
|
||||
}
|
||||
|
|
51
SteamShared/SteamShared/SteamShared/Models/SteamUser.cs
Normal file
51
SteamShared/SteamShared/SteamShared/Models/SteamUser.cs
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -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<SteamLibrary>? steamLibraries;
|
||||
public List<SteamGame>? InstalledGames;
|
||||
|
||||
/// <summary>
|
||||
/// 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<SteamGame>? GetInstalledGames()
|
||||
{
|
||||
var steamLibraries = this.GetSteamLibraries();
|
||||
|
@ -230,6 +240,87 @@ namespace SteamShared
|
|||
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
|
||||
private bool isAppManifestFile(string filePath)
|
||||
{
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
||||
<PackageReference Include="System.Management" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="SourceConfig\test_source.cfg">
|
||||
|
|
|
@ -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<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
|
||||
public Element this[int key]
|
||||
{
|
||||
|
|
Loading…
Add table
Reference in a new issue