diff --git a/ReferenceClientProxyImplementation/Services/BuildDownloadService.cs b/ReferenceClientProxyImplementation/Services/BuildDownloadService.cs
new file mode 100644
index 0000000..364c6c5
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Services/BuildDownloadService.cs
@@ -0,0 +1,76 @@
+// using System.Net;
+// using AngleSharp.Html.Parser;
+//
+// namespace ReferenceClientProxyImplementation.Services;
+//
+// public class BuildDownloadService(ILogger<BuildDownloadService> logger) {
+// private static readonly HttpClient hc = new();
+//
+// public async Task DownloadBuildFromArchiveOrg(string outputDirectory, DateTime timestamp) {
+// // 20150906025145
+// var paddedTimestamp = timestamp.ToString("yyyyMMddHHmmss");
+// await DownloadBuildFromUrl(outputDirectory, $"https://web.archive.org/web/{paddedTimestamp}im_/https://discordapp.com/login");
+// }
+//
+// public async Task DownloadBuildFromUrl(string outputDirectory, string url) {
+// logger.LogInformation("Downloading build from {url} to {outDir}", url, outputDirectory);
+// var response = await hc.GetAsync(url);
+// if (!response.IsSuccessStatusCode)
+// throw new Exception($"Failed to download build from {url}");
+// var html = await response.Content.ReadAsStringAsync();
+// File.WriteAllText(outputDirectory + "/index.html", html);
+// var parser = new HtmlParser();
+// var document = parser.ParseDocument(html);
+// var assets = document.QuerySelectorAll("link[rel=stylesheet], link[rel=icon], script, img");
+// foreach (var asset in assets) {
+// var assetUrl = asset.GetAttribute("href") ?? asset.GetAttribute("src");
+// if (assetUrl == null)
+// continue;
+// if (assetUrl.StartsWith("//")) {
+// logger.LogWarning("Skipping asset {assetUrl} as it is a protocol-relative URL", assetUrl);
+// continue;
+// }
+//
+// var assetStream = await GetAssetStream(assetUrl);
+// var assetPath = Path.Combine(outputDirectory, assetUrl.TrimStart('/'));
+// Console.WriteLine($"Downloading asset {assetUrl} to {assetPath}");
+// Directory.CreateDirectory(Path.GetDirectoryName(assetPath));
+// await using var fs = File.Create(assetPath);
+// await assetStream.CopyToAsync(fs);
+// }
+//
+// logger.LogInformation("Downloading build from {url} complete!", url);
+// }
+//
+// public async Task<Stream> GetAssetStream(string asset) {
+// asset = asset.Replace("/assets/", "");
+// var urlsToTry = new Stack<string>(new[] {
+// $"https://web.archive.org/web/0id_/https://discordapp.com/assets/{asset}",
+// $"https://web.archive.org/web/0id_/https://discord.com/assets/{asset}",
+// $"https://discord.com/assets/{asset}"
+// });
+// while (urlsToTry.TryPop(out var urlToTry)) {
+// if (string.IsNullOrWhiteSpace(urlToTry)) continue;
+// try {
+// var response = await hc.GetAsync(urlToTry, HttpCompletionOption.ResponseHeadersRead);
+// if (response.IsSuccessStatusCode) {
+// Console.WriteLine($"Got success for asset {asset} from {urlToTry}");
+// return await response.Content.ReadAsStreamAsync();
+// }
+// //redirect
+//
+// if (response.StatusCode == HttpStatusCode.Found) {
+// var redirectUrl = response.Headers.Location?.ToString();
+// if (string.IsNullOrWhiteSpace(redirectUrl)) continue;
+// urlsToTry.Push(redirectUrl);
+// }
+// else logger.LogWarning("Failed to download asset {asset} from {urlToTry}", asset, urlToTry);
+// }
+// catch {
+// // ignored
+// }
+// }
+//
+// throw new Exception($"Failed to download asset {asset}");
+// }
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Services/ClientStoreService.cs b/ReferenceClientProxyImplementation/Services/ClientStoreService.cs
new file mode 100644
index 0000000..6bd7418
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Services/ClientStoreService.cs
@@ -0,0 +1,62 @@
+using ArcaneLibs.Extensions.Streams;
+using ReferenceClientProxyImplementation.Configuration;
+using ReferenceClientProxyImplementation.Patches.Implementations;
+
+namespace ReferenceClientProxyImplementation.Services;
+
+public class ClientStoreService(ProxyConfiguration config, PatchSet patches) {
+ private static readonly HttpClient HttpClient = new();
+
+ public async Task<Stream> GetPatchedClientAsset(string relativePath) {
+ if (relativePath.StartsWith("/")) {
+ relativePath = relativePath[1..];
+ }
+
+ var path = Path.Combine(config.TestClient.RevisionPath, "patched", relativePath);
+
+ if (File.Exists(path))
+ return File.OpenRead(path);
+
+ var srcAsset = (await GetOrDownloadRawAsset(relativePath)).ReadToEnd().ToArray();
+ var result = await patches.ApplyPatches(relativePath, srcAsset);
+ Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+ if (!result.SequenceEqual(srcAsset)) {
+ await File.WriteAllBytesAsync(path, result);
+ return File.OpenRead(path);
+ }
+
+ Console.WriteLine($"No patches applied for {relativePath}, returning original asset.");
+ return new MemoryStream(srcAsset);
+ }
+
+ public async Task<Stream> GetOrDownloadRawAsset(string relativePath) {
+ relativePath = relativePath.TrimStart('/');
+ var assetPath = Path.Combine(config.TestClient.RevisionPath, "src", relativePath);
+ if (File.Exists(assetPath)) {
+ Console.WriteLine($"Asset {relativePath} already exists at {assetPath}, returning existing file.");
+ return File.OpenRead(assetPath);
+ }
+
+ var url = $"{config.TestClient.RevisionBaseUrl}/{relativePath}";
+ var response = await HttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
+ if (!response.IsSuccessStatusCode) {
+ Console.WriteLine($"Failed to download asset {relativePath} from {url}, status code: {response.StatusCode}");
+ throw new FileNotFoundException($"Asset not found: {relativePath}");
+ }
+ var contentStream = await response.Content.ReadAsStreamAsync();
+ Directory.CreateDirectory(Path.GetDirectoryName(assetPath)!);
+ await using var fileStream = File.Create(assetPath);
+ await contentStream.CopyToAsync(fileStream);
+ fileStream.Close();
+ contentStream.Close();
+ Console.WriteLine($"Downloaded asset {relativePath} to {assetPath}");
+
+ return File.OpenRead(assetPath);
+ }
+
+ public bool HasRawAsset(string relativePath) {
+ relativePath = relativePath.TrimStart('/');
+ var assetPath = Path.Combine(config.TestClient.RevisionPath, "src", relativePath);
+ return File.Exists(assetPath);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Services/ModernAssetLocator.cs b/ReferenceClientProxyImplementation/Services/ModernAssetLocator.cs
new file mode 100644
index 0000000..039fc74
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Services/ModernAssetLocator.cs
@@ -0,0 +1,3 @@
+namespace ReferenceClientProxyImplementation.Services;
+
+public class ModernAssetLocator { }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Services/TemporaryTestJob.cs b/ReferenceClientProxyImplementation/Services/TemporaryTestJob.cs
new file mode 100644
index 0000000..b8998f1
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Services/TemporaryTestJob.cs
@@ -0,0 +1,14 @@
+// namespace ReferenceClientProxyImplementation.Services;
+//
+// public class TemporaryTestJob(BuildDownloadService buildDownloadService) : BackgroundService {
+// protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
+// Console.WriteLine("Running test job");
+// var outDir =
+// "/home/Rory/git/spacebar/server-cs/DevUtils/ReferenceClientProxyImplementation/downloadCache/today/raw/";
+// if (Directory.Exists(outDir))
+// Directory.Delete(outDir, true);
+// Directory.CreateDirectory(outDir);
+// // await buildDownloadService.DownloadBuildFromArchiveOrg(outDir, new DateTime(2014, 1, 1));
+// await buildDownloadService.DownloadBuildFromUrl(outDir, "https://canary.discord.com/app");
+// }
+// }
\ No newline at end of file
|