From 1b0b4d8fb36d3d20f36486169bb27e2b851b5187 Mon Sep 17 00:00:00 2001 From: ArabKustam Date: Fri, 22 May 2026 20:18:46 +0500 Subject: [PATCH] Add modifications and installer build script --- RustPlusDesktop/MainWindow.xaml.cs | 1 + RustPlusDesktop/RustPlusDesk.csproj | 4 +- .../MainWindow/Map/MainWindow.Map.Grid.cs | 50 +- .../Map/MainWindow.Map.Interaction.cs | 44 ++ build.bat | 17 + build_setup.bat | 79 +++ modification/GridCustomizationMod.cs | 459 ++++++++++++++++++ modification/IMod.cs | 18 + modification/MainWindow.Modifications.cs | 123 +++++ modification/ModManager.cs | 276 +++++++++++ modification/README.md | 21 + modification/SmartSwitchesMod.cs | 181 +++++++ run.bat | 13 + 13 files changed, 1270 insertions(+), 16 deletions(-) create mode 100644 build.bat create mode 100644 build_setup.bat create mode 100644 modification/GridCustomizationMod.cs create mode 100644 modification/IMod.cs create mode 100644 modification/MainWindow.Modifications.cs create mode 100644 modification/ModManager.cs create mode 100644 modification/README.md create mode 100644 modification/SmartSwitchesMod.cs create mode 100644 run.bat diff --git a/RustPlusDesktop/MainWindow.xaml.cs b/RustPlusDesktop/MainWindow.xaml.cs index 0c4829c3..d444175b 100644 --- a/RustPlusDesktop/MainWindow.xaml.cs +++ b/RustPlusDesktop/MainWindow.xaml.cs @@ -546,6 +546,7 @@ await Dispatcher.InvokeAsync(() => _monumentWatcher.OnDebug += (s, msg) => Dispatcher.BeginInvoke(new Action(() => AppendLog(msg))); + InitializeModifications(); } private void OnTrackingNotification(string msg, string serverName) diff --git a/RustPlusDesktop/RustPlusDesk.csproj b/RustPlusDesktop/RustPlusDesk.csproj index 9762b4d7..d8e89208 100644 --- a/RustPlusDesktop/RustPlusDesk.csproj +++ b/RustPlusDesktop/RustPlusDesk.csproj @@ -353,7 +353,9 @@ - + + + 60 diff --git a/RustPlusDesktop/Views/MainWindow/Map/MainWindow.Map.Grid.cs b/RustPlusDesktop/Views/MainWindow/Map/MainWindow.Map.Grid.cs index d879dc5d..bca38734 100644 --- a/RustPlusDesktop/Views/MainWindow/Map/MainWindow.Map.Grid.cs +++ b/RustPlusDesktop/Views/MainWindow/Map/MainWindow.Map.Grid.cs @@ -21,12 +21,20 @@ private void RedrawGrid() double ow = _worldRectPx.Width, oh = _worldRectPx.Height; double step = ow / cells; + double shiftPxX = GridShiftX * (ow / _worldSizeS); + double shiftPxY = GridShiftY * (oh / _worldSizeS); + var stroke = new SolidColorBrush(Color.FromArgb(120, 255, 255, 255)); double thin = 1.0, thick = 2.0; - for (int i = 0; i <= cells; i++) + int extra = 3; + + // Draw vertical lines + for (int i = -extra; i <= cells + extra; i++) { - double x = ox + i * step; + double x = ox + i * step + shiftPxX; + if (x < ox || x > ox + ow) continue; + var line = new System.Windows.Shapes.Line { X1 = x, @@ -34,14 +42,17 @@ private void RedrawGrid() X2 = x, Y2 = oy + oh, Stroke = stroke, - StrokeThickness = (i % 5 == 0) ? thick : thin + StrokeThickness = (Math.Abs(i) % 5 == 0) ? thick : thin }; GridLayer.Children.Add(line); } - for (int j = 0; j <= cells; j++) + // Draw horizontal lines + for (int j = -extra; j <= cells + extra; j++) { - double y = oy + j * step; + double y = oy + j * step - shiftPxY; + if (y < oy || y > oy + oh) continue; + var line = new System.Windows.Shapes.Line { X1 = ox, @@ -49,19 +60,28 @@ private void RedrawGrid() X2 = ox + ow, Y2 = y, Stroke = stroke, - StrokeThickness = (j % 5 == 0) ? thick : thin + StrokeThickness = (Math.Abs(j) % 5 == 0) ? thick : thin }; GridLayer.Children.Add(line); } - for (int i = 0; i < cells; i++) + // Draw labels + for (int i = -extra; i < cells + extra; i++) { - string col = ColumnLabel(i); - for (int j = 0; j < cells; j++) + for (int j = -extra; j < cells + extra; j++) { + double x = ox + i * step + 1 + shiftPxX; + double y = oy + j * step + 1 - shiftPxY; + + if (x < ox || x >= ox + ow || y < oy || y >= oy + oh) continue; + + int colIdx = Math.Clamp(i, 0, cells - 1); + int rowIdx = Math.Clamp(j, 0, cells - 1); + string col = ColumnLabel(colIdx); + var tb = new TextBlock { - Text = $"{col}{j}", + Text = $"{col}{rowIdx}", Foreground = Brushes.White, FontSize = 10, Margin = new Thickness(2, 2, 0, 0), @@ -69,9 +89,6 @@ private void RedrawGrid() Padding = new Thickness(2, 0, 2, 0) }; - double x = ox + i * step + 1; - double y = oy + j * step + 1; - GridLayer.Children.Add(tb); Canvas.SetLeft(tb, x); Canvas.SetTop(tb, y); @@ -97,11 +114,14 @@ private bool TryGetGridRef(double x, double y, out string label) label = ""; if (_worldSizeS <= 0) return false; + double shiftedX = x - GridShiftX; + double shiftedY = y - GridShiftY; + int cells = Math.Max(1, (int)Math.Round(_worldSizeS / 150.0)); double cell = _worldSizeS / (double)cells; - int col = Math.Clamp((int)Math.Floor(x / cell), 0, cells - 1); - int row = Math.Clamp((int)Math.Floor((_worldSizeS - y) / cell), 0, cells - 1); + int col = Math.Clamp((int)Math.Floor(shiftedX / cell), 0, cells - 1); + int row = Math.Clamp((int)Math.Floor((_worldSizeS - shiftedY) / cell), 0, cells - 1); label = $"{ColumnLabel(col)}{row}"; return true; diff --git a/RustPlusDesktop/Views/MainWindow/Map/MainWindow.Map.Interaction.cs b/RustPlusDesktop/Views/MainWindow/Map/MainWindow.Map.Interaction.cs index 150d274e..166c1553 100644 --- a/RustPlusDesktop/Views/MainWindow/Map/MainWindow.Map.Interaction.cs +++ b/RustPlusDesktop/Views/MainWindow/Map/MainWindow.Map.Interaction.cs @@ -167,6 +167,17 @@ private void WebViewHost_MouseDown(object? sender, MouseButtonEventArgs e) var hostPos = e.GetPosition(WebViewHost); var mapPos = HostToScenePreTransform(hostPos); + if (IsDragGridMode && e.ChangedButton == MouseButton.Left) + { + _isDraggingGrid = true; + _dragGridStartMapPos = mapPos; + _dragGridStartShiftX = GridShiftX; + _dragGridStartShiftY = GridShiftY; + WebViewHost.CaptureMouse(); + e.Handled = true; + return; + } + if (_overlayToolsVisible && _currentTool != OverlayToolMode.None) { HandleOverlayMouseDown(e, mapPos); @@ -211,6 +222,31 @@ private void WebViewHost_MouseMove(object? sender, MouseEventArgs e) var hostPos = e.GetPosition(WebViewHost); var mapPos = HostToScenePreTransform(hostPos); + if (IsDragGridMode && _isDraggingGrid) + { + double deltaX = mapPos.X - _dragGridStartMapPos.X; + double deltaY = mapPos.Y - _dragGridStartMapPos.Y; + if (_worldSizeS > 0 && _worldRectPx.Width > 0 && _worldRectPx.Height > 0) + { + double newShiftX = _dragGridStartShiftX + deltaX * (_worldSizeS / _worldRectPx.Width); + double newShiftY = _dragGridStartShiftY - deltaY * (_worldSizeS / _worldRectPx.Height); + + newShiftX = Math.Clamp(newShiftX, -300.0, 300.0); + newShiftY = Math.Clamp(newShiftY, -300.0, 300.0); + + newShiftX = Math.Round(newShiftX); + newShiftY = Math.Round(newShiftY); + + GridShiftX = newShiftX; + GridShiftY = newShiftY; + ModRedrawGrid(); + + OnGridOffsetsDragged?.Invoke(newShiftX, newShiftY); + } + e.Handled = true; + return; + } + if (_overlayToolsVisible && _currentTool != OverlayToolMode.None) { HandleOverlayMouseMove(e, mapPos); @@ -237,6 +273,14 @@ private void WebViewHost_MouseUp(object? sender, MouseButtonEventArgs e) var hostPos = e.GetPosition(WebViewHost); var mapPos = HostToScenePreTransform(hostPos); + if (IsDragGridMode && _isDraggingGrid && e.ChangedButton == MouseButton.Left) + { + _isDraggingGrid = false; + WebViewHost.ReleaseMouseCapture(); + e.Handled = true; + return; + } + if (_overlayToolsVisible && _currentTool != OverlayToolMode.None) { HandleOverlayMouseUp(e, mapPos); diff --git a/build.bat b/build.bat new file mode 100644 index 00000000..9c57eb8c --- /dev/null +++ b/build.bat @@ -0,0 +1,17 @@ +@echo off +title Build Rust+ Desktop +echo ============================================== +echo Building Rust+ Desktop... +echo ============================================== +dotnet build +if %errorlevel% neq 0 ( + echo. + echo [ERROR] Build failed! + echo ============================================== + pause + exit /b %errorlevel% +) +echo. +echo [SUCCESS] Build completed successfully! +echo ============================================== +pause diff --git a/build_setup.bat b/build_setup.bat new file mode 100644 index 00000000..c959e2ce --- /dev/null +++ b/build_setup.bat @@ -0,0 +1,79 @@ +@echo off +title Build Rust+ Desktop Installer +cd /d "%~dp0" + +echo ============================================== +echo 1. Publishing application in Release configuration... +echo ============================================== +dotnet publish "%~dp0RustPlusDesktop\RustPlusDesk.csproj" -c Release -r win-x64 --self-contained true -o "%~dp0RustPlusDesktop\bin\Installer\publish" +if %errorlevel% neq 0 ( + echo. + echo [ERROR] Dotnet publish failed! + echo ============================================== + pause + exit /b %errorlevel% +) + +echo. +echo ============================================== +echo 2. Locating Inno Setup Compiler (ISCC)... +echo ============================================== + +set "ISCC_PATH=" + +:: Check common installation paths +if exist "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" ( + set "ISCC_PATH=C:\Program Files (x86)\Inno Setup 6\ISCC.exe" +) else if exist "C:\Program Files\Inno Setup 6\ISCC.exe" ( + set "ISCC_PATH=C:\Program Files\Inno Setup 6\ISCC.exe" +) else if exist "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" ( + set "ISCC_PATH=C:\Program Files (x86)\Inno Setup 5\ISCC.exe" +) else if exist "C:\Program Files\Inno Setup 5\ISCC.exe" ( + set "ISCC_PATH=C:\Program Files\Inno Setup 5\ISCC.exe" +) + +:: If not found in standard paths, try checking PATH +if "%ISCC_PATH%"=="" ( + where iscc >nul 2>nul + if %errorlevel% eq 0 ( + set "ISCC_PATH=iscc" + ) +) + +if "%ISCC_PATH%"=="" ( + echo. + echo [WARNING] Inno Setup Compiler (ISCC.exe) was not found on your system. + echo. + echo To build the setup installer (RustPlusDesk-Setup.exe): + echo 1. Download and install Inno Setup 6 from: + echo https://jrsoftware.org/isdl.php + echo 2. Once installed, run this build_setup.bat script again, OR: + echo 3. Open "%~dp0RustPlusDesktop\Installer\Setup.iss" using the Inno Setup GUI and press F9. + echo. + echo NOTE: The compiled files are already ready in: + echo "%~dp0RustPlusDesktop\bin\Installer\publish\" + echo ============================================== + pause + exit /b 0 +) + +echo Found ISCC at: "%ISCC_PATH%" +echo. +echo ============================================== +echo 3. Compiling Installer... +echo ============================================== +"%ISCC_PATH%" "%~dp0RustPlusDesktop\Installer\Setup.iss" +if %errorlevel% neq 0 ( + echo. + echo [ERROR] Inno Setup compilation failed! + echo ============================================== + pause + exit /b %errorlevel% +) + +echo. +echo ============================================== +echo [SUCCESS] Installer generated successfully! +echo Location: "%~dp0RustPlusDesktop\bin\Installer\RustPlusDesk-Setup.exe" +echo ============================================== +pause diff --git a/modification/GridCustomizationMod.cs b/modification/GridCustomizationMod.cs new file mode 100644 index 00000000..1b964df0 --- /dev/null +++ b/modification/GridCustomizationMod.cs @@ -0,0 +1,459 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using RustPlusDesk.Views; + +namespace RustPlusDesk.Modification +{ + public class GridCustomizationMod : IMod + { + public string Id => "GridCustomization"; + public string Name => "Grid Customization"; + public string Description => "Allows manual shifting of the coordinate grid horizontally (Left/Right) and vertically (Down/Up). " + + "The shift instantly affects the visual map grid lines and coordinate log/chat calculations."; + + private bool _isEnabled = false; + public bool IsEnabled + { + get => _isEnabled; + set + { + _isEnabled = value; + ApplyOrResetGridShift(); + } + } + + private MainWindow? _mainWindow; + private double _shiftX = 0.0; + private double _shiftY = 0.0; + + private Slider? _sliderX; + private Slider? _sliderY; + private TextBox? _txtXValue; + private TextBox? _txtYValue; + private bool _isUpdatingUIFromDrag = false; + + private readonly string _settingsPath; + + public GridCustomizationMod() + { + var appDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RustPlusDesk"); + _settingsPath = Path.Combine(appDir, "grid_settings.json"); + LoadSettings(); + } + + public void Initialize(MainWindow mainWindow) + { + _mainWindow = mainWindow; + if (_isEnabled) + { + ApplyGridShift(); + } + } + + public void OnChatReceived(string text, string author, ulong steamId) { } + public void OnDeviceStateChanged(uint entityId, bool isOn, string kind) { } + + private class GridSettings + { + public double ShiftX { get; set; } + public double ShiftY { get; set; } + } + + private void LoadSettings() + { + try + { + if (File.Exists(_settingsPath)) + { + var json = File.ReadAllText(_settingsPath); + var settings = JsonSerializer.Deserialize(json); + if (settings != null) + { + _shiftX = settings.ShiftX; + _shiftY = settings.ShiftY; + } + } + } + catch + { + // Ignore load failures + } + } + + private void SaveSettings() + { + try + { + var settings = new GridSettings { ShiftX = _shiftX, ShiftY = _shiftY }; + var json = JsonSerializer.Serialize(settings); + Directory.CreateDirectory(Path.GetDirectoryName(_settingsPath)!); + File.WriteAllText(_settingsPath, json); + } + catch + { + // Ignore save failures + } + } + + private void ApplyGridShift() + { + if (_mainWindow != null) + { + _mainWindow.GridShiftX = _shiftX; + _mainWindow.GridShiftY = _shiftY; + _mainWindow.ModRedrawGrid(); + } + } + + private void ApplyOrResetGridShift() + { + if (_mainWindow == null) return; + + if (_isEnabled) + { + _mainWindow.GridShiftX = _shiftX; + _mainWindow.GridShiftY = _shiftY; + } + else + { + _mainWindow.GridShiftX = 0.0; + _mainWindow.GridShiftY = 0.0; + _mainWindow.IsDragGridMode = false; + } + _mainWindow.ModRedrawGrid(); + } + + private void HandleGridOffsetsDragged(double newX, double newY) + { + if (_isUpdatingUIFromDrag) return; + _isUpdatingUIFromDrag = true; + try + { + _shiftX = newX; + _shiftY = newY; + if (_sliderX != null) _sliderX.Value = newX; + if (_sliderY != null) _sliderY.Value = newY; + if (_txtXValue != null) _txtXValue.Text = newX.ToString("F0"); + if (_txtYValue != null) _txtYValue.Text = newY.ToString("F0"); + SaveSettings(); + } + finally + { + _isUpdatingUIFromDrag = false; + } + } + + public FrameworkElement? GetConfigUI() + { + var root = new StackPanel { Margin = new Thickness(0, 10, 0, 0) }; + + // Direct Map Calibration (Drag Grid) Toggle + var toggleDragGrid = new Wpf.Ui.Controls.ToggleSwitch + { + Content = "Drag Grid on Map (Left Click & Drag)", + IsChecked = _mainWindow?.IsDragGridMode == true, + Margin = new Thickness(0, 0, 0, 20), + Foreground = Brushes.White + }; + toggleDragGrid.Checked += (s, e) => + { + if (_mainWindow != null) + { + _mainWindow.IsDragGridMode = true; + _mainWindow.ModLog("Direct Map Calibration enabled. Hold left mouse button on the map and drag the grid lines."); + } + }; + toggleDragGrid.Unchecked += (s, e) => + { + if (_mainWindow != null) + { + _mainWindow.IsDragGridMode = false; + _mainWindow.ModLog("Direct Map Calibration disabled."); + } + }; + root.Children.Add(toggleDragGrid); + + // Horizontal Shift Slider (Left/Right) + var lblX = new TextBlock + { + Text = "Horizontal Shift (meters)", + Foreground = Brushes.White, + FontWeight = FontWeights.SemiBold, + Margin = new Thickness(0, 0, 0, 5) + }; + root.Children.Add(lblX); + + var gridX = new Grid { Margin = new Thickness(0, 0, 0, 15) }; + gridX.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + gridX.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(10) }); // space + gridX.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) }); // TextBox + + _sliderX = new Slider + { + Minimum = -300, + Maximum = 300, + Value = _shiftX, + TickFrequency = 1, + IsSnapToTickEnabled = true, + VerticalAlignment = VerticalAlignment.Center + }; + _sliderX.ValueChanged += (s, e) => + { + if (_isUpdatingUIFromDrag) return; + _shiftX = Math.Round(e.NewValue); + if (_txtXValue != null) + { + _txtXValue.Text = _shiftX.ToString("F0"); + } + if (_isEnabled) + { + SaveSettings(); + ApplyGridShift(); + } + }; + Grid.SetColumn(_sliderX, 0); + gridX.Children.Add(_sliderX); + + _txtXValue = new TextBox + { + Text = _shiftX.ToString("F0"), + Width = 70, + Height = 26, + VerticalAlignment = VerticalAlignment.Center, + HorizontalContentAlignment = HorizontalAlignment.Center, + VerticalContentAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + ToolTip = "Enter offset in meters manually. Use Up/Down arrows to step by 1.", + Background = new SolidColorBrush(Color.FromRgb(40, 40, 40)), + Foreground = Brushes.White, + BorderBrush = Brushes.Gray, + Padding = new Thickness(2) + }; + _txtXValue.TextChanged += (s, e) => + { + if (_isUpdatingUIFromDrag) return; + if (double.TryParse(_txtXValue.Text, out double val)) + { + double clamped = Math.Clamp(val, -300.0, 300.0); + if (Math.Abs(_shiftX - clamped) > 0.001) + { + _shiftX = clamped; + if (_sliderX != null && Math.Abs(_sliderX.Value - clamped) > 0.001) + { + _sliderX.Value = clamped; + } + if (_isEnabled) + { + SaveSettings(); + ApplyGridShift(); + } + } + } + }; + _txtXValue.LostFocus += (s, e) => + { + _txtXValue.Text = _shiftX.ToString("F0"); + }; + _txtXValue.KeyDown += (s, e) => + { + if (e.Key == Key.Enter) + { + UIElement element = (UIElement)s; + element.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); + } + }; + _txtXValue.PreviewKeyDown += (s, e) => + { + if (e.Key == Key.Up) + { + double currentVal = _shiftX; + double newVal = Math.Clamp(currentVal + 1, -300.0, 300.0); + _txtXValue.Text = newVal.ToString("F0"); + _txtXValue.CaretIndex = _txtXValue.Text.Length; + e.Handled = true; + } + else if (e.Key == Key.Down) + { + double currentVal = _shiftX; + double newVal = Math.Clamp(currentVal - 1, -300.0, 300.0); + _txtXValue.Text = newVal.ToString("F0"); + _txtXValue.CaretIndex = _txtXValue.Text.Length; + e.Handled = true; + } + }; + Grid.SetColumn(_txtXValue, 2); + gridX.Children.Add(_txtXValue); + + root.Children.Add(gridX); + + // Vertical Shift Slider (Down/Up) + var lblY = new TextBlock + { + Text = "Vertical Shift (meters)", + Foreground = Brushes.White, + FontWeight = FontWeights.SemiBold, + Margin = new Thickness(0, 0, 0, 5) + }; + root.Children.Add(lblY); + + var gridY = new Grid { Margin = new Thickness(0, 0, 0, 20) }; + gridY.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + gridY.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(10) }); // space + gridY.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) }); // TextBox + + _sliderY = new Slider + { + Minimum = -300, + Maximum = 300, + Value = _shiftY, + TickFrequency = 1, + IsSnapToTickEnabled = true, + VerticalAlignment = VerticalAlignment.Center + }; + _sliderY.ValueChanged += (s, e) => + { + if (_isUpdatingUIFromDrag) return; + _shiftY = Math.Round(e.NewValue); + if (_txtYValue != null) + { + _txtYValue.Text = _shiftY.ToString("F0"); + } + if (_isEnabled) + { + SaveSettings(); + ApplyGridShift(); + } + }; + Grid.SetColumn(_sliderY, 0); + gridY.Children.Add(_sliderY); + + _txtYValue = new TextBox + { + Text = _shiftY.ToString("F0"), + Width = 70, + Height = 26, + VerticalAlignment = VerticalAlignment.Center, + HorizontalContentAlignment = HorizontalAlignment.Center, + VerticalContentAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + ToolTip = "Enter offset in meters manually. Use Up/Down arrows to step by 1.", + Background = new SolidColorBrush(Color.FromRgb(40, 40, 40)), + Foreground = Brushes.White, + BorderBrush = Brushes.Gray, + Padding = new Thickness(2) + }; + _txtYValue.TextChanged += (s, e) => + { + if (_isUpdatingUIFromDrag) return; + if (double.TryParse(_txtYValue.Text, out double val)) + { + double clamped = Math.Clamp(val, -300.0, 300.0); + if (Math.Abs(_shiftY - clamped) > 0.001) + { + _shiftY = clamped; + if (_sliderY != null && Math.Abs(_sliderY.Value - clamped) > 0.001) + { + _sliderY.Value = clamped; + } + if (_isEnabled) + { + SaveSettings(); + ApplyGridShift(); + } + } + } + }; + _txtYValue.LostFocus += (s, e) => + { + _txtYValue.Text = _shiftY.ToString("F0"); + }; + _txtYValue.KeyDown += (s, e) => + { + if (e.Key == Key.Enter) + { + UIElement element = (UIElement)s; + element.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); + } + }; + _txtYValue.PreviewKeyDown += (s, e) => + { + if (e.Key == Key.Up) + { + double currentVal = _shiftY; + double newVal = Math.Clamp(currentVal + 1, -300.0, 300.0); + _txtYValue.Text = newVal.ToString("F0"); + _txtYValue.CaretIndex = _txtYValue.Text.Length; + e.Handled = true; + } + else if (e.Key == Key.Down) + { + double currentVal = _shiftY; + double newVal = Math.Clamp(currentVal - 1, -300.0, 300.0); + _txtYValue.Text = newVal.ToString("F0"); + _txtYValue.CaretIndex = _txtYValue.Text.Length; + e.Handled = true; + } + }; + Grid.SetColumn(_txtYValue, 2); + gridY.Children.Add(_txtYValue); + + root.Children.Add(gridY); + + // Reset Button + var btnReset = new Button + { + Content = "Reset Offsets", + HorizontalAlignment = HorizontalAlignment.Left, + Padding = new Thickness(15, 8, 15, 8) + }; + btnReset.Click += (s, e) => + { + _isUpdatingUIFromDrag = true; + try + { + _shiftX = 0.0; + _shiftY = 0.0; + if (_sliderX != null) _sliderX.Value = 0.0; + if (_sliderY != null) _sliderY.Value = 0.0; + if (_txtXValue != null) _txtXValue.Text = "0"; + if (_txtYValue != null) _txtYValue.Text = "0"; + if (_isEnabled) + { + SaveSettings(); + ApplyGridShift(); + } + } + finally + { + _isUpdatingUIFromDrag = false; + } + }; + root.Children.Add(btnReset); + + // Loaded & Unloaded Lifecycle Subscriptions + root.Loaded += (s, e) => + { + if (_mainWindow != null) + { + _mainWindow.OnGridOffsetsDragged -= HandleGridOffsetsDragged; + _mainWindow.OnGridOffsetsDragged += HandleGridOffsetsDragged; + } + }; + root.Unloaded += (s, e) => + { + if (_mainWindow != null) + { + _mainWindow.IsDragGridMode = false; + _mainWindow.OnGridOffsetsDragged -= HandleGridOffsetsDragged; + } + }; + + return root; + } + } +} diff --git a/modification/IMod.cs b/modification/IMod.cs new file mode 100644 index 00000000..32462a65 --- /dev/null +++ b/modification/IMod.cs @@ -0,0 +1,18 @@ +using System; +using RustPlusDesk.Views; + +namespace RustPlusDesk.Modification +{ + public interface IMod + { + string Id { get; } + string Name { get; } + string Description { get; } + bool IsEnabled { get; set; } + + void Initialize(MainWindow mainWindow); + void OnChatReceived(string text, string author, ulong steamId); + void OnDeviceStateChanged(uint entityId, bool isOn, string kind); + System.Windows.FrameworkElement? GetConfigUI(); + } +} diff --git a/modification/MainWindow.Modifications.cs b/modification/MainWindow.Modifications.cs new file mode 100644 index 00000000..2d0c284c --- /dev/null +++ b/modification/MainWindow.Modifications.cs @@ -0,0 +1,123 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Threading.Tasks; +using RustPlusDesk.Models; +using RustPlusDesk.Services; +using RustPlusDesk.Modification; + +namespace RustPlusDesk.Views +{ + public partial class MainWindow + { + private ModManager? _modManager; + + public void InitializeModifications() + { + _modManager = new ModManager(this, AppendLog); + _modManager.Initialize(); + + // Add the Mods tab to the main TabControl programmatically + Dispatcher.Invoke(() => + { + try + { + var tabItem = new TabItem + { + Header = "πŸ›  Mods", + Style = (Style)FindResource("PrettyTabItem") + }; + + // Create the UI content for the modifications tab + var grid = _modManager.CreateModsTabUI(); + tabItem.Content = grid; + + MainTabs.Items.Add(tabItem); + AppendLog("[ModSystem] Added 'Mods' tab to MainTabs."); + } + catch (Exception ex) + { + AppendLog($"[ModSystem] Error creating Mods UI tab: {ex.Message}"); + } + }); + + // Register event handlers on the WebSocket client + if (_rust is RustPlusClientReal real) + { + real.DeviceStateEvent += async (id, isOn, kind) => + { + await Dispatcher.InvokeAsync(() => + { + _modManager.OnDeviceStateChanged(id, isOn, kind ?? ""); + }); + }; + + real.TeamChatReceived += async (sender, m) => + { + await Dispatcher.InvokeAsync(() => + { + _modManager.OnChatReceived(m.Text, m.Author, m.SteamId); + }); + }; + } + } + + // Helper methods for modifications + + public void ModLog(string message) + { + AppendLog($"[Mod] {message}"); + } + + public async Task ModToggleSwitchAsync(uint entityId, bool isOn) + { + try + { + if (!await EnsureConnectedAsync()) + { + AppendLog($"[Mod] Cannot toggle switch #{entityId}: not connected."); + return; + } + + AppendLog($"[Mod] Toggling switch #{entityId} to {(isOn ? "ON" : "OFF")}..."); + await _rust.ToggleSmartSwitchAsync(entityId, isOn); + + // Also update local device state in UI if it exists + var dev = FindDeviceById(_vm.Selected?.Devices, entityId); + if (dev != null) + { + _suppressToggleHandler = true; + dev.IsOn = isOn; + _suppressToggleHandler = false; + } + } + catch (Exception ex) + { + AppendLog($"[Mod] Failed to toggle switch #{entityId}: {ex.Message}"); + } + } + + public System.Collections.ObjectModel.ObservableCollection? ModGetDevices() + { + return _vm.Selected?.Devices; + } + + public double GridShiftX { get; set; } = 0.0; + public double GridShiftY { get; set; } = 0.0; + + public bool IsDragGridMode { get; set; } = false; + private bool _isDraggingGrid = false; + private Point _dragGridStartMapPos; + private double _dragGridStartShiftX; + private double _dragGridStartShiftY; + public event Action? OnGridOffsetsDragged; + + public void ModRedrawGrid() + { + Dispatcher.Invoke(() => + { + RedrawGrid(); + }); + } + } +} diff --git a/modification/ModManager.cs b/modification/ModManager.cs new file mode 100644 index 00000000..398255b0 --- /dev/null +++ b/modification/ModManager.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using RustPlusDesk.Views; + +namespace RustPlusDesk.Modification +{ + public class ModManager + { + private readonly MainWindow _mainWindow; + private readonly Action _logAction; + private readonly List _mods = new(); + private readonly string _settingsPath; + private bool _isUpdatingUI; + + public ModManager(MainWindow mainWindow, Action logAction) + { + _mainWindow = mainWindow; + _logAction = logAction; + + var appDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RustPlusDesk"); + _settingsPath = Path.Combine(appDir, "modifications.json"); + } + + public void Initialize() + { + // Register mods here + _mods.Add(new SmartSwitchesMod()); + _mods.Add(new GridCustomizationMod()); + + // Load saved settings + LoadSettings(); + + // Initialize all mods + foreach (var mod in _mods) + { + try + { + mod.Initialize(_mainWindow); + _logAction($"Loaded mod: {mod.Name} (Enabled: {mod.IsEnabled})"); + } + catch (Exception ex) + { + _logAction($"Failed to initialize mod {mod.Name}: {ex.Message}"); + } + } + } + + public void OnChatReceived(string text, string author, ulong steamId) + { + foreach (var mod in _mods.Where(m => m.IsEnabled)) + { + try + { + mod.OnChatReceived(text, author, steamId); + } + catch (Exception ex) + { + _logAction($"Error in mod {mod.Name} OnChatReceived: {ex.Message}"); + } + } + } + + public void OnDeviceStateChanged(uint entityId, bool isOn, string kind) + { + foreach (var mod in _mods.Where(m => m.IsEnabled)) + { + try + { + mod.OnDeviceStateChanged(entityId, isOn, kind); + } + catch (Exception ex) + { + _logAction($"Error in mod {mod.Name} OnDeviceStateChanged: {ex.Message}"); + } + } + } + + private void LoadSettings() + { + try + { + if (!File.Exists(_settingsPath)) return; + var json = File.ReadAllText(_settingsPath); + var settings = JsonSerializer.Deserialize>(json); + if (settings == null) return; + + foreach (var mod in _mods) + { + if (settings.TryGetValue(mod.Id, out bool isEnabled)) + { + mod.IsEnabled = isEnabled; + } + } + } + catch (Exception ex) + { + _logAction($"Failed to load mod settings: {ex.Message}"); + } + } + + public void SaveSettings() + { + try + { + var settings = _mods.ToDictionary(m => m.Id, m => m.IsEnabled); + var json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true }); + Directory.CreateDirectory(Path.GetDirectoryName(_settingsPath)!); + File.WriteAllText(_settingsPath, json); + } + catch (Exception ex) + { + _logAction($"Failed to save mod settings: {ex.Message}"); + } + } + + public Grid CreateModsTabUI() + { + var mainGrid = new Grid(); + mainGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(280) }); + mainGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + + // Left side list box + var listBox = new ListBox + { + Margin = new Thickness(10), + Background = Brushes.Transparent, + BorderThickness = new Thickness(0, 0, 1, 0), + BorderBrush = (Brush)Application.Current.FindResource("CardStrokeColorDefaultBrush") + }; + + // Right side detail panel + var detailPanel = new StackPanel + { + Margin = new Thickness(20), + Visibility = Visibility.Collapsed + }; + + var modTitle = new TextBlock + { + FontSize = 22, + FontWeight = FontWeights.Bold, + Foreground = Brushes.White, + Margin = new Thickness(0, 0, 0, 10) + }; + + var modDesc = new TextBlock + { + FontSize = 14, + Foreground = (Brush)Application.Current.FindResource("TextFillColorSecondaryBrush"), + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 0, 0, 20) + }; + + var toggleSwitch = new Wpf.Ui.Controls.ToggleSwitch + { + Content = "Enable Modification", + Margin = new Thickness(0, 0, 0, 20) + }; + + var customUiContainer = new ContentControl + { + Margin = new Thickness(0, 10, 0, 0), + HorizontalContentAlignment = HorizontalAlignment.Stretch, + VerticalContentAlignment = VerticalAlignment.Stretch + }; + + detailPanel.Children.Add(modTitle); + detailPanel.Children.Add(modDesc); + detailPanel.Children.Add(toggleSwitch); + detailPanel.Children.Add(customUiContainer); + + Grid.SetColumn(listBox, 0); + Grid.SetColumn(detailPanel, 1); + + mainGrid.Children.Add(listBox); + mainGrid.Children.Add(detailPanel); + + // Populate mods + foreach (var mod in _mods) + { + var itemGrid = new Grid { Margin = new Thickness(5) }; + itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + itemGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var nameText = new TextBlock + { + Text = mod.Name, + FontWeight = FontWeights.SemiBold, + VerticalAlignment = VerticalAlignment.Center + }; + var statusText = new TextBlock + { + Text = mod.IsEnabled ? "Enabled" : "Disabled", + FontSize = 11, + Foreground = mod.IsEnabled ? Brushes.LimeGreen : Brushes.Gray, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(10, 0, 0, 0) + }; + + Grid.SetColumn(nameText, 0); + Grid.SetColumn(statusText, 1); + itemGrid.Children.Add(nameText); + itemGrid.Children.Add(statusText); + + var listBoxItem = new ListBoxItem + { + Content = itemGrid, + Tag = mod, + Padding = new Thickness(10) + }; + + listBox.Items.Add(listBoxItem); + } + + // Selection change logic + listBox.SelectionChanged += (s, e) => + { + if (listBox.SelectedItem is ListBoxItem selectedItem && selectedItem.Tag is IMod selectedMod) + { + detailPanel.Visibility = Visibility.Visible; + modTitle.Text = selectedMod.Name; + modDesc.Text = selectedMod.Description; + + _isUpdatingUI = true; + toggleSwitch.IsChecked = selectedMod.IsEnabled; + _isUpdatingUI = false; + + customUiContainer.Content = selectedMod.GetConfigUI(); + } + else + { + detailPanel.Visibility = Visibility.Collapsed; + customUiContainer.Content = null; + } + }; + + // Define toggle event handlers once + toggleSwitch.Checked += (ts, ev) => + { + if (_isUpdatingUI) return; + HandleToggleChange(listBox, toggleSwitch, modTitle); + }; + toggleSwitch.Unchecked += (ts, ev) => + { + if (_isUpdatingUI) return; + HandleToggleChange(listBox, toggleSwitch, modTitle); + }; + + return mainGrid; + } + + private void HandleToggleChange(ListBox listBox, Wpf.Ui.Controls.ToggleSwitch toggleSwitch, TextBlock modTitle) + { + if (listBox.SelectedItem is ListBoxItem selectedItem && selectedItem.Tag is IMod selectedMod) + { + selectedMod.IsEnabled = toggleSwitch.IsChecked == true; + SaveSettings(); + + // Update status text in list item + if (selectedItem.Content is Grid grid && grid.Children.OfType().LastOrDefault() is TextBlock statusTxt) + { + statusTxt.Text = selectedMod.IsEnabled ? "Enabled" : "Disabled"; + statusTxt.Foreground = selectedMod.IsEnabled ? Brushes.LimeGreen : Brushes.Gray; + } + + _logAction($"Mod {selectedMod.Name} toggled: {(selectedMod.IsEnabled ? "Enabled" : "Disabled")}"); + } + } + } +} diff --git a/modification/README.md b/modification/README.md new file mode 100644 index 00000000..8ee022d0 --- /dev/null +++ b/modification/README.md @@ -0,0 +1,21 @@ +# ИспользованиС ΠΏΠ°ΠΏΠΊΠΈ modifications (Modifications Sharing Guide) + +Папка `modification` содСрТит всС Ρ„Π°ΠΉΠ»Ρ‹ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΡ… ΠΌΠΎΠ΄ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΉ для прилоТСния Rust+ Desktop. Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ Π»Π΅Π³ΠΊΠΎ Π΄Π΅Π»ΠΈΡ‚ΡŒΡΡ этой ΠΏΠ°ΠΏΠΊΠΎΠΉ с Π΄Ρ€ΡƒΠ³ΠΈΠΌΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡΠΌΠΈ ΠΈΠ»ΠΈ ΠΏΠ΅Ρ€Π΅Π½ΠΎΡΠΈΡ‚ΡŒ Π΅Ρ‘ Π½Π° Π΄Ρ€ΡƒΠ³ΠΈΠ΅ ΠΊΠΎΠΌΠΏΡŒΡŽΡ‚Π΅Ρ€Ρ‹. + +## Как ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΠ°ΠΏΠΊΡƒ `modification` + +1. **Π‘ΠΊΠΎΠΏΠΈΡ€ΡƒΠΉΡ‚Π΅ ΠΏΠ°ΠΏΠΊΡƒ**: ΠžΡ‚ΠΏΡ€Π°Π²ΡŒΡ‚Π΅ ΠΏΠ°ΠΏΠΊΡƒ `modification` Ρ†Π΅Π»ΠΈΠΊΠΎΠΌ Π΄Ρ€ΡƒΠ³ΠΎΠΌΡƒ Ρ‡Π΅Π»ΠΎΠ²Π΅ΠΊΡƒ ΠΈΠ»ΠΈ пСрСнСситС Π΅Ρ‘ Π½Π° Π½ΠΎΠ²Ρ‹ΠΉ ΠΊΠΎΠΌΠΏΡŒΡŽΡ‚Π΅Ρ€. +2. **ΠŸΠΎΠΌΠ΅ΡΡ‚ΠΈΡ‚Π΅ Π² ΠΊΠΎΡ€Π΅Π½ΡŒ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°**: Π‘ΠΊΠΎΠΏΠΈΡ€ΡƒΠΉΡ‚Π΅ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½ΡƒΡŽ ΠΏΠ°ΠΏΠΊΡƒ `modification` Π² ΠΊΠΎΡ€Π½Π΅Π²ΡƒΡŽ Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΡŽ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° `rustplus-desktop` (Ρ‚ΡƒΠ΄Π° ΠΆΠ΅, Π³Π΄Π΅ находятся Ρ„Π°ΠΉΠ»Ρ‹ `RustPlusDesk.sln` ΠΈ ΠΏΠ°ΠΏΠΊΠ° `RustPlusDesktop`). +3. **Π‘Π±ΠΎΡ€ΠΊΠ° ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°**: + - Π”Π²Π°ΠΆΠ΄Ρ‹ ΠΊΠ»ΠΈΠΊΠ½ΠΈΡ‚Π΅ ΠΏΠΎ Ρ„Π°ΠΉΠ»Ρƒ `build.bat` Π² ΠΊΠΎΡ€Π½Π΅ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° ΠΈΠ»ΠΈ Π²Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚Π΅ ΠΊΠΎΠΌΠ°Π½Π΄Ρƒ `dotnet build` Π² консоли. + - ΠŸΡ€ΠΎΠ΅ΠΊΡ‚ автоматичСски ΠΏΠΎΠ΄Ρ…Π²Π°Ρ‚ΠΈΡ‚ всС `.cs` Ρ„Π°ΠΉΠ»Ρ‹ ΠΈΠ· ΠΏΠ°ΠΏΠΊΠΈ `modification` ΠΏΡ€ΠΈ компиляции благодаря настроСнным ΠΏΡ€Π°Π²ΠΈΠ»Π°ΠΌ Π² Ρ„Π°ΠΉΠ»Π΅ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° `.csproj`. +4. **Запуск**: + - Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ Ρ„Π°ΠΉΠ» `run.bat` Π² ΠΊΠΎΡ€Π½Π΅ для запуска скомпилированного прилоТСния. + +## Π‘ΠΎΠ΄Π΅Ρ€ΠΆΠΈΠΌΠΎΠ΅ ΠΏΠ°ΠΏΠΊΠΈ + +- `IMod.cs` β€” ΠΎΠ±Ρ‰ΠΈΠΉ интСрфСйс для создания Π½ΠΎΠ²Ρ‹Ρ… ΠΌΠΎΠ΄ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΉ. +- `ModManager.cs` β€” ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅Ρ‚ всС ΠΌΠΎΠ΄Ρ‹ ΠΈ Π²Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ ΠΈΡ… Π² Π½ΠΎΠ²ΡƒΡŽ Π²ΠΊΠ»Π°Π΄ΠΊΡƒ `πŸ›  Mods` Π² настройках прилоТСния. +- `SmartSwitchesMod.cs` β€” модификация "Π£ΠΌΠ½Ρ‹Π΅ ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π°Ρ‚Π΅Π»ΠΈ" (автоматичСскоС Π²Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΏΠΎ Ρ‚Π°ΠΉΠΌΠ΅Ρ€Ρƒ ΠΈΠ»ΠΈ ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ Ρ‡Π΅Ρ€Π΅Π· Ρ‡Π°Ρ‚). +- `GridCustomizationMod.cs` β€” модификация для Ρ€ΡƒΡ‡Π½ΠΎΠΉ ΠΊΠ°Π»ΠΈΠ±Ρ€ΠΎΠ²ΠΊΠΈ ΠΊΠΎΠΎΡ€Π΄ΠΈΠ½Π°Ρ‚Π½ΠΎΠΉ сСтки (X/Y смСщСниС). +- `MainWindow.Modifications.cs` β€” Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ основного ΠΎΠΊΠ½Π° для ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ ΠΌΠΎΠ΄ΠΎΠ². diff --git a/modification/SmartSwitchesMod.cs b/modification/SmartSwitchesMod.cs new file mode 100644 index 00000000..6fb0baf0 --- /dev/null +++ b/modification/SmartSwitchesMod.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using RustPlusDesk.Models; +using RustPlusDesk.Views; + +namespace RustPlusDesk.Modification +{ + public class SmartSwitchesMod : IMod + { + public string Id => "SmartSwitches"; + public string Name => "Smart Switches"; + public string Description => "Enables advanced timer-based and chat-based controls for Rust smart switches. " + + "Rename your switch to 'timer(300)[off]' to turn it off 300 seconds after it is turned on. " + + "Or rename it to 'chat(!Turret)' to toggle it (ON <-> OFF) whenever that string is written in team chat."; + + public bool IsEnabled { get; set; } = true; + + private MainWindow? _mainWindow; + + // Thread-safe dictionary to keep track of active timer tasks for cancellation + private readonly ConcurrentDictionary _timerCancellationTokens = new(); + + // Regex helpers + private static readonly Regex TimerRegex = new Regex(@"timer\s*\(\s*(\d+)\s*\)\s*\[\s*off\s*\]", RegexOptions.IgnoreCase); + private static readonly Regex ChatRegex = new Regex(@"chat\s*\(\s*([^)]+)\s*\)", RegexOptions.IgnoreCase); + + public void Initialize(MainWindow mainWindow) + { + _mainWindow = mainWindow; + } + + public void OnChatReceived(string text, string author, ulong steamId) + { + if (_mainWindow == null || string.IsNullOrWhiteSpace(text)) return; + + var devices = _mainWindow.ModGetDevices(); + if (devices == null) return; + + var messageText = text.Trim(); + + // Find all smart switches matching chat(trigger) + foreach (var dev in FlattenDevices(devices)) + { + if (dev.IsGroup || string.IsNullOrWhiteSpace(dev.Name)) continue; + + // Verify it's a smart switch + bool isSmartSwitch = string.Equals(dev.Kind, "SmartSwitch", StringComparison.OrdinalIgnoreCase) || + string.Equals(dev.Kind, "Smart Switch", StringComparison.OrdinalIgnoreCase); + + if (!isSmartSwitch) continue; + + var chatMatch = ChatRegex.Match(dev.Name); + if (chatMatch.Success) + { + var trigger = chatMatch.Groups[1].Value.Trim(); + + // Compare trigger (case-insensitive, trimmed) + if (string.Equals(messageText, trigger, StringComparison.OrdinalIgnoreCase)) + { + // Toggle logic: if IsOn is true, turn OFF, otherwise turn ON + bool targetState = !(dev.IsOn == true); + _mainWindow.ModLog($"Chat triggered toggle for switch '{dev.Name}' (#{dev.EntityId}) by '{author}': new state = {(targetState ? "ON" : "OFF")}"); + + // Fire-and-forget toggle call + _ = _mainWindow.ModToggleSwitchAsync(dev.EntityId, targetState); + } + } + } + } + + public void OnDeviceStateChanged(uint entityId, bool isOn, string kind) + { + if (_mainWindow == null) return; + + var devices = _mainWindow.ModGetDevices(); + if (devices == null) return; + + // Find the device + var dev = FindDeviceById(devices, entityId); + if (dev == null || string.IsNullOrWhiteSpace(dev.Name)) return; + + // If the switch turns OFF, cancel any pending turn-off timers + if (!isOn) + { + if (_timerCancellationTokens.TryRemove(entityId, out var cts)) + { + _mainWindow.ModLog($"Switch '{dev.Name}' (#{entityId}) turned OFF. Cancelling pending auto-off timer."); + try { cts.Cancel(); } catch { } + cts.Dispose(); + } + return; + } + + // If the switch turns ON, check if it has a timer(X)[off] rule + var timerMatch = TimerRegex.Match(dev.Name); + if (timerMatch.Success) + { + if (int.TryParse(timerMatch.Groups[1].Value, out int seconds) && seconds > 0) + { + // Cancel any previous timer for the same switch + if (_timerCancellationTokens.TryRemove(entityId, out var oldCts)) + { + try { oldCts.Cancel(); } catch { } + oldCts.Dispose(); + } + + var newCts = new CancellationTokenSource(); + _timerCancellationTokens[entityId] = newCts; + + _mainWindow.ModLog($"Switch '{dev.Name}' (#{entityId}) turned ON. Scheduling auto-off in {seconds} seconds."); + + // Start background task to turn it off + _ = Task.Run(async () => + { + try + { + await Task.Delay(TimeSpan.FromSeconds(seconds), newCts.Token); + + if (!newCts.Token.IsCancellationRequested) + { + _mainWindow.ModLog($"Timer expired for switch '{dev.Name}' (#{entityId}). Turning OFF."); + await _mainWindow.ModToggleSwitchAsync(entityId, false); + } + } + catch (TaskCanceledException) + { + // Expected cancellation + } + catch (Exception ex) + { + _mainWindow.ModLog($"Error in timer task for switch #{entityId}: {ex.Message}"); + } + finally + { + // Clean up dictionary if this CTS is still the active one + if (_timerCancellationTokens.TryGetValue(entityId, out var activeCts) && activeCts == newCts) + { + _timerCancellationTokens.TryRemove(entityId, out _); + } + newCts.Dispose(); + } + }); + } + } + } + + // Helper to flatten hierarchical device lists (handling groups) + private System.Collections.Generic.IEnumerable FlattenDevices(System.Collections.IEnumerable devices) + { + foreach (var item in devices) + { + if (item is SmartDevice dev) + { + yield return dev; + if (dev.IsGroup && dev.Children != null) + { + foreach (var child in FlattenDevices(dev.Children)) + { + yield return child; + } + } + } + } + } + + // Helper to find a specific device by its Entity ID + private SmartDevice? FindDeviceById(System.Collections.IEnumerable devices, uint entityId) + { + return FlattenDevices(devices).FirstOrDefault(d => d.EntityId == entityId); + } + + public System.Windows.FrameworkElement? GetConfigUI() + { + return null; + } + } +} diff --git a/run.bat b/run.bat new file mode 100644 index 00000000..e0e86dc8 --- /dev/null +++ b/run.bat @@ -0,0 +1,13 @@ +@echo off +title Run Rust+ Desktop +echo ============================================== +echo Running Rust+ Desktop... +echo ============================================== +dotnet run --project RustPlusDesktop\RustPlusDesk.csproj +if %errorlevel% neq 0 ( + echo. + echo [ERROR] Failed to run the project. Make sure it builds correctly first. + echo ============================================== + pause + exit /b %errorlevel% +)