## 🚀 Added (New Features)

• Quick Swap Feature:
      • Users can now bind keyboard keys (both single keys and multi-key combinations, e.g.,  Alt+1 ) to switch to
      specific operator sets.
      • Auto-toggle capability: Pressing the hotkey switches to the target operator set (automatically changing side
      if the operator is on standard Attackers/Defenders side). Pressing it again toggles back to the baseline
      operator set.
      • Added specificity filtering: Resolves overlapping hotkeys (e.g., pressing  Alt+1  will only trigger the  Alt+1
      binding, successfully ignoring the individual  1  key binding).
      • Added a dedicated Quick Swap Bindings editor inside the Settings panel to configure, save, and reload these
      bindings visually.

  ## ⚙️ Updated (Refactoring & Architecture)

  • Graceful Thread Shutdown: Updated RecoilEngine.cs to run with a  CancellationToken  linked to the main window
  lifespan, matching HotkeyHandler.cs and preventing background thread leaks when exiting.
  • State Management: Updated  OperatorSets  methods to protect the quick swap transition state, ensuring manual
  adjustments (clicks or next/prev hotkeys) reset the quick-swap toggle baseline.
  • Config Serialization: Updated  MainWindow  configuration generator to save new quick-swap parameters cleanly back
  to  init.config .

  ## 🛠️ Fixed (Bugs resolved)

  • TabControl/Menu Text Colors: Fixed the styling on MainWindow.xaml tab headers. Unselected and hover states now
  render in clear light greys, and active tabs in solid white, resolving dark-on-dark invisible text bugs.
  • Dropdown Selection Visibility: Changed the text foreground color of  TextColorComboBox  in MainWindow.xaml from
  black to white to make choices readable in dark mode.
  • UI Console Text Color: Bound the console setting ( textColor ) to the actual WPF GUI log ( ConsoleOutput.
  Foreground ), making the chosen color reflect in the application window.
  ──────
  ### 📂 File-Level Changes Summary

  • helper.cs: Added helper.cs to safely parse combinations.
  • ConfigLoader.cs: Added ConfigLoader.cs class and loading logic.
  • OperatorSets.cs: Added OperatorSets.cs and state-tracking flags.
  • HotkeyHandler.cs: Registered hotkey monitoring loops for multi-key patterns.
  • RecoilEngine.cs: Integrated thread lifecycle cancellation.
  • MainWindow.xaml: Styled menus, fixed dropdown contrast, and added Quick Swap bindings textbox.
  • MainWindow.xaml.cs: Connected UI controls to backend quick-swap actions and applied dynamic log coloring.
This commit is contained in:
Trenten Pettry
2026-06-15 03:55:58 -04:00
parent d6d8a60b20
commit 7d6bdc592f
13 changed files with 646 additions and 106 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"dotnet.preferCSharpExtension": true
}
+1 -1
View File
@@ -1,7 +1,7 @@
<Application x:Class="Lithium.App" <Application x:Class="Lithium.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml"> StartupUri="../MainWindow.xaml">
<Application.Resources> <Application.Resources>
</Application.Resources> </Application.Resources>
</Application> </Application>
+13 -1
View File
@@ -1,10 +1,19 @@
using System; using System;
using System.IO; using System.IO;
namespace Lua namespace Lua
{ {
public class QuickSwapBinding
{
public int[] Keys { get; set; }
public string TargetOperator { get; set; }
public bool IsHeld { get; set; }
}
internal static class ConfigLoader internal static class ConfigLoader
{ {
public static System.Collections.Generic.List<QuickSwapBinding> quickSwaps = new System.Collections.Generic.List<QuickSwapBinding>();
// AA0001 // AA0001
public static bool debitFlag1 = false; public static bool debitFlag1 = false;
// AA0002 // AA0002
@@ -58,6 +67,7 @@ namespace Lua
jitterBurstTime = 1800; jitterBurstTime = 1800;
jitterCooldown = 3000; jitterCooldown = 3000;
textColor = ConsoleColor.Magenta; textColor = ConsoleColor.Magenta;
quickSwaps.Clear();
SetConsoleColor(); SetConsoleColor();
@@ -108,6 +118,8 @@ namespace Lua
// AA0026 // AA0026
else if (Helper.ParseConsoleColorFromAssignment(line, "textColor", out var col)) else if (Helper.ParseConsoleColorFromAssignment(line, "textColor", out var col))
textColor = col; textColor = col;
else if (Helper.ParseQuickSwap(line, out var qsKeys, out var qsOp))
quickSwaps.Add(new QuickSwapBinding { Keys = qsKeys, TargetOperator = qsOp });
} }
// AA0027 // AA0027
+111 -16
View File
@@ -1,4 +1,5 @@
using Lua; using Lua;
using System;
using System.Threading; using System.Threading;
namespace Lithium namespace Lithium
@@ -13,24 +14,49 @@ namespace Lithium
// AA0043 // AA0043
private static MainWindow _mainWindow; private static MainWindow _mainWindow;
// AA0044 // AA0044 - Added: Cancellation token for graceful shutdown
public static void Start(MainWindow window) private static CancellationTokenSource _cancellationSource;
// AA0045 - Updated: Accept cancellation token
public static void Start(MainWindow window, CancellationToken cancellationToken = default)
{ {
_mainWindow = window; _mainWindow = window;
_cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// AA0045 try
while (true)
{ {
HandleToggleCapsLock(); while (!_cancellationSource.Token.IsCancellationRequested)
HandleNextSet(); {
HandlePrevSet(); try
HandleRefresh(); {
HandleSwitchSide(); HandleToggleCapsLock();
Thread.Sleep(8); HandleNextSet();
HandlePrevSet();
HandleRefresh();
HandleSwitchSide();
HandleQuickSwaps();
Thread.Sleep(8);
}
catch (Exception ex)
{
// Log error but continue running
System.Diagnostics.Debug.WriteLine($"HotkeyHandler error: {ex.Message}");
}
}
}
finally
{
_cancellationSource?.Dispose();
} }
} }
// AA0046 // AA0046 - Static method to stop the handler gracefully
public static void Stop()
{
_cancellationSource?.Cancel();
}
// AA0047
private static void HandleToggleCapsLock() private static void HandleToggleCapsLock()
{ {
bool capsDown = Utils.IsPressed(CAPSLOCK); bool capsDown = Utils.IsPressed(CAPSLOCK);
@@ -42,7 +68,7 @@ namespace Lithium
if (!capsDown) _capsHeld = false; if (!capsDown) _capsHeld = false;
} }
// AA0047 // AA0048
private static void HandleNextSet() private static void HandleNextSet()
{ {
if (Utils.IsPressed(ConfigLoader.nextSetKey) && !_nextSetHeld) if (Utils.IsPressed(ConfigLoader.nextSetKey) && !_nextSetHeld)
@@ -56,7 +82,7 @@ namespace Lithium
} }
} }
// AA0048 // AA0049
private static void HandlePrevSet() private static void HandlePrevSet()
{ {
if (Utils.IsPressed(ConfigLoader.prevSetKey) && !_prevSetHeld) if (Utils.IsPressed(ConfigLoader.prevSetKey) && !_prevSetHeld)
@@ -70,7 +96,7 @@ namespace Lithium
} }
} }
// AA0049 // AA0050
private static void HandleRefresh() private static void HandleRefresh()
{ {
if (Utils.IsPressed(ConfigLoader.refreshKey) && !_refreshHeld) if (Utils.IsPressed(ConfigLoader.refreshKey) && !_refreshHeld)
@@ -84,7 +110,7 @@ namespace Lithium
} }
} }
// AA0050 // AA0051
private static void HandleSwitchSide() private static void HandleSwitchSide()
{ {
if (Utils.IsPressed(ConfigLoader.switchSideKey) && !_switchSideHeld) if (Utils.IsPressed(ConfigLoader.switchSideKey) && !_switchSideHeld)
@@ -97,5 +123,74 @@ namespace Lithium
_switchSideHeld = false; _switchSideHeld = false;
} }
} }
private static void HandleQuickSwaps()
{
if (ConfigLoader.quickSwaps == null || ConfigLoader.quickSwaps.Count == 0) return;
// Find all bindings that are currently fully pressed
var activeBindings = new System.Collections.Generic.List<Lua.QuickSwapBinding>();
foreach (var qs in ConfigLoader.quickSwaps)
{
bool allPressed = true;
foreach (var k in qs.Keys)
{
if (!Lua.Utils.IsPressed(k))
{
allPressed = false;
break;
}
}
if (allPressed)
{
activeBindings.Add(qs);
}
}
// Filter out bindings that are subsets of other active bindings
var triggeredBindings = new System.Collections.Generic.List<Lua.QuickSwapBinding>();
foreach (var ab in activeBindings)
{
bool hasSuperset = false;
foreach (var other in activeBindings)
{
if (other != ab && other.Keys.Length > ab.Keys.Length)
{
bool isSubset = true;
foreach (var k in ab.Keys)
{
bool found = false;
foreach (var ok in other.Keys)
{
if (ok == k) { found = true; break; }
}
if (!found) { isSubset = false; break; }
}
if (isSubset) { hasSuperset = true; break; }
}
}
if (!hasSuperset)
{
triggeredBindings.Add(ab);
}
}
// Update held status and trigger actions
foreach (var qs in ConfigLoader.quickSwaps)
{
if (triggeredBindings.Contains(qs))
{
if (!qs.IsHeld)
{
qs.IsHeld = true;
_mainWindow.HandleQuickSwap(qs.TargetOperator);
}
}
else
{
qs.IsHeld = false;
}
}
}
} }
} }
+24
View File
@@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lithium", "Lithium.csproj", "{B20F7FF2-33B9-1A60-831A-DE88D5488463}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B20F7FF2-33B9-1A60-831A-DE88D5488463}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B20F7FF2-33B9-1A60-831A-DE88D5488463}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B20F7FF2-33B9-1A60-831A-DE88D5488463}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B20F7FF2-33B9-1A60-831A-DE88D5488463}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {751AD755-CC15-4830-AF9B-8C42B731B22E}
EndGlobalSection
EndGlobal
+30 -4
View File
@@ -1,4 +1,4 @@
<Window x:Class="Lithium.MainWindow" <Window x:Class="Lithium.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wv="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf" xmlns:wv="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
@@ -7,6 +7,26 @@
WindowStyle="None" WindowStyle="None"
AllowsTransparency="True"> AllowsTransparency="True">
<Window.Resources>
<Style TargetType="TabItem">
<Setter Property="Foreground" Value="LightGray"/>
<Setter Property="Background" Value="#252525"/>
<Setter Property="BorderBrush" Value="#444"/>
<Setter Property="Padding" Value="12,6"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="#333333"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#2D2D2D"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid> <Grid>
<!-- Custom Title Bar --> <!-- Custom Title Bar -->
<Grid Height="35" VerticalAlignment="Top" MouseDown="TitleBar_MouseDown"> <Grid Height="35" VerticalAlignment="Top" MouseDown="TitleBar_MouseDown">
@@ -173,6 +193,7 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
@@ -267,7 +288,7 @@
<GroupBox Grid.Row="4" Grid.Column="0" Header="Console Settings" Foreground="White" Margin="5" BorderBrush="#444"> <GroupBox Grid.Row="4" Grid.Column="0" Header="Console Settings" Foreground="White" Margin="5" BorderBrush="#444">
<StackPanel Margin="10"> <StackPanel Margin="10">
<TextBlock Text="Text Color:" Foreground="White" Margin="0,5"/> <TextBlock Text="Text Color:" Foreground="White" Margin="0,5"/>
<ComboBox x:Name="TextColorComboBox" Width="120" HorizontalAlignment="Left" Background="#2D2D2D" Foreground="Black" BorderBrush="#444"> <ComboBox x:Name="TextColorComboBox" Width="120" HorizontalAlignment="Left" Background="#2D2D2D" Foreground="White" BorderBrush="#444">
<ComboBoxItem Content="White"/> <ComboBoxItem Content="White"/>
<ComboBoxItem Content="Magenta"/> <ComboBoxItem Content="Magenta"/>
<ComboBoxItem Content="Cyan"/> <ComboBoxItem Content="Cyan"/>
@@ -289,8 +310,13 @@
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<!-- Quick Swap Bindings -->
<GroupBox Grid.Row="5" Grid.ColumnSpan="2" Header="Quick Swap Bindings (Format: Key1+Key2 => Operator Name)" Foreground="White" Margin="5" BorderBrush="#444">
<TextBox x:Name="QuickSwapsTextBox" AcceptsReturn="True" Height="80" VerticalScrollBarVisibility="Auto" Background="#2D2D2D" Foreground="White" BorderBrush="#444" FontFamily="Consolas" FontSize="11" Margin="10"/>
</GroupBox>
<!-- Key Code Reference --> <!-- Key Code Reference -->
<GroupBox Grid.Row="5" Grid.ColumnSpan="2" Header="Common Virtual Key Codes" Foreground="White" Margin="5" BorderBrush="#444"> <GroupBox Grid.Row="6" Grid.ColumnSpan="2" Header="Common Virtual Key Codes" Foreground="White" Margin="5" BorderBrush="#444">
<ScrollViewer MaxHeight="120" VerticalScrollBarVisibility="Auto"> <ScrollViewer MaxHeight="120" VerticalScrollBarVisibility="Auto">
<TextBlock x:Name="KeyCodeReferenceText" Foreground="LightGray" FontFamily="Consolas" FontSize="10" Margin="10" TextWrapping="Wrap"> <TextBlock x:Name="KeyCodeReferenceText" Foreground="LightGray" FontFamily="Consolas" FontSize="10" Margin="10" TextWrapping="Wrap">
0x01: Left Mouse, 0x02: Right Mouse, 0x04: Middle Mouse 0x01: Left Mouse, 0x02: Right Mouse, 0x04: Middle Mouse
@@ -304,7 +330,7 @@ Full list: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-c
</GroupBox> </GroupBox>
<!-- Status --> <!-- Status -->
<Border Grid.Row="6" Grid.ColumnSpan="2" Background="#2D2D2D" Margin="5" BorderBrush="#444" BorderThickness="1"> <Border Grid.Row="7" Grid.ColumnSpan="2" Background="#2D2D2D" Margin="5" BorderBrush="#444" BorderThickness="1">
<TextBlock x:Name="SettingsStatusText" Foreground="Lime" FontFamily="Consolas" FontSize="11" Margin="10,5" Text="Settings loaded successfully" TextWrapping="Wrap"/> <TextBlock x:Name="SettingsStatusText" Foreground="Lime" FontFamily="Consolas" FontSize="11" Margin="10,5" Text="Settings loaded successfully" TextWrapping="Wrap"/>
</Border> </Border>
+204 -39
View File
@@ -1,6 +1,7 @@
using Lua; using Lua;
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@@ -24,6 +25,10 @@ namespace Lithium
// AA0114 // AA0114
private DateTime prevSetAllowed = DateTime.MinValue; private DateTime prevSetAllowed = DateTime.MinValue;
// Track threads for cleanup
private Thread _hotkeyThread;
private Thread _recoilThread;
// AA0115 // AA0115
public MainWindow() public MainWindow()
{ {
@@ -45,18 +50,42 @@ namespace Lithium
// AA0117 // AA0117
LoadSettingsToUI(); LoadSettingsToUI();
StartThread(HotkeyLoop); _hotkeyThread = StartThread(HotkeyLoop);
StartThread(RecoilLoop); _recoilThread = StartThread(RecoilLoop);
// Clean up on window close
Closing += MainWindow_Closing;
}
// Handle window closing - dispose resources
private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
try
{
// Dispose speech synthesizer
Utils.Dispose();
// Stop hotkey handler gracefully
HotkeyHandler.Stop();
// Stop recoil engine gracefully
RecoilEngine.Stop();
}
catch
{
// Ignore cleanup errors
}
} }
// AA0118 // AA0118
private void StartThread(ThreadStart action) private Thread StartThread(ThreadStart action)
{ {
var t = new Thread(action) var t = new Thread(action)
{ {
IsBackground = true IsBackground = true
}; };
t.Start(); t.Start();
return t;
} }
// AA0119 // AA0119
@@ -68,6 +97,22 @@ namespace Lithium
// ============================================================ // ============================================================
// SETTINGS MANAGEMENT // SETTINGS MANAGEMENT
// ============================================================ // ============================================================
private Brush GetBrushFromConsoleColor(ConsoleColor color)
{
switch (color)
{
case ConsoleColor.White: return Brushes.White;
case ConsoleColor.Magenta: return Brushes.Magenta;
case ConsoleColor.Cyan: return Brushes.Cyan;
case ConsoleColor.Green: return Brushes.Lime;
case ConsoleColor.Yellow: return Brushes.Yellow;
case ConsoleColor.Red: return Brushes.Red;
case ConsoleColor.Blue: return Brushes.DeepSkyBlue;
case ConsoleColor.Gray: return Brushes.Gray;
default: return Brushes.White;
}
}
// AA0121 // AA0121
private void LoadSettingsToUI() private void LoadSettingsToUI()
{ {
@@ -94,16 +139,27 @@ namespace Lithium
FavoriteAttackersTextBox.Text = ConfigLoader.favoriteAttackers ?? ""; FavoriteAttackersTextBox.Text = ConfigLoader.favoriteAttackers ?? "";
FavoriteDefendersTextBox.Text = ConfigLoader.favoriteDefenders ?? ""; FavoriteDefendersTextBox.Text = ConfigLoader.favoriteDefenders ?? "";
// AA0126 var qsLines = new System.Collections.Generic.List<string>();
foreach (var qs in ConfigLoader.quickSwaps)
{
string keysStr = string.Join("+", qs.Keys.Select(k => $"0x{k:X2}"));
qsLines.Add($"{keysStr} => {qs.TargetOperator}");
}
QuickSwapsTextBox.Text = string.Join(Environment.NewLine, qsLines);
// AA0126 - Fixed: Properly match ConsoleColor enum values
string colorName = ConfigLoader.textColor.ToString();
foreach (ComboBoxItem item in TextColorComboBox.Items) foreach (ComboBoxItem item in TextColorComboBox.Items)
{ {
if (item.Content.ToString() == ConfigLoader.textColor.ToString()) if (item.Content.ToString().Equals(colorName, StringComparison.OrdinalIgnoreCase))
{ {
TextColorComboBox.SelectedItem = item; TextColorComboBox.SelectedItem = item;
break; break;
} }
} }
ConsoleOutput.Foreground = GetBrushFromConsoleColor(ConfigLoader.textColor);
SettingsStatusText.Text = "Settings loaded successfully"; SettingsStatusText.Text = "Settings loaded successfully";
SettingsStatusText.Foreground = new SolidColorBrush(Colors.Lime); SettingsStatusText.Foreground = new SolidColorBrush(Colors.Lime);
} }
@@ -121,7 +177,15 @@ namespace Lithium
try try
{ {
string configContent = GenerateConfigContent(); string configContent = GenerateConfigContent();
File.WriteAllText("init.config", configContent); if (configContent == null)
{
SettingsStatusText.Text = "Error: Invalid configuration values";
SettingsStatusText.Foreground = new SolidColorBrush(Colors.Red);
return;
}
string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "init.config");
File.WriteAllText(configPath, configContent);
// AA0129 // AA0129
ConfigLoader.LoadInitConfig(); ConfigLoader.LoadInitConfig();
@@ -131,6 +195,16 @@ namespace Lithium
SettingsStatusText.Foreground = new SolidColorBrush(Colors.Lime); SettingsStatusText.Foreground = new SolidColorBrush(Colors.Lime);
AppendConsole("Settings updated from UI"); AppendConsole("Settings updated from UI");
} }
catch (UnauthorizedAccessException ex)
{
SettingsStatusText.Text = $"Access denied: {ex.Message}";
SettingsStatusText.Foreground = new SolidColorBrush(Colors.Red);
}
catch (IOException ex)
{
SettingsStatusText.Text = $"IO Error: {ex.Message}";
SettingsStatusText.Foreground = new SolidColorBrush(Colors.Red);
}
catch (Exception ex) catch (Exception ex)
{ {
// AA0130 // AA0130
@@ -168,6 +242,8 @@ namespace Lithium
ConfigLoader.favoriteDefenders = null; ConfigLoader.favoriteDefenders = null;
ConfigLoader.textColor = System.ConsoleColor.Magenta; ConfigLoader.textColor = System.ConsoleColor.Magenta;
QuickSwapsTextBox.Text = "";
LoadSettingsToUI(); LoadSettingsToUI();
SettingsStatusText.Text = "Settings reset to defaults (save to apply)"; SettingsStatusText.Text = "Settings reset to defaults (save to apply)";
SettingsStatusText.Foreground = new SolidColorBrush(Colors.Yellow); SettingsStatusText.Foreground = new SolidColorBrush(Colors.Yellow);
@@ -176,42 +252,104 @@ namespace Lithium
// AA0134 // AA0134
private string GenerateConfigContent() private string GenerateConfigContent()
{ {
return $@"-- Lithium Configuration File // Validate and parse jitter times
-- Generated from UI on {DateTime.Now} if (!int.TryParse(JitterBurstTimeTextBox.Text, out int jitterBurstTime))
{
AppendConsole("Error: Invalid jitter burst time value");
return null;
}
if (!int.TryParse(JitterCooldownTextBox.Text, out int jitterCooldown))
{
AppendConsole("Error: Invalid jitter cooldown value");
return null;
}
-- Basic Settings // Parse Quick Swaps from UI text box
debitFlag1 = {DebitFlagCheckBox.IsChecked.ToString().ToLower()} ConfigLoader.quickSwaps.Clear();
betaTest = {BetaTestCheckBox.IsChecked.ToString().ToLower()} var textLines = QuickSwapsTextBox.Text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
ttsEnabled = {TtsEnabledCheckBox.IsChecked.ToString().ToLower()} foreach (var rawLine in textLines)
jitterEnabled = {JitterEnabledCheckBox.IsChecked.ToString().ToLower()} {
var line = rawLine.Trim();
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("--") || line.StartsWith("#")) continue;
-- Key Bindings (Virtual Key Codes) string parsedLine = line;
nextSetKey = {ParseKeyCode(NextSetKeyTextBox.Text)} if (!line.StartsWith("quickSwap", StringComparison.OrdinalIgnoreCase))
prevSetKey = {ParseKeyCode(PrevSetKeyTextBox.Text)} {
refreshKey = {ParseKeyCode(RefreshKeyTextBox.Text)} parsedLine = "quickSwap = " + line;
switchSideKey = {ParseKeyCode(SwitchSideKeyTextBox.Text)} }
-- Jitter Settings if (Helper.ParseQuickSwap(parsedLine, out var qsKeys, out var qsOp))
jitterBurstTime = {int.Parse(JitterBurstTimeTextBox.Text)} {
jitterCooldown = {int.Parse(JitterCooldownTextBox.Text)} ConfigLoader.quickSwaps.Add(new QuickSwapBinding { Keys = qsKeys, TargetOperator = qsOp });
}
else
{
AppendConsole($"Warning: Invalid quick swap format: {line}");
}
}
-- User & Favorites var sb = new StringBuilder();
userName = ""{UserNameTextBox.Text}"" sb.AppendLine("-- Lithium Configuration File");
favoriteAttackers = ""{FavoriteAttackersTextBox.Text}"" sb.AppendLine($"-- Generated from UI on {DateTime.Now}");
favoriteDefenders = ""{FavoriteDefendersTextBox.Text}"" sb.AppendLine();
sb.AppendLine("-- Basic Settings");
sb.AppendLine($"debitFlag1 = {DebitFlagCheckBox.IsChecked.ToString().ToLower()}");
sb.AppendLine($"betaTest = {BetaTestCheckBox.IsChecked.ToString().ToLower()}");
sb.AppendLine($"ttsEnabled = {TtsEnabledCheckBox.IsChecked.ToString().ToLower()}");
sb.AppendLine($"jitterEnabled = {JitterEnabledCheckBox.IsChecked.ToString().ToLower()}");
sb.AppendLine();
sb.AppendLine("-- Key Bindings (Virtual Key Codes)");
sb.AppendLine($"nextSetKey = {ParseKeyCode(NextSetKeyTextBox.Text)}");
sb.AppendLine($"prevSetKey = {ParseKeyCode(PrevSetKeyTextBox.Text)}");
sb.AppendLine($"refreshKey = {ParseKeyCode(RefreshKeyTextBox.Text)}");
sb.AppendLine($"switchSideKey = {ParseKeyCode(SwitchSideKeyTextBox.Text)}");
sb.AppendLine();
sb.AppendLine("-- Jitter Settings");
sb.AppendLine($"jitterBurstTime = {jitterBurstTime}");
sb.AppendLine($"jitterCooldown = {jitterCooldown}");
sb.AppendLine();
sb.AppendLine("-- User & Favorites");
sb.AppendLine($"userName = \"{UserNameTextBox.Text}\"");
sb.AppendLine($"favoriteAttackers = \"{FavoriteAttackersTextBox.Text}\"");
sb.AppendLine($"favoriteDefenders = \"{FavoriteDefendersTextBox.Text}\"");
sb.AppendLine();
sb.AppendLine("-- Console Settings");
sb.AppendLine($"textColor = {((ComboBoxItem)TextColorComboBox.SelectedItem)?.Content.ToString() ?? "Magenta"}");
-- Console Settings if (ConfigLoader.quickSwaps.Count > 0)
textColor = {((ComboBoxItem)TextColorComboBox.SelectedItem)?.Content.ToString() ?? "Magenta"}"; {
sb.AppendLine();
sb.AppendLine("-- Quick Swap Bindings");
foreach (var qs in ConfigLoader.quickSwaps)
{
string keysStr = string.Join("+", qs.Keys.Select(k => $"0x{k:X2}"));
sb.AppendLine($"quickSwap = {keysStr} => {qs.TargetOperator}");
}
}
return sb.ToString();
} }
// AA0135 // AA0135
private int ParseKeyCode(string text) private int ParseKeyCode(string text)
{ {
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) if (string.IsNullOrWhiteSpace(text))
return 0x05; // Default key
try
{ {
return Convert.ToInt32(text, 16); if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
return Convert.ToInt32(text, 16);
}
if (int.TryParse(text, out int value))
return value;
} }
return int.Parse(text); catch
{
// Return default on error
}
return 0x05; // Default key
} }
// ============================================================ // ============================================================
@@ -278,6 +416,14 @@ textColor = {((ComboBoxItem)TextColorComboBox.SelectedItem)?.Content.ToString()
Utils.SafeSpeak(OperatorSets.CurrentSet); Utils.SafeSpeak(OperatorSets.CurrentSet);
} }
public void HandleQuickSwap(string targetOp)
{
OperatorSets.QuickSwapTo(targetOp);
AppendConsole($"Quick Swap → {OperatorSets.CurrentSet}");
Utils.SafeSpeak(OperatorSets.CurrentSet);
}
// ============================================================ // ============================================================
// UI CONSOLE // UI CONSOLE
// ============================================================ // ============================================================
@@ -362,16 +508,25 @@ textColor = {((ComboBoxItem)TextColorComboBox.SelectedItem)?.Content.ToString()
{ {
try try
{ {
string customSetPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "custom_set.txt");
File.WriteAllText( File.WriteAllText(
"custom_set.txt", customSetPath,
$"{OperatorNameTextBox.Text}|{RecoilXTextBox.Text}|{RecoilYTextBox.Text}|{(AttackerRadioButton.IsChecked == true ? "attacker" : "defender")}" $"{OperatorNameTextBox.Text}|{RecoilXTextBox.Text}|{RecoilYTextBox.Text}|{(AttackerRadioButton.IsChecked == true ? "attacker" : "defender")}"
); );
UpdateCustomSetConsole("Custom set saved"); UpdateCustomSetConsole("Custom set saved");
} }
catch catch (UnauthorizedAccessException)
{ {
UpdateCustomSetConsole("Failed to save custom set"); UpdateCustomSetConsole("Error: Access denied - check file permissions");
}
catch (IOException ex)
{
UpdateCustomSetConsole($"IO Error: {ex.Message}");
}
catch (Exception ex)
{
UpdateCustomSetConsole($"Failed to save custom set: {ex.Message}");
} }
} }
@@ -380,15 +535,17 @@ textColor = {((ComboBoxItem)TextColorComboBox.SelectedItem)?.Content.ToString()
{ {
try try
{ {
if (!File.Exists("custom_set.txt")) string customSetPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "custom_set.txt");
if (!File.Exists(customSetPath))
{ {
UpdateCustomSetConsole("No saved set found"); UpdateCustomSetConsole("No saved set found");
return; return;
} }
string[] parts = File.ReadAllText("custom_set.txt").Split('|'); string[] parts = File.ReadAllText(customSetPath).Split('|');
if (parts.Length != 4) if (parts.Length != 4)
throw new Exception("Invalid file"); throw new Exception("Invalid file format");
OperatorNameTextBox.Text = parts[0]; OperatorNameTextBox.Text = parts[0];
RecoilXTextBox.Text = parts[1]; RecoilXTextBox.Text = parts[1];
@@ -401,9 +558,17 @@ textColor = {((ComboBoxItem)TextColorComboBox.SelectedItem)?.Content.ToString()
UpdateCustomSetConsole("Custom set loaded"); UpdateCustomSetConsole("Custom set loaded");
} }
catch catch (UnauthorizedAccessException)
{ {
UpdateCustomSetConsole("Failed to load custom set"); UpdateCustomSetConsole("Error: Access denied - check file permissions");
}
catch (IOException ex)
{
UpdateCustomSetConsole($"IO Error: {ex.Message}");
}
catch (Exception ex)
{
UpdateCustomSetConsole($"Failed to load custom set: {ex.Message}");
} }
} }
+131 -18
View File
@@ -1,7 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks;
namespace Lua namespace Lua
{ {
@@ -31,6 +32,15 @@ namespace Lua
// AA0058 // AA0058
public static bool onAttackers = true; public static bool onAttackers = true;
public static string originalSetBeforeQuickSwap = null;
public static bool isQuickSwapping = false;
// Static HttpClient for connection pooling (FIX: previously created new instance each time)
private static readonly HttpClient _httpClient = new HttpClient
{
Timeout = System.TimeSpan.FromSeconds(10)
};
private static string CacheDirectory => private static string CacheDirectory =>
System.IO.Path.Combine( System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
@@ -108,46 +118,74 @@ namespace Lua
var allLines = new List<string>(); var allLines = new List<string>();
bool loadedOnline = false; bool loadedOnline = false;
// Ensure coax folder exists // Ensure coax folder exists (with proper error handling)
try { System.IO.Directory.CreateDirectory(CacheDirectory); } catch { } try
{
if (!System.IO.Directory.Exists(CacheDirectory))
System.IO.Directory.CreateDirectory(CacheDirectory);
}
catch
{
// Directory creation failed - will fall back to cache if available
}
// Try loading from online source // Try loading from online source
if (allowOnlineFetch && !string.IsNullOrEmpty(OnlineConfigUrl)) if (allowOnlineFetch && !string.IsNullOrEmpty(OnlineConfigUrl))
{ {
try try
{ {
using (var client = new HttpClient()) string txt = _httpClient.GetStringAsync(OnlineConfigUrl).Result;
if (!string.IsNullOrWhiteSpace(txt))
{ {
string txt = client.GetStringAsync(OnlineConfigUrl).Result; allLines.AddRange(txt.Split('\n'));
if (!string.IsNullOrWhiteSpace(txt)) // Save cache file - with proper error handling
try
{ {
allLines.AddRange(txt.Split('\n')); if (System.IO.Directory.Exists(CacheDirectory))
System.IO.File.WriteAllText(CacheFile, txt);
// Save cache file in per-user coax folder
System.IO.File.WriteAllText(CacheFile, txt);
loadedOnline = true;
} }
catch
{
// Cache write failed, but we have the data in memory
}
loadedOnline = true;
} }
} }
catch (HttpRequestException)
{
// Network error - fall back to cache
}
catch (TaskCanceledException)
{
// Timeout - fall back to cache
}
catch catch
{ {
// Ignore; fallback handled below // Other errors - fall back to cache
} }
} }
// If online fetch failed → fallback to cached file // If online fetch failed → fallback to cached file
if (!loadedOnline && System.IO.File.Exists(CacheFile)) if (!loadedOnline && System.IO.File.Exists(CacheFile))
{ {
var cached = System.IO.File.ReadAllLines(CacheFile); try
if (cached.Length > 0) {
allLines.AddRange(cached); var cached = System.IO.File.ReadAllLines(CacheFile);
if (cached.Length > 0)
allLines.AddRange(cached);
}
catch
{
// Cache file read failed - continue without it
}
} }
// Also load config.txt (local overrides) // Also load config.txt (local overrides)
var local = Helper.ReadLinesSafe("config.txt"); var local = Helper.ReadLinesSafe("config.txt");
if (local != null && local.Length > 0) if (local.Length > 0)
allLines.AddRange(local); allLines.AddRange(local);
bool inAttackers = false; bool inAttackers = false;
@@ -208,6 +246,7 @@ namespace Lua
// AA0070 // AA0070
public static void MergeSets() public static void MergeSets()
{ {
if (!isQuickSwapping) originalSetBeforeQuickSwap = null;
if (ConfigLoader.betaTest) if (ConfigLoader.betaTest)
{ {
mergedSets.Clear(); mergedSets.Clear();
@@ -236,6 +275,7 @@ namespace Lua
// AA0071 // AA0071
public static void RestoreSet(string oldSet) public static void RestoreSet(string oldSet)
{ {
if (!isQuickSwapping) originalSetBeforeQuickSwap = null;
if (setNames.Count == 0) if (setNames.Count == 0)
{ {
currentSetIndex = 0; currentSetIndex = 0;
@@ -250,6 +290,7 @@ namespace Lua
// AA0072 // AA0072
public static void SwitchSide() public static void SwitchSide()
{ {
if (!isQuickSwapping) originalSetBeforeQuickSwap = null;
if (ConfigLoader.betaTest) return; if (ConfigLoader.betaTest) return;
onAttackers = !onAttackers; onAttackers = !onAttackers;
@@ -260,6 +301,7 @@ namespace Lua
// AA0073 // AA0073
public static void NextSet() public static void NextSet()
{ {
if (!isQuickSwapping) originalSetBeforeQuickSwap = null;
if (setNames.Count == 0) return; if (setNames.Count == 0) return;
currentSetIndex = (currentSetIndex + 1) % setNames.Count; currentSetIndex = (currentSetIndex + 1) % setNames.Count;
@@ -269,6 +311,7 @@ namespace Lua
// AA0074 // AA0074
public static void PreviousSet() public static void PreviousSet()
{ {
if (!isQuickSwapping) originalSetBeforeQuickSwap = null;
if (setNames.Count == 0) return; if (setNames.Count == 0) return;
currentSetIndex = (currentSetIndex - 1 + setNames.Count) % setNames.Count; currentSetIndex = (currentSetIndex - 1 + setNames.Count) % setNames.Count;
@@ -278,7 +321,8 @@ namespace Lua
// AA0075 // AA0075
public static void SetCurrentSet(string name) public static void SetCurrentSet(string name)
{ {
if (setNames == null || setNames.Count == 0) return; if (!isQuickSwapping) originalSetBeforeQuickSwap = null;
if (setNames.Count == 0) return;
int idx = setNames.FindIndex(s => string.Equals(s, name, StringComparison.OrdinalIgnoreCase)); int idx = setNames.FindIndex(s => string.Equals(s, name, StringComparison.OrdinalIgnoreCase));
if (idx >= 0) if (idx >= 0)
@@ -341,5 +385,74 @@ namespace Lua
currentSetIndex = 0; currentSetIndex = 0;
CurrentOperator = CurrentSet; CurrentOperator = CurrentSet;
} }
public static void QuickSwapTo(string targetOp)
{
if (setNames == null || setNames.Count == 0) return;
// Check if the target operator is valid
bool inAttackers = attackers.ContainsKey(targetOp);
bool inDefenders = defenders.ContainsKey(targetOp);
if (!inAttackers && !inDefenders)
{
if (!mergedSets.ContainsKey(targetOp))
{
return; // Invalid operator
}
}
isQuickSwapping = true;
try
{
if (!ConfigLoader.betaTest)
{
if (inAttackers && !onAttackers)
{
SwitchSide();
}
else if (inDefenders && onAttackers)
{
SwitchSide();
}
}
if (string.Equals(CurrentSet, targetOp, StringComparison.OrdinalIgnoreCase))
{
if (!string.IsNullOrEmpty(originalSetBeforeQuickSwap))
{
bool origInAttackers = attackers.ContainsKey(originalSetBeforeQuickSwap);
bool origInDefenders = defenders.ContainsKey(originalSetBeforeQuickSwap);
if (!ConfigLoader.betaTest)
{
if (origInAttackers && !onAttackers)
{
SwitchSide();
}
else if (origInDefenders && onAttackers)
{
SwitchSide();
}
}
SetCurrentSet(originalSetBeforeQuickSwap);
originalSetBeforeQuickSwap = null;
}
}
else
{
if (string.IsNullOrEmpty(originalSetBeforeQuickSwap))
{
originalSetBeforeQuickSwap = CurrentSet;
}
SetCurrentSet(targetOp);
}
}
finally
{
isQuickSwapping = false;
}
}
} }
} }
+51 -22
View File
@@ -1,4 +1,4 @@
using Lithium; using Lithium;
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
@@ -25,36 +25,55 @@ namespace Lua
return (GetAsyncKeyState(key) & 0x8000) != 0; return (GetAsyncKeyState(key) & 0x8000) != 0;
} }
public static void Start(MainWindow window) private static CancellationTokenSource _cancellationSource;
{
while (true)
{
if (window.IsRecoilEnabled && IsDown(VK_LBUTTON) && IsDown(VK_RBUTTON))
{
HandleRecoilTick();
}
else
{
_remainderX = _remainderY = 0;
_jitterActive = false;
}
Thread.Sleep(2); public static void Start(MainWindow window, CancellationToken cancellationToken = default)
{
_cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
try
{
while (!_cancellationSource.Token.IsCancellationRequested)
{
// FIX: Perform key checks once and reuse the results
bool leftDown = IsDown(VK_LBUTTON);
bool rightDown = IsDown(VK_RBUTTON);
if (window.IsRecoilEnabled && leftDown && rightDown)
{
HandleRecoilTick();
}
else
{
_remainderX = _remainderY = 0;
_jitterActive = false;
}
Thread.Sleep(2);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"RecoilEngine error: {ex.Message}");
} }
} }
private static void HandleRecoilTick() private static void HandleRecoilTick()
{ {
if (!IsDown(VK_LBUTTON)) return; try
{
string setName = OperatorSets.CurrentSet;
string setName = OperatorSets.CurrentSet; if (!OperatorSets.mergedSets.TryGetValue(setName, out var recoil)) return;
if (!OperatorSets.mergedSets.TryGetValue(setName, out var recoil)) return; int now = Environment.TickCount;
int now = Environment.TickCount; HandleJitter(now);
ApplyRecoil(recoil);
HandleJitter(now); }
ApplyRecoil(recoil); catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"HandleRecoilTick error: {ex.Message}");
}
} }
private static void HandleJitter(int now) private static void HandleJitter(int now)
@@ -103,5 +122,15 @@ namespace Lua
Utils.MoveMouse(ix, iy); Utils.MoveMouse(ix, iy);
} }
public static void Stop()
{
try
{
_cancellationSource?.Cancel();
_cancellationSource?.Dispose();
}
catch { }
}
} }
} }
+21 -1
View File
@@ -7,10 +7,30 @@ namespace Lua
static class Utils static class Utils
{ {
// AA0091 // AA0091
public static readonly SpeechSynthesizer Tts = new SpeechSynthesizer(); private static SpeechSynthesizer _tts;
public static SpeechSynthesizer Tts
{
get
{
if (_tts == null)
_tts = new SpeechSynthesizer();
return _tts;
}
}
// AA0092 // AA0092
private static readonly object TtsLock = new object(); private static readonly object TtsLock = new object();
// Cleanup on application shutdown
public static void Dispose()
{
lock (TtsLock)
{
_tts?.Dispose();
_tts = null;
}
}
// AA0093 // AA0093
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern short GetAsyncKeyState(int vKey); public static extern short GetAsyncKeyState(int vKey);
Binary file not shown.
Binary file not shown.
+57 -4
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -78,9 +78,9 @@ namespace Lua
result = line.Substring(idx + 1).Trim(); result = line.Substring(idx + 1).Trim();
// AA0036 // AA0036 - Fixed: Check length before substring to avoid negative index
if ((result.StartsWith("\"") && result.EndsWith("\"")) || if (result.Length >= 2 && ((result.StartsWith("\"") && result.EndsWith("\"")) ||
(result.StartsWith("'") && result.EndsWith("'"))) (result.StartsWith("'") && result.EndsWith("'"))))
{ {
result = result.Substring(1, result.Length - 2); result = result.Substring(1, result.Length - 2);
} }
@@ -124,5 +124,58 @@ namespace Lua
if (idx >= size) return size - 1; if (idx >= size) return size - 1;
return idx; return idx;
} }
public static bool ParseQuickSwap(string line, out int[] keys, out string targetOperator)
{
keys = null;
targetOperator = null;
var idx = line.IndexOf('=');
if (idx < 0) return false;
var lhs = line.Substring(0, idx).Trim();
if (!string.Equals(lhs, "quickSwap", StringComparison.OrdinalIgnoreCase)) return false;
var rhs = line.Substring(idx + 1).Trim();
var parts = rhs.Split(new[] { "=>" }, StringSplitOptions.None);
if (parts.Length != 2) return false;
var keyPart = parts[0].Trim();
var opPart = parts[1].Trim();
if (opPart.Length >= 2 && ((opPart.StartsWith("\"") && opPart.EndsWith("\"")) ||
(opPart.StartsWith("'") && opPart.EndsWith("'"))))
{
opPart = opPart.Substring(1, opPart.Length - 2);
}
targetOperator = opPart;
var keyStrings = keyPart.Split('+');
var keyList = new System.Collections.Generic.List<int>();
foreach (var kStr in keyStrings)
{
var trimmed = kStr.Trim();
int vk = 0;
if (trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
try { vk = Convert.ToInt32(trimmed, 16); } catch { return false; }
}
else if (int.TryParse(trimmed, out int val))
{
vk = val;
}
else
{
return false;
}
keyList.Add(vk);
}
if (keyList.Count == 0) return false;
keys = keyList.ToArray();
return true;
}
} }
} }