diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..2125666
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a9b29e0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.vs/
+bin/
+obj/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..918fd8a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,23 @@
+The MIT License (MIT)
+
+Copyright © 2024 Nuclearist
+
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/MRCP.csproj b/MRCP.csproj
new file mode 100644
index 0000000..64f7bfe
--- /dev/null
+++ b/MRCP.csproj
@@ -0,0 +1,36 @@
+
+
+ net8.0
+ Exe
+ 1.0.0
+ res/MRCP.manifest
+ Nuclearist
+ Steam Manifest Request Code Provider
+ Copyright © 2024 Nuclearist
+ False
+ Enable
+ Enable
+ True
+ True
+ False
+ $([MSBuild]::GetRegistryValue('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Kits\Installed Roots', 'KitsRoot10'))
+ $(WindowsKitRoot)App Certification Kit\signtool.exe
+
+
+ True
+ True
+ MRCP.snk
+ False
+ True
+ Speed
+ False
+ False
+ False
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MRCP.sln b/MRCP.sln
new file mode 100644
index 0000000..516c4a3
--- /dev/null
+++ b/MRCP.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MRCP", "MRCP.csproj", "{50DF6FC1-E9E9-4DB8-9A2E-31D8B3500DD3}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {50DF6FC1-E9E9-4DB8-9A2E-31D8B3500DD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {50DF6FC1-E9E9-4DB8-9A2E-31D8B3500DD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {50DF6FC1-E9E9-4DB8-9A2E-31D8B3500DD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {50DF6FC1-E9E9-4DB8-9A2E-31D8B3500DD3}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {65836321-A81B-4F9B-A6AC-81DB477FDBE5}
+ EndGlobalSection
+EndGlobal
diff --git a/MRCP.snk b/MRCP.snk
new file mode 100644
index 0000000..3938b6a
Binary files /dev/null and b/MRCP.snk differ
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..661d129
--- /dev/null
+++ b/README.md
@@ -0,0 +1,57 @@
+# MRCP
+[![Discord](https://img.shields.io/discord/937821572285206659?style=flat-square&label=Discord&logo=discord&logoColor=white&color=7289DA)](https://discord.com/servers/teknology-hub-937821572285206659)
+
+## Overview
+
+Steam Manifest Request Code Provider (MRCP) is a lightweight bot that supplies Steam manifest request codes over a UDP socket using your Steam account
+
+## How to use
+
+Download the binary for your OS in [releases](https://github.com/Nuclearistt/MRCP/releases) or build it manually (relevant for Linux since Native AOT adds dynamic dependencies on specific library versions), run it once with `--setup` argument to interactively input credentials, then you may run it without any arguments (e.g as as service), it'll listen on specified port for UDP requests
+
+## Request and response data formats
+
+Request (16 bytes):
++ uint AppId
++ uint DepotId
++ ulong ManifestId
+
+Response (8 bytes): ulong ManifestRequestCode
+
+## Why do you need it
+
+While [TEK Steam Client](https://github.com/Nuclearistt/TEKSteamClient) can install arbitrary Steam apps, it is unable to get manifest request codes for apps not owned on the account it's logged on, and hence to download those apps. MCRP allows to proxy MCR requests to an account that owns the apps in question without leaking account credentials because it's running on a remote server
+
+## Client side code example
+
+```cs
+using System.Collections.Frozen;
+using System.Net.Sockets;
+using TEKSteamClient;
+
+static readonly IPEndPoint ServerEndpoint = IPEndPoint.Parse("*Your server IP*:*MCPR Port*");
+static ulong GetManifestRequestCode(uint appId, uint depotId, ulong manifestId)
+{
+ using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); //May make it a static singleton
+ Span buffer = stackalloc byte[16];
+ BitConverter.TryWriteBytes(buffer[..4], appId);
+ BitConverter.TryWriteBytes(buffer[4..8], depotId);
+ BitConverter.TryWriteBytes(buffer[8..], manifestId);
+ socket.SendTo(buffer, ServerEndpoint);
+ socket.Receive(buffer);
+ return BitConverter.ToUInt64(buffer);
+}
+
+//The following code needs to be executed only once per app lifetime, you may put it in the beginning of Main method
+CMClient.ManifestRequestCodeSourceOverrides = System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary((IEnumerable>>)
+ [
+ new(/*A depot ID*/, GetManifestRequestCode),
+ new(/*Another depot ID*/, GetManifestRequestCode)
+ ]);
+
+//Your code using TEK Steam Client goes here, it'll automatically send requests to MCRP when needed
+```
+
+## License
+
+MRCP is licensed under the [MIT](https://github.com/Nuclearistt/MRCP/blob/main/LICENSE) license.
\ No newline at end of file
diff --git a/res/MRCP.manifest b/res/MRCP.manifest
new file mode 100644
index 0000000..ff1512f
--- /dev/null
+++ b/res/MRCP.manifest
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+ True
+ UTF-8
+ SegmentHeap
+
+
+
\ No newline at end of file
diff --git a/src/Config.cs b/src/Config.cs
new file mode 100644
index 0000000..532cd79
--- /dev/null
+++ b/src/Config.cs
@@ -0,0 +1,43 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MRCP;
+
+/// App config object.
+internal class Config
+{
+ /// Singleton path to the config file.
+ private static readonly string s_filePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MRCP", "Settings.json");
+ /// Port to listen on for requests.
+ public required int Port { get; init; }
+ /// Steam account name to use.
+ public required string AccountName { get; init; }
+ /// Access/refresh token for Steam account.
+ public required string Token { get; init; }
+
+ /// Writes current object state to the file.
+ public void SaveToFile()
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(s_filePath)!);
+ byte[] data = JsonSerializer.SerializeToUtf8Bytes(this, ConfigJsonContext.Default.Config);
+ using var fileHandle = File.OpenHandle(s_filePath, FileMode.Create, FileAccess.Write, preallocationSize: data.Length);
+ RandomAccess.Write(fileHandle, data, 0);
+ }
+ /// Loads config from the file.
+ /// Config object loaded from the file.
+ public static Config Load()
+ {
+ if (!File.Exists(s_filePath))
+ throw new FileNotFoundException("Config file not found. Run the app with --setup flag.");
+ byte[] buffer;
+ using (var fileHandle = File.OpenHandle(s_filePath))
+ {
+ buffer = new byte[(int)RandomAccess.GetLength(fileHandle)];
+ RandomAccess.Read(fileHandle, buffer, 0);
+ }
+ return JsonSerializer.Deserialize(buffer, ConfigJsonContext.Default.Config)!;
+ }
+}
+[JsonSourceGenerationOptions(PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate)]
+[JsonSerializable(typeof(Config))]
+partial class ConfigJsonContext : JsonSerializerContext { }
\ No newline at end of file
diff --git a/src/Program.cs b/src/Program.cs
new file mode 100644
index 0000000..1036500
--- /dev/null
+++ b/src/Program.cs
@@ -0,0 +1,106 @@
+using System.Runtime.InteropServices;
+using System.Net.Sockets;
+using System.Net;
+using System.Runtime.CompilerServices;
+using System.Text;
+using TEKSteamClient;
+using TEKSteamClient.CM;
+using MRCP;
+
+AppDomain.CurrentDomain.UnhandledException += (sender, e) => Console.WriteLine($"Unhandled exception caught: {e.ExceptionObject}");
+Environment.ExitCode = -1;
+if (args.Length > 0 && args[0] is "--setup")
+{
+ Console.Write("Enter the port to listen on: ");
+ if (!ushort.TryParse(Console.ReadLine(), out ushort port))
+ {
+ Console.WriteLine("Error: Invalid port value");
+ return;
+ }
+ Console.Write("Enter Steam account login: ");
+ string accountName = Console.ReadLine()!;
+ Console.Write("Enter Steam account password: ");
+ var builder = new StringBuilder();
+ ConsoleKey key;
+ do
+ {
+ var keyInfo = Console.ReadKey(true);
+ key = keyInfo.Key;
+ if (key is ConsoleKey.Backspace && builder.Length > 0)
+ builder.Remove(builder.Length - 1, 1);
+ else if (!char.IsControl(keyInfo.KeyChar))
+ builder.Append(keyInfo.KeyChar);
+ } while (key != ConsoleKey.Enter);
+ Console.WriteLine();
+ var cmClient = new CMClient();
+ string? token = null;
+ try { cmClient.LogOn(accountName, ref token, builder.ToString()); }
+ catch (SteamException se)
+ {
+ Console.WriteLine($"Log on failed: {se.Message}");
+ return;
+ }
+ cmClient.Disconnect();
+ new Config
+ {
+ Port = port,
+ AccountName = accountName,
+ Token = token!
+ }.SaveToFile();
+ Environment.ExitCode = 0;
+}
+else
+{
+ var config = Config.Load();
+ var cmClient = new CMClient();
+ string? token = config.Token;
+ cmClient.LogOn(config.AccountName, ref token);
+ using var cts = new CancellationTokenSource();
+ using var signalRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, delegate
+ {
+ Environment.ExitCode = 0;
+ cts.Cancel();
+ });
+ using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) { SendTimeout = 2000 };
+ socket.Bind(new IPEndPoint(IPAddress.Any, config.Port));
+ byte[] buffer = new byte[16];
+ ref byte bufferRef = ref MemoryMarshal.GetArrayDataReference(buffer);
+ var sendSpan = new ReadOnlySpan(buffer, 0, 8);
+ var remoteAddress = new SocketAddress(AddressFamily.InterNetwork);
+ try
+ {
+ while (!cts.IsCancellationRequested)
+ {
+ var task = socket.ReceiveFromAsync(buffer, SocketFlags.None, remoteAddress, cts.Token);
+ if ((task.IsCompletedSuccessfully ? task.GetAwaiter().GetResult() : task.AsTask().GetAwaiter().GetResult()) < 16)
+ continue;
+ ulong requestCode;
+ tryAgain:
+ try
+ {
+ requestCode = cmClient.GetManifestRequestCode(Unsafe.As(ref bufferRef), Unsafe.As(ref Unsafe.AddByteOffset(ref bufferRef, 4)), Unsafe.As(ref Unsafe.AddByteOffset(ref bufferRef, 8)));
+ }
+ catch (SteamException se) when (se.Type is SteamException.ErrorType.CMNotLoggedOn)
+ {
+ try
+ {
+ cmClient.LogOn(config.AccountName, ref token);
+ goto tryAgain;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ continue;
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ continue;
+ }
+ Unsafe.As(ref bufferRef) = requestCode;
+ socket.SendTo(sendSpan, SocketFlags.None, remoteAddress);
+ }
+ }
+ catch (OperationCanceledException) { }
+}
\ No newline at end of file