Slightly cleaned up CsgoHelper

This commit is contained in:
Mathias Lui 2023-08-26 15:43:21 +02:00
parent 9f86e4433e
commit a05d9785bd
2 changed files with 142 additions and 56 deletions

View file

@ -27,8 +27,11 @@ namespace SteamShared
public static readonly int GameID = 730; 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>
/// <remarks>
/// If adjusted, <see cref="GetMaps"/> should also be adjusted to set the type.
/// </remarks>
private readonly string[] validMapPrefixes = new[] private readonly string[] validMapPrefixes = new[]
{ {
"de", "de",
@ -38,7 +41,7 @@ namespace SteamShared
}; };
/// <summary> /// <summary>
/// Gets the files relative to the CS:GO install path that are checked when <see cref="Validate(string)">validating</see>. /// Gets the files relative to the CS:GO install path that are checked when <see cref="Validate(string)">validating</see>.
/// </summary> /// </summary>
private readonly string[] filesToValidate = new[] private readonly string[] filesToValidate = new[]
{ {
@ -46,7 +49,7 @@ namespace SteamShared
}; };
/// <summary> /// <summary>
/// Gets the directories relative to the CS:GO install path that are checked when <see cref="Validate(string)">validating</see>. /// Gets the directories relative to the CS:GO install path that are checked when <see cref="Validate(string)">validating</see>.
/// </summary> /// </summary>
private readonly string[] directoriesToValidate = new[] private readonly string[] directoriesToValidate = new[]
{ {
@ -58,13 +61,17 @@ namespace SteamShared
// Nothing to do, don't use this ctor, aside from before the program initialises. // Nothing to do, don't use this ctor, aside from before the program initialises.
} }
/// <summary>
/// Creates a new instance.
/// </summary>
/// <param name="csgoPath">the root path where CS:GO is installed.</param>
public CsgoHelper(string csgoPath) public CsgoHelper(string csgoPath)
{ {
this.CsgoPath = csgoPath; this.CsgoPath = csgoPath;
} }
/// <summary> /// <summary>
/// Validates files and directories for CS:GO installed in the <see cref="CsgoPath">path</see>. /// Validates files and directories for CS:GO installed in the <see cref="CsgoPath">path</see>.
/// </summary> /// </summary>
/// <returns>whether the files and directories exist.</returns> /// <returns>whether the files and directories exist.</returns>
public bool Validate() public bool Validate()
@ -72,6 +79,10 @@ namespace SteamShared
return this.Validate(this.CsgoPath!); return this.Validate(this.CsgoPath!);
} }
/// <summary>
/// Gets a handle to the currently running CS:GO instance.
/// </summary>
/// <returns>a tuple containing the process handle, as well as the arguments it was started with, either can be null.</returns>
public (Process?,string?) GetRunningCsgo() public (Process?,string?) GetRunningCsgo()
{ {
// This is used as the normal process handle // This is used as the normal process handle
@ -93,8 +104,12 @@ namespace SteamShared
} }
/// <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.
/// </summary> /// </summary>
/// <remarks>
/// Directories and files that are checked for
/// are specified in <see cref="filesToValidate"/> and <see cref="directoriesToValidate"/>.
/// </remarks>
/// <param name="csgoPath">The path to the CS:GO install directory, in which the executable resides.</param> /// <param name="csgoPath">The path to the CS:GO install directory, in which the executable resides.</param>
/// <returns>whether the files and directories exist.</returns> /// <returns>whether the files and directories exist.</returns>
public bool Validate(string csgoPath) public bool Validate(string csgoPath)
@ -114,16 +129,28 @@ namespace SteamShared
return true; return true;
} }
/// <summary>
/// Gets all maps and tries to fill all the information it can find about it.
/// </summary>
/// <remarks>
/// Note that <see cref="CsgoPath"/> can't be null for this.
/// </remarks>
/// <returns>a list of all CS:GO maps.</returns>
public List<CsgoMap> GetMaps() public List<CsgoMap> GetMaps()
{ {
string mapOverviewsPath = System.IO.Path.Combine(this.CsgoPath!, "csgo", "resource", "overviews"); if (this.CsgoPath is null)
if (!Directory.Exists(mapOverviewsPath)) return new();
return new List<CsgoMap>();
string mapOverviewsPath = System.IO.Path.Combine(this.CsgoPath, "csgo", "resource", "overviews");
if (!Directory.Exists(mapOverviewsPath))
return new();
// Get list of all map text files which have a map type that is allowed
List<string> mapTextFiles = Directory.GetFiles(mapOverviewsPath).ToList().Where(f => f.ToLower().EndsWith(".txt")).Where(f => 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();
foreach (string file in mapTextFiles) foreach (string file in mapTextFiles)
{ {
@ -133,6 +160,7 @@ namespace SteamShared
string potentialRadarFile = System.IO.Path.Combine(this.CsgoPath!, "csgo\\resource\\overviews", System.IO.Path.GetFileNameWithoutExtension(file) + "_radar.dds"); string potentialRadarFile = System.IO.Path.Combine(this.CsgoPath!, "csgo\\resource\\overviews", System.IO.Path.GetFileNameWithoutExtension(file) + "_radar.dds");
if (File.Exists(potentialRadarFile)) if (File.Exists(potentialRadarFile))
{ {
// Radar file exists, set it (radar image visible on minimap)
map.MapImagePath = potentialRadarFile; map.MapImagePath = potentialRadarFile;
} }
@ -140,6 +168,7 @@ namespace SteamShared
string potentialBspFile = System.IO.Path.Combine(this.CsgoPath!, "csgo\\maps", System.IO.Path.GetFileNameWithoutExtension(file) + ".bsp"); string potentialBspFile = System.IO.Path.Combine(this.CsgoPath!, "csgo\\maps", System.IO.Path.GetFileNameWithoutExtension(file) + ".bsp");
if (File.Exists(potentialBspFile)) if (File.Exists(potentialBspFile))
{ {
// Map file exists, set it (actual compiled map file)
map.BspFilePath = potentialBspFile; map.BspFilePath = potentialBspFile;
} }
@ -147,6 +176,7 @@ namespace SteamShared
string potentialNavFile = System.IO.Path.Combine(this.CsgoPath!, "csgo\\maps", System.IO.Path.GetFileNameWithoutExtension(file) + ".nav"); string potentialNavFile = System.IO.Path.Combine(this.CsgoPath!, "csgo\\maps", System.IO.Path.GetFileNameWithoutExtension(file) + ".nav");
if (File.Exists(potentialNavFile)) if (File.Exists(potentialNavFile))
{ {
// NAV file exists, set it (bot navigation mesh file used for calculating movement routes)
map.NavFilePath = potentialNavFile; map.NavFilePath = potentialNavFile;
} }
@ -154,10 +184,11 @@ namespace SteamShared
string potentialAinFile = System.IO.Path.Combine(this.CsgoPath!, "csgo\\maps\\graphs", System.IO.Path.GetFileNameWithoutExtension(file) + ".ain"); string potentialAinFile = System.IO.Path.Combine(this.CsgoPath!, "csgo\\maps\\graphs", System.IO.Path.GetFileNameWithoutExtension(file) + ".ain");
if (File.Exists(potentialAinFile)) if (File.Exists(potentialAinFile))
{ {
// AIN file exists, set it (similar to NAV file, probably for older games for NPCs, probably not very important)
map.AinFilePath = potentialAinFile; map.AinFilePath = potentialAinFile;
} }
// Set map type // Set map type (if adjusted, validMapPrefixes should also be adjusted)
switch (System.IO.Path.GetFileNameWithoutExtension(file).Split('_').First().ToLower()) switch (System.IO.Path.GetFileNameWithoutExtension(file).Split('_').First().ToLower())
{ {
case "de": case "de":
@ -177,7 +208,7 @@ namespace SteamShared
break; break;
} }
// Get properties from accompanying text file // Get properties from accompanying text file (could be made prettier)
var vdf = new VDFFile(file); var vdf = new VDFFile(file);
if (vdf.RootElements.Count > 0) if (vdf.RootElements.Count > 0)
{ {
@ -228,7 +259,7 @@ namespace SteamShared
} }
} }
// Save map name without prefix // Save map name without prefix (e.g. de_dust2 is dust2)
map.MapFileName = System.IO.Path.GetFileNameWithoutExtension(file).Split('_').Last(); map.MapFileName = System.IO.Path.GetFileNameWithoutExtension(file).Split('_').Last();
Bitmap image; Bitmap image;
@ -259,7 +290,9 @@ namespace SteamShared
continue; continue;
// Some workaround I found online for some thread error I forgot // Some workaround I found online for some thread error I forgot
// Future self: We probably want to execute it on the thread that owns the image // @Future self: We probably want to execute it on the thread that owns the image
//
// Hey, future self here, we just make sure that the code is invoked on the UI thread, to access the map object
System.Windows.Application.Current.Dispatcher.Invoke((Action)delegate System.Windows.Application.Current.Dispatcher.Invoke((Action)delegate
{ {
map.MapImage = Globals.BitmapToImageSource(image); map.MapImage = Globals.BitmapToImageSource(image);
@ -272,12 +305,12 @@ namespace SteamShared
} }
/// <summary> /// <summary>
/// Gets the launch options of the specified Steam user. /// Gets the launch options of the specified Steam user.
/// </summary> /// </summary>
/// <param name="user">The Steam user of which to get the launch options.</param> /// <param name="user">The Steam user of which to get the launch options.</param>
/// <returns> /// <returns>
/// The launch options, /// the launch options,
/// or null if an error occurred, or if the passed <see cref="SteamUser.AbsoluteUserdataFolderPath"/> was wrong or <see langword="null"/> /// or null if an error occurred, or if the passed <see cref="SteamUser.AbsoluteUserdataFolderPath"/> was <see langword="null"/> or wrong.
/// </returns> /// </returns>
public string? GetLaunchOptions(SteamUser user) public string? GetLaunchOptions(SteamUser user)
{ {
@ -300,19 +333,24 @@ namespace SteamShared
return Regex.Replace(launchOptions, "\\\\(.)", "$1"); return Regex.Replace(launchOptions, "\\\\(.)", "$1");
} }
public List<CsgoWeapon> GetWeapons() /// <summary>
/// Gets a list of all fireable weapons, including their stats.
/// </summary>
/// <returns>the list of weapons, or <see langword="null"/> if something went wrong.</returns>
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");
if (!File.Exists(filePath)) if (!File.Exists(filePath))
return null!; return null;
var vdfItems = new VDFFile(filePath); var vdfItems = new VDFFile(filePath);
Element prefabs = vdfItems["items_game"]?["prefabs"]!; Element prefabs = vdfItems["items_game"]?["prefabs"]!;
Element items = vdfItems["items_game"]?["items"]!; Element items = vdfItems["items_game"]?["items"]!;
if (prefabs == null || items == null) if (prefabs is null || items is null)
// There is no prefab list or item list to read out // There is no prefab list or item list to read out
return null!; return null;
var weapons = new List<CsgoWeapon>(); var weapons = new List<CsgoWeapon>();
@ -321,10 +359,13 @@ namespace SteamShared
string? itemPrefab = item["prefab"]?.Value!; string? itemPrefab = item["prefab"]?.Value!;
string? itemName = item["name"].Value; string? itemName = item["name"].Value;
if (itemPrefab == null || !itemName!.StartsWith("weapon_")) if (itemPrefab is null || itemName is null
|| !itemName.StartsWith("weapon_"))
{
continue; continue;
}
// Here we're sure the item is supposed to be a weapon // At this point we're sure the item is supposed to be a weapon
var weapon = new CsgoWeapon(); var weapon = new CsgoWeapon();
weapon.ClassName = itemName; weapon.ClassName = itemName;
@ -350,11 +391,17 @@ namespace SteamShared
return weapons; return weapons;
} }
/// <summary>
/// Gets whether a given weapon can be fired (Hint: You can't shoot knives, at least not in the usual sense).
/// </summary>
/// <param name="weapon">The weapon to check.</param>
/// <param name="prefabTrace">The prefab trace (like a callstack of prefabs getting more and more abstract).</param>
/// <returns>whether the weapon is fireable.</returns>
private bool isWeaponFireable(CsgoWeapon weapon, List<string>? prefabTrace) private bool isWeaponFireable(CsgoWeapon weapon, List<string>? prefabTrace)
{ {
bool isWhitelisted = false; bool isWhitelisted = false;
if(prefabTrace != null) if (prefabTrace != null)
{ {
// Stuff involving prefab trace here // Stuff involving prefab trace here
if(prefabTrace.FirstOrDefault(pr => pr == "primary" || pr == "secondary") != null) if(prefabTrace.FirstOrDefault(pr => pr == "primary" || pr == "secondary") != null)
@ -363,14 +410,34 @@ namespace SteamShared
} }
// Other // Other
if(weapon.ClassName == "weapon_taser") if (weapon.ClassName == "weapon_taser")
// Allow zeus, even though it's "equipment" and listed in the melee slot // Allow zeus, even though it's "equipment" and listed in the melee slot
isWhitelisted = true; isWhitelisted = true;
return isWhitelisted; return isWhitelisted;
} }
private bool tryPopulateWeapon(CsgoWeapon weapon, Element prefabs, string prefabName, List<string>? prefabTrace = null) /// <inheritdoc cref="tryPopulateWeapon(CsgoWeapon, Element, string, List{string}?)"/>
private bool tryPopulateWeapon(CsgoWeapon weapon, Element prefabs, string prefabName)
{
return tryPopulateWeapon(weapon, prefabs, prefabName, null);
}
/// <summary>
/// Tries to populate a weapon with in-game stats.
/// </summary>
/// <param name="weapon">The weapon to be populated.</param>
/// <param name="prefabs">All prefabs that exist.</param>
/// <param name="prefabName">The name of the next (more abstract) prefab to check.</param>
/// <param name="prefabTrace">
/// This parameter has to be <see langword="null"/> and will be set by the function itself recursively.
/// Always use <see cref="tryPopulateWeapon(CsgoWeapon, Element, string)"/> instead.
/// </param>
/// <returns>
/// <see langword="true"/>, if all information could be gathered or we have reached a wall,
/// <see langword="false"/>, if the item is no weapon, or the weapon is not fireable, and its stats shouldn't be used (e.g. knives).
/// </returns>
private bool tryPopulateWeapon(CsgoWeapon weapon, Element prefabs, string prefabName, List<string>? prefabTrace)
{ {
// Get the initial prefab specified in the item // Get the initial prefab specified in the item
Element prefab = prefabs[prefabName]; Element prefab = prefabs[prefabName];
@ -395,7 +462,7 @@ namespace SteamShared
Element attributes = prefab["attributes"]; Element attributes = prefab["attributes"];
if (attributes == null && nextPrefab != null) if (attributes is null && nextPrefab is not null)
// it might have no attributes but just skip to the next prefab in that case, because they might have attributes // it might have no attributes but just skip to the next prefab in that case, because they might have attributes
// one example of this is the taser (zeus) // one example of this is the taser (zeus)
return this.tryPopulateWeapon(weapon, prefabs, nextPrefab, prefabTrace); return this.tryPopulateWeapon(weapon, prefabs, nextPrefab, prefabTrace);
@ -524,11 +591,19 @@ namespace SteamShared
if (prefabTrace == null) if (prefabTrace == null)
prefabTrace = new List<string>(); prefabTrace = new List<string>();
prefabTrace.Add(prefab.Name!); prefabTrace.Add(prefab.Name ?? "unknown prefab name");
return this.tryPopulateWeapon(weapon, prefabs, nextPrefab, prefabTrace); return this.tryPopulateWeapon(weapon, prefabs, nextPrefab, prefabTrace);
} }
/// <summary>
/// Checks whether the file name of map at the specified path is valid.
/// </summary>
/// <remarks>
/// It's seen as valid, if the prefix is contained in <see cref="validMapPrefixes"/>.
/// </remarks>
/// <param name="mapPath">The map path to be checked.</param>
/// <returns>whether the map type is valid.</returns>
private bool mapFileNameValid(string mapPath) private bool mapFileNameValid(string mapPath)
{ {
string fileName = Path.GetFileName(mapPath.ToLower()); string fileName = Path.GetFileName(mapPath.ToLower());
@ -543,11 +618,11 @@ namespace SteamShared
} }
/// <summary> /// <summary>
/// Reads entity list from uncompressed BSP file. /// Reads entity list from uncompressed BSP file.
/// </summary> /// </summary>
/// <param name="bspFilePath">The absolute path to the BSP file.</param> /// <param name="bspFilePath">The absolute path to the BSP file.</param>
/// <returns>the entity list, null if actual length differed from length specified in file, or a general error occurred.</returns> /// <returns>the entity list, null if actual length differed from length specified in file, or a general error occurred.</returns>
public string ReadEntityListFromBsp(string bspFilePath) public string? ReadEntityListFromBsp(string bspFilePath)
{ {
using(var bspFile = File.OpenRead(bspFilePath)) using(var bspFile = File.OpenRead(bspFilePath))
{ {
@ -568,25 +643,25 @@ namespace SteamShared
} }
} }
} }
return null!; return null;
} }
/// <summary> /// <summary>
/// Reads packed files from a BSP and returns whether any 1. NAV or 2. AIN files were found. /// Reads packed files from a BSP and checks for NAV and AIN files.
/// </summary> /// </summary>
/// <param name="bspFilePath">The absolute path to the BSP file.</param> /// <param name="bspFilePath">The absolute path to the BSP file.</param>
/// <returns>A tuple containing whether nav or ain files were found, in that order.</returns> /// <returns>a tuple containing whether NAV or AIN files were found, as well as the parsed NAV mesh.</returns>
public (bool, bool, NavMesh) ReadIfPackedNavFilesInBsp(string bspFilePath) public (bool NavFilesFound, bool AinFilesFound, NavMesh? NavMesh) ReadIfPackedNavFilesInBsp(string bspFilePath)
{ {
bool navFound = false; bool navFound = false;
bool ainFound = false; bool ainFound = false;
byte[] readZipBytes = null!; byte[]? readZipBytes = null;
using (var bspFile = File.OpenRead(bspFilePath)) using (var bspFile = File.OpenRead(bspFilePath))
{ {
using (var reader = new BinaryReader(bspFile)) using (var reader = new BinaryReader(bspFile))
{ {
// Stuff before lumps + pakfile index * lump array item length // Skip stuff before (lumps + (pakfile index * lump array item length))
reader.BaseStream.Position = 8 + (40 * 16); reader.BaseStream.Position = 8 + (40 * 16);
// Get lump pos and size // Get lump pos and size
@ -600,34 +675,44 @@ namespace SteamShared
} }
if (readZipBytes == null) if (readZipBytes == null)
return (false, false, null!); return (false, false, null);
using (var stream = new MemoryStream(readZipBytes)) using var stream = new MemoryStream(readZipBytes);
// Packed files are contained in a ZIP file within the BSP file
using var zip = new ZipArchive(stream, ZipArchiveMode.Read);
NavMesh? nav = null;
foreach (var entry in zip.Entries)
{ {
using(var zip = new ZipArchive(stream, ZipArchiveMode.Read)) if (entry.FullName.ToLower().EndsWith(".nav"))
{ {
NavMesh? nav = null; // Found a packed NAV file
foreach (var entry in zip.Entries) navFound = true;
{ nav = NavFile.Parse(entry.Open());
if (entry.FullName.EndsWith(".nav")) }
{ if (entry.FullName.ToLower().EndsWith(".ain"))
// Found a packed NAV file // Found a packed AIN file
navFound = true; ainFound = true;
nav = NavFile.Parse(entry.Open());
}
if(entry.FullName.EndsWith(".ain"))
// Found a packed AIN file
ainFound = true;
if (navFound && ainFound) if (navFound && ainFound)
// If both already found, return prematurely {
return (true, true, nav!); // If both are already found, return prematurely,
} // as we don't care how many there are
return (navFound, ainFound, nav!); return (navFound, ainFound, nav);
} }
} }
// We get here, if either no NAV or no AIN file has been found
// If navFound is null, nav will be null as well
return (navFound, ainFound, nav);
} }
/// <summary>
/// Checks whether a lump is unused (all zeroes).
/// </summary>
/// <param name="lump">The lump to check.</param>
/// <returns>a bool indicating if the lump is unused.</returns>
private bool isLumpUnused(byte[] lump) private bool isLumpUnused(byte[] lump)
{ {
for(int i = 0; i < lump.Length; i++) for(int i = 0; i < lump.Length; i++)
@ -635,6 +720,7 @@ namespace SteamShared
if (lump[i] != 0) if (lump[i] != 0)
return false; return false;
} }
return true; return true;
} }
} }

View file

@ -115,7 +115,7 @@ namespace SteamShared
return null; return null;
} }
foreach (string foundLib in foundSteamLibraries!) foreach (string foundLib in foundSteamLibraries)
{ {
// All paths in the file are escaped // All paths in the file are escaped
allLibraries.Add(new SteamLibrary(foundLib.Replace("\\\\", "\\"))); allLibraries.Add(new SteamLibrary(foundLib.Replace("\\\\", "\\")));