From 435daf6eb6dbd627f0a7eab3b2733f59128b4d66 Mon Sep 17 00:00:00 2001 From: MathiasL Date: Thu, 26 May 2022 01:37:51 +0200 Subject: [PATCH] Make damage printer GUI features work * Fixed a SteamHelper method that's not used anyways that threw exceptions --- .../DamagePrinterGUI/DamagePrinterGUI.csproj | 4 + DamagePrinter/DamagePrinterGUI/Globals.cs | 13 + .../DamagePrinterGUI/MainWindow.xaml | 53 +- .../DamagePrinterGUI/MainWindow.xaml.cs | 476 +++++++++++++++++- DamagePrinter/DamagePrinterGUI/Settings.cs | 112 +++++ .../SteamShared/SteamShared/SteamHelper.cs | 24 +- 6 files changed, 664 insertions(+), 18 deletions(-) create mode 100644 DamagePrinter/DamagePrinterGUI/Globals.cs create mode 100644 DamagePrinter/DamagePrinterGUI/Settings.cs diff --git a/DamagePrinter/DamagePrinterGUI/DamagePrinterGUI.csproj b/DamagePrinter/DamagePrinterGUI/DamagePrinterGUI.csproj index 6c847e2..7693050 100644 --- a/DamagePrinter/DamagePrinterGUI/DamagePrinterGUI.csproj +++ b/DamagePrinter/DamagePrinterGUI/DamagePrinterGUI.csproj @@ -10,4 +10,8 @@ False + + + + diff --git a/DamagePrinter/DamagePrinterGUI/Globals.cs b/DamagePrinter/DamagePrinterGUI/Globals.cs new file mode 100644 index 0000000..d9ec0ff --- /dev/null +++ b/DamagePrinter/DamagePrinterGUI/Globals.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DamagePrinterGUI +{ + internal static class Globals + { + public static Settings Settings { get; set; } = new Settings(); + } +} diff --git a/DamagePrinter/DamagePrinterGUI/MainWindow.xaml b/DamagePrinter/DamagePrinterGUI/MainWindow.xaml index ee98ffa..d274a31 100644 --- a/DamagePrinter/DamagePrinterGUI/MainWindow.xaml +++ b/DamagePrinter/DamagePrinterGUI/MainWindow.xaml @@ -1,15 +1,30 @@ - + Style="{DynamicResource CustomWindowStyle}" + Loaded="window_Loaded" + Closing="window_Closing"> - + + + + + + + + + + + + + + @@ -18,7 +33,35 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DamagePrinter/DamagePrinterGUI/MainWindow.xaml.cs b/DamagePrinter/DamagePrinterGUI/MainWindow.xaml.cs index 949007f..0db4657 100644 --- a/DamagePrinter/DamagePrinterGUI/MainWindow.xaml.cs +++ b/DamagePrinter/DamagePrinterGUI/MainWindow.xaml.cs @@ -1,7 +1,13 @@ -using System; +using SteamShared; +using SteamShared.SourceConfig; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; +using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -12,6 +18,7 @@ using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; +using System.Xml.Serialization; namespace DamagePrinterGUI { @@ -20,9 +27,476 @@ namespace DamagePrinterGUI /// public partial class MainWindow : Window { + static readonly uint WM_COPYDATA = 0x004A; + + [DllImport("user32.dll")] + static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern int SendMessage(IntPtr windowHandle, uint message, IntPtr wParam, IntPtr lParam); + + /// + /// Used for databinding. + /// + public Settings Settings { get; set; } = Globals.Settings; + + private static readonly string myDocumentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + private static readonly string folderPath = System.IO.Path.Combine(myDocumentsPath, "CSGO Damage Printer"); + private static readonly string settingsFilePath = System.IO.Path.Combine(folderPath, "settings.xml"); + private static string consoleLogFileName = "console.log"; + private static string? consoleLogFolderPath = null; + public MainWindow() { InitializeComponent(); + + if (!this.ensureConsoleLogAndGamePath()) + { + MessageBox.Show("The console log could not be created.\n\nAs a workaround, try to create an autoexec config and adding the line 'con_logfile console.log' to it.", "Unknown setup error", MessageBoxButton.OK, MessageBoxImage.Error); + this.Close(); + } + + Task.Run(mainLoop); } + + private void mainLoop() + { + string consoleLogPath = System.IO.Path.Combine(consoleLogFolderPath ?? string.Empty, consoleLogFileName); + + var prevTaggedPlayers = new List>(); + + // here we start fresh at line 0 cause it got deleted + while (true) + { + this.Dispatcher.Invoke(() => this.lblConsoleLogFound.Text = "No"); + // Check for the window every now and then + bool initialScan = true; + bool consoleLogExists = false; + + long oldFileSize = 0; + long nextLineOffset = 0; + if (this.findCsgoWindow()) + { + this.Dispatcher.Invoke(() => this.lblCsgoWindowFound.Text = "Yes"); + this.Dispatcher.Invoke(() => this.txtDamageOutput.AppendText("CS:GO Window found." + '\n')); + // We found the window so begin checking for the console + while (true) + { + if (!findCsgoWindow()) + break; + + if (!File.Exists(consoleLogPath)) + // Not yet + continue; + + this.Dispatcher.Invoke(() => this.lblConsoleLogFound.Text = "Yes"); + + if (Globals.Settings == null) + break; + + // Log exists + + bool update = false; + + if (!consoleLogExists && initialScan) + { + consoleLogExists = true; + initialScan = false; + + oldFileSize = 0; + try + { + oldFileSize = new FileInfo(consoleLogPath).Length; + } + catch { } + nextLineOffset = oldFileSize; // bytes from the start of the file, by default not to read anything + } + + long curFileSize = new FileInfo(consoleLogPath).Length; + if (curFileSize != oldFileSize) + { + update = true; + oldFileSize = curFileSize; + } + + if (!update) + continue; + int damageTakenTotal = 0; + + // Read in all the NEW lines of the console log + List lines = new List(); + using (var fs = File.Open(consoleLogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + fs.Position = nextLineOffset; + bool endOfFile = false; + + while (!endOfFile) + { + string line = ""; + bool endOfLine = false; + while (!endOfLine) + { + int nextByte = fs.ReadByte(); + + if (nextByte == -1 && line != "") + { + lines.Add(line); + endOfFile = true; + break; + } + + char nextChar = (char)nextByte; + + if (nextChar == '\n' && line != "") + { + lines.Add(line); + endOfLine = true; + } + else + if (nextChar != '\r') + line += nextChar; + } + } + + lines.ForEach(l => System.Diagnostics.Debug.WriteLine(l)); + + nextLineOffset = fs.Position; + } + + var taggedPlayers = new List>(); + + foreach (string line in lines) + { + /* ------------------------- + Damage Given to "BOT Eugene" - 65 in 2 hits + ------------------------- + Damage Taken from "BOT Eugene" - 117 in 4 hits*/ + if (line.StartsWith("Damage Taken")) + { + Regex regexTaken = new Regex("Damage Taken from \"(.+?)\" - (\\d+) in \\d+ hits?"); + Match takenMatch = regexTaken.Match(line); + + if (takenMatch.Success) + { + damageTakenTotal += int.Parse(takenMatch.Groups[2].Value); + } + } + else if (line.StartsWith("Damage Given")) + { + Regex regexTaken = new Regex("Damage Given to \"(.+?)\" - (\\d+) in (\\d+) hits?"); + Match givenMatch = regexTaken.Match(line); + + if (givenMatch.Success) + { + string name = givenMatch.Groups[1].Value; + int damage = int.Parse(givenMatch.Groups[2].Value); + int hits = int.Parse(givenMatch.Groups[3].Value); + + if ((Globals.Settings.PrintDeadPlayers || damage < 100) && damage > Globals.Settings.MinimumDealtDamage + && taggedPlayers.FirstOrDefault(player => player.Item1 == name) == null) + { + // not in list yet so add + taggedPlayers.Add(new Tuple(name, damage, hits)); + } + } + } + } + + if (damageTakenTotal < Globals.Settings.MinimumReceivedDamage) + { + continue; + } + + if (Globals.Settings.WithholdDuplicateConsoleOutputs && taggedPlayers.Count > 0 && areListsEqual(taggedPlayers, prevTaggedPlayers)) + { + // Last is the same as previous, likely dealt damage, died and now the round ended and it was printed again + Console.WriteLine("Double console output."); + continue; + } + + prevTaggedPlayers = taggedPlayers; + + if (taggedPlayers.Count < 1) + continue; + + string[] commands = new string[taggedPlayers.Count]; + this.Dispatcher.Invoke(() => this.txtDamageOutput.AppendText("\n")); + // We had our minimum damage taken, so print the text + for (int i = 0; i < commands.Length; i++) + { + string textToAdd = string.Empty; + commands[i] += $"{(Globals.Settings.PrintTeamChat ? "say_team" : "say")} \""; + + if (Globals.Settings.UseSpecificTerms) + { + if (taggedPlayers[i].Item2 >= 100) + { + // Dead + textToAdd = $"{taggedPlayers[i].Item1} is dead: {taggedPlayers[i].Item2}"; + } + else if (taggedPlayers[i].Item2 > 90) + { + // One-shot + textToAdd = $"{taggedPlayers[i].Item1} is one-shot: {taggedPlayers[i].Item2}"; + } + else if (taggedPlayers[i].Item2 >= 70) + { + // Lit + textToAdd = $"{taggedPlayers[i].Item1} is lit for {taggedPlayers[i].Item2}"; + } + else + { + // Tagged + textToAdd = $"{taggedPlayers[i].Item1} is tagged for {taggedPlayers[i].Item2}"; + } + } + else + { + textToAdd = $"{taggedPlayers[i].Item1} is hit for {taggedPlayers[i].Item2}"; + } + + if (Globals.Settings.PrintAmountOfShots) + textToAdd += $" in {taggedPlayers[i].Item3}"; + + commands[i] += textToAdd; + + commands[i] += "\""; + + + this.Dispatcher.Invoke(() => + { + if (this.txtDamageOutput.Text.Length > 10_000) + { + // Too much text, so delete some + this.txtDamageOutput.Text = this.txtDamageOutput.Text.Remove(0, this.txtDamageOutput.Text.IndexOf('\n', this.txtDamageOutput.Text.Length - 5_000) + 1); + } + }); + + // Print to local program "console" + if (!Globals.Settings.PrintIngameChat) + textToAdd = $"({textToAdd})"; + + this.Dispatcher.Invoke(() => + { + this.txtDamageOutput.AppendText(textToAdd + '\n'); + }); + } + + // Notify players in-game + if (Globals.Settings.PrintIngameChat) + ExecuteCommands(false, commands); + + // End of each actual check + Thread.Sleep(500); + } + } + else + { + this.Dispatcher.Invoke(() => this.lblCsgoWindowFound.Text = "No"); + // Ensure a smaller log size + File.Delete(consoleLogPath); + } + + prevTaggedPlayers.Clear(); + // End of game-alive-check, check less often + Thread.Sleep(2000); + } + } + + static bool areListsEqual(List> list1, List> list2) + { + if (list1.Count != list2.Count) + return false; + + // still same length + for (int i = 0; i < list1.Count; i++) + { + if (list1[i].Item1 != list2[i].Item1) + return false; + if (list1[i].Item2 != list2[i].Item2) + return false; + if (list1[i].Item3 != list2[i].Item3) + return false; + } + + return true; + } + + static bool ExecuteCommands(bool triggeredByInGameCommand, params string[] cmds) + { + if (cmds == null) + return false; + + IntPtr hWnd = FindWindow("Valve001", null!); + + if (hWnd == IntPtr.Zero) + return false; + + int chatTimeoutMs = 700; + int commandsHandled = 0; + + for (int i = 0; i < cmds.Length; i++) + { + if (cmds[i] == null) + continue; + + cmds[i] = cmds[i].Trim(); + + COPYDATASTRUCT data; + data.dwData = 0; + data.cbData = (uint)cmds[i].Length + 1; + data.lpData = cmds[i]; + + if (triggeredByInGameCommand) + Thread.Sleep(chatTimeoutMs); + + // Allocate for data + IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(data)); + Marshal.StructureToPtr(data, ptr, false); + + int ret = SendMessage(hWnd, WM_COPYDATA, IntPtr.Zero, ptr); + + Console.WriteLine(cmds[i]); + + // Free data + Marshal.FreeHGlobal(ptr); + + if (ret == 0) + commandsHandled++; + + if (cmds[i].StartsWith("say") || cmds[i].StartsWith("say_team")) + Thread.Sleep(chatTimeoutMs); + } + + return cmds.Length > 0 && commandsHandled == cmds.Length; + } + + private bool findCsgoWindow() + { + return FindWindow("Valve001", null!) != IntPtr.Zero; + } + + private void saveSettings() + { + if (!Directory.Exists(folderPath)) + Directory.CreateDirectory(folderPath); + + if (File.Exists(settingsFilePath)) + File.Delete(settingsFilePath); + + XmlSerializer serializer = new XmlSerializer(typeof(Settings)); + + using (var fs = File.Open(settingsFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite)) + { + serializer.Serialize(fs, Globals.Settings); + } + } + + private bool ensureConsoleLogAndGamePath() + { + // Get csgo path + var csgoPath = new SteamHelper().GetGamePathFromExactName("Counter-Strike: Global Offensive"); + + if (csgoPath == null) + { + this.lblCsgoFolderFound.Text = "No"; + return false; + } + else + { + this.lblCsgoFolderFound.Text = "Yes"; + } + + string gamePath = System.IO.Path.Combine(csgoPath, "csgo"); + string configsPath = System.IO.Path.Combine(gamePath, "cfg"); + consoleLogFolderPath = gamePath; // To use later + string autoexecPath = System.IO.Path.Combine(configsPath, "autoexec.cfg"); + + if (!File.Exists(autoexecPath)) + { + // Create autoexec and enable console logging + File.WriteAllText(autoexecPath, "con_logfile " + consoleLogFileName); + ExecuteCommands(false, "exec autoexec"); + return true; + } + else + { + // First create a backup in case our code has a bug that fucks up the autoexec or something + File.Delete(System.IO.Path.Combine(folderPath, "autoexec.backup")); + File.Copy(autoexecPath, System.IO.Path.Combine(folderPath, "autoexec.backup")); + + // Check if autoexec has the command in it. If not, create it + var autoexec = SourceCFG.FromFile(autoexecPath); + + if (autoexec == null) + return false; + + var foundCommand = autoexec.Commands?.FirstOrDefault(line => line.CommandName?.ToLower() == "con_logfile"); + if (foundCommand == null) + { + // line not found so add it. + File.AppendAllText(autoexecPath, Environment.NewLine + "con_logfile " + consoleLogFileName); + ExecuteCommands(false, "exec autoexec"); + } + else + { + // user or we ourself added this command before, so just use the path specified here + string? newConsoleLogFileName = foundCommand.GetValuesAsOne(); + + if (newConsoleLogFileName != null) + { + // We now use the set one, which may differ, or may not + consoleLogFileName = newConsoleLogFileName; + } + } + + return true; + } + } + + private void loadSettings() + { + if (File.Exists(settingsFilePath)) + { + XmlSerializer serializer = new XmlSerializer(typeof(Settings)); + + using (var fs = File.Open(settingsFilePath, FileMode.Open, FileAccess.Read)) + { + Settings? settings = null; + try + { + settings = (Settings?)serializer.Deserialize(fs); + } + catch + { + MessageBox.Show("There was an error loading the settings in " + settingsFilePath + ".\n\nYour settings have been reset to default. If you get this more often please write me an email..Sorry for that :(", "Error loading settings", MessageBoxButton.OK, MessageBoxImage.Error); + } + + if (settings != null) + Globals.Settings.ApplySettingsFrom(settings); + } + } + } + + #region events + private void window_Loaded(object sender, RoutedEventArgs e) + { + // Load settings + this.loadSettings(); + } + + private void window_Closing(object sender, System.ComponentModel.CancelEventArgs e) + { + // Save settings + this.saveSettings(); + } + #endregion + } + + struct COPYDATASTRUCT + { + public ulong dwData; + public uint cbData; + public string lpData; } } diff --git a/DamagePrinter/DamagePrinterGUI/Settings.cs b/DamagePrinter/DamagePrinterGUI/Settings.cs new file mode 100644 index 0000000..7b527a6 --- /dev/null +++ b/DamagePrinter/DamagePrinterGUI/Settings.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace DamagePrinterGUI +{ + public class Settings : DependencyObject, ICloneable + { + public int MinimumDealtDamage + { + get { return this.Dispatcher.Invoke(() => (int)GetValue(MinimumDealtDamageProperty)); } + set { SetValue(MinimumDealtDamageProperty, value); } + } + + // Using a DependencyProperty as the backing store for MinimumDealtDamage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty MinimumDealtDamageProperty = + DependencyProperty.Register("MinimumDealtDamage", typeof(int), typeof(Settings), new PropertyMetadata(20)); + + + public int MinimumReceivedDamage + { + get { return this.Dispatcher.Invoke( () => (int)GetValue(MinimumReceivedDamageProperty)); } + set { SetValue(MinimumReceivedDamageProperty, value); } + } + + // Using a DependencyProperty as the backing store for MinimumReceivedDamage. This enables animation, styling, binding, etc... + public static readonly DependencyProperty MinimumReceivedDamageProperty = + DependencyProperty.Register("MinimumReceivedDamage", typeof(int), typeof(Settings), new PropertyMetadata(100)); + + + public bool PrintDeadPlayers + { + get { return this.Dispatcher.Invoke( () => (bool)GetValue(PrintDeadPlayersProperty)); } + set { SetValue(PrintDeadPlayersProperty, value); } + } + + // Using a DependencyProperty as the backing store for PrintDeadPlayers. This enables animation, styling, binding, etc... + public static readonly DependencyProperty PrintDeadPlayersProperty = + DependencyProperty.Register("PrintDeadPlayers", typeof(bool), typeof(Settings), new PropertyMetadata(false)); + + + public bool WithholdDuplicateConsoleOutputs + { + get { return this.Dispatcher.Invoke( () => (bool)GetValue(WithholdDuplicateConsoleOutputsProperty)); } + set { SetValue(WithholdDuplicateConsoleOutputsProperty, value); } + } + + // Using a DependencyProperty as the backing store for WithholdDuplicateConsoleOutputs. This enables animation, styling, binding, etc... + public static readonly DependencyProperty WithholdDuplicateConsoleOutputsProperty = + DependencyProperty.Register("WithholdDuplicateConsoleOutputs", typeof(bool), typeof(Settings), new PropertyMetadata(true)); + + + public bool PrintAmountOfShots + { + get { return this.Dispatcher.Invoke( () => (bool)GetValue(PrintAmountOfShotsProperty)); } + set { SetValue(PrintAmountOfShotsProperty, value); } + } + + // Using a DependencyProperty as the backing store for PrintAmountOfShots. This enables animation, styling, binding, etc... + public static readonly DependencyProperty PrintAmountOfShotsProperty = + DependencyProperty.Register("PrintAmountOfShots", typeof(bool), typeof(Settings), new PropertyMetadata(false)); + + + public bool UseSpecificTerms + { + get { return this.Dispatcher.Invoke( () => (bool)GetValue(UseSpecificTermsProperty)); } + set { SetValue(UseSpecificTermsProperty, value); } + } + + // Using a DependencyProperty as the backing store for UseSpecificTerms. This enables animation, styling, binding, etc... + public static readonly DependencyProperty UseSpecificTermsProperty = + DependencyProperty.Register("UseSpecificTerms", typeof(bool), typeof(Settings), new PropertyMetadata(true)); + + + public bool PrintIngameChat + { + get { return this.Dispatcher.Invoke( () => (bool)GetValue(PrintIngameChatProperty)); } + set { SetValue(PrintIngameChatProperty, value); } + } + + // Using a DependencyProperty as the backing store for PrintIngameChat. This enables animation, styling, binding, etc... + public static readonly DependencyProperty PrintIngameChatProperty = + DependencyProperty.Register("PrintIngameChat", typeof(bool), typeof(Settings), new PropertyMetadata(true)); + + + public bool PrintTeamChat + { + get { return this.Dispatcher.Invoke( () => (bool)GetValue(PrintTeamChatProperty)); } + set { SetValue(PrintTeamChatProperty, value); } + } + + // Using a DependencyProperty as the backing store for PrintTeamChat. This enables animation, styling, binding, etc... + public static readonly DependencyProperty PrintTeamChatProperty = + DependencyProperty.Register("PrintTeamChat", typeof(bool), typeof(Settings), new PropertyMetadata(true)); + + public void ApplySettingsFrom(Settings settings) + { + foreach (System.Reflection.PropertyInfo property in typeof(Settings).GetProperties().Where(p => p.CanWrite)) + { + property.SetValue(this, property.GetValue(settings, null), null); + } + } + + public object Clone() + { + return this.MemberwiseClone(); + } + } +} diff --git a/SteamShared/SteamShared/SteamShared/SteamHelper.cs b/SteamShared/SteamShared/SteamShared/SteamHelper.cs index ace081b..5df8783 100644 --- a/SteamShared/SteamShared/SteamShared/SteamHelper.cs +++ b/SteamShared/SteamShared/SteamShared/SteamHelper.cs @@ -214,53 +214,53 @@ namespace SteamShared private void populateGameInfo(SteamGame game, Element appStateVdf) { - game.Name = appStateVdf["name"].Value; + game.Name = appStateVdf["name"]?.Value; - game.InstallFolderName = appStateVdf["installdir"].Value; + game.InstallFolderName = appStateVdf["installdir"]?.Value; - if (int.TryParse(appStateVdf["appid"].Value, out int appId)) + if (int.TryParse(appStateVdf["appid"]?.Value, out int appId)) { game.AppId = appId; } - if (int.TryParse(appStateVdf["StateFlags"].Value, out int stateFlags)) + if (int.TryParse(appStateVdf["StateFlags"]?.Value, out int stateFlags)) { game.GameState = stateFlags; } - if (long.TryParse(appStateVdf["LastUpdated"].Value, out long lastUpdated)) + if (long.TryParse(appStateVdf["LastUpdated"]?.Value, out long lastUpdated)) { game.LastUpdated = fromUnixFormat(lastUpdated); } - if (long.TryParse(appStateVdf["LastOwner"].Value, out long lastOwner)) + if (long.TryParse(appStateVdf["LastOwner"]?.Value, out long lastOwner)) { game.LastOwnerSteam64Id = lastOwner; } - if (long.TryParse(appStateVdf["BytesToDownload"].Value, out long bytesToDownload)) + if (long.TryParse(appStateVdf["BytesToDownload"]?.Value, out long bytesToDownload)) { game.BytesToDownload = bytesToDownload; } - if (long.TryParse(appStateVdf["BytesDownloaded"].Value, out long bytesDownloaded)) + if (long.TryParse(appStateVdf["BytesDownloaded"]?.Value, out long bytesDownloaded)) { game.BytesDownloaded = bytesDownloaded; } - if (long.TryParse(appStateVdf["BytesToStage"].Value, out long bytesToStage)) + if (long.TryParse(appStateVdf["BytesToStage"]?.Value, out long bytesToStage)) { game.BytesToStage = bytesToStage; } - if (long.TryParse(appStateVdf["BytesStaged"].Value, out long bytesStaged)) + if (long.TryParse(appStateVdf["BytesStaged"]?.Value, out long bytesStaged)) { game.BytesStaged = bytesStaged; } - game.KeepAutomaticallyUpdated = appStateVdf["AutoUpdateBehavior"].Value != "0"; + game.KeepAutomaticallyUpdated = appStateVdf["AutoUpdateBehavior"]?.Value != "0"; - game.AllowOtherUpdatesWhileRunning = appStateVdf["AllowOtherDownloadsWhileRunning"].Value != "0"; + game.AllowOtherUpdatesWhileRunning = appStateVdf["AllowOtherDownloadsWhileRunning"]?.Value != "0"; } #endregion }