csharp/ajohns6/SM64-NX-Launcher/SM64%20EX%20Launcher/mainForm.cs

mainForm.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using System.Text;
using System.Windows.Forms;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using LibGit2Sharp;
using SM64_NX_Launcher.Properties;

namespace SM64_NX_Launcher
{
    public partial clast mainForm : Form
    {
        JArray PAKList;
        JArray PAKListLocal;
        public static PAK pak;
        static string mainDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "sm64nx");
        public static string nxDir = Path.Combine(mainDir, "repo");
        public static string pakDir = Path.Combine(nxDir, "romfs");
        List processes = new List();
        static string onlineJSON = Path.Combine(mainDir, @"PAKList.json");
        public static string localJSON = Path.Combine(mainDir, @"PAKLocal.json");
        StringCollection gridSelections = new StringCollection();

        public mainForm()
        {
            using (updateForm updateCheck = new updateForm())
            {
                if (updateCheck.ShowDialog() == DialogResult.OK)
                {
                    updateCheck.Dispose();
                }
            }

            InitializeComponent();

            // Handle the ApplicationExit event to know when the application is exiting.
            Application.ApplicationExit += new EventHandler(this.OnApplicationExit);
            this.Text += astembly.GetExecutingastembly().GetName().Version.ToString();

            if (!Directory.Exists(mainDir))
            {
                Directory.CreateDirectory(mainDir);
            }

            loadSettings();
        }

        private void OnApplicationExit(object sender, EventArgs e)
        {
            foreach (Process process in processes)
            {
                process.Kill();
            }
        }

        private void mainForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            foreach (Process process in processes)
            {
                process.Kill();
            }
        }

        private void Main_Load(object sender, EventArgs e)
        {
            if (!File.Exists(localJSON))
            {
                File.WriteAllText(localJSON, "[]");
            }

            if (File.Exists(onlineJSON))
            {
                populateGrid(onlineJSON);
                populateGrid(localJSON);
            }
            else
            {
                pullJSON();
                populateGrid(onlineJSON);
                populateGrid(localJSON);
            }
        }

        private void populateGrid(string JSONfile)
        {
            try
            {
                string json = File.ReadAllText(JSONfile);
                if (JSONfile == onlineJSON)
                {
                    PAKList = JArray.Parse(json);
                    onlineGrid.DataSource = PAKList;

                    foreach (DataGridViewRow row in onlineGrid.Rows)
                    {
                        if (gridSelections.Contains(row.Cells[5].Value.ToString()))
                        {
                            row.Cells[0].Value = true;
                        }
                    }
                }
                else
                {
                    PAKListLocal = JArray.Parse(json);
                    localGrid.DataSource = PAKListLocal;

                    foreach (DataGridViewRow row in localGrid.Rows)
                    {
                        if (gridSelections.Contains(row.Cells[5].Value.ToString()))
                        {
                            row.Cells[0].Value = true;
                        }
                    }
                }
            }
            catch
            {
                MessageBox.Show("An error occurred while populating PAK list.\n\nIt may be corrupted. Please refresh your list.", "PAK List Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void mainForm_Shown(object sender, EventArgs e)
        {
            int result = 0;
            if (Directory.Exists(nxDir))
            {
                try
                {
                    using (var repo = new Repository(nxDir))
                    {
                        Branch b = repo.Branches["master"];
                        result = (int)b.TrackingDetails.BehindBy;
                    }
                }
                catch
                {
                    DeleteDirectory(nxDir);
                    mainForm_Shown(sender, e);
                }
            }
            else
            {
                var answer = MessageBox.Show("You do not appear to have a repository for SM64 NX.\n\nWould you like to download and build it now?", "Missing Repository", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
                if (answer == DialogResult.Yes)
                {
                    if (updateRepo())
                    {
                        return;
                    }
                    if (checkDependencies())
                    {
                        return;
                    }
                    if (buildBase()) return;
                    MessageBox.Show("Your repository, dependencies, and !!base.pak have all been successfully updated.", "Done", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
            }

            if (result > 0)
            {
                var answer = MessageBox.Show("Your current repository is out of date.\n\nWould you like to update it and rebuild?", "Update", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
                if (answer == DialogResult.Yes)
                {
                    updateRepo();
                    MessageBox.Show("Your repository has been successfully updated.", "Done", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
            }
        }

        private bool buildBase()
        {
            this.outputText.Visible = true;

            if (!File.Exists(Path.Combine(nxDir, "baserom.us.z64")))
            {
                if (File.Exists(Path.Combine(nxDir, "baserom.us.z64")))
                {
                    File.Move(Path.Combine(nxDir, "baserom.us.z64"), Path.Combine(nxDir, "baserom.us.z64"));
                }
                else
                {
                    MessageBox.Show("You are missing baserom.us.z64.\n\nPlease select your baserom.us.z64.", "ROM Missing", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    Boolean exit = false;
                    var result = new OpenFileDialog();
                    while (!exit)
                    {
                        if (result.ShowDialog() == DialogResult.OK)
                        {
                            if (checkHash(result.FileName))
                            {
                                File.Copy(result.FileName, Path.Combine(nxDir, "baserom.us.z64"));
                                exit = true;
                            }
                            else
                            {
                                var answer = MessageBox.Show("ROM hash is not valid.\n\nWould you like to select another?", "Invalid ROM Hash", MessageBoxButtons.YesNo, MessageBoxIcon.Error);
                                if (answer == DialogResult.No) return true;
                            }
                        }
                        else
                        {
                            return true;
                        }
                    }
                }
            }
            if (runProcess("cmd.exe", @"/C build.bat"))
            {
                this.outputText.Text += "Traditional build.bat file failed.\n\nAttempting static calls as a fallback...\n\n";

                if (runProcess("cmd.exe", " /C py extract_astets.py && py ..\\scripts\\pak.py --source . --output ..\\romfs\\!!base.pak"))
                {
                    MessageBox.Show("!!Base.pak failed to build.\n\nCheck for error messages in log or share them with troubleshooters.", "Base PAK Build Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return true;
                }
            }
            
            return false;
        }

        private void pullJSON()
        {
            string url = "https://api.jsonbin.io/b/5f05f3e0a62f9b4b27613c5a/latest";

            HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
            using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
            {
                StreamReader reader = new StreamReader(response.GetResponseStream());
                File.WriteAllText(onlineJSON, reader.ReadToEnd());
            }
            MessageBox.Show("Your PAK List has been successfully updated.", "Done", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        private void pullJSON(object sender, EventArgs e)
        {
            string url = "https://api.jsonbin.io/b/5f05f3e0a62f9b4b27613c5a/latest";

            HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
            using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
            {
                StreamReader reader = new StreamReader(response.GetResponseStream());
                File.WriteAllText(onlineJSON, reader.ReadToEnd());
            }

            populateGrid(onlineJSON);
            MessageBox.Show("Your PAK list has been successfully updated.", "Done", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        private void outputText_TextChanged(object sender, EventArgs e)
        {
            this.outputText.SelectionStart = this.outputText.Text.Length;
            this.outputText.ScrollToCaret();
        }

        private void selectedModsButton_Click(object sender, EventArgs e)
        {
            foreach(DataGridViewRow row in onlineGrid.Rows)
            {
                Boolean enable = Convert.ToBoolean(row.Cells[0].Value);

                PAK pak = new PAK();
                pak.modName = row.Cells[1].Value.ToString();
                pak.modCreator = row.Cells[2].Value.ToString();
                pak.modType = row.Cells[3].Value.ToString();
                pak.modDesc = row.Cells[4].Value.ToString();
                pak.modDir = row.Cells[5].Value.ToString();
                pak.modURL = row.Cells[6].Value.ToString();
                pak.modFile = row.Cells[7].Value.ToString();
                pak.modHash = row.Cells[8].Value.ToString();

                if (enable)
                {
                    enablePAK(pak);
                }
                else
                {
                    disablePAK(pak);
                }
            }

            foreach (DataGridViewRow row in localGrid.Rows)
            {
                Boolean enable = Convert.ToBoolean(row.Cells[0].Value);

                PAK pak = new PAK();
                pak.modName = row.Cells[1].Value.ToString();
                pak.modCreator = row.Cells[2].Value.ToString();
                pak.modType = row.Cells[3].Value.ToString();
                pak.modDesc = row.Cells[4].Value.ToString();
                pak.modDir = row.Cells[5].Value.ToString();
                pak.modURL = row.Cells[6].Value.ToString();
                pak.modFile = row.Cells[7].Value.ToString();
                pak.modHash = row.Cells[8].Value.ToString();

                if (enable)
                {
                    enablePAK(pak);
                }
                else
                {
                    disablePAK(pak);
                }
            }

            if (launch())
            {
                saveSettings();
                if (!this.closeCheck.Checked) Application.Exit();
            }
        }

        private void noModsButton_Click(object sender, EventArgs e)
        {
            foreach (DataGridViewRow row in onlineGrid.Rows)
            {
                PAK pak = new PAK();
                pak.modName = row.Cells[1].Value.ToString();
                pak.modCreator = row.Cells[2].Value.ToString();
                pak.modType = row.Cells[3].Value.ToString();
                pak.modDesc = row.Cells[4].Value.ToString();
                pak.modDir = row.Cells[5].Value.ToString();
                pak.modURL = row.Cells[6].Value.ToString();
                pak.modFile = row.Cells[7].Value.ToString();
                pak.modHash = row.Cells[8].Value.ToString();

                disablePAK(pak);
            }

            foreach (DataGridViewRow row in onlineGrid.Rows)
            {
                PAK pak = new PAK();
                pak.modName = row.Cells[1].Value.ToString();
                pak.modCreator = row.Cells[2].Value.ToString();
                pak.modType = row.Cells[3].Value.ToString();
                pak.modDesc = row.Cells[4].Value.ToString();
                pak.modDir = row.Cells[5].Value.ToString();
                pak.modURL = row.Cells[6].Value.ToString();
                pak.modFile = row.Cells[7].Value.ToString();
                pak.modHash = row.Cells[8].Value.ToString();

                disablePAK(pak);
            }

            if (launch() && !this.closeCheck.Checked)
            {
                Application.Exit();
            }
        }

        private bool launch()
        {
            if (!File.Exists(Path.Combine(pakDir,"!!base.pak")))
            {
                MessageBox.Show("You do not have !!base.pak.\n\nIt will need to be generated from baserom.us.z64 before launching.\n\nThis is a one-time process.", "Base Build Notice", MessageBoxButtons.OK, MessageBoxIcon.Information);
                if (checkDependencies()) return false;
                if (buildBase()) return false;
            }
            Process sm64nx = new Process();

            ProcessStartInfo sm64nxStart = new ProcessStartInfo();
            sm64nxStart.FileName = Path.Combine(nxDir,"sm64.exe");
            sm64nxStart.WorkingDirectory = nxDir;

            sm64nx.StartInfo = sm64nxStart;
            sm64nx.Start();

            return true;
        }

        private void enablePAK(PAK pak)
        {
            if (Directory.Exists(Path.Combine(pakDir,"~" + pak.modDir)) && !Directory.Exists(Path.Combine(pakDir, pak.modDir)))
            {
                if (checkHash(Path.Combine(pakDir, "~" + pak.modDir, pak.modFile), pak))
                {
                    Directory.Move(Path.Combine(pakDir, "~" + pak.modDir), Path.Combine(pakDir, pak.modDir));
                }
                else
                {
                    DeleteDirectory(Path.Combine(pakDir, "~" + pak.modDir));
                    using (progressForm PAKDownload = new progressForm("Incorrect " + pak.modName + " Hash", "Downloading new copy of " + pak.modFile + "...", pak))
                    {
                        if (PAKDownload.ShowDialog() == DialogResult.OK)
                        {
                            PAKDownload.Dispose();
                        }
                    }
                }
            }
            else if (Directory.Exists(Path.Combine(pakDir, pak.modDir)) && (!checkHash(Path.Combine(pakDir, pak.modDir, pak.modFile), pak)))
            {
                using (progressForm PAKDownload = new progressForm("Incorrect " + pak.modName + " Hash", "Downloading new copy of " + pak.modFile + "...", pak))
                {
                    if (PAKDownload.ShowDialog() == DialogResult.OK)
                    {
                        PAKDownload.Dispose();
                    }
                }
            }
            else if (!Directory.Exists(Path.Combine(pakDir, pak.modDir)) && !Directory.Exists(Path.Combine(pakDir, "~" + pak.modDir)))
            {
                using (progressForm PAKDownload = new progressForm(pak.modName + " PAK Download", "Downloading " + pak.modFile + "...", pak))
                {
                    DeleteDirectory(Path.Combine(pakDir, pak.modDir));
                    if (PAKDownload.ShowDialog() == DialogResult.OK)
                    {
                        PAKDownload.Dispose();
                    }
                }
            }
            gridSelections.Add(pak.modDir);
        }

        private void disablePAK(PAK pak)
        {
            if (Directory.Exists(Path.Combine(pakDir, pak.modDir)) && !Directory.Exists(Path.Combine(pakDir, "~" + pak.modDir)))
            {
                Directory.Move(Path.Combine(pakDir, pak.modDir), Path.Combine(pakDir, "~" + pak.modDir));
            }
            if (gridSelections.Contains(pak.modDir))
            {
                gridSelections.Remove(pak.modDir);
            }
        }

        private Boolean checkDependencies()
        {
            this.outputText.Visible = true;

            this.outputText.Text += "Python Dependency Update\n";
            if (runProcess("cmd.exe", @"/C py -m pip install --upgrade pip wheel pillow zstandard tqdm xxhash"))
            {
                MessageBox.Show("Unable to update dependencies.\n\nCheck the log output or provide it for support.", "Dependency Update Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return true;
            }

            this.outputText.Text += "Python dependencies updated!";
            return false;
        }

        private Boolean runProcess(string filename, string arguments)
        {
            this.outputText.Text += "\nExecuting " + filename + arguments + "\n";

            ProcessStartInfo sInfo = new ProcessStartInfo();
            sInfo.FileName = filename;
            sInfo.Arguments = arguments;
            sInfo.WorkingDirectory = Path.Combine(nxDir, "import");
            sInfo.RedirectStandardOutput = true;
            sInfo.RedirectStandardError = true;
            sInfo.UseShellExecute = false;
            sInfo.CreateNoWindow = true;

            Process process = new Process();
            process.StartInfo = sInfo;
            processes.Add(process);

            process.OutputDataReceived += new DataReceivedEventHandler((s, e) =>
            {
                this.BeginInvoke(new MethodInvoker(() =>
                {
                    this.outputText.Text += e.Data + "\n";
                }));
            });

            process.ErrorDataReceived += new DataReceivedEventHandler((s, e) =>
            {
                this.BeginInvoke(new MethodInvoker(() =>
                {
                    this.outputText.Text += e.Data + "\n";
                }));
            });

            process.Start();
            process.BeginOutputReadLine();
            process.BeginErrorReadLine();
            while (!process.HasExited)
            {
                Application.DoEvents();
            }

            processes.Remove(process);
            
            if (process.ExitCode > 0)
            {
                return true;
            }

            process.Close();

            return false;
        }

        private Boolean updateRepo()
        {
            if (Directory.Exists(nxDir))
            {
                using (var repo = new Repository(nxDir))
                {
                    try
                    {
                        Commands.Pull(repo, new Signature("guest", "guest", DateTimeOffset.Now), new PullOptions());
                    }
                    catch
                    {
                        MessageBox.Show("Unable to update repository.\n\nCheck your Internet connect or try again later.", "Repository Update Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        return true;
                    }
                }
            }
            else
            {
                MessageBox.Show("You are about to download the sm64nx repository.\n\nDepending on your system, this could take several minutes.\n\nDownload will begin when you clikc OK.", "Repository Download", MessageBoxButtons.OK, MessageBoxIcon.Information);

                using (progressForm repoClone = new progressForm("Repository Download", "Downloading a fresh copy of the sm64nx repository..."))
                {
                    if (repoClone.ShowDialog() == DialogResult.OK)
                    {
                        repoClone.Dispose();
                    }
                }
            }

            return false;
        }

        private void updateButton_Click(object sender, EventArgs e)
        {
            if (updateRepo())
            {
                MessageBox.Show("Unable to update repository.\n\nCheck your Internet connect or try again later.", "Repository Update Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            if (checkDependencies())
            {
                MessageBox.Show("Unable to update dependencies.\n\nCheck the log output or provide it for support.", "Dependency Update Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            MessageBox.Show("Your repository and dependencies have been successfully updated.", "Done", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        private Boolean checkHash(string file)
        {
            using (SHA1Managed sha1Hasher = new SHA1Managed())
            using (FileStream stream = new FileStream(file, FileMode.Open))
            using (BufferedStream buffer = new BufferedStream(stream))
            {
                byte[] hash = sha1Hasher.ComputeHash(buffer);
                StringBuilder hashString = new StringBuilder(2 * hash.Length);
                foreach (byte b in hash)
                {
                    hashString.AppendFormat("{0:x2}", b);
                }

                if (hashString.ToString() == "9bef1128717f958171a4afac3ed78ee2bb4e86ce") return true;
            }
            return false;
        }

        private Boolean checkHash(string file, PAK pak)
        {
            if (pak.modURL is "") return true;
            if (pak.modHash is null)
            {
                pullJSON();
                return false;
            }

            using (SHA1Managed sha1Hasher = new SHA1Managed())
            using (FileStream stream = new FileStream(file, FileMode.Open))
            using (BufferedStream buffer = new BufferedStream(stream))
            {
                byte[] hash = sha1Hasher.ComputeHash(buffer);
                StringBuilder hashString = new StringBuilder(2 * hash.Length);
                foreach (byte b in hash)
                {
                    hashString.AppendFormat("{0:x2}", b);
                }

                if (hashString.ToString().ToUpper() == pak.modHash) return true;
            }
            return false;
        }
        public static void DeleteDirectory(string targetDir)
        {
            if (!Directory.Exists(targetDir)) return;

            File.SetAttributes(targetDir, FileAttributes.Normal);

            string[] files = Directory.GetFiles(targetDir);
            string[] dirs = Directory.GetDirectories(targetDir);

            foreach (string file in files)
            {
                File.SetAttributes(file, FileAttributes.Normal);
                File.Delete(file);
            }

            foreach (string dir in dirs)
            {
                DeleteDirectory(dir);
            }

            Directory.Delete(targetDir, false);
        }
        
        public void loadSettings()
        {
            if (Settings.Default.activeMods != null)
            {
                this.gridSelections = Settings.Default.activeMods;
                this.closeCheck.Checked = Settings.Default.closeLauncher;
            }
        }

        public void saveSettings()
        {
            Settings.Default.activeMods = this.gridSelections;
            Settings.Default.closeLauncher = this.closeCheck.Checked;
            Settings.Default.Save();
        }

        private void addButton_Click(object sender, EventArgs e)
        {
            using (addForm addPAK = new addForm())
            {
                if (addPAK.ShowDialog() == DialogResult.OK)
                {
                    addPAK.Dispose();
                }
            }

            populateGrid(localJSON);
        }

        private void removeButton_Click(object sender, EventArgs e)
        {
            StringCollection removals = new StringCollection();
            foreach (DataGridViewRow row in localGrid.Rows)
            {
                if (Convert.ToBoolean(row.Cells[0].Value))
                {
                    removals.Add(row.Cells[5].Value.ToString());
                }
            }

            foreach (string pak in removals)
            {
                if (Directory.Exists(Path.Combine(pakDir, pak)))
                {
                    DeleteDirectory(Path.Combine(pakDir, pak));
                }

                if (Directory.Exists(Path.Combine(pakDir, "~" + pak)))
                {
                    DeleteDirectory(Path.Combine(pakDir, "~" + pak));
                }

                gridSelections.Remove(pak);

                string jsonString = File.ReadAllText(mainForm.localJSON);
                var list = JsonConvert.DeserializeObject(jsonString);
                list.Remove(list.Single( s => s.modDir == pak));
                var convertedJSON = JsonConvert.SerializeObject(list);
                File.WriteAllText(mainForm.localJSON, convertedJSON);
            }
                
            saveSettings();

            populateGrid(localJSON);
        }
    }
    public clast PAK
    {
        public string modName { get; set; }
        public string modDesc { get; set; }
        public string modURL { get; set; }
        public string modDir { get; set; }
        public string modFile { get; set; }
        public string modCreator { get; set; }
        public string modType { get; set; }
        public string modHash { get; set; }
    }
}