Files
rookie/MainForm.cs
jp64k 2c1f0ec259 Overhauled UI (WIP)
Overhauled UI (left side navigation bar, device information, search, background, text colors) for a cleaner and more modern-looking interface. Removed no longer used resources. Adjusted layout and visibility of certain panels and controls for a cleaner interface.

TODO:
- Finish the full UI overhaul.
- Fix MainForm Designer so it doesn't override the changes.
2025-12-02 23:01:45 +01:00

5132 lines
210 KiB
C#
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AndroidSideloader.Models;
using AndroidSideloader.Utilities;
using JR.Utils.GUI.Forms;
using Microsoft.Web.WebView2.Core;
using Newtonsoft.Json;
using SergeUtils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace AndroidSideloader
{
public partial class MainForm : Form
{
private readonly ListViewColumnSorter lvwColumnSorter;
private static readonly SettingsManager settings = SettingsManager.Instance;
#if DEBUG
public static bool debugMode = true;
public bool DeviceConnected;
public bool keyheld;
public bool keyheld2;
public static string CurrAPK;
public static string CurrPCKG;
List<UploadGame> gamesToUpload = new List<UploadGame>();
public static string currremotesimple = String.Empty;
#else
public bool keyheld;
public static string CurrAPK;
public static string CurrPCKG;
private readonly List<UploadGame> gamesToUpload = new List<UploadGame>();
public static bool debugMode = false;
public bool DeviceConnected = false;
public static string currremotesimple = "";
#endif
private const int EM_SETMARGINS = 0xD3;
private const int EC_LEFTMARGIN = 0x0001;
private const int EC_RIGHTMARGIN = 0x0002;
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
private string freeSpaceText = "";
private string freeSpaceTextDetailed = "";
private int _questStorageProgress = 0;
private bool _trailerPlayerInitialized; // player.html created and loaded
private bool _trailerHtmlLoaded; // initial navigation completed
private static readonly Dictionary<string, string> _videoIdCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); // per game cache
private bool isLoading = true;
public static bool isOffline = false;
public static bool noRcloneUpdating;
public static bool noAppCheck = false;
public static bool hasPublicConfig = false;
public static bool UsingPublicConfig = false;
public static bool enviromentCreated = false;
public static PublicConfig PublicConfigFile;
public static string PublicMirrorExtraArgs = " --tpslimit 1.0 --tpslimit-burst 3";
public static string storedIpPath;
public static string aaptPath;
private bool manualIP;
private System.Windows.Forms.Timer _debounceTimer;
private CancellationTokenSource _cts;
private List<ListViewItem> _allItems;
private Dictionary<string, List<ListViewItem>> _searchIndex;
public MainForm()
{
storedIpPath = Path.Combine(Environment.CurrentDirectory, "platform-tools", "StoredIP.txt");
aaptPath = Path.Combine(Environment.CurrentDirectory, "platform-tools", "aapt.exe");
InitializeComponent();
Logger.Initialize();
InitializeTimeReferences();
CheckCommandLineArguments();
CreateSearchIcon();
searchTextBox.HandleCreated += (s, e) => ApplySearchTextMargins();
_debounceTimer = new System.Windows.Forms.Timer { Interval = 100, Enabled = false };
_debounceTimer.Tick += async (sender, e) => await RunSearch();
gamesQueListBox.DataSource = gamesQueueList;
SetCurrentLogPath();
StartTimers();
lvwColumnSorter = new ListViewColumnSorter();
gamesListView.ListViewItemSorter = lvwColumnSorter;
if (searchTextBox.Visible) { _ = searchTextBox.Focus(); }
this.questInfoPanel.MouseEnter += this.QuestInfoHoverEnter;
this.questInfoPanel.MouseLeave += this.QuestInfoHoverLeave;
}
private void CreateSearchIcon()
{
if (this.searchIconPictureBox == null)
{
this.searchIconPictureBox = new PictureBox
{
Name = "searchIconPictureBox",
SizeMode = PictureBoxSizeMode.Zoom,
Size = new Size(16, 16),
BackColor = searchTextBox.BackColor, // blend with textbox
TabStop = false,
Enabled = false // let clicks go to the textbox
};
this.searchIconPictureBox.Image = Properties.Resources.SearchGlass;
}
// Parent the icon to the same container as the textbox
var host = searchTextBox.Parent ?? this;
if (this.searchIconPictureBox.Parent != host)
{
host.Controls.Add(this.searchIconPictureBox);
this.searchIconPictureBox.BringToFront();
}
// 6px left padding inside the textbox area
int leftInset = 6;
var pt = new Point(
searchTextBox.Left + leftInset,
searchTextBox.Top + (searchTextBox.Height - searchIconPictureBox.Height) / 2
);
searchIconPictureBox.Location = pt;
}
private void ApplySearchTextMargins()
{
if (searchTextBox == null) return;
// icon width + left inset + small gap
int leftInset = 6;
int leftMarginPixels = (searchIconPictureBox?.Width ?? 16) + leftInset + 2;
int rightMarginPixels = 2;
if (searchTextBox.IsHandleCreated)
{
int lParam = (rightMarginPixels << 16) | (leftMarginPixels & 0xFFFF);
SendMessage(searchTextBox.Handle, EM_SETMARGINS, (IntPtr)(EC_LEFTMARGIN | EC_RIGHTMARGIN), (IntPtr)lParam);
}
}
private void CheckCommandLineArguments()
{
string[] args = Environment.GetCommandLineArgs();
foreach (string arg in args)
{
if (arg == "--offline")
{
isOffline = true;
}
if (arg == "--no-rclone-update")
{
noRcloneUpdating = true;
}
if (arg == "--disable-app-check")
{
noAppCheck = true;
}
}
}
private void InitializeTimeReferences()
{
// Initialize time references
TimeSpan newDayReference = new TimeSpan(96, 0, 0); // Time between asking for new apps if user clicks No. (DEFAULT: 96 hours)
TimeSpan newDayReference2 = new TimeSpan(72, 0, 0); // Time between asking for updates after uploading. (DEFAULT: 72 hours)
// Calculate time differences
DateTime A = settings.LastLaunch;
DateTime B = DateTime.Now;
DateTime C = settings.LastLaunch2;
TimeSpan comparison = B - A;
TimeSpan comparison2 = B - C;
// Reset properties if enough time has passed
if (comparison > newDayReference)
{
ResetPropertiesAfterTimePassed();
}
if (comparison2 > newDayReference2)
{
ResetProperties2AfterTimePassed();
}
}
private void ResetPropertiesAfterTimePassed()
{
settings.ListUpped = false;
settings.NonAppPackages = String.Empty;
settings.AppPackages = String.Empty;
settings.LastLaunch = DateTime.Now;
settings.Save();
}
private void ResetProperties2AfterTimePassed()
{
settings.LastLaunch2 = DateTime.Now;
settings.SubmittedUpdates = String.Empty;
settings.Save();
}
private void SetCurrentLogPath()
{
if (string.IsNullOrEmpty(settings.CurrentLogPath))
{
settings.CurrentLogPath = Path.Combine(Environment.CurrentDirectory, "debuglog.txt");
}
}
private void StartTimers()
{
// Start timers
System.Windows.Forms.Timer t = new System.Windows.Forms.Timer
{
Interval = 840000 // 14 mins between wakeup commands
};
t.Tick += new EventHandler(timer_Tick);
t.Start();
System.Windows.Forms.Timer t2 = new System.Windows.Forms.Timer
{
Interval = 300 // 30ms
};
t2.Tick += new EventHandler(timer_Tick2);
t2.Start();
}
private async Task GetPublicConfigAsync()
{
await Task.Run(() => GetDependencies.updatePublicConfig());
try
{
string configFilePath = Path.Combine(Environment.CurrentDirectory, "vrp-public.json");
if (File.Exists(configFilePath))
{
string configFileData = File.ReadAllText(configFilePath);
PublicConfig config = JsonConvert.DeserializeObject<PublicConfig>(configFileData);
if (config != null && !string.IsNullOrWhiteSpace(config.BaseUri) && !string.IsNullOrWhiteSpace(config.Password))
{
PublicConfigFile = config;
hasPublicConfig = true;
}
}
}
catch
{
hasPublicConfig = false;
}
}
public static string donorApps = String.Empty;
private string oldTitle = String.Empty;
public static bool updatesNotified = false;
public static string backupFolder;
private static void KillAdbProcesses()
{
try
{
foreach (var p in Process.GetProcessesByName("adb"))
{
try
{
if (!p.HasExited)
{
p.Kill();
p.WaitForExit(3000);
}
}
catch (Exception ex)
{
Logger.Log($"Failed to kill adb process (PID {p.Id}): {ex.Message}", LogLevel.WARNING);
}
}
}
catch (Exception ex)
{
Logger.Log($"Error enumerating adb processes: {ex.Message}", LogLevel.WARNING);
}
}
private async void Form1_Load(object sender, EventArgs e)
{
_ = Logger.Log("Starting AndroidSideloader Application");
// Hard kill any lingering adb.exe instances to avoid port/handle conflicts
KillAdbProcesses();
// Show the form immediately
this.Show();
Application.DoEvents();
// Basic UI setup
CenterToScreen();
gamesListView.View = View.Details;
gamesListView.FullRowSelect = true;
gamesListView.GridLines = false;
etaLabel.Text = String.Empty;
speedLabel.Text = String.Empty;
diskLabel.Text = String.Empty;
verLabel.Text = Updater.LocalVersion;
settings.MainDir = Environment.CurrentDirectory;
settings.Save();
changeTitle(isOffline ? "Starting in Offline Mode..." : "Initializing...");
// Non-blocking background cleanup
_ = Task.Run(() =>
{
try
{
if (Directory.Exists(Sideloader.TempFolder))
{
Directory.Delete(Sideloader.TempFolder, true);
_ = Directory.CreateDirectory(Sideloader.TempFolder);
}
}
catch (Exception ex)
{
Logger.Log($"Error cleaning temp folder: {ex.Message}", LogLevel.WARNING);
}
});
// Non-blocking log file cleanup
_ = Task.Run(() =>
{
try
{
string logFilePath = settings.CurrentLogPath;
if (File.Exists(logFilePath))
{
FileInfo fileInfo = new FileInfo(logFilePath);
if (fileInfo.Length > 5 * 1024 * 1024)
{
File.Delete(logFilePath);
}
}
}
catch { }
});
// Dependencies and RCLONE in background
if (!isOffline)
{
_ = Task.Run(() =>
{
changeTitle("Downloading Dependencies...");
GetDependencies.downloadFiles();
changeTitle("Initializing RCLONE...");
RCLONE.Init();
});
}
// Crashlog handling
if (File.Exists("crashlog.txt"))
{
if (File.Exists(settings.CurrentCrashPath))
{
File.Delete(settings.CurrentCrashPath);
}
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form,
$"Sideloader crashed during your last use.\nPress OK if you'd like to send us your crash log.\n\n NOTE: THIS CAN TAKE UP TO 30 SECONDS.",
"Crash Detected", MessageBoxButtons.OKCancel);
if (dialogResult == DialogResult.OK)
{
if (File.Exists(Path.Combine(Environment.CurrentDirectory, "crashlog.txt")))
{
string UUID = SideloaderUtilities.UUID();
System.IO.File.Move("crashlog.txt", Path.Combine(Environment.CurrentDirectory, $"{UUID}.log"));
settings.CurrentCrashPath = Path.Combine(Environment.CurrentDirectory, $"{UUID}.log");
settings.CurrentCrashName = UUID;
settings.Save();
Clipboard.SetText(UUID);
// Upload in background
_ = Task.Run(() =>
{
_ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.CurrentCrashPath}\" RSL-gameuploads:CrashLogs");
this.Invoke(() =>
{
_ = FlexibleMessageBox.Show(Program.form,
$"Your CrashLog has been copied to the server.\nPlease mention your CrashLogID ({settings.CurrentCrashName}) to the Mods.\nIt has been automatically copied to your clipboard.");
Clipboard.SetText(settings.CurrentCrashName);
});
});
}
}
else
{
File.Delete(Path.Combine(Environment.CurrentDirectory, "crashlog.txt"));
}
}
// ADB initialization in background
_ = Task.Run(() =>
{
_ = Logger.Log("Attempting to Initialize ADB Server");
if (File.Exists(Path.Combine(Environment.CurrentDirectory, "platform-tools", "adb.exe")))
{
_ = ADB.RunAdbCommandToString("kill-server");
_ = ADB.RunAdbCommandToString("start-server");
}
});
// Continue with Form1_Shown
this.Form1_Shown(sender, e);
}
private async void Form1_Shown(object sender, EventArgs e)
{
searchTextBox.Enabled = false;
// Disclaimer thread
new Thread(() =>
{
Thread.Sleep(5000);
freeDisclaimer.Invoke(() =>
{
freeDisclaimer.Dispose();
freeDisclaimer.Enabled = false;
});
}).Start();
if (!isOffline)
{
string configFilePath = Path.Combine(Environment.CurrentDirectory, "vrp-public.json");
// Public config check
if (File.Exists(configFilePath))
{
await GetPublicConfigAsync();
if (!hasPublicConfig)
{
_ = FlexibleMessageBox.Show(Program.form,
"Failed to fetch public mirror config, and the current one is unreadable.\r\nPlease ensure you can access https://vrpirates.wiki/ in your browser.",
"Config Update Failed", MessageBoxButtons.OK);
}
}
else if (settings.AutoUpdateConfig && settings.CreatePubMirrorFile)
{
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form,
"Rookie has detected that you are missing the public config file, would you like to create it?",
"Public Config Missing", MessageBoxButtons.YesNo);
if (dialogResult == DialogResult.Yes)
{
File.Create(configFilePath).Close();
await GetPublicConfigAsync();
if (!hasPublicConfig)
{
_ = FlexibleMessageBox.Show(Program.form,
"Failed to fetch public mirror config, and the current one is unreadable.\r\nPlease ensure you can access https://vrpirates.wiki/ in your browser.",
"Config Update Failed", MessageBoxButtons.OK);
}
}
else
{
settings.CreatePubMirrorFile = false;
settings.AutoUpdateConfig = false;
settings.Save();
}
}
// WebView cleanup in background
_ = Task.Run(() =>
{
try
{
string webViewDirectoryPath = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "RSL", "EBWebView");
if (Directory.Exists(webViewDirectoryPath))
{
Directory.Delete(webViewDirectoryPath, true);
}
}
catch { }
});
// Pre-initialize trailer player in background
try
{
await EnsureTrailerEnvironmentAsync();
}
catch { /* swallow prewarm should never crash startup */ }
}
// UI setup
remotesList.Items.Clear();
if (hasPublicConfig)
{
UsingPublicConfig = true;
_ = Logger.Log($"Using Public Mirror");
}
if (isOffline)
{
lblMirror.Text = " Offline Mode";
remotesList.Size = System.Drawing.Size.Empty;
_ = Logger.Log($"Using Offline Mode");
}
if (settings.NodeviceMode)
{
btnNoDevice.Text = "Enable Sideloading";
}
progressBar.Style = ProgressBarStyle.Marquee;
// Update check
if (!debugMode && settings.CheckForUpdates && !isOffline)
{
Updater.AppName = "AndroidSideloader";
Updater.Repository = "VRPirates/rookie";
await Updater.Update();
}
if (!isOffline)
{
changeTitle("Getting Upload Config...");
await Task.Run(() => SideloaderRCLONE.updateUploadConfig());
_ = Logger.Log("Initializing Servers");
changeTitle("Initializing Servers...");
await initMirrors();
if (!UsingPublicConfig)
{
changeTitle("Grabbing the Games List...");
await Task.Run(() => SideloaderRCLONE.initGames(currentRemote));
}
}
else
{
changeTitle("Offline mode enabled, no Rclone");
}
changeTitle("Connecting to your Quest...");
await Task.Run(() =>
{
if (!string.IsNullOrEmpty(settings.IPAddress))
{
string path = Path.Combine(Environment.CurrentDirectory, "platform-tools", "adb.exe");
ProcessOutput wakeywakey = ADB.RunCommandToString($"\"{path}\" shell input keyevent KEYCODE_WAKEUP", path);
if (wakeywakey.Output.Contains("more than one"))
{
settings.Wired = true;
settings.Save();
}
else if (wakeywakey.Output.Contains("found"))
{
settings.Wired = false;
settings.Save();
}
}
if (File.Exists(storedIpPath) && !settings.Wired)
{
string IPcmndfromtxt = File.ReadAllText(storedIpPath);
settings.IPAddress = IPcmndfromtxt;
settings.Save();
ProcessOutput IPoutput = ADB.RunAdbCommandToString(IPcmndfromtxt);
if (IPoutput.Output.Contains("attempt failed") || IPoutput.Output.Contains("refused"))
{
this.Invoke(() =>
{
_ = FlexibleMessageBox.Show(Program.form,
"Attempt to connect to saved IP has failed. This is usually due to rebooting the device or not having a STATIC IP set in your router.\nYou must enable Wireless ADB again!");
});
settings.IPAddress = "";
settings.Save();
try { File.Delete(storedIpPath); }
catch (Exception ex) { Logger.Log($"Unable to delete StoredIP.txt due to {ex.Message}", LogLevel.ERROR); }
}
else
{
_ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_available 1");
_ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_enabled 1");
}
}
else if (!File.Exists(storedIpPath))
{
settings.IPAddress = "";
settings.Save();
}
});
// Metadata updates
if (UsingPublicConfig)
{
await Task.Run(() =>
{
changeTitle("Updating Metadata...");
SideloaderRCLONE.UpdateMetadataFromPublic();
changeTitle("Processing Metadata...");
SideloaderRCLONE.ProcessMetadataFromPublic();
});
}
else if (!isOffline)
{
await Task.Run(() =>
{
changeTitle("Updating Game Notes...");
SideloaderRCLONE.UpdateGameNotes(currentRemote);
changeTitle("Updating Game Thumbnails (This may take a minute or two)...");
SideloaderRCLONE.UpdateGamePhotos(currentRemote);
SideloaderRCLONE.UpdateNouns(currentRemote);
if (!Directory.Exists(SideloaderRCLONE.ThumbnailsFolder) ||
!Directory.Exists(SideloaderRCLONE.NotesFolder))
{
this.Invoke(() =>
{
_ = FlexibleMessageBox.Show(Program.form,
"It seems you are missing the thumbnails and/or notes database, the first start of the sideloader takes a bit more time, so dont worry if it looks stuck!");
});
}
});
}
progressBar.Style = ProgressBarStyle.Marquee;
changeTitle("Populating Game List...");
_ = await CheckForDevice();
if (ADB.DeviceID.Length < 5)
{
nodeviceonstart = true;
}
// Parallel execution
await Task.WhenAll(
Task.Run(() => listAppsBtn()),
Task.Run(() => showAvailableSpace())
);
downloadInstallGameButton.Enabled = true;
isLoading = false;
// Initialize list view
initListView(false);
// Cleanup in background
_ = Task.Run(() =>
{
string[] files = Directory.GetFiles(Environment.CurrentDirectory);
foreach (string file in files)
{
string fileName = Path.GetFileName(file);
if (!fileName.Contains(settings.CurrentLogName) &&
!fileName.Contains(settings.CurrentCrashName) &&
!fileName.Contains("debuglog") &&
fileName.EndsWith(".txt"))
{
try { System.IO.File.Delete(file); } catch { }
}
}
});
searchTextBox.Enabled = true;
if (isOffline)
{
lblMirror.Text = " Offline Mode";
remotesList.Size = System.Drawing.Size.Empty;
_ = Logger.Log($"Using Offline Mode");
}
changeTitlebarToDevice();
UpdateQuestInfoPanel();
}
private void timer_Tick(object sender, EventArgs e)
{
_ = ADB.RunAdbCommandToString("shell input keyevent KEYCODE_WAKEUP");
}
private void timer_Tick2(object sender, EventArgs e)
{
keyheld = false;
}
public async void changeTitle(string txt, bool reset = true)
{
try
{
if (ProgressText.IsDisposed)
{
return;
}
this.Invoke(() => { oldTitle = txt; Text = "Rookie Sideloader v" + Updater.LocalVersion + " | " + txt; });
ProgressText.Invoke(() =>
{
if (!ProgressText.IsDisposed)
{
var states = new[] { "Sideloading", "Installing", "Copying", "Comparing", "Deleting" };
if (ProgressText.ForeColor == Color.LimeGreen)
{
ProgressText.ForeColor = Color.White;
}
if (states.Any(txt.Contains))
{
ProgressText.ForeColor = Color.LimeGreen;
}
ProgressText.Text = txt;
}
});
if (!reset)
{
return;
}
await Task.Delay(TimeSpan.FromSeconds(5));
this.Invoke(() => { Text = "Rookie Sideloader v" + Updater.LocalVersion + " | " + oldTitle; });
ProgressText.Invoke(() =>
{
if (!ProgressText.IsDisposed)
{
ProgressText.Text = oldTitle;
}
});
}
catch
{
}
}
private void ShowSubMenu(Panel subMenu)
{
subMenu.Visible = subMenu.Visible == false;
}
private async void startsideloadbutton_Click(object sender, EventArgs e)
{
ProcessOutput output = new ProcessOutput("", "");
string path = string.Empty;
using (OpenFileDialog openFileDialog = new OpenFileDialog())
{
openFileDialog.Filter = "Android apps (*.apk)|*.apk";
openFileDialog.FilterIndex = 2;
openFileDialog.RestoreDirectory = true;
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
path = openFileDialog.FileName;
}
else
{
return;
}
}
ADB.DeviceID = GetDeviceID();
Thread t1 = new Thread(() =>
{
output += ADB.Sideload(path);
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
showAvailableSpace();
ShowPrcOutput(output);
}
public void ShowPrcOutput(ProcessOutput prcout)
{
string message = $"Output: {prcout.Output}";
if (prcout.Error.Length != 0)
{
message += $"\nError: {prcout.Error}";
}
_ = FlexibleMessageBox.Show(Program.form, message);
}
public List<string> Devices = new List<string>();
public async Task<int> CheckForDevice()
{
Devices.Clear();
string output = string.Empty;
string error = string.Empty;
string battery = string.Empty;
ADB.DeviceID = GetDeviceID();
Thread t1 = new Thread(() =>
{
output = ADB.RunAdbCommandToString("devices").Output;
});
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
string[] line = output.Split('\n');
int i = 0;
devicesComboBox.Items.Clear();
_ = Logger.Log("Devices:");
foreach (string currLine in line)
{
if (i > 0 && currLine.Length > 0)
{
Devices.Add(currLine.Split(' ')[0]);
_ = devicesComboBox.Items.Add(currLine.Split(' ')[0]);
_ = Logger.Log(currLine.Split(' ')[0] + "\n", LogLevel.INFO, false);
}
Debug.WriteLine(currLine);
i++;
}
if (devicesComboBox.Items.Count > 0)
{
devicesComboBox.SelectedIndex = 0;
}
battery = ADB.RunAdbCommandToString("shell dumpsys battery").Output;
battery = Utilities.StringUtilities.RemoveEverythingBeforeFirst(battery, "level:");
battery = Utilities.StringUtilities.RemoveEverythingAfterFirst(battery, "\n");
battery = Utilities.StringUtilities.KeepOnlyNumbers(battery);
batteryLabel.Text = battery + "%";
UpdateQuestInfoPanel();
return devicesComboBox.SelectedIndex;
}
public async void devicesbutton_Click(object sender, EventArgs e)
{
_ = await CheckForDevice();
changeTitlebarToDevice();
showAvailableSpace();
}
public static void notify(string message)
{
if (settings.EnableMessageBoxes)
{
_ = FlexibleMessageBox.Show(new Form
{
TopMost = true,
StartPosition = FormStartPosition.CenterScreen
}, message);
}
}
private async void obbcopybutton_Click(object sender, EventArgs e)
{
ProcessOutput output = new ProcessOutput(String.Empty, String.Empty);
FolderSelectDialog dialog = new FolderSelectDialog
{
Title = "Select OBB folder (must be direct OBB folder, E.G: com.Company.AppName)"
};
if (dialog.Show(Handle))
{
progressBar.Style = ProgressBarStyle.Marquee;
string path = dialog.FileName;
changeTitle($"Copying {path} obb to device...");
Thread t1 = new Thread(() =>
{
output += output += ADB.CopyOBB(path);
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
Program.form.changeTitle("Done.");
showAvailableSpace();
ShowPrcOutput(output);
Program.form.changeTitle(String.Empty);
}
}
public void changeTitlebarToDevice()
{
if (Devices.Contains("unauthorized"))
{
DeviceConnected = false;
this.Invoke(() =>
{
Text = "Device Not Authorized";
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "Please check inside your headset for ADB DEBUGGING prompt/notification, check the box \"Always allow from this computer.\" and hit OK.", "Not Authorized", MessageBoxButtons.RetryCancel);
if (dialogResult == DialogResult.Retry)
{
devicesbutton.PerformClick();
}
else
{
return;
}
});
}
else if (Devices.Count > 0 && Devices[0].Length > 1) // Check if Devices list is not empty and the first device has a valid length
{
this.Invoke(() => { Text = "Device Connected with ID | " + Devices[0].Replace("device", String.Empty); });
DeviceConnected = true;
}
else
{
this.Invoke(() =>
{
DeviceConnected = false;
Text = "No Device Connected";
if (!settings.NodeviceMode)
{
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "No device found. Please ensure the following: \n\n -Developer mode is enabled. \n -ADB drivers are installed. \n -ADB connection is enabled on your device (this can reset). \n -Your device is plugged in.\n\nThen press \"Retry\"", "No device found.", MessageBoxButtons.RetryCancel);
if (dialogResult == DialogResult.Retry)
{
devicesbutton.PerformClick();
}
else
{
return;
}
}
});
}
}
public async void showAvailableSpace()
{
string AvailableSpace = string.Empty;
if (!settings.NodeviceMode || DeviceConnected)
{
try
{
ADB.DeviceID = GetDeviceID();
Thread t1 = new Thread(() =>
{
AvailableSpace = ADB.GetAvailableSpace();
});
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
diskLabel.Invoke(() => { diskLabel.Text = AvailableSpace; });
UpdateQuestInfoPanel();
}
catch (Exception ex)
{
_ = Logger.Log($"Unable to get available space with the exception: {ex}", LogLevel.ERROR);
}
}
}
public string GetDeviceID()
{
string deviceId = string.Empty;
int index = -1;
devicesComboBox.Invoke(() => { index = devicesComboBox.SelectedIndex; });
if (index != -1)
{
devicesComboBox.Invoke(() => { deviceId = devicesComboBox.SelectedItem.ToString(); });
}
return deviceId;
}
public static string taa = String.Empty;
private async void backupadbbutton_Click(object sender, EventArgs e)
{
if (m_combo.SelectedIndex == -1)
{
notify("Please select an App from the Dropdown");
return;
}
if (!settings.CustomBackupDir)
{
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
}
else
{
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
}
if (!Directory.Exists(backupFolder))
{
_ = Directory.CreateDirectory(backupFolder);
}
string output = String.Empty;
string date_str = "ab." + DateTime.Today.ToString("yyyy.MM.dd");
string CurrBackups = Path.Combine(backupFolder, date_str);
Program.form.Invoke(new Action(() =>
{
FlexibleMessageBox.Show(Program.form, $"Backing up Game Data to {backupFolder}\\{date_str}");
}));
_ = Directory.CreateDirectory(CurrBackups);
string GameName = m_combo.SelectedItem.ToString();
string packageName = Sideloader.gameNameToPackageName(GameName);
string InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {packageName} | grep versionCode -F\"").Output;
changeTitle("Running ADB Backup...");
_ = FlexibleMessageBox.Show(Program.form, "Click OK on this Message...\r\nThen on your Quest, Unlock your device and confirm the backup operation by clicking on 'Back Up My Data'");
output = ADB.RunAdbCommandToString($"adb backup -f \"{CurrBackups}\\{packageName}.ab\" {packageName}").Output;
changeTitle(" \n\n");
}
private async void backupbutton_Click(object sender, EventArgs e)
{
if (!settings.CustomBackupDir)
{
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
}
else
{
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
}
if (!Directory.Exists(backupFolder))
{
_ = Directory.CreateDirectory(backupFolder);
}
DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, $"Do you want to backup to {backupFolder}?", "Backup?", MessageBoxButtons.YesNo);
if (dialogResult1 == DialogResult.No)
{
return;
}
ProcessOutput output = new ProcessOutput(String.Empty, String.Empty);
Thread t1 = new Thread(() =>
{
string date_str = DateTime.Today.ToString("yyyy.MM.dd");
string CurrBackups = Path.Combine(backupFolder, date_str);
Program.form.Invoke(new Action(() =>
{
FlexibleMessageBox.Show(Program.form, $"This may take up to a minute. Backing up gamesaves to {backupFolder}\\{date_str} (year.month.date)");
}));
_ = Directory.CreateDirectory(CurrBackups);
output = ADB.RunAdbCommandToString($"pull \"/sdcard/Android/data\" \"{CurrBackups}\"");
changeTitle("Backing up Game Data in SD/Android/data...");
try
{
Directory.Move(ADB.adbFolderPath + "\\data", CurrBackups + "\\data");
}
catch (Exception ex)
{
_ = Logger.Log($"Exception on backup: {ex}", LogLevel.ERROR);
}
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
changeTitle("Backing up Game Data in SD/Android/data...");
}
ShowPrcOutput(output);
changeTitle(" \n\n");
}
private async void restorebutton_Click(object sender, EventArgs e)
{
ProcessOutput output = new ProcessOutput("", "");
string output_abRestore = string.Empty;
if (!settings.CustomBackupDir)
{
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
}
else
{
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
}
FileDialog fileDialog = new OpenFileDialog();
fileDialog.Title = "Select a .ab Backup file or press Cancel to select a Folder";
fileDialog.CheckFileExists = true;
fileDialog.CheckPathExists = true;
fileDialog.ValidateNames = false;
fileDialog.InitialDirectory = backupFolder;
fileDialog.Filter = "Android Backup Files (*.ab)|*.ab|All Files (*.*)|*.*";
FolderBrowserDialog folderDialog = new FolderBrowserDialog();
folderDialog.Description = "Select Game Backup folder";
folderDialog.SelectedPath = backupFolder;
folderDialog.ShowNewFolderButton = false; // To prevent creating new folders
DialogResult fileDialogResult = fileDialog.ShowDialog();
DialogResult folderDialogResult = DialogResult.Cancel;
if (fileDialogResult == DialogResult.OK)
{
string selectedPath = fileDialog.FileName;
Logger.Log("Selected .ab file: " + selectedPath);
_ = FlexibleMessageBox.Show(Program.form, "Click OK on this Message...\r\nThen on your Quest, Unlock your device and confirm the backup operation by clicking on 'Restore My Data'\r\nRookie will remain frozen until the process is completed.");
output_abRestore = ADB.RunAdbCommandToString($"adb restore \"{selectedPath}\"").Output;
}
if (fileDialogResult != DialogResult.OK)
{
folderDialogResult = folderDialog.ShowDialog();
}
if (folderDialogResult == DialogResult.OK)
{
string selectedFolder = folderDialog.SelectedPath;
Logger.Log("Selected folder: " + selectedFolder);
Thread t1 = new Thread(() =>
{
if (selectedFolder.Contains("data"))
{
output += ADB.RunAdbCommandToString($"push \"{selectedFolder}\" /sdcard/Android/");
}
else
{
output += ADB.RunAdbCommandToString($"push \"{selectedFolder}\" /sdcard/Android/data/");
}
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
}
if (folderDialogResult == DialogResult.OK)
{
ShowPrcOutput(output);
}
else if (fileDialogResult == DialogResult.OK)
{
_ = FlexibleMessageBox.Show(Program.form, $"{output_abRestore}");
}
}
private string listApps()
{
ADB.DeviceID = GetDeviceID();
return ADB.RunAdbCommandToString("shell pm list packages -3").Output;
}
public void listAppsBtn()
{
m_combo.Invoke(() => { m_combo.Items.Clear(); });
string[] packages = listApps()
.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(p => p.StartsWith("package:") ? p.Substring(8).Trim() : p.Trim())
.ToArray();
// Save the full string for settings
settings.InstalledApps = string.Join("\n", packages);
settings.Save();
List<string> displayNames = new List<string>();
foreach (string pkg in packages)
{
string name = pkg;
foreach (string[] game in SideloaderRCLONE.games)
{
if (game.Length > 2 && game[2].Equals(pkg, StringComparison.OrdinalIgnoreCase))
{
name = game[0]; // Friendly game name
break;
}
}
displayNames.Add(name);
}
// Sort and populate combo
foreach (string name in displayNames.OrderBy(n => n))
{
if (!string.IsNullOrWhiteSpace(name))
{
m_combo.Invoke(() => { _ = m_combo.Items.Add(name); });
}
}
m_combo.Invoke(() => { m_combo.MatchingMethod = StringMatchingMethod.NoWildcards; });
}
public static bool isuploading = false;
public static bool isworking = false;
private async void getApkButton_Click(object sender, EventArgs e)
{
if (isOffline)
{
notify("You are not connected to the Internet!");
return;
}
if (m_combo.SelectedIndex == -1)
{
notify("Please select an App from the Dropdown");
return;
}
DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, $"Do you want to upload {m_combo.SelectedItem} now?", "Upload app?", MessageBoxButtons.YesNo);
if (dialogResult1 == DialogResult.No)
{
return;
}
string deviceCodeName = ADB.RunAdbCommandToString("shell getprop ro.product.device").Output.ToLower().Trim();
string codeNamesLink = "https://raw.githubusercontent.com/VRPirates/rookie/master/codenames";
bool codenameExists = false;
try
{
codenameExists = HttpClient.GetStringAsync(codeNamesLink).Result.Contains(deviceCodeName);
}
catch
{
_ = Logger.Log("Unable to download Codenames file.");
FlexibleMessageBox.Show(Program.form, $"Error downloading Codenames File from Github", "Verification Error", MessageBoxButtons.OK);
}
_ = Logger.Log($"Found Device Code Name: {deviceCodeName}");
_ = Logger.Log($"Identified as Meta Device: {codenameExists}");
if (codenameExists)
{
if (!isworking)
{
isworking = true;
progressBar.Style = ProgressBarStyle.Marquee;
string HWID = SideloaderUtilities.UUID();
string GameName = m_combo.SelectedItem.ToString();
string packageName = Sideloader.gameNameToPackageName(GameName);
string InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {packageName} | grep versionCode -F\"").Output;
InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingBeforeFirst(InstalledVersionCode, "versionCode=");
InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingAfterFirst(InstalledVersionCode, " ");
ulong VersionInt = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(InstalledVersionCode));
string gameName = $"{GameName} v{VersionInt} {packageName} {HWID.Substring(0, 1)} {deviceCodeName}";
string gameZipName = $"{gameName}.zip";
// Delete both zip & txt if the files exist, most likely due to a failed upload.
if (File.Exists($"{settings.MainDir}\\{gameZipName}"))
{
File.Delete($"{settings.MainDir}\\{gameZipName}");
}
if (File.Exists($"{settings.MainDir}\\{gameName}.txt"))
{
File.Delete($"{settings.MainDir}\\{gameName}.txt");
}
ProcessOutput output = new ProcessOutput("", "");
changeTitle("Extracting APK....");
_ = Directory.CreateDirectory($"{settings.MainDir}\\{packageName}");
Thread t1 = new Thread(() =>
{
output = Sideloader.getApk(GameName);
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
changeTitle("Extracting obb if it exists....");
Thread t2 = new Thread(() =>
{
output += ADB.RunAdbCommandToString($"pull \"/sdcard/Android/obb/{packageName}\" \"{settings.MainDir}\\{packageName}\"");
})
{
IsBackground = true
};
t2.Start();
while (t2.IsAlive)
{
await Task.Delay(100);
}
File.WriteAllText($"{settings.MainDir}\\{packageName}\\HWID.txt", HWID);
File.WriteAllText($"{settings.MainDir}\\{packageName}\\uploadMethod.txt", "manual");
changeTitle("Zipping extracted application...");
string cmd = $"7z a -mx1 \"{gameZipName}\" .\\{packageName}\\*";
string path = $"{settings.MainDir}\\7z.exe";
progressBar.Style = ProgressBarStyle.Continuous;
Thread t4 = new Thread(() =>
{
_ = ADB.RunCommandToString(cmd, path);
})
{
IsBackground = true
};
t4.Start();
while (t4.IsAlive)
{
await Task.Delay(100);
}
changeTitle("Uploading to server, you can continue to use Rookie while it uploads in the background.");
ULLabel.Visible = true;
isworking = false;
isuploading = true;
Thread t3 = new Thread(() =>
{
string currentlyUploading = GameName;
changeTitle("Uploading to server, you can continue to use Rookie while it uploads in the background.");
// Get size of pending zip upload and write to text file
long zipSize = new FileInfo($"{settings.MainDir}\\{gameZipName}").Length;
File.WriteAllText($"{settings.MainDir}\\{gameName}.txt", zipSize.ToString());
// Upload size file.
_ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.MainDir}\\{gameName}.txt\" RSL-gameuploads:");
// Upload zip.
_ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.MainDir}\\{gameZipName}\" RSL-gameuploads:");
// Delete files once uploaded.
File.Delete($"{settings.MainDir}\\{gameName}.txt");
File.Delete($"{settings.MainDir}\\{gameZipName}");
this.Invoke(() => FlexibleMessageBox.Show(Program.form, $"Upload of {currentlyUploading} is complete! Thank you for your contribution!"));
Directory.Delete($"{settings.MainDir}\\{packageName}", true);
})
{
IsBackground = true
};
t3.Start();
isuploading = true;
while (t3.IsAlive)
{
await Task.Delay(100);
}
changeTitle(" \n\n");
isuploading = false;
ULLabel.Visible = false;
}
else
{
_ = MessageBox.Show("You must wait until each app is finished uploading to start another.");
}
}
else
{
FlexibleMessageBox.Show(Program.form, $"You are attempting to upload from an unknown device, please connect a Meta Quest device to upload", "Unknown Device", MessageBoxButtons.OK);
}
}
private async void uninstallAppButton_Click(object sender, EventArgs e)
{
if (!settings.CustomBackupDir)
{
backupFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), $"Rookie Backups");
}
else
{
backupFolder = Path.Combine((settings.BackupDir), $"Rookie Backups");
}
string packagename;
if (m_combo.SelectedIndex == -1)
{
_ = FlexibleMessageBox.Show(Program.form, "Please select an app first");
return;
}
string GameName = m_combo.SelectedItem.ToString();
DialogResult dialogresult = FlexibleMessageBox.Show($"Are you sure you want to uninstall {GameName}?", "Proceed with uninstall?", MessageBoxButtons.YesNo);
if (dialogresult == DialogResult.No)
{
return;
}
DialogResult dialogresult2 = FlexibleMessageBox.Show($"Do you want to attempt to automatically backup any saves to {backupFolder}\\(TodaysDate)", "Attempt Game Backup?", MessageBoxButtons.YesNo);
packagename = !GameName.Contains(".") ? Sideloader.gameNameToPackageName(GameName) : GameName;
if (dialogresult2 == DialogResult.Yes)
{
Sideloader.BackupGame(packagename);
}
ProcessOutput output = new ProcessOutput("", "");
progressBar.Style = ProgressBarStyle.Marquee;
Thread t1 = new Thread(() =>
{
output += Sideloader.UninstallGame(packagename);
});
t1.Start();
t1.IsBackground = true;
while (t1.IsAlive)
{
await Task.Delay(100);
}
ShowPrcOutput(output);
showAvailableSpace();
progressBar.Style = ProgressBarStyle.Continuous;
m_combo.Items.RemoveAt(m_combo.SelectedIndex);
}
private async void copyBulkObbButton_Click(object sender, EventArgs e)
{
FolderSelectDialog dialog = new FolderSelectDialog
{
Title = "Select your folder with OBBs"
};
if (dialog.Show(Handle))
{
Thread t1 = new Thread(() =>
{
Sideloader.RecursiveOutput = new ProcessOutput(String.Empty, String.Empty);
Sideloader.RecursiveCopyOBB(dialog.FileName);
})
{
IsBackground = true
};
t1.Start();
showAvailableSpace();
while (t1.IsAlive)
{
await Task.Delay(100);
}
ShowPrcOutput(Sideloader.RecursiveOutput);
}
}
private async void Form1_DragDrop(object sender, DragEventArgs e)
{
if (nodeviceonstart && !updatesNotified)
{
_ = await CheckForDevice();
changeTitlebarToDevice();
showAvailableSpace();
changeTitle("Device now detected... refreshing update list.");
listAppsBtn();
initListView(false);
}
Program.form.changeTitle($"Processing dropped file. If Rookie freezes, please wait. Do not close Rookie!");
DragDropLbl.Visible = false;
ProcessOutput output = new ProcessOutput(String.Empty, String.Empty);
ADB.DeviceID = GetDeviceID();
progressBar.Style = ProgressBarStyle.Marquee;
CurrPCKG = String.Empty;
string[] datas = (string[])e.Data.GetData(DataFormats.FileDrop);
foreach (string data in datas)
{
string directory = Path.GetDirectoryName(data);
//if is directory
string dir = Path.GetDirectoryName(data);
string path = $"{dir}\\Install.txt";
if (Directory.Exists(data))
{
string installFilePath = Directory.GetFiles(data)
.FirstOrDefault(f => string.Equals(Path.GetFileName(f), "install.txt", StringComparison.OrdinalIgnoreCase));
if (installFilePath != null)
{
// Run commands from install.txt
output += Sideloader.RunADBCommandsFromFile(installFilePath);
continue; // Skip further processing if install.txt is found
}
if (!data.Contains("+") && !data.Contains("_") && data.Contains("."))
{
_ = Logger.Log($"Copying {data} to device");
Program.form.changeTitle($"Copying {data} to device...");
Thread t2 = new Thread(() =>
{
output += ADB.CopyOBB(data);
})
{
IsBackground = true
};
t2.Start();
while (t2.IsAlive)
{
await Task.Delay(100);
}
Program.form.changeTitle(String.Empty);
settings.CurrPckg = dir;
settings.Save();
}
Program.form.changeTitle(String.Empty);
string extension = Path.GetExtension(data);
string[] files = Directory.GetFiles(data);
foreach (string file2 in files)
{
if (File.Exists(file2))
{
if (file2.EndsWith(".apk"))
{
string pathname = Path.GetDirectoryName(file2);
string filename = file2.Replace($"{pathname}\\", String.Empty);
string cmd = $"\"{aaptPath}\" dump badging \"{file2}\" | findstr -i \"package: name\"";
_ = Logger.Log($"Running adb command-{cmd}");
string cmdout = ADB.RunCommandToString(cmd, file2).Output;
cmdout = Utilities.StringUtilities.RemoveEverythingBeforeFirst(cmdout, "=");
cmdout = Utilities.StringUtilities.RemoveEverythingAfterFirst(cmdout, " ");
cmdout = cmdout.Replace("'", String.Empty);
cmdout = cmdout.Replace("=", String.Empty);
CurrPCKG = cmdout;
CurrAPK = file2;
System.Windows.Forms.Timer t3 = new System.Windows.Forms.Timer
{
Interval = 150000 // 150 seconds to fail
};
t3.Tick += timer_Tick4;
t3.Start();
Program.form.changeTitle($"Sideloading apk ({filename})");
Thread t2 = new Thread(() =>
{
output += ADB.Sideload(file2);
})
{
IsBackground = true
};
t2.Start();
while (t2.IsAlive)
{
await Task.Delay(100);
}
t3.Stop();
if (Directory.Exists($"{pathname}\\{cmdout}"))
{
_ = Logger.Log($"Copying obb folder to device- {cmdout}");
Program.form.changeTitle($"Copying obb folder to device...");
Thread t1 = new Thread(() =>
{
if (!string.IsNullOrEmpty(cmdout))
{
_ = ADB.RunAdbCommandToString($"shell rm -rf \"/sdcard/Android/obb/{cmdout}\" && mkdir \"/sdcard/Android/obb/{cmdout}\"");
}
_ = ADB.RunAdbCommandToString($"push \"{pathname}\\{cmdout}\" /sdcard/Android/obb/");
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
}
}
if (file2.EndsWith(".zip") && settings.BMBFChecked)
{
string datazip = file2;
string zippath = Path.GetDirectoryName(data);
datazip = datazip.Replace(zippath, "");
datazip = Utilities.StringUtilities.RemoveEverythingAfterFirst(datazip, ".");
datazip = datazip.Replace(".", "");
string command2 = $"\"{settings.MainDir}\\7z.exe\" e \"{file2}\" -o\"{zippath}\\{datazip}\\\"";
Thread t2 = new Thread(() =>
{
_ = ADB.RunCommandToString(command2, file2);
output += ADB.RunAdbCommandToString($"push \"{zippath}\\{datazip}\" \"/sdcard/ModData/com.beatgames.beatsaber/Mods/SongLoader/CustomLevels/\"");
})
{
IsBackground = true
};
t2.Start();
while (t2.IsAlive)
{
await Task.Delay(100);
}
Directory.Delete($"{zippath}\\{datazip}", true);
}
}
}
string[] folders = Directory.GetDirectories(data);
foreach (string folder in folders)
{
_ = Logger.Log($"Copying {folder} to device");
Program.form.changeTitle($"Copying {folder} to device...");
Thread t2 = new Thread(() =>
{
output += ADB.CopyOBB(folder);
})
{
IsBackground = true
};
t2.Start();
while (t2.IsAlive)
{
await Task.Delay(100);
}
Program.form.changeTitle("");
settings.CurrPckg = dir;
settings.Save();
}
}
//if it's a file
else if (File.Exists(data))
{
string extension = Path.GetExtension(data);
if (extension == ".apk")
{
if (File.Exists($"{dir}\\Install.txt"))
{
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "Special instructions have been found with this file, would you like to run them automatically?", "Special Instructions found!", MessageBoxButtons.OKCancel);
if (dialogResult == DialogResult.Cancel)
{
return;
}
else
{
_ = Logger.Log($"Sideloading custom install.txt");
changeTitle("Sideloading custom install.txt automatically.");
Thread t1 = new Thread(() =>
{
output += Sideloader.RunADBCommandsFromFile(path);
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
changeTitle(" \n\n");
}
}
else
{
string pathname = Path.GetDirectoryName(data);
string dataname = data.Replace($"{pathname}\\", "");
string cmd = $"\"{aaptPath}\" dump badging \"{data}\" | findstr -i \"package: name\"";
_ = Logger.Log($"Running adb command-{cmd}");
string cmdout = ADB.RunCommandToString(cmd, data).Output;
cmdout = Utilities.StringUtilities.RemoveEverythingBeforeFirst(cmdout, "=");
cmdout = Utilities.StringUtilities.RemoveEverythingAfterFirst(cmdout, " ");
cmdout = cmdout.Replace("'", "");
cmdout = cmdout.Replace("=", "");
CurrPCKG = cmdout;
CurrAPK = data;
System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer
{
Interval = 150000 // 150 seconds to fail
};
timer.Tick += timer_Tick4;
timer.Start();
changeTitle($"Installing {dataname}...");
Thread t1 = new Thread(() =>
{
output += ADB.Sideload(data);
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
timer.Stop();
if (Directory.Exists($"{pathname}\\{cmdout}"))
{
_ = Logger.Log($"Copying obb folder to device- {cmdout}");
Program.form.changeTitle($"Copying obb folder to device...");
Thread t2 = new Thread(() =>
{
if (!string.IsNullOrEmpty(cmdout))
{
_ = ADB.RunAdbCommandToString($"shell rm -rf \"/sdcard/Android/obb/{cmdout}\" && mkdir \"/sdcard/Android/obb/{cmdout}\"");
}
_ = ADB.RunAdbCommandToString($"push \"{pathname}\\{cmdout}\" /sdcard/Android/obb/");
})
{
IsBackground = true
};
t2.Start();
while (t2.IsAlive)
{
await Task.Delay(100);
}
changeTitle(" \n\n");
}
}
}
//If obb is dragged and dropped alone onto Rookie, Rookie will recreate its obb folder automatically with this code.
else if (extension == ".obb")
{
string filename = Path.GetFileName(data);
string foldername = filename.Substring(filename.IndexOf('.') + 1);
foldername = foldername.Substring(foldername.IndexOf('.') + 1);
foldername = foldername.Replace(".obb", "");
foldername = Path.Combine(Environment.CurrentDirectory, foldername);
_ = Directory.CreateDirectory(foldername);
File.Copy(data, Path.Combine(foldername, filename));
path = foldername;
Thread t1 = new Thread(() =>
{
output += ADB.CopyOBB(path);
})
{
IsBackground = true
};
_ = Logger.Log($"Copying obb folder to device- {path}");
Program.form.changeTitle($"Copying obb folder to device ({filename})");
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
Directory.Delete(foldername, true);
changeTitle(" \n\n");
}
// BMBF Zip extraction then push to BMBF song folder on Quest.
else if (extension == ".zip" && settings.BMBFChecked)
{
string datazip = data;
string zippath = Path.GetDirectoryName(data);
datazip = datazip.Replace(zippath, "");
datazip = Utilities.StringUtilities.RemoveEverythingAfterFirst(datazip, ".");
datazip = datazip.Replace(".", "");
string command = $"\"{settings.MainDir}\\7z.exe\" e \"{data}\" -o\"{zippath}\\{datazip}\\\"";
Thread t1 = new Thread(() =>
{
_ = ADB.RunCommandToString(command, data);
output += ADB.RunAdbCommandToString($"push \"{zippath}\\{datazip}\" \"/sdcard/ModData/com.beatgames.beatsaber/Mods/SongLoader/CustomLevels/\"");
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
Directory.Delete($"{zippath}\\{datazip}", true);
}
else if (extension == ".txt")
{
changeTitle("Sideloading custom install.txt automatically.");
Thread t1 = new Thread(() =>
{
output += Sideloader.RunADBCommandsFromFile(path);
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
changeTitle(" \n\n");
}
}
}
progressBar.Style = ProgressBarStyle.Continuous;
showAvailableSpace();
DragDropLbl.Visible = false;
ShowPrcOutput(output);
listAppsBtn();
}
private void Form1_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
e.Effect = DragDropEffects.Copy;
}
DragDropLbl.Visible = true;
DragDropLbl.Text = "Drag apk or obb";
changeTitle(DragDropLbl.Text);
}
private void Form1_DragLeave(object sender, EventArgs e)
{
DragDropLbl.Visible = false;
DragDropLbl.Text = String.Empty;
changeTitle(" \n\n");
}
private List<string> newGamesList = new List<string>();
private List<string> newGamesToUploadList = new List<string>();
private readonly List<UpdateGameData> gamesToAskForUpdate = new List<UpdateGameData>();
public static bool loaded = false;
public static string rookienamelist;
public static bool updates = false;
public static bool newapps = false;
public static int newint = 0;
public static int updint = 0;
public static bool nodeviceonstart = false;
public static bool either = false;
private bool _allItemsInitialized = false;
private async void initListView(bool favoriteView)
{
// Stopwatch for perf logging
var sw = Stopwatch.StartNew();
Logger.Log("initListView started");
int upToDateCount = 0;
int updateAvailableCount = 0;
int newerThanListCount = 0;
rookienamelist = string.Empty;
loaded = false;
// Read installed packages prepared by listAppsBtn()
string installedApps = settings.InstalledApps;
string[] packageList = installedApps.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
// Early return if no packages available
if (packageList.Length == 0)
{
Logger.Log("No installed packages found");
loaded = true;
return;
}
string[] blacklist = new string[] { };
string[] whitelist = new string[] { };
// Load blacklists/whitelists concurrently
await Task.WhenAll(
Task.Run(() =>
{
if (File.Exists($"{settings.MainDir}\\nouns\\blacklist.txt"))
{
blacklist = File.ReadAllLines($"{settings.MainDir}\\nouns\\blacklist.txt");
}
// Merge local blacklist.json if present
string localBlacklistPath = Path.Combine(settings.MainDir, "blacklist.json");
if (File.Exists(localBlacklistPath))
{
try
{
string jsonContent = File.ReadAllText(localBlacklistPath);
string[] localBlacklist = JsonConvert.DeserializeObject<string[]>(jsonContent);
if (localBlacklist != null && localBlacklist.Length > 0)
{
var combined = new List<string>(blacklist);
combined.AddRange(localBlacklist);
blacklist = combined.ToArray();
Logger.Log($"Loaded {localBlacklist.Length} entries from local blacklist");
}
}
catch (Exception ex)
{
Logger.Log($"Error loading local blacklist: {ex.Message}", LogLevel.WARNING);
}
}
}),
Task.Run(() =>
{
if (File.Exists($"{settings.MainDir}\\nouns\\whitelist.txt"))
{
whitelist = File.ReadAllLines($"{settings.MainDir}\\nouns\\whitelist.txt");
}
})
);
Logger.Log($"Blacklist/Whitelist loaded in {sw.ElapsedMilliseconds}ms");
var GameList = new List<ListViewItem>();
var rookieList = new List<string>();
var installedGames = packageList.ToList();
var blacklistItems = blacklist.ToList();
var whitelistItems = whitelist.ToList();
newGamesToUploadList = whitelistItems.Intersect(installedGames, StringComparer.OrdinalIgnoreCase).ToList();
if (SideloaderRCLONE.games.Count > 5)
{
progressBar.Style = ProgressBarStyle.Marquee;
// Fast path: collect all installed version codes in one call
Dictionary<string, ulong> installedVersions = new Dictionary<string, ulong>(StringComparer.OrdinalIgnoreCase);
await Task.Run(() =>
{
Logger.Log("Fast path fetching version codes...");
// Single dumpsys parse
try
{
var dump = ADB.RunAdbCommandToString("shell dumpsys package").Output;
var map = new Dictionary<string, ulong>(StringComparer.OrdinalIgnoreCase);
string currentPkg = null;
// dumpsys package structure:
// Package [com.example.app] (..)
// versionCode=12345 ...
foreach (var rawLine in dump.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
{
var line = rawLine.Trim();
if (line.StartsWith("Package [", StringComparison.OrdinalIgnoreCase))
{
var start = line.IndexOf('[');
var end = line.IndexOf(']');
if (start >= 0 && end > start)
{
currentPkg = line.Substring(start + 1, end - start - 1);
}
else
{
currentPkg = null;
}
continue;
}
if (currentPkg != null && line.StartsWith("versionCode=", StringComparison.OrdinalIgnoreCase))
{
var after = line.Substring("versionCode=".Length).Trim();
var digits = new string(after.TakeWhile(char.IsDigit).ToArray());
if (!string.IsNullOrEmpty(digits) && ulong.TryParse(digits, out var v))
{
map[currentPkg] = v;
}
continue;
}
}
// Filter to the installed third-party packages
var set = new HashSet<string>(packageList, StringComparer.OrdinalIgnoreCase);
foreach (var kv in map)
{
if (set.Contains(kv.Key))
{
installedVersions[kv.Key] = kv.Value;
}
}
Logger.Log($"Collected {installedVersions.Count} version codes in {sw.ElapsedMilliseconds}ms");
}
catch (Exception ex)
{
Logger.Log($"'dumpsys package' failed: {ex.Message}", LogLevel.ERROR);
}
});
// Precompute cloud max version per package to avoid scanning inside the loop
var cloudMaxVersionByPackage = new Dictionary<string, ulong>(StringComparer.OrdinalIgnoreCase);
foreach (var release in SideloaderRCLONE.games)
{
string pkg = release[SideloaderRCLONE.PackageNameIndex];
// Parse cloud version once
ulong v = 0;
try
{
v = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(release[SideloaderRCLONE.VersionCodeIndex]));
}
catch
{
v = 0;
}
if (cloudMaxVersionByPackage.TryGetValue(pkg, out var existing))
{
if (v > existing) cloudMaxVersionByPackage[pkg] = v;
}
else
{
cloudMaxVersionByPackage[pkg] = v;
}
}
Logger.Log($"Precomputed cloud max versions in {sw.ElapsedMilliseconds}ms");
// Process games and colorize based on version comparisons
await Task.Run(() =>
{
Color colorFont_installedGame = ColorTranslator.FromHtml("#3c91e6");
Color colorFont_updateAvailable = ColorTranslator.FromHtml("#4daa57");
Color colorFont_donateGame = ColorTranslator.FromHtml("#cb9cf2");
Color colorFont_error = ColorTranslator.FromHtml("#f52f57");
foreach (string[] release in SideloaderRCLONE.games)
{
string packagename = release[SideloaderRCLONE.PackageNameIndex];
rookieList.Add(packagename);
if (!rookienamelist.Contains(release[SideloaderRCLONE.GameNameIndex]))
{
rookienamelist += release[SideloaderRCLONE.GameNameIndex] + "\n";
}
var item = new ListViewItem(release);
// Installed?
if (installedVersions.TryGetValue(packagename, out ulong installedVersionInt))
{
item.ForeColor = colorFont_installedGame;
try
{
ulong cloudVersionInt = 0;
cloudMaxVersionByPackage.TryGetValue(packagename, out cloudVersionInt);
if (installedVersionInt == cloudVersionInt)
{
upToDateCount++;
}
else if (installedVersionInt < cloudVersionInt)
{
item.ForeColor = colorFont_updateAvailable;
updateAvailableCount++;
}
else if (installedVersionInt > cloudVersionInt)
{
newerThanListCount++;
bool dontget = blacklistItems.Contains(packagename, StringComparer.OrdinalIgnoreCase);
if (!dontget)
{
item.ForeColor = colorFont_donateGame;
if (!updatesNotified && !isworking && updint < 6 && !settings.SubmittedUpdates.Contains(packagename))
{
either = true;
updates = true;
updint++;
string RlsName = Sideloader.PackageNametoGameName(packagename);
string GameName = Sideloader.gameNameToSimpleName(RlsName);
var gameData = new UpdateGameData(GameName, packagename, installedVersionInt);
gamesToAskForUpdate.Add(gameData);
}
}
}
//Logger.Log($"Checked game {release[SideloaderRCLONE.GameNameIndex]}; cloudversion={cloudVersionInt} localversion={installedVersionInt}");
}
catch (Exception ex)
{
item.ForeColor = colorFont_error;
Logger.Log($"An error occurred while rendering game {release[SideloaderRCLONE.GameNameIndex]} in ListView", LogLevel.ERROR);
Logger.Log($"ExMsg: {ex.Message}", LogLevel.ERROR);
}
}
if (favoriteView)
{
// Only add favorited games when favoriteView is true
if (settings.FavoritedGames.Contains(item.SubItems[1].Text))
{
GameList.Add(item);
}
}
else
{
GameList.Add(item);
}
}
});
Logger.Log($"Game processing completed in {sw.ElapsedMilliseconds}ms");
}
else if (!isOffline)
{
// If no games are loaded, try switching mirrors then rebuild
SwitchMirrors();
if (!isOffline)
{
initListView(false);
}
return;
}
if (blacklistItems.Count == 0 && GameList.Count == 0 && !settings.NodeviceMode && !isOffline)
{
_ = FlexibleMessageBox.Show(Program.form,
"Rookie seems to have failed to load all resources. Please try restarting Rookie a few times.\nIf error still persists please disable any VPN or firewalls (rookie uses direct download so a VPN is not needed)\nIf this error still persists try a system reboot, reinstalling the program, and lastly posting the problem on telegram.",
"Error loading blacklist or game list!");
}
// New apps detection
newGamesList = installedGames
.Except(rookieList, StringComparer.OrdinalIgnoreCase)
.Except(blacklistItems, StringComparer.OrdinalIgnoreCase)
.ToList();
if (blacklistItems.Count > 100 && rookieList.Count > 100)
{
await ProcessNewApps(newGamesList, blacklistItems);
}
progressBar.Style = ProgressBarStyle.Continuous;
if (either && !updatesNotified && !noAppCheck)
{
changeTitle("\n\n");
DonorsListViewForm donorForm = new DonorsListViewForm();
_ = donorForm.ShowDialog(this);
_ = Focus();
}
// Update UI with computed list
this.Invoke(() =>
{
changeTitle("Populating update list...\n\n");
lblUpToDate.Text = $"[{upToDateCount}] UP TO DATE";
lblUpToDate.ForeColor = ColorTranslator.FromHtml("#3c91e6");
lblUpdateAvailable.Text = $"[{updateAvailableCount}] UPDATE AVAILABLE";
lblUpdateAvailable.ForeColor = ColorTranslator.FromHtml("#4daa57");
lblNeedsDonate.Text = $"[{newerThanListCount}] NEWER THAN LIST";
lblNeedsDonate.ForeColor = ColorTranslator.FromHtml("#cb9cf2");
ListViewItem[] arr = GameList.ToArray();
gamesListView.BeginUpdate();
gamesListView.Items.Clear();
gamesListView.Items.AddRange(arr);
gamesListView.EndUpdate();
});
changeTitle("\n\n");
// Build search index only once
if (!_allItemsInitialized)
{
_allItems = gamesListView.Items.Cast<ListViewItem>().ToList();
_searchIndex = new Dictionary<string, List<ListViewItem>>(StringComparer.OrdinalIgnoreCase);
foreach (var item in _allItems)
{
string gameName = item.Text;
if (!_searchIndex.ContainsKey(gameName))
{
_searchIndex[gameName] = new List<ListViewItem>();
}
_searchIndex[gameName].Add(item);
if (item.SubItems.Count > 1)
{
string releaseName = item.SubItems[1].Text;
if (!_searchIndex.ContainsKey(releaseName))
{
_searchIndex[releaseName] = new List<ListViewItem>();
}
_searchIndex[releaseName].Add(item);
}
}
_allItemsInitialized = true;
}
loaded = true;
Logger.Log($"initListView completed in {sw.ElapsedMilliseconds}ms");
}
private async Task ProcessNewApps(List<string> newGamesList, List<string> blacklistItems)
{
foreach (UpdateGameData gameData in gamesToAskForUpdate)
{
if (!updatesNotified && !settings.SubmittedUpdates.Contains(gameData.Packagename))
{
either = true;
updates = true;
donorApps += gameData.GameName + ";" + gameData.Packagename + ";" + gameData.InstalledVersionInt + ";" + "Update" + "\n";
}
}
string baseApkPath = Path.Combine(Environment.CurrentDirectory, "platform-tools", "base.apk");
if (blacklistItems.Count > 100 && !noAppCheck)
{
foreach (string newGamesToUpload in newGamesList)
{
try
{
changeTitle("Unrecognized App Found. Downloading APK to take a closer look. (This may take a minute)");
bool onapplist = false;
string NewApp = settings.NonAppPackages + "\n" + settings.AppPackages;
if (NewApp.Contains(newGamesToUpload))
{
onapplist = true;
Logger.Log($"App '{newGamesToUpload}' found in app list.", LogLevel.INFO);
}
string RlsName = Sideloader.PackageNametoGameName(newGamesToUpload);
Logger.Log($"Release name obtained: {RlsName}", LogLevel.INFO);
if (!updatesNotified && !onapplist && newint < 6)
{
either = true;
newapps = true;
Logger.Log($"New app detected: {newGamesToUpload}, starting APK extraction and AAPT process.", LogLevel.INFO);
string apppath = ADB.RunAdbCommandToString($"shell pm path {newGamesToUpload}").Output;
Logger.Log($"ADB command 'pm path' executed. Path: {apppath}", LogLevel.INFO);
apppath = Utilities.StringUtilities.RemoveEverythingBeforeFirst(apppath, "/");
apppath = Utilities.StringUtilities.RemoveEverythingAfterFirst(apppath, "\r\n");
if (File.Exists(baseApkPath))
{
File.Delete(baseApkPath);
Logger.Log("Old base.apk file deleted.", LogLevel.INFO);
}
Logger.Log($"Pulling APK from path: {apppath}", LogLevel.INFO);
_ = ADB.RunAdbCommandToString($"pull \"{apppath}\"");
string cmd = $"\"{aaptPath}\" dump badging \"{baseApkPath}\" | findstr -i \"application-label\"";
Logger.Log($"Running AAPT command: {cmd}", LogLevel.INFO);
string ReleaseName = ADB.RunCommandToString(cmd, aaptPath).Output;
Logger.Log($"AAPT command output: {ReleaseName}", LogLevel.INFO);
ReleaseName = Utilities.StringUtilities.RemoveEverythingBeforeFirst(ReleaseName, "'");
ReleaseName = Utilities.StringUtilities.RemoveEverythingAfterFirst(ReleaseName, "\r\n");
ReleaseName = ReleaseName.Replace("'", "");
File.Delete(baseApkPath);
Logger.Log("Base.apk deleted after extracting release name.", LogLevel.INFO);
if (ReleaseName.Contains("Microsoft Windows"))
{
ReleaseName = RlsName;
Logger.Log("Release name fallback to RlsName due to Microsoft Windows detection.", LogLevel.INFO);
}
Logger.Log($"Final Release Name: {ReleaseName}", LogLevel.INFO);
string GameName = Sideloader.gameNameToSimpleName(RlsName);
Logger.Log($"Fetching version code for app: {newGamesToUpload}", LogLevel.INFO);
string InstalledVersionCode;
InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {newGamesToUpload} | grep versionCode -F\"").Output;
Logger.Log($"Version code command output: {InstalledVersionCode}", LogLevel.INFO);
InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingBeforeFirst(InstalledVersionCode, "versionCode=");
InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingAfterFirst(InstalledVersionCode, " ");
ulong installedVersionInt = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(InstalledVersionCode));
Logger.Log($"Parsed installed version code: {installedVersionInt}", LogLevel.INFO);
donorApps += ReleaseName + ";" + newGamesToUpload + ";" + installedVersionInt + ";" + "New App" + "\n";
Logger.Log($"Donor app info updated: {ReleaseName}; {newGamesToUpload}; {installedVersionInt}", LogLevel.INFO);
newint++;
}
}
catch (Exception ex)
{
Logger.Log($"Exception occured in ProcessNewApps: {ex.Message}", LogLevel.ERROR);
}
}
}
}
private static readonly HttpClient HttpClient = new HttpClient();
public static async void doUpload()
{
Program.form.changeTitle("Uploading to server, you can continue to use Rookie while it uploads in the background.");
Program.form.ULLabel.Visible = true;
isworking = true;
string deviceCodeName = ADB.RunAdbCommandToString("shell getprop ro.product.device").Output.ToLower().Trim();
string codeNamesLink = "https://raw.githubusercontent.com/VRPirates/rookie/master/codenames";
bool codenameExists = false;
try
{
codenameExists = HttpClient.GetStringAsync(codeNamesLink).Result.Contains(deviceCodeName);
}
catch
{
_ = Logger.Log("Unable to download Codenames file.");
FlexibleMessageBox.Show(Program.form, $"Error downloading Codenames File from Github", "Verification Error", MessageBoxButtons.OK);
}
if (codenameExists)
{
foreach (UploadGame game in Program.form.gamesToUpload)
{
Thread t3 = new Thread(() =>
{
string packagename = game.Pckgcommand;
string gameName = $"{game.Uploadgamename} v{game.Uploadversion} {game.Pckgcommand} {SideloaderUtilities.UUID().Substring(0, 1)} {deviceCodeName}";
string gameZipName = $"{gameName}.zip";
// Delete both zip & txt if the files exist, most likely due to a failed upload.
if (File.Exists($"{settings.MainDir}\\{gameZipName}"))
{
File.Delete($"{settings.MainDir}\\{gameZipName}");
}
if (File.Exists($"{settings.MainDir}\\{gameName}.txt"))
{
File.Delete($"{settings.MainDir}\\{gameName}.txt");
}
string path = $"{settings.MainDir}\\7z.exe";
string cmd = $"7z a -mx1 \"{settings.MainDir}\\{gameZipName}\" .\\{game.Pckgcommand}\\*";
Program.form.changeTitle("Zipping extracted application...");
_ = ADB.RunCommandToString(cmd, path);
if (Directory.Exists($"{settings.MainDir}\\{game.Pckgcommand}"))
{
Directory.Delete($"{settings.MainDir}\\{game.Pckgcommand}", true);
}
Program.form.changeTitle("Uploading to server, you may continue to use Rookie while it uploads.");
// Get size of pending zip upload and write to text file
long zipSize = new FileInfo($"{settings.MainDir}\\{gameZipName}").Length;
File.WriteAllText($"{settings.MainDir}\\{gameName}.txt", zipSize.ToString());
// Upload size file.
_ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.MainDir}\\{gameName}.txt\" RSL-gameuploads:");
// Upload zip.
_ = RCLONE.runRcloneCommand_UploadConfig($"copy \"{settings.MainDir}\\{gameZipName}\" RSL-gameuploads:");
if (game.isUpdate)
{
settings.SubmittedUpdates += game.Pckgcommand + "\n";
settings.Save();
}
// Delete files once uploaded.
if (File.Exists($"{settings.MainDir}\\{gameName}.txt"))
{
File.Delete($"{settings.MainDir}\\{gameName}.txt");
}
if (File.Exists($"{settings.MainDir}\\{gameZipName}"))
{
File.Delete($"{settings.MainDir}\\{gameZipName}");
}
})
{
IsBackground = true
};
t3.Start();
while (t3.IsAlive)
{
isuploading = true;
await Task.Delay(100);
}
}
Program.form.gamesToUpload.Clear();
isworking = false;
isuploading = false;
Program.form.ULLabel.Visible = false;
Program.form.changeTitle(" \n\n");
}
else
{
FlexibleMessageBox.Show(Program.form, $"You are attempting to upload from an unknown device, please connect a Meta Quest device to upload", "Unknown Device", MessageBoxButtons.OK);
}
}
public static async void newPackageUpload()
{
if (!string.IsNullOrEmpty(settings.NonAppPackages) && !settings.ListUpped)
{
Random r = new Random();
int x = r.Next(9999);
int y = x;
File.WriteAllText($"{settings.MainDir}\\FreeOrNonVR{y}.txt", settings.NonAppPackages);
string path = $"{settings.MainDir}\\rclone\\rclone.exe";
Thread t1 = new Thread(() =>
{
_ = ADB.RunCommandToString($"\"{settings.MainDir}\\rclone\\rclone.exe\" copy \"{settings.MainDir}\\FreeOrNonVR{y}.txt\" VRP-debuglogs:InstalledGamesList", path);
File.Delete($"{settings.MainDir}\\FreeOrNonVR{y}.txt");
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
settings.ListUpped = true;
settings.Save();
}
}
public async Task extractAndPrepareGameToUploadAsync(string GameName, string packagename, ulong installedVersionInt, bool isupdate)
{
progressBar.Style = ProgressBarStyle.Marquee;
Thread t1 = new Thread(() =>
{
_ = Sideloader.getApk(packagename);
});
changeTitle("Extracting APK file....");
t1.IsBackground = true;
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
changeTitle("Extracting obb if it exists....");
Thread t2 = new Thread(() =>
{
_ = ADB.RunAdbCommandToString($"pull \"/sdcard/Android/obb/{packagename}\" \"{settings.MainDir}\\{packagename}\"");
})
{
IsBackground = true
};
t2.Start();
while (t2.IsAlive)
{
await Task.Delay(100);
}
string HWID = SideloaderUtilities.UUID();
File.WriteAllText($"{settings.MainDir}\\{packagename}\\HWID.txt", HWID);
progressBar.Style = ProgressBarStyle.Continuous;
UploadGame game = new UploadGame
{
isUpdate = isupdate,
Pckgcommand = packagename,
Uploadgamename = GameName,
Uploadversion = installedVersionInt
};
gamesToUpload.Add(game);
}
private async Task initMirrors()
{
_ = Logger.Log("Looking for Additional Mirrors...");
int index = 0;
await Task.Run(() => remotesList.Invoke(() =>
{
index = remotesList.SelectedIndex;
remotesList.Items.Clear();
}));
// Retry logic for RCLONE availability
string[] mirrors = null;
int maxRetries = 10;
int retryCount = 0;
while (retryCount < maxRetries)
{
try
{
mirrors = await Task.Run(() => RCLONE.runRcloneCommand_DownloadConfig("listremotes").Output.Split('\n'));
break; // Success, exit retry loop
}
catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 2) // File not found
{
retryCount++;
Logger.Log($"RCLONE not ready yet, attempt {retryCount}/{maxRetries}. Waiting...", LogLevel.WARNING);
if (retryCount >= maxRetries)
{
Logger.Log("RCLONE failed to initialize after multiple attempts", LogLevel.ERROR);
_ = FlexibleMessageBox.Show(Program.form,
"RCLONE could not be initialized. Please check your internet connection and restart the application.\n\nIf the problem persists, try running as Administrator.",
"RCLONE Initialization Failed",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
// Fallback: Add only public mirror if available
if (hasPublicConfig)
{
await Task.Run(() => remotesList.Invoke(() =>
{
_ = remotesList.Items.Add("Public");
remotesList.SelectedIndex = 0;
}));
currentRemote = "Public";
UsingPublicConfig = true;
}
return;
}
// Wait before retry (exponential backoff: 500ms, 1s, 2s, 4s, ...)
await Task.Delay(Math.Min(500 * (int)Math.Pow(2, retryCount - 1), 5000));
}
catch (Exception ex)
{
Logger.Log($"Unexpected error initializing mirrors: {ex.Message}", LogLevel.ERROR);
_ = FlexibleMessageBox.Show(Program.form,
$"Error initializing mirrors: {ex.Message}",
"Mirror Initialization Error",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return;
}
}
if (mirrors == null)
{
Logger.Log("Failed to retrieve mirrors list", LogLevel.ERROR);
return;
}
_ = Logger.Log("Loaded following mirrors: ");
int itemsCount = 0;
if (hasPublicConfig)
{
_ = remotesList.Items.Add("Public");
itemsCount++;
}
foreach (string mirror in mirrors)
{
if (mirror.Contains("mirror"))
{
_ = Logger.Log(mirror.Remove(mirror.Length - 1));
await Task.Run(() => remotesList.Invoke(() =>
{
_ = remotesList.Items.Add(mirror.Remove(mirror.Length - 1).Replace("VRP-mirror", ""));
}));
itemsCount++;
}
}
if (itemsCount > 0)
{
await Task.Run(() => remotesList.Invoke(() =>
{
remotesList.SelectedIndex = 0;
string selectedRemote = remotesList.SelectedItem.ToString();
currentRemote = "";
if (selectedRemote != "Public")
{
currentRemote = "VRP-mirror";
}
currentRemote = string.Concat(currentRemote, selectedRemote);
}));
}
}
public static string processError = string.Empty;
public static string currentRemote = string.Empty;
private readonly string wrDelimiter = "-------";
private void deviceDropContainer_Click(object sender, EventArgs e)
{
ToggleContainer(deviceDropContainer, deviceDrop);
}
private void sideloadContainer_Click(object sender, EventArgs e)
{
ToggleContainer(sideloadContainer, sideloadDrop);
}
private void installedAppsMenuContainer_Click(object sender, EventArgs e)
{
ToggleContainer(installedAppsMenuContainer, installedAppsMenu);
}
private void backupDrop_Click(object sender, EventArgs e)
{
ToggleContainer(backupContainer, backupDrop);
}
private void otherDrop_Click(object sender, EventArgs e)
{
ToggleContainer(otherContainer, otherDrop);
}
private void ToggleContainer(Panel containerToToggle, Button dropButton)
{
// Collapse all other containers
CollapseAllContainers(containerToToggle);
// Toggle the clicked container
bool isExpanding = !containerToToggle.Visible;
containerToToggle.Visible = isExpanding;
}
private void CollapseAllContainers(Panel exceptThis = null)
{
var containers = new[]
{
deviceDropContainer,
sideloadContainer,
installedAppsMenuContainer,
backupContainer,
otherContainer
};
var buttons = new[]
{
deviceDrop,
sideloadDrop,
installedAppsMenu,
backupDrop,
otherDrop
};
for (int i = 0; i < containers.Length; i++)
{
if (containers[i] != exceptThis && containers[i].Visible)
{
containers[i].Visible = false;
}
}
}
private void settingsButton_Click(object sender, EventArgs e)
{
SettingsForm settingsForm = new SettingsForm();
settingsForm.Show(Program.form);
}
private void aboutBtn_Click(object sender, EventArgs e)
{
string about = $@"Version: {Updater.LocalVersion}
- Software orignally coded by rookie.wtf
- Thanks to the VRP Mod Staff, data team, and anyone else we missed!
- Thanks to VRP staff of the present and past: fenopy, Maxine, JarJarBlinkz
pmow, SytheZN, Roma/Rookie, Flow, Ivan, Kaladin, HarryEffinPotter, John, Sam Hoque
- Additional Thanks and Credits:
- -- rclone https://rclone.org/
- -- 7zip https://www.7-zip.org/
- -- ErikE: https://stackoverflow.com/users/57611/erike
- -- Serge Weinstock (SergeUtils)
- -- Mike Gold https://www.c-sharpcorner.com/members/mike-gold2
";
_ = FlexibleMessageBox.Show(Program.form, about);
}
private async void ADBWirelessEnable_Click(object sender, EventArgs e)
{
bool Manual;
DialogResult res = FlexibleMessageBox.Show(Program.form, "Do you want Rookie to find the IP or enter it manually\nYes = Automatic\nNo = Manual", "Automatic/Manual", MessageBoxButtons.YesNo);
Manual = res == DialogResult.No;
if (Manual)
{
adbCmd_CommandBox.Visible = true;
adbCmd_CommandBox.Clear();
adbCmd_Label.Visible = true;
adbCmd_Label.Text = "Enter your Quest IP Address";
adbCmd_background.Visible = true;
manualIP = true;
_ = adbCmd_CommandBox.Focus();
Program.form.changeTitle("Attempting manual connection...", false);
}
else
{
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "Make sure your Quest is plugged in VIA USB then press OK, if you need a moment press Cancel and come back when you're ready.", "Connect Quest now.", MessageBoxButtons.OKCancel);
if (dialogResult == DialogResult.Cancel)
{
return;
}
_ = ADB.RunAdbCommandToString("devices");
_ = ADB.RunAdbCommandToString("tcpip 5555");
_ = FlexibleMessageBox.Show(Program.form, "Press OK to get your Quest's local IP address.", "Obtain local IP address", MessageBoxButtons.OKCancel);
await Task.Delay(1000);
string input = ADB.RunAdbCommandToString("shell ip route").Output;
settings.WirelessADB = true;
settings.Save();
_ = new string[] { String.Empty };
string[] strArrayOne = input.Split(' ');
if (strArrayOne[0].Length > 7)
{
string IPaddr = strArrayOne[8];
string IPcmnd = "connect " + IPaddr + ":5555";
_ = FlexibleMessageBox.Show(Program.form, $"Your Quest's local IP address is: {IPaddr}\n\nPlease disconnect your Quest then wait 2 seconds.\nOnce it is disconnected hit OK", "", MessageBoxButtons.OK);
await Task.Delay(2000);
_ = ADB.RunAdbCommandToString(IPcmnd);
_ = await Program.form.CheckForDevice();
Program.form.changeTitlebarToDevice();
Program.form.showAvailableSpace();
settings.IPAddress = IPcmnd;
settings.Save();
try
{
File.WriteAllText(storedIpPath, IPcmnd);
}
catch (Exception ex) { Logger.Log($"Unable to write to StoredIP.txt due to {ex.Message}", LogLevel.ERROR); }
ADB.wirelessadbON = true;
_ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_available 1");
_ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_enabled 1");
}
else
{
_ = FlexibleMessageBox.Show(Program.form, "No device connected! Connect quest via USB and start again!");
}
}
}
private async void listApkButton_Click(object sender, EventArgs e)
{
string titleMessage = "Refreshing connected devices, installed apps and update list...";
changeTitle(titleMessage);
if (isLoading) { return; }
isLoading = true;
progressBar.Style = ProgressBarStyle.Marquee;
devicesbutton_Click(sender, e);
await initMirrors();
isLoading = false;
await refreshCurrentMirror(titleMessage);
}
private async Task refreshCurrentMirror(string titleMessage)
{
changeTitle(titleMessage);
if (isLoading) { return; }
isLoading = true;
progressBar.Style = ProgressBarStyle.Marquee;
Thread t1 = new Thread(() =>
{
if (!UsingPublicConfig)
{
SideloaderRCLONE.initGames(currentRemote);
}
listAppsBtn();
})
{
IsBackground = false
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
initListView(false);
isLoading = false;
changeTitle(" \n\n");
}
private static readonly HttpClient client = new HttpClient();
public static bool reset = false;
public static bool updatedConfig = false;
public static int steps = 0;
public static bool gamesAreDownloading = false;
private readonly BindingList<string> gamesQueueList = new BindingList<string>();
public static int quotaTries = 0;
public static bool timerticked = false;
public static bool skiponceafterremove = false;
public bool SwitchMirrors()
{
bool success = true;
try
{
quotaTries++;
remotesList.Invoke((MethodInvoker)delegate
{
if (quotaTries > remotesList.Items.Count)
{
ShowError_QuotaExceeded();
if (System.Windows.Forms.Application.MessageLoop)
{
// Process.GetCurrentProcess().Kill();
isOffline = true;
success = false;
return;
}
}
if (remotesList.SelectedIndex + 1 == remotesList.Items.Count)
{
reset = true;
for (int i = 0; i < steps; i++)
{
remotesList.SelectedIndex--;
}
}
if (reset)
{
remotesList.SelectedIndex--;
}
if (remotesList.Items.Count > remotesList.SelectedIndex && !reset)
{
remotesList.SelectedIndex++;
steps++;
}
});
}
catch
{
success = false;
}
return success;
}
private static void ShowError_QuotaExceeded()
{
string errorMessage =
$@"Unable to connect to Remote Server. Rookie is unable to connect to our Servers.
First time launching Rookie? Please relaunch and try again.
Please visit our Telegram (https://t.me/VRPirates) or Discord (https://discord.gg/tBKMZy7QDA) for Troubleshooting steps!
";
_ = FlexibleMessageBox.Show(Program.form, errorMessage, "Unable to connect to Remote Server");
}
public async void cleanupActiveDownloadStatus()
{
speedLabel.Text = String.Empty;
etaLabel.Text = String.Empty;
progressBar.Value = 0;
gamesQueueList.RemoveAt(0);
}
public void SetProgress(int progress)
{
progressBar.Value = progress;
}
public bool isinstalling = false;
public static bool isInDownloadExtract = false;
public static bool removedownloading = false;
public async void downloadInstallGameButton_Click(object sender, EventArgs e)
{
{
if (!settings.CustomDownloadDir)
{
settings.DownloadDir = Environment.CurrentDirectory.ToString();
}
bool obbsMismatch = false;
if (nodeviceonstart && !updatesNotified)
{
_ = await CheckForDevice();
changeTitlebarToDevice();
showAvailableSpace();
listAppsBtn();
}
progressBar.Style = ProgressBarStyle.Marquee;
if (gamesListView.SelectedItems.Count == 0)
{
progressBar.Style = ProgressBarStyle.Continuous;
changeTitle("You must select a game from the Game List!");
return;
}
string namebox = gamesListView.SelectedItems[0].ToString();
string nameboxtranslated = Sideloader.gameNameToSimpleName(namebox);
int count = 0;
string[] gamesToDownload;
if (gamesListView.SelectedItems.Count > 0)
{
count = gamesListView.SelectedItems.Count;
gamesToDownload = new string[count];
for (int i = 0; i < count; i++)
{
gamesToDownload[i] = gamesListView.SelectedItems[i].SubItems[SideloaderRCLONE.ReleaseNameIndex].Text;
}
}
else
{
return;
}
progressBar.Value = 0;
progressBar.Style = ProgressBarStyle.Continuous;
string game = gamesToDownload.Length == 1 ? $"\"{gamesToDownload[0]}\"" : "the selected games";
isinstalling = true;
//Add games to the queue
for (int i = 0; i < gamesToDownload.Length; i++)
{
gamesQueueList.Add(gamesToDownload[i]);
}
if (gamesAreDownloading)
{
return;
}
gamesAreDownloading = true;
//Do user json on firsttime
if (settings.UserJsonOnGameInstall)
{
Thread userJsonThread = new Thread(() => { changeTitle("Pushing user.json"); Sideloader.PushUserJsons(); })
{
IsBackground = true
};
userJsonThread.Start();
}
ProcessOutput output = new ProcessOutput("", "");
string gameName = "";
while (gamesQueueList.Count > 0)
{
gameName = gamesQueueList.ToArray()[0];
string packagename = Sideloader.gameNameToPackageName(gameName);
string versioncode = Sideloader.gameNameToVersionCode(gameName);
string dir = Path.GetDirectoryName(gameName);
string gameDirectory = Path.Combine(settings.DownloadDir, gameName);
string downloadDirectory = Path.Combine(settings.DownloadDir, gameName);
string path = gameDirectory;
string gameNameHash = string.Empty;
using (MD5 md5 = MD5.Create())
{
byte[] bytes = Encoding.UTF8.GetBytes(gameName + "\n");
byte[] hash = md5.ComputeHash(bytes);
StringBuilder sb = new StringBuilder();
foreach (byte b in hash)
{
_ = sb.Append(b.ToString("x2"));
}
gameNameHash = sb.ToString();
}
ProcessOutput gameDownloadOutput = new ProcessOutput("", "");
_ = Logger.Log($"Starting Game Download");
Thread t1;
string extraArgs = string.Empty;
if (settings.SingleThreadMode)
{
extraArgs = "--transfers 1 --multi-thread-streams 0";
}
string bandwidthLimit = string.Empty;
if (settings.BandwidthLimit > 0)
{
bandwidthLimit = $"--bwlimit={settings.BandwidthLimit}M";
}
if (UsingPublicConfig)
{
bool doDownload = true;
bool skipRedownload = false;
if (settings.UseDownloadedFiles == true)
{
skipRedownload = true;
}
if (Directory.Exists(gameDirectory))
{
if (skipRedownload == true)
{
if (Directory.Exists($"{settings.DownloadDir}\\{gameName}"))
{
doDownload = false;
}
}
else
{
DialogResult res = FlexibleMessageBox.Show(Program.form,
$"{gameName} exists in destination directory.\r\nWould you like to overwrite it?",
"Download again?", MessageBoxButtons.YesNo);
doDownload = res == DialogResult.Yes;
}
if (doDownload)
{
// only delete after extraction; allows for resume if the fetch fails midway.
if (Directory.Exists($"{settings.DownloadDir}\\{gameName}"))
{
Directory.Delete($"{settings.DownloadDir}\\{gameName}", true);
}
}
}
if (doDownload)
{
downloadDirectory = $"{settings.DownloadDir}\\{gameNameHash}";
_ = Logger.Log($"rclone copy \"Public:{SideloaderRCLONE.RcloneGamesFolder}/{gameName}\"");
t1 = new Thread(() =>
{
string rclonecommand =
$"copy \":http:/{gameNameHash}/\" \"{downloadDirectory}\" {extraArgs} --progress --rc {bandwidthLimit}";
gameDownloadOutput = RCLONE.runRcloneCommand_PublicConfig(rclonecommand);
});
Utilities.Metrics.CountDownload(packagename, versioncode);
}
else
{
t1 = new Thread(() => { gameDownloadOutput = new ProcessOutput("Download skipped."); });
}
}
else
{
_ = Directory.CreateDirectory(gameDirectory);
downloadDirectory = $"{SideloaderRCLONE.RcloneGamesFolder}/{gameName}";
_ = Logger.Log($"rclone copy \"{currentRemote}:{downloadDirectory}\"");
t1 = new Thread(() =>
{
gameDownloadOutput = RCLONE.runRcloneCommand_DownloadConfig($"copy \"{currentRemote}:{downloadDirectory}\" \"{settings.DownloadDir}\\{gameName}\" {extraArgs} --progress --rc --retries 2 --low-level-retries 1 --check-first {bandwidthLimit}");
});
Utilities.Metrics.CountDownload(packagename, versioncode);
}
if (Directory.Exists(downloadDirectory))
{
string[] partialFiles = Directory.GetFiles($"{downloadDirectory}", "*.partial");
foreach (string file in partialFiles)
{
File.Delete(file);
_ = Logger.Log($"Deleted partial file: {file}");
}
}
t1.IsBackground = true;
t1.Start();
changeTitle("Downloading game " + gameName, false);
speedLabel.Text = "Starting download...";
etaLabel.Text = "Please wait...";
//Download
while (t1.IsAlive)
{
try
{
HttpResponseMessage response = await client.PostAsync("http://127.0.0.1:5572/core/stats", null);
string foo = await response.Content.ReadAsStringAsync();
//Debug.WriteLine("RESP CONTENT " + foo);
dynamic results = JsonConvert.DeserializeObject<dynamic>(foo);
if (results["transferring"] != null)
{
double totalSize = 0;
double downloadedSize = 0;
long fileCount = 0;
long transfersComplete = 0;
long totalChecks = 0;
long globalEta = 0;
float speed = 0;
float downloadSpeed = 0;
double estimatedFileCount = 0;
totalSize = results["totalBytes"];
downloadedSize = results["bytes"];
fileCount = results["totalTransfers"];
totalChecks = results["totalChecks"];
transfersComplete = results["transfers"];
globalEta = results["eta"];
speed = results["speed"];
estimatedFileCount = Math.Ceiling(totalSize / 524288000); // maximum part size
if (totalChecks > fileCount)
{
fileCount = totalChecks;
}
if (estimatedFileCount > fileCount)
{
fileCount = (long)estimatedFileCount;
}
downloadSpeed = speed / 1000000;
totalSize /= 1000000;
downloadedSize /= 1000000;
// Logger.Log("Files: " + transfersComplete.ToString() + "/" + fileCount.ToString() + " (" + Convert.ToInt32((downloadedSize / totalSize) * 100).ToString() + "% Complete)");
// Logger.Log("Downloaded: " + downloadedSize.ToString() + " of " + totalSize.ToString());
progressBar.Style = ProgressBarStyle.Continuous;
progressBar.Value = Convert.ToInt32((downloadedSize / totalSize) * 100);
TimeSpan time = TimeSpan.FromSeconds(globalEta);
etaLabel.Text = etaLabel.Text = "ETA: " + time.ToString(@"hh\:mm\:ss") + " left";
speedLabel.Text = "DLS: " + transfersComplete.ToString() + "/" + fileCount.ToString() + " files - " + string.Format("{0:0.00}", downloadSpeed) + " MB/s";
}
}
catch
{
}
await Task.Delay(100);
}
if (removedownloading)
{
changeTitle("Keep game files?", false);
try
{
cleanupActiveDownloadStatus();
DialogResult res = FlexibleMessageBox.Show(
$"{gameName} already has some downloaded files, do you want to delete them?\n\nClick NO to keep the files if you wish to resume your download later.",
"Delete Temporary Files?", MessageBoxButtons.YesNo);
if (res == DialogResult.Yes)
{
changeTitle("Deleting game files", false);
if (UsingPublicConfig)
{
if (Directory.Exists($"{settings.DownloadDir}\\{gameNameHash}"))
{
Directory.Delete($"{settings.DownloadDir}\\{gameNameHash}", true);
}
if (Directory.Exists($"{settings.DownloadDir}\\{gameName}"))
{
Directory.Delete($"{settings.DownloadDir}\\{gameName}", true);
}
}
else
{
Directory.Delete(settings.DownloadDir + "\\" + gameName, true);
}
}
}
catch (Exception ex)
{
_ = FlexibleMessageBox.Show(Program.form, $"Error deleting game files: {ex.Message}");
}
changeTitle("");
break;
}
{
//Quota Errors
bool isinstalltxt = false;
string installTxtPath = null;
bool quotaError = false;
bool otherError = false;
if (gameDownloadOutput.Error.Length > 0 && !isOffline)
{
string err = gameDownloadOutput.Error.ToLower();
err += gameDownloadOutput.Output.ToLower();
if ((err.Contains("quota") && err.Contains("exceeded")) || err.Contains("directory not found"))
{
quotaError = true;
SwitchMirrors();
cleanupActiveDownloadStatus();
}
else if (!gameDownloadOutput.Error.Contains("Serving remote control on http://127.0.0.1:5572/"))
{
otherError = true;
//Remove current game
cleanupActiveDownloadStatus();
_ = FlexibleMessageBox.Show(Program.form, $"Rclone error: {gameDownloadOutput.Error}");
output += new ProcessOutput("", "Download Failed");
}
}
if (UsingPublicConfig && otherError == false && gameDownloadOutput.Output != "Download skipped.")
{
Thread extractionThread = new Thread(() =>
{
Invoke(new Action(() =>
{
speedLabel.Text = "Extracting..."; etaLabel.Text = "Please wait...";
progressBar.Style = ProgressBarStyle.Continuous;
progressBar.Value = 0;
isInDownloadExtract = true;
}));
try
{
changeTitle("Extracting " + gameName, false);
Zip.ExtractFile($"{settings.DownloadDir}\\{gameNameHash}\\{gameNameHash}.7z.001", $"{settings.DownloadDir}", PublicConfigFile.Password);
Program.form.changeTitle("");
}
catch (ExtractionException ex)
{
Invoke(new Action(() =>
{
cleanupActiveDownloadStatus();
}));
otherError = true;
this.Invoke(() => _ = FlexibleMessageBox.Show(Program.form, $"7zip error: {ex.Message}"));
output += new ProcessOutput("", "Extract Failed");
}
})
{
IsBackground = true
};
extractionThread.Start();
while (extractionThread.IsAlive)
{
await Task.Delay(100);
}
if (Directory.Exists($"{settings.DownloadDir}\\{gameNameHash}"))
{
Directory.Delete($"{settings.DownloadDir}\\{gameNameHash}", true);
}
}
if (quotaError == false && otherError == false)
{
ADB.DeviceID = GetDeviceID();
quotaTries = 0;
progressBar.Value = 0;
progressBar.Style = ProgressBarStyle.Continuous;
changeTitle("Installing game apk " + gameName, false);
etaLabel.Text = "ETA: Wait for install...";
speedLabel.Text = "DLS: Finished";
if (File.Exists(Path.Combine(settings.DownloadDir, gameName, "install.txt")))
{
isinstalltxt = true;
installTxtPath = Path.Combine(settings.DownloadDir, gameName, "install.txt");
}
else if (File.Exists(Path.Combine(settings.DownloadDir, gameName, "Install.txt")))
{
isinstalltxt = true;
installTxtPath = Path.Combine(settings.DownloadDir, gameName, "Install.txt");
}
string[] files = Directory.GetFiles(settings.DownloadDir + "\\" + gameName);
Debug.WriteLine("Game Folder is: " + settings.DownloadDir + "\\" + gameName);
Debug.WriteLine("FILES IN GAME FOLDER: ");
if (isinstalltxt)
{
if (!settings.NodeviceMode || !nodeviceonstart && DeviceConnected)
{
Thread installtxtThread = new Thread(() =>
{
output += Sideloader.RunADBCommandsFromFile(installTxtPath);
changeTitle(" \n\n");
});
installtxtThread.Start();
while (installtxtThread.IsAlive)
{
await Task.Delay(100);
}
}
else
{
output.Output = "\n--- SIDELOADING DISABLED ---\nAll tasks finished.";
}
}
else
{
if (!settings.NodeviceMode || !nodeviceonstart && DeviceConnected)
{
// Find the APK file to install
string apkFile = files.FirstOrDefault(file => Path.GetExtension(file) == ".apk");
if (apkFile != null)
{
CurrAPK = apkFile;
CurrPCKG = packagename;
System.Windows.Forms.Timer t = new System.Windows.Forms.Timer
{
Interval = 150000 // 150 seconds to fail
};
t.Tick += new EventHandler(timer_Tick4);
t.Start();
Thread apkThread = new Thread(() =>
{
Program.form.changeTitle($"Sideloading apk...");
output += ADB.Sideload(apkFile, packagename);
})
{
IsBackground = true
};
apkThread.Start();
while (apkThread.IsAlive)
{
await Task.Delay(100);
}
t.Stop();
Debug.WriteLine(wrDelimiter);
if (Directory.Exists($"{settings.DownloadDir}\\{gameName}\\{packagename}"))
{
deleteOBB(packagename);
Thread obbThread = new Thread(() =>
{
changeTitle($"Copying {packagename} obb to device...");
ADB.RunAdbCommandToString($"shell mkdir \"/sdcard/Android/obb/{packagename}\"");
output += ADB.RunAdbCommandToString($"push \"{settings.DownloadDir}\\{gameName}\\{packagename}\" \"/sdcard/Android/obb\"");
Program.form.changeTitle("");
})
{
IsBackground = true
};
obbThread.Start();
while (obbThread.IsAlive)
{
await Task.Delay(100);
}
if (!nodeviceonstart | DeviceConnected)
{
if (!output.Output.Contains("offline"))
{
try
{
obbsMismatch = await compareOBBSizes(packagename, gameName, output);
}
catch (Exception ex) { _ = FlexibleMessageBox.Show(Program.form, $"Error comparing OBB sizes: {ex.Message}"); }
}
}
}
}
}
else
{
output.Output = "\n--- SIDELOADING DISABLED ---\nAll tasks finished.\n";
}
changeTitle($"Installation of {gameName} completed.");
}
if (settings.DeleteAllAfterInstall)
{
changeTitle("Deleting game files", false);
try { Directory.Delete(settings.DownloadDir + "\\" + gameName, true); } catch (Exception ex) { _ = FlexibleMessageBox.Show(Program.form, $"Error deleting game files: {ex.Message}"); }
}
//Remove current game
cleanupActiveDownloadStatus();
}
}
}
if (removedownloading)
{
removedownloading = false;
gamesAreDownloading = false;
isinstalling = false;
return;
}
if (!obbsMismatch)
{
changeTitle("Refreshing games list, please wait... \n");
showAvailableSpace();
listAppsBtn();
if (!updateAvailableClicked && !upToDate_Clicked && !NeedsDonation_Clicked && !settings.NodeviceMode && !gamesQueueList.Any())
{
initListView(false);
}
if (settings.EnableMessageBoxes)
{
ShowPrcOutput(output);
}
progressBar.Style = ProgressBarStyle.Continuous;
etaLabel.Text = "ETA: Finished Queue";
speedLabel.Text = "DLS: Finished Queue";
ProgressText.Text = "";
gamesAreDownloading = false;
isinstalling = false;
changeTitle(" \n\n");
}
}
}
private void deleteOBB(string packagename)
{
changeTitle("Deleting old OBB Folder...");
Logger.Log("Attempting to delete old OBB Folder");
ADB.RunAdbCommandToString($"shell rm -rf \"/sdcard/Android/obb/{packagename}\"");
}
private const string OBBFolderPath = "/sdcard/Android/obb/";
// Logic to compare OBB folders.
private async Task<bool> compareOBBSizes(string packageName, string gameName, ProcessOutput output)
{
string localFolderPath = Path.Combine(settings.DownloadDir, gameName, packageName);
if (!Directory.Exists(localFolderPath))
{
return false;
}
try
{
changeTitle("Comparing obbs...");
Logger.Log("Comparing OBBs");
DirectoryInfo localFolder = new DirectoryInfo(localFolderPath);
long totalLocalFolderSize = localFolderSize(localFolder) / (1024 * 1024);
string remoteFolderSizeResult = ADB.RunAdbCommandToString($"shell du -m \"{OBBFolderPath}{packageName}\"").Output;
string cleanedRemoteFolderSize = cleanRemoteFolderSize(remoteFolderSizeResult);
int localObbSize = (int)totalLocalFolderSize;
int remoteObbSize = Convert.ToInt32(cleanedRemoteFolderSize);
Logger.Log($"Total local folder size in bytes: {totalLocalFolderSize} Remote Size: {cleanedRemoteFolderSize}");
if (remoteObbSize < localObbSize)
{
return await handleObbSizeMismatchAsync(packageName, gameName, output);
}
return false;
}
catch (FormatException ex)
{
_ = FlexibleMessageBox.Show(Program.form, "The OBB Folder on the Quest seems to not exist or be empty\nPlease redownload the game or sideload the obb manually.", "OBB Size Undetectable!", MessageBoxButtons.OK);
Logger.Log($"Unable to compare obbs with the exception: {ex.Message}", LogLevel.ERROR);
FlexibleMessageBox.Show($"Error comparing OBB sizes: {ex.Message}");
return false;
}
catch (Exception ex)
{
Logger.Log($"Unexpected error occurred while comparing OBBs: {ex.Message}", LogLevel.ERROR);
FlexibleMessageBox.Show(Program.form, $"Unexpected error comparing OBB sizes: {ex.Message}");
return false;
}
}
private string cleanRemoteFolderSize(string rawSize)
{
string replaced = Regex.Replace(rawSize, "[^c]*$", "");
return Regex.Replace(replaced, "[^0-9]", "");
}
// Logic to handle mismatches after comparison.
private async Task<bool> handleObbSizeMismatchAsync(string packageName, string gameName, ProcessOutput output)
{
var dialogResult = MessageBox.Show(Program.form, "Warning! It seems like the OBB wasn't pushed correctly, this means that the game may not launch correctly.\n Do you want to retry the push?", "OBB Size Mismatch!", MessageBoxButtons.YesNo);
if (dialogResult != DialogResult.Yes)
{
await refreshGamesListAsync(output);
return true;
}
changeTitle("Retrying push");
string obbFolderPath = Path.Combine(settings.DownloadDir, gameName, packageName);
if (!Directory.Exists(obbFolderPath))
{
return false;
}
await Task.Run(() =>
{
changeTitle($"Copying {packageName} obb to device...");
output += ADB.RunAdbCommandToString($"push \"{obbFolderPath}\" \"{OBBFolderPath}\"");
Program.form.changeTitle("");
});
return await compareOBBSizes(packageName, gameName, output);
}
private async Task refreshGamesListAsync(ProcessOutput output)
{
changeTitle("Refreshing games list, please wait...");
showAvailableSpace();
listAppsBtn();
if (!updateAvailableClicked && !upToDate_Clicked && !NeedsDonation_Clicked && !settings.NodeviceMode && !gamesQueueList.Any())
{
initListView(false);
}
if (settings.EnableMessageBoxes)
{
ShowPrcOutput(output);
}
progressBar.Style = ProgressBarStyle.Continuous;
etaLabel.Text = "ETA: Finished Queue";
speedLabel.Text = "DLS: Finished Queue";
ProgressText.Text = string.Empty;
gamesAreDownloading = false;
isinstalling = false;
changeTitle(" \n\n");
}
static long localFolderSize(DirectoryInfo localFolder)
{
long totalLocalFolderSize = 0;
// Get all files into the directory
FileInfo[] allFiles = localFolder.GetFiles();
// Loop through every file and get size of it
foreach (FileInfo file in allFiles)
{
totalLocalFolderSize += file.Length;
}
// Find all subdirectories
DirectoryInfo[] subFolders = localFolder.GetDirectories();
// Loop through every subdirectory and get size of each
foreach (DirectoryInfo dir in subFolders)
{
totalLocalFolderSize += localFolderSize(dir);
}
// Return the total size of folder
return totalLocalFolderSize;
}
private void timer_Tick4(object sender, EventArgs e)
{
_ = new ProcessOutput("", "");
if (!timerticked)
{
timerticked = true;
bool isinstalled = false;
if (settings.InstalledApps.Contains(CurrPCKG))
{
isinstalled = true;
}
if (isinstalled)
{
if (!settings.AutoReinstall)
{
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "In place upgrade has failed." +
"\n\nThis means the app must be uninstalled first before updating.\nRookie can attempt to " +
"do this while retaining your savedata.\nWhile the vast majority of games can be backed up there " +
"are some exceptions\n(we don't know which apps can't be backed up as there is no list online)\n\nDo you want " +
"Rookie to uninstall and reinstall the app automatically?", "In place upgrade failed", MessageBoxButtons.OKCancel);
if (dialogResult == DialogResult.Cancel)
{
return;
}
}
changeTitle("Performing reinstall, please wait...");
_ = ADB.RunAdbCommandToString("kill-server");
_ = ADB.RunAdbCommandToString("devices");
_ = ADB.RunAdbCommandToString($"pull /sdcard/Android/data/{CurrPCKG} \"{Environment.CurrentDirectory}\"");
_ = Sideloader.UninstallGame(CurrPCKG);
changeTitle("Reinstalling Game");
_ = ADB.RunAdbCommandToString($"install -g \"{CurrAPK}\"");
_ = ADB.RunAdbCommandToString($"push \"{Environment.CurrentDirectory}\\{CurrPCKG}\" /sdcard/Android/data/");
timerticked = false;
if (Directory.Exists(Path.Combine(Environment.CurrentDirectory, CurrPCKG)))
{
Directory.Delete(Path.Combine(Environment.CurrentDirectory, CurrPCKG), true);
}
changeTitle(" \n\n");
return;
}
else
{
DialogResult dialogResult2 = FlexibleMessageBox.Show(Program.form, "This install is taking an unusual amount of time, you can keep waiting or cancel the install.\n" +
"Would you like to cancel the installation?", "Cancel install?", MessageBoxButtons.YesNo);
if (dialogResult2 == DialogResult.Yes)
{
changeTitle("Stopping Install...");
_ = ADB.RunAdbCommandToString("kill-server");
_ = ADB.RunAdbCommandToString("devices");
}
else
{
timerticked = false;
return;
}
}
}
}
private async void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (isinstalling)
{
DialogResult res1 = FlexibleMessageBox.Show(Program.form, "There are downloads and/or installations in progress,\nif you exit now you'll have to start the entire process over again.\nAre you sure you want to exit?", "Still downloading/installing.",
MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2);
if (res1 != DialogResult.Yes)
{
e.Cancel = true;
return;
}
else
{
RCLONE.killRclone();
}
}
else if (isuploading)
{
DialogResult res = FlexibleMessageBox.Show(Program.form, "There is an upload still in progress, if you exit now\nyou'll have to start the entire process over again.\nAre you sure you want to exit?", "Still uploading.",
MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2);
if (res != DialogResult.Yes)
{
e.Cancel = true;
return;
}
else
{
RCLONE.killRclone();
_ = ADB.RunAdbCommandToString("kill-server");
}
}
else
{
RCLONE.killRclone();
_ = ADB.RunAdbCommandToString("kill-server");
}
}
private async void ADBWirelessDisable_Click(object sender, EventArgs e)
{
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "Are you sure you want to delete your saved Quest IP address/command?", "Remove saved IP address?", MessageBoxButtons.YesNo);
if (dialogResult == DialogResult.No)
{
_ = FlexibleMessageBox.Show(Program.form, "Saved IP data reset cancelled.");
return;
}
else
{
ADB.wirelessadbON = false;
_ = FlexibleMessageBox.Show(Program.form, "Make sure your device is not connected to USB and press OK.");
_ = ADB.RunAdbCommandToString("devices");
_ = ADB.RunAdbCommandToString("shell USB");
await Task.Delay(2000);
_ = ADB.RunAdbCommandToString("disconnect");
await Task.Delay(2000);
_ = ADB.RunAdbCommandToString("kill-server");
await Task.Delay(2000);
_ = ADB.RunAdbCommandToString("start-server");
settings.IPAddress = String.Empty;
settings.Save();
_ = Program.form.GetDeviceID();
Program.form.changeTitlebarToDevice();
_ = FlexibleMessageBox.Show(Program.form, "Relaunch Rookie to complete the process and switch back to USB adb.");
if (File.Exists(storedIpPath))
{
File.Delete(storedIpPath);
}
}
}
private void gamesQueListBox_MouseClick(object sender, MouseEventArgs e)
{
if (gamesQueListBox.SelectedIndex == 0 && gamesQueueList.Count == 1)
{
removedownloading = true;
RCLONE.killRclone();
}
if (gamesQueListBox.SelectedIndex != -1 && gamesQueListBox.SelectedIndex != 0)
{
_ = gamesQueueList.Remove(gamesQueListBox.SelectedItem.ToString());
}
}
private void devicesComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
showAvailableSpace();
}
private async void remotesList_SelectedIndexChanged(object sender, EventArgs e)
{
if (remotesList.SelectedItem != null)
{
string selectedRemote = remotesList.SelectedItem.ToString();
if (selectedRemote == "Public")
{
UsingPublicConfig = true;
}
else
{
UsingPublicConfig = false;
remotesList.Invoke(() => { currentRemote = "VRP-mirror" + selectedRemote; });
}
await refreshCurrentMirror("Refreshing App List...");
}
}
private void QuestOptionsButton_Click(object sender, EventArgs e)
{
QuestForm Form = new QuestForm();
Form.Show(Program.form);
}
private void listView1_ColumnClick(object sender, ColumnClickEventArgs e)
{
// Determine if clicked column is already the column that is being sorted.
if (e.Column == lvwColumnSorter.SortColumn)
{
// Reverse the current sort direction for this column.
lvwColumnSorter.Order = lvwColumnSorter.Order == SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending;
}
else
{
lvwColumnSorter.SortColumn = e.Column;
lvwColumnSorter.Order = e.Column == 4 ? SortOrder.Descending : SortOrder.Ascending;
}
// Perform the sort with these new sort options.
gamesListView.Sort();
}
private void CheckEnter(object sender, System.Windows.Forms.KeyPressEventArgs e)
{
if (e.KeyChar == (char)Keys.Enter)
{
if (searchTextBox.Visible)
{
if (gamesListView.SelectedItems.Count > 0)
{
downloadInstallGameButton_Click(sender, e);
}
}
searchTextBox.Visible = false;
adbCmd_background.Visible = false;
if (adbCmd_CommandBox.Visible)
{
changeTitle($"Entered command: ADB {adbCmd_CommandBox.Text}");
_ = ADB.RunAdbCommandToString(adbCmd_CommandBox.Text);
changeTitle(" \n\n");
}
adbCmd_CommandBox.Visible = false;
adbCmd_Label.Visible = false;
adbCmd_background.Visible = false;
}
if (e.KeyChar == (char)Keys.Escape)
{
searchTextBox.Visible = false;
adbCmd_background.Visible = false;
adbCmd_CommandBox.Visible = false;
adbCmd_btnToggleUpdates.Visible = false;
adbCmd_btnSend.Visible = false;
adbCmd_Label.Visible = false;
adbCmd_background.Visible = false;
}
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == (Keys.Control | Keys.F))
{
// Show search box.
searchTextBox.Clear();
searchTextBox.Visible = true;
adbCmd_background.Visible = true;
_ = searchTextBox.Focus();
}
if (keyData == (Keys.Control | Keys.L))
{
if (loaded)
{
StringBuilder copyGamesListView = new StringBuilder();
foreach (ListViewItem item in gamesListView.Items)
{
// Assuming the game name is in the first column (subitem index 0)
copyGamesListView.Append(item.SubItems[0].Text).Append("\n");
}
Clipboard.SetText(copyGamesListView.ToString());
_ = MessageBox.Show("Entire game list copied as a paragraph to clipboard!\nPress CTRL+V to paste it anywhere!");
}
}
if (keyData == (Keys.Alt | Keys.L))
{
if (loaded)
{
StringBuilder copyGamesListView = new StringBuilder();
foreach (ListViewItem item in gamesListView.Items)
{
// Assuming the game name is in the first column (subitem index 0)
copyGamesListView.Append(item.SubItems[0].Text).Append(", ");
}
// Remove the last ", " if there's any content in rookienamelist2
if (copyGamesListView.Length > 2)
{
copyGamesListView.Length -= 2;
}
Clipboard.SetText(copyGamesListView.ToString());
_ = MessageBox.Show("Entire game list copied as a paragraph to clipboard!\nPress CTRL+V to paste it anywhere!");
}
}
if (keyData == (Keys.Control | Keys.H))
{
string HWID = SideloaderUtilities.UUID();
Clipboard.SetText(HWID);
_ = FlexibleMessageBox.Show(Program.form, $"Your unique HWID is:\n\n{HWID}\n\nThis has been automatically copied to your clipboard. Press CTRL+V in a message to send it.");
}
if (keyData == (Keys.Control | Keys.R))
{
adbCmd_CommandBox.Visible = true;
adbCmd_btnToggleUpdates.Visible = true;
adbCmd_btnSend.Visible = true;
adbCmd_CommandBox.Clear();
adbCmd_Label.Visible = true;
adbCmd_background.Visible = true;
_ = adbCmd_CommandBox.Focus();
}
if (keyData == (Keys.Control | Keys.F4))
{
try
{
// Relaunch the program using Sideloader Launcher
_ = Process.Start(Application.StartupPath + "\\Sideloader Launcher.exe");
Process.GetCurrentProcess().Kill();
}
catch
{ }
}
if (keyData == Keys.F3)
{
if (Application.OpenForms.OfType<QuestForm>().Count() == 0)
{
QuestForm Form = new QuestForm();
Form.Show(Program.form);
}
}
if (keyData == Keys.F4)
{
if (Application.OpenForms.OfType<SettingsForm>().Count() == 0)
{
SettingsForm Form = new SettingsForm();
Form.Show(Program.form);
}
}
if (keyData == Keys.F5)
{
_ = GetDeviceID();
_ = FlexibleMessageBox.Show(Program.form, "If your device is not Connected, hit reconnect first or it won't work!\nNOTE: THIS MAY TAKE UP TO 60 SECONDS.\nThere will be a Popup text window with all updates available when it is done!", "Is device connected?", MessageBoxButtons.OKCancel);
listAppsBtn();
initListView(false);
}
bool dialogIsUp = false;
if (keyData == Keys.F1 && !dialogIsUp)
{
_ = FlexibleMessageBox.Show(Program.form, "Shortcuts:\nF1 -------- Shortcuts List\nF3 -------- Quest Options\nF4 -------- Rookie Settings\nF5 -------- Refresh Gameslist\n\nCTRL+R - Run custom ADB command.\nCTRL+L - Copy entire list of Game Names to clipboard seperated by new lines.\nALT+L - Copy entire list of Game Names to clipboard seperated by commas(in a paragraph).CTRL+P - Copy packagename to clipboard on game select.\nCTRL + F4 - Instantly relaunch Rookie Sideloader.");
}
if (keyData == (Keys.Control | Keys.P))
{
DialogResult dialogResult = FlexibleMessageBox.Show(Program.form, "Do you wish to copy Package Name of games selected from list to clipboard?", "Copy package to clipboard?", MessageBoxButtons.YesNo);
if (dialogResult == DialogResult.Yes)
{
settings.PackageNameToCB = true;
settings.Save();
}
if (dialogResult == DialogResult.No)
{
settings.PackageNameToCB = false;
settings.Save();
}
}
return base.ProcessCmdKey(ref msg, keyData);
}
private async void searchTextBox_TextChanged(object sender, EventArgs e)
{
_debounceTimer.Stop();
_debounceTimer.Start();
}
private async Task RunSearch()
{
_debounceTimer.Stop();
// Cancel any ongoing searches
_cts?.Cancel();
string searchTerm = searchTextBox.Text;
// Ignore placeholder text
if (searchTerm == "Search..." || string.IsNullOrWhiteSpace(searchTerm))
{
RestoreFullList();
return;
}
_cts = new CancellationTokenSource();
var token = _cts.Token;
try
{
// Perform search using index for faster lookups
var matches = await Task.Run(() =>
{
if (token.IsCancellationRequested) return new List<ListViewItem>();
var results = new HashSet<ListViewItem>(); // Avoid duplicates
// Try exact match first using index
if (_searchIndex != null && _searchIndex.TryGetValue(searchTerm, out var exactMatches))
{
foreach (var match in exactMatches)
{
if (token.IsCancellationRequested) return new List<ListViewItem>();
results.Add(match);
}
}
// Then do partial matches using index
if (_searchIndex != null)
{
foreach (var kvp in _searchIndex)
{
if (token.IsCancellationRequested) return new List<ListViewItem>();
if (kvp.Key.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0)
{
foreach (var item in kvp.Value)
{
results.Add(item);
}
}
}
}
else
{
// Fallback to linear search if index not built yet
foreach (var item in _allItems)
{
if (token.IsCancellationRequested) return new List<ListViewItem>();
if (item.Text.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0 ||
(item.SubItems.Count > 1 && item.SubItems[1].Text.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0))
{
results.Add(item);
}
}
}
return results.ToList();
}, token);
// Check if cancelled before updating UI
if (token.IsCancellationRequested) return;
// Update UI on main thread
gamesListView.BeginUpdate();
try
{
gamesListView.Items.Clear();
if (matches.Count > 0)
{
gamesListView.Items.AddRange(matches.ToArray());
}
}
finally
{
gamesListView.EndUpdate();
}
}
catch (OperationCanceledException)
{
// Search was cancelled - this is expected behavior
}
catch (Exception ex)
{
Logger.Log($"Error during search: {ex.Message}", LogLevel.ERROR);
}
}
private void ADBcommandbox_Enter(object sender, EventArgs e)
{
_ = adbCmd_CommandBox.Focus();
}
private bool fullScreen = false;
[DefaultValue(false)]
public bool FullScreen
{
get { return fullScreen; }
set
{
fullScreen = value;
if (value)
{
MainForm.ActiveForm.FormBorderStyle = FormBorderStyle.None;
webView21.Anchor = (AnchorStyles.Top | AnchorStyles.Left);
webView21.Location = new System.Drawing.Point(0, 0);
webView21.Size = MainForm.ActiveForm.Size;
}
else
{
MainForm.ActiveForm.FormBorderStyle = FormBorderStyle.Sizable;
webView21.Anchor = (AnchorStyles.Left | AnchorStyles.Bottom);
webView21.Location = gamesPictureBox.Location;
webView21.Size = new System.Drawing.Size(374, 214);
}
}
}
static string ExtractVideoId(string html)
{
// We want the first strict 11-char YouTube video ID after /watch?v=
var m = Regex.Match(html, @"\/watch\?v=([A-Za-z0-9_\-]{11})");
return m.Success ? m.Groups[1].Value : string.Empty;
}
private async Task CreateEnvironment()
{
// Fast path: already initialized
if (webView21.CoreWebView2 != null) return;
// Create environment
var userDataFolder = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "RSL");
var env = await CoreWebView2Environment.CreateAsync(userDataFolder: userDataFolder);
await webView21.EnsureCoreWebView2Async(env);
// Map local folder to a trusted origin (https://app.local)
var webroot = Path.Combine(Environment.CurrentDirectory, "webroot");
Directory.CreateDirectory(webroot);
webView21.CoreWebView2.SetVirtualHostNameToFolderMapping(
"app.local", webroot, CoreWebView2HostResourceAccessKind.Allow);
// Minimal settings required for the player page
var s = webView21.CoreWebView2.Settings;
s.IsScriptEnabled = true; // allow IFrame API
s.IsWebMessageEnabled = true; // allow PostWebMessageAsString from host
}
private void InitializeTrailerPlayer()
{
if (_trailerPlayerInitialized) return;
string webroot = Path.Combine(Environment.CurrentDirectory, "webroot");
Directory.CreateDirectory(webroot);
string playerHtml = Path.Combine(webroot, "player.html");
// Lightweight HTML with YouTube IFrame API and WebView2 message bridge
var html = @"<!doctype html>
<html>
<head>
<meta charset=""utf-8"">
<meta name=""viewport"" content=""width=device-width,initial-scale=1""/>
<title>Trailer Player</title>
<style>
html,body { margin:0; background:#000; height:100%; overflow:hidden; }
#player { width:100vw; height:100vh; }
</style>
<script src=""https://www.youtube.com/iframe_api""></script>
<script>
let player;
let pendingId = null;
// Youtube trailer player
function onYouTubeIframeAPIReady() {
// Initialize without video
player = new YT.Player('player', {
playerVars: {
autoplay: 0, // prevent autoplay at initialization
mute: 1, // keep muted so subsequent loads can play instantly if desired
playsinline: 1,
rel: 0,
modestbranding: 1
},
events: {
'onReady': () => {
// Do nothing by default. Only play after we receive a video id message.
if (pendingId) {
// If we received a message before ready, load now.
player.loadVideoById(pendingId);
pendingId = null;
}
}
}
});
}
// WebView2 message hook: app posts the 11-char video id.
(function(){
if (window.chrome && window.chrome.webview) {
window.chrome.webview.addEventListener('message', e => {
const id = (e && e.data) ? String(e.data).trim() : '';
if (!/^[A-Za-z0-9_\-]{11}$/.test(id)) return;
if (player && player.loadVideoById) {
player.loadVideoById(id);
} else {
pendingId = id;
}
});
}
})();
</script>
</head>
<body>
<div id=""player""></div>
</body>
</html>";
File.WriteAllText(playerHtml, html, Encoding.UTF8);
_trailerPlayerInitialized = true;
}
// Ensure environment + initial navigation
private async Task EnsureTrailerEnvironmentAsync()
{
if (webView21.CoreWebView2 == null)
{
await CreateEnvironment();
}
InitializeTrailerPlayer();
if (!_trailerHtmlLoaded)
{
webView21.CoreWebView2.NavigationCompleted += (s, e) =>
{
_trailerHtmlLoaded = true;
};
webView21.CoreWebView2.Navigate("https://app.local/player.html");
}
}
private async Task ShowVideoAsync(string videoId)
{
if (string.IsNullOrEmpty(videoId)) return;
await EnsureTrailerEnvironmentAsync();
// If first load still in progress, small retry loop
int tries = 0;
while (!_trailerHtmlLoaded && tries < 50)
{
await Task.Delay(50);
tries++;
}
// Post the raw ID; page builds final URL
webView21.CoreWebView2.PostWebMessageAsString(videoId);
}
private async Task<string> ResolveVideoIdAsync(string gameName)
{
if (string.IsNullOrWhiteSpace(gameName)) return string.Empty;
if (_videoIdCache.TryGetValue(gameName, out var cached))
return cached;
// Lightweight search
try
{
string query = WebUtility.UrlEncode(gameName + " VR trailer");
string searchUrl = $"https://www.youtube.com/results?search_query={query}";
using (var http = new HttpClient())
{
http.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/119.0");
var html = await http.GetStringAsync(searchUrl);
var vid = ExtractVideoId(html);
if (!string.IsNullOrEmpty(vid))
{
_videoIdCache[gameName] = vid;
return vid;
}
}
}
catch
{
// swallow return empty
}
return string.Empty;
}
private static CancellationTokenSource VideoDownloadTokenSource { get; set; }
public async void gamesListView_SelectedIndexChanged(object sender, EventArgs e)
{
if (gamesListView.SelectedItems.Count < 1)
{
return;
}
string CurrentPackageName = gamesListView.SelectedItems[gamesListView.SelectedItems.Count - 1].SubItems[SideloaderRCLONE.PackageNameIndex].Text;
string CurrentReleaseName = gamesListView.SelectedItems[gamesListView.SelectedItems.Count - 1].SubItems[SideloaderRCLONE.ReleaseNameIndex].Text;
string CurrentGameName = gamesListView.SelectedItems[gamesListView.SelectedItems.Count - 1].SubItems[SideloaderRCLONE.GameNameIndex].Text;
Console.WriteLine(CurrentGameName);
// Thumbnail
if (!keyheld)
{
if (settings.PackageNameToCB)
{
Clipboard.SetText(CurrentPackageName);
}
keyheld = true;
}
string[] imageExtensions = { ".jpg", ".png" };
string ImagePath = String.Empty;
foreach (string extension in imageExtensions)
{
string path = Path.Combine(SideloaderRCLONE.ThumbnailsFolder, $"{CurrentPackageName}{extension}");
if (File.Exists(path))
{
ImagePath = path;
break;
}
}
if (gamesPictureBox.BackgroundImage != null)
{
gamesPictureBox.BackgroundImage.Dispose();
}
gamesPictureBox.BackgroundImage = File.Exists(ImagePath) ? Image.FromFile(ImagePath) : new Bitmap(367, 214);
// Fast trailer loading path
webView21.Enabled = true;
webView21.Show();
try
{
var videoId = await ResolveVideoIdAsync(CurrentGameName);
if (string.IsNullOrEmpty(videoId))
{
changeTitle("No Trailer found");
}
else
{
await ShowVideoAsync(videoId);
}
}
catch (Exception ex)
{
changeTitle($"Error loading Trailer: {ex.Message}");
Logger.Log("Error loading Trailer");
Logger.Log(ex.Message);
}
string NotePath = $"{SideloaderRCLONE.NotesFolder}\\{CurrentReleaseName}.txt";
notesRichTextBox.Text = File.Exists(NotePath) ? File.ReadAllText(NotePath) : "";
}
public void UpdateGamesButton_Click(object sender, EventArgs e)
{
_ = GetDeviceID();
_ = FlexibleMessageBox.Show(Program.form, "If your device is not Connected, hit reconnect first or it won't work!\nNOTE: THIS MAY TAKE UP TO 60 SECONDS.\nThere will be a Popup text window with all updates available when it is done!", "Is device connected?", MessageBoxButtons.OKCancel);
listAppsBtn();
initListView(false);
if (SideloaderRCLONE.games.Count < 1)
{
_ = FlexibleMessageBox.Show(Program.form, "There are no games in rclone, please check your internet connection and check if the config is working properly");
return;
}
// if (gamesToUpdate.Length > 0)
// FlexibleMessageBox.Show(Program.form, gamesToUpdate);
// else
// FlexibleMessageBox.Show(Program.form, "All your games are up to date!");
}
private void gamesListView_MouseDoubleClick(object sender, MouseEventArgs e)
{
if (gamesListView.SelectedItems.Count > 0)
{
downloadInstallGameButton_Click(sender, e);
}
}
private void MountButton_Click(object sender, EventArgs e)
{
_ = ADB.RunAdbCommandToString("shell svc usb setFunctions mtp true");
}
private void freeDisclaimer_Click(object sender, EventArgs e)
{
_ = Process.Start("https://github.com/VRPirates/rookie");
}
private void searchTextBox_Enter(object sender, EventArgs e)
{
if (searchTextBox.Text == "Search..." && searchTextBox.ForeColor == Color.LightGray)
{
searchTextBox.Text = "";
searchTextBox.ForeColor = Color.White;
}
}
private void searchTextBox_Leave(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(searchTextBox.Text))
{
searchTextBox.Text = "Search...";
searchTextBox.ForeColor = Color.LightGray;
}
if (searchTextBox.Visible)
{
adbCmd_background.Visible = false;
}
else
{
_ = gamesListView.Focus();
}
}
private void gamesListView_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)Keys.Enter)
{
if (gamesListView.SelectedItems.Count > 0)
{
downloadInstallGameButton_Click(sender, e);
}
}
}
bool updateAvailableClicked = false;
private void updateAvailable_Click(object sender, EventArgs e)
{
lblUpToDate.Click -= lblUpToDate_Click;
lblUpdateAvailable.Click -= updateAvailable_Click;
lblNeedsDonate.Click -= lblNeedsDonate_Click;
if (upToDate_Clicked || NeedsDonation_Clicked)
{
upToDate_Clicked = false;
NeedsDonation_Clicked = false;
updateAvailableClicked = false;
}
if (!updateAvailableClicked)
{
updateAvailableClicked = true;
FilterListByColor(ColorTranslator.FromHtml("#4daa57")); // Update available color
}
else
{
updateAvailableClicked = false;
RestoreFullList();
}
lblUpToDate.Click += lblUpToDate_Click;
lblUpdateAvailable.Click += updateAvailable_Click;
lblNeedsDonate.Click += lblNeedsDonate_Click;
}
private async void ADBcommandbox_KeyPress(object sender, KeyPressEventArgs e)
{
searchTextBox.KeyPress += new
System.Windows.Forms.KeyPressEventHandler(CheckEnter);
if (e.KeyChar == (char)Keys.Enter)
{
if (manualIP)
{
string IPaddr;
IPaddr = adbCmd_CommandBox.Text;
string IPcmnd = "connect " + IPaddr + ":5555";
await Task.Delay(1000);
string errorChecker = ADB.RunAdbCommandToString(IPcmnd).Output;
if (errorChecker.Contains("cannot resolve host") | errorChecker.Contains("cannot connect to"))
{
changeTitle(String.Empty);
_ = FlexibleMessageBox.Show(Program.form, "Manual ADB over WiFi Connection failed\nExiting...", "Manual IP Connection Failed!", MessageBoxButtons.OK);
manualIP = false;
adbCmd_CommandBox.Visible = false;
adbCmd_btnToggleUpdates.Visible = false;
adbCmd_btnSend.Visible = false;
adbCmd_Label.Visible = false;
adbCmd_background.Visible = false;
adbCmd_Label.Text = "Type ADB Command";
_ = gamesListView.Focus();
}
else
{
_ = await Program.form.CheckForDevice();
Program.form.showAvailableSpace();
settings.IPAddress = IPcmnd;
settings.Save();
try { File.WriteAllText(storedIpPath, IPcmnd); }
catch (Exception ex) { Logger.Log($"Unable to write to StoredIP.txt due to {ex.Message}", LogLevel.ERROR); }
ADB.wirelessadbON = true;
_ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_available 1");
_ = ADB.RunAdbCommandToString("shell settings put global wifi_wakeup_enabled 1");
manualIP = false;
adbCmd_CommandBox.Visible = false;
adbCmd_btnToggleUpdates.Visible = false;
adbCmd_btnSend.Visible = false;
adbCmd_Label.Visible = false;
adbCmd_background.Visible = false;
adbCmd_Label.Text = "Type ADB Command";
changeTitle("");
Program.form.changeTitlebarToDevice();
_ = gamesListView.Focus();
}
}
else
{
string sentCommand = adbCmd_CommandBox.Text.Replace("adb", "");
Program.form.changeTitle($"Running adb command: ADB {sentCommand}");
string output = ADB.RunAdbCommandToString(adbCmd_CommandBox.Text).Output;
_ = FlexibleMessageBox.Show(Program.form, $"Ran adb command: ADB {sentCommand}\r\nOutput:\r\n{output}");
adbCmd_CommandBox.Visible = false;
adbCmd_btnToggleUpdates.Visible = false;
adbCmd_btnSend.Visible = false;
adbCmd_Label.Visible = false;
adbCmd_background.Visible = false;
_ = gamesListView.Focus();
Program.form.changeTitle(String.Empty);
}
}
if (e.KeyChar == (char)Keys.Escape)
{
adbCmd_CommandBox.Visible = false;
adbCmd_btnToggleUpdates.Visible = false;
adbCmd_btnSend.Visible = false;
adbCmd_Label.Visible = false;
adbCmd_background.Visible = false;
_ = gamesListView.Focus();
}
}
private void ADBcommandbox_Leave(object sender, EventArgs e)
{
adbCmd_background.Visible = false;
adbCmd_CommandBox.Visible = false;
adbCmd_btnToggleUpdates.Visible = false;
adbCmd_btnSend.Visible = false;
adbCmd_Label.Visible = false;
}
private void gamesQueListBox_MouseDown(object sender, MouseEventArgs e)
{
if (gamesQueListBox.SelectedItem == null)
{
return;
}
_ = gamesQueListBox.DoDragDrop(gamesQueListBox.SelectedItem, DragDropEffects.Move);
}
private void gamesQueListBox_DragOver(object sender, DragEventArgs e)
{
e.Effect = DragDropEffects.Move;
}
private async void pullAppToDesktopBtn_Click(object sender, EventArgs e)
{
if (m_combo.SelectedIndex == -1)
{
notify("Please select an App from the Dropdown");
return;
}
DialogResult dialogResult1 = FlexibleMessageBox.Show(Program.form, $"Do you want to extract {m_combo.SelectedItem}'s apk and obb to a folder on your desktop now?", "Extract app?", MessageBoxButtons.YesNo);
if (dialogResult1 == DialogResult.No)
{
return;
}
if (!isworking)
{
isworking = true;
progressBar.Style = ProgressBarStyle.Marquee;
string HWID = SideloaderUtilities.UUID();
string GameName = m_combo.SelectedItem.ToString();
string packageName = Sideloader.gameNameToPackageName(GameName);
string InstalledVersionCode = ADB.RunAdbCommandToString($"shell \"dumpsys package {packageName} | grep versionCode -F\"").Output;
InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingBeforeFirst(InstalledVersionCode, "versionCode=");
InstalledVersionCode = Utilities.StringUtilities.RemoveEverythingAfterFirst(InstalledVersionCode, " ");
ulong VersionInt = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(InstalledVersionCode));
if (Directory.Exists($"{settings.MainDir}\\{packageName}"))
{
Directory.Delete($"{settings.MainDir}\\{packageName}", true);
}
ProcessOutput output = new ProcessOutput("", "");
changeTitle("Extracting APK....");
_ = Directory.CreateDirectory($"{settings.MainDir}\\{packageName}");
Thread t1 = new Thread(() =>
{
output = Sideloader.getApk(GameName);
})
{
IsBackground = true
};
t1.Start();
while (t1.IsAlive)
{
await Task.Delay(100);
}
changeTitle("Extracting obb if it exists....");
Thread t2 = new Thread(() =>
{
output += ADB.RunAdbCommandToString($"pull \"/sdcard/Android/obb/{packageName}\" \"{settings.MainDir}\\{packageName}\"");
})
{
IsBackground = true
};
t2.Start();
while (t2.IsAlive)
{
await Task.Delay(100);
}
if (File.Exists($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip"))
{
File.Delete($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip");
}
string path = $"{settings.MainDir}\\7z.exe";
string cmd = $"7z a -mx1 \"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip\" .\\{packageName}\\*";
Program.form.changeTitle("Zipping extracted application...");
Thread t3 = new Thread(() =>
{
_ = ADB.RunCommandToString(cmd, path);
})
{
IsBackground = true
};
t3.Start();
while (t3.IsAlive)
{
await Task.Delay(100);
}
if (File.Exists($"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\\{GameName} v{VersionInt} {packageName}.zip"))
{
File.Delete($"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\\{GameName} v{VersionInt} {packageName}.zip");
}
File.Copy($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip", $"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\\{GameName} v{VersionInt} {packageName}.zip");
File.Delete($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip");
Directory.Delete($"{settings.MainDir}\\{packageName}", true);
isworking = false;
Program.form.changeTitle(" \n\n");
progressBar.Style = ProgressBarStyle.Continuous;
_ = FlexibleMessageBox.Show(Program.form, $"{GameName} pulled to:\n\n{GameName} v{VersionInt} {packageName}.zip\n\nOn your desktop!");
}
}
bool upToDate_Clicked = false;
private void lblUpToDate_Click(object sender, EventArgs e)
{
lblUpToDate.Click -= lblUpToDate_Click;
lblUpdateAvailable.Click -= updateAvailable_Click;
lblNeedsDonate.Click -= lblNeedsDonate_Click;
if (updateAvailableClicked || NeedsDonation_Clicked)
{
updateAvailableClicked = false;
NeedsDonation_Clicked = false;
upToDate_Clicked = false;
}
if (!upToDate_Clicked)
{
upToDate_Clicked = true;
FilterListByColor(ColorTranslator.FromHtml("#3c91e6")); // Up to date color
}
else
{
upToDate_Clicked = false;
RestoreFullList();
}
lblUpToDate.Click += lblUpToDate_Click;
lblUpdateAvailable.Click += updateAvailable_Click;
lblNeedsDonate.Click += lblNeedsDonate_Click;
}
bool NeedsDonation_Clicked = false;
private void lblNeedsDonate_Click(object sender, EventArgs e)
{
lblUpToDate.Click -= lblUpToDate_Click;
lblUpdateAvailable.Click -= updateAvailable_Click;
lblNeedsDonate.Click -= lblNeedsDonate_Click;
if (updateAvailableClicked || upToDate_Clicked)
{
updateAvailableClicked = false;
upToDate_Clicked = false;
NeedsDonation_Clicked = false;
}
if (!NeedsDonation_Clicked)
{
NeedsDonation_Clicked = true;
FilterListByColor(ColorTranslator.FromHtml("#cb9cf2")); // Needs donation color
}
else
{
NeedsDonation_Clicked = false;
RestoreFullList();
}
lblUpToDate.Click += lblUpToDate_Click;
lblUpdateAvailable.Click += updateAvailable_Click;
lblNeedsDonate.Click += lblNeedsDonate_Click;
}
private void FilterListByColor(Color targetColor)
{
changeTitle("Filtering Game List...");
if (_allItems == null || _allItems.Count == 0)
{
changeTitle("No games to filter");
return;
}
var filteredItems = _allItems
.Where(item => item.ForeColor.ToArgb() == targetColor.ToArgb())
.ToArray();
gamesListView.BeginUpdate();
gamesListView.Items.Clear();
gamesListView.Items.AddRange(filteredItems);
gamesListView.EndUpdate();
changeTitle(" \n\n");
}
private void RestoreFullList()
{
if (_allItems == null || _allItems.Count == 0)
{
changeTitle("No games to restore");
return;
}
gamesListView.BeginUpdate();
gamesListView.Items.Clear();
gamesListView.Items.AddRange(_allItems.ToArray());
gamesListView.EndUpdate();
changeTitle(" \n\n");
}
public static void OpenDirectory(string directoryPath)
{
if (Directory.Exists(directoryPath))
{
ProcessStartInfo p = new ProcessStartInfo
{
Arguments = directoryPath,
FileName = "explorer.exe"
};
Process.Start(p);
}
}
private void searchTextBox_Click(object sender, EventArgs e)
{
searchTextBox.Clear();
_ = searchTextBox.Focus();
}
private async void btnRunAdbCmd_Click(object sender, EventArgs e)
{
adbCmd_CommandBox.Visible = true;
adbCmd_btnToggleUpdates.Visible = true;
adbCmd_btnSend.Visible = true;
adbCmd_CommandBox.Clear();
adbCmd_Label.Text = "Type ADB Command";
adbCmd_Label.Visible = true;
adbCmd_background.Visible = true;
_ = adbCmd_CommandBox.Focus();
}
private void btnOpenDownloads_Click(object sender, EventArgs e)
{
string pathToOpen = settings.CustomDownloadDir ? $"{settings.DownloadDir}" : $"{Environment.CurrentDirectory}";
OpenDirectory(pathToOpen);
}
private void btnNoDevice_Click(object sender, EventArgs e)
{
bool currentStatus = settings.NodeviceMode || false;
if (currentStatus)
{
// No Device Mode is currently On. Toggle it Off
settings.NodeviceMode = false;
btnNoDevice.Text = "Disable Sideloading";
changeTitle($"Sideloading has been Enabled");
}
else
{
settings.NodeviceMode = true;
settings.DeleteAllAfterInstall = false;
btnNoDevice.Text = "Enable Sideloading";
changeTitle($"Sideloading Disabled. Games will only Download.");
}
settings.Save();
}
private void adbCmd_btnToggleUpdates_Click(object sender, EventArgs e)
{
string adbResult = ADB.RunAdbCommandToString("adb shell pm list packages -d").Output;
bool isUpdatesDisabled = adbResult.Contains("com.oculus.updater");
if (isUpdatesDisabled == true)
{
// Updates are already disabled. Enable them
adbCmd_CommandBox.Text = "adb shell pm enable com.oculus.updater";
}
else
{
adbCmd_CommandBox.Text = "shell pm disable-user --user 0 com.oculus.updater";
}
// adb shell pm enable com.oculus.updater
KeyPressEventArgs enterKeyPressArgs = new KeyPressEventArgs((char)Keys.Enter);
ADBcommandbox_KeyPress(adbCmd_CommandBox, enterKeyPressArgs);
}
private void adbCmd_btnSend_Click(object sender, EventArgs e)
{
KeyPressEventArgs enterKeyPressArgs = new KeyPressEventArgs((char)Keys.Enter);
ADBcommandbox_KeyPress(adbCmd_CommandBox, enterKeyPressArgs);
}
private ListViewItem _rightClickedItem;
private void gamesListView_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
_rightClickedItem = gamesListView.GetItemAt(e.X, e.Y);
gamesListView.SelectedItems.Clear();
if (_rightClickedItem != null)
{
_rightClickedItem.Selected = true;
}
// Get the name of the release of the right-clicked item
string packageName = _rightClickedItem.SubItems[1].Text;
// Check if the game is favorited and update the menu item text accordingly
ToolStripMenuItem favoriteMenuItem = favoriteGame.Items[0] as ToolStripMenuItem;
if (SettingsManager.Instance.FavoritedGames.Contains(packageName))
{
favoriteButton.Text = "Unfavorite"; // If it's already favorited, show "Unfavorite"
}
else
{
favoriteButton.Text = "Favorite"; // If it's not favorited, show "Favorite"
}
// Show the context menu at the mouse position
favoriteGame.Show(gamesListView, e.Location);
}
}
private void favoriteButton_Click(object sender, EventArgs e)
{
if (_rightClickedItem != null)
{
string packageName = _rightClickedItem.SubItems[1].Text;
// Check the menu item's text to decide whether to add or remove the game from favorites
if ((sender as ToolStripMenuItem).Text == "Favorite")
{
// Add to favorites
settings.AddFavoriteGame(packageName);
Console.WriteLine($"{packageName} has been added to favorites.");
}
else if ((sender as ToolStripMenuItem).Text == "Unfavorite")
{
// Remove from favorites
settings.RemoveFavoriteGame(packageName);
Console.WriteLine($"{packageName} has been removed from favorites.");
}
// After adding/removing, update the context menu text
ToolStripMenuItem favoriteMenuItem = sender as ToolStripMenuItem;
if (settings.FavoritedGames.Contains(packageName))
{
favoriteMenuItem.Text = "Unfavorite";
}
else
{
favoriteMenuItem.Text = "Favorite";
}
}
}
private void favoriteSwitcher_Click(object sender, EventArgs e)
{
if (favoriteSwitcher.Text == "Games List")
{
favoriteSwitcher.Text = "Favorited Games";
initListView(true);
}
else
{
favoriteSwitcher.Text = "Games List";
initListView(false);
}
}
public async void UpdateQuestInfoPanel()
{
// Check if device is actually connected by checking Devices list
bool hasDevice = Devices != null && Devices.Count > 0 && !Devices.Contains("unauthorized");
if ((!settings.NodeviceMode && hasDevice) || DeviceConnected)
{
try
{
ADB.DeviceID = GetDeviceID();
// Get device model
string deviceModel = ADB.RunAdbCommandToString("shell getprop ro.product.model").Output.Trim();
if (string.IsNullOrEmpty(deviceModel))
{
deviceModel = "Device";
}
// Get storage info
string storageOutput = ADB.RunAdbCommandToString("shell df /sdcard").Output;
string[] lines = storageOutput.Split('\n');
long totalSpace = 0;
long usedSpace = 0;
long freeSpace = 0;
if (lines.Length > 1)
{
string[] parts = lines[1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 4)
{
if (long.TryParse(parts[1], out totalSpace) &&
long.TryParse(parts[2], out usedSpace) &&
long.TryParse(parts[3], out freeSpace))
{
totalSpace = totalSpace / 1024; // Convert to MB
usedSpace = usedSpace / 1024;
freeSpace = freeSpace / 1024;
// Format free space display
if (freeSpace > 1024)
{
freeSpaceText = $"{(freeSpace / 1024.0):F2} GB AVAILABLE";
}
else
{
freeSpaceText = $"{freeSpace} MB AVAILABLE";
}
freeSpaceTextDetailed = $"{(usedSpace / 1024.0):F0} GB OF {(totalSpace / 1024.0):F0} GB USED";
}
}
}
// Calculate storage percentage used
int storagePercentUsed = totalSpace > 0 ? (int)((usedSpace * 100) / totalSpace) : 0;
// Update UI on main thread
questInfoPanel.Invoke(() =>
{
questInfoLabel.Text = deviceModel;
diskLabel.Text = freeSpaceText;
// Update custom progress bar
SetQuestStorageProgress(storagePercentUsed);
});
}
catch (Exception ex)
{
Logger.Log($"Unable to update quest info panel: {ex.Message}", LogLevel.ERROR);
questInfoPanel.Invoke(() =>
{
questInfoLabel.Text = "Device Info Unavailable";
SetQuestStorageProgress(0);
});
}
}
else
{
questInfoPanel.Invoke(() =>
{
questInfoLabel.Text = "No Device Connected";
SetQuestStorageProgress(0);
});
}
// Determine visibility based on device existence
bool showQuestInfo = ((!settings.NodeviceMode && hasDevice) || DeviceConnected);
// Toggle visibility atomically on UI thread
questInfoPanel.Invoke(() =>
{
questStorageProgressBar.Visible = showQuestInfo;
batteryLevImg.Visible = showQuestInfo;
batteryLabel.Visible = showQuestInfo;
questInfoLabel.Visible = showQuestInfo;
diskLabel.Visible = showQuestInfo;
});
}
private void QuestInfoHoverEnter(object sender, EventArgs e)
{
// Only react when device info is shown
if (!questStorageProgressBar.Visible) return;
diskLabel.Text = freeSpaceTextDetailed;
}
// Restore the original baseline text ("XX GB FREE")
private void QuestInfoHoverLeave(object sender, EventArgs e)
{
// Only react when device info is shown
if (!questStorageProgressBar.Visible) return;
// Ignore leave fired when moving between child controls inside the container
var panel = questInfoPanel;
var mouse = panel.PointToClient(MousePosition);
if (panel.ClientRectangle.Contains(mouse)) return;
diskLabel.Text = freeSpaceText;
}
private void questStorageProgressBar_Paint(object sender, PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.Clear(questStorageProgressBar.BackColor);
int w = questStorageProgressBar.ClientSize.Width;
int h = questStorageProgressBar.ClientSize.Height;
if (w <= 0 || h <= 0) return;
// Rounded rectangle parameters (outer + fill share the same geometry).
int radius = 10;
Color bgColor = Color.FromArgb(28, 32, 38);
Color borderColor = Color.FromArgb(60, 65, 75);
// Modern fill gradient (adjust as desired)
Color progressStart = Color.FromArgb(43, 160, 140);
Color progressEnd = Color.FromArgb(30, 110, 95);
// Build the rounded outer path (used for background and clipping).
var outer = new RoundedRectangleF(w, h, radius);
// Paint background
using (var bgBrush = new SolidBrush(bgColor))
using (var borderPen = new Pen(borderColor, 1f))
{
g.FillPath(bgBrush, outer.Path);
g.DrawPath(borderPen, outer.Path);
}
// Progress fraction and width
float p = Math.Max(0f, Math.Min(1f, _questStorageProgress / 100f));
int progressWidth = Math.Max(0, (int)(w * p));
if (progressWidth <= 0) return;
// Near-full rounding behavior:
// As progress approaches 100%, progressively include the outer right-rounded corners.
// Threshold start at 97%. At 97% -> straight cut; at 100% -> fully rounded outer corners.
float t = 0f;
if (p > 0.97f)
{
t = Math.Min(1f, (p - 0.97f) / 0.03f); // 0..1 over last 3%
}
// Build a clipping region for the fill: intersection of outer rounded rect with
// the left rectangular portion [0..progressWidth]
// plus a progressive right-cap region that extends into the rounded corners
// with width up to 2*radius, scaled by t.
Region fillClip = new Region(new Rectangle(0, 0, progressWidth, h));
if (t > 0f)
{
int capWidth = (int)(t * (2 * radius));
if (capWidth > 0)
{
// This rectangle sits inside the area of the right rounded corners,
// so union-ing it with the rectangular clip allows the fill to
// progressively "wrap" into the curvature.
var rightCapRect = new Rectangle(w - (2 * radius), 0, capWidth, h);
fillClip.Union(rightCapRect);
}
}
Region prevClip = g.Clip;
try
{
// Final fill region = outer rounded path ∩ fillClip
using (var outerRegion = new Region(outer.Path))
{
outerRegion.Intersect(fillClip);
g.SetClip(outerRegion, CombineMode.Replace);
using (var progressBrush = new LinearGradientBrush(
new Rectangle(0, 0, Math.Max(1, progressWidth), h),
progressStart,
progressEnd,
LinearGradientMode.Horizontal))
{
// Fill the outer path; clipping ensures the fill grows left to right,
// stays fully flush to the outer geometry, and never exceeds it.
g.FillPath(progressBrush, outer.Path);
}
}
}
finally
{
// Restore clip and re-stroke border to keep outline crisp
g.Clip = prevClip;
using (var borderPen = new Pen(borderColor, 1f))
{
g.DrawPath(borderPen, outer.Path);
}
}
}
private void SetQuestStorageProgress(int percentage)
{
_questStorageProgress = Math.Max(0, Math.Min(100, percentage));
if (questStorageProgressBar.InvokeRequired)
{
questStorageProgressBar.Invoke(new Action(() => questStorageProgressBar.Invalidate()));
}
else
{
questStorageProgressBar.Invalidate();
}
}
}
public static class ControlExtensions
{
public static void Invoke(this Control control, Action action)
{
if (control.InvokeRequired)
{
_ = control.Invoke(new MethodInvoker(action), null);
}
else
{
action.Invoke();
}
}
}
}