Skip to content

Commit 90c0a0a

Browse files
committed
Fixed issue with large libraries crashing due to OutOfMemoryException. Started work on SAM.Console and the various changes to support it.
1 parent ea66116 commit 90c0a0a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1446
-255
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,12 @@ flowchart TB
103103
A special thank you to [JetBrains](https://www.jetbrains.com/) for their continued [Support of Open-Source Projects](https://www.jetbrains.com/community/opensource/#support) like **SAM**.
104104

105105
> [!NOTE]
106-
> Active **SAM** contributors are eligible to receieve complimentary licenses [for non-commercial development] for **all** **JetBrains** products. For questions regarding eligability please refer to the [Open Source FAQ](https://sales.jetbrains.com/hc/en-gb/categories/13706169183250-Free-Licenses-for-OSS-development).
106+
> Active **SAM** contributors are eligible to receieve complimentary licenses [^1] for **all** **JetBrains** products. For questions regarding eligability please refer to the [Open Source FAQ](https://sales.jetbrains.com/hc/en-gb/categories/13706169183250-Free-Licenses-for-OSS-development).
107107
108108
## Acknowledgements
109109

110110
<p align="center">
111-
<a href="https://github.com/DevExpress/DevExpress.Mvvm.Free">DevExpress MVVM</a> • <a href="https://github.com/RudeySH/SteamCountries">SteamCountries</a> • <a href="https://github.com/lepoco/wpfui">WPF UI</a>
111+
<a href="https://github.com/DevExpress/DevExpress.Mvvm.Free">DevExpress</a> • <a href="https://github.com/RudeySH/SteamCountries">SteamCountries</a> • <a href="https://github.com/lepoco/wpfui">WPF UI</a>
112112
</p>
113113

114114
## Resources
@@ -117,3 +117,7 @@ A special thank you to [JetBrains](https://www.jetbrains.com/) for their continu
117117
- [Steamworks Overview](https://partner.steamgames.com/doc/sdk/api)
118118
- [Steamworks API](https://partner.steamgames.com/doc/api)
119119
- [Steamworks Web API](https://partner.steamgames.com/doc/webapi)
120+
121+
---
122+
123+
[^1]: For non-commercial development

src/SAM.API/SAM.API.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<PrivateAssets>all</PrivateAssets>
2222
<!--<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>-->
2323
</PackageReference>
24+
<PackageReference Include="ValveKeyValue" Version="0.8.2.162" />
2425
</ItemGroup>
2526

2627
<ItemGroup>

src/SAM.API/Steam.cs

Lines changed: 89 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -3,125 +3,124 @@
33
using System.Runtime.InteropServices;
44
using Microsoft.Win32;
55

6-
namespace SAM.API
6+
namespace SAM.API;
7+
8+
public static class Steam
79
{
8-
public static class Steam
9-
{
10-
private static string _installPath;
10+
private static string _installPath;
1111

12-
private static nint _handle = nint.Zero;
13-
private static NativeCreateInterface _callCreateInterface;
14-
private static NativeSteamGetCallback _callSteamBGetCallback;
15-
private static NativeSteamFreeLastCallback _callSteamFreeLastCallback;
12+
private static nint _handle = nint.Zero;
13+
private static NativeCreateInterface _callCreateInterface;
14+
private static NativeSteamGetCallback _callSteamBGetCallback;
15+
private static NativeSteamFreeLastCallback _callSteamFreeLastCallback;
1616

17-
private static string SteamClientDll => Environment.Is64BitProcess ? @"steamclient64.dll" : @"steamclient.dll";
17+
private static string SteamClientDll => Environment.Is64BitProcess ? @"steamclient64.dll" : @"steamclient.dll";
1818

19-
private static Delegate GetExportDelegate<TDelegate>(nint module, string name)
20-
{
21-
var address = Native.GetProcAddress(module, name);
22-
return address == nint.Zero ? null : Marshal.GetDelegateForFunctionPointer(address, typeof(TDelegate));
23-
}
19+
private static Delegate GetExportDelegate<TDelegate>(nint module, string name)
20+
{
21+
var address = Native.GetProcAddress(module, name);
22+
return address == nint.Zero ? null : Marshal.GetDelegateForFunctionPointer(address, typeof(TDelegate));
23+
}
2424

25-
private static TDelegate GetExportFunction<TDelegate>(nint module, string name)
26-
where TDelegate : class
27-
{
28-
return GetExportDelegate<TDelegate>(module, name) as TDelegate;
29-
}
25+
private static TDelegate GetExportFunction<TDelegate>(nint module, string name)
26+
where TDelegate : class
27+
{
28+
return GetExportDelegate<TDelegate>(module, name) as TDelegate;
29+
}
3030

31-
public static string GetInstallPath()
32-
{
33-
if (!string.IsNullOrEmpty(_installPath)) return _installPath;
31+
public static string GetInstallPath()
32+
{
33+
if (!string.IsNullOrEmpty(_installPath)) return _installPath;
3434

35-
// TODO: consider switching this to HKCU:\SOFTWARE\Valve\Steam\ActiveProcess\SteamClientDll
36-
using var view32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
37-
using var clsid32 = view32.OpenSubKey(@"Software\Valve\Steam", false);
35+
// TODO: consider switching this to HKCU:\SOFTWARE\Valve\Steam\ActiveProcess\SteamClientDll
36+
using var view32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
37+
using var clsid32 = view32.OpenSubKey(@"Software\Valve\Steam", false);
3838

39-
_installPath = (string) clsid32?.GetValue(@"InstallPath");
40-
return _installPath;
39+
_installPath = (string) clsid32?.GetValue(@"InstallPath");
40+
return _installPath;
4141

42-
//return (string)Registry.GetValue(@"HKEY_LOCAL_MACHINE\Software\Valve\Steam", "InstallPath", null);
43-
}
42+
//return (string)Registry.GetValue(@"HKEY_LOCAL_MACHINE\Software\Valve\Steam", "InstallPath", null);
43+
}
4444

45-
public static TClass CreateInterface<TClass>(string version)
46-
where TClass : INativeWrapper, new()
47-
{
48-
var address = _callCreateInterface(version, nint.Zero);
45+
public static TClass CreateInterface<TClass>(string version)
46+
where TClass : INativeWrapper, new()
47+
{
48+
var address = _callCreateInterface(version, nint.Zero);
4949

50-
if (address == nint.Zero) return default;
50+
if (address == nint.Zero) return default;
5151

52-
var rez = new TClass();
53-
rez.SetupFunctions(address);
54-
return rez;
55-
}
52+
var rez = new TClass();
53+
rez.SetupFunctions(address);
54+
return rez;
55+
}
5656

57-
public static bool GetCallback(int pipe, out CallbackMessage message, out int call)
58-
{
59-
return _callSteamBGetCallback(pipe, out message, out call);
60-
}
57+
public static bool GetCallback(int pipe, out CallbackMessage message, out int call)
58+
{
59+
return _callSteamBGetCallback(pipe, out message, out call);
60+
}
6161

62-
public static bool FreeLastCallback(int pipe)
63-
{
64-
return _callSteamFreeLastCallback(pipe);
65-
}
62+
public static bool FreeLastCallback(int pipe)
63+
{
64+
return _callSteamFreeLastCallback(pipe);
65+
}
6666

67-
public static bool Load()
67+
public static bool Load()
68+
{
69+
if (_handle != nint.Zero)
6870
{
69-
if (_handle != nint.Zero)
70-
{
71-
Native.FreeLibrary(_handle);
72-
_handle = nint.Zero;
73-
}
71+
Native.FreeLibrary(_handle);
72+
_handle = nint.Zero;
73+
}
7474

75-
var path = GetInstallPath();
76-
if (path == null) return false;
75+
var path = GetInstallPath();
76+
if (path == null) return false;
7777

78-
Native.SetDllDirectory(path + ";" + Path.Combine(path, "bin"));
79-
path = Path.Combine(path, SteamClientDll);
78+
Native.SetDllDirectory(path + ";" + Path.Combine(path, "bin"));
79+
path = Path.Combine(path, SteamClientDll);
8080

81-
var module = Native.LoadLibraryEx(path, nint.Zero, Native.LoadWithAlteredSearchPath);
82-
if (module == nint.Zero) return false;
81+
var module = Native.LoadLibraryEx(path, nint.Zero, Native.LoadWithAlteredSearchPath);
82+
if (module == nint.Zero) return false;
8383

84-
_callCreateInterface = GetExportFunction<NativeCreateInterface>(module, "CreateInterface");
85-
if (_callCreateInterface == null) return false;
84+
_callCreateInterface = GetExportFunction<NativeCreateInterface>(module, "CreateInterface");
85+
if (_callCreateInterface == null) return false;
8686

87-
_callSteamBGetCallback = GetExportFunction<NativeSteamGetCallback>(module, "Steam_BGetCallback");
88-
if (_callSteamBGetCallback == null) return false;
87+
_callSteamBGetCallback = GetExportFunction<NativeSteamGetCallback>(module, "Steam_BGetCallback");
88+
if (_callSteamBGetCallback == null) return false;
8989

90-
_callSteamFreeLastCallback = GetExportFunction<NativeSteamFreeLastCallback>(module, "Steam_FreeLastCallback");
91-
if (_callSteamFreeLastCallback == null) return false;
90+
_callSteamFreeLastCallback = GetExportFunction<NativeSteamFreeLastCallback>(module, "Steam_FreeLastCallback");
91+
if (_callSteamFreeLastCallback == null) return false;
9292

93-
_handle = module;
94-
return true;
95-
}
93+
_handle = module;
94+
return true;
95+
}
9696

97-
// TODO: replace with CSWin32's PInvoke
98-
private struct Native
99-
{
100-
[DllImport("kernel32.dll", SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)]
101-
internal static extern nint GetProcAddress(nint module, string name);
97+
// TODO: replace with CSWin32's PInvoke
98+
private struct Native
99+
{
100+
[DllImport("kernel32.dll", SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)]
101+
internal static extern nint GetProcAddress(nint module, string name);
102102

103-
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
104-
internal static extern nint LoadLibraryEx(string path, nint file, uint flags);
103+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
104+
internal static extern nint LoadLibraryEx(string path, nint file, uint flags);
105105

106-
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
107-
[return: MarshalAs(UnmanagedType.Bool)]
108-
internal static extern bool SetDllDirectory(string path);
106+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
107+
[return: MarshalAs(UnmanagedType.Bool)]
108+
internal static extern bool SetDllDirectory(string path);
109109

110-
[DllImport("kernel32.dll", SetLastError = true)]
111-
public static extern bool FreeLibrary(nint hModule);
110+
[DllImport("kernel32.dll", SetLastError = true)]
111+
public static extern bool FreeLibrary(nint hModule);
112112

113-
internal const uint LoadWithAlteredSearchPath = 8;
114-
}
113+
internal const uint LoadWithAlteredSearchPath = 8;
114+
}
115115

116-
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
117-
private delegate nint NativeCreateInterface(string version, nint returnCode);
116+
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
117+
private delegate nint NativeCreateInterface(string version, nint returnCode);
118118

119-
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
120-
[return: MarshalAs(UnmanagedType.I1)]
121-
private delegate bool NativeSteamGetCallback(int pipe, out CallbackMessage message, out int call);
119+
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
120+
[return: MarshalAs(UnmanagedType.I1)]
121+
private delegate bool NativeSteamGetCallback(int pipe, out CallbackMessage message, out int call);
122122

123-
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
124-
[return: MarshalAs(UnmanagedType.I1)]
125-
private delegate bool NativeSteamFreeLastCallback(int pipe);
126-
}
123+
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
124+
[return: MarshalAs(UnmanagedType.I1)]
125+
private delegate bool NativeSteamFreeLastCallback(int pipe);
127126
}

src/SAM.API/Types/KeyValue.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class KeyValue
99
{
1010
private static readonly KeyValue _invalid = new();
1111

12-
public List<KeyValue> Children;
12+
public List<KeyValue> Children { get; set; }
1313
public string Name = @"<root>";
1414
public KeyValueType Type = KeyValueType.None;
1515
public bool Valid;
@@ -19,7 +19,7 @@ public KeyValue this[string key]
1919
{
2020
get
2121
{
22-
var child = Children?.SingleOrDefault(c => string.Compare(c.Name, key, StringComparison.InvariantCultureIgnoreCase) == 0);
22+
var child = Children?.SingleOrDefault(c => c?.Name?.Equals(key, StringComparison.InvariantCultureIgnoreCase) ?? false);
2323

2424
return child ?? _invalid;
2525
}
@@ -120,7 +120,7 @@ public static KeyValue LoadAsBinary(string path)
120120

121121
public bool ReadAsBinary(Stream input)
122122
{
123-
Children = new ();
123+
Children = [ ];
124124

125125
try
126126
{
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Linq;
5+
using System.Reflection;
6+
using System.Text;
7+
using System.Text.RegularExpressions;
8+
using System.Threading.Tasks;
9+
using Newtonsoft.Json;
10+
11+
namespace SAM.Console.Commands;
12+
13+
public enum Command
14+
{
15+
Manage,
16+
List,
17+
Start,
18+
Help,
19+
Version
20+
}
21+
22+
[DebuggerDisplay("{ToString()}")]
23+
public class CommandTaskArgument
24+
{
25+
public string Name { get; set; }
26+
public object Value { get; set; }
27+
public bool IsValid => !string.IsNullOrEmpty(Name);
28+
29+
public CommandTaskArgument(string name)
30+
{
31+
Name = name;
32+
}
33+
34+
public CommandTaskArgument(string name, object value)
35+
{
36+
Name = name;
37+
Value = value;
38+
}
39+
40+
public override string ToString()
41+
{
42+
if (Value == null)
43+
{
44+
return $"{Name}";
45+
}
46+
47+
var value = $"{Value}";
48+
49+
// only quote it if there's whitespace characters
50+
if (Regex.IsMatch(value, @"\s", RegexOptions.Singleline))
51+
{
52+
value = $"\"{value}\"";
53+
}
54+
55+
return $"{Name} {value}";
56+
}
57+
}
58+
59+
public class CommandTask
60+
{
61+
private const string EXE_NAME = @"SAM.Console.exe";
62+
63+
private static Assembly CurrentAssembly => Assembly.GetExecutingAssembly();
64+
private static string AssemblyLocation => CurrentAssembly.Location;
65+
66+
public IList<CommandTaskArgument> Arguments { get; set; } = [ ];
67+
68+
public void AddArgument(string name)
69+
{
70+
Arguments.Add(new (name));
71+
}
72+
73+
public void AddArgument(string name, object value)
74+
{
75+
Arguments.Add(new (name, value));
76+
}
77+
78+
public void AddArgument(CommandTaskArgument arg)
79+
{
80+
Arguments.Add(arg);
81+
}
82+
83+
public string Execute()
84+
{
85+
if (!Arguments.Any()) throw new InvalidOperationException($"Arguments must be set before executing the command.");
86+
87+
var args = Arguments.Select(a => a.ToString()).ToList();
88+
89+
var psi = new ProcessStartInfo(EXE_NAME, args)
90+
{
91+
UseShellExecute = false,
92+
RedirectStandardOutput = true,
93+
CreateNoWindow = true
94+
};
95+
var proc = Process.Start(psi);
96+
97+
proc.WaitForExit();
98+
99+
var output = proc.StandardOutput.ReadToEnd();
100+
101+
return output;
102+
}
103+
104+
public T ExecuteAs<T>()
105+
{
106+
if (!Arguments.Any()) throw new InvalidOperationException($"Arguments must be set before executing the command.");
107+
108+
var args = Arguments.Select(a => a.ToString()).ToList();
109+
110+
var psi = new ProcessStartInfo(EXE_NAME, args)
111+
{
112+
UseShellExecute = false,
113+
RedirectStandardOutput = true,
114+
CreateNoWindow = true
115+
};
116+
var proc = Process.Start(psi);
117+
118+
proc.WaitForExit();
119+
120+
var output = proc.StandardOutput.ReadToEnd();
121+
122+
var result = JsonConvert.DeserializeObject<T>(output);
123+
124+
return result;
125+
}
126+
}

0 commit comments

Comments
 (0)