Initial Upload

This commit is contained in:
ResetXPDR 2024-02-16 14:59:22 +11:00
parent 4ea241e620
commit 6806e32d8c
51 changed files with 5861 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

373
.gitignore vendored Normal file
View File

@ -0,0 +1,373 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# CUSTOM
Releases/
AppPackage.zip
Build.ps1
CopyToMSFS.ps1
desktop.ini
build.lck
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/DynamicLOD/CopyToMSFS.ps1
/DynamicLOD_ResetEdition/DevNotes.txt

6
Installer/App.config Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
</configuration>

9
Installer/App.xaml Normal file
View File

@ -0,0 +1,9 @@
<Application x:Class="Installer.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Installer"
StartupUri="InstallerWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>

8
Installer/App.xaml.cs Normal file
View File

@ -0,0 +1,8 @@
using System.Windows;
namespace Installer
{
public partial class App : Application
{
}
}

View File

@ -0,0 +1,10 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0018:Inlinevariablendeklaration", Justification = "<Ausstehend>", Scope = "member", Target = "~M:Installer.InstallerFunctions.CheckVersion(System.String,System.String,System.Boolean,System.Boolean)~System.Boolean")]
[assembly: SuppressMessage("Style", "IDE1006:Benennungsstile", Justification = "<Ausstehend>")]
[assembly: SuppressMessage("Style", "IDE0044:Modifizierer \"readonly\" hinzufügen")]

175
Installer/Installer.csproj Normal file
View File

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{BF3DD08A-7547-4352-B1DD-9A343D757421}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>Installer</RootNamespace>
<AssemblyName>Installer</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<IsWebBootstrapper>false</IsWebBootstrapper>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>0.3.2.0</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<PublishWizardCompleted>true</PublishWizardCompleted>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>x64</PlatformTarget>
<DebugType>embedded</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<PropertyGroup>
<StartupObject>Installer.App</StartupObject>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<ManifestCertificateThumbprint>BBBE9D07E0F8DB75A8B91438A6C03470F945C71F</ManifestCertificateThumbprint>
</PropertyGroup>
<PropertyGroup>
<ManifestKeyFile>ResetXPDR.pfx</ManifestKeyFile>
</PropertyGroup>
<PropertyGroup>
<GenerateManifests>true</GenerateManifests>
</PropertyGroup>
<PropertyGroup>
<SignManifests>false</SignManifests>
</PropertyGroup>
<PropertyGroup>
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<PropertyGroup>
<AssemblyOriginatorKeyFile>ResetXPDR.pfx</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.IO.Compression, Version=4.1.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<HintPath>..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll</HintPath>
<Private>True</Private>
<Private>True</Private>
</Reference>
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.IO.Compression.ZipFile, Version=4.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<HintPath>..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll</HintPath>
<Private>True</Private>
<Private>True</Private>
</Reference>
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Page Include="InstallerWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="InstallerWorker.cs" />
<Compile Include="InstallerFunctions.cs" />
<Compile Include="InstallerWindow.xaml.cs">
<DependentUpon>InstallerWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Include="Parameters.cs" />
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Include="AppPackage.zip" />
<EmbeddedResource Include="MSFS2020_AutoFPS.config" />
<None Include="Installer_TemporaryKey.pfx" />
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<None Include="ResetXPDR.pfx" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="App.config" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.8">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.8 %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<Resource Include="icon.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -0,0 +1,570 @@
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Xml;
namespace Installer
{
public enum AutoStart
{
REMOVE = -1,
NONE = 0,
FSUIPC,
EXE
}
public static class InstallerFunctions
{
public static bool GetProcessRunning(string name)
{
Process proc = Process.GetProcessesByName(name).FirstOrDefault();
return proc != null && proc.ProcessName == name;
}
#region Install Actions
public static bool AutoStartFsuipc(bool removeEntry = false)
{
bool result = false;
string programParam = "READY";
if (CheckFSUIPC("7.4.0"))
programParam = "CONNECTED";
try
{
string regPath = (string)Registry.GetValue(Parameters.ipcRegPath, Parameters.ipcRegInstallDirValue, null);
if (!string.IsNullOrEmpty(regPath))
regPath += "\\" + "FSUIPC7.ini";
else
return false;
if (File.Exists(regPath))
{
string fileContent = File.ReadAllText(regPath, Encoding.Default);
if (!fileContent.Contains("[Programs]") && !removeEntry)
{
fileContent += $"\r\n[Programs]\r\nRunIf1={programParam},CLOSE,{Parameters.binPath}";
File.WriteAllText(regPath, fileContent, Encoding.Default);
result = true;
}
else
{
RegexOptions regOptions = RegexOptions.Compiled | RegexOptions.Multiline;
var runMatches = Regex.Matches(fileContent, @"[;]{0,1}Run(\d+).*", regOptions);
int lastRun = 0;
if (runMatches.Count > 0 && runMatches[runMatches.Count - 1].Groups.Count == 2)
lastRun = Convert.ToInt32(runMatches[runMatches.Count - 1].Groups[1].Value);
var runIfMatches = Regex.Matches(fileContent, @"[;]{0,1}RunIf(\d+).*", regOptions);
int lastRunIf = 0;
if (runIfMatches.Count > 0 && runIfMatches[runIfMatches.Count - 1].Groups.Count == 2)
lastRunIf = Convert.ToInt32(runIfMatches[runIfMatches.Count - 1].Groups[1].Value);
if (Regex.IsMatch(fileContent, @"^[;]{0,1}Run(\d+).*" + Parameters.appName + "\\.exe", regOptions))
{
if (!removeEntry)
fileContent = Regex.Replace(fileContent, @"^[;]{0,1}Run(\d+).*" + Parameters.appName + "\\.exe", $"RunIf{lastRunIf + 1}={programParam},CLOSE,{Parameters.binPath}", regOptions);
else
fileContent = Regex.Replace(fileContent, @"^[;]{0,1}Run(\d+).*" + Parameters.appName + "\\.exe", $"", regOptions);
File.WriteAllText(regPath, fileContent, Encoding.Default);
result = true;
}
else if (Regex.IsMatch(fileContent, @"^[;]{0,1}RunIf(\d+).*" + Parameters.appName + "\\.exe", regOptions))
{
if (!removeEntry)
fileContent = Regex.Replace(fileContent, @"^[;]{0,1}RunIf(\d+).*" + Parameters.appName + "\\.exe", $"RunIf$1={programParam},CLOSE,{Parameters.binPath}", regOptions);
else
fileContent = Regex.Replace(fileContent, @"^[;]{0,1}RunIf(\d+).*" + Parameters.appName + "\\.exe", $"", regOptions);
File.WriteAllText(regPath, fileContent, Encoding.Default);
result = true;
}
else
{
int index = -1;
if (runIfMatches.Count > 0 && runMatches.Count > 0)
{
index = runIfMatches[runIfMatches.Count - 1].Index + runIfMatches[runIfMatches.Count - 1].Length;
if (runMatches[runMatches.Count - 1].Index > runIfMatches[runIfMatches.Count - 1].Index)
index = runMatches[runMatches.Count - 1].Index + runMatches[runMatches.Count - 1].Length;
}
else if (runIfMatches.Count > 0)
index = runIfMatches[runIfMatches.Count - 1].Index + runIfMatches[runIfMatches.Count - 1].Length;
else if (runMatches.Count > 0)
index = runMatches[runMatches.Count - 1].Index + runMatches[runMatches.Count - 1].Length;
if (index > 0 && !removeEntry)
{
fileContent = fileContent.Insert(index + 1, $"RunIf{lastRunIf + 1}={programParam},CLOSE,{Parameters.binPath}\r\n");
File.WriteAllText(regPath, fileContent, Encoding.Default);
result = true;
}
else if (!removeEntry)
{
fileContent = Regex.Replace(fileContent, @"^\[Programs\]\r\n", $"[Programs]\r\nRunIf{lastRunIf + 1}={programParam},CLOSE,{Parameters.binPath}\r\n", regOptions);
File.WriteAllText(regPath, fileContent, Encoding.Default);
result = true;
}
}
}
}
}
catch (Exception e)
{
MessageBox.Show($"Exception '{e.GetType()}' during AutoStartFsuipc", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
return result;
}
public static bool AutoStartExe(bool removeEntry = false)
{
bool result = false;
try
{
string path = Parameters.msExeStore;
if (!File.Exists(path))
path = Parameters.msExeSteam;
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(File.ReadAllText(path));
bool found = false;
XmlNode simbase = xmlDoc.ChildNodes[1];
List<XmlNode> removeList = new List<XmlNode>();
foreach (XmlNode outerNode in simbase.ChildNodes)
{
if (outerNode.Name == "Launch.Addon" && outerNode.InnerText.Contains(Parameters.appBinary))
{
found = true;
if (!removeEntry)
{
foreach (XmlNode innerNode in outerNode.ChildNodes)
{
if (innerNode.Name == "Disabled")
innerNode.InnerText = "False";
else if (innerNode.Name == "Path")
innerNode.InnerText = Parameters.binPath;
else if (innerNode.Name == "CommandLine")
innerNode.InnerText = "";
else if (innerNode.Name == "ManualLoad")
innerNode.InnerText = "False";
}
}
else
removeList.Add(outerNode);
}
}
foreach (XmlNode node in removeList)
xmlDoc.ChildNodes[1].RemoveChild(node);
if (!found && !removeEntry)
{
XmlNode outerNode = xmlDoc.CreateElement("Launch.Addon");
XmlNode innerNode = xmlDoc.CreateElement("Disabled");
innerNode.InnerText = "False";
outerNode.AppendChild(innerNode);
innerNode = xmlDoc.CreateElement("ManualLoad");
innerNode.InnerText = "False";
outerNode.AppendChild(innerNode);
innerNode = xmlDoc.CreateElement("Name");
innerNode.InnerText = Parameters.appName;
outerNode.AppendChild(innerNode);
innerNode = xmlDoc.CreateElement("Path");
innerNode.InnerText = Parameters.binPath;
outerNode.AppendChild(innerNode);
xmlDoc.ChildNodes[1].AppendChild(outerNode);
}
xmlDoc.Save(path);
result = true;
}
catch (Exception e)
{
MessageBox.Show($"Exception '{e.GetType()}' during AutoStartExe", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
return result;
}
public static bool PlaceDesktopLink()
{
bool result = false;
try
{
IShellLink link = (IShellLink)new ShellLink();
link.SetDescription("Start " + Parameters.appName);
link.SetPath(Parameters.binPath);
IPersistFile file = (IPersistFile)link;
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
file.Save(Path.Combine(desktopPath, $"{Parameters.appName}.lnk"), false);
result = true;
}
catch (Exception e)
{
MessageBox.Show($"Exception '{e.GetType()}' during PlaceDesktopLink", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
return result;
}
public static bool DeleteOldFiles()
{
try
{
if (!Directory.Exists(Parameters.binDir))
return true;
Directory.Delete(Parameters.binDir, true);
Directory.CreateDirectory(Parameters.binDir);
return (new DirectoryInfo(Parameters.binDir)).GetFiles().Length == 0;
}
catch (Exception e)
{
MessageBox.Show($"Exception '{e.GetType()}' during RemoveOldFiles", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
}
public static bool ExtractZip(string extractDir = null, string zipFile = null)
{
try
{
if (zipFile == null)
{
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"Installer.{Parameters.fileName}"))
{
ZipArchive archive = new ZipArchive(stream);
archive.ExtractToDirectory(Parameters.binDir);
stream.Close();
}
RunCommand($"powershell -WindowStyle Hidden -Command \"dir -Path {Parameters.binDir} -Recurse | Unblock-File\"");
}
else
{
using (Stream stream = new FileStream(zipFile, FileMode.Open))
{
ZipArchive archive = new ZipArchive(stream);
archive.ExtractToDirectory(extractDir);
stream.Close();
}
}
return true;
}
catch (Exception e)
{
MessageBox.Show($"Exception '{e.GetType()}' during ExtractZip", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
}
public static bool InstallWasm()
{
bool result = false;
try
{
}
catch (Exception e)
{
MessageBox.Show($"Exception '{e.GetType()}' during InstallWasm", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
return result;
}
public static bool DownloadFile(string url, string file)
{
bool result = false;
try
{
var webClient = new WebClient();
webClient.DownloadFile(url, file);
result = File.Exists(file);
}
catch (Exception e)
{
MessageBox.Show($"Exception '{e.GetType()}' during DownloadFile", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
return result;
}
#endregion
#region Check Requirements
public static bool CheckFSUIPC(string version = null)
{
bool result = false;
string ipcVersion = Parameters.ipcVersion;
if (!string.IsNullOrEmpty(version))
ipcVersion = version;
try
{
string regVersion = (string)Registry.GetValue(Parameters.ipcRegPath, Parameters.ipcRegValue, null);
if (!string.IsNullOrWhiteSpace(regVersion))
{
regVersion = regVersion.Substring(1);
int index = regVersion.IndexOf("(beta)");
if (index > 0)
regVersion = regVersion.Substring(0, index).TrimEnd();
result = CheckVersion(regVersion, ipcVersion, true, false);
}
}
catch (Exception e)
{
MessageBox.Show($"Exception '{e.GetType()}' during CheckFSUIPC", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
return result;
}
public static bool CheckVersion(string versionInstalled, string versionRequired, bool majorEqual, bool ignoreBuild)
{
bool majorMatch = false;
bool minorMatch = false;
bool patchMatch = false;
string[] strInst = versionInstalled.Split('.');
string[] strReq = versionRequired.Split('.');
int vInst;
int vReq;
bool prevWasEqual = false;
for (int i = 0; i < strInst.Length; i++)
{
if (Regex.IsMatch(strInst[i], @"(\d+)\D"))
strInst[i] = strInst[i].Substring(0, strInst[i].Length - 1);
}
//Major
if (int.TryParse(strInst[0], out vInst) && int.TryParse(strReq[0], out vReq))
{
if (majorEqual)
majorMatch = vInst == vReq;
else
majorMatch = vInst >= vReq;
prevWasEqual = vInst == vReq;
}
//Minor
if (int.TryParse(strInst[1], out vInst) && int.TryParse(strReq[1], out vReq))
{
if (prevWasEqual)
minorMatch = vInst >= vReq;
else
minorMatch = true;
prevWasEqual = vInst == vReq;
}
//Patch
if (!ignoreBuild)
{
if (int.TryParse(strInst[2], out vInst) && int.TryParse(strReq[2], out vReq))
{
if (prevWasEqual)
patchMatch = vInst >= vReq;
else
patchMatch = true;
}
}
else
patchMatch = true;
return majorMatch && minorMatch && patchMatch;
}
public static bool CheckPackageVersion(string packagePath, string packageName, string version)
{
try
{
string file = packagePath + "\\" + packageName + "\\manifest.json";
if (File.Exists(file))
{
string[] lines = File.ReadAllLines(file);
foreach (string line in lines)
{
if (Parameters.wasmRegex.IsMatch(line))
{
var matches = Parameters.wasmRegex.Matches(line);
if (matches.Count == 1 && matches[0].Groups.Count >= 2)
return CheckVersion(matches[0].Groups[1].Value, version, false, false);
}
}
}
}
catch (Exception e)
{
MessageBox.Show($"Exception '{e.GetType()}' during CheckPackageVersion", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
return false;
}
public static string FindPackagePath(string confFile)
{
string[] lines = File.ReadAllLines(confFile);
foreach (string line in lines)
{
if (line.StartsWith(Parameters.msStringPackage))
{
return line.Replace("\"", "").Substring(Parameters.msStringPackage.Length) + "\\Community";
}
}
return "";
}
public static bool CheckInstalledMSFS(out string packagePath)
{
try
{
if (File.Exists(Parameters.msConfigStore))
{
packagePath = FindPackagePath(Parameters.msConfigStore);
return !string.IsNullOrWhiteSpace(packagePath) && Directory.Exists(packagePath);
}
else if (File.Exists(Parameters.msConfigSteam))
{
packagePath = FindPackagePath(Parameters.msConfigSteam);
return !string.IsNullOrWhiteSpace(packagePath) && Directory.Exists(packagePath);
}
packagePath = "";
return false;
}
catch (Exception e)
{
MessageBox.Show($"Exception '{e.GetType()}' during CheckInstalledMSFS", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
packagePath = "";
return false;
}
public static string RunCommand(string command)
{
var pProcess = new Process();
pProcess.StartInfo.FileName = "cmd.exe";
pProcess.StartInfo.Arguments = "/C" + command;
pProcess.StartInfo.UseShellExecute = false;
pProcess.StartInfo.CreateNoWindow = true;
pProcess.StartInfo.RedirectStandardOutput = true;
pProcess.Start();
string strOutput = pProcess.StandardOutput.ReadToEnd();
pProcess.WaitForExit();
return strOutput ?? "";
}
public static bool StringGreaterEqual(string input, int compare)
{
if (int.TryParse(input, NumberStyles.Number, CultureInfo.InvariantCulture, out int numA) && numA >= compare)
return true;
else
return false;
}
public static bool StringEqual(string input, int compare)
{
if (int.TryParse(input, NumberStyles.Number, CultureInfo.InvariantCulture, out int numA) && numA == compare)
return true;
else
return false;
}
public static bool StringGreater(string input, int compare)
{
if (int.TryParse(input, NumberStyles.Number, CultureInfo.InvariantCulture, out int numA) && numA > compare)
return true;
else
return false;
}
public static bool CheckDotNet()
{
try
{
bool installedDesktop = false;
string output = RunCommand("dotnet --list-runtimes");
var matches = Parameters.netDesktop.Matches(output);
foreach (Match match in matches)
{
if (!match.Success || match.Groups.Count != 5)
continue;
if (!StringEqual(match.Groups[2].Value, Parameters.netMajor))
continue;
if ((StringEqual(match.Groups[3].Value, Parameters.netMinor) && StringGreaterEqual(match.Groups[4].Value, Parameters.netPatch))
|| StringGreater(match.Groups[3].Value, Parameters.netMinor))
installedDesktop = true;
}
return installedDesktop;
}
catch (Exception e)
{
MessageBox.Show($"Exception '{e.GetType()}' during CheckDotNet", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
}
#endregion
}
[ComImport]
[Guid("00021401-0000-0000-C000-000000000046")]
internal class ShellLink
{
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
internal interface IShellLink
{
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out IntPtr pfd, int fFlags);
void GetIDList(out IntPtr ppidl);
void SetIDList(IntPtr pidl);
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
void GetHotkey(out short pwHotkey);
void SetHotkey(short wHotkey);
void GetShowCmd(out int piShowCmd);
void SetShowCmd(int iShowCmd);
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
void Resolve(IntPtr hwnd, int fFlags);
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
}

View File

@ -0,0 +1,28 @@
<Window x:Class="Installer.InstallerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Installer"
mc:Ignorable="d"
Title="MSFS2020_AutoFPS Installer" Height="600" Width="600" SizeToContent="WidthAndHeight" ResizeMode="CanMinimize">
<Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<Label HorizontalContentAlignment="Center" Margin="12,0,12,0">This App installs/updates MSFS__AutoFPS for your current User.</Label>
<Label HorizontalContentAlignment="Center" Margin="12,-8,12,0">Software Requirements will be automatically checked and installed.</Label>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<CheckBox Name="chkDesktopLink" IsChecked="True" HorizontalAlignment="Left" Margin="0,12,12,0" Click="chkDesktopLink_Click">Create Link on Desktop</CheckBox>
<RadioButton Name="radioRemove" GroupName="AutoStart" HorizontalAlignment="Left" Margin="0,13,0,0" Click="radio_Click" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" ToolTip="Remove existing Auto-Start Entries (FSUIPC and MSFS).">Remove Auto-Start</RadioButton>
<RadioButton Name="radioNone" GroupName="AutoStart" IsChecked="True" HorizontalAlignment="Left" Margin="0,1,0,0" Click="radio_Click" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" ToolTip="The Installer does not touch anything.&#x0a;You have to take Care of Adding/Updating/Removing the Auto-Start Entries.">Do not configure Auto-Start</RadioButton>
<RadioButton Name="radioFsuipc" GroupName="AutoStart" HorizontalAlignment="Left" Margin="0,1,0,0" Click="radio_Click" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" ToolTip="MSFS2020_AutoFPS will be started by FSUIPC - it will modify your FSUIPC7.ini File.&#x0a;An existing Entry will automatically be updated, else a new Entry is inserted.&#x0a;An existing Auto-Start Entry in your FSUIPC7.ini will automatically be removed!&#x0a;A Backup of the Files is not created!">Auto-Start with FSUIPC</RadioButton>
<RadioButton Name="radioExe" GroupName="AutoStart" HorizontalAlignment="Left" Margin="0,1,0,0" Click="radio_Click" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" ToolTip="MSFS2020_AutoFPS will be started by MSFS - it will modify your EXE.xml File.&#x0a;An existing Entry will automatically be updated, else a new Entry is inserted.&#x0a;An existing Auto-Start Entry in your FSUIPC7.ini will automatically be removed!&#x0a;A Backup of the Files is not created!">Auto-Start with MSFS</RadioButton>
</StackPanel>
<TextBlock Name="txtMessages" MinHeight="0" Margin="12,16,12,0"></TextBlock>
<Label Name="lblResult" HorizontalContentAlignment="Center" FontWeight="DemiBold" Margin="12,0,12,0"></Label>
<Label Name="lblAvWarning" HorizontalContentAlignment="Center" FontWeight="DemiBold" Margin="12,0,12,0" Visibility="Collapsed">Remember to set/update your Anti-Virus Exclusion, if necessary.</Label>
<Label Name="lblRebootWarning" HorizontalContentAlignment="Center" FontWeight="DemiBold" Margin="12,-8,12,0" Visibility="Collapsed">If you have installed MSFS2020_AutoFPS for the first Time, reboot your PC after Installation has finished!</Label>
<Button Name="btnInstall" FontWeight="DemiBold" Width="128" MinHeight="24" Margin="12,16,12,12" Click="btnInstall_Click">Install!</Button>
<Button Name="btnRemove" FontWeight="DemiBold" Width="128" MinHeight="24" Margin="12,4,12,12" Foreground="Red" Click="btnRemove_Click" ToolTip="Attention: Will remove the whole App including its current Configuration. Does also remove Auto-Start Entries for it from FSUIPC and MSFS.">Remove!</Button>
</StackPanel>
</Grid>
</Window>

View File

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
namespace Installer
{
public partial class InstallerWindow : Window
{
private InstallerWorker worker;
private Queue<string> messageQueue;
private DispatcherTimer timer;
private bool workerHasFinished = false;
public InstallerWindow()
{
InitializeComponent();
string assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
assemblyVersion = assemblyVersion.Substring(0, assemblyVersion.LastIndexOf('.'));
Title += " (" + assemblyVersion + ")";
if (Directory.Exists(Parameters.appDir))
btnInstall.Content = "Update!";
else
{
btnRemove.IsEnabled = false;
btnRemove.Visibility = Visibility.Hidden;
}
messageQueue = new Queue<string>();
worker = new InstallerWorker(messageQueue);
timer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(200)
};
timer.Tick += OnTick;
}
protected void OnTick(object sender, EventArgs e)
{
while (messageQueue.Count > 0)
{
txtMessages.Text += messageQueue.Dequeue().ToString() + "\n";
}
if (!worker.IsRunning)
{
timer.Stop();
workerHasFinished = true;
if (worker.HasError)
{
lblResult.Content = "ERROR during Installation!";
lblResult.Foreground = new SolidColorBrush(Colors.Red);
}
else
{
lblResult.Content = "FINISHED successfully!";
lblResult.Foreground = new SolidColorBrush(Colors.DarkGreen);
lblAvWarning.Visibility = Visibility.Visible;
lblRebootWarning.Visibility = Visibility.Visible;
}
btnInstall.Content = "Close";
btnInstall.IsEnabled = true;
Activate();
}
}
private void btnInstall_Click(object sender, RoutedEventArgs e)
{
if (!workerHasFinished)
{
if (InstallerFunctions.GetProcessRunning(Parameters.appName))
{
MessageBox.Show($"Please stop {Parameters.appName} and try again.", $"{Parameters.appName} is running!", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
chkDesktopLink_Click(null, null);
radio_Click(null, null);
btnInstall.IsEnabled = false;
btnRemove.Visibility = Visibility.Hidden;
btnRemove.IsEnabled = false;
timer.Start();
Task.Run(worker.Run);
}
else
{
App.Current.Shutdown();
}
}
private void btnRemove_Click(object sender, RoutedEventArgs e)
{
if (InstallerFunctions.GetProcessRunning(Parameters.appName))
{
MessageBox.Show($"Please stop {Parameters.appName} and try again.", $"{Parameters.appName} is running!", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
else
{
btnRemove.IsEnabled = false;
btnInstall.IsEnabled = false;
try
{
Directory.Delete(Parameters.appDir, true);
InstallerFunctions.AutoStartExe(true);
InstallerFunctions.AutoStartFsuipc(true);
}
catch (Exception ex)
{
MessageBox.Show($"Exception '{ex.GetType()}' during Uninstall", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
lblResult.Content = "REMOVED successfully!";
lblResult.Foreground = new SolidColorBrush(Colors.DarkGreen);
}
}
private void chkDesktopLink_Click(object sender, RoutedEventArgs e)
{
worker.CfgDesktopLink = chkDesktopLink.IsChecked ?? false;
}
private void radio_Click(object sender, RoutedEventArgs e)
{
if (radioNone.IsChecked == true)
worker.CfgAutoStart = AutoStart.NONE;
else if (radioFsuipc.IsChecked == true)
worker.CfgAutoStart = AutoStart.FSUIPC;
else if (radioExe.IsChecked == true)
worker.CfgAutoStart = AutoStart.EXE;
else if (radioRemove.IsChecked == true)
worker.CfgAutoStart = AutoStart.REMOVE;
}
}
}

View File

@ -0,0 +1,205 @@
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Windows;
namespace Installer
{
public class InstallerWorker
{
private Queue<string> messageList = null;
public bool IsRunning { get; private set; } = false;
public bool HasError { get; private set; } = false;
public bool CfgDesktopLink { get; set; } = false;
public AutoStart CfgAutoStart { get; set; } = AutoStart.NONE;
public InstallerWorker(Queue<string> messageList)
{
this.messageList = messageList;
}
public void Run()
{
IsRunning = true;
InstallDotNet();
if (!HasError)
InstallWasm();
if (!HasError)
InstallApp();
if (!HasError)
SetupAutoStart();
messageList.Enqueue("\nDone.");
if (!HasError)
messageList.Enqueue($"MSFS2020_AutoFPS was installed to {Parameters.appDir}");
IsRunning = false;
}
protected void InstallDotNet()
{
messageList.Enqueue("\nChecking .NET 7 Desktop Runtime ...");
if (InstallerFunctions.CheckDotNet())
messageList.Enqueue("Runtime already installed!");
else
{
messageList.Enqueue("Runtime not installed or outdated!");
messageList.Enqueue("Downloading .NET Desktop Runtime ...");
if (!InstallerFunctions.DownloadFile(Parameters.netUrl, Parameters.netUrlFile))
{
HasError = true;
messageList.Enqueue("Could not download .NET Runtime!");
return;
}
messageList.Enqueue("Installing .NET Desktop Runtime ...");
InstallerFunctions.RunCommand($"{Parameters.netUrlFile} /install /quiet /norestart");
File.Delete(Parameters.netUrlFile);
}
}
protected void InstallWasm()
{
messageList.Enqueue("\nChecking MobiFlight WASM/Event Module ...");
if (!InstallerFunctions.CheckInstalledMSFS(out string packagePath))
{
HasError = true;
messageList.Enqueue("Could not determine Package Path!");
return;
}
if (InstallerFunctions.CheckPackageVersion(packagePath, Parameters.wasmMobiName, Parameters.wasmMobiVersion))
{
messageList.Enqueue("Module already installed!");
}
else
{
if (!InstallerFunctions.GetProcessRunning("FlightSimulator"))
{
messageList.Enqueue("Module not installed or outdated!");
if (Directory.Exists(packagePath + @"\" + Parameters.wasmMobiName))
{
messageList.Enqueue("Deleting old Version ...");
Directory.Delete(packagePath + @"\" + Parameters.wasmMobiName, true);
}
messageList.Enqueue("Downloading MobiFlight Module ...");
if (!InstallerFunctions.DownloadFile(Parameters.wasmUrl, Parameters.wasmUrlFile))
{
HasError = true;
messageList.Enqueue("Could not download MobiFlight Module!");
return;
}
messageList.Enqueue("Extracting new Version ...");
if (!InstallerFunctions.ExtractZip(packagePath, Parameters.wasmUrlFile))
{
HasError = true;
messageList.Enqueue("Error while extracting MobiFlight Module!");
return;
}
File.Delete(Parameters.wasmUrlFile);
}
else
{
HasError = true;
messageList.Enqueue("Can not install/update Module while MSFS is running.");
MessageBox.Show("Please stop MSFS and try again.", "MSFS is running!", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
}
protected void InstallApp()
{
messageList.Enqueue("\nChecking Application State ...");
if (!Directory.Exists(Parameters.appDir))
{
messageList.Enqueue("Installing MSFS2020_AutoFPS ...");
messageList.Enqueue("Extracting Application ...");
if (!InstallerFunctions.ExtractZip())
{
HasError = true;
messageList.Enqueue("Error while extracting Application!");
return;
}
}
else
{
messageList.Enqueue("Deleting old Version ...");
if (Directory.Exists(Parameters.binDir))
Directory.Delete(Parameters.binDir, true);
Directory.CreateDirectory(Parameters.binDir);
messageList.Enqueue("Extracting new Version ...");
if (!InstallerFunctions.ExtractZip())
{
HasError = true;
messageList.Enqueue("Error while extracting Application!");
return;
}
}
if (!File.Exists(Parameters.confFile))
{
messageList.Enqueue("Creating Config File ...");
using (var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Installer.MSFS2020_AutoFPS.config"))
{
using (var file = new FileStream(Parameters.confFile, FileMode.Create, FileAccess.Write))
{
resource.CopyTo(file);
}
}
}
if (CfgDesktopLink)
{
messageList.Enqueue("Placing Shortcut ...");
InstallerFunctions.PlaceDesktopLink();
}
}
protected void SetupAutoStart()
{
if (CfgAutoStart == AutoStart.NONE)
return;
if (CfgAutoStart == AutoStart.FSUIPC)
{
messageList.Enqueue("Check/Remove MSFS Auto-Start ...");
InstallerFunctions.AutoStartExe(true);
messageList.Enqueue("Setup FSUIPC Auto-Start ...");
if (InstallerFunctions.AutoStartFsuipc())
messageList.Enqueue("Auto-Start added successfully!");
else
{
messageList.Enqueue("Failed to add Auto-Start!");
HasError = true;
}
}
if (CfgAutoStart == AutoStart.EXE)
{
messageList.Enqueue("Check/Remove FSUIPC Auto-Start ...");
InstallerFunctions.AutoStartFsuipc(true);
messageList.Enqueue("Setup EXE.xml Auto-Start ...");
if (InstallerFunctions.AutoStartExe())
messageList.Enqueue("Auto-Start added successfully!");
else
{
messageList.Enqueue("Failed to add Auto-Start!");
HasError = true;
}
}
if (CfgAutoStart == AutoStart.REMOVE)
{
messageList.Enqueue("Check/Remove FSUIPC Auto-Start ...");
InstallerFunctions.AutoStartFsuipc(true);
messageList.Enqueue("Check/Remove MSFS Auto-Start ...");
InstallerFunctions.AutoStartExe(true);
}
}
}
}

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<appSettings>
<add key="mfLvarPerFrame" value="15" />
<add key="ConfigVersion" value="1" />
<add key="logFilePath" value="MSFS2020_AutoFPS.log" />
<add key="logLevel" value="Debug" />
<add key="waitForConnect" value="true" />
<add key="openWindow" value="true" />
<add key="simBinary" value="FlightSimulator" />
<add key="simModule" value="WwiseLibPCx64P.dll" />
<add key="offsetModuleBase" value="0x004B2368" />
<add key="offsetPointerMain" value="0x3D0" />
<add key="offsetPointerTlod" value="0xC" />
<add key="offsetPointerTlodVr" value="0x114" />
<add key="offsetPointerOlod" value="0xC" />
<add key="simMinLod" value="10" />
<add key="selectedProfile" value="0" />
<add key="tlodPairs0" value="0:100|1500:150|5000:200" />
<add key="olodPairs0" value="0:100|2500:150|7500:200" />
<add key="isVr0" value="false" />
<add key="tlodPairs1" value="0:100|500:125|2000:150|7500:175|15000:200|20000:225" />
<add key="olodPairs1" value="0:100|500:125|2500:150|5000:175|9500:200" />
<add key="isVr1" value="false" />
<add key="tlodPairs2" value="0:100|1500:150|5000:200" />
<add key="olodPairs2" value="0:100|2500:150|7500:200" />
<add key="isVr2" value="true" />
<add key="tlodPairs3" value="0:100|1000:150|2000:200|3000:250|4000:300" />
<add key="olodPairs3" value="0:100|1000:70|2000:50|3000:30|4000:10" />
<add key="tlodPairs4" value="0:100|1000:200|2000:300|3000:400" />
<add key="olodPairs4" value="0:200|1000:150|2000:100|3000:50" />
<add key="tlodPairs5" value="0:100|1500:150|5000:200" />
<add key="olodPairs5" value="0:100|2500:150|7500:200" />
<add key="useTargetFps" value="false" />
<add key="targetFpsIndex" value="0" />
<add key="targetFps" value="40" />
<add key="FpsTolerance" value="5" />
<add key="constraintTicks" value="60" />
<add key="constraintDelayTicks" value="1" />
<add key="decreaseTlod" value="50" />
<add key="decreaseOlod" value="50" />
<add key="minLod" value="100" />
<add key="DecCloudQ" value="false" />
<add key="CloudRecoveryTLOD" value="100" />
<add key="minTLod" value="50" />
<add key="maxTLod" value="200" />
<add key="LodStepMax" value="false" />
<add key="LodStepMaxInc" value="5" />
<add key="LodStepMaxDec" value="5" />
<add key="offsetPointerCloudQ" value="0x44" />
<add key="offsetPointerCloudQVr" value="0x108" />
<add key="offsetPointerVrMode" value="0x1C" />
<add key="offsetPointerFgMode" value="0x4A" />
</appSettings>

43
Installer/Parameters.cs Normal file
View File

@ -0,0 +1,43 @@
using System;
using System.Text.RegularExpressions;
namespace Installer
{
public static class Parameters
{
public static readonly string fileName = "AppPackage.zip";
public static readonly string appName = "MSFS2020_AutoFPS";
public static readonly string appBinary = $"{appName}.exe";
public static readonly string appDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\MSFS2020_AutoFPS";
public static readonly string binDir = appDir + @"\bin";
public static readonly string binPath = binDir + @"\MSFS2020_AutoFPS.exe";
public static readonly string confFile = appDir + @"\MSFS2020_AutoFPS.config";
public static readonly Regex netDesktop = new Regex(@"Microsoft.WindowsDesktop.App ((\d+)\.(\d+)\.(\d+)).+", RegexOptions.Compiled);
public static readonly int netMajor = 7;
public static readonly int netMinor = 0;
public static readonly int netPatch = 14;
public static readonly string netVersion = $"{netMajor}.{netMinor}.{netPatch}";
public static readonly string netUrl = "https://download.visualstudio.microsoft.com/download/pr/8f5b0079-2bb4-49cd-874e-0f58703eff6e/7010b5f213a2c436a307eb385dbb16ff/windowsdesktop-runtime-7.0.14-win-x64.exe";
public static readonly string netUrlFile = "windowsdesktop-runtime-7.0.14-win-x64.exe";
public static readonly string ipcRegPath = @"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\FSUIPC7";
public static readonly string ipcRegInstallDirValue = "InstallDir";
public static readonly string ipcRegValue = "DisplayVersion";
public static readonly string ipcVersion = "7.4.0";
public static readonly Regex wasmRegex = new Regex("^\\s*\"package_version\":\\s*\"([0-9\\.]+)\"\\s*,\\s*$", RegexOptions.Compiled);
public static readonly string wasmMobiName = "mobiflight-event-module";
public static readonly string wasmMobiVersion = "0.7.1";
public static readonly string wasmUrl = "https://github.com/MobiFlight/MobiFlight-WASM-Module/releases/download/0.7.1/mobiflight-event-module-0.7.1.zip";
public static readonly string wasmUrlFile = "mobiflight-event-module-0.7.1.zip";
public static readonly string msConfigStore = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Packages\Microsoft.FlightSimulator_8wekyb3d8bbwe\LocalCache\UserCfg.opt";
public static readonly string msConfigSteam = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\Microsoft Flight Simulator\UserCfg.opt";
public static readonly string msStringPackage = "InstalledPackagesPath ";
public static readonly string msExeStore = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Packages\Microsoft.FlightSimulator_8wekyb3d8bbwe\LocalCache\EXE.xml";
public static readonly string msExeSteam = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\Microsoft Flight Simulator\EXE.xml";
}
}

View File

@ -0,0 +1,53 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows;
// Allgemeine Informationen über eine Assembly werden über die folgenden
// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern,
// die einer Assembly zugeordnet sind.
[assembly: AssemblyTitle("MSFS2020_AutoFPS Installer")]
[assembly: AssemblyDescription("Installer App for MSFS2020_AutoFPS")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("ResetXPDR")]
[assembly: AssemblyProduct("MSFS2020_AutoFPS Installer")]
[assembly: AssemblyCopyright("Copyright © 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly
// für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von
// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen.
[assembly: ComVisible(false)]
//Um mit dem Erstellen lokalisierbarer Anwendungen zu beginnen, legen Sie
//<UICulture>ImCodeVerwendeteKultur</UICulture> in der .csproj-Datei
//in einer <PropertyGroup> fest. Wenn Sie in den Quelldateien beispielsweise Deutsch
//(Deutschland) verwenden, legen Sie <UICulture> auf \"de-DE\" fest. Heben Sie dann die Auskommentierung
//des nachstehenden NeutralResourceLanguage-Attributs auf. Aktualisieren Sie "en-US" in der nachstehenden Zeile,
//sodass es mit der UICulture-Einstellung in der Projektdatei übereinstimmt.
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //Speicherort der designspezifischen Ressourcenwörterbücher
//(wird verwendet, wenn eine Ressource auf der Seite nicht gefunden wird,
// oder in den Anwendungsressourcen-Wörterbüchern nicht gefunden werden kann.)
ResourceDictionaryLocation.SourceAssembly //Speicherort des generischen Ressourcenwörterbuchs
//(wird verwendet, wenn eine Ressource auf der Seite nicht gefunden wird,
// designspezifischen Ressourcenwörterbuch nicht gefunden werden kann.)
)]
// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten:
//
// Hauptversion
// Nebenversion
// Buildnummer
// Revision
//
// Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden,
// indem Sie "*" wie unten gezeigt eingeben:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.4.0.0")]
[assembly: AssemblyFileVersion("0.4.0.0")]

View File

@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
// <auto-generated>
// Dieser Code wurde von einem Tool generiert.
// Laufzeitversion: 4.0.30319.42000
//
// Änderungen an dieser Datei können fehlerhaftes Verhalten verursachen und gehen verloren, wenn
// der Code erneut generiert wird.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Installer.Properties
{
/// <summary>
/// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw.
/// </summary>
// Diese Klasse wurde von der StronglyTypedResourceBuilder-Klasse
// über ein Tool wie ResGen oder Visual Studio automatisch generiert.
// Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen
// mit der Option /str erneut aus, oder erstellen Sie Ihr VS-Projekt neu.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
}
/// <summary>
/// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Installer.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle
/// Ressourcenlookups, die diese stark typisierte Ressourcenklasse verwenden.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Installer.Properties
{
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get
{
return defaultInstance;
}
}
}
}

View File

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

BIN
Installer/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="System.IO.Compression" version="4.3.0" targetFramework="net48" />
<package id="System.IO.Compression.ZipFile" version="4.3.0" targetFramework="net48" />
</packages>

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) [2024] [ResetXPDR]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

31
MSFS2020_AutoFPS.sln Normal file
View File

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.34031.279
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MSFS2020_AutoFPS", "MSFS2020_AutoFPS\MSFS2020_AutoFPS.csproj", "{8841C1AB-3A51-473C-B0F9-8705650478B2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Installer", "Installer\Installer.csproj", "{BF3DD08A-7547-4352-B1DD-9A343D757421}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8841C1AB-3A51-473C-B0F9-8705650478B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8841C1AB-3A51-473C-B0F9-8705650478B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8841C1AB-3A51-473C-B0F9-8705650478B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8841C1AB-3A51-473C-B0F9-8705650478B2}.Release|Any CPU.Build.0 = Release|Any CPU
{BF3DD08A-7547-4352-B1DD-9A343D757421}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BF3DD08A-7547-4352-B1DD-9A343D757421}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BF3DD08A-7547-4352-B1DD-9A343D757421}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BF3DD08A-7547-4352-B1DD-9A343D757421}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3AB5D6B8-BCDB-4095-ADB1-FFBE9D42D76B}
EndGlobalSection
EndGlobal

BIN
MSFS2020_AutoFPS.zip Normal file

Binary file not shown.

13
MSFS2020_AutoFPS/App.xaml Normal file
View File

@ -0,0 +1,13 @@
<Application x:Class="MSFS2020_AutoFPS.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MSFS2020_AutoFPS"
ShutdownMode="OnExplicitShutdown">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="NotifyIconResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -0,0 +1,149 @@
using H.NotifyIcon;
using Serilog;
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
namespace MSFS2020_AutoFPS
{
public partial class App : Application
{
private ServiceModel Model;
private ServiceController Controller;
private TaskbarIcon notifyIcon;
public static new App Current => Application.Current as App;
public static string ConfigFile = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\MSFS2020_AutoFPS\MSFS2020_AutoFPS.config";
public static string AppDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\MSFS2020_AutoFPS\bin";
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
if (Process.GetProcessesByName("MSFS2020_AutoFPS").Length > 1)
{
MessageBox.Show("MSFS2020_AutoFPS is already running!", "Critical Error", MessageBoxButton.OK, MessageBoxImage.Error);
Application.Current.Shutdown();
return;
}
if (Process.GetProcessesByName("DynamicLOD_ResetEdition").Length > 0)
{
MessageBox.Show("DynamicLOD_ResetEdition is already running!", "Critical Error", MessageBoxButton.OK, MessageBoxImage.Error);
Application.Current.Shutdown();
return;
}
if (Process.GetProcessesByName("DynamicLOD").Length > 0)
{
MessageBox.Show("A pre-ResetEdition version of AutoLOD is already running!", "Critical Error", MessageBoxButton.OK, MessageBoxImage.Error);
Application.Current.Shutdown();
return;
}
Directory.SetCurrentDirectory(AppDir);
if (!File.Exists(ConfigFile))
{
string ConfigFileDefault = Directory.GetCurrentDirectory() + @"\MSFS2020_AutoFPS.config";
if (!File.Exists(ConfigFileDefault))
{
MessageBox.Show("No Configuration File found! Closing ...", "Critical Error", MessageBoxButton.OK, MessageBoxImage.Error);
Application.Current.Shutdown();
return;
}
else
{
File.Copy(ConfigFileDefault, ConfigFile);
}
}
Model = new();
InitLog();
InitSystray();
Controller = new(Model);
Task.Run(Controller.Run);
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
timer.Tick += OnTick;
timer.Start();
MainWindow = new MainWindow(notifyIcon.DataContext as NotifyIconViewModel, Model);
if (Model.OpenWindow)
MainWindow.Show();
}
protected override void OnExit(ExitEventArgs e)
{
if (Model != null)
{
Model.CancellationRequested = true;
if (Model.DefaultSettingsRead && Model.IsSessionRunning)
{
//Logger.Log(LogLevel.Information, "App:OnExit", $"Resetting LODs to {Model.DefaultTLOD} / {Model.DefaultOLOD} and VR {Model.DefaultTLOD_VR} / {Model.DefaultOLOD_VR}");
Logger.Log(LogLevel.Information, "ServiceController:ServiceLoop", $"Sim still running, resetting TLODs to PC / VR {Model.DefaultTLOD} / {Model.DefaultTLOD_VR}");
Model.MemoryAccess.SetTLOD_PC(Model.DefaultTLOD);
Model.MemoryAccess.SetTLOD_VR(Model.DefaultTLOD_VR);
//Model.MemoryAccess.SetOLOD_PC(Model.DefaultOLOD);
//Model.MemoryAccess.SetOLOD_VR(Model.DefaultOLOD_VR);
Logger.Log(LogLevel.Information, "App:OnExit", $"Resetting cloud quality to {Model.DefaultCloudQ} / VR {Model.DefaultCloudQ_VR}");
Model.MemoryAccess.SetCloudQ(Model.DefaultCloudQ);
Model.MemoryAccess.SetCloudQ_VR(Model.DefaultCloudQ_VR);
}
}
notifyIcon?.Dispose();
base.OnExit(e);
Logger.Log(LogLevel.Information, "App:OnExit", "MSFS2020_AutoFPS exiting ...");
}
protected void OnTick(object sender, EventArgs e)
{
if (Model.ServiceExited)
{
Current.Shutdown();
}
}
protected void InitLog()
{
string logFilePath = @"..\log\" + Model.GetSetting("logFilePath", "MSFS2020_AutoFPS.log");
string logLevel = Model.GetSetting("logLevel", "Debug"); ;
LoggerConfiguration loggerConfiguration = new LoggerConfiguration().WriteTo.File(logFilePath, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 3,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message} {NewLine}{Exception}");
if (logLevel == "Warning")
loggerConfiguration.MinimumLevel.Warning();
else if (logLevel == "Debug")
loggerConfiguration.MinimumLevel.Debug();
else if (logLevel == "Verbose")
loggerConfiguration.MinimumLevel.Verbose();
else
loggerConfiguration.MinimumLevel.Information();
Log.Logger = loggerConfiguration.CreateLogger();
Log.Information($"-----------------------------------------------------------------------");
Logger.Log(LogLevel.Information, "App:InitLog", $"MSFS2020_AutoFPS started! Log Level: {logLevel} Log File: {logFilePath}");
}
protected void InitSystray()
{
Logger.Log(LogLevel.Information, "App:InitSystray", $"Creating SysTray Icon ...");
notifyIcon = (TaskbarIcon)FindResource("NotifyIcon");
notifyIcon.Icon = GetIcon("icon.ico");
notifyIcon.ForceCreate(false);
}
public static Icon GetIcon(string filename)
{
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"MSFS2020_AutoFPS.{filename}");
return new Icon(stream);
}
}
}

View File

@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.IO;
using System.Xml;
namespace MSFS2020_AutoFPS
{
public class ConfigurationFile
{
private Dictionary<string, string> appSettings = new();
private XmlDocument xmlDoc = new();
public string this[string key]
{
get => GetSetting(key);
set => SetSetting(key, value);
}
public void LoadConfiguration()
{
xmlDoc = new();
xmlDoc.LoadXml(File.ReadAllText(App.ConfigFile));
XmlNode xmlSettings = xmlDoc.ChildNodes[1];
appSettings.Clear();
foreach(XmlNode child in xmlSettings.ChildNodes)
appSettings.Add(child.Attributes["key"].Value, child.Attributes["value"].Value);
}
public void SaveConfiguration()
{
foreach (XmlNode child in xmlDoc.ChildNodes[1])
child.Attributes["value"].Value = appSettings[child.Attributes["key"].Value];
xmlDoc.Save(App.ConfigFile);
}
public string GetSetting(string key, string defaultValue = "")
{
if (appSettings.ContainsKey(key))
return appSettings[key];
else
{
XmlNode newNode = xmlDoc.CreateElement("add");
XmlAttribute attribute = xmlDoc.CreateAttribute("key");
attribute.Value = key;
newNode.Attributes.Append(attribute);
attribute = xmlDoc.CreateAttribute("value");
attribute.Value = defaultValue;
newNode.Attributes.Append(attribute);
xmlDoc.ChildNodes[1].AppendChild(newNode);
appSettings.Add(key, defaultValue);
SaveConfiguration();
return defaultValue;
}
}
public void SetSetting(string key, string value)
{
if (appSettings.ContainsKey(key))
{
appSettings[key] = value;
SaveConfiguration();
}
}
}
}

View File

View File

@ -0,0 +1,13 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0044:Modifizierer \"readonly\" hinzufügen")]
[assembly: SuppressMessage("Performance", "CA1822:Member als statisch markieren", Justification = "<Ausstehend>", Scope = "member", Target = "~M:MSFS2020_AutoFPS.MobiSimConnect.SimConnect_OnException(Microsoft.FlightSimulator.SimConnect.SimConnect,Microsoft.FlightSimulator.SimConnect.SIMCONNECT_RECV_EXCEPTION)")]
[assembly: SuppressMessage("Interoperability", "SYSLIB1054:Verwenden Sie \\\"LibraryImportAttribute\\\" anstelle von \\\"DllImportAttribute\\\", um P/Invoke-Marshallingcode zur Kompilierzeit zu generieren.")]
[assembly: SuppressMessage("Performance", "CA1822:Member als statisch markieren", Justification = "<Ausstehend>", Scope = "member", Target = "~M:MSFS2020_AutoFPS.NotifyIconViewModel.ExitApplication")]
[assembly: SuppressMessage("Style", "IDE1006:Benennungsstile")]
[assembly: SuppressMessage("Usage", "CA2211:Nicht konstante Felder dürfen nicht sichtbar sein")]

View File

@ -0,0 +1,139 @@
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespace MSFS2020_AutoFPS
{
public static class IPCManager
{
public static readonly int waitDuration = 30000;
public static MobiSimConnect SimConnect { get; set; } = null;
public static bool WaitForSimulator(ServiceModel model)
{
bool simRunning = IsSimRunning();
bool logEntryMade = false;
if (!simRunning && model.WaitForConnect)
{
do
{
if (!logEntryMade)
{
Logger.Log(LogLevel.Information, "IPCManager:WaitForSimulator", $"Simulator not started - waiting {waitDuration / 1000}s between Retries");
logEntryMade = true;
}
Thread.Sleep(waitDuration);
}
while (!IsSimRunning() && !model.CancellationRequested);
Thread.Sleep(waitDuration);
return true;
}
else if (simRunning)
{
Logger.Log(LogLevel.Information, "IPCManager:WaitForSimulator", $"Simulator started");
return true;
}
else
{
Logger.Log(LogLevel.Error, "IPCManager:WaitForSimulator", $"Simulator not started - aborting");
return false;
}
}
public static bool IsProcessRunning(string name)
{
Process proc = Process.GetProcessesByName(name).FirstOrDefault();
return proc != null && proc.ProcessName == name;
}
public static bool IsSimRunning()
{
return IsProcessRunning("FlightSimulator");
}
public static bool WaitForConnection(ServiceModel model)
{
if (!IsSimRunning())
return false;
SimConnect = new MobiSimConnect();
bool mobiRequested = SimConnect.Connect();
bool logEntryMade = false;
if (!SimConnect.IsConnected)
{
do
{
if (!logEntryMade)
{
Logger.Log(LogLevel.Information, "IPCManager:WaitForConnection", $"Connection not established - waiting {waitDuration / 1000}s between Retries");
logEntryMade = true;
}
Thread.Sleep(waitDuration / 2);
if (!mobiRequested)
mobiRequested = SimConnect.Connect();
}
while (!SimConnect.IsConnected && IsSimRunning() && !model.CancellationRequested);
return SimConnect.IsConnected;
}
else
{
Logger.Log(LogLevel.Information, "IPCManager:WaitForConnection", $"SimConnect is opened");
return true;
}
}
public static bool WaitForSessionReady(ServiceModel model)
{
int waitDuration = 5000;
SimConnect.SubscribeSimVar("CAMERA STATE", "Enum");
SimConnect.SubscribeSimVar("PLANE IN PARKING STATE", "Bool");
Thread.Sleep(250);
bool isReady = IsCamReady();
bool logEntryMade = false;
while (IsSimRunning() && !isReady && !model.CancellationRequested)
{
if (!logEntryMade)
{
Logger.Log(LogLevel.Information, "IPCManager:WaitForSessionReady", $"Session not ready - waiting {waitDuration / 1000}s between Retries");
logEntryMade = true;
}
Thread.Sleep(waitDuration);
isReady = IsCamReady();
}
if (!isReady)
{
Logger.Log(LogLevel.Information, "IPCManager:WaitForSessionReady", $"SimConnect or Simulator not available - aborting");
return false;
}
return true;
}
public static bool IsCamReady()
{
float value = SimConnect.ReadSimVar("CAMERA STATE", "Enum");
bool parkState = SimConnect.ReadSimVar("PLANE IN PARKING STATE", "Bool") == 1;
return value >= 2 && value <= 5 && !parkState;
}
public static void CloseSafe()
{
try
{
if (SimConnect != null)
{
SimConnect.Disconnect();
SimConnect = null;
}
}
catch { }
}
}
}

View File

@ -0,0 +1,354 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Eventing.Reader;
using System.Drawing.Printing;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace MSFS2020_AutoFPS
{
public class LODController
{
private MobiSimConnect SimConnect;
private ServiceModel Model;
private int[] verticalStats = new int[5];
private float[] verticalStatsVS = new float[5];
private int verticalIndex = 0;
private int altAboveGnd = 0;
private int groundSpeed = 0;
private float tlod = 0;
private float tlod_dec = 0;
private float olod = 0;
private float olod_dec = 0;
private int cloudQ = 0;
private int cloudQ_VR = 0;
public bool FirstStart { get; set; } = true;
private int fpsModeTicks = 0;
private int fpsModeDelayTicks = 0;
public LODController(ServiceModel model)
{
Model = model;
SimConnect = IPCManager.SimConnect;
SimConnect.SubscribeSimVar("VERTICAL SPEED", "feet per second");
SimConnect.SubscribeSimVar("PLANE ALT ABOVE GROUND", "feet");
SimConnect.SubscribeSimVar("PLANE ALT ABOVE GROUND MINUS CG", "feet");
SimConnect.SubscribeSimVar("SIM ON GROUND", "Bool");
SimConnect.SubscribeSimVar("GROUND VELOCITY", "knots");
tlod = Model.MemoryAccess.GetTLOD_PC();
olod = Model.MemoryAccess.GetOLOD_PC();
cloudQ = Model.MemoryAccess.GetCloudQ_PC();
cloudQ_VR = Model.MemoryAccess.GetCloudQ_VR();
if (cloudQ > Model.DefaultCloudQ) Model.DefaultCloudQ = cloudQ;
if (cloudQ_VR > Model.DefaultCloudQ_VR) Model.DefaultCloudQ_VR = cloudQ_VR;
Model.CurrentPairTLOD = 0;
Model.CurrentPairOLOD = 0;
Model.fpsMode = false;
Model.tlod_step = false;
Model.olod_step = false;
}
private void UpdateVariables()
{
float vs = SimConnect.ReadSimVar("VERTICAL SPEED", "feet per second");
Model.OnGround = SimConnect.ReadSimVar("SIM ON GROUND", "Bool") == 1.0f;
verticalStatsVS[verticalIndex] = vs;
if (vs >= 8.0f)
verticalStats[verticalIndex] = 1;
else if (vs <= -8.0f)
verticalStats[verticalIndex] = -1;
else
verticalStats[verticalIndex] = 0;
verticalIndex++;
if (verticalIndex >= verticalStats.Length || verticalIndex >= verticalStatsVS.Length)
verticalIndex = 0;
Model.VerticalTrend = VerticalAverage();
altAboveGnd = (int)SimConnect.ReadSimVar("PLANE ALT ABOVE GROUND", "feet");
if (altAboveGnd == 0 && !Model.OnGround)
altAboveGnd = (int)SimConnect.ReadSimVar("PLANE ALT ABOVE GROUND MINUS CG", "feet");
groundSpeed = (int)SimConnect.ReadSimVar("GROUND VELOCITY", "knots");
tlod = Model.MemoryAccess.GetTLOD_PC();
olod = Model.MemoryAccess.GetOLOD_PC();
cloudQ = Model.MemoryAccess.GetCloudQ_PC();
cloudQ_VR = Model.MemoryAccess.GetCloudQ_VR();
}
public void RunTick()
{
UpdateVariables();
float newTLOD;
float deltaFPS = GetAverageFPS() - Model.TargetFPS;
if (Math.Abs(deltaFPS) >= Model.TargetFPS * Model.FPSTolerance / 100)
{
newTLOD = tlod + Math.Sign(deltaFPS) * Model.LodStepMaxInc * (Math.Abs(deltaFPS) >= Model.TargetFPS * 2 * Model.FPSTolerance / 100 && (groundSpeed < 1 || !Model.OnGround) ? 2 : 1) * (altAboveGnd < 1000 && !Model.OnGround ? (float)altAboveGnd / 1000 : 1);
newTLOD = Math.Min(Model.MaxTLOD, Math.Max(Model.MinTLOD, newTLOD));
if (Math.Abs(tlod - newTLOD) >= 1 && (!Model.OnGround || groundSpeed < 1))
{
Model.MemoryAccess.SetTLOD(newTLOD);
Model.tlod_step = true;
}
else Model.tlod_step = false;
if (Model.DecCloudQ && newTLOD == Model.MinTLOD)
{
if (Model.MemoryAccess.IsVrModeActive() && Model.DefaultCloudQ_VR >= 1)
{
Model.MemoryAccess.SetCloudQ_VR(Model.DefaultCloudQ_VR - 1);
Model.DecCloudQActive = true;
}
if (!Model.MemoryAccess.IsVrModeActive() && Model.DefaultCloudQ >= 1)
{
Model.MemoryAccess.SetCloudQ(Model.DefaultCloudQ - 1);
Model.DecCloudQActive = true;
}
}
if (Model.DecCloudQ && Model.DecCloudQActive && newTLOD >= Model.CloudRecoveryTLOD)
{
if (Model.MemoryAccess.IsVrModeActive()) Model.MemoryAccess.SetCloudQ_VR(Model.DefaultCloudQ_VR);
else Model.MemoryAccess.SetCloudQ(Model.DefaultCloudQ);
Model.DecCloudQActive = false;
}
}
else
{
Model.tlod_step = false;
Model.olod_step = false;
}
return;
if (FirstStart)
{
fpsModeTicks++;
if (fpsModeTicks > 2)
FindPairs();
Model.ForceEvaluation = false;
return;
}
if (Model.ForceEvaluation) FindPairs();
if (Model.UseTargetFPS)
{
if (GetAverageFPS() < Model.TargetFPS)
{
if (!Model.fpsMode)
{
if (fpsModeDelayTicks >= Model.ConstraintDelayTicks)
{
Logger.Log(LogLevel.Information, "LODController:RunTick", $"FPS Constraint active");
Model.fpsMode = true;
if (Model.DecCloudQ && Model.DefaultCloudQ >= 1)
{
Model.MemoryAccess.SetCloudQ(Model.DefaultCloudQ - 1);
}
if (Model.DecCloudQ && Model.DefaultCloudQ_VR >= 1)
{
Model.MemoryAccess.SetCloudQ_VR(Model.DefaultCloudQ_VR - 1);
}
tlod_dec = Model.DecreaseTLOD;
olod_dec = Model.DecreaseOLOD;
fpsModeDelayTicks = 0;
}
else fpsModeDelayTicks++; }
}
else if (GetAverageFPS() > Model.TargetFPS + (Model.DecCloudQ ? Model.CloudRecoveryTLOD : 0) && Model.fpsMode)
{
fpsModeTicks++;
if (fpsModeTicks > Model.ConstraintTicks || Model.ForceEvaluation)
ResetFPSMode();
}
else fpsModeDelayTicks = 0;
}
else if (!Model.UseTargetFPS && Model.fpsMode)
ResetFPSMode();
float evalResult = EvaluateLodPairByHeight(ref Model.CurrentPairTLOD, Model.PairsTLOD[Model.SelectedProfile]);
if (VerticalAverage() > 0 && Model.CurrentPairTLOD + 1 < Model.PairsTLOD[Model.SelectedProfile].Count)
Model.AltLead = Math.Abs(Model.PairsTLOD[Model.SelectedProfile][Model.CurrentPairTLOD + 1].Item2 - Model.PairsTLOD[Model.SelectedProfile][Model.CurrentPairTLOD].Item2) / Model.LodStepMaxInc * VSAverage();
else if (VerticalAverage() < 0 && Model.CurrentPairTLOD - 1 >= 0)
Model.AltLead = Math.Abs(Model.PairsTLOD[Model.SelectedProfile][Model.CurrentPairTLOD].Item2 - Model.PairsTLOD[Model.SelectedProfile][Model.CurrentPairTLOD - 1].Item2) / Model.LodStepMaxDec * VSAverage();
else Model.AltLead = 0;
float newlod = EvaluateLodValue(Model.PairsTLOD[Model.SelectedProfile], Model.CurrentPairTLOD, tlod_dec, true);
if (tlod != newlod)
{
if (evalResult > 0) Logger.Log(LogLevel.Information, "LODController:RunTick", $"Setting TLOD {newlod}" + (Model.LodStepMax ? " in steps" : ""));
if (!Model.ForceEvaluation)
{
Model.tlod_step = true;
if (tlod > newlod)
{
if (tlod - Model.LodStepMaxDec > newlod) newlod = tlod - Model.LodStepMaxDec;
else Model.tlod_step = false;
}
else
{
if (tlod + Model.LodStepMaxInc < newlod) newlod = tlod + Model.LodStepMaxInc;
else Model.tlod_step = false;
}
}
Model.MemoryAccess.SetTLOD(newlod);
}
else Model.tlod_step = false;
evalResult = EvaluateLodPairByHeight(ref Model.CurrentPairOLOD, Model.PairsOLOD[Model.SelectedProfile]);
newlod = EvaluateLodValue(Model.PairsOLOD[Model.SelectedProfile], Model.CurrentPairOLOD, olod_dec, false);
if (olod != newlod)
{
if (evalResult > 0) Logger.Log(LogLevel.Information, "LODController:RunTick", $"Setting OLOD {newlod}" + (Model.LodStepMax ? " in steps" : ""));
if (!Model.ForceEvaluation)
{
Model.olod_step = true;
if (olod > newlod)
{
if (olod - Model.LodStepMaxDec > newlod) newlod = olod - Model.LodStepMaxDec;
else Model.olod_step = false;
}
else
{
if (olod + Model.LodStepMaxInc < newlod) newlod = olod + Model.LodStepMaxInc;
else Model.olod_step = false;
}
}
Model.MemoryAccess.SetOLOD(newlod);
}
else Model.olod_step = false;
Model.ForceEvaluation = false;
}
private float EvaluateLodValue(List<(float, float)> pairs, int currentPair, float decrement, bool tlod)
{
if (Model.fpsMode)
return Math.Max(pairs[currentPair].Item2 - decrement, Math.Max((tlod ? Model.MinTLOD : Model.MaxTLOD), Model.SimMinLOD));
else
return Math.Max(pairs[currentPair].Item2, Model.SimMinLOD);
}
private void ResetFPSMode(bool logEntry = true)
{
if (logEntry) Logger.Log(LogLevel.Information, "LODController:RunTick", $"FPS Constraint lifted");
Model.fpsMode = false;
fpsModeTicks = 0;
fpsModeDelayTicks = 0;
tlod_dec = 0;
olod_dec = 0;
if (Model.DecCloudQ || Model.ForceEvaluation)
{
Model.MemoryAccess.SetCloudQ(Model.DefaultCloudQ);
Model.MemoryAccess.SetCloudQ_VR(Model.DefaultCloudQ_VR);
}
}
private float EvaluateLodPairByHeight(ref int index, List<(float, float)> lodPairs)
{
float result = -1.0f;
Logger.Log(LogLevel.Verbose, "LODController:EvaluateLodByHeight", $"VerticalAverage {VerticalAverage()}");
if ((VerticalAverage() > 0 || Model.ForceEvaluation) && index + 1 < lodPairs.Count && altAboveGnd + Math.Abs(lodPairs[index + 1].Item2 - lodPairs[index].Item2) / Model.LodStepMaxInc * VSAverage() > lodPairs[index + 1].Item1)
{
index++;
Logger.Log(LogLevel.Information, "LODController:EvaluateLodByHeight", $"Higher Pair found (altAboveGnd: {altAboveGnd} | index: {index} | lod: {lodPairs[index].Item2})");
return lodPairs[index].Item2;
}
else if ((VerticalAverage() < 0 || Model.ForceEvaluation) && index - 1 >= 0 && altAboveGnd + Math.Abs(lodPairs[index].Item2 - lodPairs[index - 1].Item2) / Model.LodStepMaxDec * VSAverage() < lodPairs[index].Item1)
{
index--;
Logger.Log(LogLevel.Information, "LODController:EvaluateLodByHeight", $"Lower Pair found (altAboveGnd: {altAboveGnd} | index: {index} | lod: {lodPairs[index].Item2})");
return lodPairs[index].Item2;
}
else if (VerticalAverage() == 0 && index + 1 < lodPairs.Count && altAboveGnd * 0.95 > lodPairs[index + 1].Item1)
{
index++;
Logger.Log(LogLevel.Information, "LODController:EvaluateLodByHeight", $"Higher Pair found (altAboveGnd: {altAboveGnd} | index: {index} | lod: {lodPairs[index].Item2})");
return lodPairs[index].Item2;
}
else if (VerticalAverage() == 0 && altAboveGnd * 1.05 < lodPairs[index].Item1 && index - 1 >= 0)
{
index--;
Logger.Log(LogLevel.Information, "LODController:EvaluateLodByHeight", $"Lower Pair found (altAboveGnd: {altAboveGnd} | index: {index} | lod: {lodPairs[index].Item2})");
return lodPairs[index].Item2;
}
return result;
}
public int VerticalAverage()
{
return verticalStats.Sum();
}
public float VSAverage()
{
return verticalStatsVS.Average();
}
private void FindPairs()
{
Logger.Log(LogLevel.Information, "LODController:FindPairs", $"Finding Pairs (onGround: {Model.OnGround} | tlod: {tlod} | olod: {olod})");
if (!Model.OnGround)
{
int result = 0;
for (int i = 0; i < Model.PairsTLOD[Model.SelectedProfile].Count; i++)
{
if (altAboveGnd > Model.PairsTLOD[Model.SelectedProfile][i].Item1)
result = i;
}
Model.CurrentPairTLOD = result;
Logger.Log(LogLevel.Information, "LODController:FindPairs", $"TLOD Index {result}");
if (Model.ForceEvaluation || tlod != Model.PairsTLOD[Model.SelectedProfile][result].Item2)
{
Logger.Log(LogLevel.Information, "LODController:FindPairs", $"Setting TLOD {Model.PairsTLOD[Model.SelectedProfile][result].Item2}");
Model.MemoryAccess.SetTLOD(Model.PairsTLOD[Model.SelectedProfile][result].Item2);
}
result = 0;
for (int i = 0; i < Model.PairsOLOD[Model.SelectedProfile].Count; i++)
{
if (altAboveGnd > Model.PairsOLOD[Model.SelectedProfile][i].Item1)
result = i;
}
Model.CurrentPairOLOD = result;
Logger.Log(LogLevel.Information, "LODController:FindPairs", $"OLOD Index {result}");
if (Model.ForceEvaluation || olod != Model.PairsOLOD[Model.SelectedProfile][result].Item2)
{
Logger.Log(LogLevel.Information, "LODController:FindPairs", $"Setting OLOD {Model.PairsOLOD[Model.SelectedProfile][result].Item2}");
Model.MemoryAccess.SetOLOD(Model.PairsOLOD[Model.SelectedProfile][result].Item2);
}
}
else
{
int result = 0;
Model.CurrentPairTLOD = result;
if (Model.ForceEvaluation || tlod != Model.PairsTLOD[Model.SelectedProfile][result].Item2)
{
Logger.Log(LogLevel.Information, "LODController:FindPairs", $"Setting TLOD {Model.PairsTLOD[Model.SelectedProfile][result].Item2}");
Model.MemoryAccess.SetTLOD(Model.PairsTLOD[Model.SelectedProfile][result].Item2);
}
Model.CurrentPairOLOD = result;
if (Model.ForceEvaluation || olod != Model.PairsOLOD[Model.SelectedProfile][result].Item2)
{
Logger.Log(LogLevel.Information, "LODController:FindPairs", $"Setting OLOD {Model.PairsOLOD[Model.SelectedProfile][result].Item2}");
Model.MemoryAccess.SetOLOD(Model.PairsOLOD[Model.SelectedProfile][result].Item2);
}
}
ResetFPSMode(false);
FirstStart = false;
}
public float GetAverageFPS()
{
if (Model.MemoryAccess.IsFgModeActive())
return (float)Math.Round(IPCManager.SimConnect.GetAverageFPS() * 2.0f);
else
return (float)Math.Round(IPCManager.SimConnect.GetAverageFPS());
}
}
}

View File

@ -0,0 +1,55 @@
using System.Collections;
namespace MSFS2020_AutoFPS
{
public enum LogLevel
{
Critical = 5,
Error = 4,
Warning = 3,
Information = 2,
Debug = 1,
Verbose = 0,
}
public static class Logger
{
public static readonly Queue MessageQueue = new();
public static void Log(LogLevel level, string context, string message)
{
string entry = string.Format("[ {0,-32} ] {1}", (context.Length <= 32 ? context : context[0..32]), message.Replace("\n", "").Replace("\r", "").Replace("\t", ""));
switch (level)
{
case LogLevel.Critical:
Serilog.Log.Logger.Fatal(entry);
break;
case LogLevel.Error:
Serilog.Log.Logger.Error(entry);
break;
case LogLevel.Warning:
Serilog.Log.Logger.Warning(entry);
break;
case LogLevel.Information:
Serilog.Log.Logger.Information(entry);
break;
case LogLevel.Debug:
Serilog.Log.Logger.Debug(entry);
break;
case LogLevel.Verbose:
Serilog.Log.Logger.Verbose(entry);
break;
default:
Serilog.Log.Logger.Debug(entry);
break;
}
if (level > LogLevel.Debug)
{
if (message.Length > 80)
MessageQueue.Enqueue(message[1..80]);
else
MessageQueue.Enqueue(message);
}
}
}
}

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<appSettings>
<add key="mfLvarPerFrame" value="15" />
<add key="ConfigVersion" value="1" />
<add key="logFilePath" value="MSFS2020_AutoFPS.log" />
<add key="logLevel" value="Debug" />
<add key="waitForConnect" value="true" />
<add key="openWindow" value="true" />
<add key="simBinary" value="FlightSimulator" />
<add key="simModule" value="WwiseLibPCx64P.dll" />
<add key="offsetModuleBase" value="0x004B2368" />
<add key="offsetPointerMain" value="0x3D0" />
<add key="offsetPointerTlod" value="0xC" />
<add key="offsetPointerTlodVr" value="0x114" />
<add key="offsetPointerOlod" value="0xC" />
<add key="simMinLod" value="10" />
<add key="selectedProfile" value="0" />
<add key="tlodPairs0" value="0:100|1500:150|5000:200" />
<add key="olodPairs0" value="0:100|2500:150|7500:200" />
<add key="isVr0" value="false" />
<add key="tlodPairs1" value="0:100|500:125|2000:150|7500:175|15000:200|20000:225" />
<add key="olodPairs1" value="0:100|500:125|2500:150|5000:175|9500:200" />
<add key="isVr1" value="false" />
<add key="tlodPairs2" value="0:100|1500:150|5000:200" />
<add key="olodPairs2" value="0:100|2500:150|7500:200" />
<add key="isVr2" value="true" />
<add key="tlodPairs3" value="0:100|1000:150|2000:200|3000:250|4000:300" />
<add key="olodPairs3" value="0:100|1000:70|2000:50|3000:30|4000:10" />
<add key="tlodPairs4" value="0:100|1000:200|2000:300|3000:400" />
<add key="olodPairs4" value="0:200|1000:150|2000:100|3000:50" />
<add key="tlodPairs5" value="0:100|1500:150|5000:200" />
<add key="olodPairs5" value="0:100|2500:150|7500:200" />
<add key="useTargetFps" value="false" />
<add key="targetFpsIndex" value="0" />
<add key="targetFps" value="40" />
<add key="FpsTolerance" value="5" />
<add key="constraintTicks" value="60" />
<add key="constraintDelayTicks" value="1" />
<add key="decreaseTlod" value="50" />
<add key="decreaseOlod" value="50" />
<add key="minLod" value="100" />
<add key="DecCloudQ" value="false" />
<add key="CloudRecoveryTLOD" value="100" />
<add key="minTLod" value="50" />
<add key="maxTLod" value="200" />
<add key="LodStepMax" value="false" />
<add key="LodStepMaxInc" value="5" />
<add key="LodStepMaxDec" value="5" />
<add key="offsetPointerCloudQ" value="0x44" />
<add key="offsetPointerCloudQVr" value="0x108" />
<add key="offsetPointerVrMode" value="0x1C" />
<add key="offsetPointerFgMode" value="0x4A" />
</appSettings>

View File

@ -0,0 +1,69 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>disable</Nullable>
<UseWPF>true</UseWPF>
<StartupObject>MSFS2020_AutoFPS.App</StartupObject>
<ApplicationIcon>icon.ico</ApplicationIcon>
<PlatformTarget>x64</PlatformTarget>
<Authors>Fragtality</Authors>
<Description>Another Adapative LOD Implementation based on muumimorko
Work</Description>
<Copyright>Copyright © 2024</Copyright>
<AllowUnsafeBlocks>False</AllowUnsafeBlocks>
<Version>0.4.0</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<None Remove="Dev Notes.txt" />
<None Remove="icon.ico" />
</ItemGroup>
<ItemGroup>
<Content Include="icon.ico" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="icon.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="H.NotifyIcon.Wpf" Version="2.0.124" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.FlightSimulator.SimConnect">
<HintPath>Microsoft.FlightSimulator.SimConnect.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Update="MSFS2020_AutoFPS.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Microsoft.FlightSimulator.SimConnect.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="SimConnect.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="rem powershell -ExecutionPolicy Unrestricted -file &quot;$(ProjectDir)CopyToMSFS.ps1&quot; $(ConfigurationName)" />
</Target>
</Project>

View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34525.116
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoLOD_ResetEdition", "AutoLOD_ResetEdition.csproj", "{0711DA4D-2FE5-48D7-A3FF-BAF603826D20}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0711DA4D-2FE5-48D7-A3FF-BAF603826D20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0711DA4D-2FE5-48D7-A3FF-BAF603826D20}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0711DA4D-2FE5-48D7-A3FF-BAF603826D20}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0711DA4D-2FE5-48D7-A3FF-BAF603826D20}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BFE76200-F3AB-4DA7-A79D-23E7F052AF08}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,157 @@
<Window x:Class="MSFS2020_AutoFPS.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MSFS2020_AutoFPS"
mc:Ignorable="d"
Title="MSFS2020_AutoFPS" Height="480" Width="402" SizeToContent="WidthAndHeight" ResizeMode="NoResize" IsVisibleChanged="Window_IsVisibleChanged" Closing="Window_Closing">
<Window.Resources>
<Style TargetType="GroupBox">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}" FontWeight="DemiBold"/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Viewbox Name="viewBox" Stretch="None" StretchDirection="DownOnly" HorizontalAlignment="Left" Width="399">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="240*" MinWidth="240"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="64*"/>
<RowDefinition Height="64*"/>
<RowDefinition Height="64*"/>
<RowDefinition Height="256*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Vertical">
<GroupBox Grid.Row="0" Grid.Column="0" BorderBrush="DarkGray" BorderThickness="1" Margin="10,8,10,8">
<GroupBox.Header>Connection Status</GroupBox.Header>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<Label Name="lblConnStatMSFS" Height="40" VerticalContentAlignment="Center" VerticalAlignment="Center" TextBlock.Foreground="Red" Padding="8,0,16,0">MSFS</Label>
<Label Name="lblConnStatSimConnect" Height="40" VerticalContentAlignment="Center" VerticalAlignment="Center" TextBlock.Foreground="Red" Padding="8,0,16,0">SimConnect</Label>
<Label Name="lblConnStatSession" Height="40" VerticalContentAlignment="Center" VerticalAlignment="Center" TextBlock.Foreground="Red" Padding="8,0,16,0">Session</Label>
</StackPanel>
</GroupBox>
<GroupBox Grid.Row="1" Grid.Column="0" BorderBrush="DarkGray" BorderThickness="1" Margin="10,14,10,0">
<GroupBox.Header>Sim Values</GroupBox.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="32" Width="*"/>
<ColumnDefinition MinWidth="32" Width="*"/>
<ColumnDefinition MinWidth="32" Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition MinHeight="32" Height="*"/>
<RowDefinition MinHeight="32" Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<Label MinWidth="60" VerticalContentAlignment="Center">ØFPS:</Label>
<Label Name="lblSimFPS" MinWidth="64" VerticalContentAlignment="Center">n/a</Label>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Label MinWidth="60" VerticalContentAlignment="Center">TLOD:</Label>
<Label Name="lblSimTLOD" MinWidth="64" VerticalContentAlignment="Center">n/a</Label>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<Label MinWidth="60" VerticalContentAlignment="Center">OLOD:</Label>
<Label Name="lblSimOLOD" MinWidth="64" VerticalContentAlignment="Center">n/a</Label>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<Label MinWidth="60" VerticalContentAlignment="Center">AGL:</Label>
<Label Name="lblPlaneAGL" MinWidth="64" VerticalContentAlignment="Center">n/a</Label>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Label MinWidth="60" VerticalContentAlignment="Center">FPM:</Label>
<Label Name="lblPlaneVS" MinWidth="64" VerticalContentAlignment="Center">n/a</Label>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<Label MinWidth="60" VerticalContentAlignment="Center">Clouds:</Label>
<Label Name="lblSimCloudQs" MinWidth="64" VerticalContentAlignment="Center">n/a</Label>
</StackPanel>
</Grid>
</GroupBox>
</StackPanel>
<StackPanel Orientation="Vertical" Grid.Row="3" HorizontalAlignment="Center" Width="402">
<GroupBox BorderBrush="DarkGray" BorderThickness="1" Margin="10,8,10,8">
<GroupBox.Header>General</GroupBox.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="369*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="6*"/>
<RowDefinition Height="29*"/>
<RowDefinition Height="33.96"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="0,0,0,34" Grid.RowSpan="2"/>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" Margin="0,8,0,0" Grid.ColumnSpan="2">
<CheckBox x:Name="chkOpenWindow" VerticalContentAlignment="Center" Click="chkOpenWindow_Click" Content="Open Window on App Start" Height="15" Width="337"/>
</StackPanel>
<StackPanel Grid.Row="2" Grid.Column="0" Orientation="Horizontal" Margin="0,8,0,0">
<Label Name="lblIsVR" MinWidth="50" VerticalContentAlignment="Center" Content=""/>
<Label x:Name="lblsimCompatible" MinWidth="50" VerticalContentAlignment="Center" Content=""/>
<TextBlock x:Name="lblappUrl" VerticalAlignment="Center"> <Hyperlink NavigateUri="https://github.com/ResetXPDR/MSFS_AutoLOD/releases/latest" RequestNavigate="Hyperlink_RequestNavigate">
here</Hyperlink>
</TextBlock>
</StackPanel>
</Grid>
</GroupBox>
</StackPanel>
<StackPanel Orientation="Vertical" Grid.Column="0" Grid.Row="2">
<GroupBox BorderBrush="DarkGray" BorderThickness="1" Margin="10,8,10,8">
<GroupBox.Header>MSFS Settings</GroupBox.Header>
<Grid Margin="0,2,0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<Label Content="Target FPS" MinWidth="100"/>
<TextBox x:Name="txtTargetFPS" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Height="24" MaxHeight="24" MinHeight="24" Width="42" LostFocus="TextBox_LostFocus" KeyUp="TextBox_KeyUp" Margin="0,0,30,0"/>
<Label Content="FPS Tolerance" MinWidth="100"/>
<TextBox x:Name="txtFPSTolerance" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Height="24" MaxHeight="24" MinHeight="24" Width="42" LostFocus="TextBox_LostFocus" KeyUp="TextBox_KeyUp" Margin="5,0,0,0"/>
<Label Content="%"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<Label Content="TLOD Minimum" MinWidth="100"/>
<TextBox Name="txtMinTLod" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Height="24" MaxHeight="24" MinHeight="24" Width="42" LostFocus="TextBox_LostFocus" KeyUp="TextBox_KeyUp" Margin="0,0,30,0"/>
<Label Content="TLOD Maximum" MinWidth="100"/>
<TextBox x:Name="txtMaxTLod" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Height="24" MaxHeight="24" MinHeight="24" Width="42" LostFocus="TextBox_LostFocus" KeyUp="TextBox_KeyUp" Margin="5,0,0,0"/>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" Margin="0,2,0,0">
<Label Name="lblLodStepMax" VerticalContentAlignment="Center" Width ="96" Padding="5,5,0,5">TLOD Steps</Label>
<TextBox Name="txtLodStepMaxInc" Margin="4,0,0,0" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Height="24" MaxHeight="24" MinHeight="24" Width="42" LostFocus="TextBox_LostFocus" KeyUp="TextBox_KeyUp" TextChanged="txtLodStepMaxInc_TextChanged"></TextBox>
<Label VerticalContentAlignment="Center" Width ="133" Padding="5,5,0,5">Up</Label>
<TextBox x:Name="txtLodStepMaxDec" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Height="24" MaxHeight="24" MinHeight="24" Width="42" LostFocus="TextBox_LostFocus" KeyUp="TextBox_KeyUp" TextChanged="txtLodStepMaxDec_TextChanged" Margin="2,0,5,0"/>
<Label VerticalContentAlignment="Center" Padding="5,5,0,5">Down</Label>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,2,0,0">
<CheckBox x:Name="chkDecCloudQ" VerticalContentAlignment="Center" Click="chkDecCloudQ_Click" Width="152" Content="Decrease Cloud Quality" Checked="chkDecCloudQ_Checked" Padding="4,0,0,0"/>
<Label Name= "lblCloudRecoveryTLOD" >Cloud Recovery TLOD</Label>
<TextBox Name="txtCloudRecoveryTLOD" Margin="1,0,0,0" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Height="24" MaxHeight="24" MinHeight="24" Width="42" LostFocus="TextBox_LostFocus" KeyUp="TextBox_KeyUp"></TextBox>
</StackPanel>
</StackPanel>
</Grid>
</GroupBox>
</StackPanel>
</Grid>
</Viewbox>
</Window>

View File

@ -0,0 +1,642 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Navigation;
using System.Windows.Threading;
using static System.Net.WebRequestMethods;
namespace MSFS2020_AutoFPS
{
public partial class MainWindow : Window
{
protected NotifyIconViewModel notifyModel;
protected ServiceModel serviceModel;
protected DispatcherTimer timer;
protected int editPairTLOD = -1;
protected int editPairOLOD = -1;
public MainWindow(NotifyIconViewModel notifyModel, ServiceModel serviceModel)
{
InitializeComponent();
this.notifyModel = notifyModel;
this.serviceModel = serviceModel;
string assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
assemblyVersion = assemblyVersion[0..assemblyVersion.LastIndexOf('.')];
Title += " (" + assemblyVersion + (serviceModel.TestVersion ? "-concept_demo" : "")+ ")";
//FillIndices(dgTlodPairs);
//FillIndices(dgOlodPairs);
timer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
timer.Tick += OnTick;
string latestAppVersionStr = GetFinalRedirect("https://github.com/ResetXPDR/MSFS2020_AutoFPS/releases/latest");
lblappUrl.Visibility = Visibility.Hidden;
if (int.TryParse(assemblyVersion.Replace(".", ""), CultureInfo.InvariantCulture, out int currentAppVersion) && latestAppVersionStr != null && latestAppVersionStr.Length > 70)
{
latestAppVersionStr = latestAppVersionStr.Substring(latestAppVersionStr.Length - 5, 5);
if (int.TryParse(latestAppVersionStr.Replace(".", ""), CultureInfo.InvariantCulture, out int LatestAppVersion))
{
if ((serviceModel.TestVersion && LatestAppVersion >= currentAppVersion) || LatestAppVersion > currentAppVersion)
{
lblsimCompatible.Content = "Newer app version " + (latestAppVersionStr) + " now available";
lblsimCompatible.Foreground = new SolidColorBrush(Colors.Green);
lblappUrl.Visibility = Visibility.Visible;
}
else
{
if (serviceModel.TestVersion)
{
lblsimCompatible.Content = latestAppVersionStr + " version is latest formal release. Check link works";
lblsimCompatible.Foreground = new SolidColorBrush(Colors.Green);
lblappUrl.Visibility = Visibility.Visible;
}
else
{
lblsimCompatible.Content = "Latest app version is installed";
lblsimCompatible.Foreground = new SolidColorBrush(Colors.Green);
}
}
}
}
}
public static string GetFinalRedirect(string url)
{
if (string.IsNullOrWhiteSpace(url))
return url;
int maxRedirCount = 8; // prevent infinite loops
string newUrl = url;
do
{
HttpWebRequest req = null;
HttpWebResponse resp = null;
try
{
req = (HttpWebRequest)HttpWebRequest.Create(url);
req.Method = "HEAD";
req.AllowAutoRedirect = false;
resp = (HttpWebResponse)req.GetResponse();
switch (resp.StatusCode)
{
case HttpStatusCode.OK:
return newUrl;
case HttpStatusCode.Redirect:
case HttpStatusCode.MovedPermanently:
case HttpStatusCode.RedirectKeepVerb:
case HttpStatusCode.RedirectMethod:
newUrl = resp.Headers["Location"];
if (newUrl == null)
return url;
if (newUrl.IndexOf("://", System.StringComparison.Ordinal) == -1)
{
// Doesn't have a URL Schema, meaning it's a relative or absolute URL
Uri u = new Uri(new Uri(url), newUrl);
newUrl = u.ToString();
}
break;
default:
return newUrl;
}
url = newUrl;
}
catch (WebException)
{
// Return the last known good URL
return newUrl;
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MainWindow.xaml:GetFinalRedirect", $"Exception {ex}: {ex.Message}");
return null;
}
finally
{
if (resp != null)
resp.Close();
}
} while (maxRedirCount-- > 0);
return newUrl;
}
protected void LoadSettings()
{
chkOpenWindow.IsChecked = serviceModel.OpenWindow;
//chkUseTargetFPS.IsChecked = serviceModel.UseTargetFPS;
//cbProfile.SelectedIndex = serviceModel.SelectedProfile;
//dgTlodPairs.ItemsSource = serviceModel.PairsTLOD[serviceModel.SelectedProfile].ToDictionary(x => x.Item1, x => x.Item2);
//dgOlodPairs.ItemsSource = serviceModel.PairsOLOD[serviceModel.SelectedProfile].ToDictionary(x => x.Item1, x => x.Item2);
txtTargetFPS.Text = Convert.ToString(serviceModel.TargetFPS, CultureInfo.CurrentUICulture);
txtFPSTolerance.Text = Convert.ToString(serviceModel.FPSTolerance, CultureInfo.CurrentUICulture);
//txtDecreaseTlod.Text = Convert.ToString(serviceModel.DecreaseTLOD, CultureInfo.CurrentUICulture);
//txtDecreaseOlod.Text = Convert.ToString(serviceModel.DecreaseOLOD, CultureInfo.CurrentUICulture);
txtMinTLod.Text = Convert.ToString(serviceModel.MinTLOD, CultureInfo.CurrentUICulture);
txtMaxTLod.Text = Convert.ToString(serviceModel.MaxTLOD, CultureInfo.CurrentUICulture);
//txtConstraintTicks.Text = Convert.ToString(serviceModel.ConstraintTicks, CultureInfo.CurrentUICulture);
//txtConstraintDelayTicks.Text = Convert.ToString(serviceModel.ConstraintDelayTicks, CultureInfo.CurrentUICulture);
chkDecCloudQ.IsChecked = serviceModel.DecCloudQ;
txtCloudRecoveryTLOD.Text = Convert.ToString(serviceModel.CloudRecoveryTLOD, CultureInfo.CurrentUICulture);
txtLodStepMaxInc.Text = Convert.ToString(serviceModel.LodStepMaxInc, CultureInfo.CurrentUICulture);
txtLodStepMaxDec.Text = Convert.ToString(serviceModel.LodStepMaxDec, CultureInfo.CurrentUICulture);
}
protected static void FillIndices(DataGrid dataGrid)
{
DataGridTextColumn column0 = new()
{
Header = "#",
Width = 16
};
Binding bindingColumn0 = new()
{
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridRow), 1),
Converter = new RowToIndexConvertor()
};
column0.Binding = bindingColumn0;
dataGrid.Columns.Add(column0);
}
protected void UpdateStatus()
{
if (serviceModel.IsSimRunning)
lblConnStatMSFS.Foreground = new SolidColorBrush(Colors.DarkGreen);
else
lblConnStatMSFS.Foreground = new SolidColorBrush(Colors.Red);
if (IPCManager.SimConnect != null && IPCManager.SimConnect.IsReady)
lblConnStatSimConnect.Foreground = new SolidColorBrush(Colors.DarkGreen);
else
lblConnStatSimConnect.Foreground = new SolidColorBrush(Colors.Red);
if (serviceModel.IsSessionRunning)
lblConnStatSession.Foreground = new SolidColorBrush(Colors.DarkGreen);
else
lblConnStatSession.Foreground = new SolidColorBrush(Colors.Red);
}
protected string CloudQualityLabel(int CloudQuality)
{
if (CloudQuality == 0) return "Low";
else if (CloudQuality == 1) return "Medium";
else if (CloudQuality == 2) return "High";
else if (CloudQuality == 3) return "Ultra";
else return "n/a";
}
protected float GetAverageFPS()
{
if (serviceModel.MemoryAccess != null && serviceModel.MemoryAccess.IsFgModeActive())
return IPCManager.SimConnect.GetAverageFPS() * 2.0f;
else
return IPCManager.SimConnect.GetAverageFPS();
}
protected void UpdateLiveValues()
{
if (IPCManager.SimConnect != null && IPCManager.SimConnect.IsConnected)
lblSimFPS.Content = GetAverageFPS().ToString("F0");
else
lblSimFPS.Content = "n/a";
if (serviceModel.MemoryAccess != null)
{
lblappUrl.Visibility = Visibility.Hidden;
lblSimTLOD.Content = serviceModel.MemoryAccess.GetTLOD_PC().ToString("F0");
lblSimOLOD.Content = serviceModel.MemoryAccess.GetOLOD_PC().ToString("F0");
if (serviceModel.MemoryAccess.IsVrModeActive())
{
lblSimCloudQs.Content = CloudQualityLabel(serviceModel.MemoryAccess.GetCloudQ_VR());
lblIsVR.Content = "VR Mode active";
}
else
{
lblSimCloudQs.Content = CloudQualityLabel(serviceModel.MemoryAccess.GetCloudQ_PC());
lblIsVR.Content = "PC Mode" + (serviceModel.MemoryAccess.IsFgModeActive() ? " & FG" : "") + " active";
}
if (serviceModel.MemoryAccess.MemoryWritesAllowed())
{
lblsimCompatible.Visibility = Visibility.Hidden;
}
else
{
lblsimCompatible.Content = "Incompatible MSFS version - Sim Values read only";
lblsimCompatible.Foreground = new SolidColorBrush(Colors.Red);
}
}
else
{
lblSimTLOD.Content = "n/a";
lblSimOLOD.Content = "n/a";
lblSimCloudQs.Content = "n/a";
}
if (serviceModel.UseTargetFPS && serviceModel.IsSessionRunning)
{
if (GetAverageFPS() < serviceModel.TargetFPS)
lblSimFPS.Foreground = new SolidColorBrush(Colors.Red);
else
lblSimFPS.Foreground = new SolidColorBrush(Colors.DarkGreen);
}
else
{
lblSimFPS.Foreground = new SolidColorBrush(Colors.Black);
}
lblSimTLOD.Foreground = new SolidColorBrush(Colors.Black);
lblSimCloudQs.Foreground = new SolidColorBrush(Colors.Black);
if (serviceModel.MemoryAccess != null)
{
if (serviceModel.MemoryAccess.GetTLOD_PC() == serviceModel.MinTLOD) lblSimTLOD.Foreground = new SolidColorBrush(Colors.Red);
else if (serviceModel.MemoryAccess.GetTLOD_PC() == serviceModel.MaxTLOD) lblSimTLOD.Foreground = new SolidColorBrush(Colors.Green);
else if (serviceModel.tlod_step) lblSimTLOD.Foreground = new SolidColorBrush(Colors.Orange);
}
if (serviceModel.DecCloudQ && serviceModel.DecCloudQActive) lblSimCloudQs.Foreground = new SolidColorBrush(Colors.Red);
}
protected void UpdateAircraftValues()
{
if (IPCManager.SimConnect != null && IPCManager.SimConnect.IsConnected)
{
var simConnect = IPCManager.SimConnect;
lblPlaneAGL.Content = simConnect.ReadSimVar("PLANE ALT ABOVE GROUND", "feet").ToString("F0");
lblPlaneVS.Content = (simConnect.ReadSimVar("VERTICAL SPEED", "feet per second") * 60.0f).ToString("F0");
//if (serviceModel.OnGround)
// lblVSTrend.Content = "Ground";
//else if (serviceModel.VerticalTrend > 0)
// lblVSTrend.Content = "Climb";
//else if (serviceModel.VerticalTrend < 0)
// lblVSTrend.Content = "Descent";
//else
// lblVSTrend.Content = "Cruise";
}
else
{
lblPlaneAGL.Content = "n/a";
lblPlaneVS.Content = "n/a";
//lblVSTrend.Content = "n/a";
}
}
protected static void UpdateIndex(DataGrid grid, List<(float, float)> pairs, int index)
{
if (index >= 0 && index < pairs.Count)
grid.SelectedIndex = index;
}
protected void OnTick(object sender, EventArgs e)
{
UpdateStatus();
UpdateLiveValues();
UpdateAircraftValues();
//if (serviceModel.IsSessionRunning)
//{
// UpdateIndex(dgTlodPairs, serviceModel.PairsTLOD[serviceModel.SelectedProfile], serviceModel.CurrentPairTLOD);
// UpdateIndex(dgOlodPairs, serviceModel.PairsOLOD[serviceModel.SelectedProfile], serviceModel.CurrentPairOLOD);
//}
}
protected void Window_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (!IsVisible)
{
notifyModel.CanExecuteHideWindow = false;
notifyModel.CanExecuteShowWindow = true;
timer.Stop();
}
else
{
LoadSettings();
chkCloudRecoveryTLOD_WindowVisibility();
timer.Start();
}
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
e.Cancel = true;
Hide();
}
//private void chkUseTargetFPS_Click(object sender, RoutedEventArgs e)
//{
// serviceModel.SetSetting("useTargetFps", chkUseTargetFPS.IsChecked.ToString().ToLower());
// LoadSettings();
//}
private void chkOpenWindow_Click(object sender, RoutedEventArgs e)
{
serviceModel.SetSetting("openWindow", chkOpenWindow.IsChecked.ToString().ToLower());
LoadSettings();
}
private void chkDecCloudQ_Click(object sender, RoutedEventArgs e)
{
serviceModel.SetSetting("DecCloudQ", chkDecCloudQ.IsChecked.ToString().ToLower());
LoadSettings();
chkCloudRecoveryTLOD_WindowVisibility();
}
private void TextBox_LostFocus(object sender, RoutedEventArgs e)
{
TextBox_SetSetting(sender as TextBox);
}
private void TextBox_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter || e.Key != Key.Return)
return;
TextBox_SetSetting(sender as TextBox);
}
private void TextBox_SetSetting(TextBox sender)
{
if (sender == null || string.IsNullOrWhiteSpace(sender.Text))
return;
string key;
bool intValue = false;
bool notNegative = true;
bool zeroAllowed = false;
switch (sender.Name)
{
case "txtTargetFPS":
key = "targetFps";
intValue = true;
break;
case "txtFPSTolerance":
key = "FpsTolerance";
intValue = true;
break;
case "txtDecreaseTlod":
key = "decreaseTlod";
break;
case "txtDecreaseOlod":
key = "decreaseOlod";
break;
case "txtConstraintTicks":
key = "constraintTicks";
intValue = true;
break;
case "txtConstraintDelayTicks":
key = "constraintDelayTicks";
intValue = true;
zeroAllowed = true;
break;
case "txtCloudRecoveryTLOD":
key = "CloudRecoveryTLOD";
intValue = true;
zeroAllowed = true;
break;
case "txtMinTLod":
key = "minTLod";
break;
case "txtMaxTLod":
key = "maxTLod";
break;
case "txtLodStepMaxInc":
key = "LodStepMaxInc";
intValue = true;
break;
case "txtLodStepMaxDec":
key = "LodStepMaxDec";
intValue = true;
break;
default:
key = "";
break;
}
if (key == "")
return;
if (intValue && int.TryParse(sender.Text, CultureInfo.InvariantCulture, out int iValue) && (iValue != 0 || zeroAllowed))
{
if (notNegative)
iValue = Math.Abs(iValue);
serviceModel.SetSetting(key, Convert.ToString(iValue, CultureInfo.InvariantCulture));
}
if (!intValue && float.TryParse(sender.Text, new RealInvariantFormat(sender.Text), out float fValue))
{
if (notNegative)
fValue = Math.Abs(fValue);
serviceModel.SetSetting(key, Convert.ToString(fValue, CultureInfo.InvariantCulture));
}
LoadSettings();
}
private static void SetPairTextBox(DataGrid sender, TextBox alt, TextBox value, ref int index)
{
if (sender.SelectedIndex == -1 || sender.SelectedItem == null)
return;
var item = (KeyValuePair<float, float>)sender.SelectedItem;
alt.Text = Convert.ToString((int)item.Key, CultureInfo.CurrentUICulture);
value.Text = Convert.ToString(item.Value, CultureInfo.CurrentUICulture);
index = sender.SelectedIndex;
}
//private void dgTlodPairs_MouseDoubleClick(object sender, MouseButtonEventArgs e)
//{
// SetPairTextBox(dgTlodPairs, txtTlodAlt, txtTlodValue, ref editPairTLOD);
//}
//private void dgOlodPairs_MouseDoubleClick(object sender, MouseButtonEventArgs e)
//{
// SetPairTextBox(dgOlodPairs, txtOlodAlt, txtOlodValue, ref editPairOLOD);
//}
private void ChangeLodPair(ref int pairIndex, TextBox alt, TextBox value, List<(float, float)> pairs)
{
if (pairIndex == -1)
return;
if (pairIndex == 0 && alt.Text != "0")
alt.Text = "0";
if (int.TryParse(alt.Text, CultureInfo.InvariantCulture, out int agl) && float.TryParse(value.Text, new RealInvariantFormat(value.Text), out float lod)
&& pairIndex < pairs.Count && agl >= 0 && lod >= serviceModel.SimMinLOD)
{
var oldPair = pairs[pairIndex];
pairs[pairIndex] = (agl, lod);
if (pairs.Count(pair => pair.Item1 == agl) > 1)
pairs[pairIndex] = oldPair;
serviceModel.SavePairs();
}
LoadSettings();
alt.Text = "";
value.Text = "";
pairIndex = -1;
}
//private void btnTlodChange_Click(object sender, RoutedEventArgs e)
//{
// ChangeLodPair(ref editPairTLOD, txtTlodAlt, txtTlodValue, serviceModel.PairsTLOD[serviceModel.SelectedProfile]);
//}
//private void btnOlodChange_Click(object sender, RoutedEventArgs e)
//{
// ChangeLodPair(ref editPairOLOD, txtOlodAlt, txtOlodValue, serviceModel.PairsOLOD[serviceModel.SelectedProfile]);
//}
private void AddLodPair(ref int pairIndex, TextBox alt, TextBox value, List<(float, float)> pairs)
{
if (int.TryParse(alt.Text, CultureInfo.InvariantCulture, out int agl) && float.TryParse(value.Text, new RealInvariantFormat(value.Text), out float lod)
&& agl >= 0 && lod >= serviceModel.SimMinLOD
&& !pairs.Any(pair => pair.Item1 == agl))
{
pairs.Add((agl, lod));
ServiceModel.SortTupleList(pairs);
serviceModel.SavePairs();
}
LoadSettings();
alt.Text = "";
value.Text = "";
pairIndex = -1;
}
//private void btnTlodAdd_Click(object sender, RoutedEventArgs e)
//{
// AddLodPair(ref editPairTLOD, txtTlodAlt, txtTlodValue, serviceModel.PairsTLOD[serviceModel.SelectedProfile]);
//}
//private void btnOlodAdd_Click(object sender, RoutedEventArgs e)
//{
// AddLodPair(ref editPairOLOD, txtOlodAlt, txtOlodValue, serviceModel.PairsOLOD[serviceModel.SelectedProfile]);
//}
private void RemoveLoadPair(ref int pairIndex, TextBox alt, TextBox value, List<(float, float)> pairs)
{
if (pairIndex < 1 || pairIndex >= pairs.Count)
return;
pairs.RemoveAt(pairIndex);
ServiceModel.SortTupleList(pairs);
serviceModel.SavePairs();
LoadSettings();
alt.Text = "";
value.Text = "";
pairIndex = -1;
}
//private void btnTlodRemove_Click(object sender, RoutedEventArgs e)
//{
// RemoveLoadPair(ref editPairTLOD, txtTlodAlt, txtTlodValue, serviceModel.PairsTLOD[serviceModel.SelectedProfile]);
//}
//private void btnOlodRemove_Click(object sender, RoutedEventArgs e)
//{
// RemoveLoadPair(ref editPairOLOD, txtOlodAlt, txtOlodValue, serviceModel.PairsOLOD[serviceModel.SelectedProfile]);
//}
private void txtLodStepMaxInc_TextChanged(object sender, TextChangedEventArgs e)
{
}
private void txtLodStepMaxDec_TextChanged(object sender, TextChangedEventArgs e)
{
}
private void chkDecCloudQ_Checked(object sender, RoutedEventArgs e)
{
}
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
try
{
var myProcess = new Process();
myProcess.StartInfo.UseShellExecute = true;
myProcess.StartInfo.FileName = "https://github.com/ResetXPDR/MSFS2020_AutoFPS/releases/latest";
myProcess.Start();
e.Handled = true;
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MainWindow.xaml:Hyperlink_RequestNavigate", $"Exception {ex}: {ex.Message}");
}
}
private void chkCloudRecoveryTLOD_WindowVisibility()
{
if (serviceModel.DecCloudQ)
{
lblCloudRecoveryTLOD.Visibility = Visibility.Visible;
txtCloudRecoveryTLOD.Visibility = Visibility.Visible;
}
else
{
lblCloudRecoveryTLOD.Visibility = Visibility.Hidden;
txtCloudRecoveryTLOD.Visibility = Visibility.Hidden;
}
}
}
public class RowToIndexConvertor : MarkupExtension, IValueConverter
{
static RowToIndexConvertor convertor;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && value is DataGridRow row)
{
return row.GetIndex();
}
else
{
return -1;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
convertor ??= new RowToIndexConvertor();
return convertor;
}
public RowToIndexConvertor()
{
}
}
}

View File

@ -0,0 +1,128 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace MSFS2020_AutoFPS
{
public static class MemoryInterface
{
public const int PROCESS_VM_OPERATION = 0x0008;
public const int PROCESS_VM_READ = 0x0010;
public const int PROCESS_VM_WRITE = 0x0020;
private static Process proc;
private static IntPtr procHandle;
public static long GetModuleAddress(string Name)
{
try
{
if (proc != null)
{
foreach (ProcessModule ProcMod in proc.Modules)
{
if (Name == ProcMod.ModuleName)
return (long)ProcMod.BaseAddress;
}
}
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryInterface:GetModuleAddress", $"Exception {ex}: {ex.Message}");
}
return -1;
}
public static bool Attach(string name)
{
try
{
if (Process.GetProcessesByName(name).Length > 0)
{
proc = Process.GetProcessesByName(name)[0];
procHandle = NativeMethods.OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, false, proc.Id);
return true;
}
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryInterface:Attach", $"Exception {ex}: {ex.Message}");
}
return false;
}
public static void WriteMemory<T>(long Address, object Value)
{
try
{
var buffer = StructureToByteArray(Value);
NativeMethods.NtWriteVirtualMemory(checked((int)procHandle), Address, buffer, buffer.Length, out _);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryInterface:WriteMemory", $"Exception {ex}: {ex.Message}");
}
}
public static T ReadMemory<T>(long address) where T : struct
{
try
{
var ByteSize = Marshal.SizeOf(typeof(T));
var buffer = new byte[ByteSize];
NativeMethods.NtReadVirtualMemory(checked((int)procHandle), address, buffer, buffer.Length, out _);
return ByteArrayToStructure<T>(buffer);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryInterface:ReadMemory", $"Exception {ex}: {ex.Message}");
}
return default;
}
private static T ByteArrayToStructure<T>(byte[] bytes) where T : struct
{
var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
return (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
}
finally
{
handle.Free();
}
}
private static byte[] StructureToByteArray(object obj)
{
var length = Marshal.SizeOf(obj);
var array = new byte[length];
var pointer = Marshal.AllocHGlobal(length);
Marshal.StructureToPtr(obj, pointer, true);
Marshal.Copy(pointer, array, 0, length);
Marshal.FreeHGlobal(pointer);
return array;
}
}
internal static class NativeMethods
{
[DllImport("kernel32.dll")]
internal static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[DllImport("ntdll.dll")]
internal static extern IntPtr NtWriteVirtualMemory(int ProcessHandle, long BaseAddress, byte[] Buffer, int NumberOfBytesToWrite, out int NumberOfBytesWritten);
[DllImport("ntdll.dll")]
internal static extern bool NtReadVirtualMemory(int ProcessHandle, long BaseAddress, byte[] Buffer, int NumberOfBytesToRead, out int NumberOfBytesRead);
}
}

View File

@ -0,0 +1,387 @@
using System;
using System.Diagnostics.Eventing.Reader;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Shapes;
namespace MSFS2020_AutoFPS
{
public class MemoryManager
{
private ServiceModel Model;
private long addrTLOD;
private long addrOLOD;
private long addrTLOD_VR;
private long addrOLOD_VR;
private long addrCloudQ;
private long addrCloudQ_VR;
private long addrVrMode;
private long addrFgMode;
private long offsetPointerAnsioFilter = -0x18;
private long offsetWaterWaves = 0x3C;
private bool allowMemoryWrites = false;
private bool isDX12 = false;
public MemoryManager(ServiceModel model)
{
try
{
this.Model = model;
MemoryInterface.Attach(Model.SimBinary);
GetActiveDXVersion();
Logger.Log(LogLevel.Debug, "MemoryManager:MemoryManager", $"Trying offsetModuleBase: 0x{model.OffsetModuleBase.ToString("X8")}");
GetMSFSMemoryAddresses();
if (addrTLOD > 0) MemoryBoundaryTest();
if (!allowMemoryWrites)
{
Logger.Log(LogLevel.Debug, "MemoryManager:MemoryManager", $"Boundary tests failed - possible MSFS memory map change");
ModuleOffsetSearch();
}
else Logger.Log(LogLevel.Debug, "MemoryManager:MemoryManager", $"Boundary tests passed - memory writes enabled");
if (!allowMemoryWrites) Logger.Log(LogLevel.Debug, "MemoryManager:MemoryManager", $"Boundary test failed - memory writes disabled");
GetMSFSMemoryAddresses();
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:MemoryManager", $"Exception {ex}: {ex.Message}");
}
}
private void ModuleOffsetSearch()
{
long offsetBase = 0x00400000;
bool offsetFound = false;
long offset = 0;
long moduleBase = MemoryInterface.GetModuleAddress(Model.SimModule);
// 0x004AF3C8 was muumimorko version offsetBase
// 0x004B2368 was Fragtality version offsetBase
Logger.Log(LogLevel.Debug, "MemoryManager:ModuleOffsetSearch", $"OffsetModuleBase search started");
while (offset < 0x100000 && !offsetFound)
{
addrTLOD = MemoryInterface.ReadMemory<long>(moduleBase + offsetBase + offset) + Model.OffsetPointerMain;
if (addrTLOD > 0)
{
addrTLOD_VR = MemoryInterface.ReadMemory<long>(addrTLOD) + Model.OffsetPointerTlodVr;
addrTLOD = MemoryInterface.ReadMemory<long>(addrTLOD) + Model.OffsetPointerTlod;
addrOLOD_VR = addrTLOD_VR + Model.OffsetPointerOlod;
addrOLOD = addrTLOD + Model.OffsetPointerOlod;
addrCloudQ = addrTLOD + Model.OffsetPointerCloudQ;
addrCloudQ_VR = addrCloudQ + Model.OffsetPointerCloudQVr;
addrVrMode = addrTLOD - Model.OffsetPointerVrMode;
addrFgMode = addrTLOD - Model.OffsetPointerFgMode;
MemoryBoundaryTest();
}
if (allowMemoryWrites) offsetFound = true;
else offset++;
}
if (offsetFound)
{
Model.SetSetting("offsetModuleBase", "0x" + (offsetBase + offset).ToString("X8"));
Logger.Log(LogLevel.Debug, "MemoryManager:ModuleOffsetSearch", $"New offsetModuleBase found and saved: 0x{(offsetBase + offset).ToString("X8")}");
}
else Logger.Log(LogLevel.Debug, "MemoryManager:ModuleOffsetSearch", $"OffsetModuleBase not found after {offset} iterations");
}
private void MemoryBoundaryTest()
{
// Boundary check a few known setting memory addresses to see if any fail which likely indicates MSFS memory map has changed
if (GetTLOD_PC() < 10 || GetTLOD_PC() > 400 || GetTLOD_VR() < 10 || GetTLOD_VR() > 400
|| GetOLOD_PC() < 10 || GetOLOD_PC() > 400 || GetOLOD_VR() < 10 || GetOLOD_VR() > 400
|| GetCloudQ_PC() < 0 || GetCloudQ_PC() > 3 || GetCloudQ_VR() < 0 || GetCloudQ_VR() > 3
|| MemoryInterface.ReadMemory<int>(addrVrMode) < 0 || MemoryInterface.ReadMemory<int>(addrVrMode) > 1
|| MemoryInterface.ReadMemory<int>(addrTLOD + offsetPointerAnsioFilter) < 1 || MemoryInterface.ReadMemory<int>(addrTLOD + offsetPointerAnsioFilter) > 16
|| !(MemoryInterface.ReadMemory<int>(addrTLOD + offsetWaterWaves) == 128 || MemoryInterface.ReadMemory<int>(addrTLOD + offsetWaterWaves) == 256 || MemoryInterface.ReadMemory<int>(addrTLOD + offsetWaterWaves) == 512))
allowMemoryWrites = false;
else allowMemoryWrites = true;
}
private void GetMSFSMemoryAddresses()
{
long moduleBase = MemoryInterface.GetModuleAddress(Model.SimModule);
addrTLOD = MemoryInterface.ReadMemory<long>(moduleBase + Model.OffsetModuleBase) + Model.OffsetPointerMain;
if (addrTLOD > 0)
{
addrTLOD_VR = MemoryInterface.ReadMemory<long>(addrTLOD) + Model.OffsetPointerTlodVr;
addrTLOD = MemoryInterface.ReadMemory<long>(addrTLOD) + Model.OffsetPointerTlod;
addrOLOD_VR = addrTLOD_VR + Model.OffsetPointerOlod;
addrOLOD = addrTLOD + Model.OffsetPointerOlod;
addrCloudQ = addrTLOD + Model.OffsetPointerCloudQ;
addrCloudQ_VR = addrCloudQ + Model.OffsetPointerCloudQVr;
addrVrMode = addrTLOD - Model.OffsetPointerVrMode;
addrFgMode = addrTLOD - Model.OffsetPointerFgMode;
if (allowMemoryWrites)
{
Logger.Log(LogLevel.Debug, "MemoryManager:GetMSFSMemoryAddresses", $"Address TLOD: 0x{addrTLOD:X} / {addrTLOD}");
Logger.Log(LogLevel.Debug, "MemoryManager:GetMSFSMemoryAddresses", $"Address OLOD: 0x{addrOLOD:X} / {addrOLOD}");
Logger.Log(LogLevel.Debug, "MemoryManager:GetMSFSMemoryAddresses", $"Address CloudQ: 0x{addrCloudQ:X} / {addrCloudQ}");
Logger.Log(LogLevel.Debug, "MemoryManager:GetMSFSMemoryAddresses", $"Address TLOD VR: 0x{addrTLOD_VR:X} / {addrTLOD_VR}");
Logger.Log(LogLevel.Debug, "MemoryManager:GetMSFSMemoryAddresses", $"Address OLOD VR: 0x{addrOLOD_VR:X} / {addrOLOD_VR}");
Logger.Log(LogLevel.Debug, "MemoryManager:GetMSFSMemoryAddresses", $"Address CloudQ VR: 0x{addrCloudQ_VR:X} / {addrCloudQ_VR}");
Logger.Log(LogLevel.Debug, "MemoryManager:GetMSFSMemoryAddresses", $"Address VrMode: 0x{addrVrMode:X} / {addrVrMode}");
Logger.Log(LogLevel.Debug, "MemoryManager:GetMSFSMemoryAddresses", $"Address FgMode: 0x{addrFgMode:X} / {addrFgMode}");
}
}
}
private void GetActiveDXVersion()
{
string filecontents;
string MSFSOptionsFile = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\Microsoft Flight Simulator\UserCfg.opt";
if (File.Exists(MSFSOptionsFile))
{
StreamReader sr = new StreamReader(MSFSOptionsFile);
filecontents = sr.ReadToEnd();
if (filecontents.Contains("PreferD3D12 1")) isDX12 = true;
sr.Close();
Logger.Log(LogLevel.Debug, "MemoryManager:GetActiveDXVersion", $"Steam MSFS version detected - " + (isDX12 ? "DX12" : "DX11"));
}
else
{
MSFSOptionsFile = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Packages\Microsoft.FlightSimulator_8wekyb3d8bbwe\LocalCache\UserCfg.opt";
if (File.Exists(MSFSOptionsFile))
{
StreamReader sr = new StreamReader(MSFSOptionsFile);
filecontents = sr.ReadToEnd();
if (filecontents.Contains("PreferD3D12 1")) isDX12 = true;
sr.Close();
Logger.Log(LogLevel.Debug, "MemoryManager:GetActiveDXVersion", $"MS Store MSFS version detected - " + (isDX12 ? "DX12" : "DX11"));
}
}
}
public bool MemoryWritesAllowed()
{
return allowMemoryWrites;
}
public bool IsVrModeActive()
{
try
{
return MemoryInterface.ReadMemory<int>(addrVrMode) == 1;
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:IsVrModeActive", $"Exception {ex}: {ex.Message}");
}
return false;
}
[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
private bool IsActiveWindowMSFS()
{
const int nChars = 256;
string activeWindowTitle;
StringBuilder Buff = new StringBuilder(nChars);
IntPtr handle = GetForegroundWindow();
if (GetWindowText(handle, Buff, nChars) > 0)
{
activeWindowTitle = Buff.ToString();
if (activeWindowTitle.Length > 26 && activeWindowTitle.Substring(0, 26) == "Microsoft Flight Simulator")
return true;
}
return false;
}
public bool IsFgModeActive()
{
try
{
if (isDX12 && !Model.MemoryAccess.IsVrModeActive() && IsActiveWindowMSFS())
return MemoryInterface.ReadMemory<byte>(addrFgMode) == 1;
else return false;
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:IsFgModeActive", $"Exception {ex}: {ex.Message}");
}
return false;
}
public float GetTLOD_PC()
{
try
{
return (float)Math.Round(MemoryInterface.ReadMemory<float>(addrTLOD) * 100.0f);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:GetTLOD", $"Exception {ex}: {ex.Message}");
}
return 0.0f;
}
public float GetTLOD_VR()
{
try
{
return (float)Math.Round(MemoryInterface.ReadMemory<float>(addrTLOD_VR) * 100.0f);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:GetTLOD_VR", $"Exception {ex}: {ex.Message}");
}
return 0.0f;
}
public float GetOLOD_PC()
{
try
{
return (float)Math.Round(MemoryInterface.ReadMemory<float>(addrOLOD) * 100.0f);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:GetOLOD", $"Exception {ex}: {ex.Message}");
}
return 0.0f;
}
public float GetOLOD_VR()
{
try
{
return (float)Math.Round(MemoryInterface.ReadMemory<float>(addrOLOD_VR) * 100.0f);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:GetOLOD_VR", $"Exception {ex}: {ex.Message}");
}
return 0.0f;
}
public int GetCloudQ_PC()
{
try
{
return MemoryInterface.ReadMemory<int>(addrCloudQ);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:GetCloudQ", $"Exception {ex}: {ex.Message}");
}
return -1;
}
public int GetCloudQ_VR()
{
try
{
return MemoryInterface.ReadMemory<int>(addrCloudQ_VR);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:GetCloudQ VR", $"Exception {ex}: {ex.Message}");
}
return -1;
}
public void SetTLOD(float value)
{
if (allowMemoryWrites)
{
SetTLOD_PC(value);
SetTLOD_VR(value);
}
}
public void SetTLOD_PC(float value)
{
try
{
MemoryInterface.WriteMemory<float>(addrTLOD, value / 100.0f);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:SetTLOD", $"Exception {ex}: {ex.Message}");
}
}
public void SetTLOD_VR(float value)
{
try
{
MemoryInterface.WriteMemory<float>(addrTLOD_VR, value / 100.0f);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:SetTLOD VR", $"Exception {ex}: {ex.Message}");
}
}
public void SetOLOD(float value)
{
if (allowMemoryWrites)
{
SetOLOD_PC(value);
SetOLOD_VR(value);
}
}
public void SetOLOD_PC(float value)
{
try
{
MemoryInterface.WriteMemory<float>(addrOLOD, value / 100.0f);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:SetOLOD", $"Exception {ex}: {ex.Message}");
}
}
public void SetOLOD_VR(float value)
{
try
{
MemoryInterface.WriteMemory<float>(addrOLOD_VR, value / 100.0f);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:SetOLOD VR", $"Exception {ex}: {ex.Message}");
}
}
public void SetCloudQ(int value)
{
if (allowMemoryWrites)
{
try
{
MemoryInterface.WriteMemory<int>(addrCloudQ, value);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:SetCloudQ", $"Exception {ex}: {ex.Message}");
}
}
}
public void SetCloudQ_VR(int value)
{
if (allowMemoryWrites)
{
try
{
MemoryInterface.WriteMemory<int>(addrCloudQ_VR, value);
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MemoryManager:SetCloudQ VR", $"Exception {ex}: {ex.Message}");
}
}
}
}
}

View File

@ -0,0 +1,88 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace MSFS2020_AutoFPS
{
public enum MOBIFLIGHT_CLIENT_DATA_ID
{
MOBIFLIGHT_LVARS,
MOBIFLIGHT_CMD,
MOBIFLIGHT_RESPONSE
}
public enum PILOTSDECK_CLIENT_DATA_ID
{
MOBIFLIGHT_LVARS = 1989,
MOBIFLIGHT_CMD,
MOBIFLIGHT_RESPONSE
}
public enum SIMCONNECT_REQUEST_ID
{
Dummy = 0
}
public enum SIMCONNECT_DEFINE_ID
{
Dummy = 0
}
public enum SIMCONNECT_NOTIFICATION_GROUP_ID
{
SIMCONNECT_GROUP_PRIORITY_DEFAULT,
SIMCONNECT_GROUP_PRIORITY_HIGHEST
}
public enum SIM_EVENTS
{
SIM,
PAUSE,
FRAME
};
//public enum NOTFIY_GROUP
//{
// GROUP0
//};
public class SimVar
{
public UInt32 ID { get; set; }
public String Name { get; set; }
public float Data { get; set; }
public SimVar(uint iD, float data = 0.0f)
{
ID = iD;
Data = data;
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ClientDataValue
{
public float data;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ClientDataString
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = (int)MobiSimConnect.MOBIFLIGHT_MESSAGE_SIZE)]
public byte[] data;
public ClientDataString(string strData)
{
byte[] txtBytes = Encoding.ASCII.GetBytes(strData);
var ret = new byte[1024];
Array.Copy(txtBytes, ret, txtBytes.Length);
data = ret;
}
}
public struct ResponseString
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = (int)MobiSimConnect.MOBIFLIGHT_MESSAGE_SIZE)]
public String Data;
}
}

View File

@ -0,0 +1,471 @@
using Microsoft.FlightSimulator.SimConnect;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
namespace MSFS2020_AutoFPS
{
public class MobiSimConnect : IDisposable
{
public const string MOBIFLIGHT_CLIENT_DATA_NAME_COMMAND = "MobiFlight.Command";
public const string MOBIFLIGHT_CLIENT_DATA_NAME_RESPONSE = "MobiFlight.Response";
public const uint MOBIFLIGHT_MESSAGE_SIZE = 1024;
public const uint WM_PILOTSDECK_SIMCONNECT = 0x1989;
public const string CLIENT_NAME = "MSFS2020_AutoFPS";
public const string PILOTSDECK_CLIENT_DATA_NAME_SIMVAR = $"{CLIENT_NAME}.LVars";
public const string PILOTSDECK_CLIENT_DATA_NAME_COMMAND = $"{CLIENT_NAME}.Command";
public const string PILOTSDECK_CLIENT_DATA_NAME_RESPONSE = $"{CLIENT_NAME}.Response";
protected SimConnect simConnect = null;
protected IntPtr simConnectHandle = IntPtr.Zero;
protected Thread simConnectThread = null;
private static bool cancelThread = false;
protected bool isSimConnected = false;
protected bool isMobiConnected = false;
protected bool isReceiveRunning = false;
public bool IsConnected { get { return isSimConnected && isMobiConnected; } }
public bool IsReady { get { return IsConnected && isReceiveRunning; } }
public bool SimIsPaused { get; private set; }
public bool SimIsRunning { get; private set; }
private const int fpsLen = 60 * 5; // 5 second average for Autolod
private float[] fpsStatistic;
private int fpsIndex;
protected uint nextID = 1;
protected const int reorderTreshold = 150;
protected Dictionary<string, uint> addressToIndex = new();
protected Dictionary<uint, float> simVars = new();
public MobiSimConnect()
{
fpsIndex = -1;
fpsStatistic = new float[fpsLen];
}
public float GetAverageFPS()
{
if (fpsIndex == -1)
return 0;
else
{
return fpsStatistic.Sum() / fpsLen;
}
}
public bool Connect()
{
try
{
if (isSimConnected)
return true;
simConnect = new SimConnect(CLIENT_NAME, simConnectHandle, WM_PILOTSDECK_SIMCONNECT, null, 0);
simConnect.OnRecvOpen += new SimConnect.RecvOpenEventHandler(SimConnect_OnOpen);
simConnect.OnRecvQuit += new SimConnect.RecvQuitEventHandler(SimConnect_OnQuit);
simConnect.OnRecvException += new SimConnect.RecvExceptionEventHandler(SimConnect_OnException);
cancelThread = false;
simConnectThread = new(new ThreadStart(SimConnect_ReceiveThread))
{
IsBackground = true
};
simConnectHandle = new IntPtr(simConnectThread.ManagedThreadId);
simConnectThread.Start();
Logger.Log(LogLevel.Information, "MobiSimConnect:Connect", $"SimConnect Connection open");
return true;
}
catch (Exception ex)
{
simConnectThread = null;
simConnectHandle = IntPtr.Zero;
cancelThread = true;
simConnect = null;
Logger.Log(LogLevel.Error, "MobiSimConnect:Connect", $"Exception while opening SimConnect! (Exception: {ex.GetType()} {ex.Message})");
}
return false;
}
protected void SimConnect_OnOpen(SimConnect sender, SIMCONNECT_RECV_OPEN data)
{
try
{
isSimConnected = true;
simConnect.OnRecvClientData += new SimConnect.RecvClientDataEventHandler(SimConnect_OnClientData);
simConnect.OnRecvEvent += new SimConnect.RecvEventEventHandler(SimConnect_OnReceiveEvent);
simConnect.OnRecvEventFrame += new SimConnect.RecvEventFrameEventHandler(Simconnect_OnRecvEventFrame);
CreateDataAreaDefaultChannel();
CreateEventSubscription();
Logger.Log(LogLevel.Information, "MobiSimConnect:SimConnect_OnOpen", $"SimConnect OnOpen received");
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MobiSimConnect:SimConnect_OnOpen", $"Exception during SimConnect OnOpen! (Exception: {ex.GetType()} {ex.Message})");
}
}
protected void SimConnect_ReceiveThread()
{
ulong ticks = 0;
int delay = 100;
int repeat = 5000 / delay;
int errors = 0;
isReceiveRunning = true;
while (!cancelThread && simConnect != null && isReceiveRunning)
{
try
{
simConnect.ReceiveMessage();
if (isSimConnected && !isMobiConnected && ticks % (ulong)repeat == 0)
{
Logger.Log(LogLevel.Debug, "MobiSimConnect:SimConnect_ReceiveThread", $"Sending Ping to MobiFlight WASM Module");
SendMobiWasmCmd("MF.DummyCmd");
SendMobiWasmCmd("MF.Ping");
}
}
catch (Exception ex)
{
errors++;
if (errors > 6)
{
isReceiveRunning = false;
Logger.Log(LogLevel.Error, "MobiSimConnect:SimConnect_ReceiveThread", $"Maximum Errors reached, closing Receive Thread! (Exception: {ex.GetType()})");
return;
}
}
Thread.Sleep(delay);
ticks++;
}
isReceiveRunning = false;
return;
}
protected void CreateEventSubscription()
{
//simConnect.MapClientEventToSimEvent(SIM_EVENTS.SIM, "SIM");
//simConnect.AddClientEventToNotificationGroup(NOTFIY_GROUP.GROUP0, SIM_EVENTS.SIM, false);
//simConnect.MapClientEventToSimEvent(SIM_EVENTS.PAUSE, "PAUSE");
//simConnect.AddClientEventToNotificationGroup(NOTFIY_GROUP.GROUP0, SIM_EVENTS.PAUSE, false);
//simConnect.MapClientEventToSimEvent(SIM_EVENTS.FRAME, "FRAME");
//simConnect.AddClientEventToNotificationGroup(NOTFIY_GROUP.GROUP0, SIM_EVENTS.FRAME, false);
simConnect.SubscribeToSystemEvent(SIM_EVENTS.SIM, "sim");
simConnect.SubscribeToSystemEvent(SIM_EVENTS.PAUSE, "pause");
simConnect.SubscribeToSystemEvent(SIM_EVENTS.FRAME, "frame");
}
protected void CreateDataAreaDefaultChannel()
{
simConnect.MapClientDataNameToID(MOBIFLIGHT_CLIENT_DATA_NAME_COMMAND, MOBIFLIGHT_CLIENT_DATA_ID.MOBIFLIGHT_CMD);
simConnect.MapClientDataNameToID(MOBIFLIGHT_CLIENT_DATA_NAME_RESPONSE, MOBIFLIGHT_CLIENT_DATA_ID.MOBIFLIGHT_RESPONSE);
simConnect.AddToClientDataDefinition((SIMCONNECT_DEFINE_ID)0, 0, MOBIFLIGHT_MESSAGE_SIZE, 0, 0);
simConnect.RegisterStruct<SIMCONNECT_RECV_CLIENT_DATA, ResponseString>((SIMCONNECT_DEFINE_ID)0);
simConnect.RequestClientData(MOBIFLIGHT_CLIENT_DATA_ID.MOBIFLIGHT_RESPONSE,
(SIMCONNECT_REQUEST_ID)0,
(SIMCONNECT_DEFINE_ID)0,
SIMCONNECT_CLIENT_DATA_PERIOD.ON_SET,
SIMCONNECT_CLIENT_DATA_REQUEST_FLAG.CHANGED,
0,
0,
0);
}
protected void CreateDataAreaClientChannel()
{
simConnect.MapClientDataNameToID(PILOTSDECK_CLIENT_DATA_NAME_COMMAND, PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_CMD);
simConnect.MapClientDataNameToID(PILOTSDECK_CLIENT_DATA_NAME_RESPONSE, PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_RESPONSE);
simConnect.MapClientDataNameToID(PILOTSDECK_CLIENT_DATA_NAME_SIMVAR, PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_LVARS);
simConnect.AddToClientDataDefinition((SIMCONNECT_DEFINE_ID)0, 0, MOBIFLIGHT_MESSAGE_SIZE, 0, 0);
simConnect.RegisterStruct<SIMCONNECT_RECV_CLIENT_DATA, ResponseString>((SIMCONNECT_DEFINE_ID)0);
simConnect.RequestClientData(PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_RESPONSE,
(SIMCONNECT_REQUEST_ID)0,
(SIMCONNECT_DEFINE_ID)0,
SIMCONNECT_CLIENT_DATA_PERIOD.ON_SET,
SIMCONNECT_CLIENT_DATA_REQUEST_FLAG.CHANGED,
0,
0,
0);
}
protected void SimConnect_OnClientData(SimConnect sender, SIMCONNECT_RECV_CLIENT_DATA data)
{
try
{
if (data.dwRequestID == 0)
{
var request = (ResponseString)data.dwData[0];
if (request.Data == "MF.Pong")
{
if (!isMobiConnected)
{
Logger.Log(LogLevel.Information, "MobiSimConnect:SimConnect_OnClientData", $"MobiFlight WASM Ping acknowledged - opening Client Connection");
SendMobiWasmCmd($"MF.Clients.Add.{CLIENT_NAME}");
}
}
if (request.Data == $"MF.Clients.Add.{CLIENT_NAME}.Finished")
{
CreateDataAreaClientChannel();
isMobiConnected = true;
SendClientWasmCmd("MF.SimVars.Clear");
SendClientWasmCmd($"MF.Config.MAX_VARS_PER_FRAME.Set.{ServiceModel.MfLvarsPerFrame}");
Logger.Log(LogLevel.Information, "MobiSimConnect:SimConnect_OnClientData", $"MobiFlight WASM Client Connection opened");
}
}
else
{
var simData = (ClientDataValue)data.dwData[0];
if (simVars.ContainsKey(data.dwRequestID))
{
simVars[data.dwRequestID] = simData.data;
}
else
Logger.Log(LogLevel.Warning, "MobiSimConnect:SimConnect_OnClientData", $"The received ID '{data.dwRequestID}' is not subscribed! (Data: {data})");
}
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MobiSimConnect:SimConnect_OnClientData", $"Exception during SimConnect OnClientData! (Exception: {ex.GetType()}) (Data: {data})");
}
}
protected void SimConnect_OnQuit(SimConnect sender, SIMCONNECT_RECV data)
{
Disconnect();
}
public void Disconnect()
{
try
{
// Disabled functionality due to spurious error messages for unknown reason
// If anyone knows what the possible issue is here, let me know!
//if (isMobiConnected)
// SendClientWasmCmd("MF.SimVars.Clear");
cancelThread = true;
//if (simConnectThread != null)
//{
// simConnectThread.Interrupt();
// simConnectThread.Join(500);
// simConnectThread = null;
//}
if (simConnect != null)
{
simConnect.Dispose();
simConnect = null;
simConnectHandle = IntPtr.Zero;
}
isSimConnected = false;
isMobiConnected = false;
nextID = 1;
simVars.Clear();
addressToIndex.Clear();
Logger.Log(LogLevel.Information, "MobiSimConnect:Disconnect", $"SimConnect Connection closed");
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MobiSimConnect:Disconnect", $"Exception during disconnecting from SimConnect! (Exception: {ex.GetType()} {ex.Message})");
}
}
private void Simconnect_OnRecvEventFrame(SimConnect sender, SIMCONNECT_RECV_EVENT_FRAME recEvent)
{
if (SimIsRunning && !SimIsPaused && recEvent != null)
{
if (fpsIndex == -1)
{
fpsIndex = 1;
for (int i = 0; i < fpsStatistic.Length; i++)
{
fpsStatistic[i] = recEvent.fFrameRate;
}
}
else
{
fpsStatistic[fpsIndex] = recEvent.fFrameRate;
fpsIndex++;
if (fpsIndex >= fpsStatistic.Length)
fpsIndex = 0;
}
}
}
private void SimConnect_OnReceiveEvent(SimConnect sender, SIMCONNECT_RECV_EVENT recEvent)
{
if (recEvent != null)
{
if (recEvent.uEventID == (uint)SIM_EVENTS.PAUSE)
{
if (recEvent.dwData == 1)
SimIsPaused = true;
else
SimIsPaused = false;
}
else if (recEvent.uEventID == (uint)SIM_EVENTS.SIM)
{
if (recEvent.dwData == 1)
SimIsRunning = true;
else
SimIsRunning = false;
}
}
}
public void Dispose()
{
Disconnect();
GC.SuppressFinalize(this);
}
private void SendClientWasmCmd(string command)
{
SendWasmCmd(PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_CMD, (MOBIFLIGHT_CLIENT_DATA_ID)0, command);
}
private void SendClientWasmDummyCmd()
{
SendWasmCmd(PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_CMD, (MOBIFLIGHT_CLIENT_DATA_ID)0, "MF.DummyCmd");
}
private void SendMobiWasmCmd(string command)
{
SendWasmCmd(MOBIFLIGHT_CLIENT_DATA_ID.MOBIFLIGHT_CMD, (MOBIFLIGHT_CLIENT_DATA_ID)0, command);
}
private void SendWasmCmd(Enum cmdChannelId, Enum cmdId, string command)
{
simConnect.SetClientData(cmdChannelId, cmdId, SIMCONNECT_CLIENT_DATA_SET_FLAG.DEFAULT, 0, new ClientDataString(command));
}
protected void SimConnect_OnException(SimConnect sender, SIMCONNECT_RECV_EXCEPTION data)
{
if (data.dwException != 3 && data.dwException != 29)
Logger.Log(LogLevel.Error, "MobiSimConnect:SimConnect_OnException", $"Exception received: (Exception: {data.dwException})");
}
public void SubscribeLvar(string address)
{
SubscribeVariable($"(L:{address})");
}
public void SubscribeSimVar(string name, string unit)
{
SubscribeVariable($"(A:{name}, {unit})");
}
protected void SubscribeVariable(string address)
{
try
{
if (!addressToIndex.ContainsKey(address))
{
RegisterVariable(nextID, address);
simVars.Add(nextID, 0.0f);
addressToIndex.Add(address, nextID);
nextID++;
}
else
Logger.Log(LogLevel.Warning, "MobiSimConnect:SubscribeAddress", $"The Address '{address}' is already subscribed");
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MobiSimConnect:SubscribeAddress", $"Exception while subscribing SimVar '{address}'! (Exception: {ex.GetType()}) (Message: {ex.Message})");
}
}
protected void RegisterVariable(uint ID, string address)
{
simConnect.AddToClientDataDefinition(
(SIMCONNECT_DEFINE_ID)ID,
(ID - 1) * sizeof(float),
sizeof(float),
0,
0);
simConnect?.RegisterStruct<SIMCONNECT_RECV_CLIENT_DATA, ClientDataValue>((SIMCONNECT_DEFINE_ID)ID);
simConnect?.RequestClientData(
PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_LVARS,
(SIMCONNECT_REQUEST_ID)ID,
(SIMCONNECT_DEFINE_ID)ID,
SIMCONNECT_CLIENT_DATA_PERIOD.ON_SET,
SIMCONNECT_CLIENT_DATA_REQUEST_FLAG.CHANGED,
0,
0,
0
);
SendClientWasmCmd($"MF.SimVars.Add.{address}");
}
public void UnsubscribeAll()
{
try
{
SendClientWasmCmd("MF.SimVars.Clear");
nextID = 1;
simVars.Clear();
addressToIndex.Clear();
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MobiSimConnect:UnsubscribeAll", $"Exception while unsubscribing SimVars! (Exception: {ex.GetType()}) (Message: {ex.Message})");
}
}
public float ReadLvar(string address)
{
if (addressToIndex.TryGetValue($"(L:{address})", out uint index) && simVars.TryGetValue(index, out float value))
return value;
else
return 0;
}
public float ReadSimVar(string name, string unit)
{
string address = $"(A:{name}, {unit})";
if (addressToIndex.TryGetValue(address, out uint index) && simVars.TryGetValue(index, out float value))
return value;
else
return 0;
}
public void WriteLvar(string address, float value)
{
SendClientWasmCmd($"MF.SimVars.Set.{string.Format(new CultureInfo("en-US").NumberFormat, "{0:G}", value)} (>L:{address})");
SendClientWasmDummyCmd();
}
public void ExecuteCode(string code)
{
SendClientWasmCmd($"MF.SimVars.Set.{code}");
SendClientWasmDummyCmd();
}
public int AltAboveGround(bool onGround)
{
int alt = (int)ReadSimVar("PLANE ALT ABOVE GROUND", "feet");
if (alt == 0 && onGround)
alt = (int)ReadSimVar("PLANE ALT ABOVE GROUND MINUS CG", "feet");
return alt;
}
}
}

View File

@ -0,0 +1,39 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tb="clr-namespace:H.NotifyIcon;assembly=H.NotifyIcon.Wpf"
xmlns:local="clr-namespace:MSFS2020_AutoFPS">
<ContextMenu
x:Shared="false"
x:Key="SysTrayMenu"
>
<MenuItem
Header="Show Window"
Command="{Binding ShowWindowCommand}"
/>
<MenuItem
Header="Hide Window"
Command="{Binding HideWindowCommand}"
/>
<Separator />
<MenuItem
Header="Exit"
Command="{Binding ExitApplicationCommand}"
/>
</ContextMenu>
<tb:TaskbarIcon
x:Key="NotifyIcon"
ToolTipText="Left-click to show Window, Right-click for Menu"
LeftClickCommand="{Binding ShowWindowCommand}"
NoLeftClickDelay="True"
ContextMenu="{StaticResource SysTrayMenu}">
<tb:TaskbarIcon.DataContext>
<local:NotifyIconViewModel />
</tb:TaskbarIcon.DataContext>
</tb:TaskbarIcon>
</ResourceDictionary>

View File

@ -0,0 +1,40 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using H.NotifyIcon;
using System.Windows;
namespace MSFS2020_AutoFPS
{
public partial class NotifyIconViewModel : ObservableObject
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ShowWindowCommand))]
public bool canExecuteShowWindow = true;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(HideWindowCommand))]
public bool canExecuteHideWindow;
[RelayCommand(CanExecute = nameof(CanExecuteShowWindow))]
public void ShowWindow()
{
Application.Current.MainWindow.Show(disableEfficiencyMode: true);
CanExecuteShowWindow = false;
CanExecuteHideWindow = true;
}
[RelayCommand(CanExecute = nameof(CanExecuteHideWindow))]
public void HideWindow()
{
Application.Current.MainWindow.Hide(enableEfficiencyMode: false);
CanExecuteShowWindow = true;
CanExecuteHideWindow = false;
}
[RelayCommand]
public void ExitApplication()
{
Application.Current.Shutdown();
}
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Globalization;
namespace MSFS2020_AutoFPS
{
public class RealInvariantFormat : IFormatProvider
{
public NumberFormatInfo formatInfo = CultureInfo.InvariantCulture.NumberFormat;
public RealInvariantFormat(string value)
{
if (value == null)
{
formatInfo = new CultureInfo("en-US").NumberFormat;
return;
}
int lastPoint = value.LastIndexOf('.');
int lastComma = value.LastIndexOf(',');
if (lastComma > lastPoint)
{
formatInfo = new CultureInfo("de-DE").NumberFormat;
}
else
{
formatInfo = new CultureInfo("en-US").NumberFormat;
}
}
public object GetFormat(Type formatType)
{
if (formatType == typeof(NumberFormatInfo))
{
return formatInfo;
}
else
return null;
}
}
}

View File

@ -0,0 +1,137 @@
using System;
using System.Threading;
namespace MSFS2020_AutoFPS
{
public class ServiceController
{
protected ServiceModel Model;
protected int Interval = 1000;
public ServiceController(ServiceModel model)
{
this.Model = model;
}
public void Run()
{
try
{
Logger.Log(LogLevel.Information, "ServiceController:Run", $"Service starting ...");
while (!Model.CancellationRequested)
{
if (Wait())
{
ServiceLoop();
}
else
{
if (!IPCManager.IsSimRunning())
{
Model.CancellationRequested = true;
Model.ServiceExited = true;
Logger.Log(LogLevel.Critical, "ServiceController:Run", $"Session aborted, Retry not possible - exiting Program");
return;
}
else
{
Reset();
Logger.Log(LogLevel.Information, "ServiceController:Run", $"Session aborted, Retry possible - Waiting for new Session");
}
}
}
IPCManager.CloseSafe();
}
catch (Exception ex)
{
Logger.Log(LogLevel.Critical, "ServiceController:Run", $"Critical Exception occured: {ex.Source} - {ex.Message}");
}
}
protected bool Wait()
{
if (!IPCManager.WaitForSimulator(Model))
{
Model.IsSimRunning = false;
return false;
}
else
Model.IsSimRunning = true;
if (!IPCManager.WaitForConnection(Model))
return false;
if (!IPCManager.WaitForSessionReady(Model))
{
Model.IsSessionRunning = false;
return false;
}
else
Model.IsSessionRunning = true;
return true;
}
protected void Reset()
{
try
{
IPCManager.SimConnect?.Disconnect();
IPCManager.SimConnect = null;
Model.IsSessionRunning = false;
}
catch (Exception ex)
{
Logger.Log(LogLevel.Critical, "ServiceController:Reset", $"Exception during Reset: {ex.Source} - {ex.Message}");
}
}
protected void ServiceLoop()
{
Model.MemoryAccess = new MemoryManager(Model);
var lodController = new LODController(Model);
Logger.Log(LogLevel.Information, "ServiceController:ServiceLoop", "Starting Service Loop");
Model.DefaultTLOD = Model.MemoryAccess.GetTLOD_PC();
Model.DefaultTLOD_VR = Model.MemoryAccess.GetTLOD_VR();
//Model.DefaultOLOD =Model.MemoryAccess.GetOLOD_PC();
//Model.DefaultOLOD_VR = Model.MemoryAccess.GetOLOD_VR();
//Logger.Log(LogLevel.Information, "ServiceController:ServiceLoop", $"Initial LODs PC {Model.DefaultTLOD} / {Model.DefaultOLOD} and VR {Model.DefaultTLOD_VR} / {Model.DefaultOLOD_VR}");
Logger.Log(LogLevel.Information, "ServiceController:ServiceLoop", $"Initial TLODs " + $"PC / VR {Model.DefaultTLOD} / {Model.DefaultTLOD_VR}");
Model.DefaultCloudQ = Model.MemoryAccess.GetCloudQ_PC();
Model.DefaultCloudQ_VR = Model.MemoryAccess.GetCloudQ_VR();
Logger.Log(LogLevel.Information, "ServiceController:ServiceLoop", $"Initial cloud quality PC {Model.DefaultCloudQ} / VR {Model.DefaultCloudQ_VR}");
Model.DefaultSettingsRead = true;
while (!Model.CancellationRequested && IPCManager.IsSimRunning() && IPCManager.IsCamReady())
{
try
{
lodController.RunTick();
}
catch (Exception ex)
{
Logger.Log(LogLevel.Critical, "ServiceController:ServiceLoop", $"Critical Exception during ServiceLoop() {ex.GetType()} {ex.Message} {ex.Source}");
}
Thread.Sleep(Interval);
}
Logger.Log(LogLevel.Information, "ServiceController:ServiceLoop", "ServiceLoop ended");
if (true && IPCManager.IsSimRunning())
{
// Logger.Log(LogLevel.Information, "ServiceController:ServiceLoop", $"Sim still running, resetting LODs to {Model.DefaultTLOD} / {Model.DefaultOLOD} and VR {Model.DefaultTLOD_VR} / {Model.DefaultOLOD_VR}");
Logger.Log(LogLevel.Information, "ServiceController:ServiceLoop", $"Sim still running, resetting TLODs to PC / VR {Model.DefaultTLOD} / {Model.DefaultTLOD_VR}");
Model.MemoryAccess.SetTLOD_PC(Model.DefaultTLOD);
Model.MemoryAccess.SetTLOD_VR(Model.DefaultTLOD_VR);
//Model.MemoryAccess.SetOLOD_PC(Model.DefaultOLOD);
//Model.MemoryAccess.SetOLOD_VR(Model.DefaultOLOD_VR);
Logger.Log(LogLevel.Information, "ServiceController:ServiceLoop", $"Sim still running, resetting cloud quality to {Model.DefaultCloudQ} / VR {Model.DefaultCloudQ_VR}");
Model.MemoryAccess.SetCloudQ(Model.DefaultCloudQ);
Model.MemoryAccess.SetCloudQ_VR(Model.DefaultCloudQ_VR);
}
Model.IsSessionRunning = false;
Model.MemoryAccess = null;
}
}
}

View File

@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
namespace MSFS2020_AutoFPS
{
public class ServiceModel
{
public static readonly int maxProfile = 6;
private static readonly int BuildConfigVersion = 1;
public int ConfigVersion { get; set; }
public bool ServiceExited { get; set; } = false;
public bool CancellationRequested { get; set; } = false;
public bool IsSimRunning { get; set; } = false;
public bool IsSessionRunning { get; set; } = false;
public MemoryManager MemoryAccess { get; set; } = null;
public int VerticalTrend { get; set; }
public float AltLead { get; set; }
public bool OnGround { get; set; } = true;
public bool ForceEvaluation { get; set; } = false;
public int SelectedProfile { get; set; } = 0;
public List<List<(float, float)>> PairsTLOD { get; set; }
public int CurrentPairTLOD;
public List<List<(float, float)>> PairsOLOD { get; set; }
public int CurrentPairOLOD;
public bool fpsMode { get; set; }
public bool UseTargetFPS { get; set; }
public int TargetFPS { get; set; }
public int FPSTolerance { get; set; }
public int CloudRecoveryTLOD { get; set; }
public bool DecCloudQActive { get; set; }
public int ConstraintTicks { get; set; }
public int ConstraintDelayTicks { get; set; }
public float DecreaseTLOD { get; set; }
public float DecreaseOLOD { get; set; }
public float MinTLOD { get; set; }
public float MaxTLOD { get; set; }
public float SimMinLOD { get; set; }
public float DefaultTLOD { get; set; } = 100;
public float DefaultTLOD_VR { get; set; } = 100;
public float DefaultOLOD { get; set; } = 100;
public float DefaultOLOD_VR { get; set; } = 100;
public int DefaultCloudQ { get; set; } = 2;
public int DefaultCloudQ_VR { get; set; } = 2;
public bool DefaultSettingsRead { get; set; } = false;
public int LodStepMaxInc { get; set; }
public int LodStepMaxDec { get; set; }
public bool tlod_step { get; set; } = false;
public bool olod_step { get; set; } = false;
public string LogLevel { get; set; }
public static int MfLvarsPerFrame { get; set; }
public bool WaitForConnect { get; set; }
public bool OpenWindow { get; set; }
public bool DecCloudQ { get; set; }
public bool LodStepMax { get; set; }
public string SimBinary { get; set; }
public string SimModule { get; set; }
public long OffsetModuleBase { get; set; }
public long OffsetPointerMain { get; set; }
public long OffsetPointerTlod { get; set; }
public long OffsetPointerTlodVr { get; set; }
public long OffsetPointerOlod { get; set; }
public long OffsetPointerCloudQ { get; set; }
public long OffsetPointerCloudQVr { get; set; }
public long OffsetPointerVrMode { get; set; }
public long OffsetPointerFgMode { get; set; }
public bool TestVersion { get; set; } = false;
protected ConfigurationFile ConfigurationFile = new();
public ServiceModel()
{
CurrentPairTLOD = 0;
CurrentPairOLOD = 0;
LoadConfiguration();
}
protected void LoadConfiguration()
{
ConfigurationFile.LoadConfiguration();
TestVersion = true;
LogLevel = Convert.ToString(ConfigurationFile.GetSetting("logLevel", "Debug"));
MfLvarsPerFrame = Convert.ToInt32(ConfigurationFile.GetSetting("mfLvarPerFrame", "15"));
ConfigVersion = Convert.ToInt32(ConfigurationFile.GetSetting("ConfigVersion", "1"));
WaitForConnect = Convert.ToBoolean(ConfigurationFile.GetSetting("waitForConnect", "true"));
OpenWindow = Convert.ToBoolean(ConfigurationFile.GetSetting("openWindow", "true"));
DecCloudQ = Convert.ToBoolean(ConfigurationFile.GetSetting("DecCloudQ", "false"));
SimBinary = Convert.ToString(ConfigurationFile.GetSetting("simBinary", "FlightSimulator"));
SimModule = Convert.ToString(ConfigurationFile.GetSetting("simModule", "WwiseLibPCx64P.dll"));
UseTargetFPS = Convert.ToBoolean(ConfigurationFile.GetSetting("useTargetFps", "true"));
TargetFPS = Convert.ToInt32(ConfigurationFile.GetSetting("targetFps", "40"));
FPSTolerance = Convert.ToInt32(ConfigurationFile.GetSetting("FpsTolerance", "5"));
CloudRecoveryTLOD = Convert.ToInt32(ConfigurationFile.GetSetting("CloudRecoveryTLOD", "100"));
ConstraintTicks = Convert.ToInt32(ConfigurationFile.GetSetting("constraintTicks", "60"));
ConstraintDelayTicks = Convert.ToInt32(ConfigurationFile.GetSetting("constraintDelayTicks", "1"));
DecreaseTLOD = Convert.ToSingle(ConfigurationFile.GetSetting("decreaseTlod", "50"), new RealInvariantFormat(ConfigurationFile.GetSetting("decreaseTlod", "50")));
DecreaseOLOD = Convert.ToSingle(ConfigurationFile.GetSetting("decreaseOlod", "50"), new RealInvariantFormat(ConfigurationFile.GetSetting("decreaseOlod", "50")));
MinTLOD = Convert.ToSingle(ConfigurationFile.GetSetting("minTLod", "50"), new RealInvariantFormat(ConfigurationFile.GetSetting("minTLod", "50")));
MaxTLOD = Convert.ToSingle(ConfigurationFile.GetSetting("maxTLod", "200"), new RealInvariantFormat(ConfigurationFile.GetSetting("maxTLod", "200")));
LodStepMaxInc = Convert.ToInt32(ConfigurationFile.GetSetting("LodStepMaxInc", "5"));
LodStepMaxDec = Convert.ToInt32(ConfigurationFile.GetSetting("LodStepMaxDec", "5"));
OffsetModuleBase = Convert.ToInt64(ConfigurationFile.GetSetting("offsetModuleBase", "0x004B2368"), 16);
OffsetPointerMain = Convert.ToInt64(ConfigurationFile.GetSetting("offsetPointerMain", "0x3D0"), 16);
OffsetPointerTlod = Convert.ToInt64(ConfigurationFile.GetSetting("offsetPointerTlod", "0xC"), 16);
OffsetPointerTlodVr = Convert.ToInt64(ConfigurationFile.GetSetting("offsetPointerTlodVr", "0x114"), 16);
OffsetPointerOlod = Convert.ToInt64(ConfigurationFile.GetSetting("offsetPointerOlod", "0xC"), 16);
OffsetPointerCloudQ = Convert.ToInt64(ConfigurationFile.GetSetting("offsetPointerCloudQ", "0x44"), 16);
OffsetPointerCloudQVr = Convert.ToInt64(ConfigurationFile.GetSetting("offsetPointerCloudQVr", "0x108"), 16);
OffsetPointerVrMode = Convert.ToInt64(ConfigurationFile.GetSetting("offsetPointerVrMode", "0x1C"), 16);
OffsetPointerFgMode = Convert.ToInt64(ConfigurationFile.GetSetting("offsetPointerFgMode", "0x4A"), 16);
SimMinLOD = Convert.ToSingle(ConfigurationFile.GetSetting("simMinLod", "10"), new RealInvariantFormat(ConfigurationFile.GetSetting("simMinLod", "10")));
SelectedProfile = Convert.ToInt32(ConfigurationFile.GetSetting("selectedProfile", "0"));
PairsTLOD = new();
PairsOLOD = new();
for (int i = 0; i < maxProfile; i++)
{
PairsTLOD.Add(LoadPairs(ConfigurationFile.GetSetting($"tlodPairs{i}", "0:100|1500:150|5000:200")));
PairsOLOD.Add(LoadPairs(ConfigurationFile.GetSetting($"olodPairs{i}", "0:100|2500:50|7500:10")));
}
CurrentPairTLOD = 0;
CurrentPairOLOD = 0;
ForceEvaluation = true;
if (ConfigVersion < BuildConfigVersion)
{
//CHANGE SETTINGS IF NEEDED, Example:
SetSetting("ConfigVersion", Convert.ToString(BuildConfigVersion));
}
}
public static List<(float, float)> LoadPairs(string settings)
{
List<(float, float)> pairsList = new();
string[] strPairs = settings.Split('|');
int alt;
float lod;
foreach (string pair in strPairs)
{
string[] parts = pair.Split(':');
alt = Convert.ToInt32(parts[0]);
lod = Convert.ToSingle(parts[1], new RealInvariantFormat(parts[1]));
pairsList.Add((alt, lod));
}
SortTupleList(pairsList);
return pairsList;
}
public static void SortTupleList(List<(float, float)> pairsList)
{
pairsList.Sort((x, y) => x.Item1.CompareTo(y.Item1));
}
public static string CreateLodString(List<(float, float)> pairsList)
{
string result = "";
bool first = true;
foreach (var pair in pairsList)
{
if (first)
first = false;
else
result += "|";
result += $"{Convert.ToString((int)pair.Item1)}:{Convert.ToString(pair.Item2, CultureInfo.InvariantCulture)}";
}
return result;
}
public string GetSetting(string key, string defaultValue = "")
{
return ConfigurationFile[key] ?? defaultValue;
}
public void SetSetting(string key, string value, bool noLoad = false)
{
ConfigurationFile[key] = value;
if (!noLoad)
LoadConfiguration();
}
public void SavePairs()
{
for (int i = 0; i < maxProfile; i++)
{
ConfigurationFile[$"tlodPairs{i}"] = CreateLodString(PairsTLOD[i]);
ConfigurationFile[$"olodPairs{i}"] = CreateLodString(PairsOLOD[i]);
}
LoadConfiguration();
}
}
}

Binary file not shown.

BIN
MSFS2020_AutoFPS/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

459
MobiSimConnect.cs.orig Normal file
View File

@ -0,0 +1,459 @@
using Microsoft.FlightSimulator.SimConnect;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
namespace DynamicLOD_ResetEdition
{
public class MobiSimConnect : IDisposable
{
public const string MOBIFLIGHT_CLIENT_DATA_NAME_COMMAND = "MobiFlight.Command";
public const string MOBIFLIGHT_CLIENT_DATA_NAME_RESPONSE = "MobiFlight.Response";
public const uint MOBIFLIGHT_MESSAGE_SIZE = 1024;
public const uint WM_PILOTSDECK_SIMCONNECT = 0x1989;
public const string CLIENT_NAME = "DynamicLOD_ResetEdition";
public const string PILOTSDECK_CLIENT_DATA_NAME_SIMVAR = $"{CLIENT_NAME}.LVars";
public const string PILOTSDECK_CLIENT_DATA_NAME_COMMAND = $"{CLIENT_NAME}.Command";
public const string PILOTSDECK_CLIENT_DATA_NAME_RESPONSE = $"{CLIENT_NAME}.Response";
protected SimConnect simConnect = null;
protected IntPtr simConnectHandle = IntPtr.Zero;
protected Thread simConnectThread = null;
private static bool cancelThread = false;
protected bool isSimConnected = false;
protected bool isMobiConnected = false;
protected bool isReceiveRunning = false;
public bool IsConnected { get { return isSimConnected && isMobiConnected; } }
public bool IsReady { get { return IsConnected && isReceiveRunning; } }
public bool SimIsPaused { get; private set; }
public bool SimIsRunning { get; private set; }
private const int fpsLen = 60;
private float[] fpsStatistic;
private int fpsIndex;
protected uint nextID = 1;
protected const int reorderTreshold = 150;
protected Dictionary<string, uint> addressToIndex = new();
protected Dictionary<uint, float> simVars = new();
public MobiSimConnect()
{
fpsIndex = -1;
fpsStatistic = new float[fpsLen];
}
public float GetAverageFPS()
{
if (fpsIndex == -1)
return 0;
else
{
return fpsStatistic.Sum() / fpsLen;
}
}
public bool Connect()
{
try
{
if (isSimConnected)
return true;
simConnect = new SimConnect(CLIENT_NAME, simConnectHandle, WM_PILOTSDECK_SIMCONNECT, null, 0);
simConnect.OnRecvOpen += new SimConnect.RecvOpenEventHandler(SimConnect_OnOpen);
simConnect.OnRecvQuit += new SimConnect.RecvQuitEventHandler(SimConnect_OnQuit);
simConnect.OnRecvException += new SimConnect.RecvExceptionEventHandler(SimConnect_OnException);
cancelThread = false;
simConnectThread = new(new ThreadStart(SimConnect_ReceiveThread))
{
IsBackground = true
};
simConnectHandle = new IntPtr(simConnectThread.ManagedThreadId);
simConnectThread.Start();
Logger.Log(LogLevel.Information, "MobiSimConnect:Connect", $"SimConnect Connection open");
return true;
}
catch (Exception ex)
{
simConnectThread = null;
simConnectHandle = IntPtr.Zero;
cancelThread = true;
simConnect = null;
Logger.Log(LogLevel.Error, "MobiSimConnect:Connect", $"Exception while opening SimConnect! (Exception: {ex.GetType()} {ex.Message})");
}
return false;
}
protected void SimConnect_OnOpen(SimConnect sender, SIMCONNECT_RECV_OPEN data)
{
try
{
isSimConnected = true;
simConnect.OnRecvClientData += new SimConnect.RecvClientDataEventHandler(SimConnect_OnClientData);
simConnect.OnRecvEvent += new SimConnect.RecvEventEventHandler(SimConnect_OnReceiveEvent);
simConnect.OnRecvEventFrame += new SimConnect.RecvEventFrameEventHandler(Simconnect_OnRecvEventFrame);
CreateDataAreaDefaultChannel();
CreateEventSubscription();
Logger.Log(LogLevel.Information, "MobiSimConnect:SimConnect_OnOpen", $"SimConnect OnOpen received");
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MobiSimConnect:SimConnect_OnOpen", $"Exception during SimConnect OnOpen! (Exception: {ex.GetType()} {ex.Message})");
}
}
protected void SimConnect_ReceiveThread()
{
ulong ticks = 0;
int delay = 100;
int repeat = 5000 / delay;
int errors = 0;
isReceiveRunning = true;
while (!cancelThread && simConnect != null && isReceiveRunning)
{
try
{
simConnect.ReceiveMessage();
if (isSimConnected && !isMobiConnected && ticks % (ulong)repeat == 0)
{
Logger.Log(LogLevel.Debug, "MobiSimConnect:SimConnect_ReceiveThread", $"Sending Ping to MobiFlight WASM Module");
SendMobiWasmCmd("MF.DummyCmd");
SendMobiWasmCmd("MF.Ping");
}
}
catch (Exception ex)
{
errors++;
if (errors > 6)
{
isReceiveRunning = false;
Logger.Log(LogLevel.Error, "MobiSimConnect:SimConnect_ReceiveThread", $"Maximum Errors reached, closing Receive Thread! (Exception: {ex.GetType()})");
return;
}
}
Thread.Sleep(delay);
ticks++;
}
isReceiveRunning = false;
return;
}
protected void CreateEventSubscription()
{
//simConnect.MapClientEventToSimEvent(SIM_EVENTS.SIM, "SIM");
//simConnect.AddClientEventToNotificationGroup(NOTFIY_GROUP.GROUP0, SIM_EVENTS.SIM, false);
//simConnect.MapClientEventToSimEvent(SIM_EVENTS.PAUSE, "PAUSE");
//simConnect.AddClientEventToNotificationGroup(NOTFIY_GROUP.GROUP0, SIM_EVENTS.PAUSE, false);
//simConnect.MapClientEventToSimEvent(SIM_EVENTS.FRAME, "FRAME");
//simConnect.AddClientEventToNotificationGroup(NOTFIY_GROUP.GROUP0, SIM_EVENTS.FRAME, false);
simConnect.SubscribeToSystemEvent(SIM_EVENTS.SIM, "sim");
simConnect.SubscribeToSystemEvent(SIM_EVENTS.PAUSE, "pause");
simConnect.SubscribeToSystemEvent(SIM_EVENTS.FRAME, "frame");
}
protected void CreateDataAreaDefaultChannel()
{
simConnect.MapClientDataNameToID(MOBIFLIGHT_CLIENT_DATA_NAME_COMMAND, MOBIFLIGHT_CLIENT_DATA_ID.MOBIFLIGHT_CMD);
simConnect.MapClientDataNameToID(MOBIFLIGHT_CLIENT_DATA_NAME_RESPONSE, MOBIFLIGHT_CLIENT_DATA_ID.MOBIFLIGHT_RESPONSE);
simConnect.AddToClientDataDefinition((SIMCONNECT_DEFINE_ID)0, 0, MOBIFLIGHT_MESSAGE_SIZE, 0, 0);
simConnect.RegisterStruct<SIMCONNECT_RECV_CLIENT_DATA, ResponseString>((SIMCONNECT_DEFINE_ID)0);
simConnect.RequestClientData(MOBIFLIGHT_CLIENT_DATA_ID.MOBIFLIGHT_RESPONSE,
(SIMCONNECT_REQUEST_ID)0,
(SIMCONNECT_DEFINE_ID)0,
SIMCONNECT_CLIENT_DATA_PERIOD.ON_SET,
SIMCONNECT_CLIENT_DATA_REQUEST_FLAG.CHANGED,
0,
0,
0);
}
protected void CreateDataAreaClientChannel()
{
simConnect.MapClientDataNameToID(PILOTSDECK_CLIENT_DATA_NAME_COMMAND, PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_CMD);
simConnect.MapClientDataNameToID(PILOTSDECK_CLIENT_DATA_NAME_RESPONSE, PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_RESPONSE);
simConnect.MapClientDataNameToID(PILOTSDECK_CLIENT_DATA_NAME_SIMVAR, PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_LVARS);
simConnect.AddToClientDataDefinition((SIMCONNECT_DEFINE_ID)0, 0, MOBIFLIGHT_MESSAGE_SIZE, 0, 0);
simConnect.RegisterStruct<SIMCONNECT_RECV_CLIENT_DATA, ResponseString>((SIMCONNECT_DEFINE_ID)0);
simConnect.RequestClientData(PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_RESPONSE,
(SIMCONNECT_REQUEST_ID)0,
(SIMCONNECT_DEFINE_ID)0,
SIMCONNECT_CLIENT_DATA_PERIOD.ON_SET,
SIMCONNECT_CLIENT_DATA_REQUEST_FLAG.CHANGED,
0,
0,
0);
}
protected void SimConnect_OnClientData(SimConnect sender, SIMCONNECT_RECV_CLIENT_DATA data)
{
try
{
if (data.dwRequestID == 0)
{
var request = (ResponseString)data.dwData[0];
if (request.Data == "MF.Pong")
{
if (!isMobiConnected)
{
Logger.Log(LogLevel.Information, "MobiSimConnect:SimConnect_OnClientData", $"MobiFlight WASM Ping acknowledged - opening Client Connection");
SendMobiWasmCmd($"MF.Clients.Add.{CLIENT_NAME}");
}
}
if (request.Data == $"MF.Clients.Add.{CLIENT_NAME}.Finished")
{
CreateDataAreaClientChannel();
isMobiConnected = true;
SendClientWasmCmd("MF.SimVars.Clear");
SendClientWasmCmd($"MF.Config.MAX_VARS_PER_FRAME.Set.{ServiceModel.MfLvarsPerFrame}");
Logger.Log(LogLevel.Information, "MobiSimConnect:SimConnect_OnClientData", $"MobiFlight WASM Client Connection opened");
}
}
else
{
var simData = (ClientDataValue)data.dwData[0];
if (simVars.ContainsKey(data.dwRequestID))
{
simVars[data.dwRequestID] = simData.data;
}
else
Logger.Log(LogLevel.Warning, "MobiSimConnect:SimConnect_OnClientData", $"The received ID '{data.dwRequestID}' is not subscribed! (Data: {data})");
}
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MobiSimConnect:SimConnect_OnClientData", $"Exception during SimConnect OnClientData! (Exception: {ex.GetType()}) (Data: {data})");
}
}
protected void SimConnect_OnQuit(SimConnect sender, SIMCONNECT_RECV data)
{
Disconnect();
}
public void Disconnect()
{
try
{
if (isMobiConnected)
SendClientWasmCmd("MF.SimVars.Clear");
cancelThread = true;
if (simConnectThread != null)
{
simConnectThread.Interrupt();
simConnectThread.Join(500);
simConnectThread = null;
}
if (simConnect != null)
{
simConnect.Dispose();
simConnect = null;
simConnectHandle = IntPtr.Zero;
}
isSimConnected = false;
isMobiConnected = false;
nextID = 1;
simVars.Clear();
addressToIndex.Clear();
Logger.Log(LogLevel.Information, "MobiSimConnect:Disconnect", $"SimConnect Connection closed");
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MobiSimConnect:Disconnect", $"Exception during disconnecting from SimConnect! (Exception: {ex.GetType()} {ex.Message})");
}
}
private void Simconnect_OnRecvEventFrame(SimConnect sender, SIMCONNECT_RECV_EVENT_FRAME recEvent)
{
if (SimIsRunning && !SimIsPaused && recEvent != null)
{
if (fpsIndex == -1)
{
fpsIndex = 1;
for (int i = 0; i < fpsStatistic.Length; i++)
{
fpsStatistic[i] = recEvent.fFrameRate;
}
}
else
{
fpsStatistic[fpsIndex] = recEvent.fFrameRate;
fpsIndex++;
if (fpsIndex >= fpsStatistic.Length)
fpsIndex = 0;
}
}
}
private void SimConnect_OnReceiveEvent(SimConnect sender, SIMCONNECT_RECV_EVENT recEvent)
{
if (recEvent != null)
{
if (recEvent.uEventID == (uint)SIM_EVENTS.PAUSE)
{
if (recEvent.dwData == 1)
SimIsPaused = true;
else
SimIsPaused = false;
}
else if (recEvent.uEventID == (uint)SIM_EVENTS.SIM)
{
if (recEvent.dwData == 1)
SimIsRunning = true;
else
SimIsRunning = false;
}
}
}
public void Dispose()
{
Disconnect();
GC.SuppressFinalize(this);
}
private void SendClientWasmCmd(string command)
{
SendWasmCmd(PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_CMD, (MOBIFLIGHT_CLIENT_DATA_ID)0, command);
}
private void SendClientWasmDummyCmd()
{
SendWasmCmd(PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_CMD, (MOBIFLIGHT_CLIENT_DATA_ID)0, "MF.DummyCmd");
}
private void SendMobiWasmCmd(string command)
{
SendWasmCmd(MOBIFLIGHT_CLIENT_DATA_ID.MOBIFLIGHT_CMD, (MOBIFLIGHT_CLIENT_DATA_ID)0, command);
}
private void SendWasmCmd(Enum cmdChannelId, Enum cmdId, string command)
{
simConnect.SetClientData(cmdChannelId, cmdId, SIMCONNECT_CLIENT_DATA_SET_FLAG.DEFAULT, 0, new ClientDataString(command));
}
protected void SimConnect_OnException(SimConnect sender, SIMCONNECT_RECV_EXCEPTION data)
{
if (data.dwException != 3 && data.dwException != 29)
Logger.Log(LogLevel.Error, "MobiSimConnect:SimConnect_OnException", $"Exception received: (Exception: {data.dwException})");
}
public void SubscribeLvar(string address)
{
SubscribeVariable($"(L:{address})");
}
public void SubscribeSimVar(string name, string unit)
{
SubscribeVariable($"(A:{name}, {unit})");
}
protected void SubscribeVariable(string address)
{
try
{
if (!addressToIndex.ContainsKey(address))
{
RegisterVariable(nextID, address);
simVars.Add(nextID, 0.0f);
addressToIndex.Add(address, nextID);
nextID++;
}
else
Logger.Log(LogLevel.Warning, "MobiSimConnect:SubscribeAddress", $"The Address '{address}' is already subscribed");
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MobiSimConnect:SubscribeAddress", $"Exception while subscribing SimVar '{address}'! (Exception: {ex.GetType()}) (Message: {ex.Message})");
}
}
protected void RegisterVariable(uint ID, string address)
{
simConnect.AddToClientDataDefinition(
(SIMCONNECT_DEFINE_ID)ID,
(ID - 1) * sizeof(float),
sizeof(float),
0,
0);
simConnect?.RegisterStruct<SIMCONNECT_RECV_CLIENT_DATA, ClientDataValue>((SIMCONNECT_DEFINE_ID)ID);
simConnect?.RequestClientData(
PILOTSDECK_CLIENT_DATA_ID.MOBIFLIGHT_LVARS,
(SIMCONNECT_REQUEST_ID)ID,
(SIMCONNECT_DEFINE_ID)ID,
SIMCONNECT_CLIENT_DATA_PERIOD.ON_SET,
SIMCONNECT_CLIENT_DATA_REQUEST_FLAG.CHANGED,
0,
0,
0
);
SendClientWasmCmd($"MF.SimVars.Add.{address}");
}
public void UnsubscribeAll()
{
try
{
SendClientWasmCmd("MF.SimVars.Clear");
nextID = 1;
simVars.Clear();
addressToIndex.Clear();
}
catch (Exception ex)
{
Logger.Log(LogLevel.Error, "MobiSimConnect:UnsubscribeAll", $"Exception while unsubscribing SimVars! (Exception: {ex.GetType()}) (Message: {ex.Message})");
}
}
public float ReadLvar(string address)
{
if (addressToIndex.TryGetValue($"(L:{address})", out uint index) && simVars.TryGetValue(index, out float value))
return value;
else
return 0;
}
public float ReadSimVar(string name, string unit)
{
string address = $"(A:{name}, {unit})";
if (addressToIndex.TryGetValue(address, out uint index) && simVars.TryGetValue(index, out float value))
return value;
else
return 0;
}
public void WriteLvar(string address, float value)
{
SendClientWasmCmd($"MF.SimVars.Set.{string.Format(new CultureInfo("en-US").NumberFormat, "{0:G}", value)} (>L:{address})");
SendClientWasmDummyCmd();
}
public void ExecuteCode(string code)
{
SendClientWasmCmd($"MF.SimVars.Set.{code}");
SendClientWasmDummyCmd();
}
}
}

View File

@ -1 +1,97 @@
# MSFS2020_AutoFPS
Based on muumimorko's idea and code in MSFS_AdaptiveLOD, as further developed by Fragtality in DynamicLOD and myself in DynamicLOD_ResetEdition.<br/><br/>
This utility is new development that is a simplification DynamicLOD_ResetEdition. It aims to improve MSFS performance and smoothness by automatically changing the TLOD and OLOD based on the current AGL and an easy to use GUI. It provides features such as:<br/>
- Adjusting TLOD automatically to achieve a user-defined target FPS band based on user-defined maximum and minimum LODs,<br/>
- Simultaneous PC and VR mode compatibilty,<br>
- Cloud quality decrease option for when FPS can't be achieved at the lowest desired TLOD,<br/>
- Correct display of FPS with Frame Generation active,<br/>
- Auto future MSFS version compatibility, provided MSFS memory changes are like in previous updates,<br/>
- Update prompt if newer utility version found on startup,<br/>
- Auto restoration of original settings changed by the utility, and</br>
- Greatly simplified GUI.<br/><br/>
Important:<br/>
- This utility directly accesses active MSFS memory locations while MSFS is running to read and set OLOD, TLOD and cloud quality settings on the fly. From 0.3.7 version onwards, the utility will first verify that the MSFS memory locations being used are still valid and if not, likely because of an MSFS version change, will attempt to find where they have been relocated. If it does find the new memory locations and they pass validation tests, the utility will update itself automatically and will function as normal. If it can't find or validate MSFS memory locations at any time when starting up, the utility will self-restrict to read only mode to prevent the utility making changes to unknown MSFS memory locations.<br/>
- As such, I believe the app to be robust in its interaction with validated MSFS memory locations and to be responsible in disabling itself if it can't guarantee that. Nonetheless, this utility is offered as is and no responsibility will be taken for unintended negative side effects. Use at your own risk!<br/><br/>
If you are not familiar with what MSFS graphics settings do, specifically TLOD, OLOD and cloud quality, and don't understand the consequences of changing them, it is highly recommended you do not use this utility.
<br/><br/>
This utility is unsigned because I am a hobbyist and the cost of obtaining certification is prohibitive to me. As a result, you may get a warning message of a potentially dangerous app when you download it in a web browser like Chrome. You can either trust this download, based on feedback you can easily find on Avsim and Youtube, and run a virus scan and malware scan before you install just be sure, otherwise choose not to and not have this utility version.<br/><br/>
## Requirements
The Installer will install the following Software:
- .NET 7 Desktop Runtime (x64)
- MobiFlight Event/WASM Module
<br/>
Currently in development, but when available [Download here](https://github.com/ResetXPDR/AutoLOD_ResetEdition/releases/latest)
(Under Assests, the AutoLOD_ResetEdition-Installer-vXYZ.exe File)
<br/><br/>
## Installation / Update / Uninstall
Basically: Just run the Installer.<br/>
Some Notes:
- AutoLOD_ResetEdition has to be stopped before installing.
- If the MobiFlight Module is not installed or outdated, MSFS also has to be stopped.
- If you have duplicate MobiFlight Modules installed, in either your official or community folders, the utility may display 0 value Sim Values and otherwise not function. Remove the duplicate versions, rerun the utility installer and it should now work.
- Do not run the Installer as Admin!
- If you wish to retain your settings for an update version, do NOT uninstall first, as that deletes all app files, including the config file. Just run the installer, select update and your settings will be retained.
- For Auto-Start either your FSUIPC7.ini or EXE.xml (MSFS) is modified. The Installer does not create a Backup.
- The utility may be blocked by Windows Security or your AV-Scanner, try if unblocking and/or setting an Exception helps (for the whole Folder)
- The Installation-Location is fixed to %appdata%\AutoLOD_ResetEdition (your Users AppData\Roaming Folder) and can't be changed.
- Binary in %appdata%\AutoLOD_ResetEdition\bin
- Logs in %appdata%\AutoLOD_ResetEdition\log
- Config: %appdata%\AutoLOD_ResetEdition\AutoLOD_ResetEdition.config
<br/><br/>
## Usage / Configuration
This section is currently TBD
- Starting manually: anytime, but preferably before MSFS or in the Main Menu. The utility will stop itself when MSFS closes.
- Closing the Window does not close the utiltiy, use the Context Menu of the SysTray Icon.
- Clicking on the SysTray Icon opens the Window (again).
- Runnning as Admin NOT required (BUT: It is required to be run under the same User/Elevation as MSFS).
- Connection Status
- Red values indicate not connected, green is connected.
- Sim Values
- Will not show valid values unless all three connections are green.
- Red values mean FPS Adaption is active, orange means LOD stepping is active, black means steady state, n/a means not available right now.
- General
- You can have (exactly) six different Sets/Profiles for the AGL/LOD Pairs to switch between (manually but dynamically).
- Cruise LOD Updates, when checked, will continue to update LOD values based on AGL in the cruise phase, which is useful for VFR flights over undulating terrain and has an otherwise negligble impact on high level or IFR flights so it is recommended to enable this.
- LOD Step Max, when checked, allows the utility to slow the rate of change in LOD per second, with increase and decrease being individually settable, to smooth out LOD table changes. This allows you to have large steps in your LOD tables without experiencing abrupt changes like having it disabled would do, hence it is recommended to turn it on and start out with the default steps of 5.
- App status area in the bottom right will display messages depending on connection status about new utility updates, compatibility test failures, PC or VR mode and whether Frame Generation is currently active (MSFS must have the focus for this to display FG FPS correctly).
- LOD Level Tables
- The first Pair with AGL 0 can not be deleted. The AGL can not be changed. Only the xLOD.
- Additional Pairs can be added at any AGL and xLOD desired. Pairs will always be sorted by AGL.
- Plus is Add, Minus is Remove, S is Set (Change). Remove and Set require to double-click the Pair first.
- A Pair is selected (and the configured xLOD applied) when the current AGL is above the configured AGL. If the current AGL goes below the configured AGL, the next lower Pair will be selected.
- A new Pair is only selected in Accordance to the VS Trend - i.e. a lower Pair won't be selected if you're actually Climbing (only the next higher)
- Many users are finding it better to reduce, not increase, OLOD values at higher altitudes as you can't clearly see objects from such distances anyway, especially in VR.
- FPS Adaption:
- Settings in the FPS adaption area only work if you have checked Limit LODs.
- FPS Adaption will activate when your FPS is below the target FPS you have set, after any Delay start you have set.
- Reduce TLOD/OLOD is the maximum values it will reduce those settings by from the current LOD pair values, minimum TLOD/OLOD permitting. If you want to use the Decrease Cloud Quality option without reducing LODs, set these both to 0.
- Minimum TLOD/OLOD is the minimum values it will allow those settings to reduce to.
- Delay start is how many seconds of FPS below the target FPS have to occur before FPS Adaption will activate, to stop it false triggering with a transient FPS drop. Default is 1 second but 2 seconds is good too.
- Reduce for is how many seconds of FPS above the target FPS, plus cloud recover FPS if used, have to occur before FPS Adaption will cancel, to stop it false cancelling with a unsustained FPS increases.
- Decrease Cloud Quality, when checked, will reduce cloud quality by one level while FPS adaption is active.
- Cloud Recovery FPS + is how many FPS to add to the target FPS for determining whether to cancel FPS adaption once activated. This provides an FPS buffer to account for the increased FPS achieved by reducing cloud quality to stop FPS adaption constantly toggling on and off.
- **Less is more**:
- Fewer Increments/Decrements are better of reasonable Step-Size (roughly in the Range of 25-75) or use Step LOD Max to spread LOD changes out over time.
- Don't overdo it with extreme low or high xLOD Values. A xLOD of 100 is reasonable fine on Ground, 200-ish is reasonable fine in the air. 400 if you have a super computer.
- Tune your AGL/LOD Pairs to the desired Performance (which is more than just FPS).
- FPS Adaption is just *one temporary* Adjustment on the current AGL/xLOD Pair to fight some special/rare Situations.
- Forcing the Sim to (un)load Objects in rapid Succession defeats the Goal to reduce Stutters. It is *not* about FPS.
- Smooth Transitions lead to smoother experiences.
<br/><br/>