diff --git a/LauncherSettings.cs b/LauncherSettings.cs index 4982928..d036f99 100644 --- a/LauncherSettings.cs +++ b/LauncherSettings.cs @@ -18,6 +18,11 @@ namespace WACCALauncher [JsonProperty] public bool StrictMode { get; set; } = true; [JsonProperty] public string ProfileDir { get; set; } = "_profiles"; [JsonProperty] public bool DisableIO4 { get; set; } = false; + [JsonProperty] public bool LegacyIO4 { get; set; } = false; + + [JsonProperty] public bool DisableLights { get; set; } = false; + + [JsonProperty] public bool DisableGameKill { get; set; } = false; public static LauncherSettings Load() { diff --git a/MainForm.cs b/MainForm.cs index 6d7df93..91f0075 100644 --- a/MainForm.cs +++ b/MainForm.cs @@ -28,7 +28,7 @@ namespace WACCALauncher private static System.Timers.Timer _delayTimer; private readonly System.Windows.Forms.Timer _t = new System.Windows.Forms.Timer(); - public LauncherState _state; + public LauncherState State; private static Font _menuFont; @@ -39,8 +39,8 @@ namespace WACCALauncher private static Label _buttonLabel = new Label(); // legacy SharpInput IO4 - private readonly DirectInput _input = new DirectInput(); - private Joystick _ioBoard; + private readonly DirectInput _directInput = new DirectInput(); + private Joystick _io4Legacy; // LilyConsole hardware control private readonly LightController _consoleLights = new LightController(); @@ -50,7 +50,7 @@ namespace WACCALauncher public static Profile DefaultProfile; public Profile SelectedProfile; - private bool _skipUpdater = false; + private bool _skipUpdater; private readonly Process _gameProcess = new Process(); private readonly Process _amdaemonProcess = new Process(); @@ -59,6 +59,12 @@ namespace WACCALauncher internal LauncherSettings settings; + private const IO4ButtonState GameKillKeys = IO4ButtonState.VolumeUp | IO4ButtonState.VolumeDown | + IO4ButtonState.Service | IO4ButtonState.Test; + + private static string menuButtonLabel = "Press SERVICE button to select\nPress TEST button to decide"; + private static string errorButtonLabel = "Press TEST button to exit"; + [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); @@ -67,19 +73,32 @@ namespace WACCALauncher _menuFont = FontLoader.LoadFont(); InitializeComponent(); - - // start input timer + + LoadLauncherSettings(); + + // used for legacy input and UI state input _t.Tick += Tick; _t.Interval = 20; _t.Start(); - + // if we're running on a vertical monitor, compensate for ring offset if (Program.IsCorrectRes()) { var bounds = Program.CurrentScreen.Bounds; SetBounds(bounds.X, bounds.Y + 362, Width, Height); } + + if (!settings.DisableLights) + { + _consoleLights.Initialize(); + _consoleLights.Clear(); + } + if (!settings.DisableIO4 && !settings.LegacyIO4) + { + RegisterIO4Events(); + } + // bind game exit to be handled properly (ONLY ONCE) _gameProcess.Exited += HandleGameClosed; @@ -87,37 +106,61 @@ namespace WACCALauncher StartLaunchTimer(); } + // UI thread public void UpdateLoadingText() { _loadingLabel.Text = string.Join("",_loadingText,_skipUpdater ? "!!!" : "..."); } + // UI thread protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { - if (_state == LauncherState.Launching || _state == LauncherState.InMenu) + switch (State) { - switch(keyData) + case LauncherState.InMenu: { - 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(); + switch (keyData) + { + case Keys.Up: + case Keys.Left: + _menuManager.CursorUp(); + return true; + case Keys.Down: + case Keys.Right: + _menuManager.CursorDown(); + return true; + case Keys.Enter: + case Keys.F2: + _menuManager.MenuSelect(); + return true; + case Keys.Escape: + case Keys.F1: + _menuManager.MenuBack(); + return true; + } - return true; + break; + } + case LauncherState.Launching: + { + switch (keyData) + { + case Keys.U: + _skipUpdater = !_skipUpdater; + return true; + case Keys.Escape: + case Keys.F1: + MenuShow(); + return true; + } + + break; + } + case LauncherState.Error when keyData == Keys.Escape || keyData == Keys.F1: + { + Application.Exit(); + return true; } - } - else if (_state == LauncherState.Error && keyData == Keys.Escape) - { - Application.Exit(); - return true; } return base.ProcessCmdKey(ref msg, keyData); @@ -150,86 +193,211 @@ namespace WACCALauncher return true; } + // timer thread private void Tick(object sender, EventArgs e) { UpdateLoadingText(); - // set up IO board controls if enabled - if (!settings.DisableIO4) + // handle IO board controls if enabled + if (!settings.DisableIO4 && settings.LegacyIO4) { - if (_ioBoard == null) - { - var gamepads = _input.GetDevices(DeviceClass.GameControl, DeviceEnumerationFlags.AttachedOnly); - - // 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 - { - // TODO: make this behave safer - _ioBoard.Poll(); - var padStates = _ioBoard.GetBufferedData(); - - foreach (var padState in padStates) - { - if(padState.Offset >= JoystickOffset.Buttons0 - && padState.Offset < JoystickOffset.Buttons10) - { - var pressed = padState.Value > 0; - - 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; - case LauncherState.GameRunning: - if (ButtonsHeld(_ioBoard,new[] {0, 1, 6, 9})) _gameProcess.Kill(); - - break; - } - } - } - } + LegacyIO4Handler(sender, e); } } + private void SetIO4Color(LightColor color) + { + if (InvokeRequired) + { + BeginInvoke(new MethodInvoker(() => SetIO4Color(color))); + return; + } + + if(settings.DisableIO4 || settings.DisableLights) return; + _io4.SetColor(color); + } + + private void SetConsolePattern(LightFrame frame) + { + if (InvokeRequired) + { + BeginInvoke(new MethodInvoker(() => SetConsolePattern(frame))); + return; + } + + if (settings.DisableLights) return; + _consoleLights.SendLightFrame(frame); + } + + // timer thread + private void LegacyIO4Handler(object sender, EventArgs e) + { + if (_io4Legacy == null) + { + var gamepads = _directInput.GetDevices(DeviceClass.GameControl, DeviceEnumerationFlags.AttachedOnly); + if (gamepads.Count == 0) return; + + // it will be the only gamepad on the system + var guid = gamepads[0].InstanceGuid; + _io4Legacy = new Joystick(_directInput, guid); + _io4Legacy.Properties.BufferSize = 128; + _io4Legacy.Acquire(); + } + else + { + // TODO: make this behave safer + _io4Legacy.Poll(); + var padStates = _io4Legacy.GetBufferedData(); + + foreach (var padState in padStates) + { + if(padState.Offset >= JoystickOffset.Buttons0 + && padState.Offset < JoystickOffset.Buttons10) + { + var pressed = padState.Value > 0; + + 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(_io4Legacy, 1); + + // test + if (ButtonPressed(padState, 9)) MenuShow(); + + break; + case LauncherState.Error: + // test + if (ButtonPressed(padState, 9)) Application.Exit(); + + break; + case LauncherState.GameRunning: + if (ButtonsHeld(_io4Legacy,new[] {0, 1, 6, 9}) && !settings.DisableGameKill) + _gameProcess.Kill(); + + break; + } + } + } + } + } + + private void RegisterIO4Events() + { + if (!_io4.Initialized) + { + _io4.Initialize(); + SetIO4Color(LightColor.White); + } + + _io4.ButtonPressed += OnIO4ButtonPressed; + _io4.ButtonStateChanged += OnIO4ButtonStateChanged; + } + + // held buttons + private void OnIO4ButtonStateChanged(IO4ButtonState btnState) + { + BeginInvoke(new MethodInvoker(() => + { + if (IsDisposed || Disposing) return; + + switch (State) + { + case LauncherState.Launching: + _skipUpdater = (btnState & IO4ButtonState.VolumeUp) != 0; + break; + case LauncherState.GameRunning: + if ((btnState & GameKillKeys) == GameKillKeys && !settings.DisableGameKill) + { + SetIO4Color(LightColor.Red); + _gameProcess.Kill(); + } + break; + } + })); + } + + // pressed buttons + private void OnIO4ButtonPressed(IO4ButtonState btnState) + { + BeginInvoke(new MethodInvoker(() => + { + if (IsDisposed || Disposing) return; + + switch (State) + { + case LauncherState.InMenu: + if((btnState & IO4ButtonState.VolumeDown) != 0) _menuManager.CursorDown(); + if((btnState & IO4ButtonState.VolumeUp) != 0) _menuManager.CursorUp(); + if((btnState & IO4ButtonState.Service) != 0) _menuManager.CursorDown(); + if((btnState & IO4ButtonState.Test) != 0) _menuManager.MenuSelect(); + break; + case LauncherState.Launching: + if ((btnState & IO4ButtonState.Test) != 0) MenuShow(); + break; + case LauncherState.Error: + if ((btnState & IO4ButtonState.Test) != 0) Application.Exit(); + break; + } + })); + } + + // UI thread private void StartLaunchTimer() { - if (_state == LauncherState.Launching && _delayTimer != null && _delayTimer.Enabled) return; - _state = LauncherState.Launching; + if (State == LauncherState.Launching && _delayTimer != null && _delayTimer.Enabled) return; + State = LauncherState.Launching; _delayTimer = new System.Timers.Timer(5000); _delayTimer.Elapsed += LaunchDefault; _delayTimer.Enabled = true; + + SetProfileColor(SelectedProfile ?? DefaultProfile); + SetProfilePattern(SelectedProfile ?? DefaultProfile); } + private void SetProfileColor(Profile profile) + { + var lightColor = LightColor.White; + try + { + if (profile?.PanelColor != null) + { + var color = ColorTranslator.FromHtml(profile?.PanelColor); + lightColor = new LightColor(color.R, color.G, color.B, color.A); + } + } + catch (Exception) + { + lightColor = LightColor.Off; + } + + SetIO4Color(lightColor); + } + + private void SetProfilePattern(Profile profile) + { + if (profile?.PatternLightFrame != null) + { + SetConsolePattern(profile.PatternLightFrame); + } + } + + // UI thread public void MenuShow() { _delayTimer.Stop(); @@ -238,9 +406,13 @@ namespace WACCALauncher _buttonLabel.Show(); menuListBox.Visible = menuListBox.Enabled = true; - _state = LauncherState.InMenu; + SetIO4Color(LightColor.White); + _consoleLights.Clear(); + + State = LauncherState.InMenu; } + // UI thread public void MenuHide() { menuLabel.Hide(); @@ -249,18 +421,20 @@ namespace WACCALauncher _menuManager.ReturnToRoot(); - if (_state == LauncherState.Error) return; + if (State == LauncherState.Error) return; _loadingLabel.Show(); StartLaunchTimer(); } + // UI thread public void MenuUpdateLabel(string text) { menuLabel.Text = text.ToUpper(); } + // UI thread private void MainForm_Load(object sender, EventArgs e) { menuListBox.Font = _menuFont; @@ -268,7 +442,7 @@ namespace WACCALauncher SuspendLayout(); _loadingLabel.Font = _menuFont; - _loadingLabel.ForeColor = Program.IsRecommendedWinVer() ? Color.White : Color.DarkOrange; + _loadingLabel.ForeColor = Program.IsRecommendedWinVer() ? Color.White : Color.Yellow; _loadingLabel.BackColor = Color.Transparent; _loadingLabel.Location = new Point(0, 525); _loadingLabel.Name = "loadingLabel"; @@ -297,7 +471,7 @@ namespace WACCALauncher _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.Text = menuButtonLabel.ToUpper(); _buttonLabel.TextAlign = ContentAlignment.MiddleCenter; _buttonLabel.Visible = false; @@ -316,6 +490,7 @@ namespace WACCALauncher _menuManager = new MenuManager(mainMenu, menuListBox, this); } + // UI thread private void ReloadConfig() { DefaultProfile = null; @@ -326,6 +501,9 @@ namespace WACCALauncher LoadProfiles(); SetDefaultProfile(); + + SetProfileColor(SelectedProfile ?? DefaultProfile); + SetProfilePattern(SelectedProfile ?? DefaultProfile); } private Menu GetMenu() @@ -414,7 +592,63 @@ namespace WACCALauncher new MenuAction("nesting 333") }) }), - GenerateLargeMenu("Large Menu", 50) + GenerateLargeMenu("Large Menu", 50), + new Menu("Light testing", items: new List() + { + new Menu("Panel Lights", items: new List() + { + new MenuAction("Red", () => SetIO4Color(LightColor.Red)), + new MenuAction("Green", () => SetIO4Color(LightColor.Green)), + new MenuAction("Blue", () => SetIO4Color(LightColor.Blue)), + new MenuAction("White", () => SetIO4Color(LightColor.White)), + new MenuAction("Scarlet", () => SetIO4Color(new LightColor(255, 36, 0))), + new MenuAction("Off", () => SetIO4Color(LightColor.Black)) + }), + new Menu("Console Lights", items: new List() + { + new MenuAction("Red", () => SetConsolePattern(new LightFrame(LightColor.Red))), + new MenuAction("Green", () => SetConsolePattern(new LightFrame(LightColor.Green))), + new MenuAction("Blue", () => SetConsolePattern(new LightFrame(LightColor.Blue))), + new MenuAction("White", () => SetConsolePattern(new LightFrame(LightColor.White))), + new MenuAction("Scarlet", () => SetConsolePattern(new LightFrame(new LightColor(255, 36, 0)))), + new MenuAction("Off", () => _consoleLights.Clear()) + }), + }), + new Menu("New menu items", items: new List() + { + new MenuListOption("Test guy", f => Console.WriteLine($"float: {f}"), new Tuple, List>( + new List() + { + "Float 1", + "Float 2", + "Float 3", + "Float 4" + }, + new List() + { + 1.45f, + 15.4f, + 87.1104f, + 2013.1484f + } + )), + new MenuListOption("Test guy 2", s => Console.WriteLine($"string: {s}"), new Tuple, List>( + new List() + { + "String 1", + "String 2", + "String 3", + "String 4" + }, + new List() + { + "we", + "really", + "out", + "here" + } + )) + }) }); } @@ -495,9 +729,11 @@ namespace WACCALauncher { if (File.Exists(profile.UpdaterPath)) { - Invoke(new Action(() => _state = LauncherState.Updating)); - - Invoke(new Action(() => _loadingText = "CHECKING FOR UPDATES")); + BeginInvoke(new MethodInvoker(() => + { + State = LauncherState.Updating; + _loadingText = "CHECKING FOR UPDATES"; + })); var updater = new Process(); updater.StartInfo.FileName = profile.UpdaterPath; @@ -513,7 +749,7 @@ namespace WACCALauncher DisplayError("Updater error", $"{profile}: updater closed with exit code {updater.ExitCode}"); } - Invoke(new Action(() => _loadingText = "LOADING")); + BeginInvoke(new MethodInvoker(() => _loadingText = "LOADING")); } else if (settings.StrictMode) { @@ -525,11 +761,13 @@ namespace WACCALauncher { if (profile.UpdaterPath != null && !_skipUpdater) LaunchUpdater(profile); - if (_state == LauncherState.Error) return; + if (State == LauncherState.Error) return; - Invoke(new Action(() => _state = LauncherState.GameStarting)); - - Invoke(new Action(() => _loadingText = "STARTING")); + BeginInvoke(new MethodInvoker(() => + { + State = LauncherState.GameStarting; + _loadingText = "STARTING"; + })); #if !DEBUG // it's annoying to have my explorer killed all the time KillExplorer(); @@ -563,6 +801,8 @@ namespace WACCALauncher } } + BeginInvoke(new MethodInvoker(() => _consoleLights.Close(false))); + if (profile.InjectGame && profile.Type == ProfileType.WACCA) { var gamePath = _gameProcess.StartInfo.FileName; @@ -575,21 +815,30 @@ namespace WACCALauncher _gameProcess.Start(); - Invoke(new Action(() => _state = LauncherState.GameRunning)); - - Invoke(new Action(() => _loadingText = "RUNNING")); + BeginInvoke(new MethodInvoker(() => + { + State = LauncherState.GameRunning; + _loadingText = "RUNNING"; + })); } private void LaunchDefault(object source, ElapsedEventArgs e) { - Invoke(new Action(() => StopTimer())); + Invoke(new MethodInvoker(StopTimer)); LaunchGame(SelectedProfile ?? DefaultProfile); } private void HandleGameClosed(object source, EventArgs e) { - Invoke(new Action(() => _state = LauncherState.GameClosed)); + // if we are in a different thread, call this on the UI thread instead + if (InvokeRequired) + { + BeginInvoke(new MethodInvoker(() => HandleGameClosed(source, e))); + return; + } + + State = LauncherState.GameClosed; // it will stay open if we don't close it try @@ -600,13 +849,13 @@ namespace WACCALauncher { // amdaemon wasn't running } - + if (settings.UseWatchdog) { - Invoke(new Action(() => { - _loadingText = "RESTARTING"; - StartLaunchTimer(); - })); + if (!settings.DisableLights) _consoleLights.Initialize(); + + _loadingText = "RESTARTING"; + StartLaunchTimer(); } else Application.Exit(); } @@ -620,7 +869,7 @@ namespace WACCALauncher 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"); + DisplayError("Invalid Config", "launcher.json could not be parsed, check for json errors"); } LauncherSettings.Save(settings); } @@ -641,15 +890,21 @@ namespace WACCALauncher { try { - Profiles.Add(Profile.LoadFromJson(file.FullName)); + var profileObj = Profile.LoadFromJson(file.FullName); + if (profileObj != null && Profiles.Find(x => x.Name == profileObj.Name) != null) + { + throw new ProfileLoadException($"Duplicate profile name \"{profileObj.Name}\""); + } + Profiles.Add(profileObj); } catch (JsonReaderException) { - if(settings.StrictMode) DisplayError("Profile Parse Error", string.Format("Unable to parse {0}, fix errors", file.Name)); + if(settings.StrictMode) DisplayError("Profile Parse Error", + $"Unable to parse {file.Name}, fix json errors"); } catch (ProfileLoadException ex) { - DisplayError("Profile Load Error", string.Format("{0}: {1}", file.Name, ex.Message)); + DisplayError("Profile Load Error", $"{file.Name}: {ex.Message}"); } } } else profileDir.Create(); @@ -691,13 +946,17 @@ namespace WACCALauncher // show an error and halt execution private void DisplayError(string error, string description = "") { - // 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(); - })); + // if we are in a different thread, call this on the UI thread instead + if (InvokeRequired) + { + BeginInvoke(new Action(() => DisplayError(error, description))); + return; + } + + if(State == LauncherState.InMenu) MenuHide(); + State = LauncherState.Error; + StopTimer(); + _loadingLabel?.Hide(); var errorLabel = new Label(); SuspendLayout(); @@ -714,7 +973,10 @@ namespace WACCALauncher errorLabel.Text = errorText.ToString().ToUpper(); errorLabel.TextAlign = ContentAlignment.MiddleCenter; - Invoke(new Action(() => Controls.Add(errorLabel))); + Controls.Add(errorLabel); + + _buttonLabel.Text = errorButtonLabel.ToUpper(); + _buttonLabel.Show(); } public static void StopTimer() @@ -724,6 +986,16 @@ namespace WACCALauncher private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { + if (_io4.Initialized) + { + _io4.ButtonPressed -= OnIO4ButtonPressed; + _io4.Close(); + } + + if (_consoleLights.Initialized) + { + _consoleLights.Close(); + } OpenDesktop(); } diff --git a/Menu.cs b/Menu.cs index 537ae9c..6c67229 100644 --- a/Menu.cs +++ b/Menu.cs @@ -47,6 +47,82 @@ namespace WACCALauncher } } + internal interface IControlledMenuItem : IMenuItem + { + void Forward(); + void Back(); + } + + internal class MenuListOption : IControlledMenuItem + { + private Tuple, List> _items; + + private int _index; + private int _maxIndex; + private readonly Action _selectAction; + private readonly Action _changedAction; + + private bool _isActive; + + public MenuListOption(string name, Action selectAction, Tuple, List> items, Action changedAction = null) + { + Name = name; + _selectAction = selectAction; + _changedAction = changedAction; + + if (items.Item1.Count != items.Item2.Count) + { + throw new ArgumentException("Lists in item tuple must be the same length"); + } + + _items = items; + _maxIndex = _items.Item1.Count; + } + + public string Name { get; } + + public void Select(MainForm form) + { + if (!_isActive) + { + form._menuManager.TakeControl(this); + _isActive = true; + IndexChanged(); + } + else + { + // run action with argument based on currently selected index + _selectAction?.Invoke(_items.Item2[_index]); + form._menuManager.ReturnControl(); + _isActive = false; + } + } + + public void Forward() + { + var newIdx = (_index + 1) % _maxIndex; + _index = newIdx; + IndexChanged(); + } + + public void Back() + { + var newIdx = (_index - 1 + _maxIndex) % _maxIndex; + _index = newIdx; + IndexChanged(); + } + + private void IndexChanged() + { + _changedAction?.Invoke(_items.Item2[_index]); + } + + public override string ToString() + { + return $"{Name}\t{_items.Item1[_index]}"; + } + } + internal class MenuReturn : IMenuItem { public string Name { get; } diff --git a/MenuManager.cs b/MenuManager.cs index fe9c1c7..9082d28 100644 --- a/MenuManager.cs +++ b/MenuManager.cs @@ -9,6 +9,8 @@ namespace WACCALauncher private Stack> _menuLevels = new Stack>(); private WaccaList _list; private MainForm _form; + + internal IControlledMenuItem _controlledItem; public MenuManager(Menu root, WaccaList list, MainForm form) { @@ -21,14 +23,30 @@ namespace WACCALauncher public void CursorUp() { + // scroll options + if (_controlledItem != null) + { + _controlledItem.Back(); + RefreshList(); + return; + } + // move cursor up - var idx = ((_list.SelectedIndex - 1) + _list.Items.Count) % _list.Items.Count; + var idx = (_list.SelectedIndex - 1 + _list.Items.Count) % _list.Items.Count; _list.SelectedIndex = idx; if (_list.Items[idx] is MenuSeparator) CursorUp(); } public void CursorDown() { + // scroll options + if (_controlledItem != null) + { + _controlledItem.Forward(); + RefreshList(); + return; + } + // move cursor down var idx = (_list.SelectedIndex + 1) % _list.Items.Count; _list.SelectedIndex = idx; @@ -40,7 +58,7 @@ namespace WACCALauncher // back from current menu item if (_menuLevels.Count < 1) { - _form._state = LauncherState.Launching; + _form.State = LauncherState.Launching; _form.MenuHide(); if (MainForm.DefaultProfile == null) _form.SetDefaultProfile(); } @@ -50,7 +68,7 @@ namespace WACCALauncher public void MenuSelect() { // select menu item - (_list.SelectedItem as IMenuItem).Select(_form); + (_list.SelectedItem as IMenuItem)?.Select(_form); } public Menu GetCurrentMenu() @@ -80,8 +98,7 @@ namespace WACCALauncher private void UpdateList(int idx = 0) { - _list.Items.Clear(); - _list.Items.AddRange(_currentMenu.GetItems().ToArray()); + _list.DataSource = _currentMenu.GetItems(); _form.MenuUpdateLabel(_currentMenu.Heading); if (_list.Items.Count > 0) _list.SelectedIndex = idx; } @@ -90,5 +107,17 @@ namespace WACCALauncher { _list.Invalidate(); } + + internal void TakeControl(IControlledMenuItem item) + { + _controlledItem = item; + RefreshList(); + } + + internal void ReturnControl() + { + _controlledItem = null; + RefreshList(); + } } } diff --git a/Profile.cs b/Profile.cs index 9bb4243..b0a3702 100644 --- a/Profile.cs +++ b/Profile.cs @@ -1,8 +1,10 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.Drawing; using System.IO; using System.Linq; +using LilyConsole; namespace WACCALauncher { @@ -42,7 +44,9 @@ namespace WACCALauncher public string UpdaterArgs; [JsonProperty] - public string ConsoleColor; + public string ConsolePattern; + + public LightFrame PatternLightFrame; [JsonProperty] public string PanelColor; @@ -114,6 +118,11 @@ namespace WACCALauncher } } + if (loaded.ConsolePattern != null && File.Exists(Path.Combine(loaded.BasePath, loaded.ConsolePattern))) + { + loaded.SetLightFrame(); + } + return loaded; } @@ -140,6 +149,36 @@ namespace WACCALauncher return missingFiles == 0; } + + private void SetLightFrame() + { + if (ConsolePattern == null) return; + + var lightFrame = new LightFrame(LightColor.Red); + + try + { + var patternBmp = new Bitmap(Path.Combine(BasePath, ConsolePattern)); + var lightLayer = new LightLayer(); + + for (byte y = 0; y < 4; y++) + { + for (byte x = 0; x < 60; x++) + { + var pixel = patternBmp.GetPixel(x, y); + lightLayer[x, y] = new LightColor(pixel.R, pixel.G, pixel.B, pixel.A); + } + } + + lightFrame.layers[0] = lightLayer; + } + catch (Exception) + { + /* we don't care */ + } + + PatternLightFrame = lightFrame; + } public override string ToString() { diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 7fea0d5..b2c16e7 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Yellowberry")] [assembly: AssemblyProduct("WACCALauncher")] -[assembly: AssemblyCopyright("Copyright © Yellowberry 2025")] +[assembly: AssemblyCopyright("Copyright © Yellowberry 2026")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -31,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.12.0.1")] -[assembly: AssemblyFileVersion("0.12.0.1")] +[assembly: AssemblyVersion("0.13.0.0")] +[assembly: AssemblyFileVersion("0.13.0.0")] diff --git a/README.md b/README.md index 9e723ed..8419eb4 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Also allows you to run arbitrary programs as well. You'll want a directory structure something like this: ``` -WACCA/ +C:/WACCA/ ├── _profiles/ │ ├── WACCA.json │ ├── Reverse.json @@ -35,8 +35,8 @@ 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. 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. +One limitation to be aware of is if you have a profile that has the same `Name` as another one, +it will throw an error. But I'm not sure why you would want that anyway. 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. @@ -114,6 +114,16 @@ if not needed. 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. +### Advanced Features + +You can provide a color for the lower panel with `PanelColor`, in a valid HTML color code, +to be used during loading. If an invalid color was provided, the panel lights will be turned off. +If a color is not provided, it will default to a white color. + +You can provide the `BasePath` relative path of an image (.png) that is sized 60px x 4px to be shown +on the WACCA console while starting with `ConsolePattern`. The top pixels are closest to the screen, +wrapped counter-clockwise starting from 12 o'clock, one pixel per touch segment. + ## Usage When you start the launcher, you will be presented with a loading prompt. @@ -124,9 +134,12 @@ It is controlled in the same way you would navigate the in-game test menu. 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 +If you do not interrupt the loading prompt, the selected default profile will be launched within 5 seconds. +If you want to close the game, and go back to the launcher, press all four IO4 buttons at once, +`Volume Up`/`Down` + `TEST` + `SERVICE`. This can be disabled in `launcher.json`. + ## Watchdog By default, the process watchdog is enabled. WACCALauncher runs in the background while you are playing, @@ -139,7 +152,8 @@ If this behavior is undesired, it can be disabled by setting `UseWatchdog` to fa ## Auto-launch 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.** + +***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`, you can run `SetShell.reg` to reconfigure your registry accordingly. diff --git a/WACCALauncher.csproj b/WACCALauncher.csproj index 2de8061..13d23c4 100644 --- a/WACCALauncher.csproj +++ b/WACCALauncher.csproj @@ -41,21 +41,12 @@ Resources\Dark.ico - - packages\HidSharp.2.6.4\lib\net35\HidSharp.dll - - - packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll - - - packages\SharpDX.4.2.0\lib\net45\SharpDX.dll - - - packages\SharpDX.DirectInput.4.2.0\lib\net45\SharpDX.DirectInput.dll - lib\LilyConsole.dll + + lib\HidSharp.dll + @@ -97,7 +88,6 @@ True - SettingsSingleFileGenerator Settings.Designer.cs @@ -120,6 +110,11 @@ + + + + + diff --git a/WaccaList.cs b/WaccaList.cs index 5326c10..9c2f301 100644 --- a/WaccaList.cs +++ b/WaccaList.cs @@ -108,15 +108,23 @@ namespace WACCALauncher e.Graphics.FillRectangle(bgBrush, bounds); - var text = item.ToString().ToUpper(); + var text = item.ToString().ToUpper().Split('\t'); var selected = (e.State & DrawItemState.Selected) == DrawItemState.Selected; + if (selected && item is MenuSeparator) text[0] = "- boo -"; // shows up if you select a separator via mouse 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 (item is IControlledMenuItem) + { + if (item == menuManager._controlledItem && selected) selected = false; + e.Graphics.DrawString(text[1], Font, item == menuManager._controlledItem ? highlightBrush : textBrush, bounds, new StringFormat() { Alignment = StringAlignment.Far }); + } + + e.Graphics.DrawString(text[0], Font, selected ? highlightBrush : textBrush, bounds, new StringFormat() { Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap }); + if (!AtTop && GetTopIndex() == e.Index) { @@ -156,10 +164,10 @@ namespace WACCALauncher base.OnPaint(e); } - - protected override void OnSelectedValueChanged(EventArgs e) + + protected override void OnDoubleClick(EventArgs e) { - base.OnSelectedValueChanged(e); + menuManager.MenuSelect(); } } } diff --git a/lib/HidSharp.LICENSE.txt b/lib/HidSharp.LICENSE.txt new file mode 100644 index 0000000..a1ac2de --- /dev/null +++ b/lib/HidSharp.LICENSE.txt @@ -0,0 +1,192 @@ +HIDSharp +Copyright 2010-2025 James F. Bellinger + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/lib/HidSharp.dll b/lib/HidSharp.dll new file mode 100644 index 0000000..1d1d9ef Binary files /dev/null and b/lib/HidSharp.dll differ diff --git a/lib/LilyConsole.dll b/lib/LilyConsole.dll new file mode 100644 index 0000000..877bfc1 Binary files /dev/null and b/lib/LilyConsole.dll differ diff --git a/packages.config b/packages.config deleted file mode 100644 index c264464..0000000 --- a/packages.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file