├── PropertyServer.Plugin ├── .gitignore ├── PropertyServer │ ├── properties.png │ ├── properties.xcf │ ├── Settings │ │ ├── GeneralSettings.cs │ │ └── LogLevelSetting.cs │ ├── Ui │ │ ├── RepairShakeItWindow.xaml.cs │ │ ├── IpPortValidationRule.cs │ │ ├── Converters.cs │ │ ├── SettingsControl.xaml.cs │ │ ├── RepairShakeItViewModel.cs │ │ ├── SettingsViewModel.cs │ │ ├── SettingsControl.xaml │ │ └── RepairShakeItWindow.xaml │ ├── ShakeIt │ │ ├── TreeElement.cs │ │ ├── GroupContainer.cs │ │ ├── Profile.cs │ │ ├── Converter.cs │ │ ├── EffectsContainerCollector.cs │ │ ├── EffectsContainerBase.cs │ │ └── ShakeItAccessor.cs │ ├── AutoUpdate │ │ ├── GitHubVersionInfo.cs │ │ └── AutoUpdater.cs │ ├── ISimHub.cs │ ├── RawDataManager.cs │ ├── Comm │ │ ├── Server.cs │ │ └── Client.cs │ ├── Property │ │ ├── PropertySource.cs │ │ ├── PropertyAccessor.cs │ │ └── SimHubProperty.cs │ ├── SubscriptionManager.cs │ └── PropertyServerPlugin.cs ├── packages.config ├── ComputedProperties │ ├── IScriptValidator.cs │ ├── Ui │ │ ├── PerformanceWindow.xaml.cs │ │ ├── PerformanceWindow.xaml │ │ ├── EditScriptWindow.xaml.cs │ │ ├── ComputedPropertiesViewModel.cs │ │ ├── ComputedPropertiesControl.xaml │ │ ├── EditScriptWindow.xaml │ │ ├── ComputedPropertiesControl.xaml.cs │ │ └── EditScriptWindowViewModel.cs │ ├── Performance │ │ ├── PerfData.cs │ │ └── PerfToken.cs │ ├── IComputedPropertiesManager.cs │ ├── PluginManagerAccessor.cs │ └── ScriptData.cs ├── PreCommon │ └── Ui │ │ ├── IconResources │ │ ├── InfoRounded.svg │ │ ├── ErrorRounded.svg │ │ ├── DeleteRounded.svg │ │ ├── CalculateOutlined.svg │ │ └── Calculate.svg │ │ ├── Util │ │ ├── Converters.cs │ │ ├── RelayCommand.cs │ │ └── ObservableObject.cs │ │ └── IconResources.xaml ├── Properties │ ├── Resources.Designer.cs │ └── Resources.resx └── PropertyServer.Plugin.csproj ├── .gitignore ├── doc ├── ComputedProperties │ ├── Split-RPMs.png │ ├── ETS2-Retarder.png │ ├── Control-Mapping.png │ ├── Performance-Window.png │ ├── Examples │ │ ├── ETS2 - Fuel Time.js │ │ └── ETS2 - Light Stage.js │ └── ComputedProperties.adoc ├── PropertyServer │ └── Repair-ShakeIt.png ├── SvgToWpf.adoc ├── Building.adoc └── Release.adoc ├── Directory.Build.props ├── version.json ├── copyApiFromSimHub.bat ├── deploy.bat ├── scripts └── CopyToSimHub.ps1 ├── SimHubPropertyServer.sln ├── .run └── SimHubWPF.exe.run.xml ├── README.adoc └── COPYING.LESSER /PropertyServer.Plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /obj -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /*.DotSettings.user 3 | /.vs 4 | 5 | /packages 6 | /bin 7 | /obj 8 | 9 | /SimHub 10 | -------------------------------------------------------------------------------- /doc/ComputedProperties/Split-RPMs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pre-martin/SimHubPropertyServer/HEAD/doc/ComputedProperties/Split-RPMs.png -------------------------------------------------------------------------------- /doc/PropertyServer/Repair-ShakeIt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pre-martin/SimHubPropertyServer/HEAD/doc/PropertyServer/Repair-ShakeIt.png -------------------------------------------------------------------------------- /doc/ComputedProperties/ETS2-Retarder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pre-martin/SimHubPropertyServer/HEAD/doc/ComputedProperties/ETS2-Retarder.png -------------------------------------------------------------------------------- /doc/ComputedProperties/Control-Mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pre-martin/SimHubPropertyServer/HEAD/doc/ComputedProperties/Control-Mapping.png -------------------------------------------------------------------------------- /doc/ComputedProperties/Performance-Window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pre-martin/SimHubPropertyServer/HEAD/doc/ComputedProperties/Performance-Window.png -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pre-martin/SimHubPropertyServer/HEAD/PropertyServer.Plugin/PropertyServer/properties.png -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/properties.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pre-martin/SimHubPropertyServer/HEAD/PropertyServer.Plugin/PropertyServer/properties.xcf -------------------------------------------------------------------------------- /PropertyServer.Plugin/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/SvgToWpf.adoc: -------------------------------------------------------------------------------- 1 | = SVG to WPF 2 | 3 | . Create a valid SVG file 4 | . Open the SVG file in InkScape 5 | . "Save as" - Select "*.xaml" 6 | . Export with 7 | ** "WPF" 8 | ** "DrawingImage" 9 | ** "DynamicResource" 10 | . Move data to `IconResources.xaml` 11 | 12 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/IScriptValidator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | namespace SimHub.Plugins.ComputedProperties 5 | { 6 | public interface IScriptValidator 7 | { 8 | void ValidateScript(string script); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PreCommon/Ui/IconResources/InfoRounded.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PreCommon/Ui/IconResources/ErrorRounded.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | all 6 | 3.5.119 7 | 8 | 9 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/Ui/PerformanceWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | namespace SimHub.Plugins.ComputedProperties.Ui 5 | { 6 | public partial class PerformanceWindow 7 | { 8 | public PerformanceWindow() 9 | { 10 | InitializeComponent(); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PreCommon/Ui/IconResources/DeleteRounded.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PreCommon/Ui/IconResources/CalculateOutlined.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "1.13", 4 | "publicReleaseRefSpec": [ 5 | "^refs/heads/main$", 6 | "^refs/heads/release/v\\d+(?:\\.\\d+)?$" 7 | ], 8 | "cloudBuild": { 9 | "buildNumber": { 10 | "enabled": true 11 | } 12 | }, 13 | "release": { 14 | "branchName": "release/v{version}" 15 | } 16 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PreCommon/Ui/IconResources/Calculate.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/Settings/GeneralSettings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Converters; 6 | 7 | namespace SimHub.Plugins.PropertyServer.Settings 8 | { 9 | public class GeneralSettings 10 | { 11 | public int Port { get; set; } = 18082; 12 | 13 | [JsonConverter(typeof(StringEnumConverter))] 14 | public LogLevelSetting LogLevel { get; set; } = LogLevelSetting.Info; 15 | } 16 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/Performance/PerfData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | namespace SimHub.Plugins.ComputedProperties.Performance 5 | { 6 | /// 7 | /// Collects performance data. 8 | /// 9 | public class PerfData 10 | { 11 | public int Calls { get; set; } 12 | public int Skipped { get; set; } 13 | public double Time { get; set; } 14 | public double Duration => Calls == 0 ? 0 : Time / Calls; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /copyApiFromSimHub.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set SIMHUB_DIR=\Games\SimHub 4 | 5 | del /q SimHub\* 6 | 7 | copy "%SIMHUB_DIR%\SimHub.Plugins.dll" SimHub 8 | copy "%SIMHUB_DIR%\GameReaderCommon.dll" SimHub 9 | copy "%SIMHUB_DIR%\log4net.dll" SimHub 10 | copy "%SIMHUB_DIR%\Newtonsoft.Json.dll" SimHub 11 | copy "%SIMHUB_DIR%\MahApps.Metro.dll" SimHub 12 | copy "%SIMHUB_DIR%\MahApps.Metro.SimpleChildWindow.dll" SimHub 13 | 14 | copy "%SIMHUB_DIR%\Jint.dll" SimHub 15 | copy "%SIMHUB_DIR%\Acornima.dll" SimHub 16 | copy "%SIMHUB_DIR%\ICSharpCode.AvalonEdit.dll" SimHub 17 | -------------------------------------------------------------------------------- /doc/Building.adoc: -------------------------------------------------------------------------------- 1 | = Building 2 | 3 | . The project requires .NET Framework 4.8, because SimHub is built with this framework. 4 | . Copy required DLLs from SimHub to the local directory `SimHub` (see `copyApiFromSimHub.bat`): 5 | - SimHub.Plugins.dll 6 | - GameReaderCommon.dll 7 | - log4net.dll 8 | - Newtonsoft.Json.dll 9 | - MahApps.Metro.dll 10 | - MahApps.Metro.SimpleChildWindow.dll 11 | - Jint.dll 12 | - Acornima.dll 13 | - ICSharpCode.AvalonEdit.dll 14 | . Restore NuGet packages: + 15 | `msbuild -t:restore -p:Platform="Any CPU" -p:RestorePackagesConfig=true` 16 | . Build the project: + 17 | `msbuild -p:Platform="Any CPU" -p:Configuration=Release` 18 | -------------------------------------------------------------------------------- /deploy.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | rem Script to deploy locally. 5 | rem If SimHub is started with admin privileges, the script has to be started as admin, too. 6 | 7 | set CONFIG=Release 8 | if "%1%" == "debug" set CONFIG=Debug 9 | 10 | echo. 11 | echo Building for configuration: %CONFIG% 12 | echo. 13 | 14 | "C:\Program Files\Microsoft Visual Studio\2022\Community\Msbuild\Current\Bin\amd64\MSBuild.exe" -p:Configuration=%CONFIG% SimHubPropertyServer.sln 15 | if %errorlevel% neq 0 exit /b 1 16 | 17 | taskkill /im SimHubWPF.exe /t /f 18 | timeout /t 1 19 | 20 | copy /y PropertyServer.Plugin\bin\%CONFIG%\PropertyServer.dll \Games\SimHub\ 21 | 22 | start /d \Games\SimHub SimHubWPF.exe 23 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/Ui/RepairShakeItWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System.Windows; 5 | using SimHub.Plugins.PropertyServer.ShakeIt; 6 | 7 | namespace SimHub.Plugins.PropertyServer.Ui 8 | { 9 | public partial class RepairShakeItWindow 10 | { 11 | public RepairShakeItWindow() 12 | { 13 | InitializeComponent(); 14 | 15 | var shakeItBassAccessor = new ShakeItAccessor(); 16 | ((RepairShakeItViewModel)DataContext).ShakeItAccessor = shakeItBassAccessor; 17 | } 18 | 19 | private void CloseButton_Click(object sender, RoutedEventArgs e) 20 | { 21 | Close(null); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/Settings/LogLevelSetting.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using log4net.Core; 5 | 6 | namespace SimHub.Plugins.PropertyServer.Settings 7 | { 8 | public enum LogLevelSetting 9 | { 10 | Debug, 11 | Info 12 | } 13 | 14 | public static class LogLevelSettingEx 15 | { 16 | public static Level ToLog4Net(this LogLevelSetting setting) 17 | { 18 | switch (setting) 19 | { 20 | case LogLevelSetting.Debug: 21 | return Level.Debug; 22 | case LogLevelSetting.Info: 23 | return Level.Info; 24 | default: 25 | return Level.Info; 26 | } 27 | } 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/ShakeIt/TreeElement.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | namespace SimHub.Plugins.PropertyServer.ShakeIt 5 | { 6 | /// 7 | /// This class describes an element in a tree structure. 8 | /// 9 | public abstract class TreeElement 10 | { 11 | protected TreeElement(TreeElement parent) 12 | { 13 | Parent = parent; 14 | } 15 | 16 | /// 17 | /// Parent of this element or null if this element is at the root. 18 | /// 19 | public TreeElement Parent { get; } 20 | 21 | /// 22 | /// Returns the name of this element, including all names of all parent elements. 23 | /// 24 | public abstract string RecursiveName { get; } 25 | } 26 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PreCommon/Ui/Util/Converters.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Globalization; 6 | using System.Windows; 7 | using System.Windows.Data; 8 | 9 | namespace SimHub.Plugins.PreCommon.Ui.Util 10 | { 11 | public class BooleanToVisibilityConverter : IValueConverter 12 | { 13 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 14 | { 15 | var boolValue = value != null && (bool)value; 16 | boolValue = (parameter != null && parameter.ToString().ToLower() == "negate") ? !boolValue : boolValue; 17 | return boolValue ? Visibility.Visible : Visibility.Collapsed; 18 | } 19 | 20 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 21 | { 22 | throw new NotImplementedException(); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /scripts/CopyToSimHub.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Martin Renner 2 | # LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | # Include as "Before launch" task into IDE. For example: 5 | # - Tool: pwsh 6 | # - Args: -NoProfile -File "$ProjectFileDir$/scripts/CopyToSimHub.ps1" -Configuration "$ConfigurationName$" 7 | 8 | param( 9 | [string]$Configuration = "Debug", 10 | [string]$TargetDir = "/Games/SimHub" 11 | ) 12 | 13 | $ErrorActionPreference = "Stop" 14 | 15 | $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path 16 | $ProjectDir = (Get-Item $ScriptDir).Parent 17 | $SourceDll = Join-Path $ProjectDir "PropertyServer.Plugin/bin/$Configuration/PropertyServer.dll" 18 | 19 | if (-not (Test-Path $TargetDir)) { 20 | Write-Host "Target directory $TargetDir does not exist." 21 | Exit 1 22 | } 23 | 24 | try { 25 | Copy-Item -Path $SourceDll -Destination $TargetDir -Force 26 | Write-Host "Copied $SourceDll to $TargetDir" 27 | } 28 | catch { 29 | Write-Error $_ 30 | Exit 1 31 | } 32 | 33 | Exit 0 34 | -------------------------------------------------------------------------------- /doc/ComputedProperties/Examples/ETS2 - Fuel Time.js: -------------------------------------------------------------------------------- 1 | // v1 Calculates the estimated time remaining until the fuel tank is empty based on current speed and fuel range. 2 | 3 | const FUEL_TIME_PROP = 'ComputedPropertiesPlugin.ETS2.FuelTimeRemaining'; 4 | 5 | function init() { 6 | createProperty(FUEL_TIME_PROP); 7 | subscribe('DataCorePlugin.GameRawData.TruckValues.CurrentValues.DashboardValues.FuelValue.Range', 'calculateFuelTime'); 8 | } 9 | 10 | function calculateFuelTime() { 11 | // Math.floor() to avoid an excessivly high update frequency 12 | const fuelDistance = Math.floor(getPropertyValue('DataCorePlugin.GameRawData.TruckValues.CurrentValues.DashboardValues.FuelValue.Range')); 13 | var speed = Math.floor(getPropertyValue('SpeedKmh')); 14 | speed = roundUpToFive(speed); // Reduce oscillation by rounding up in increments of 5 (65, 70, 75, 80, ...) 15 | if (speed < 30) speed = 30; 16 | 17 | const fuelTime = fuelDistance / speed; 18 | 19 | setPropertyValue(FUEL_TIME_PROP, fuelTime); 20 | } 21 | 22 | const roundUpToFive = (num) => Math.ceil(num / 5) * 5; 23 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/Ui/IpPortValidationRule.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System.Globalization; 5 | using System.Text.RegularExpressions; 6 | using System.Windows.Controls; 7 | 8 | namespace SimHub.Plugins.PropertyServer.Ui 9 | { 10 | public class IpPortValidationRule : ValidationRule 11 | { 12 | // regex to match: 13 | // - 1 to 4 digit inputs (may start with 1-9) 14 | // - 5 digit inputs (may start with 1-5) 15 | // - special cases for 65, 655 and 6553 16 | private readonly Regex _portRegex = 17 | new Regex("^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$"); 18 | 19 | 20 | public override ValidationResult Validate(object value, CultureInfo cultureInfo) 21 | { 22 | return _portRegex.IsMatch(value as string ?? string.Empty) 23 | ? new ValidationResult(true, null) 24 | : new ValidationResult(false, "Only 1 to 65535 is valid"); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/Performance/PerfToken.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Diagnostics; 6 | 7 | namespace SimHub.Plugins.ComputedProperties.Performance 8 | { 9 | /// 10 | /// Simple helper to measure performance data. Can be used in a using directive, which starts the stopwatch. 11 | /// The stopwatch will be stopped, when the using context is exited. 12 | /// 13 | public class PerfToken : IDisposable 14 | { 15 | private readonly Stopwatch _stopwatch; 16 | private readonly PerfData _perfData; 17 | 18 | public PerfToken(PerfData perfData) 19 | { 20 | _stopwatch = Stopwatch.StartNew(); 21 | _perfData = perfData; 22 | } 23 | 24 | public void Dispose() 25 | { 26 | _stopwatch.Stop(); 27 | var ms = _stopwatch.Elapsed.TotalMilliseconds; 28 | _perfData.Calls++; 29 | _perfData.Time += _stopwatch.Elapsed.TotalMilliseconds; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PreCommon/Ui/Util/RelayCommand.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Windows.Input; 6 | 7 | namespace SimHub.Plugins.PreCommon.Ui.Util 8 | { 9 | public class RelayCommand : ICommand 10 | { 11 | private readonly Action _execute; 12 | private readonly Predicate _canExecute; 13 | 14 | public RelayCommand(Action execute) : this(null, execute) 15 | { 16 | } 17 | 18 | public RelayCommand(Predicate canExecute, Action execute) 19 | { 20 | _canExecute = canExecute; 21 | _execute = execute; 22 | } 23 | 24 | public bool CanExecute(object parameter) 25 | { 26 | return _canExecute?.Invoke((T)parameter) ?? true; 27 | } 28 | 29 | public void Execute(object parameter) 30 | { 31 | _execute((T)parameter); 32 | } 33 | 34 | public event EventHandler CanExecuteChanged 35 | { 36 | add => CommandManager.RequerySuggested += value; 37 | remove => CommandManager.RequerySuggested -= value; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /SimHubPropertyServer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32929.385 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PropertyServer.Plugin", "PropertyServer.Plugin\PropertyServer.Plugin.csproj", "{B3A7DBD0-917C-4FF1-9AAB-D3DC6ED29AE2}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {B3A7DBD0-917C-4FF1-9AAB-D3DC6ED29AE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {B3A7DBD0-917C-4FF1-9AAB-D3DC6ED29AE2}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {B3A7DBD0-917C-4FF1-9AAB-D3DC6ED29AE2}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {B3A7DBD0-917C-4FF1-9AAB-D3DC6ED29AE2}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {9DEB1B20-2193-479A-9EA4-40EDF63A2982} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/IComputedPropertiesManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using Jint; 6 | 7 | namespace SimHub.Plugins.ComputedProperties 8 | { 9 | public interface IComputedPropertiesManager 10 | { 11 | object GetPropertyValue(string propertyName); 12 | object GetRawData(); 13 | void CreateProperty(string propertyName); 14 | void SetPropertyValue(string propertyName, object value); 15 | void StartRole(string roleName); 16 | void StopRole(string roleName); 17 | void TriggerInputPress(string inputName); 18 | void TriggerInputRelease(string inputName); 19 | 20 | void PrepareEngine( 21 | Engine engine, 22 | Func getRawData, 23 | Action log, 24 | Action createProperty, 25 | Action subscribe, 26 | Func getPropertyValue, 27 | Action setPropertyValue, 28 | Action startRole, 29 | Action stopRole, 30 | Action triggerInputPress, 31 | Action triggerInputRelease); 32 | 33 | void SaveScripts(); 34 | } 35 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/ShakeIt/GroupContainer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace SimHub.Plugins.PropertyServer.ShakeIt 7 | { 8 | /// 9 | /// Wrapper for the SimHub class "GroupContainer". 10 | /// 11 | /// 12 | /// Modifications to the structure are not supported! Most properties are read-only. 13 | /// 14 | public class GroupContainer : EffectsContainerBase 15 | { 16 | private readonly DataPlugins.ShakeItV3.EffectsContainers.GroupContainer _simHubGroupContainer; 17 | private readonly List _effectsContainers = new List(); 18 | 19 | public GroupContainer(TreeElement parent, DataPlugins.ShakeItV3.EffectsContainers.GroupContainer simHubGroupContainer) 20 | : base(parent, simHubGroupContainer) 21 | { 22 | _simHubGroupContainer = simHubGroupContainer; 23 | Converter.Convert(this, simHubGroupContainer.EffectsContainers, _effectsContainers); 24 | } 25 | 26 | public IList EffectsContainers => _effectsContainers.AsReadOnly(); 27 | 28 | public override string FullName() => Description; 29 | } 30 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/AutoUpdate/GitHubVersionInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System.Collections.Generic; 5 | using Newtonsoft.Json; 6 | 7 | namespace SimHub.Plugins.PropertyServer.AutoUpdate 8 | { 9 | public class GitHubVersionInfo 10 | { 11 | [JsonProperty("tag_name", Required = Required.Always)] 12 | public string RawTagName { get; set; } 13 | 14 | [JsonIgnore] 15 | public string TagName => RawTagName.TrimStart('v', 'V'); 16 | 17 | [JsonProperty("draft")] 18 | public bool Draft { get; set; } 19 | 20 | [JsonProperty("prerelease")] 21 | public bool Prerelease { get; set; } 22 | 23 | [JsonProperty("assets")] 24 | public List Assets { get; set; } 25 | } 26 | 27 | public class GitHubAsset 28 | { 29 | [JsonProperty("name", Required = Required.Always)] 30 | public string Name { get; set; } 31 | 32 | [JsonProperty("size")] 33 | public long Size { get; set; } 34 | 35 | [JsonProperty("digest", Required = Required.Always)] 36 | public string Digest { get; set; } 37 | 38 | [JsonProperty("browser_download_url", Required = Required.Always)] 39 | public string BrowserDownloadUrl { get; set; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.run/SimHubWPF.exe.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 21 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PreCommon/Ui/Util/ObservableObject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | using System.Runtime.CompilerServices; 7 | 8 | namespace SimHub.Plugins.PreCommon.Ui.Util 9 | { 10 | public class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging 11 | { 12 | public event PropertyChangedEventHandler PropertyChanged; 13 | 14 | protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) 15 | { 16 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 17 | } 18 | 19 | public event PropertyChangingEventHandler PropertyChanging; 20 | 21 | protected virtual void OnPropertyChanging([CallerMemberName] string propertyName = null) 22 | { 23 | PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName)); 24 | } 25 | 26 | protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string propertyName = null) 27 | { 28 | if (EqualityComparer.Default.Equals(field, newValue)) return false; 29 | 30 | OnPropertyChanging(propertyName); 31 | 32 | field = newValue; 33 | 34 | OnPropertyChanged(propertyName); 35 | 36 | return true; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/ShakeIt/Profile.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using SimHub.Plugins.DataPlugins.ShakeItV3.Settings; 7 | 8 | namespace SimHub.Plugins.PropertyServer.ShakeIt 9 | { 10 | /// 11 | /// Wrapper for the SimHub class "ShakeItProfile". 12 | /// 13 | /// 14 | /// Modifications to the structure are not supported! Most properties are read-only. 15 | /// 16 | public class Profile : TreeElement 17 | { 18 | private readonly ShakeItProfile _simHubShakeItProfile; 19 | private readonly List _effectsContainers = new List(); 20 | 21 | public Profile(ShakeItProfile simHubShakeItProfile) : base(null) 22 | { 23 | _simHubShakeItProfile = simHubShakeItProfile; 24 | Converter.Convert(this, simHubShakeItProfile.EffectsContainers, _effectsContainers); 25 | } 26 | 27 | public Guid ProfileId 28 | { 29 | get => _simHubShakeItProfile.ProfileId; 30 | set => _simHubShakeItProfile.ProfileId = value; 31 | } 32 | 33 | public string Name => _simHubShakeItProfile.Name; 34 | 35 | public IList EffectsContainers => _effectsContainers.AsReadOnly(); 36 | 37 | public override string RecursiveName => Name; 38 | } 39 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/Ui/Converters.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Globalization; 6 | using System.Windows.Data; 7 | using System.Windows.Media; 8 | 9 | namespace SimHub.Plugins.PropertyServer.Ui 10 | { 11 | /// 12 | /// Takes two booleans (IsNewVersionAvailable and IsVersionCheckError) and determines the background color 13 | /// for the "new version" label. 14 | /// 15 | public class VersionCheckMessageBackgroundConverter : IMultiValueConverter 16 | { 17 | public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) 18 | { 19 | if (values.Length < 2) 20 | { 21 | return Brushes.Transparent; 22 | } 23 | 24 | var isNewVersionAvailable = values[0] is bool b1 && b1; 25 | var isVersionCheckError = values[1] is bool b2 && b2; 26 | if (isVersionCheckError) 27 | { 28 | return Brushes.Transparent; 29 | } 30 | 31 | return isNewVersionAvailable 32 | ? new SolidColorBrush(Color.FromRgb(170, 80, 80)) 33 | : new SolidColorBrush(Color.FromRgb(30, 120, 30)); 34 | } 35 | 36 | public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) 37 | { 38 | throw new NotImplementedException(); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/ShakeIt/Converter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System.Collections.Generic; 5 | using System.Collections.ObjectModel; 6 | 7 | namespace SimHub.Plugins.PropertyServer.ShakeIt 8 | { 9 | /// 10 | /// Class responsible for converting from the SimHub internal data model into our wrapped model. 11 | /// 12 | public abstract class Converter 13 | { 14 | 15 | public static void Convert(TreeElement parent, ObservableCollection simHubEffectsContainers, IList effectsContainers) 16 | { 17 | foreach (var simHubEffectsContainer in simHubEffectsContainers) 18 | { 19 | var effectsContainer = Convert(parent, simHubEffectsContainer); 20 | effectsContainers.Add(effectsContainer); 21 | } 22 | } 23 | 24 | public static EffectsContainerBase Convert(TreeElement parent, DataPlugins.ShakeItV3.EffectsContainers.EffectsContainerBase simHubEffectsContainer) 25 | { 26 | if (simHubEffectsContainer is DataPlugins.ShakeItV3.EffectsContainers.GroupContainer simHubGroupContainer) 27 | { 28 | var groupContainer = new GroupContainer(parent, simHubGroupContainer); 29 | return groupContainer; 30 | } 31 | 32 | var effectsContainerBase = new EffectsContainerBase(parent, simHubEffectsContainer); 33 | return effectsContainerBase; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /doc/ComputedProperties/Examples/ETS2 - Light Stage.js: -------------------------------------------------------------------------------- 1 | // v1 - Light Stage Control for ETS2. Controls parking lights, low beam and high beam lights. 2 | 3 | function init() { 4 | } 5 | 6 | function lightDec() { 7 | const parking = getPropertyValue('GameRawData.TruckValues.CurrentValues.LightsValues.Parking'); 8 | const beamLow = getPropertyValue('GameRawData.TruckValues.CurrentValues.LightsValues.BeamLow'); 9 | const beamHigh = getPropertyValue('GameRawData.TruckValues.CurrentValues.LightsValues.BeamHigh'); 10 | 11 | if (parking === true && beamLow === true && beamHigh === true) { // High Beam -> Low Beam 12 | startRole('RainLight'); 13 | stopRole('RainLight'); 14 | } else if (beamLow === true || parking === true) { // Low Beam -> Off 15 | startRole('Headlights'); 16 | stopRole('Headlights'); 17 | } 18 | } 19 | 20 | function lightInc() { 21 | const parking = getPropertyValue('GameRawData.TruckValues.CurrentValues.LightsValues.Parking'); 22 | const beamLow = getPropertyValue('GameRawData.TruckValues.CurrentValues.LightsValues.BeamLow'); 23 | const beamHigh = getPropertyValue('GameRawData.TruckValues.CurrentValues.LightsValues.BeamHigh'); 24 | 25 | if (parking === false && beamLow === false) { // Off -> Parking 26 | startRole('Headlights'); 27 | stopRole('Headlights'); 28 | } else if (parking === true && beamLow === false) { // Parking -> Low Beam 29 | startRole('Headlights'); 30 | stopRole('Headlights'); 31 | } else if (parking === true && beamLow === true && beamHigh === false) { // Low Beam -> High Beam 32 | startRole('RainLight'); 33 | stopRole('RainLight'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/PluginManagerAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Reflection; 7 | 8 | namespace SimHub.Plugins.ComputedProperties 9 | { 10 | /// 11 | /// Access to private fields of the PluginManager and its related classes. 12 | /// 13 | public class PluginManagerAccessor 14 | { 15 | private IDictionary _generatedProperties; 16 | private PropertyInfo _propertySupportStatus; 17 | 18 | public void Init(PluginManager pluginManager) 19 | { 20 | var generatedPropertiesField = typeof(PluginManager).GetField("GeneratedProperties", BindingFlags.NonPublic | BindingFlags.Instance); 21 | if (generatedPropertiesField != null) 22 | { 23 | var dictObj = generatedPropertiesField.GetValue(pluginManager); 24 | if (dictObj is IDictionary dict) 25 | { 26 | _generatedProperties = dict; 27 | } 28 | } 29 | 30 | _propertySupportStatus = typeof(PropertyEntry).GetProperty(nameof(SupportStatus)); 31 | } 32 | 33 | public void SetPropertySupportStatus(string name, Type pluginType, SupportStatus status) 34 | { 35 | if (_generatedProperties == null || _propertySupportStatus == null) return; 36 | 37 | var propName = (pluginType.Name + '.' + name).ToLowerInvariant(); 38 | if (_generatedProperties.TryGetValue(propName, out var propertyEntry)) 39 | { 40 | _propertySupportStatus.SetValue(propertyEntry, status); 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /doc/Release.adoc: -------------------------------------------------------------------------------- 1 | = Development and Release Process 2 | 3 | == Development 4 | 5 | This project uses the Gitflow workflow. Although this workflow is a little bit more complex than e.g. the GitHub workflow, it assures that only tagged versions are on the main branch. Thus, our users always get the correct documentation for the latest version without having to switch branches in the UI. 6 | 7 | The whole development happens on the `develop` branch. New features are started from the `develop` branch and merged back into it when they are finished. 8 | 9 | 10 | == Release 11 | 12 | When a new version is ready to be released, the following steps are taken: 13 | 14 | . Switch to the `develop` branch: + 15 | `git switch develop` 16 | . Create a release branch based on `version.json`: + 17 | `nbgv prepare-release` 18 | . Push the `develop` branch as nbgv updated the version number: + 19 | `git push` 20 | . Switch to the release branch: + 21 | `git switch release/v1.2` 22 | . Push the release branch: + 23 | `git push origin release/v1.2` 24 | 25 | The release branch can be used to further stabilize the release. If there are any changes made to the release branch, they should be merged back into the `develop` branch as well. 26 | 27 | When the release is ready, the following steps are taken: 28 | 29 | . Merge the release branch into the `main` branch: + 30 | - `git switch main` 31 | - `git merge --no-ff release/v1.2` 32 | - `git push` 33 | . Create a tag and push it afterward: 34 | - `nbgv tag` 35 | - `git push origin v1.2.3` 36 | . Build the plugin: 37 | - see link:Building.adoc[Building.adoc] 38 | . Create a release in GitHub from the tag and attach the file `PropertyServer.dll` 39 | . Merge the main branch back into the `develop` branch: + 40 | `git switch develop` + 41 | `git merge --no-ff main` + 42 | `git push` 43 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/ShakeIt/EffectsContainerCollector.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace SimHub.Plugins.PropertyServer.ShakeIt 8 | { 9 | /// 10 | /// Helper class to collect ShakeIt effect groups and effects by given criteria. 11 | /// 12 | public class EffectsContainerCollector 13 | { 14 | private Dictionary> GuidToEffect { get; } = new Dictionary>(); 15 | 16 | /// 17 | /// Groups all effect groups and effects of a given profile by their Guid. 18 | /// 19 | /// 20 | /// In theory, there should be no duplicates (i.e. more than one element for a given Guid). Practically, this happens. 21 | /// 22 | public Dictionary> ByGuid(Profile profile) 23 | { 24 | CollectByGuid(profile.EffectsContainers); 25 | return GuidToEffect; 26 | } 27 | 28 | private void CollectByGuid(IEnumerable effectsContainers) 29 | { 30 | foreach (var effectsContainer in effectsContainers) 31 | { 32 | if (!GuidToEffect.ContainsKey(effectsContainer.ContainerId)) 33 | { 34 | GuidToEffect[effectsContainer.ContainerId] = new List(); 35 | } 36 | 37 | GuidToEffect[effectsContainer.ContainerId].Add(effectsContainer); 38 | 39 | if (effectsContainer is GroupContainer groupContainer) 40 | { 41 | CollectByGuid(groupContainer.EffectsContainers); 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/Ui/SettingsControl.xaml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System.Windows; 5 | using System.Windows.Forms; 6 | using System.Windows.Input; 7 | using SimHub.Plugins.Styles; 8 | 9 | namespace SimHub.Plugins.PropertyServer.Ui 10 | { 11 | public partial class SettingsControl 12 | { 13 | public SettingsControl() 14 | { 15 | InitializeComponent(); 16 | } 17 | 18 | private SettingsViewModel ViewModel => (SettingsViewModel)DataContext; 19 | 20 | private void RepairButton_Click(object sender, RoutedEventArgs e) 21 | { 22 | var repairShakeItWindow = new RepairShakeItWindow(); 23 | Configuration.ShowChildWindow(this, repairShakeItWindow, null); 24 | } 25 | 26 | private async void UpdateButton_Click(object sender, RoutedEventArgs e) 27 | { 28 | var result = await SHMessageBox.Show( 29 | $"This will download a new version of the plugin.\n" + 30 | "SimHub will restart automatically after the update.\n" + 31 | "The release notes can be found at https://github.com/pre-martin/SimHubPropertyServer/releases.\n\n" + 32 | "Be sure to check if there is also a new version of the Stream Deck plugin available!", 33 | "Confirm download", MessageBoxButton.OKCancel, MessageBoxImage.Information); 34 | if (result != DialogResult.OK) return; 35 | 36 | await ViewModel.Update(); 37 | } 38 | 39 | private async void SecretArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) 40 | { 41 | if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) 42 | { 43 | await ViewModel.CheckForNewVersion(true); 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/Ui/PerformanceWindow.xaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/Ui/EditScriptWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Windows; 6 | using System.Windows.Forms; 7 | using MahApps.Metro.Controls; 8 | using MahApps.Metro.Controls.Dialogs; 9 | using SimHub.Plugins.OutputPlugins.Dash.WPFUI; 10 | 11 | namespace SimHub.Plugins.ComputedProperties.Ui 12 | { 13 | public partial class EditScriptWindow 14 | { 15 | 16 | public EditScriptWindow() 17 | { 18 | InitializeComponent(); 19 | ShowOk = true; 20 | ShowCancel = true; 21 | } 22 | 23 | private EditScriptWindowViewModel ViewModel => (EditScriptWindowViewModel)DataContext; 24 | 25 | public override string Title => "Edit script"; 26 | 27 | private void TextEditor_OnTextChanged(object sender, EventArgs e) 28 | { 29 | ViewModel?.OnScriptChanged(CodeEditor.Text); 30 | } 31 | 32 | private async void InsertProperty_Click(object sender, RoutedEventArgs e) 33 | { 34 | var pp = new PropertiesPicker(); 35 | var result = await pp.ShowDialogAsync(this); 36 | if (result == DialogResult.OK) 37 | { 38 | if (pp.Result != null) 39 | { 40 | CodeEditor.SelectedText = $"'{pp.Result.GetPropertyName()}'"; 41 | } 42 | } 43 | } 44 | 45 | private async void InsertSample_Click(object sender, RoutedEventArgs e) 46 | { 47 | var result = await (Window.GetWindow(this) as MetroWindow).ShowMessageAsync("Insert Sample", "This will replace your current script with the sample script. Continue?", MessageDialogStyle.AffirmativeAndNegative); 48 | if (result == MessageDialogResult.Affirmative) 49 | { 50 | ViewModel.InsertSample(); 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/ISimHub.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using SimHub.Plugins.PropertyServer.ShakeIt; 7 | 8 | namespace SimHub.Plugins.PropertyServer 9 | { 10 | /// 11 | /// Interface used to communicate with SimHub. 12 | /// 13 | public interface ISimHub 14 | { 15 | /// 16 | /// Triggers an input in SimHub. 17 | /// 18 | void TriggerInput(string inputName); 19 | 20 | /// 21 | /// Triggers the start of an input in SimHub. 22 | /// 23 | void TriggerInputPressed(string inputName); 24 | 25 | /// 26 | /// Triggers the end of an input in SimHub. 27 | /// 28 | void TriggerInputReleased(string inputName); 29 | 30 | /// 31 | /// Returns the structure of the ShakeIt Bass configuration (profiles with effect groups and effects). 32 | /// 33 | ICollection ShakeItBassStructure(); 34 | 35 | /// 36 | /// Tries to find a ShakeIt Bass effect or effect group with the given id. 37 | /// 38 | /// null if nothing is found. 39 | EffectsContainerBase FindShakeItBassEffect(Guid id); 40 | 41 | /// 42 | /// Returns the structure of the ShakeIt Motors configuration (profiles with effect groups and effects). 43 | /// 44 | ICollection ShakeItMotorsStructure(); 45 | 46 | /// 47 | /// Tries to find a ShakeIt Motors effect or effect group with the given id. 48 | /// 49 | /// null if nothing is found. 50 | EffectsContainerBase FindShakeItMotorsEffect(Guid id); 51 | 52 | /// 53 | /// Restart SimHub. 54 | /// 55 | void RestartSimHub(); 56 | } 57 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/ShakeIt/EffectsContainerBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | 6 | namespace SimHub.Plugins.PropertyServer.ShakeIt 7 | { 8 | /// 9 | /// Wrapper on the class ShakeIt Bass EffectsContainerBase. 10 | /// 11 | /// 12 | /// The corresponding SimHub class is abstract and has subclasses for each concrete effect. As we are not interested in the concrete 13 | /// effects, we use this class also for the effects, as it already contains all the data we are interested in. 14 | ///

15 | /// Most properties are read-only. 16 | /// 17 | public class EffectsContainerBase : TreeElement 18 | { 19 | private readonly DataPlugins.ShakeItV3.EffectsContainers.EffectsContainerBase _simHubEffectsContainerBase; 20 | 21 | public EffectsContainerBase(TreeElement parent, 22 | DataPlugins.ShakeItV3.EffectsContainers.EffectsContainerBase simHubEffectsContainerBase) : base(parent) 23 | { 24 | _simHubEffectsContainerBase = simHubEffectsContainerBase; 25 | } 26 | 27 | public Guid ContainerId 28 | { 29 | get => _simHubEffectsContainerBase.ContainerId; 30 | set => _simHubEffectsContainerBase.ContainerId = value; 31 | } 32 | 33 | public string ContainerName => _simHubEffectsContainerBase.ContainerName; 34 | public string Description => _simHubEffectsContainerBase.Description; 35 | public double Gain => _simHubEffectsContainerBase.Gain; 36 | public bool IsMuted => _simHubEffectsContainerBase.IsMuted; 37 | 38 | public virtual string FullName() 39 | { 40 | return string.IsNullOrWhiteSpace(Description) ? ContainerName : ContainerName + $" ({Description})"; 41 | } 42 | 43 | public override string RecursiveName 44 | { 45 | get 46 | { 47 | if (Parent == null) 48 | { 49 | return Description.Trim(); 50 | } 51 | 52 | return Parent.RecursiveName + " / " + Description; 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/Ui/ComputedPropertiesViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Collections.ObjectModel; 6 | using SimHub.Plugins.PreCommon.Ui.Util; 7 | 8 | namespace SimHub.Plugins.ComputedProperties.Ui 9 | { 10 | public class ComputedPropertiesViewModel : ObservableObject 11 | { 12 | public ComputedPropertiesViewModel(ObservableCollection scripts, IScriptValidator scriptValidator) 13 | { 14 | Scripts = scripts; 15 | ScriptValidator = scriptValidator; 16 | } 17 | 18 | ///

19 | /// For IDE only 20 | /// 21 | public ComputedPropertiesViewModel() : this(new ObservableCollection(), null) 22 | { 23 | } 24 | 25 | public IScriptValidator ScriptValidator { get; } 26 | 27 | public ObservableCollection Scripts { get; } 28 | 29 | private ScriptData _selectedScript; 30 | 31 | public ScriptData SelectedScript 32 | { 33 | get => _selectedScript; 34 | set => SetProperty(ref _selectedScript, value); 35 | } 36 | 37 | public void DeleteScript(ScriptData scriptData) 38 | { 39 | Scripts.Remove(scriptData); 40 | } 41 | 42 | public void AddScript(ScriptData scriptData) 43 | { 44 | // Insert at the correct sorted position 45 | var index = 0; 46 | while (index < Scripts.Count && 47 | string.Compare(Scripts[index].Name, scriptData.Name, StringComparison.OrdinalIgnoreCase) < 0) 48 | { 49 | index++; 50 | } 51 | 52 | Scripts.Insert(index, scriptData); 53 | SelectedScript = scriptData; 54 | } 55 | 56 | public void UpdateScript(ScriptData scriptData) 57 | { 58 | // Remove and re-insert to update position if name changed 59 | if (Scripts.Contains(scriptData)) 60 | { 61 | Scripts.Remove(scriptData); 62 | AddScript(scriptData); 63 | SelectedScript = scriptData; 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/RawDataManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System.Reflection; 5 | using log4net; 6 | 7 | namespace SimHub.Plugins.PropertyServer 8 | { 9 | /// 10 | /// Handles the access to game specific raw data. 11 | /// 12 | public class RawDataManager 13 | { 14 | private static readonly ILog Log = LogManager.GetLogger(typeof(RawDataManager)); 15 | 16 | private string _currentRawDataType; 17 | private FieldInfo _accGraphicsField; 18 | private FieldInfo _accPhysicsField; 19 | 20 | public object AccGraphics; 21 | public object AccPhysics; 22 | 23 | /// 24 | /// Has to be called in the game loop. Only after a call to this method, the properties in this class will return 25 | /// valid data. 26 | /// 27 | public void UpdateObjects(object rawData) 28 | { 29 | if (rawData == null) 30 | { 31 | Reset(); 32 | return; 33 | } 34 | 35 | var rawDataType = rawData.GetType().FullName; 36 | if (rawDataType != null && rawDataType != _currentRawDataType) 37 | { 38 | // Type of raw data has changed. We have a new game, so configure the access via reflection now. 39 | Reset(); 40 | _currentRawDataType = rawDataType; 41 | Log.Info("Detected a new game"); 42 | 43 | if (rawDataType.EndsWith("ACCRawData")) 44 | { 45 | // Assetto Corsa Competizione 46 | Log.Info("New game is ACC"); 47 | _accGraphicsField = rawData.GetType().GetField("Graphics"); 48 | _accPhysicsField = rawData.GetType().GetField("Physics"); 49 | } 50 | else 51 | { 52 | Log.Info($"Don't know how to handle {rawDataType}. Access to raw data is not possible."); 53 | } 54 | } 55 | 56 | AccGraphics = _accGraphicsField?.GetValue(rawData); 57 | AccPhysics = _accPhysicsField?.GetValue(rawData); 58 | } 59 | 60 | private void Reset() 61 | { 62 | _currentRawDataType = null; 63 | _accGraphicsField = null; 64 | _accPhysicsField = null; 65 | AccGraphics = null; 66 | AccPhysics = null; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // 5 | // Changes to this file may cause incorrect behavior and will be lost if 6 | // the code is regenerated. 7 | // 8 | //------------------------------------------------------------------------------ 9 | 10 | namespace SimHub.Plugins.PropertyServer.Properties { 11 | using System; 12 | 13 | 14 | /// 15 | /// A strongly-typed resource class, for looking up localized strings, etc. 16 | /// 17 | // This class was auto-generated by the StronglyTypedResourceBuilder 18 | // class via a tool like ResGen or Visual Studio. 19 | // To add or remove a member, edit your .ResX file then rerun ResGen 20 | // with the /str option, or rebuild your VS project. 21 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 22 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 23 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 24 | internal class Resources { 25 | 26 | private static global::System.Resources.ResourceManager resourceMan; 27 | 28 | private static global::System.Globalization.CultureInfo resourceCulture; 29 | 30 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 31 | internal Resources() { 32 | } 33 | 34 | /// 35 | /// Returns the cached ResourceManager instance used by this class. 36 | /// 37 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 38 | internal static global::System.Resources.ResourceManager ResourceManager { 39 | get { 40 | if (object.ReferenceEquals(resourceMan, null)) { 41 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SimHub.Plugins.Properties.Resources", typeof(Resources).Assembly); 42 | resourceMan = temp; 43 | } 44 | return resourceMan; 45 | } 46 | } 47 | 48 | /// 49 | /// Overrides the current thread's CurrentUICulture property for all 50 | /// resource lookups using this strongly typed resource class. 51 | /// 52 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 53 | internal static global::System.Globalization.CultureInfo Culture { 54 | get { 55 | return resourceCulture; 56 | } 57 | set { 58 | resourceCulture = value; 59 | } 60 | } 61 | 62 | /// 63 | /// Looks up a localized resource of type System.Drawing.Bitmap. 64 | /// 65 | internal static System.Drawing.Bitmap properties { 66 | get { 67 | object obj = ResourceManager.GetObject("properties", resourceCulture); 68 | return ((System.Drawing.Bitmap)(obj)); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = SimHub Property Server & Computed Properties 2 | :toc: 3 | :sectnums: 4 | ifdef::env-github[] 5 | :tip-caption: :bulb: 6 | :warning-caption: :warning: 7 | endif::[] 8 | ifndef::env-github[] 9 | :tip-caption: 💡 10 | :warning-caption: ⚠️ 11 | endif::[] 12 | 13 | TIP: Always read the *correct version* of the documentation, which matches the version of the plugin that you have installed. To do so, use the dropdown in the top left, which usually contains the value "main". Select the "tag" that matches your installed version. 14 | 15 | 16 | == About 17 | 18 | This project offers two plugins for https://www.simhubdash.com/[SimHub]. 19 | 20 | === SimHub Property Server 21 | 22 | Allows access to SimHub properties via a tcp connection (thus: for other applications). 23 | 24 | Other applications can subscribe for property changes. They will then receive updates each time, when the value of a subscribed property changes. Clients can also trigger SimHub Controls and SimHub Control Mapper Roles, thus trigger actions inside SimHub or the simulation. 25 | 26 | One use case is my project https://github.com/pre-martin/StreamDeckSimHubPlugin[StreamDeckSimHubPlugin], which allows updating the state of Stream Deck keys via SimHub properties, and of course triggering of SimHub Controls and SimHub Control Mapper Roles. 27 | 28 | === Computed Properties 29 | 30 | This is a really powerful plugin: It allows user-defined JavaScript code to be executed within SimHub. 31 | 32 | User-defined code can be executed for example, when another property changes its value. Or this code can be triggered via SimHub Controls or SimHub Control Mapper Roles, which also includes the Stream Deck as a source, if my plugin for Stream Deck, the https://github.com/pre-martin/StreamDeckSimHubPlugin[StreamDeckSimHubPlugin], is installed. 33 | 34 | A possible use case could be, that you press a specific Stream Deck key, and your user-defined code in SimHub starts to do some calculations, like calculating tyre pressure differences and propose new values for your next pit stop. These new values can be exposed via new SimHub properties (defined by your code), so that you can display them for example on SimHub dashes or on your Stream Deck. 35 | 36 | 37 | == Installation 38 | 39 | WARNING: To download, do not use the green button! Instead, click on "Releases" on the right side and download the file `PropertyServer.dll`. 40 | 41 | The plugin requires at least SimHub version **9.6.0**. 42 | 43 | Simply copy the file `PropertyServer.dll` into the root directory of your SimHub installation. After having launched SimHub, the plugin has to be activated under "_Settings_ > _Plugins_" (usually SimHub will autodetect the plugin and ask at startup, if the plugin shall be activated). 44 | 45 | Optionally, the checkbox "_Show in left menu_" can be activated. This will show an entry named "_Property Server_" and/or "_Computed Properties_" in the left menu bar, which allows to interact with the plugins. If "_Show in left menu_" is not enabled, the plugins can be found under "_Additional Plugins_ > _Property Server_" and "_Additional Plugins_ > _Computed Properties_". 46 | 47 | After installation, the checkbox "_Show in left menu_" can be found under "_Settings_ > _Plugins_". 48 | 49 | 50 | == Usage 51 | 52 | Both plugins have their own sub-pages for their documentation: 53 | 54 | link:doc/PropertyServer/PropertyServer.adoc[Go to SimHub Property Server]. This documentation is mainly relevant if you are a software developer and want to access SimHub from other applications, or if you want to know details about ShakeIt Properties and their potential problems (“Repair ShakeIt Profiles”). 55 | 56 | link:doc/ComputedProperties/ComputedProperties.adoc[Go to Computed Properties]. 57 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/Ui/ComputedPropertiesControl.xaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | 56 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/Comm/Server.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Net; 7 | using System.Net.Sockets; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using log4net; 11 | 12 | namespace SimHub.Plugins.PropertyServer.Comm 13 | { 14 | /// 15 | /// Server listening for TCP connections and dispatching them to Client objects. 16 | /// 17 | public class Server 18 | { 19 | private static readonly ILog Log = LogManager.GetLogger(typeof(Server)); 20 | private static int _clientId; 21 | private readonly CancellationTokenSource _cts = new CancellationTokenSource(); 22 | private TcpListener _tcpListener; 23 | private long _running; 24 | private readonly List _clients = new List(); 25 | private readonly ISimHub _simHub; 26 | private readonly SubscriptionManager _subscriptionManager; 27 | private readonly int _port; 28 | 29 | private bool Running 30 | { 31 | get => Interlocked.Read(ref _running) == 1; 32 | set => Interlocked.Exchange(ref _running, Convert.ToInt64(value)); 33 | } 34 | 35 | public Server(ISimHub simHub, SubscriptionManager subscriptionManager, int port) 36 | { 37 | _simHub = simHub; 38 | _subscriptionManager = subscriptionManager; 39 | _port = port; 40 | } 41 | 42 | /// 43 | /// Starts the server which is listening for connections. This method is blocking until "Stop()" is getting called. 44 | /// 45 | public async Task Start() 46 | { 47 | _tcpListener = new TcpListener(IPAddress.Loopback, _port); 48 | _tcpListener.Start(); 49 | Log.Info($"Listening on port {_port}"); 50 | 51 | Running = true; 52 | while (Running) 53 | { 54 | try 55 | { 56 | var tcpClient = await _tcpListener.AcceptTcpClientAsync(); 57 | LogicalThreadContext.Properties["client"] = Interlocked.Increment(ref _clientId); 58 | Log.Info($"New connection from client {tcpClient.Client.RemoteEndPoint}"); 59 | var client = new Client(_simHub, _subscriptionManager, tcpClient); 60 | _clients.Add(client); 61 | var clientTask = client.Start(_cts.Token); 62 | 63 | #pragma warning disable CS4014 64 | clientTask.ContinueWith(t => 65 | #pragma warning restore CS4014 66 | { 67 | Log.Info("Client has finished. Removing it."); 68 | _clients.Remove(client); 69 | }, _cts.Token); 70 | } 71 | catch (ObjectDisposedException ode) 72 | { 73 | // TcpListener was stopped. "_running" should be false at this moment. 74 | if (Running) Log.Warn($"Server main loop got interrupted, but _running is still true: {ode}"); 75 | else Log.Info("Server main loop got interrupted."); 76 | } 77 | } 78 | 79 | Log.Info("Exiting Listener"); 80 | } 81 | 82 | public async Task Stop() 83 | { 84 | Log.Info($"Ending server, disconnecting {_clients.Count} clients"); 85 | 86 | // Clone list to avoid InvalidOperationException because of concurrent modification. 87 | var clients = new List(_clients); 88 | foreach (var client in clients) 89 | { 90 | await client.Disconnect(); 91 | } 92 | 93 | Running = false; 94 | _cts.Cancel(); 95 | _tcpListener.Stop(); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/Ui/EditScriptWindow.xaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 49 | 50 | 51 | 54 | 58 | 59 | 60 | 61 | 62 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/Property/PropertySource.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using GameReaderCommon; 6 | using SimHub.Plugins.PropertyServer.ShakeIt; 7 | 8 | namespace SimHub.Plugins.PropertyServer.Property 9 | { 10 | /// 11 | /// Possible sources for SimHub properties. 12 | /// 13 | public enum PropertySource 14 | { 15 | /// 16 | /// For properties which can be found in the class GameReaderCommon.GameData 17 | /// 18 | GameData, 19 | 20 | /// 21 | /// For properties which can be found in the class GameReaderCommon.GameData.StatusDataBase 22 | /// 23 | StatusDataBase, 24 | 25 | /// 26 | /// Assetto Corsa Competizione - Rawdata "Graphics" 27 | /// 28 | AccGraphics, 29 | 30 | /// 31 | /// Assetto Corsa Competizione - Rawdata "Physics" 32 | /// 33 | AccPhysics, 34 | 35 | /// 36 | /// Generic access to a property via PluginManager. These are all properties listed in SimHub under "Available properties". 37 | /// 38 | Generic, 39 | 40 | /// 41 | /// Access to a (small) subset of ShakeIt Bass data. 42 | /// 43 | ShakeItBass, 44 | 45 | /// 46 | /// Access to a (small) subset of ShakeIt Motors data. 47 | /// 48 | ShakeItMotors 49 | } 50 | 51 | public static class PropertySourceEx 52 | { 53 | /// 54 | /// Determines the underlying Type of a given PropertySource. 55 | /// 56 | /// The Type or null if the underlying type cannot be found. 57 | public static Type GetPropertySourceType(this PropertySource propertySource) 58 | { 59 | switch (propertySource) 60 | { 61 | case PropertySource.GameData: 62 | return typeof(GameData); 63 | case PropertySource.StatusDataBase: 64 | return typeof(StatusDataBase); 65 | case PropertySource.AccGraphics: 66 | return Type.GetType("ACSharedMemory.ACC.MMFModels.Graphics, ACSharedMemory"); 67 | case PropertySource.AccPhysics: 68 | return Type.GetType("ACSharedMemory.ACC.MMFModels.Physics, ACSharedMemory"); 69 | case PropertySource.Generic: 70 | return typeof(PluginManager); 71 | case PropertySource.ShakeItBass: 72 | return typeof(ShakeItAccessor); 73 | case PropertySource.ShakeItMotors: 74 | return typeof(ShakeItAccessor); 75 | default: 76 | throw new ArgumentException($"Unknown PropertySource {propertySource}"); 77 | } 78 | } 79 | 80 | /// 81 | /// Determines the name prefix of a given PropertySource. 82 | /// 83 | public static string GetPropertyPrefix(this PropertySource propertySource) 84 | { 85 | switch (propertySource) 86 | { 87 | case PropertySource.GameData: 88 | return "dcp"; // [DataCorePlugin.xyz] 89 | case PropertySource.StatusDataBase: 90 | return "dcp.gd"; // [DataCorePlugin.GameData.xyz] 91 | case PropertySource.AccGraphics: 92 | return "acc.graphics"; 93 | case PropertySource.AccPhysics: 94 | return "acc.physics"; 95 | case PropertySource.Generic: 96 | return ""; 97 | case PropertySource.ShakeItBass: 98 | return "sib"; // ShakeItBass 99 | case PropertySource.ShakeItMotors: 100 | return "sim"; // ShakeItMotors 101 | default: 102 | throw new ArgumentException($"Unknown PropertySource {propertySource}"); 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/Ui/ComputedPropertiesControl.xaml.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Windows; 7 | using System.Windows.Forms; 8 | using System.Windows.Input; 9 | using SimHub.Plugins.Styles; 10 | using SimHub.Plugins.UI; 11 | 12 | namespace SimHub.Plugins.ComputedProperties.Ui 13 | { 14 | public partial class ComputedPropertiesControl 15 | { 16 | private readonly IComputedPropertiesManager _computedPropertiesManager; 17 | 18 | public ComputedPropertiesControl(IComputedPropertiesManager computedPropertiesManager) 19 | { 20 | _computedPropertiesManager = computedPropertiesManager; 21 | InitializeComponent(); 22 | } 23 | 24 | private ComputedPropertiesViewModel ViewModel => (ComputedPropertiesViewModel)DataContext; 25 | 26 | private async void NewScript_Click(object sender, RoutedEventArgs e) 27 | { 28 | var scriptData = new ScriptData(); 29 | var editWindow = new EditScriptWindow 30 | { 31 | DataContext = new EditScriptWindowViewModel(ViewModel.ScriptValidator, scriptData) 32 | }; 33 | 34 | var result = await editWindow.ShowDialogWindowAsync(this, DialogOptions.Resizable, 1000, 800); 35 | if (result == DialogResult.OK) 36 | { 37 | ViewModel.AddScript(((EditScriptWindowViewModel)editWindow.DataContext).GetScriptData()); 38 | _computedPropertiesManager.SaveScripts(); 39 | } 40 | } 41 | 42 | private async void Entry_MouseDoubleClick(object sender, MouseButtonEventArgs e) 43 | { 44 | var selectedItem = ViewModel.SelectedScript; 45 | if (selectedItem == null) return; 46 | 47 | var editWindow = new EditScriptWindow 48 | { 49 | // Create a clone of ScriptData, so that the editor does not work on the original data. 50 | DataContext = new EditScriptWindowViewModel(ViewModel.ScriptValidator, selectedItem.Clone()) 51 | }; 52 | var result = await editWindow.ShowDialogWindowAsync(this, DialogOptions.Resizable | DialogOptions.DoNotCloseOnEscape, 53 | 1000, 800); 54 | if (result == DialogResult.OK) 55 | { 56 | // OK: Copy all data from the dialog (view model) into the underlying DataContext. 57 | var scriptData = ((EditScriptWindowViewModel)editWindow.DataContext).GetScriptData(); 58 | var existingEntry = ViewModel.Scripts.SingleOrDefault(data => data.Guid == scriptData.Guid); 59 | if (existingEntry != null) 60 | { 61 | existingEntry.Name = scriptData.Name; 62 | existingEntry.Script = scriptData.Script; 63 | existingEntry.Reset(); 64 | ViewModel.UpdateScript(existingEntry); 65 | _computedPropertiesManager.SaveScripts(); 66 | } 67 | } 68 | } 69 | 70 | private async void Entry_InfoClick(object sender, RoutedEventArgs e) 71 | { 72 | if (!(((FrameworkElement)sender).DataContext is ScriptData scriptData)) return; 73 | 74 | var performanceWindow = new PerformanceWindow { DataContext = scriptData.FunctionPerformance }; 75 | await performanceWindow.ShowDialogWindowAsync(this, DialogOptions.Resizable | DialogOptions.DoNotCloseOnEscape, 500, 76 | 400); 77 | } 78 | 79 | private async void Entry_DeleteClick(object sender, RoutedEventArgs e) 80 | { 81 | if (!(((FrameworkElement)sender).DataContext is ScriptData scriptData)) return; 82 | 83 | var linesOfCode = string.IsNullOrWhiteSpace(scriptData?.Script) 84 | ? 0 85 | : scriptData.Script.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None).Length; 86 | 87 | var result = await SHMessageBox.Show( 88 | $"Are you sure you want to delete the script \n\"{scriptData.Name}\"? \nIt has {linesOfCode} lines of code.", 89 | "Confirm delete", MessageBoxButton.OKCancel, MessageBoxImage.Question); 90 | 91 | if (result != DialogResult.OK) return; 92 | ViewModel.DeleteScript(scriptData); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PreCommon/Ui/IconResources.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/AutoUpdate/AutoUpdater.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.IO; 6 | using System.Net.Http; 7 | using System.Security.Cryptography; 8 | using System.Threading.Tasks; 9 | using Newtonsoft.Json; 10 | 11 | namespace SimHub.Plugins.PropertyServer.AutoUpdate 12 | { 13 | /// 14 | /// Handles automatic updates by checking GitHub releases and downloading new versions. 15 | /// 16 | public class AutoUpdater 17 | { 18 | private const string GitHubApiUrl = "https://api.github.com/repos/pre-martin/SimHubPropertyServer/releases/latest"; 19 | private const string UserAgent = "SimHubPropertyServer-Updater"; 20 | 21 | public async Task GetLatestVersion() 22 | { 23 | HttpResponseMessage response; 24 | using (var httpClient = new HttpClient()) 25 | { 26 | httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); 27 | response = await httpClient.GetAsync(GitHubApiUrl); 28 | response.EnsureSuccessStatusCode(); 29 | } 30 | 31 | var content = await response.Content.ReadAsStringAsync(); 32 | return JsonConvert.DeserializeObject(content); 33 | } 34 | 35 | /// 36 | /// Downloads and applies the latest version of the PropertyServer plugin. 37 | /// 38 | /// Is thrown for various I/O problems or validation errors 39 | public async Task Update() 40 | { 41 | var versionInfo = await GetLatestVersion(); 42 | var asset = versionInfo.Assets.Find(a => a.Name == "PropertyServer.dll"); 43 | if (asset == null) 44 | { 45 | throw new Exception("Asset \"PropertyServer.dll\" not found in GitHub release."); 46 | } 47 | 48 | var size = asset.Size; 49 | var digest = asset.Digest; 50 | var downloadUrl = asset.BrowserDownloadUrl; 51 | 52 | await DownloadAndVerifyAsync(downloadUrl, digest, size, "PropertyServer.dll.new"); 53 | 54 | File.Delete("PropertyServer.dll.old"); 55 | File.Move("PropertyServer.dll", "PropertyServer.dll.old"); 56 | File.Move("PropertyServer.dll.new", "PropertyServer.dll"); 57 | } 58 | 59 | private async Task DownloadAndVerifyAsync(string downloadUrl, string expectedDigest, long expectedSize, string targetFilePath) 60 | { 61 | using (var httpClient = new HttpClient()) 62 | { 63 | httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); 64 | httpClient.DefaultRequestHeaders.Add("Accept", "application/octet-stream"); 65 | using (var response = await httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead)) 66 | { 67 | response.EnsureSuccessStatusCode(); 68 | using (var stream = await response.Content.ReadAsStreamAsync()) 69 | using (var fileStream = File.Create(targetFilePath)) 70 | { 71 | await stream.CopyToAsync(fileStream); 72 | } 73 | } 74 | } 75 | 76 | var fileInfo = new FileInfo(targetFilePath); 77 | if (fileInfo.Length != expectedSize) 78 | { 79 | throw new Exception($"Downloaded file size mismatch. Expected: {expectedSize}, actual: {fileInfo.Length}"); 80 | } 81 | 82 | if (expectedDigest.StartsWith("sha256:")) 83 | { 84 | var sha256Digest = expectedDigest.Substring("sha256:".Length); 85 | using (var fileStream = File.OpenRead(targetFilePath)) 86 | using (var sha256 = SHA256.Create()) 87 | { 88 | var computedHash = sha256.ComputeHash(fileStream); 89 | var computedHashString = BitConverter.ToString(computedHash).Replace("-", "").ToLowerInvariant(); 90 | if (!string.Equals(computedHashString, sha256Digest, StringComparison.OrdinalIgnoreCase)) 91 | { 92 | throw new Exception($"SHA256 digest mismatch for downloaded file. Expected: {sha256Digest}, actual: {computedHashString}"); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/Ui/EditScriptWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using ICSharpCode.AvalonEdit.Document; 8 | using SimHub.Plugins.OutputPlugins.Dash.GLCDTemplating; 9 | using SimHub.Plugins.PreCommon.Ui.Util; 10 | 11 | namespace SimHub.Plugins.ComputedProperties.Ui 12 | { 13 | public class EditScriptWindowViewModel : ObservableObject 14 | { 15 | private readonly IScriptValidator _scriptValidator; 16 | 17 | /// 18 | /// ExpressionValue allows us to get the syntax highlighting, which is partly "internal". 19 | /// 20 | public ExpressionValue Formula { get; } = new ExpressionValue { Interpreter = Interpreter.Javascript }; 21 | 22 | private readonly ScriptData _scriptData; 23 | 24 | public string Name 25 | { 26 | get => _scriptData?.Name ?? string.Empty; 27 | set 28 | { 29 | _scriptData.Name = value; 30 | UpdateButtonState(); 31 | } 32 | } 33 | 34 | private TextDocument _script = new TextDocument(); 35 | 36 | public TextDocument Script 37 | { 38 | get => _script; 39 | set => SetProperty(ref _script, value); 40 | } 41 | 42 | private bool _isOkEnabled; 43 | 44 | public bool IsOkEnabled 45 | { 46 | get => _isOkEnabled; 47 | set => SetProperty(ref _isOkEnabled, value); 48 | } 49 | 50 | private string _problems = string.Empty; 51 | 52 | public string Problems 53 | { 54 | get => _problems; 55 | set => SetProperty(ref _problems, value); 56 | } 57 | 58 | private CancellationTokenSource _debounceTokenSource; 59 | 60 | public EditScriptWindowViewModel(IScriptValidator scriptValidator, ScriptData scriptData) 61 | { 62 | _scriptValidator = scriptValidator; 63 | _scriptData = scriptData; 64 | 65 | Script = new TextDocument(scriptData.Script); 66 | UpdateButtonState(); 67 | } 68 | 69 | /// 70 | /// Default constructor for IDE only 71 | /// 72 | public EditScriptWindowViewModel() 73 | { 74 | } 75 | 76 | public ScriptData GetScriptData() 77 | { 78 | _scriptData.Script = Script.Text; 79 | return _scriptData; 80 | } 81 | 82 | private void UpdateButtonState() 83 | { 84 | IsOkEnabled = !string.IsNullOrWhiteSpace(Name); 85 | } 86 | 87 | /// 88 | /// Inserts a minimal, functioning script into the editor. 89 | /// 90 | public void InsertSample() 91 | { 92 | Script = new TextDocument(@" 93 | /** Initialization. Only called by the plugin once for each script. */ 94 | function init() { 95 | // create a new property in SimHub 96 | createProperty('ComputedPropertiesPlugin.MyNewDateAndTime'); 97 | // instruct the plugin to call 'calculate' (see below) whenever 'DataCorePlugin.CurrentDateTime' changes 98 | subscribe('DataCorePlugin.CurrentDateTime', 'calculate'); 99 | } 100 | 101 | /** Called by the plugin whenever a corresponding property value (subscription) has changed. */ 102 | function calculate() { 103 | const currentDateTime = getPropertyValue('DataCorePlugin.CurrentDateTime'); 104 | // set the value of the computed property. 105 | setPropertyValue('ComputedPropertiesPlugin.MyNewDateAndTime', 'My time and date: ' + currentDateTime); 106 | } 107 | "); 108 | } 109 | 110 | /// 111 | /// Uses debouncing to validate the script code. 112 | /// 113 | public void OnScriptChanged(string code) 114 | { 115 | _debounceTokenSource?.Cancel(); 116 | _debounceTokenSource = new CancellationTokenSource(); 117 | var token = _debounceTokenSource.Token; 118 | 119 | _ = Task.Run(async () => 120 | { 121 | try 122 | { 123 | await Task.Delay(TimeSpan.FromMilliseconds(500), token); 124 | if (!token.IsCancellationRequested && _scriptValidator != null && !string.IsNullOrWhiteSpace(code)) 125 | { 126 | try 127 | { 128 | _scriptValidator.ValidateScript(code); 129 | Problems = string.Empty; 130 | } 131 | catch (Exception e) 132 | { 133 | Problems = e.Message; 134 | } 135 | } 136 | } 137 | catch (TaskCanceledException) 138 | { 139 | // ignore cancellation 140 | } 141 | }, token); 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/Ui/RepairShakeItViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Collections.ObjectModel; 7 | using System.ComponentModel; 8 | using System.Linq; 9 | using System.Windows.Data; 10 | using System.Windows.Input; 11 | using SimHub.Plugins.PreCommon.Ui.Util; 12 | using SimHub.Plugins.PropertyServer.ShakeIt; 13 | 14 | namespace SimHub.Plugins.PropertyServer.Ui 15 | { 16 | /// 17 | /// ViewModel for the "Repair ShakeIt" view. 18 | /// 19 | public class RepairShakeItViewModel : ObservableObject 20 | { 21 | public ShakeItAccessor ShakeItAccessor { get; set; } 22 | public ICommand ScanShakeItBassCommand { get; } 23 | public ICommand ScanShakeItMotorsCommand { get; } 24 | public ICommand RepairCommand { get; } 25 | public bool ShowScanHint => Profiles == null; 26 | public bool ShowDuplicatesList => Profiles != null && Profiles.Count > 0; 27 | public bool ShowNoResults => Profiles != null && Profiles.Count == 0; 28 | 29 | private ObservableCollection _profiles; 30 | private int _currentMode = 1; // 1 = Bass, 2 = Motors 31 | 32 | public ObservableCollection Profiles 33 | { 34 | get => _profiles; 35 | private set 36 | { 37 | SetProperty(ref _profiles, value); 38 | OnPropertyChanged(nameof(ShowScanHint)); 39 | OnPropertyChanged(nameof(ShowDuplicatesList)); 40 | OnPropertyChanged(nameof(ShowNoResults)); 41 | } 42 | } 43 | 44 | public RepairShakeItViewModel() 45 | { 46 | ScanShakeItBassCommand = new RelayCommand(o => FindShakeItBassDuplicates()); 47 | ScanShakeItMotorsCommand = new RelayCommand(o => FindShakeItMotorsDuplicates()); 48 | RepairCommand = new RelayCommand( 49 | e => Profiles != null && Profiles.Any(p => p.IsChecked), 50 | o => Repair() 51 | ); 52 | } 53 | 54 | private void FindShakeItBassDuplicates() 55 | { 56 | FindShakeItDuplicates(ShakeItAccessor.BassProfiles()); 57 | _currentMode = 1; 58 | } 59 | 60 | private void FindShakeItMotorsDuplicates() 61 | { 62 | FindShakeItDuplicates(ShakeItAccessor.MotorsProfiles()); 63 | _currentMode = 2; 64 | } 65 | 66 | private void FindShakeItDuplicates(ICollection profiles) 67 | { 68 | var tempProfiles = new ObservableCollection(); 69 | foreach (var profile in profiles) 70 | { 71 | var guidToEffects = ShakeItAccessor.GroupEffectsByGuid(profile); 72 | var duplicates = guidToEffects.Where(kv => kv.Value.Count > 1).SelectMany(kv => kv.Value).ToList(); 73 | if (duplicates.Count > 0) 74 | { 75 | var profileHolder = new ProfileHolder(profile.Name, guidToEffects, duplicates); 76 | tempProfiles.Add(profileHolder); 77 | } 78 | } 79 | 80 | Profiles = tempProfiles; 81 | } 82 | 83 | private void Repair() 84 | { 85 | var profile = Profiles?.FirstOrDefault(p => p.IsChecked); 86 | if (profile == null) return; 87 | 88 | var guidToEffects = profile.GuidToEffects; 89 | foreach (var kv in guidToEffects.Where(kv => kv.Value.Count > 1)) 90 | { 91 | for (var i = 1; i < kv.Value.Count; i++) 92 | { 93 | kv.Value[i].ContainerId = Guid.NewGuid(); 94 | } 95 | } 96 | 97 | if (_currentMode == 1) FindShakeItBassDuplicates(); 98 | else FindShakeItMotorsDuplicates(); 99 | } 100 | } 101 | 102 | public class ProfileHolder 103 | { 104 | public Dictionary> GuidToEffects { get; } 105 | 106 | public ProfileHolder(string name, Dictionary> guidToEffects, List duplicates) 107 | { 108 | Name = name; 109 | GuidToEffects = guidToEffects; 110 | 111 | var duplicatesHolder = duplicates.Select(ecb => new EffectsHolder(ecb)).ToList(); 112 | var duplicatesCollectionView = CollectionViewSource.GetDefaultView(duplicatesHolder); 113 | duplicatesCollectionView.GroupDescriptions.Add(new PropertyGroupDescription("ContainerId")); 114 | Duplicates = duplicatesCollectionView; 115 | } 116 | 117 | public bool IsChecked { get; set; } 118 | 119 | public string Name { get; set; } 120 | 121 | public ICollectionView Duplicates { get; set; } 122 | } 123 | 124 | public class EffectsHolder 125 | { 126 | private readonly EffectsContainerBase _effectsContainerBase; 127 | 128 | public EffectsHolder(EffectsContainerBase effectsContainerBase) 129 | { 130 | _effectsContainerBase = effectsContainerBase; 131 | } 132 | 133 | public Guid ContainerId => _effectsContainerBase.ContainerId; 134 | 135 | public string RecursiveName => _effectsContainerBase.RecursiveName; 136 | } 137 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/Ui/SettingsViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using System.Windows; 9 | using System.Windows.Input; 10 | using log4net; 11 | using SimHub.Plugins.PreCommon.Ui.Util; 12 | using SimHub.Plugins.PropertyServer.AutoUpdate; 13 | using SimHub.Plugins.PropertyServer.Settings; 14 | using SimHub.Plugins.Styles; 15 | 16 | namespace SimHub.Plugins.PropertyServer.Ui 17 | { 18 | /// 19 | /// ViewModel for the "Settings" control. 20 | /// 21 | public class SettingsViewModel : ObservableObject 22 | { 23 | private static readonly ILog Log = LogManager.GetLogger(typeof(SettingsViewModel)); 24 | private readonly ISimHub _simHub; 25 | private readonly GeneralSettings _settings; 26 | private readonly AutoUpdater _autoUpdater = new AutoUpdater(); 27 | 28 | public event EventHandler LogLevelChangedEvent; 29 | public string Version => "Version " + ThisAssembly.AssemblyFileVersion; 30 | public int Port { get => _settings.Port; set => _settings.Port = value; } 31 | public List LogLevels { get; private set; } 32 | public LogLevelSetting SelectedLogLevel 33 | { 34 | get => _settings.LogLevel; 35 | set 36 | { 37 | _settings.LogLevel = value; 38 | LogLevelChangedEvent?.Invoke(this, EventArgs.Empty); 39 | } 40 | } 41 | 42 | #region Version Check Properties 43 | 44 | private string _versionCheckMessage = string.Empty; 45 | public string VersionCheckMessage 46 | { 47 | get => _versionCheckMessage; 48 | set => SetProperty(ref _versionCheckMessage, value); 49 | } 50 | 51 | private bool _isNewVersionAvailable; 52 | public bool IsNewVersionAvailable 53 | { 54 | get => _isNewVersionAvailable; 55 | set => SetProperty(ref _isNewVersionAvailable, value); 56 | } 57 | 58 | private bool _isVersionCheckError; 59 | public bool IsVersionCheckError 60 | { 61 | get => _isVersionCheckError; 62 | set => SetProperty(ref _isVersionCheckError, value); 63 | } 64 | 65 | public ICommand CheckNewVersionCommand { get; } 66 | 67 | #endregion 68 | 69 | public SettingsViewModel(ISimHub simHub, GeneralSettings settings) 70 | { 71 | _simHub = simHub; 72 | _settings = settings; 73 | _ = Task.Run(CheckForNewVersion); 74 | PopulateFromSettings(settings); 75 | CheckNewVersionCommand = new RelayCommand(o => Task.Run(CheckForNewVersion)); 76 | } 77 | 78 | private void PopulateFromSettings(GeneralSettings settings) 79 | { 80 | this.Port = settings.Port; 81 | this.LogLevels = new List(Enum.GetValues(typeof(LogLevelSetting)).Cast()); 82 | this.SelectedLogLevel = settings.LogLevel; 83 | } 84 | 85 | private async Task CheckForNewVersion() 86 | { 87 | await CheckForNewVersion(false); 88 | } 89 | 90 | public async Task CheckForNewVersion(bool testMode) 91 | { 92 | IsVersionCheckError = false; 93 | IsNewVersionAvailable = false; 94 | VersionCheckMessage = string.Empty; 95 | 96 | try 97 | { 98 | var versionInfo = testMode 99 | ? new GitHubVersionInfo { RawTagName = "v99.99.1", Draft = false, Prerelease = false } 100 | : await _autoUpdater.GetLatestVersion(); 101 | 102 | var currentVersion = new Version(ThisAssembly.AssemblyFileVersion); 103 | var latestVersion = new Version(versionInfo.TagName); 104 | if (latestVersion > currentVersion && !versionInfo.Draft && !versionInfo.Prerelease) 105 | { 106 | IsNewVersionAvailable = true; 107 | VersionCheckMessage = $"New version available: {versionInfo.TagName}"; 108 | } 109 | else 110 | { 111 | IsNewVersionAvailable = false; 112 | VersionCheckMessage = "You are using the latest version."; 113 | } 114 | } 115 | catch (Exception ex) 116 | { 117 | Log.Error($"Error checking for new version: {ex.Message}"); 118 | IsVersionCheckError = true; 119 | VersionCheckMessage = $"Error checking for new version: {ex.Message}"; 120 | } 121 | } 122 | 123 | public async Task Update() 124 | { 125 | try 126 | { 127 | await _autoUpdater.Update(); 128 | _simHub.RestartSimHub(); 129 | } 130 | catch (Exception ex) 131 | { 132 | Log.Error("Updated failed", ex); 133 | await SHMessageBox.Show($"Update failed: {ex.Message}.\n" + 134 | "See \"Logs\\PropertyServer.log\" for details.", 135 | "Update Error", MessageBoxButton.OK, MessageBoxImage.Error); 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/SubscriptionManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using log4net; 9 | using SimHub.Plugins.PropertyServer.Property; 10 | 11 | namespace SimHub.Plugins.PropertyServer 12 | { 13 | /// 14 | /// Handles subscriptions from clients and maps them onto properties from SimHub. The whole class is thread-safe 15 | /// so that it can be called from clients and from SimHub. 16 | /// 17 | public class SubscriptionManager 18 | { 19 | private static readonly ILog Log = LogManager.GetLogger(typeof(SubscriptionManager)); 20 | private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1); 21 | private readonly Dictionary _properties = new Dictionary(); 22 | 23 | /// 24 | /// Subscribes to a given SimHub property. 25 | /// 26 | /// The name of the SimHub property. See class GameReaderCommon.StatusDataBase. 27 | /// This handler will be called when the value of the property has changed. 28 | /// Will be called if the subscription was not possible. 29 | /// An existing or a newly created instance of SimHubProperty, or null. 30 | public async Task Subscribe( 31 | string propertyName, 32 | Func eventHandler, 33 | Func errorCallback) 34 | { 35 | await _semaphore.WaitAsync(); 36 | try 37 | { 38 | if (_properties.TryGetValue(propertyName, out var property)) 39 | { 40 | // We already know the property. So just add the callback and send the current value. 41 | await property.AddSubscriber(eventHandler); 42 | Log.Info( 43 | $"Added subscription to existing property {propertyName} (has now {property.SubscriberCount} subscriptions)"); 44 | await eventHandler.Invoke(new ValueChangedEventArgs(property)); 45 | return property; 46 | } 47 | 48 | // Create a new property instance. 49 | property = await PropertyAccessor.CreateProperty(propertyName, errorCallback); 50 | if (property == null) return null; 51 | 52 | Log.Info($"Created new property {propertyName}"); 53 | await property.AddSubscriber(eventHandler); 54 | _properties[propertyName] = property; 55 | // Send "null" to client. If no sim is running, the first regular update could take some time. 56 | await eventHandler.Invoke(new ValueChangedEventArgs(property)); 57 | return property; 58 | } 59 | finally 60 | { 61 | _semaphore.Release(); 62 | } 63 | } 64 | 65 | /// 66 | /// Unsubscribes from a SimHub property. 67 | /// 68 | /// The name of the SimHub property. 69 | /// The handler that shall be removed from the property. 70 | /// true if the handler was unsubscribed. 71 | public async Task Unsubscribe(string propertyName, Func eventHandler) 72 | { 73 | await _semaphore.WaitAsync(); 74 | try 75 | { 76 | if (_properties.TryGetValue(propertyName, out var property)) 77 | { 78 | await property.RemoveSubscriber(eventHandler); 79 | Log.Info($"Removed subscription from {propertyName} ({property.SubscriberCount} remaining)"); 80 | if (!property.HasSubscribers) 81 | { 82 | // No more active subscribers on this property: Remove it. 83 | Log.Info($"Removing property {propertyName}, it has no more subscriptions"); 84 | _properties.Remove(propertyName); 85 | } 86 | 87 | return true; 88 | } 89 | 90 | // We have a confused client. Print some info into the log. 91 | Log.Info($"Client wanted to unsubscribe from {propertyName}, but it is not known"); 92 | return false; 93 | } 94 | finally 95 | { 96 | _semaphore.Release(); 97 | } 98 | } 99 | 100 | /// 101 | /// Returns a list of all SimHub properties for which subscriptions exist. 102 | /// 103 | /// A shallow clone of the internal list, which is safe to iterate, but whose elements could be stale. 104 | public async Task> GetProperties() 105 | { 106 | await _semaphore.WaitAsync(); 107 | try 108 | { 109 | return new Dictionary(_properties); 110 | } 111 | finally 112 | { 113 | _semaphore.Release(); 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/ShakeIt/ShakeItAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using SimHub.Plugins.DataPlugins.ShakeItV3; 8 | using SimHub.Plugins.DataPlugins.ShakeItV3.Settings; 9 | 10 | namespace SimHub.Plugins.PropertyServer.ShakeIt 11 | { 12 | /// 13 | /// We use this class to manage access to the "settings" properties of the ShakeIt plugins. It allows us to read the 14 | /// whole configuration of the ShakeIt Bass and the ShakeIt Motors plugin. 15 | /// 16 | public class ShakeItAccessor 17 | { 18 | private readonly ShakeITBSV3Plugin _shakeItBassPlugin = PluginManager.GetInstance().GetPlugin(); 19 | private readonly ShakeITMotorsV3Plugin _shakeItMotorsPlugin = PluginManager.GetInstance().GetPlugin(); 20 | 21 | /// 22 | /// Returns all known profiles of the given plugin, which must be a "ShakeItV3" plugin. 23 | /// 24 | /// 25 | /// This returns the original data structure of the ShakeIt internals! Be careful when modifying the data. 26 | /// 27 | private IEnumerable SimHubProfiles(ShakeITV3PluginBase shakeItPlugin) 28 | where T : IOutputManager where TSettingsType : ShakeItSettings, new() 29 | { 30 | return shakeItPlugin?.Settings == null ? Enumerable.Empty() : shakeItPlugin.Settings.Profiles; 31 | } 32 | 33 | /// 34 | /// Returns a view on all known profiles of the plugin "ShakeIt Bass Shakers". 35 | /// 36 | /// 37 | /// We wrap the internal classes into our own model, in order to be independent of ShakeIt implementation details. 38 | /// 39 | public ICollection BassProfiles() 40 | { 41 | var simHubProfiles = SimHubProfiles(_shakeItBassPlugin); 42 | return simHubProfiles.Select(simHubProfile => new Profile(simHubProfile)).ToList(); 43 | } 44 | 45 | /// 46 | /// Tries to find an effect group or effect with the given Guid by searching in all available profiles. 47 | /// 48 | /// 49 | /// Caution: SimHub does not enforce that each element has an unique Guid! This method returns only the first matching element. 50 | /// 51 | /// An instance of EffectsContainerBase or one if its subclasses, or null if the element was not found. 52 | public EffectsContainerBase FindBassEffect(Guid guid) 53 | { 54 | var simHubProfiles = SimHubProfiles(_shakeItBassPlugin); 55 | return simHubProfiles.Select(simHubProfile => FindEffect(simHubProfile.EffectsContainers, guid)).FirstOrDefault(result => result != null); 56 | } 57 | 58 | /// 59 | /// Returns a view on all known profiles of the plugin "ShakeIt Motors". 60 | /// 61 | /// 62 | /// We wrap the internal classes into our own model, in order to be independent of ShakeIt implementation details. 63 | /// 64 | public ICollection MotorsProfiles() 65 | { 66 | var simHubProfiles = SimHubProfiles(_shakeItMotorsPlugin); 67 | return simHubProfiles.Select(simHubProfile => new Profile(simHubProfile)).ToList(); 68 | } 69 | 70 | /// 71 | /// Tries to find an effect group or effect with the given Guid by searching in all available profiles. 72 | /// 73 | /// 74 | /// Caution: SimHub does not enforce that each element has an unique Guid! This method returns only the first matching element. 75 | /// 76 | /// An instance of EffectsContainerBase or one if its subclasses, or null if the element was not found. 77 | public EffectsContainerBase FindMotorsEffect(Guid guid) 78 | { 79 | var simHubProfiles = SimHubProfiles(_shakeItMotorsPlugin); 80 | return simHubProfiles.Select(simHubProfile => FindEffect(simHubProfile.EffectsContainers, guid)).FirstOrDefault(result => result != null); 81 | } 82 | 83 | /// 84 | /// Groups all effect groups and effects of a given profile by their Guid. 85 | /// 86 | public Dictionary> GroupEffectsByGuid(Profile profile) 87 | { 88 | var effectsContainerCollector = new EffectsContainerCollector(); 89 | return effectsContainerCollector.ByGuid(profile); 90 | } 91 | 92 | private EffectsContainerBase FindEffect(IEnumerable simHubEffectsContainerBases, Guid guid) 93 | { 94 | foreach (var simHubEffectsContainerBase in simHubEffectsContainerBases) 95 | { 96 | if (simHubEffectsContainerBase.ContainerId == guid) 97 | { 98 | return Converter.Convert(null, simHubEffectsContainerBase); 99 | } 100 | 101 | if (simHubEffectsContainerBase is DataPlugins.ShakeItV3.EffectsContainers.GroupContainer simHubGroupContainer) 102 | { 103 | var result = FindEffect(simHubGroupContainer.EffectsContainers, guid); 104 | if (result != null) 105 | { 106 | return result; 107 | } 108 | } 109 | 110 | } 111 | 112 | return null; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/Ui/SettingsControl.xaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 57 | 58 | 59 | 60 | 61 | 62 | 64 | 65 | 66 | 67 | 68 | 69 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 81 | 82 | 83 | 86 | 87 | 89 | 90 | 91 | 92 | 93 | 94 | Repair 95 | 96 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | ..\PropertyServer\properties.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 123 | 124 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/PropertyServer/Ui/RepairShakeItWindow.xaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 50 | 58 | 59 | 60 | 61 | 62 | 63 | Scan ShakeIt Bass 64 | Scan ShakeIt Motors 65 | 66 | 67 | 68 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 109 | 114 | 115 | 116 | 117 | Repair 118 | 119 | Close 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /COPYING.LESSER: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /PropertyServer.Plugin/ComputedProperties/ScriptData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2025 Martin Renner 2 | // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Acornima.Ast; 7 | using Jint; 8 | using log4net; 9 | using Newtonsoft.Json; 10 | using SimHub.Plugins.ComputedProperties.Performance; 11 | using SimHub.Plugins.PreCommon.Ui.Util; 12 | 13 | namespace SimHub.Plugins.ComputedProperties 14 | { 15 | /// 16 | /// JavaScript code with its related data (like guid, name, parsed code, ...) 17 | /// 18 | public class ScriptData : ObservableObject 19 | { 20 | public const string InitFunction = "init"; 21 | private static readonly ILog Log = LogManager.GetLogger(typeof(ScriptData)); 22 | 23 | private ScriptData(Guid guid) 24 | { 25 | this.Guid = guid; 26 | } 27 | 28 | public ScriptData() : this(Guid.NewGuid()) 29 | { 30 | } 31 | 32 | public Guid Guid { get; } 33 | 34 | private string _name = string.Empty; 35 | 36 | public string Name 37 | { 38 | get => _name; 39 | set => SetProperty(ref _name, value); 40 | } 41 | 42 | private string _script = string.Empty; 43 | 44 | public string Script 45 | { 46 | get => _script; 47 | set => SetProperty(ref _script, value); 48 | } 49 | 50 | [JsonIgnore] 51 | private bool _hasErrors; 52 | 53 | [JsonIgnore] 54 | public bool HasErrors 55 | { 56 | get => _hasErrors; 57 | private set => SetProperty(ref _hasErrors, value); 58 | } 59 | 60 | [JsonIgnore] 61 | private Engine _engine; 62 | 63 | [JsonIgnore] 64 | private Prepared