diff --git a/.gitignore b/.gitignore index 9491a2f..2697326 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Rider +.idea/ \ No newline at end of file diff --git a/.idea/.idea.WACCALauncher.dir/.idea/.gitignore b/.idea/.idea.WACCALauncher.dir/.idea/.gitignore deleted file mode 100644 index 04a8467..0000000 --- a/.idea/.idea.WACCALauncher.dir/.idea/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/modules.xml -/contentModel.xml -/.idea.WACCALauncher.iml -/projectSettingsUpdater.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.idea.WACCALauncher.dir/.idea/indexLayout.xml b/.idea/.idea.WACCALauncher.dir/.idea/indexLayout.xml deleted file mode 100644 index 7b08163..0000000 --- a/.idea/.idea.WACCALauncher.dir/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/.idea.WACCALauncher/.idea/.gitignore b/.idea/.idea.WACCALauncher/.idea/.gitignore deleted file mode 100644 index e2687a9..0000000 --- a/.idea/.idea.WACCALauncher/.idea/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/contentModel.xml -/.idea.WACCALauncher.iml -/modules.xml -/projectSettingsUpdater.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.idea.WACCALauncher/.idea/indexLayout.xml b/.idea/.idea.WACCALauncher/.idea/indexLayout.xml deleted file mode 100644 index 7b08163..0000000 --- a/.idea/.idea.WACCALauncher/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/FontLoader.cs b/FontLoader.cs new file mode 100644 index 0000000..31afea5 --- /dev/null +++ b/FontLoader.cs @@ -0,0 +1,32 @@ +using System; +using System.Drawing; +using System.Drawing.Text; + +namespace WACCALauncher +{ + internal static class FontLoader + { + public static Font loadedFont; + + [System.Runtime.InteropServices.DllImport("gdi32.dll")] + private static extern IntPtr AddFontMemResourceEx(IntPtr pbFont, uint cbFont, + IntPtr pdv, [System.Runtime.InteropServices.In] ref uint pcFonts); + + private static readonly PrivateFontCollection fontCollection = new PrivateFontCollection(); + + public static Font LoadFont() + { + if (loadedFont != null) return loadedFont; + var fontData = Properties.Resources.menufont; + var fontPtr = System.Runtime.InteropServices.Marshal.AllocCoTaskMem(fontData.Length); + System.Runtime.InteropServices.Marshal.Copy(fontData, 0, fontPtr, fontData.Length); + uint dummy = 0; + fontCollection.AddMemoryFont(fontPtr, Properties.Resources.menufont.Length); + AddFontMemResourceEx(fontPtr, (uint)Properties.Resources.menufont.Length, IntPtr.Zero, ref dummy); + System.Runtime.InteropServices.Marshal.FreeCoTaskMem(fontPtr); + + loadedFont = new Font(fontCollection.Families[0], 22.5F); + return loadedFont; + } + } +} diff --git a/LICENSE.txt b/LICENSE.txt index 1733de7..d69c574 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Yellowberry +Copyright (c) 2023-2025 Yellowberry Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/LauncherSettings.cs b/LauncherSettings.cs new file mode 100644 index 0000000..78ca355 --- /dev/null +++ b/LauncherSettings.cs @@ -0,0 +1,42 @@ +using Newtonsoft.Json; +using System.IO; + +namespace WACCALauncher +{ + // TODO: implement themes + public enum LauncherTheme + { + Dark + } + + [JsonObject] + public class LauncherSettings + { + [JsonProperty] + public string DefaultProfile { get; set; } = string.Empty; + + [JsonProperty] + public bool UseWatchdog { get; set; } = true; + + [JsonProperty] + public LauncherTheme Theme { get; set; } = LauncherTheme.Dark; + + [JsonProperty] + public bool StrictMode { get; set; } = true; + + [JsonProperty] + public string ProfileDir { get; set; } = "_profiles"; + + public static LauncherSettings Load() + { + return JsonConvert.DeserializeObject(File.ReadAllText("launcher.json")); + } + + public static void Save(LauncherSettings settings) + { + if (settings == null) settings = new LauncherSettings(); + + File.WriteAllText("launcher.json", JsonConvert.SerializeObject(settings, Formatting.Indented)); + } + } +} \ No newline at end of file diff --git a/MainForm.Designer.cs b/MainForm.Designer.cs index 7818899..48d40f9 100644 --- a/MainForm.Designer.cs +++ b/MainForm.Designer.cs @@ -29,7 +29,7 @@ private void InitializeComponent() { this.menuLabel = new System.Windows.Forms.Label(); - this.waccaListTest = new WACCALauncher.WaccaList(); + this.menuListBox = new WACCALauncher.WaccaList(); this.SuspendLayout(); // // menuLabel @@ -45,20 +45,13 @@ this.menuLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; this.menuLabel.Visible = false; // - // waccaListTest + // menuListBox // - this.waccaListTest.BackColor = System.Drawing.Color.Black; - this.waccaListTest.BorderStyle = System.Windows.Forms.BorderStyle.None; - this.waccaListTest.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawVariable; - this.waccaListTest.Enabled = false; - this.waccaListTest.ForeColor = System.Drawing.Color.White; - this.waccaListTest.FormattingEnabled = true; - this.waccaListTest.ItemHeight = 40; - this.waccaListTest.Location = new System.Drawing.Point(200, 240); - this.waccaListTest.Name = "waccaListTest"; - this.waccaListTest.Size = new System.Drawing.Size(680, 600); - this.waccaListTest.TabIndex = 1; - this.waccaListTest.Visible = false; + this.menuListBox.Enabled = false; + this.menuListBox.FormattingEnabled = true; + this.menuListBox.Name = "menuListBox"; + this.menuListBox.TabIndex = 1; + this.menuListBox.Visible = false; // // MainForm // @@ -66,7 +59,7 @@ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.BackColor = System.Drawing.Color.Black; this.ClientSize = new System.Drawing.Size(1080, 1080); - this.Controls.Add(this.waccaListTest); + this.Controls.Add(this.menuListBox); this.Controls.Add(this.menuLabel); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; this.KeyPreview = true; @@ -75,7 +68,7 @@ this.StartPosition = System.Windows.Forms.FormStartPosition.Manual; this.Text = "WACCA Launcher"; this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); - this.Load += new System.EventHandler(this.Form1_Load); + this.Load += new System.EventHandler(this.MainForm_Load); this.ResumeLayout(false); } @@ -83,7 +76,7 @@ #endregion private System.Windows.Forms.Label menuLabel; - private WaccaList waccaListTest; + private WaccaList menuListBox; } } diff --git a/MainForm.cs b/MainForm.cs index fec639a..945eda0 100644 --- a/MainForm.cs +++ b/MainForm.cs @@ -4,196 +4,232 @@ using System.Drawing; using System.IO; using System.Text; using System.Windows.Forms; -using System.Drawing.Text; using System.Timers; using System.Diagnostics; -using IniParser; -using IniParser.Model; using SharpDX.DirectInput; -using System.Linq; -using System.Reflection; -using WACCA; +using Newtonsoft.Json; namespace WACCALauncher { + public enum LauncherState + { + Launching, + Updating, + GameStarting, + GameRunning, + InMenu, + GameClosed, + Error + } + public partial class MainForm : Form { private static System.Timers.Timer _delayTimer; private readonly System.Windows.Forms.Timer _t = new System.Windows.Forms.Timer(); - [System.Runtime.InteropServices.DllImport("gdi32.dll")] - private static extern IntPtr AddFontMemResourceEx(IntPtr pbFont, uint cbFont, - IntPtr pdv, [System.Runtime.InteropServices.In] ref uint pcFonts); + public LauncherState _state; - private readonly PrivateFontCollection _fonts = new PrivateFontCollection(); - private Label _loadingLabel; - private Label _versionLabel; + private static Font _menuFont; - private Font _menuFont; + private static string _loadingText = "LOADING"; + + private static Label _loadingLabel = new Label(); + private static Label _versionLabel = new Label(); + private static Label _buttonLabel = new Label(); private readonly DirectInput _input = new DirectInput(); private Joystick _ioBoard; - public readonly List Versions = new List(); - public Version DefaultVer; + public static readonly List Profiles = new List(); + public static Profile DefaultProfile; + public Profile SelectedProfile; - private readonly IniData _config; - private readonly FileIniDataParser _parser = new FileIniDataParser(); + private bool _skipUpdater = false; private readonly Process _gameProcess = new Process(); - private bool _gameRunning = false; - public bool AutoLaunch = true; + private readonly Process _amdaemonProcess = new Process(); - public MenuManager _menuManager; - private VFD _vfd; + internal MenuManager _menuManager; + + internal LauncherSettings settings; + + [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); public MainForm() { + _menuFont = FontLoader.LoadFont(); + InitializeComponent(); + // start input timer _t.Tick += Tick; _t.Interval = 20; _t.Start(); - _delayTimer = new System.Timers.Timer(5000); - _delayTimer.Elapsed += LaunchDefault; - - _delayTimer.Enabled = true; - - // Load embedded font into memory - - LoadFont(); - - try + // if we're running on a vertical monitor, compensate for ring offset + if (Program.IsCorrectRes()) { - _config = _parser.ReadFile("wacca.ini"); - } - catch (IniParser.Exceptions.ParsingException) - { - DisplayError("Config error", "wacca.ini could not be read, check for errors"); + var bounds = Program.CurrentScreen.Bounds; + SetBounds(bounds.X, bounds.Y + 362, Width, Height); } - if (!Program.IsCorrectRes()) return; - var bounds = Program.CurrentScreen.Bounds; - this.SetBounds(bounds.X, bounds.Y + 362, Width, Height); + // bind game exit to be handled properly (ONLY ONCE) + _gameProcess.Exited += HandleGameClosed; + + // timer started before we check profiles, it gets stopped when errors are thrown + StartLaunchTimer(); } - private void LoadFont() + public void UpdateLoadingText() { - var fontData = Properties.Resources.menufont; - var fontPtr = System.Runtime.InteropServices.Marshal.AllocCoTaskMem(fontData.Length); - System.Runtime.InteropServices.Marshal.Copy(fontData, 0, fontPtr, fontData.Length); - uint dummy = 0; - _fonts.AddMemoryFont(fontPtr, Properties.Resources.menufont.Length); - AddFontMemResourceEx(fontPtr, (uint)Properties.Resources.menufont.Length, IntPtr.Zero, ref dummy); - System.Runtime.InteropServices.Marshal.FreeCoTaskMem(fontPtr); - - _menuFont = new Font(_fonts.Families[0], 22.5F); + _loadingLabel.Text = string.Join("",_loadingText,_skipUpdater ? "!!!" : "..."); } protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { - if (!_gameRunning) + if (_state == LauncherState.Launching || _state == LauncherState.InMenu) { - if (keyData == Keys.Up) { _menuManager.CursorUp(); return true; } - else if (keyData == Keys.Down) { _menuManager.CursorDown(); return true; } - else if (keyData == Keys.Enter) { _menuManager.MenuSelect(); return true; } - else if (keyData == Keys.Escape) + switch(keyData) { - if (AutoLaunch) MenuShow(); - else _menuManager.MenuBack(); - return true; + case Keys.Up: + _menuManager.CursorUp(); + return true; + case Keys.Down: + _menuManager.CursorDown(); + return true; + case Keys.Enter: + _menuManager.MenuSelect(); + return true; + case Keys.Escape: + if (_state == LauncherState.InMenu) _menuManager.MenuBack(); + else MenuShow(); + + return true; } } + else if (_state == LauncherState.Error && keyData == Keys.Escape) + { + Application.Exit(); + return true; + } return base.ProcessCmdKey(ref msg, keyData); } - private bool[] _buttonStates; - private bool[] _lastButtonStates = new bool[4]; - - private async void Tick(object sender, EventArgs e) + private bool ButtonPressed(JoystickUpdate state, int button) { + return state.Offset == (JoystickOffset.Buttons0 + button) && state.Value > 0; + } + + private bool ButtonReleased(JoystickUpdate state, int button) + { + return state.Offset == (JoystickOffset.Buttons0 + button) && state.Value == 0; + } + + private bool ButtonHeld(Joystick joystick, int button) + { + return joystick.GetCurrentState().Buttons[button]; + } + + private void Tick(object sender, EventArgs e) + { + UpdateLoadingText(); + + if (_state != LauncherState.Launching && _state != LauncherState.InMenu) return; + var gamepads = _input.GetDevices(DeviceClass.GameControl, DeviceEnumerationFlags.AttachedOnly); - try - { - _gameRunning = _gameProcess.StartTime != null; - } - catch (InvalidOperationException) {} - + // set up IO board controls if (_ioBoard == null && gamepads.Count > 0) { - Console.WriteLine("gamepad detected"); // it will be the only gamepad on the system var guid = gamepads[0].InstanceGuid; _ioBoard = new Joystick(_input, guid); + _ioBoard.Properties.BufferSize = 128; _ioBoard.Acquire(); } - else if (gamepads.Count > 0 && !_gameRunning) + else if (gamepads.Count > 0) { _ioBoard.Poll(); - _buttonStates = _ioBoard.GetCurrentState().Buttons; + var padStates = _ioBoard.GetBufferedData(); - // vol down - if (_buttonStates[0] && !_lastButtonStates[0]) + foreach (var padState in padStates) { - Console.WriteLine("vol down"); - _menuManager.CursorDown(); - } - - // vol up - if (_buttonStates[1] && !_lastButtonStates[1]) - { - Console.WriteLine("vol up"); - _menuManager.CursorUp(); - } - - // service - if (_buttonStates[6] && !_lastButtonStates[6]) - { - Console.WriteLine("service button"); - _menuManager.CursorDown(); - } - - // test - if (_buttonStates[9] && !_lastButtonStates[9]) - { - Console.WriteLine("test button"); - if(AutoLaunch) + if(padState.Offset >= JoystickOffset.Buttons0 + && padState.Offset < JoystickOffset.Buttons10) { - MenuShow(); - } - else _menuManager.MenuSelect(); - } + var pressed = padState.Value > 0; - _lastButtonStates = _buttonStates; + switch (_state) + { + case LauncherState.InMenu: + // vol down + if (ButtonPressed(padState, 0)) _menuManager.CursorDown(); + + // vol up + if (ButtonPressed(padState, 1)) _menuManager.CursorUp(); + + // service + if (ButtonPressed(padState, 6)) _menuManager.CursorDown(); + + // test + if (ButtonPressed(padState, 9)) _menuManager.MenuSelect(); + + break; + case LauncherState.Launching: + // vol up (held) + _skipUpdater = ButtonHeld(_ioBoard, 1); + + // test + if (ButtonPressed(padState, 9)) MenuShow(); + + break; + case LauncherState.Error: + // test + if (ButtonPressed(padState, 9)) Application.Exit(); + + break; + } + } + } } } + private void StartLaunchTimer() + { + if (_state == LauncherState.Launching && _delayTimer != null && _delayTimer.Enabled) return; + _state = LauncherState.Launching; + _delayTimer = new System.Timers.Timer(5000); + _delayTimer.Elapsed += LaunchDefault; + + _delayTimer.Enabled = true; + } + public void MenuShow() { _delayTimer.Stop(); _loadingLabel.Hide(); menuLabel.Show(); - waccaListTest.Visible = waccaListTest.Enabled = true; - AutoLaunch = false; + _buttonLabel.Show(); + menuListBox.Visible = menuListBox.Enabled = true; + + _state = LauncherState.InMenu; } public void MenuHide() { - AutoLaunch = true; menuLabel.Hide(); - waccaListTest.Visible = waccaListTest.Enabled = false; + _buttonLabel.Hide(); + menuListBox.Visible = menuListBox.Enabled = false; - if (_hasError) return; + _menuManager.ReturnToRoot(); + + if (_state == LauncherState.Error) return; _loadingLabel.Show(); - _delayTimer = new System.Timers.Timer(5000); - _delayTimer.Elapsed += LaunchDefault; - - _delayTimer.Enabled = true; + StartLaunchTimer(); } public void MenuUpdateLabel(string text) @@ -201,40 +237,23 @@ namespace WACCALauncher menuLabel.Text = text.ToUpper(); } - private static void vfd_test() + private void MainForm_Load(object sender, EventArgs e) { - var vfd = new VFD(); - vfd.Reset(); - vfd.PowerOn(); - vfd.Brightness(VFD.Bright._50); - vfd.CanvasShift(0); - vfd.Cursor(0, 0); - vfd.FontSize(VFD.Font._16_16); - vfd.Write("Testing VFD!"); - vfd.Cursor(0, 2); - vfd.ScrollSpeed(15); - vfd.ScrollText(Math.PI.ToString() + " "); - vfd.ScrollStart(); - } - - private void Form1_Load(object sender, EventArgs e) - { - _loadingLabel = new Label(); - waccaListTest.Font = _menuFont; + menuListBox.Font = _menuFont; + SuspendLayout(); _loadingLabel.Font = _menuFont; - _loadingLabel.ForeColor = Program.IsCorrectVer() ? Color.White : Color.DarkOrange; - _loadingLabel.Location = new Point(458, 525); + _loadingLabel.ForeColor = Program.IsRecommendedWinVer() ? Color.White : Color.DarkOrange; + _loadingLabel.BackColor = Color.Transparent; + _loadingLabel.Location = new Point(0, 525); _loadingLabel.Name = "loadingLabel"; - _loadingLabel.Size = new Size(164, 30); + _loadingLabel.Size = new Size(Width, 30); _loadingLabel.TabIndex = 0; _loadingLabel.Text = "LOADING..."; _loadingLabel.TextAlign = ContentAlignment.MiddleCenter; - this.Controls.Add(_loadingLabel); - - _versionLabel = new Label(); + Controls.Add(_loadingLabel); _versionLabel.Font = _menuFont; _versionLabel.ForeColor = Color.FromArgb(50,50,50); @@ -242,467 +261,457 @@ namespace WACCALauncher _versionLabel.Name = "versionLabel"; _versionLabel.Size = new Size(164, 30); _versionLabel.TabIndex = 0; - _versionLabel.Text = Assembly.GetEntryAssembly().GetName().Version.ToString(); + _versionLabel.Text = System.Reflection.Assembly.GetEntryAssembly().GetName().Version.ToString(); _versionLabel.TextAlign = ContentAlignment.MiddleCenter; - Console.WriteLine(_versionLabel.Text); + Controls.Add(_versionLabel); - this.Controls.Add(_versionLabel); + _buttonLabel.Font = _menuFont; + _buttonLabel.ForeColor = Color.White; + _buttonLabel.BackColor = Color.Transparent; + _buttonLabel.Location = new Point(0, 900); + _buttonLabel.Name = "buttonLabel"; + _buttonLabel.Size = new Size(Width, 60); + _buttonLabel.TabIndex = 0; + _buttonLabel.Text = "Press SERVICE button to select\nPress TEST button to decide".ToUpper(); + _buttonLabel.TextAlign = ContentAlignment.MiddleCenter; + _buttonLabel.Visible = false; - LoadVersionsFromConfig(); - - var mainMenu = new ConfigMenu("Launcher Settings", items: new List() { - new ConfigMenu("one-time launch", ConfigMenuAction.Menu, items: GetOTLMenu()), - new ConfigMenu("set default version", ConfigMenuAction.Menu, items: GetDefaultVersionMenu()), - new ConfigMenu("exit to windows", ConfigMenuAction.Command, method: Application.Exit), - new ConfigMenu("launch game", ConfigMenuAction.Return) - }); + Controls.Add(_buttonLabel); - _menuManager = new MenuManager(mainMenu, waccaListTest, this); - - _loadingLabel.Font = _menuFont; menuLabel.Font = _menuFont; + + ReloadConfig(); + + var mainMenu = GetMenu(); + + #if DEBUG + mainMenu.Items.Add(GetDebugMenu()); + #endif + + _menuManager = new MenuManager(mainMenu, menuListBox, this); } - public List GetDefaultVersionMenu() + private void ReloadConfig() { - var defVerMenu = new List(); + DefaultProfile = null; + Profiles.Clear(); - foreach (var ver in Versions) - { - var name = ver.GameVersion == VersionType.Custom ? ver.CustomName : ver.ToString(); - defVerMenu.Add(new ConfigMenu($"({(ver == DefaultVer ? 'X' : ' ')}) {name}", ConfigMenuAction.VersionSelect, version: ver, defVer: true)); - } + LoadLauncherSettings(); - defVerMenu.Add(new ConfigMenu("Return to settings", ConfigMenuAction.Return)); + LoadProfiles(); - return defVerMenu; + SetDefaultProfile(); } - public List GetOTLMenu() + private Menu GetMenu() { - var otlMenu = new List(); - - foreach (var ver in Versions) + return new Menu("launcher menu", main: true, items: new List() { - var name = ver.GameVersion == VersionType.Custom ? ver.CustomName : ver.ToString(); - otlMenu.Add(new ConfigMenu(name, ConfigMenuAction.VersionSelect, version: ver, defVer: false)); + new ProfileMenu("one-time launch", type: ProfileMenuType.OneTimeLaunch), + new ProfileMenu("set default profile", type: ProfileMenuType.SetDefault), + new MenuSeparator(), + new Menu("cab management", items: new List() { + new MenuAction("launch file explorer", OpenFileExplorer), + new MenuAction("reload configs", ReloadConfig), + new MenuAction("exit to windows", Application.Exit), + new MenuSeparator(), + new MenuAction("reboot cab (!)", RebootCab) + }), + new MenuSeparator(), + new MenuReturn("launch game") + }); + } + + private Menu GetDebugMenu() + { + return new Menu("Debug Menu", items: new List() { + new MenuAction("start desktop", OpenDesktop), + new Menu("nesting 1", items: new List() + { + new Menu("nesting 11", items: new List() + { + new MenuAction("nesting 111"), + new MenuAction("nesting 112"), + new MenuAction("nesting 113") + }), + new Menu("nesting 12", items: new List() + { + new MenuAction("nesting 121"), + new MenuAction("nesting 122"), + new MenuAction("nesting 123") + }), + new Menu("nesting 13", items: new List() + { + new MenuAction("nesting 131"), + new MenuAction("nesting 132"), + new MenuAction("nesting 133") + }) + }), + new Menu("nesting 2", items: new List() + { + new Menu("nesting 21", items: new List() + { + new MenuAction("nesting 211"), + new MenuAction("nesting 212"), + new MenuAction("nesting 213") + }), + new Menu("nesting 22", items: new List() + { + new MenuAction("nesting 221"), + new MenuAction("nesting 222"), + new MenuAction("nesting 223") + }), + new Menu("nesting 23", items: new List() + { + new MenuAction("nesting 231"), + new MenuAction("nesting 232"), + new MenuAction("nesting 233") + }) + }), + new Menu("nesting 3", items: new List() + { + new Menu("nesting 31", items: new List() + { + new MenuAction("nesting 311"), + new MenuAction("nesting 312"), + new MenuAction("nesting 313") + }), + new Menu("nesting 32", items: new List() + { + new MenuAction("nesting 321"), + new MenuAction("nesting 322"), + new MenuAction("nesting 323") + }), + new Menu("nesting 33", items: new List() + { + new MenuAction("nesting 331"), + new MenuAction("nesting 332"), + new MenuAction("nesting 333") + }) + }), + GenerateLargeMenu("Large Menu", 50) + }); + } + + private Menu GenerateLargeMenu(string name, int count) + { + var menu = new Menu(name); + + menu.Items.Add(GenerateLargeBigTextMenu("really big text", 50)); + + for (int i = 0; i < count; i++) + { + menu.Items.Add(new MenuAction($"{name} {i}")); } - otlMenu.Add(new ConfigMenu("Return to settings", ConfigMenuAction.Return)); + return menu; + } - return otlMenu; + private Menu GenerateLargeBigTextMenu(string name, int count) + { + var menu = new Menu(name); + + for (int i = 0; i < count; i++) + { + menu.Items.Add(new MenuAction($"super ultra omega mega epic swag gaming WWWWWW {i}")); + } + + return menu; } private static void KillExplorer() { - Process.Start(@"C:\Windows\System32\taskkill.exe", @"/F /IM explorer.exe"); + Process.Start("taskkill.exe", "/F /IM explorer.exe"); } private static void KillAMDaemon() { - Process.Start(@"C:\Windows\System32\taskkill.exe", @"/F /IM amdaemon.exe"); + Process.Start("taskkill.exe", "/F /IM amdaemon.exe"); } - private static void OpenExplorer() + // i love hacks + private static void OpenDesktop() { - var processes = Process.GetProcessesByName("explorer"); - if (processes.Length == 0) Process.Start("explorer.exe"); - } - - public void LaunchGame(Version version) - { - Console.WriteLine("launching game"); - _gameProcess.StartInfo.FileName = version.BatchPath; - _gameProcess.EnableRaisingEvents = true; - - //this.Hide(); - _gameProcess.Exited += QuitLauncher; - _gameProcess.Start(); - } - - public void LaunchGame(VersionType type) - { - LaunchGame(Versions.Find(x => x.GameVersion == type)); - } - - public void LaunchGame(string gameId) - { - LaunchGame(Versions.Find(x => x.GameId == gameId)); - } - - private void QuitLauncher(Object source, EventArgs e) - { - // Only exit after the game has closed, so that the launcher doesn't keep opening when configured as a shell - KillAMDaemon(); // Just in case it gets left open - Application.Exit(); - } - - private void LaunchDefault(Object source, ElapsedEventArgs e) - { - _delayTimer.Stop(); - KillExplorer(); - LaunchGame(DefaultVer); - } - - private void LoadVersionsFromConfig() - { - if (_config == null) return; - foreach (VersionType item in (VersionType[])Enum.GetValues(typeof(VersionType))) + if(FindWindow("Progman", null) == IntPtr.Zero) { - var iniPath = _config["versions"][item.ToString().ToLower()]; - if (string.IsNullOrEmpty(iniPath)) continue; - Console.WriteLine($"Found path for {item.ToString().Replace('_', ' ')}: \"{iniPath}\""); - try + Process.Start("explorer.exe"); + } + } + + // more hacks, yippee + private static void OpenFileExplorer() + { + var exp = new Process(); + exp.StartInfo.FileName = "explorer.exe"; + + // force file explorer to open instead of desktop + exp.StartInfo.Arguments = "\"\""; + + exp.Start(); + } + + private static void RebootCab() + { + Process.Start("shutdown.exe", "/f /r /t 0"); + } + + private static ProcessStartInfo SetUpAmdaemon(Profile profile) + { + var si = new ProcessStartInfo(); + si.WorkingDirectory = Path.Combine(profile.GetBaseDir().FullName, "bin"); + si.WindowStyle = ProcessWindowStyle.Minimized; + si.FileName = "inject.exe"; + + var args = new string[] { + "-d -k", + profile.InjectDLL, + "amdaemon.exe", + profile.GetAmdaemonArgs() + }; + + si.Arguments = string.Join(" ", args); + + return si; + } + + private void LaunchUpdater(Profile profile) + { + if (File.Exists(profile.UpdaterPath)) + { + Invoke(new Action(() => _state = LauncherState.Updating)); + + Invoke(new Action(() => _loadingText = "CHECKING FOR UPDATES")); + + var updater = new Process(); + updater.StartInfo.FileName = profile.UpdaterPath; + updater.StartInfo.WorkingDirectory = Path.GetDirectoryName(profile.UpdaterPath).ToString(); + updater.StartInfo.Arguments = profile.UpdaterArgs; + + updater.Start(); + + updater.WaitForExit(); + + if (settings.StrictMode && updater.ExitCode != 0) { - var version = new Version(iniPath, item); - if (!version.HasSegatools && version.GameVersion != VersionType.Saturn) - { - DisplayError("Segatools missing", $"Ensure segatools is present in the bin folder ({version})"); - return; - } - if (version.BatchPath == String.Empty && version.GameVersion == VersionType.Saturn) - { - DisplayError("Saturn missing", $"Check path, cannot find saturn data"); - return; - } - Versions.Add(version); + DisplayError("Updater error", $"{profile}: updater closed with exit code {updater.ExitCode}"); } - catch (Exception ex) when (ex is NotSupportedException || ex is DirectoryNotFoundException || ex is ArgumentException) + + Invoke(new Action(() => _loadingText = "LOADING")); + } + else if (settings.StrictMode) + { + DisplayError("Updater error", $"{profile}: Could not find updater"); + } + } + + public void LaunchGame(Profile profile) + { + if (profile.UpdaterPath != null && !_skipUpdater) LaunchUpdater(profile); + + if (_state == LauncherState.Error) return; + + Invoke(new Action(() => _state = LauncherState.GameStarting)); + + Invoke(new Action(() => _loadingText = "STARTING")); + + #if !DEBUG // it's annoying to have my explorer killed all the time + KillExplorer(); + #endif + + _gameProcess.EnableRaisingEvents = true; + _gameProcess.StartInfo.FileName = profile.GetGamePath(); + _gameProcess.StartInfo.WorkingDirectory = profile.BasePath; + + switch (profile.Type) + { + case ProfileType.WACCA: { - DisplayError($"Invalid path for {item.ToString().Replace('_', ' ')}", "Check the config paths for errors and try again"); + if(profile.Configs.Count > 0) + { + _amdaemonProcess.StartInfo = SetUpAmdaemon(profile); + _amdaemonProcess.Start(); + break; + } + else + { + DisplayError("No amdaemon configs specified"); + return; + } + } + case ProfileType.Generic: + { + break; + } + default: + { + DisplayError("Invalid launch type", $"Unknown launch type \"{profile.Type}\""); return; } } - int num_customs; - if(int.TryParse(_config["general"]["num_customs"], out num_customs)) + if (profile.InjectGame) { - for (var i = 1; i < num_customs + 1; i++) { - var customVer = _config[$"custom_{i}"]; + var gamePath = _gameProcess.StartInfo.FileName; + _gameProcess.StartInfo.FileName = "inject.exe"; + _gameProcess.StartInfo.WorkingDirectory = Path.Combine(profile.GetBaseDir().FullName, "bin"); + _gameProcess.StartInfo.Arguments = string.Join(" ", new string[] { "-d -k", profile.InjectDLL, $"\"{gamePath}\"" }); + _gameProcess.StartInfo.WindowStyle = ProcessWindowStyle.Minimized; + } + else _gameProcess.StartInfo.WindowStyle = ProcessWindowStyle.Normal; - var customPath = customVer["path"]; - var customName = customVer["name"]; + _gameProcess.Start(); - if (string.IsNullOrEmpty(customPath)) continue; + Invoke(new Action(() => _state = LauncherState.GameRunning)); + + Invoke(new Action(() => _loadingText = "RUNNING")); + } + + private void LaunchDefault(object source, ElapsedEventArgs e) + { + Invoke(new Action(() => StopTimer())); + + LaunchGame(SelectedProfile ?? DefaultProfile); + } + + private void HandleGameClosed(object source, EventArgs e) + { + Invoke(new Action(() => _state = LauncherState.GameClosed)); + + // it will stay open if we don't close it + KillAMDaemon(); + + if (settings.UseWatchdog) + { + Invoke(new Action(() => { + _loadingText = "RESTARTING"; + StartLaunchTimer(); + })); + } + else Application.Exit(); + } + + private void LoadLauncherSettings() + { + try + { + settings = LauncherSettings.Load(); + } + catch (FileNotFoundException) { /* don't care, we'll just make a new one */ } + catch (JsonReaderException) + { + DisplayError("Invalid Config", "launcher.json could not be parsed, check for errors"); + } + LauncherSettings.Save(settings); + } + + private void LoadProfiles() + { + // we already loaded them + if (Profiles.Count > 0) return; + + var curDir = Environment.CurrentDirectory; + + var profileDir = new DirectoryInfo(Path.Combine(curDir, settings.ProfileDir)); + if (profileDir.Exists) + { + var verFiles = profileDir.EnumerateFiles("*.json", SearchOption.AllDirectories); + + foreach (var file in verFiles) + { try { - var version = new Version(customPath, VersionType.Custom, $"custom_{i}", customName); - if (!version.HasSegatools) - { - DisplayError("Segatools missing", $"Ensure segatools is present in the bin folder ({version})"); - return; - } - Versions.Add(version); - Console.WriteLine($"Found path for {customName}: \"{customPath}\""); + Profiles.Add(Profile.LoadFromJson(file.FullName)); } - catch (Exception ex) when (ex is NotSupportedException || ex is DirectoryNotFoundException || ex is ArgumentException) + catch (JsonReaderException) { - DisplayError($"Invalid path for {customName}", "Check the config paths for errors and try again"); - return; + if(settings.StrictMode) DisplayError("Profile Parse Error", string.Format("Unable to parse {0}, fix errors", file.Name)); + } + catch (ProfileLoadException ex) + { + DisplayError("Profile Load Error", string.Format("{0}: {1}", file.Name, ex.Message)); } } - } - + } else profileDir.Create(); - if (Versions.Count == 0) + if (Profiles.Count == 0) { - DisplayError("No versions found", "Check the config paths for errors and try again"); - } - else - { - if (_config["general"]["default_ver"] == null || _config["general"]["default_ver"] == string.Empty) - { - SetDefaultVer(Versions.First()); - } - - DefaultVer = Versions.Find(x => x.GameId == _config["general"]["default_ver"]); + DisplayError("No Profiles Found", "Ensure you have json files in profile folder"); } } - public void SetDefaultVer(Version version) + // select profile for OTL launch + public void SelectProfile(Profile profile) { - DefaultVer = version; - _config["general"]["default_ver"] = DefaultVer.GameId; - _parser.WriteFile("wacca.ini", _config); + MenuHide(); + StopTimer(); + SelectedProfile = profile; + LaunchGame(SelectedProfile); } - private bool _hasError = false; + // set configured default, set first profile entry as default if not set + public void SetDefaultProfile() + { + if (DefaultProfile != null || Profiles.Count == 0) return; + var profile = Profiles.Find(x => x.Name == settings.DefaultProfile && x.Name != string.Empty); + + if (profile == null) + { + profile = Profiles[0]; + + settings.DefaultProfile = profile.Name; + LauncherSettings.Save(settings); + } + + DefaultProfile = profile; + } + + // show an error and halt execution private void DisplayError(string error, string description = "") { - _hasError = true; - _delayTimer.Stop(); - _loadingLabel?.Hide(); + // invoke used to be able to display error even on timer thread + Invoke(new Action(() => { + if(_state == LauncherState.InMenu) MenuHide(); + _state = LauncherState.Error; + StopTimer(); + _loadingLabel?.Hide(); + })); var errorLabel = new Label(); SuspendLayout(); errorLabel.Font = _menuFont; errorLabel.ForeColor = Color.Red; - errorLabel.Location = new Point(90, 495); + errorLabel.Location = new Point(90, 480); errorLabel.AutoSize = false; errorLabel.Name = "errorLabel"; - errorLabel.Size = new Size(900, 90); + errorLabel.Size = new Size(900, 120); var errorText = new StringBuilder(); errorText.AppendLine("ERROR: " + error + "\n"); if (description != string.Empty) errorText.AppendLine(description); errorLabel.Text = errorText.ToString().ToUpper(); errorLabel.TextAlign = ContentAlignment.MiddleCenter; - Controls.Add(errorLabel); + Invoke(new Action(() => Controls.Add(errorLabel))); } - public void StopTimer() + public static void StopTimer() { _delayTimer.Stop(); } private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { - OpenExplorer(); + OpenDesktop(); } - } - public enum VersionType - { - Unknown = 0, - WACCA, - WACCA_S, - Lily, - Lily_R, - Reverse, - Offline, - Custom = 10, - Saturn - } - - public class Version - { - private readonly DirectoryInfo _dir; - - public DirectoryInfo GameDirectoryInfo => _dir; - public readonly VersionType GameVersion; - public readonly string GameId; - public readonly string CustomName; - public readonly bool HasSegatools = false; - public readonly string BatchPath = string.Empty; - - public Version(string path, VersionType version, string gameId = "", string customName = "") +#if DEBUG + protected override void OnPaint(PaintEventArgs e) { - this._dir = new DirectoryInfo(path); - if (!_dir.Exists) throw new DirectoryNotFoundException(); - - this.GameVersion = version; - this.GameId = gameId != string.Empty ? gameId : version.ToString().ToLower(); - if (customName != string.Empty) this.CustomName = customName; - var binPath = Path.Combine(_dir.FullName, "bin"); - - if (version != VersionType.Saturn) - { - if (CheckForSegatools(binPath)) - { - HasSegatools = true; - BatchPath = Path.Combine(binPath, "start.bat"); - } - } - else if (CheckForSaturn(path)) - { - BatchPath = Path.Combine(path, "SaturnGame.exe"); - } - } - - private bool CheckForSegatools(string path) - { - return File.Exists(Path.Combine(path, "segatools.ini")) && - File.Exists(Path.Combine(path, "mercuryhook.dll")) && - File.Exists(Path.Combine(path, "inject.exe")); - } - - private bool CheckForSaturn(string path) - { - return Directory.Exists(Path.Combine(path, "SaturnGame_data")) && - File.Exists(Path.Combine(path, "SaturnGame.exe")); - } - - public override string ToString() - { - return GameVersion.ToString().Replace('_', ' '); - } - } - - public enum ConfigMenuAction - { - None = 0, - Menu, - Command, - VersionSelect, - ItemSelect, - Return - } - - public class ConfigMenu - { - public string Name; - public List Items; - public ConfigMenu ParentMenu; - - private readonly ConfigMenuAction _action; - private readonly Action _method; - private readonly List _options; - private readonly Version _version; - private readonly bool _defVer; - - public void Select(MainForm form) - { - if (_action == ConfigMenuAction.Command) - { - // only works with static methods, why - this._method(); - } - else if (_action == ConfigMenuAction.Menu && Items != null) - { - form._menuManager.NavigateToSubmenu(this); - } - else if (_action == ConfigMenuAction.ItemSelect && _options != null) - { - // generate list of options and cycle through them, complicated - return; - } - else if (_action == ConfigMenuAction.VersionSelect && _version != null) - { - if(_defVer) - { - Console.WriteLine($"setting default version to {_version}"); - form.SetDefaultVer(_version); - // TODO: this is kinda jank, fix this - form._menuManager.UpdateCurrentMenuItems(form.GetDefaultVersionMenu()); - } - else - { - Console.WriteLine($"one-time launch for {_version}"); - form.MenuHide(); - form.StopTimer(); - form.LaunchGame(_version); - } - - } - else if (_action == ConfigMenuAction.Return) { form._menuManager.MenuBack(); } - } - - public ConfigMenu(string name, ConfigMenuAction action = ConfigMenuAction.None, Action method = null, List items = null, List options = null, Version version = null, bool defVer = false) - { - this.Name = name; - this._action = action; - - if (action == ConfigMenuAction.Menu && items == null) - throw new ArgumentException($"Menu item '{name}' was defined with Menu type, but has no menu associated."); - else if (action == ConfigMenuAction.Command && method == null) - throw new ArgumentException($"Menu item '{name}' was defined with Command type, but has no method associated."); - else if (action == ConfigMenuAction.ItemSelect && options == null) - throw new ArgumentException($"Menu item '{name}' was defined with ItemSelect type, but has no options associated."); - else if (action == ConfigMenuAction.VersionSelect && version == null) - throw new ArgumentException($"Menu item '{name}' was defined with VersionSelect type, but has no version associated."); - - this.Items = items; - this._method = method; - this._options = options; - this._version = version; - this._defVer = defVer; - } - - public override string ToString() - { - return Name; - } - } - - public class MenuManager - { - private ConfigMenu _rootMenu; - private ConfigMenu _currentMenu; - private WaccaList _list; - private MainForm _form; - - public MenuManager(ConfigMenu root, WaccaList list, MainForm form) - { - _rootMenu = root; - _currentMenu = _rootMenu; - _list = list; - _list.AssignMenuManager(this); - _form = form; - UpdateList(); - } - - public void CursorUp() - { - // move cursor up - if (_form.AutoLaunch) return; - - var idx = ((_list.SelectedIndex - 1) + _list.Items.Count) % _list.Items.Count; - _list.SelectedIndex = idx; - } - - public void CursorDown() - { - // move cursor down - if (_form.AutoLaunch) return; - - var idx = (_list.SelectedIndex + 1) % _list.Items.Count; - _list.SelectedIndex = idx; - } - - public void MenuBack() - { - // back from current menu item - if (_form.AutoLaunch) return; - - Console.WriteLine("MenuBack"); - if (_form._menuManager.GetCurrentMenu().ParentMenu == null) - _form.MenuHide(); - else _form._menuManager.NavigateBack(); - } - - public void MenuSelect() - { - // select menu item - if (_form.AutoLaunch) return; - - Console.WriteLine("MenuSelect"); - (_list.SelectedItem as ConfigMenu).Select(_form); - } - - public ConfigMenu GetCurrentMenu() - { - return _currentMenu; - } - - public void NavigateToSubmenu(ConfigMenu menu) - { - menu.ParentMenu = _currentMenu; - _currentMenu = menu; - UpdateList(); - } - - public void NavigateBack() - { - _currentMenu = _currentMenu.ParentMenu; - UpdateList(); - } - - public void UpdateCurrentMenuItems(List items) - { - _currentMenu.Items = items; - UpdateList(preserveIndex: true); - } - - private void UpdateList(bool preserveIndex = false) - { - var oldIndex = _list.SelectedIndex; - _list.Items.Clear(); - _list.Items.AddRange(_currentMenu.Items.ToArray()); - _form.MenuUpdateLabel(_currentMenu.Name); - if (_list.Items.Count > 0) _list.SelectedIndex = preserveIndex ? oldIndex : 0; + base.OnPaint(e); + e.Graphics.DrawEllipse(Pens.DeepPink, 10, 10, Width - 20, Height - 20); } +#endif } } diff --git a/Menu.cs b/Menu.cs new file mode 100644 index 0000000..537ae9c --- /dev/null +++ b/Menu.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; + +namespace WACCALauncher +{ + internal interface IMenuItem + { + string Name { get; } + + void Select(MainForm form); + } + + internal class MenuSeparator : IMenuItem + { + public string Name => ""; + + // this should never be called + public void Select(MainForm form) + { + return; + } + public override string ToString() + { + return ""; + } + } + + internal class MenuAction : IMenuItem + { + public string Name { get; } + private readonly Action _action; + + public MenuAction(string name, Action action = null) + { + Name = name; + _action = action; + } + + public void Select(MainForm form) + { + _action?.Invoke(); + } + + public override string ToString() + { + return Name; + } + } + + internal class MenuReturn : IMenuItem + { + public string Name { get; } + + public MenuReturn(string name = "Return") + { + Name = name; + } + + public void Select(MainForm form) + { + form._menuManager.MenuBack(); + } + + public override string ToString() + { + return Name; + } + } + + internal class Menu : IMenuItem + { + public string Name { get; } + public string Heading { get; } + public List Items { get; } = new List(); + + public bool MainMenu { get; } + + public Menu(string name, string heading = null, bool main = false) + { + Name = name; + Heading = heading ?? name; + MainMenu = main; + } + + public Menu(string name, List items, bool main = false) + { + Name = Heading = name; + Items = items; + MainMenu = main; + } + + public List GetItems() + { + if(MainMenu) return Items; + + var list = new List(); + + list.AddRange(Items); + list.Add(new MenuSeparator()); + list.Add(new MenuReturn()); + + return list; + } + + public void Select(MainForm form) + { + form._menuManager.NavigateToSubmenu(this); + } + + public override string ToString() + { + return Name; + } + } + + public enum ProfileMenuType + { + OneTimeLaunch, + SetDefault, + Editing + } + + internal class ProfileMenu : Menu + { + public ProfileMenuType MenuType { get; } + + public ProfileMenu(string name, string heading = null, ProfileMenuType type = ProfileMenuType.OneTimeLaunch) : base(name, heading) + { + heading = heading ?? "Select Profile"; + MenuType = type; + FillList(); + } + + internal void FillList() + { + if(Items.Count > 0) Items.Clear(); + + foreach (var profile in MainForm.Profiles) + { + Items.Add(new MenuProfileItem(profile, MenuType)); + } + } + } + + internal class MenuProfileItem : IMenuItem + { + public string Name { get => ProfileData.Name; } + + public ProfileMenuType MenuType { get; } + + public Profile ProfileData { get; } + + public MenuProfileItem(Profile profile, ProfileMenuType type) + { + ProfileData = profile; + MenuType = type; + } + + public void Select(MainForm form) + { + // TODO: implement profile config toggle editing interface + switch(MenuType) + { + case ProfileMenuType.OneTimeLaunch: + form.SelectProfile(ProfileData); + break; + case ProfileMenuType.SetDefault: + form.settings.DefaultProfile = ProfileData.Name; + LauncherSettings.Save(form.settings); + + MainForm.DefaultProfile = ProfileData; + + form._menuManager.RefreshList(); + break; + } + } + + public override string ToString() + { + switch (MenuType) + { + case ProfileMenuType.SetDefault: + return $"[{(MainForm.DefaultProfile == ProfileData ? 'X' : ' ')}] {Name}"; + default: + return Name; + } + } + } +} diff --git a/MenuManager.cs b/MenuManager.cs new file mode 100644 index 0000000..0fa1379 --- /dev/null +++ b/MenuManager.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; + +namespace WACCALauncher +{ + internal class MenuManager + { + private Menu _currentMenu; + private Stack> _menuLevels = new Stack>(); + private WaccaList _list; + private MainForm _form; + + public MenuManager(Menu root, WaccaList list, MainForm form) + { + _currentMenu = root; + _list = list; + _list.AssignMenuManager(this); + _form = form; + UpdateList(); + } + + public void CursorUp() + { + // move cursor up + var idx = ((_list.SelectedIndex - 1) + _list.Items.Count) % _list.Items.Count; + _list.SelectedIndex = idx; + if (_list.Items[idx] is MenuSeparator) CursorUp(); + _list.Invalidate(); + } + + public void CursorDown() + { + // move cursor down + var idx = (_list.SelectedIndex + 1) % _list.Items.Count; + _list.SelectedIndex = idx; + if (_list.Items[idx] is MenuSeparator) CursorDown(); + _list.Invalidate(); + } + + public void MenuBack() + { + // back from current menu item + if (_menuLevels.Count < 1) + { + _form._state = LauncherState.Launching; + _form.MenuHide(); + if (MainForm.DefaultProfile == null) _form.SetDefaultProfile(); + } + else NavigateBack(); + } + + public void MenuSelect() + { + // select menu item + (_list.SelectedItem as IMenuItem).Select(_form); + } + + public Menu GetCurrentMenu() + { + return _currentMenu; + } + + public void NavigateToSubmenu(Menu menu) + { + _menuLevels.Push(new Tuple(_currentMenu, _list.SelectedIndex)); + _currentMenu = menu; + if (menu is ProfileMenu profileMenu) profileMenu.FillList(); + UpdateList(); + } + + public void NavigateBack() + { + var back = _menuLevels.Pop(); + _currentMenu = back.Item1; + UpdateList(back.Item2); + } + + public void ReturnToRoot() + { + while(_menuLevels.Count > 0) NavigateBack(); + } + + private void UpdateList(int idx = 0) + { + _list.Items.Clear(); + _list.Items.AddRange(_currentMenu.GetItems().ToArray()); + _form.MenuUpdateLabel(_currentMenu.Heading); + if (_list.Items.Count > 0) _list.SelectedIndex = idx; + } + + internal void RefreshList() + { + _list.Invalidate(); + } + } +} diff --git a/Profile.cs b/Profile.cs new file mode 100644 index 0000000..d30aa96 --- /dev/null +++ b/Profile.cs @@ -0,0 +1,130 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; + +namespace WACCALauncher +{ + public enum ProfileType + { + Generic, + WACCA + } + + public class ProfileLoadException : Exception + { + public ProfileLoadException(string message) : base(message) { } + } + + [JsonObject] + public class Profile + { + [JsonIgnore] + public string ConfigPath; + + [JsonProperty, JsonRequired] + public string Name { get; set; } + + [JsonProperty, JsonRequired] + public ProfileType Type { get; set; } + + [JsonProperty, JsonRequired] + public string BasePath; + + [JsonProperty, JsonRequired] + public string GamePath; + + [JsonProperty] + public string UpdaterPath; + + [JsonProperty] + public string UpdaterArgs; + + public DirectoryInfo GetBaseDir() + { + return new DirectoryInfo(BasePath); + } + + public string GetGamePath() + { + return Path.Combine(BasePath, GamePath); + } + + // for WACCA type + + [JsonProperty] + public List Configs { get; set; } = new List(); + + [JsonProperty] + public bool InjectGame = true; + + [JsonProperty] + public string InjectDLL = "mercuryhook.dll"; + + public string GetAmdaemonArgs() + { + return string.Format("-f -c {0}", string.Join(" ", Configs)); + } + + // static methods + + public static Profile LoadFromJson(string path) + { + var loaded = JsonConvert.DeserializeObject(File.ReadAllText(path)); + loaded.ConfigPath = path; + + if (!Directory.Exists(loaded.BasePath)) + { + throw new ProfileLoadException("Base dir not found"); + } + else if (!File.Exists(loaded.GetGamePath())) + { + throw new ProfileLoadException("Game file not found"); + } + + if (loaded.Type == ProfileType.WACCA) + { + var binPath = Path.Combine(loaded.BasePath, "bin"); + if (!CheckAmdaemonConfigs(binPath, loaded.Configs)) + { + throw new ProfileLoadException("configs missing, check config and files"); + } + else if (!CheckForInjector(binPath, loaded.InjectDLL)) + { + throw new ProfileLoadException("Segatools missing, check config and files"); + } + } + + return loaded; + } + + public void SaveToJson() + { + File.WriteAllText(ConfigPath, JsonConvert.SerializeObject(this, Formatting.Indented)); + } + + private static bool CheckForInjector(string path, string dll) + { + return File.Exists(Path.Combine(path, dll)) && + File.Exists(Path.Combine(path, "segatools.ini")) && + File.Exists(Path.Combine(path, "inject.exe")); + } + + public static bool CheckAmdaemonConfigs(string path, List configs) + { + int missingFiles = 0; + + foreach (var cfg in configs) + { + if (!File.Exists(Path.Combine(path, cfg))) missingFiles++; + } + + return missingFiles == 0; + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/Program.cs b/Program.cs index e6c11f1..5fc5ffb 100644 --- a/Program.cs +++ b/Program.cs @@ -1,10 +1,6 @@ using Microsoft.Win32; using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; using System.IO; -using System.Linq; using System.Reflection; using System.Windows.Forms; @@ -18,21 +14,29 @@ namespace WACCALauncher public static Screen CurrentScreen = Screen.PrimaryScreen; - public static bool IsCorrectRes() + public static bool IsCorrectRes(Screen selected = null) { - return CurrentScreen.Bounds.Width == 1080 - && CurrentScreen.Bounds.Height == 1920; + var screen = selected ?? CurrentScreen; + + // 1080p vertical resolution + var correctPixelRes = screen.Bounds.Width == 1080 + && screen.Bounds.Height == 1920; + + // 9:16 aspect ratio check + var correctAspect = (screen.Bounds.Width / screen.Bounds.Height) == 0.5625; + + return correctPixelRes || correctAspect; } - public static bool IsCorrectVer() + public static bool IsRecommendedWinVer() { - // ensures Enterprise 2016 LTSB is used + // Enterprise 2016 LTSB is recommended, for various reasons return BuildNumber == 14393 && ReleaseId == 1607; } - public static bool IsCorrectEnv() + public static bool IsRecommendedEnv() { - return IsCorrectRes() && IsCorrectVer(); + return IsCorrectRes() && IsRecommendedWinVer(); } /// @@ -41,9 +45,17 @@ namespace WACCALauncher [STAThread] private static void Main() { - // for testing - if(Screen.AllScreens.Length > 1) - CurrentScreen = Screen.AllScreens[1]; + // if we have multiple screens, try to start on one with proper resolution, if any + if (Screen.AllScreens.Length > 1) + { + foreach (Screen screen in Screen.AllScreens) + { + if (IsCorrectRes(screen)) + { + CurrentScreen = screen; + } + } + } // when running as system shell, we must set our working directory manually var whereAmI = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index dbddbca..b28d78a 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -8,9 +7,9 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("WACCALauncher")] [assembly: AssemblyDescription("me when i launch my wacca")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("YellowberryHN")] +[assembly: AssemblyCompany("Yellowberry")] [assembly: AssemblyProduct("WACCALauncher")] -[assembly: AssemblyCopyright("Copyright © YellowberryHN 2024")] +[assembly: AssemblyCopyright("Copyright © Yellowberry 2025")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -32,5 +31,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.11.0.0")] -[assembly: AssemblyFileVersion("0.11.0.0")] +[assembly: AssemblyVersion("0.12.0.0")] +[assembly: AssemblyFileVersion("0.12.0.0")] diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs index 628e4f3..b3700db 100644 --- a/Properties/Resources.Designer.cs +++ b/Properties/Resources.Designer.cs @@ -60,6 +60,26 @@ namespace WACCALauncher.Properties { } } + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Dark { + get { + object obj = ResourceManager.GetObject("Dark", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon Light { + get { + object obj = ResourceManager.GetObject("Light", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// diff --git a/Properties/Resources.resx b/Properties/Resources.resx index b439fe7..5092427 100644 --- a/Properties/Resources.resx +++ b/Properties/Resources.resx @@ -118,6 +118,12 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\Dark.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\Light.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\font.ttf;System.Byte[], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 diff --git a/README.md b/README.md index 9249632..9e723ed 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,22 @@ -# WACCA Launcher +# WACCALauncher Allows you to run multiple versions of WACCA from the same drive. +Also allows you to run arbitrary programs as well. -## Configuration +## Recommended Folder Structure You'll want a directory structure something like this: ``` WACCA/ +├── _profiles/ +│ ├── WACCA.json +│ ├── Reverse.json +│ ├── OSD.json +│ └── ... ├── WACCALauncher.exe -├── wacca.ini +├── launcher.json +├── ... └── Versions/ ├── WACCA/ │ ├── bin/ @@ -22,51 +29,116 @@ WACCA/ └── Omega Supermix Deluxe ``` -Each version must have segatools already configured, with a `start.bat` file present. +## Profiles -You'll want to create a `wacca.ini` file next to the launcher that looks something like this: +A profile is an entry in the list of profiles displayed in the launcher. +It lets you define launch options for whatever you're launching. -``` -[general] -default_ver = reverse +Profiles are .json files, you can have as many profiles in your profiles folder as you want. +One limitation to be aware of is if you have a profile that shares a name with another one, +weird things happen. But I'm not sure why you would want that anyway. -[versions] -wacca = C:\WACCA\Versions\WACCA -wacca_s = C:\WACCA\Versions\WACCA S -lily = C:\WACCA\Versions\Lily -lily_r = C:\WACCA\Versions\Lily R -reverse = C:\WACCA\Versions\Reverse -offline = C:\WACCA\Versions\Offline +Profiles are validated when the launcher starts, and will throw an error if the configuration is invalid, +or if the configured files or folders do not exist. + +The filename of a profile is only used for sorting, so you can sort your profiles however you wish. + +There are currently only 2 valid types of profiles: +- `WACCA`, meant for launching WACCA versions +- `Generic`, meant for launching arbitrary programs + +### WACCA Profiles + +A WACCA profile contains information about where key game files are, and how to launch the game. + +Each WACCA profile must have segatools already configured in the `bin` folder. + +Here's an example of what a WACCA Reverse profile would look like: + +```json +{ + "Name": "Reverse", + "Type": "WACCA", + "BasePath": "C:\\WACCA\\Versions\\Reverse", + "GamePath": "WindowsNoEditor\\Mercury\\Binaries\\Win64\\Mercury-Win64-Shipping.exe", + "Configs": [ + "config.json", + "config_lan_install_server.json", + "config_region_jpn.json" + ] +} ``` -Versions can be omitted, but all paths provided must be valid. +### Generic Profiles -If you have a custom version of the game you wish to launch, you may do so by adding -`num_customs = 1` under `[general]`, and then adding a block at the end of the file -that looks something like this: +A generic profile can be used to launch pretty much any program. -``` -[custom_1] -name = WACCA Omega Supermix Deluxe -path = C:\WACCA\Versions\Omega Supermix Deluxe -type = reverse +Here's an example of a profile you can use to launch Portal 2, for some reason: + +```json +{ + "Name": "Portal 2 (why?)", + "Type": "Generic", + "BasePath": "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Portal 2", + "GamePath": "portal2.exe" +} ``` -`type` must match one of the versions listed above, it will likely be `reverse` unless specified. +### Updaters + +Profiles have optional support for an update executable/script to be run before running the game itself. + +Updaters can be added to any type of profile, and have a few specific behaviors to look out for. Updaters +are run from the directory they live in, not from the `BasePath` directory, for consistency reasons. + +To add an updater to a profile, simply provide the full path to the updater in the `UpdaterPath` key. +If you need to pass arguments to the updater, you may use the `UpdaterArgs` key, but this may be omitted +if not needed. + +```json +{ + "Name": "Omega Supermix Deluxe", + "Type": "WACCA", + "BasePath": "C:\\WACCA\\Versions\\Omega Supermix Deluxe", + "GamePath": "WindowsNoEditor\\Mercury\\Binaries\\Win64\\Mercury-Win64-Shipping.exe", + "Configs": [ + "config.json", + "config_lan_install_server.json", + "config_region_jpn.json" + ], + "UpdaterPath": "C:\\WACCA\\Versions\\Omega Supermix Deluxe\\bin\\updater\\wupdate.exe", + "UpdaterArgs": "-t OSD" +} +``` + +You may skip the updater for the default profile by holding the Volume Up button while launching. +The `...` will turn into `!!!` while you are holding it. ## Usage When you start the launcher, you will be presented with a loading prompt. -You may press `TEST` or Esc to open the configuration menu. This menu allows you -to change the default startup version, as well as launch a version manually. -This menu is controlled in the same way you would navigate the in-game test menu. +You may press the `TEST` button or Esc to open the launcher menu. +This menu allows you to change the default startup profile, as well as launch a profile manually. +It is controlled in the same way you would navigate the in-game test menu. -If you do not interfere with the loading prompt, the selected default version +You can also perform various tasks helpful for cab management, like open File Explorer, reload profiles, +or reboot your cab without a hard power cycle. + +If you do not interfere with the loading prompt, the selected default profile will be launched within 5 seconds. +## Watchdog + +By default, the process watchdog is enabled. WACCALauncher runs in the background while you are playing, +and if it detects the game closed in some way, it will clean up and attempt to open the game again, +with the same 5-second grace period as the initial launch. You may interrupt this by going into the +launcher menu, as before. + +If this behavior is undesired, it can be disabled by setting `UseWatchdog` to false in `launcher.json`. + ## Auto-launch -You can configure your cab to start the launcher instead of explorer.exe when you log in. +You can configure your cab to start the launcher instead of the Windows Desktop when you log in. **This should only be done on a cab. You have been warned.** If you've installed the launcher in the suggested location of `C:\WACCA`, @@ -78,7 +150,9 @@ softlocked on a black screen at next boot!** ## Notice -This code was never intended to be public, as such, it is a bit of a dumpster fire. +This code was originally never intended to be public, as such, it is a bit of a dumpster fire. + +As of version 0.12, I've cleaned it up a bit, but there's still a lot of work to be done. If you want to help make the code a little less awful, issues and pull requests are welcome. ## License diff --git a/Resources/Dark.ico b/Resources/Dark.ico new file mode 100644 index 0000000..0bc0556 Binary files /dev/null and b/Resources/Dark.ico differ diff --git a/Resources/Light.ico b/Resources/Light.ico new file mode 100644 index 0000000..b837c1c Binary files /dev/null and b/Resources/Light.ico differ diff --git a/WACCALauncher.csproj b/WACCALauncher.csproj index 0692d85..b7210b4 100644 --- a/WACCALauncher.csproj +++ b/WACCALauncher.csproj @@ -37,9 +37,12 @@ 4 false + + Resources\Dark.ico + - - packages\ini-parser.2.5.2\lib\net20\INIFileParser.dll + + packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll packages\SharpDX.4.2.0\lib\net45\SharpDX.dll @@ -58,18 +61,22 @@ + + + Form MainForm.cs + + Component - MainForm.cs @@ -104,6 +111,8 @@ PreserveNewest + + diff --git a/WACCALauncherTemplating.pdn b/WACCALauncherTemplating.pdn deleted file mode 100644 index 68727db..0000000 Binary files a/WACCALauncherTemplating.pdn and /dev/null differ diff --git a/WaccaList.cs b/WaccaList.cs index 93c3b38..5326c10 100644 --- a/WaccaList.cs +++ b/WaccaList.cs @@ -1,11 +1,5 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; namespace WACCALauncher @@ -14,31 +8,158 @@ namespace WACCALauncher { private MenuManager menuManager; + private Brush bgBrush; + private Brush textBrush; + private Brush highlightBrush; + + private PointF[] upArrow { get; } + private PointF[] downArrow { get; } + public WaccaList() { + SetStyle( + ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | + ControlStyles.UserPaint, + true); + DrawMode = DrawMode.OwnerDrawFixed; + BackColor = Color.Black; + ForeColor = Color.White; BorderStyle = BorderStyle.None; - DrawMode = DrawMode.OwnerDrawVariable; ItemHeight = 40; Size = new Size(680, 600); Location = new Point(200, 240); + + upArrow = new PointF[] { + new PointF(Bounds.Width - 30, 30), + new PointF(Bounds.Width - 20, 10), + new PointF(Bounds.Width - 10, 30) + }; + + downArrow = new PointF[] { + new PointF(Bounds.Width - 30, Bounds.Height - 30), + new PointF(Bounds.Width - 20, Bounds.Height - 10), + new PointF(Bounds.Width - 10, Bounds.Height - 30) + }; + + bgBrush = new SolidBrush(BackColor); + textBrush = new SolidBrush(ForeColor); + highlightBrush = Brushes.Red; } - public void AssignMenuManager(MenuManager manager) + // hide scrollbar, we draw our own + protected override CreateParams CreateParams + { + get + { + CreateParams cp = base.CreateParams; + cp.Style = cp.Style & ~0x200000; + return cp; + } + } + + [System.Runtime.InteropServices.DllImport("user32.dll")] + private static extern int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + + private const int LB_GETTOPINDEX = 0x018E; + + private int GetTopIndex() + { + return SendMessage(Handle, LB_GETTOPINDEX, IntPtr.Zero, IntPtr.Zero); + } + + private int GetBottomIndex() + { + int topIdx = GetTopIndex(); + + int itemsVisible = ClientSize.Height / 40; + int bottomIndex = topIdx + itemsVisible - 1; + + return Math.Min(bottomIndex, Items.Count - 1); + } + + private bool AtTop + { + get => GetTopIndex() == 0; + } + + private bool AtBottom + { + get => GetBottomIndex() == Items.Count - 1; + } + + private bool Scrollable + { + get => !(AtTop && AtBottom); + } + + internal void AssignMenuManager(MenuManager manager) { menuManager = manager; } protected override void OnDrawItem(DrawItemEventArgs e) { - e.Graphics.FillRectangle(new SolidBrush(BackColor), e.Bounds); - if (Items.Count < 1 || e.Index < 0) return; - var text = Items[e.Index].ToString().ToUpper(); + var item = Items[e.Index]; + var bounds = e.Bounds; + + e.Graphics.FillRectangle(bgBrush, bounds); + + var text = item.ToString().ToUpper(); var selected = (e.State & DrawItemState.Selected) == DrawItemState.Selected; - e.Graphics.DrawString(text, Font, selected ? Brushes.Red : Brushes.White, e.Bounds); + if (Scrollable) + { + bounds = new Rectangle(bounds.X, bounds.Y, bounds.Width - 40, bounds.Height); + } + + e.Graphics.DrawString(text, Font, selected ? highlightBrush : textBrush, bounds, new StringFormat() { Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap }); + + if (!AtTop && GetTopIndex() == e.Index) + { + e.Graphics.FillPolygon(textBrush, upArrow); + } + if (!AtBottom && GetBottomIndex() == e.Index) + { + e.Graphics.FillPolygon(textBrush, downArrow); + } + + base.OnDrawItem(e); + } + + protected override void OnPaint(PaintEventArgs e) + { + Region iRegion = new Region(e.ClipRectangle); + e.Graphics.FillRegion(bgBrush, iRegion); + if (Items.Count > 0) + { + for (int i = 0; i < Items.Count; ++i) + { + Rectangle irect = GetItemRectangle(i); + if (e.ClipRectangle.IntersectsWith(irect)) + { + OnDrawItem(new DrawItemEventArgs( + e.Graphics, + Font, + irect, i, + SelectedIndex == i ? DrawItemState.Selected : DrawItemState.Default, + ForeColor, + BackColor + )); + iRegion.Complement(irect); + } + } + } + + base.OnPaint(e); + } + + protected override void OnSelectedValueChanged(EventArgs e) + { + base.OnSelectedValueChanged(e); } } } diff --git a/WaccaVFD.cs b/WaccaVFD.cs deleted file mode 100644 index 6a173de..0000000 --- a/WaccaVFD.cs +++ /dev/null @@ -1,445 +0,0 @@ -using System; -using System.Text; -using System.IO.Ports; -using System.Security.Policy; -using System.Drawing; -using System.Drawing.Imaging; -using static System.Net.Mime.MediaTypeNames; -using System.Linq; -using System.Data.Common; -using System.Runtime.InteropServices; -using System.Drawing.Drawing2D; -using System.ComponentModel; -using System.Globalization; - -namespace WACCA -{ - // - // Summary: - // Represents an ordered pair of integer x- and y-coordinates that defines a point - // in a two-dimensional plane. - [Serializable] - [TypeConverter(typeof(PointConverter))] - [ComVisible(true)] - public struct VFDPoint - { - public static readonly VFDPoint Empty; - - private short x; - - private byte y; - - - [Browsable(false)] - public bool IsEmpty - { - get - { - if (x == 0) - { - return y == 0; - } - - return false; - } - } - - public short X - { - get - { - return x; - } - set - { - x = value; - } - } - - public byte Y - { - get - { - return y; - } - set - { - y = value; - } - } - - public VFDPoint(short x, byte y) - { - this.x = x; - this.y = y; - } - - public VFDPoint(Size sz) - { - x = (short)sz.Width; - y = (byte)sz.Height; - } - - public static explicit operator Size(VFDPoint p) - { - return new Size(p.X, p.Y); - } - - public static VFDPoint operator +(VFDPoint pt, Size sz) - { - return Add(pt, sz); - } - - public static VFDPoint operator -(VFDPoint pt, Size sz) - { - return Subtract(pt, sz); - } - - public static bool operator ==(VFDPoint left, VFDPoint right) - { - if (left.X == right.X) - { - return left.Y == right.Y; - } - - return false; - } - - public static bool operator !=(VFDPoint left, VFDPoint right) - { - return !(left == right); - } - - public static VFDPoint Add(VFDPoint pt, Size sz) - { - return new VFDPoint((short)(pt.X + sz.Width), (byte)(pt.Y + sz.Height)); - } - - public static VFDPoint Subtract(VFDPoint pt, Size sz) - { - return new VFDPoint((short)(pt.X - sz.Width), (byte)(pt.Y - sz.Height)); - } - - public override bool Equals(object obj) - { - if (!(obj is VFDPoint)) - { - return false; - } - - VFDPoint point = (VFDPoint)obj; - if (point.X == X) - { - return point.Y == Y; - } - - return false; - } - - public override int GetHashCode() - { - return x ^ y; - } - - public void Offset(short dx, byte dy) - { - X += dx; - Y += dy; - } - - public void Offset(VFDPoint p) - { - Offset(p.X, p.Y); - } - - public override string ToString() - { - return "{X=" + X.ToString(CultureInfo.CurrentCulture) + ",Y=" + Y.ToString(CultureInfo.CurrentCulture) + "}"; - } - } - - class VFD - { - SerialPort port; - public Lang language { get; private set; } = Lang.SIMP_CHINESE; - public Font font { get; private set; } = Font._16_16; - public Bright brightness { get; private set; } = Bright._100; - public bool power { get; private set; } = false; - - /// - /// Establish a connection to a VFD, and prepare it for use. - /// - /// The port that the VFD connected to - public VFD(string portName = "COM2") - { - port = new SerialPort(portName, 115200); - port.Open(); - Reset(); - } - - private void VFD_Write(byte number) - { - Console.WriteLine(BitConverter.ToString(new byte[] { number })); - port.Write(new byte[] { number }, 0, 1); - } - - private void VFD_Write(byte[] bytes) - { - Console.WriteLine(BitConverter.ToString(bytes)); - port.Write(bytes, 0, bytes.Length); - } - - private void VFD_Write(string text) - { - // Get correct encoding for current language - int codeNumber; - switch(language) - { - case Lang.SIMP_CHINESE: - codeNumber = 936; // GB2312 - break; - case Lang.TRAD_CHINESE: - codeNumber = 950; // Big5 - break; - case Lang.JAPANESE: - codeNumber = 932; // Shift-JIS - break; - case Lang.KOREAN: - codeNumber = 949; // KSC5601 - break; - default: - codeNumber = 932; - break; - } - - // Convert Unicode string to encoded bytes - Encoding unicodeEncoding = Encoding.Unicode; - Encoding correctEncoding = Encoding.GetEncoding(codeNumber); - byte[] unicodeBytes = unicodeEncoding.GetBytes(text); - byte[] encodedBytes = Encoding.Convert(unicodeEncoding, correctEncoding, unicodeBytes); - - Console.WriteLine(BitConverter.ToString(correctEncoding.GetBytes(text))); - port.Write(encodedBytes, 0, encodedBytes.Length); - } - - /* - #define LEFT_HI(x) (((x) & 0x100) >> 8) - #define LEFT_LO(x) ((x) & 0xFF) - - #define FTB_PORT_WRITE_LEFT(x) {FTB_PORT.write(LEFT_HI(x)); FTB_PORT.write(LEFT_LO(x));} - */ - - private void VFD_WriteShort(short x) - { - byte hi = (byte)(((x) & 0x100) >> 8); - byte lo = (byte)((x) & 0xFF); - VFD_Write(new byte[] {hi, lo}); - } - - public void Write(string text) - { - VFD_Write(text); - } - - public void Reset() - { - VFD_Write(new byte[] { 0x1B, 0x0B }); - } - - public void Clear() - { - VFD_Write(new byte[] { 0x1B, 0x0C }); - } - - public void TestPayload() // fucked - { - VFD_Write(Encoding.ASCII.GetString(new byte[] { 0x1b, 0x0c, 0x1b, 0x52, 0x1b, 0x40, 0x00, 0x00, 0x00, 0x00, 0x9f, 0x02, 0x1b, 0x41, 0x00, 0x1b, 0x50, 0x50, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x1b, 0x51})); - } - - public enum Bright { - _0 = 0, - _25 = 1, - _50 = 2, - _75 = 3, - _100 = 4 - } - - public void Brightness(Bright brightness) - { - VFD_Write(new byte[] { 0x1B, 0x20, (byte)brightness }); - } - - public void PowerOn() - { - Power(true); - } - - public void PowerOff() - { - Power(false); - } - - private void Power(bool on) - { - VFD_Write(new byte[] { 0x1B, 0x21, (byte)(on ? 0x01 : 0x00) }); - } - - public void CanvasShift(short left) - { - VFD_Write(new byte[] { 0x1B, 0x22 }); - VFD_WriteShort(left); - } - - public void Cursor(short left, byte top) - { - VFD_Write(new byte[] { 0x1B, 0x30 }); - VFD_WriteShort(left); - VFD_Write(top); - } - - public enum Lang { - SIMP_CHINESE, - TRAD_CHINESE, - JAPANESE, - KOREAN - } - - public void Language(Lang lang) - { - language = lang; - VFD_Write(new byte[] { 0x1B, 0x32, (byte)language }); - } - - public enum Font - { - _16_16, - _6_8 - } - - public void FontSize(Font size) - { - // 3 - font = size; - VFD_Write(new byte[] { 0x1B, 0x33, (byte)size }); - } - - public void CreateScrollBox(short left, byte top, short width, byte height) - { - VFD_Write(new byte[] { 0x1B, 0x40 }); - VFD_WriteShort(left); - VFD_Write(top); - VFD_WriteShort(width); - VFD_Write(height); - } - - public void ScrollSpeed(byte divisor) - { - VFD_Write(new byte[] { 0x1B, 0x41, (byte)divisor }); - } - - public void ScrollText(string text) - { - if (text.Length >= 0x100) throw new ArgumentOutOfRangeException("Text is too long."); - VFD_Write(new byte[] { 0x1B, 0x50, (byte)text.Length }); - VFD_Write(text); - } - - public void ScrollStart() - { - VFD_Write(new byte[] { 0x1B, 0x51 }); - } - - public void ScrollStop() - { - VFD_Write(new byte[] { 0x1B, 0x52 }); - } - - public enum BlinkMode - { - Off = 0, - Invert = 1, - All = 2 - } - - public void BlinkSet(BlinkMode blink, byte interval) - { - VFD_Write(new byte[] { 0x1B, 0x23, (byte)blink, interval }); - } - - public void ClearLine(byte line) - { - Cursor(0, line); - VFD_Write("".PadLeft(20)); - Cursor(0, line); - } - - public void DrawBitmap(Bitmap bmp, Point origin) - { - if (bmp.PixelFormat != PixelFormat.Format1bppIndexed) - throw new ArgumentException("Provided bitmap is not monochrome"); - - // We have to do it this way because of a GDI+ bug - bmp.RotateFlip(RotateFlipType.Rotate270FlipNone); - RotateNoneFlipYMono(bmp); - - Rectangle bounds = new Rectangle(new Point(), bmp.Size); - - var data = bmp.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format1bppIndexed); - - VFD_Write("\x1B\x2E"); - VFD_WriteShort((short)origin.X); - VFD_Write((byte)origin.Y); - VFD_WriteShort((short)bmp.Height); // Inverted because image was flipped - VFD_Write((byte)((bmp.Width / 8)-1)); - - int bytes = ( bmp.Width * bmp.Height ) / 8; - - // Create a byte array to hold the pixel data - byte[] pixelData = new byte[bytes]; - - // Copy the data from the pointer to the byte array - Marshal.Copy(data.Scan0, pixelData, 0, bytes); - - VFD_Write(pixelData); - - bmp.UnlockBits(data); - } - - private static void RotateNoneFlipYMono(Bitmap bmp) - { - if (bmp == null || bmp.PixelFormat != PixelFormat.Format1bppIndexed) - throw new ArgumentException("Provided bitmap is not monochrome"); - - var height = bmp.Height; - var width = bmp.Width; - // width in dwords - var stride = (width + 31) >> 5; - // total image size - var size = stride * height; - // alloc storage for pixels - var bytes = new int[size]; - - // get image pixels - var rect = new Rectangle(Point.Empty, bmp.Size); - var bd = bmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format1bppIndexed); - Marshal.Copy(bd.Scan0, bytes, 0, size); - - // flip by swapping dwords - int halfSize = size >> 1; - for (int y1 = 0, y2 = size - stride; y1 < halfSize; y1 += stride, y2 -= stride) - { - int end = y1 + stride; - for (int x1 = y1, x2 = y2; x1 < end; x1++, x2++) - { - bytes[x1] ^= bytes[x2]; - bytes[x2] ^= bytes[x1]; - bytes[x1] ^= bytes[x2]; - } - } - - // copy pixels back - Marshal.Copy(bytes, 0, bd.Scan0, size); - bmp.UnlockBits(bd); - } - } -} diff --git a/app.config b/app.config index ff99501..a826409 100644 --- a/app.config +++ b/app.config @@ -1,3 +1,6 @@ - + + + + diff --git a/packages.config b/packages.config index b801fba..b3b7f92 100644 --- a/packages.config +++ b/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file