using System.Diagnostics; using Newtonsoft.Json; using RobloxLegacy.AppData; using RobloxLegacy.Utilities; using ICSharpCode.SharpZipLib.Zip; namespace RobloxLegacy; public class VersionData { [JsonProperty("version")] public required string Version { get; set; } [JsonProperty("clientVersionUpload")] public required string UploadHash { get; set; } } public class VersionManager : IDisposable { private static readonly HttpClient Client = new(); private const string CdnUrl = "https://setup.rbxcdn.com"; private const string DllName = "lptch.dll"; private string? _currentVersion; private readonly string _tempPath = Path.Combine(Path.GetTempPath(), $"RobloxLegacy.{Guid.NewGuid().ToString()}"); private readonly IAppData _appData; public VersionManager(IAppData appData) { Directory.CreateDirectory(_tempPath); _currentVersion = Registry.GetVersion(appData.PackageName); _appData = appData; } private static string GetVersionPath(string version) { var localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); return Path.Combine(localAppDataPath, "Roblox", "Versions", version); } private async Task GetLatestVersion() { var url = $"https://clientsettings.roblox.com/v2/client-version/{_appData.PackageName}/channel/LIVE"; var response = await Client.GetAsync(url); if (!response.IsSuccessStatusCode) // just to be safe return null; var version = await response.Content.ReadAsAsync(); return version; } private static void ExtractBundle(string version, string folder, string tempFile) { var fastZip = new FastZip(); var extractPath = Path.Combine(GetVersionPath(version), folder); fastZip.ExtractZip(tempFile, extractPath, null); } private void WriteAppSettings() { var appSettings = new Resource("AppSettings.xml").GetBytes(); File.WriteAllBytesAsync(Path.Combine(GetVersionPath(_currentVersion), "AppSettings.xml"), appSettings); } private void PatchStudio() { Logger.Info("Patching application..."); var exePath = Path.Combine(GetVersionPath(_currentVersion), _appData.ExecutableName); // rename imports first Patcher.RenameImports(exePath, ["KERNEL32.dll", "MFPlat.DLL"], DllName); // now we can write the dll to the folder var dllContents = new Resource(DllName).GetBytes(); File.WriteAllBytesAsync(Path.Combine(GetVersionPath(_currentVersion), DllName), dllContents); } public async Task InstallPackage() { var version = await GetLatestVersion(); if (version == null) throw new Exception("No version data found"); if (version.UploadHash == _currentVersion) return; Logger.Info($"Installing {_appData.PackageName} version {version.Version}..."); foreach (var file in _appData.PackageFiles) { try { var fileName = $"{version.UploadHash}-{file.Key}"; var fileBytes = await Client.GetByteArrayAsync($"{CdnUrl}/{fileName}"); var zipPath = Path.Combine(_tempPath, fileName); await File.WriteAllBytesAsync(zipPath, fileBytes); ExtractBundle(version.UploadHash, file.Value, zipPath); } catch (HttpRequestException) { Logger.Error($"Failed to download {file.Key}"); } } if(_currentVersion != null) Directory.Delete(GetVersionPath(_currentVersion), true); _currentVersion = version.UploadHash; WriteAppSettings(); Registry.SaveVersion(_appData.PackageName, version.UploadHash); // need to patch the executable if (_appData.PackageName == "WindowsStudio64") PatchStudio(); } public void LaunchApp() { Logger.Info($"Launching {_appData.PackageName}..."); if(string.IsNullOrEmpty(_currentVersion)) throw new Exception("No version data found"); var startInfo = new ProcessStartInfo() { FileName = _appData.ExecutableName, WorkingDirectory = GetVersionPath(_currentVersion), UseShellExecute = true }; Process.Start(startInfo); } public void Dispose() { Directory.Delete(_tempPath, true); GC.SuppressFinalize(this); } }