From 3ffebbeecad6b480f696cf9b3802c9458c0a06a8 Mon Sep 17 00:00:00 2001 From: Anton Pupkov Date: Sat, 20 Jan 2024 00:54:14 -0800 Subject: [PATCH 1/5] Add Steam utilities --- Dota2GSI/CMakeLists.txt | 1 + Dota2GSI/Utils/SteamUtils.cs | 330 +++++++++++++++++++++++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 Dota2GSI/Utils/SteamUtils.cs diff --git a/Dota2GSI/CMakeLists.txt b/Dota2GSI/CMakeLists.txt index 120dd00..19b0452 100644 --- a/Dota2GSI/CMakeLists.txt +++ b/Dota2GSI/CMakeLists.txt @@ -102,6 +102,7 @@ SET(SOURCES StateHandlers/ProviderHandler.cs StateHandlers/RoshanHandler.cs StateHandlers/WearablesHandler.cs + Utils/SteamUtils.cs ) SET(README "${CMAKE_SOURCE_DIR}/README.md") diff --git a/Dota2GSI/Utils/SteamUtils.cs b/Dota2GSI/Utils/SteamUtils.cs new file mode 100644 index 0000000..0c97abc --- /dev/null +++ b/Dota2GSI/Utils/SteamUtils.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Dota2GSI.Utils +{ + /// + /// Class that serializes and deserializes ACF file format. + /// + public class ACF + { + /// + /// The items of this ACF element. + /// + public Dictionary Items => _items; + + /// + /// The children of this ACF element. + /// + public Dictionary Children => _children; + + private Dictionary _items = new Dictionary(); + private Dictionary _children = new Dictionary(); + + private static Dictionary _escape_characters = new Dictionary() { + { 'r', '\r' }, + { 'n', '\n' }, + { 't', '\t' }, + { '\'', '\'' }, + { '"', '"' }, + { '\\', '\\' }, + { 'b', '\b' }, + { 'f', '\f' }, + { 'v', '\v' } + }; + + public ACF() + { + } + + public ACF(string filename) + { + if (File.Exists(filename)) + { + using (FileStream file_stream = new FileStream(filename, FileMode.Open)) + { + ParseStream(new StreamReader(file_stream)); + } + } + } + + internal ACF(StreamReader stream) + { + ParseStream(stream); + } + + private void ParseStream(StreamReader stream) + { + bool seeking_brace = false; + + if ((char)stream.Peek() == '{') + { + // Consume { + stream.Read(); + seeking_brace = true; + } + + while (!stream.EndOfStream) + { + if (seeking_brace && (char)stream.Peek() == '}') + { + // Consume } + stream.Read(); + break; + } + + // Attempt to read the item key + object key = ReadValue(stream); + + if (key != null && key is string str_key) + { + object value = ReadValue(stream); + + if (value != null) + { + if (value is string str_value) + { + _items.Add(str_key.ToLowerInvariant(), str_value); + } + else if (value is ACF acf_value) + { + _children.Add(str_key.ToLowerInvariant(), acf_value); + } + } + } + + // Skip over any whitespace characters to get to next value + while (char.IsWhiteSpace((char)stream.Peek())) + { + stream.Read(); + } + } + } + + private object ReadValue(StreamReader stream) + { + object return_value = null; + + // Skip over any whitespace characters to get to next value + while (char.IsWhiteSpace((char)stream.Peek())) + { + stream.Read(); + } + + char peekchar = (char)stream.Peek(); + + if (peekchar.Equals('{')) + { + return_value = new ACF(stream); + } + else if (peekchar.Equals('/')) + { + // Comment, read until end of line + stream.ReadLine(); + } + else + { + return_value = ReadString(stream); + } + + return return_value; + } + + private string ReadString(StreamReader instream) + { + StringBuilder builder = new StringBuilder(); + + bool isQuote = ((char)instream.Peek()).Equals('"'); + + if (isQuote) + { + instream.Read(); + } + + for (char chr = (char)instream.Read(); !instream.EndOfStream; chr = (char)instream.Read()) + { + + if (isQuote && chr.Equals('"') || + !isQuote && char.IsWhiteSpace(chr)) // Arrived at end of string. + { + break; + } + + if (chr.Equals('\\')) // Fix up escaped characters. + { + // Read next character. + char escape = (char)instream.Read(); + + if (_escape_characters.ContainsKey(escape)) + { + builder.Append(_escape_characters[escape]); + } + } + else + { + builder.Append(chr); + } + } + + return builder.ToString(); + } + + public string BuildString(int indent_amount = 0) + { + int longest_key_value = 0; + + foreach (var item_kvp in _items) + { + if (item_kvp.Key.Length > longest_key_value) + { + longest_key_value = item_kvp.Key.Length; + } + } + + string indentation = ""; + for (int i = 0; i < indent_amount; i++) + { + indentation += " "; + } + + StringBuilder stringBuilder = new StringBuilder(); + + foreach (var item_kvp in _items) + { + // Indent beginning + stringBuilder.Append(indentation); + stringBuilder.Append($"\"{item_kvp.Key}\""); + // Pretty print + for (int i = item_kvp.Key.Length; i < longest_key_value; i++) + { + stringBuilder.Append(' '); + } + // Separator between the key and value + stringBuilder.Append(" "); + stringBuilder.Append($"\"{item_kvp.Value}\""); + stringBuilder.AppendLine(); + } + + foreach (var child_kvp in _children) + { + // Indent beginning + stringBuilder.Append(indentation); + stringBuilder.AppendLine($"\"{child_kvp.Key}\""); + // Opening { + stringBuilder.Append(indentation); + stringBuilder.AppendLine("{"); + stringBuilder.Append(child_kvp.Value.BuildString(indent_amount + 1)); + // Closing } + stringBuilder.Append(indentation); + stringBuilder.AppendLine("}"); + } + + return stringBuilder.ToString(); + } + + /// + public override string ToString() + { + return BuildString(); + } + + /// + public override bool Equals(object obj) + { + return obj is ACF other && + _items.SequenceEqual(other._items) && + _children.SequenceEqual(other._children); + } + + /// + public override int GetHashCode() + { + int hashCode = 610350854; + + foreach (var item in _items) + { + hashCode = hashCode * -379045661 + item.GetHashCode(); + } + + foreach (var child in _children) + { + hashCode = hashCode * -379045661 + child.GetHashCode(); + } + + return hashCode; + } + } + + /// + /// A class for handling Steam games + /// + public static class SteamUtils + { + /// + /// Retrieves a path to a specified AppID + /// + /// The game's AppID + /// Path to the location of AppID's install + public static string GetGamePath(int game_id) + { + try + { + string steam_path = ""; + + if (OperatingSystem.IsWindows()) + { + try + { + steam_path = (string)Microsoft.Win32.Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Valve\Steam", "InstallPath", null); + if (string.IsNullOrWhiteSpace(steam_path)) + { + steam_path = (string)Microsoft.Win32.Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Valve\Steam", "InstallPath", null); + } + } + catch (Exception) + { + steam_path = ""; + } + } + + if (String.IsNullOrWhiteSpace(steam_path)) + { + return null; + } + + string libraries_file = Path.Combine(steam_path, "SteamApps", "libraryfolders.vdf"); + if (File.Exists(libraries_file)) + { + ACF lib_data = new ACF(libraries_file); + var library_folders = lib_data.Children["libraryfolders"]; + + foreach (var library_entry_kvp in library_folders.Children) + { + var library_entry = library_entry_kvp.Value; + string library_path = library_entry.Items["path"]; + + var manifest_file = Path.Combine(library_path, "steamapps", $"appmanifest_{game_id}.acf"); + if (File.Exists(manifest_file)) + { + ACF manifest_data = new ACF(manifest_file); + string install_dir = manifest_data.Children["appstate"].Items["installdir"]; + string appid_path = Path.Combine(library_path, "steamapps", "common", install_dir); + if (Directory.Exists(appid_path)) + { + return appid_path; + } + } + } + } + } + catch (Exception) + { + } + + return null; + } + } +} \ No newline at end of file From 32d058366582c233c133c75bf5fa03e3e91f3b79 Mon Sep 17 00:00:00 2001 From: Anton Pupkov Date: Sat, 20 Jan 2024 01:08:15 -0800 Subject: [PATCH 2/5] Add support for automatically generating GSI configuration file --- Dota2GSI/CMakeLists.txt | 1 + Dota2GSI/Dota2GSIFile.cs | 86 +++++++++++++++++++++++++++++++++++ Dota2GSI/GameStateListener.cs | 23 ++++++++-- 3 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 Dota2GSI/Dota2GSIFile.cs diff --git a/Dota2GSI/CMakeLists.txt b/Dota2GSI/CMakeLists.txt index 19b0452..568905b 100644 --- a/Dota2GSI/CMakeLists.txt +++ b/Dota2GSI/CMakeLists.txt @@ -17,6 +17,7 @@ INCLUDE(CSharpUtilities) SET(SOURCES Dota2EventsInterface.cs + Dota2GSIFile.cs EventDispatcher.cs EventHandler.cs EventsInterface.cs diff --git a/Dota2GSI/Dota2GSIFile.cs b/Dota2GSI/Dota2GSIFile.cs new file mode 100644 index 0000000..50a38cb --- /dev/null +++ b/Dota2GSI/Dota2GSIFile.cs @@ -0,0 +1,86 @@ +using Dota2GSI.Utils; +using System; +using System.IO; + +namespace Dota2GSI +{ + /// + /// Class handling Game State Integration configuration file generation. + /// + public class Dota2GSIFile + { + /// + /// Attempts to create a Game State Integration configuraion file.
+ /// The configuration will target http://localhost:{port}/ address.
+ /// Returns true on success, false otherwise. + ///
+ /// The name of your integration. + /// The port for your integration. + /// Returns true on success, false otherwise. + public static bool CreateFile(string name, int port) + { + return CreateFile(name, $"http://localhost:{port}/"); + } + + /// + /// Attempts to create a Game State Integration configuraion file.
+ /// The configuration will target the specified URI address.
+ /// Returns true on success, false otherwise. + ///
+ /// The name of your integration. + /// The URI for your integration. + /// Returns true on success, false otherwise. + public static bool CreateFile(string name, string uri) + { + string game_path = SteamUtils.GetGamePath(570); + + try + { + if (!string.IsNullOrWhiteSpace(game_path)) + { + string gsifolder = game_path + @"\game\dota\cfg\gamestate_integration\"; + Directory.CreateDirectory(gsifolder); + string gsifile = gsifolder + @$"gamestate_integration_{name}.cfg"; + + ACF provider_configuration = new ACF(); + provider_configuration.Items["auth"] = "1"; + provider_configuration.Items["provider"] = "1"; + provider_configuration.Items["map"] = "1"; + provider_configuration.Items["player"] = "1"; + provider_configuration.Items["hero"] = "1"; + provider_configuration.Items["abilities"] = "1"; + provider_configuration.Items["items"] = "1"; + provider_configuration.Items["events"] = "1"; + provider_configuration.Items["buildings"] = "1"; + provider_configuration.Items["league"] = "1"; + provider_configuration.Items["draft"] = "1"; + provider_configuration.Items["wearables"] = "1"; + provider_configuration.Items["minimap"] = "1"; + provider_configuration.Items["roshan"] = "1"; + provider_configuration.Items["couriers"] = "1"; + provider_configuration.Items["neutralitems"] = "1"; + + ACF gsi_configuration = new ACF(); + gsi_configuration.Items["uri"] = uri; + gsi_configuration.Items["timeout"] = "5.0"; + gsi_configuration.Items["buffer"] = "0.1"; + gsi_configuration.Items["throttle"] = "0.1"; + gsi_configuration.Items["heartbeat"] = "10.0"; + gsi_configuration.Children["data"] = provider_configuration; + + ACF gsi = new ACF(); + gsi.Children[$"{name} Integration Configuration"] = gsi_configuration; + + File.WriteAllText(gsifile, gsi.ToString()); + + return true; + } + } + catch (Exception) + { + } + + return false; + } + } +} diff --git a/Dota2GSI/GameStateListener.cs b/Dota2GSI/GameStateListener.cs index cb4d951..5c5ce7f 100644 --- a/Dota2GSI/GameStateListener.cs +++ b/Dota2GSI/GameStateListener.cs @@ -56,7 +56,12 @@ private set /// /// Gets the port that is being listened. /// - public int Port { get { return _port; } } + public int Port => _port; + + /// + /// Gets the URI that is being listened. + /// + public string URI => _uri; /// /// Returns whether or not the listener is running. @@ -70,6 +75,7 @@ private set private bool _is_running = false; private int _port; + private string _uri; private HttpListener _http_listener; private AutoResetEvent _wait_for_connection = new AutoResetEvent(false); private GameState _previous_game_state = new GameState(); @@ -117,8 +123,9 @@ private GameStateListener() public GameStateListener(int Port) : this() { _port = Port; + _uri = $"http://localhost:{_port}/"; _http_listener = new HttpListener(); - _http_listener.Prefixes.Add("http://localhost:" + Port + "/"); + _http_listener.Prefixes.Add(_uri); } /// @@ -141,11 +148,21 @@ public GameStateListener(string URI) : this() } _port = Convert.ToInt32(PortMatch.Groups[1].Value); - + _uri = URI; _http_listener = new HttpListener(); _http_listener.Prefixes.Add(URI); } + /// + /// Attempts to create a Game State Integration configuraion file. + /// + /// The name of your integration. + /// Returns true on success, false otherwise. + public bool GenerateGSIConfigFile(string name) + { + return Dota2GSIFile.CreateFile(name, _uri); + } + /// /// Starts listening for GameState requests. /// From 143fc0ebc9f479ba774b972b07a090ef64577fd3 Mon Sep 17 00:00:00 2001 From: Anton Pupkov Date: Fri, 19 Jan 2024 17:44:45 -0800 Subject: [PATCH 3/5] Update example program --- .../Dota2GSI Example program/Program.cs | 69 +------------------ 1 file changed, 3 insertions(+), 66 deletions(-) diff --git a/Dota2GSI Example program/Dota2GSI Example program/Program.cs b/Dota2GSI Example program/Dota2GSI Example program/Program.cs index 3b69055..49caf37 100644 --- a/Dota2GSI Example program/Dota2GSI Example program/Program.cs +++ b/Dota2GSI Example program/Dota2GSI Example program/Program.cs @@ -1,9 +1,6 @@ using Dota2GSI; using Dota2GSI.EventMessages; -using Microsoft.Win32; using System; -using System.Diagnostics; -using System.IO; using System.Threading; namespace Dota2GSI_Example_program @@ -14,18 +11,13 @@ class Program static void Main(string[] args) { - CreateGsifile(); + _gsl = new GameStateListener(4000); - Process[] pname = Process.GetProcessesByName("Dota2"); - if (pname.Length == 0) + if (!_gsl.GenerateGSIConfigFile("Example")) { - Console.WriteLine("Dota 2 is not running. Please start Dota 2."); - Console.ReadLine(); - Environment.Exit(0); + Console.WriteLine("Could not generate GSI configuration file."); } - _gsl = new GameStateListener(4000); - // There are many callbacks that can be subscribed. // This example shows a few. // _gsl.NewGameState += OnNewGameState; // `NewGameState` can be used alongside `GameEvent`. Just not in this example. @@ -246,60 +238,5 @@ static void OnNewGameState(GameState gs) Console.WriteLine("Press ESC to quit"); } - - private static void CreateGsifile() - { - RegistryKey regKey = Registry.CurrentUser.OpenSubKey(@"Software\Valve\Steam"); - - if (regKey != null) - { - string gsifolder = regKey.GetValue("SteamPath") + - @"\steamapps\common\dota 2 beta\game\dota\cfg\gamestate_integration"; - Directory.CreateDirectory(gsifolder); - string gsifile = gsifolder + @"\gamestate_integration_testGSI.cfg"; - if (File.Exists(gsifile)) - return; - - string[] contentofgsifile = - { - "\"Dota 2 Integration Configuration\"", - "{", - " \"uri\" \"http://localhost:4000\"", - " \"timeout\" \"5.0\"", - " \"buffer\" \"0.1\"", - " \"throttle\" \"0.1\"", - " \"heartbeat\" \"30.0\"", - " \"data\"", - " {", - " \"auth\" \"1\"", - " \"provider\" \"1\"", - " \"map\" \"1\"", - " \"player\" \"1\"", - " \"hero\" \"1\"", - " \"abilities\" \"1\"", - " \"items\" \"1\"", - " \"events\" \"1\"", - " \"buildings\" \"1\"", - " \"league\" \"1\"", - " \"draft\" \"1\"", - " \"wearables\" \"1\"", - " \"minimap\" \"1\"", - " \"roshan\" \"1\"", - " \"couriers\" \"1\"", - " \"neutralitems\" \"1\"", - " }", - "}", - - }; - - File.WriteAllLines(gsifile, contentofgsifile); - } - else - { - Console.WriteLine("Registry key for steam not found, cannot create Gamestate Integration file"); - Console.ReadLine(); - Environment.Exit(0); - } - } } } From db872e89dbeca1a9feaef0fabcc44b16f0237071 Mon Sep 17 00:00:00 2001 From: Anton Pupkov Date: Sat, 20 Jan 2024 00:54:01 -0800 Subject: [PATCH 4/5] Update readme --- README.md | 74 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 5dbe43a..d5ea60b 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ Dota 2 GSI (Game State Integration) =================================== [![.NET](https://github.com/antonpup/Dota2GSI/actions/workflows/dotnet.yml/badge.svg?branch=master)](https://github.com/antonpup/Dota2GSI/actions/workflows/dotnet.yml) -![GitHub Release](https://img.shields.io/github/v/release/antonpup/Dota2GSI) +[![GitHub Release](https://img.shields.io/github/v/release/antonpup/Dota2GSI)](https://github.com/antonpup/Dota2GSI/releases/latest) [![NuGet Version](https://img.shields.io/nuget/v/Dota2GSI)](https://www.nuget.org/packages/Dota2GSI) +[![NuGet Downloads](https://img.shields.io/nuget/dt/Dota2GSI?label=nuget%20downloads)](https://www.nuget.org/packages/Dota2GSI) A C# library to interface with the Game State Integration found in Dota 2. @@ -32,38 +33,7 @@ Install from [nuget](https://www.nuget.org/packages/Dota2GSI). ## How to use -1. Before `Dota2GSI` can be used, you must create a Game State Integration configuration file where you would specify the URI for the HTTP Requests and select which providers the game should expose to your application. To do this you must create a new configuration file in `/game/dota/cfg/gamestate_integration/gamestate_integration_.cfg` where `` should be the name of your application (it can be anything). Add the following content to your configuration file (make sure you replace `` with your application's name, `` with the port you would like to use for your application, and other values can also be tweaked as you wish): -``` -" Integration Configuration" -{ - "uri" "http://localhost:/" - "timeout" "5.0" - "buffer" "0.1" - "throttle" "0.1" - "heartbeat" "30.0" - "data" - { - "auth" "1" - "provider" "1" - "map" "1" - "player" "1" - "hero" "1" - "abilities" "1" - "items" "1" - "events" "1" - "buildings" "1" - "league" "1" - "draft" "1" - "wearables" "1" - "minimap" "1" - "roshan" "1" - "couriers" "1" - "neutralitems" "1" - } -} -``` - -2. After installing the [Dota2GSI nuget package](https://www.nuget.org/packages/Dota2GSI) in your project, create an instance of `GameStateListener`, providing a custom port or custom URI you defined in the configuration file in the previous step. +1. After installing the [Dota2GSI nuget package](https://www.nuget.org/packages/Dota2GSI) in your project, create an instance of `GameStateListener`, providing a custom port or custom URI you defined in the configuration file in the previous step. ```C# GameStateListener gsl = new GameStateListener(3000); //http://localhost:3000/ ``` @@ -73,6 +43,44 @@ GameStateListener gsl = new GameStateListener("http://127.0.0.1:1234/"); ``` > **Please note**: If your application needs to listen to a URI other than `http://localhost:*/` (for example `http://192.168.0.2:100/`), you will need to run your application with administrator privileges. +2. Create a Game State Integration configuration file. This can either be done manually by creating a file `/game/dota/cfg/gamestate_integration/gamestate_integration_.cfg` where `` should be the name of your application (it can be anything). Or you can use the built-in function `GenerateGSIConfigFile()` to automatically locate the game directory and generate the file. The function will automatically take into consideration the URI or port specified when a `GameStateListener` instance was created in the previous step. +```C# +if (!gsl.GenerateGSIConfigFile("Example")) +{ + Console.WriteLine("Could not generate GSI configuration file."); +} +``` +The function `GenerateGSIConfigFile` takes a string `name` as the parameter. This is the `` mentioned earlier. The function will also return `True` when file generation was successful, and `False` otherwise. The resulting file from the above code should look like this: +``` +"Example Integration Configuration" +{ + "uri" "http://localhost:3000/" + "timeout" "5.0" + "buffer" "0.1" + "throttle" "0.1" + "heartbeat" "10.0" + "data" + { + "auth" "1" + "provider" "1" + "map" "1" + "player" "1" + "hero" "1" + "abilities" "1" + "items" "1" + "events" "1" + "buildings" "1" + "league" "1" + "draft" "1" + "wearables" "1" + "minimap" "1" + "roshan" "1" + "couriers" "1" + "neutralitems" "1" + } +} +``` + 3. Create handlers and subscribe for events your application will be using. (A full list of exposed Game Events can be found in the [Implemented Game Events](#implemented-game-events) section.) If your application just needs `GameState` information, this is done by subscribing to `NewGameState` event and creating a handler for it: ```C# From 7e241088444cc5d6ea9bdf5747dd9cabad335111 Mon Sep 17 00:00:00 2001 From: Anton Pupkov Date: Sat, 20 Jan 2024 00:53:40 -0800 Subject: [PATCH 5/5] Fix from CounterStrike2GSI --- Dota2GSI/GameStateListener.cs | 49 ++++++++++++++++++++++------------- Dota2GSI/Nodes/Node.cs | 1 + 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/Dota2GSI/GameStateListener.cs b/Dota2GSI/GameStateListener.cs index 5c5ce7f..ac93dde 100644 --- a/Dota2GSI/GameStateListener.cs +++ b/Dota2GSI/GameStateListener.cs @@ -1,4 +1,4 @@ -using Dota2GSI.EventMessages; +using Dota2GSI.EventMessages; using Newtonsoft.Json.Linq; using System; using System.IO; @@ -43,13 +43,24 @@ public GameState CurrentGameState { get { - return _current_game_state; + lock (gamestate_lock) + { + return _current_game_state; + } } private set { - _previous_game_state = _current_game_state; - _current_game_state = value; - RaiseOnNewGameState(ref _current_game_state); + lock (gamestate_lock) + { + if (_current_game_state.Equals(value)) + { + return; + } + + _previous_game_state = _current_game_state; + _current_game_state = value; + RaiseOnNewGameState(ref _current_game_state); + } } } @@ -58,9 +69,9 @@ private set /// public int Port => _port; - /// - /// Gets the URI that is being listened. - /// + /// + /// Gets the URI that is being listened. + /// public string URI => _uri; /// @@ -73,6 +84,8 @@ private set /// public event NewGameStateHandler NewGameState = delegate { }; + private readonly object gamestate_lock = new object(); + private bool _is_running = false; private int _port; private string _uri; @@ -117,12 +130,12 @@ private GameStateListener() } /// - /// A GameStateListener that listens for connections on http://localhost:port/. + /// A GameStateListener that listens for connections on http://localhost:port/. /// - /// The port to listen on. - public GameStateListener(int Port) : this() + /// The port to listen on. + public GameStateListener(int port) : this() { - _port = Port; + _port = port; _uri = $"http://localhost:{_port}/"; _http_listener = new HttpListener(); _http_listener.Prefixes.Add(_uri); @@ -153,11 +166,11 @@ public GameStateListener(string URI) : this() _http_listener.Prefixes.Add(URI); } - /// - /// Attempts to create a Game State Integration configuraion file. - /// - /// The name of your integration. - /// Returns true on success, false otherwise. + /// + /// Attempts to create a Game State Integration configuraion file. + /// + /// The name of your integration. + /// Returns true on success, false otherwise. public bool GenerateGSIConfigFile(string name) { return Dota2GSIFile.CreateFile(name, _uri); @@ -247,7 +260,7 @@ private void RaiseOnNewGameState(ref GameState game_state) { RaiseEvent(NewGameState, game_state); - _game_state_handler.OnNewGameState(CurrentGameState); + _game_state_handler.OnNewGameState(game_state); } /// diff --git a/Dota2GSI/Nodes/Node.cs b/Dota2GSI/Nodes/Node.cs index ec3a50f..4b6dcd7 100644 --- a/Dota2GSI/Nodes/Node.cs +++ b/Dota2GSI/Nodes/Node.cs @@ -233,6 +233,7 @@ public override bool Equals(object obj) } return obj is Node other && + _ParsedData != null && _ParsedData.Equals(other._ParsedData) && _successfully_retrieved_any_value.Equals(other._successfully_retrieved_any_value); }