commit 67d8c3fab7a4a5accd0a3e965838b9a47b79c908 Author: Chris Date: Mon Apr 6 17:46:20 2020 -0600 initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..927f725 --- /dev/null +++ b/.gitignore @@ -0,0 +1,277 @@ +investigation/ + +## 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 + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates +*.vcxproj.filters + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.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 + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# 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 +# TODO: 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 +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable 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 + +# 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 +node_modules/ +orleans.codegen.cs + +# 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 + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# 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/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/ diff --git a/EdLauncher.sln b/EdLauncher.sln new file mode 100644 index 0000000..d649604 --- /dev/null +++ b/EdLauncher.sln @@ -0,0 +1,52 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D211D925-EFF3-46D3-AC52-28CE80DEFA89}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{141F970C-8B2A-4D21-92B7-98E264DAFEB4}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "EdLauncher", "src\EdLauncher.fsproj", "{55188BBB-C5EE-403E-A006-12DEDF4B4D2F}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "EdLauncher.Tests", "tests\EdLauncher.Tests.fsproj", "{BEA4B51C-5BA7-408D-A822-C5B61D103166}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{DCB76620-F400-4748-84ED-AA4DEA205281}" +ProjectSection(SolutionItems) = preProject + README.md = README.md + .gitignore = .gitignore +EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {55188BBB-C5EE-403E-A006-12DEDF4B4D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55188BBB-C5EE-403E-A006-12DEDF4B4D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEA4B51C-5BA7-408D-A822-C5B61D103166}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BEA4B51C-5BA7-408D-A822-C5B61D103166}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEA4B51C-5BA7-408D-A822-C5B61D103166}.Debug|x64.ActiveCfg = Debug|Any CPU + {BEA4B51C-5BA7-408D-A822-C5B61D103166}.Debug|x64.Build.0 = Debug|Any CPU + {BEA4B51C-5BA7-408D-A822-C5B61D103166}.Debug|x86.ActiveCfg = Debug|Any CPU + {BEA4B51C-5BA7-408D-A822-C5B61D103166}.Debug|x86.Build.0 = Debug|Any CPU + {BEA4B51C-5BA7-408D-A822-C5B61D103166}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BEA4B51C-5BA7-408D-A822-C5B61D103166}.Release|Any CPU.Build.0 = Release|Any CPU + {BEA4B51C-5BA7-408D-A822-C5B61D103166}.Release|x64.ActiveCfg = Release|Any CPU + {BEA4B51C-5BA7-408D-A822-C5B61D103166}.Release|x64.Build.0 = Release|Any CPU + {BEA4B51C-5BA7-408D-A822-C5B61D103166}.Release|x86.ActiveCfg = Release|Any CPU + {BEA4B51C-5BA7-408D-A822-C5B61D103166}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {55188BBB-C5EE-403E-A006-12DEDF4B4D2F} = {D211D925-EFF3-46D3-AC52-28CE80DEFA89} + {BEA4B51C-5BA7-408D-A822-C5B61D103166} = {141F970C-8B2A-4D21-92B7-98E264DAFEB4} + EndGlobalSection +EndGlobal diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..632bdb2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Chris + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ + diff --git a/src/EdLauncher.fsproj b/src/EdLauncher.fsproj new file mode 100644 index 0000000..353d7a8 --- /dev/null +++ b/src/EdLauncher.fsproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + diff --git a/src/Program.fs b/src/Program.fs new file mode 100644 index 0000000..ebc58c2 --- /dev/null +++ b/src/Program.fs @@ -0,0 +1,61 @@ +open System +open System.IO +open System.Runtime.InteropServices +open Steam +open Types +open Settings + +let log = + let write level msg = printfn "[%s] - %s" level msg + { Debug = fun msg -> write "DBG" msg + Info = fun msg -> write "INF" msg + Warn = fun msg -> write "WRN" msg + Error = fun msg -> write "ERR" msg } + +let getOsIdent() = + let platToStr plat = + if plat = OSPlatform.Linux then "Linux" + elif plat = OSPlatform.Windows then "Win" + elif plat = OSPlatform.OSX then "Mac" + elif plat = OSPlatform.FreeBSD then "FreeBSD" + else "Unknown" + let platform = + [ OSPlatform.Linux; OSPlatform.Windows; OSPlatform.OSX; OSPlatform.FreeBSD ] + |> List.pick (fun p -> if RuntimeInformation.IsOSPlatform(p) then Some p else None) + |> platToStr + let arch = + match RuntimeInformation.ProcessArchitecture with + | Architecture.Arm -> "Arm" + | Architecture.Arm64 -> "Arm64" + | Architecture.X64 -> "64" + | Architecture.X86 -> "32" + | unknownArch -> unknownArch.ToString() + + platform + arch + +let getUserDetails = function + | Dev -> Ok { UserId = 12345UL; SessionToken = "DevToken" } + | Oculus _ -> Error "Oculus not supported" + | Frontier -> Error "Frontier not supported" + | Steam _ -> + use steam = new Steam(log) + steam.Login() + +let printInfo platform user = + printfn "Elite: Dangerous Launcher" + printfn "Platform: %A" platform + printfn "OS: %s" (getOsIdent()) + match user with + | Error msg -> printfn "User: Error - %s" msg + | Ok user -> + printfn "User: %u" user.UserId + printfn "Session Token: %s" user.SessionToken + +[] +let main argv = + let settings = parseArgs log Settings.defaults [| |] //argv + let user = getUserDetails settings.Platform + + printInfo settings.Platform user + + 0 diff --git a/src/Settings.fs b/src/Settings.fs new file mode 100644 index 0000000..3fd117e --- /dev/null +++ b/src/Settings.fs @@ -0,0 +1,35 @@ +module Settings + +open Types +open System + +let defaults = + { Platform = Dev + ProductMode = Pancake + AutoRun = true + AutoQuit = true + WatchForCrashes = true + RemoteLogging = false } + +let parseArgs log defaults (args: string[]) = + let getArg (flag:string) i = + if i + 1 < args.Length && not (String.IsNullOrEmpty args.[i + 1]) && not (args.[i + 1].StartsWith '/') then + flag.ToLowerInvariant(), Some args.[i + 1] + else + flag.ToLowerInvariant(), None + args + |> Array.mapi (fun index value -> index, value) + |> Array.filter (fun (_, arg) -> not (String.IsNullOrEmpty(arg))) + |> Array.fold (fun s (i, arg) -> + match getArg arg i with + | "/steamid", Some id -> { s with Platform = Steam id } + | "/oculus", Some nonce -> { s with Platform = Oculus nonce } + | "/noremotelogs", _ -> { s with RemoteLogging = false } + | "/nowatchdog", _ -> { s with WatchForCrashes = false } + | "/vr", _ -> { s with ProductMode = Vr } + | "/autorun", _ -> { s with AutoRun = true } + | "/autoquit", _ -> { s with AutoQuit = true } + | _ -> + log.Warn <| sprintf "Ignoring argument '%s'" arg + s + ) defaults \ No newline at end of file diff --git a/src/Steam.fs b/src/Steam.fs new file mode 100644 index 0000000..51d5978 --- /dev/null +++ b/src/Steam.fs @@ -0,0 +1,127 @@ +module Steam + +open System +open System.IO +open System.Runtime.InteropServices +open Types + +[] +let SteamLib = +#if WINDOWS + "steam_api64.dll" +#else + "libsteam_api.so" +#endif + +[] +extern bool SteamAPI_Init() + +[] +extern void SteamAPI_Shutdown() + +[] +extern IntPtr SteamClient(); + +[] +extern int SteamAPI_GetHSteamPipe() + +[] +extern int SteamAPI_ISteamClient_ConnectToGlobalUser(IntPtr instance, int pipe) + +[] +extern IntPtr SteamAPI_ISteamClient_GetISteamUser(IntPtr instance, int user, int pipe, string version) + +[] +extern uint64 SteamAPI_ISteamUser_GetSteamID(IntPtr instance) + +[] +extern uint32 SteamAPI_ISteamUser_GetAuthSessionTicket(IntPtr instance, byte[] buffer, uint32 size, uint32& count) + +[] +extern void SteamAPI_ISteamUser_CancelAuthTicket(IntPtr instance, uint32 hTicket) + +type SteamUser = + { UserId: UInt64 + SessionToken: string } + +type Steam(log : ILog) = + let mutable disposed = false + let mutable initialized = false + let mutable userHandle = IntPtr.Zero + let mutable sessionToken = 0u + + let cleanup disposing = + if not disposed then + log.Debug "Disposing Steam resources" + disposed <- true + + if userHandle <> IntPtr.Zero then + log.Debug "Cancelling auth ticket" + SteamAPI_ISteamUser_CancelAuthTicket(userHandle, sessionToken) + + if initialized then + log.Debug "closing steam" + SteamAPI_Shutdown() + + let getCurrentUserHandle() = + let client = SteamClient() + if client <> IntPtr.Zero then + log.Debug "Got steam client" + let pipe = SteamAPI_GetHSteamPipe() + if pipe <> 0 then + log.Debug "Got steam pipe" + let globalUser = SteamAPI_ISteamClient_ConnectToGlobalUser(client, pipe) + if globalUser <> 0 then + log.Debug "Got steam global user" + userHandle <- SteamAPI_ISteamClient_GetISteamUser(client, globalUser, pipe, "SteamUser019") + if userHandle <> IntPtr.Zero then + log.Debug "Got steam user" + Some userHandle + else None + else None + else None + else None + + let bytesToHex (bytes: byte[]) = + bytes + |> Array.map (fun b -> String.Format("{0:X2}", b)) + |> String.concat "" + + let init() = + if not <| File.Exists(SteamLib) then + Error <| sprintf "Unable to find steam library '%s'" SteamLib + elif SteamAPI_Init() then + initialized <- true + Ok () + else + Error "Unable to initialize Steam" + + member this.Login() = + match init() with + | Error m -> Error m + | Ok _ -> + let errorResult = Error "Unable to get current steam user. Make sure your Steam client is running." + match getCurrentUserHandle() with + | None -> errorResult + | Some handle -> + let userId = SteamAPI_ISteamUser_GetSteamID handle + + if userId <> 0UL then + let rawToken = Array.zeroCreate 1024 + let mutable count = 0u + sessionToken <- SteamAPI_ISteamUser_GetAuthSessionTicket(handle, rawToken, 1024u, &count) + + if sessionToken <> 0u then + Ok { UserId = userId; SessionToken = bytesToHex rawToken } + else errorResult + else errorResult + + interface IDisposable with + member this.Dispose() = + cleanup true + GC.SuppressFinalize(this) + + override this.Finalize() = + cleanup false + + diff --git a/src/Types.fs b/src/Types.fs new file mode 100644 index 0000000..d9b3ea5 --- /dev/null +++ b/src/Types.fs @@ -0,0 +1,36 @@ +module Types + +type ILog = + { Debug: string -> unit + Info: string -> unit + Warn: string -> unit + Error: string -> unit } + with static member Noop = { Debug = (fun _ -> ()); Info = (fun _ -> ()); Warn = (fun _ -> ()); Error = (fun _ -> ()) } +type Platform = + | Steam of string + | Frontier + | Oculus of string + | Dev +type ProductMode = Vr | Pancake +type AutoRun = bool +type AutoQuit = bool +type WatchForCrashes = bool +type RemoteLogging = bool +type LauncherSettings = + { Platform: Platform + ProductMode: ProductMode + AutoRun: AutoRun + AutoQuit: AutoQuit + WatchForCrashes: WatchForCrashes + RemoteLogging: RemoteLogging } +type ServerStatus = Healthy +type LocalVersion = Version +type LauncherStatus = + | Current + | Supported + | Expired + | Future +type ServerInfo = + { Status: ServerStatus} +type User = + { Name: string } diff --git a/tests/EdLauncher.Tests.fsproj b/tests/EdLauncher.Tests.fsproj new file mode 100644 index 0000000..7b1fb9c --- /dev/null +++ b/tests/EdLauncher.Tests.fsproj @@ -0,0 +1,25 @@ + + + + Exe + netcoreapp3.1 + false + + + + + + + + + + + + + + + + + + + diff --git a/tests/Main.fs b/tests/Main.fs new file mode 100644 index 0000000..1913e2f --- /dev/null +++ b/tests/Main.fs @@ -0,0 +1,5 @@ +open Expecto + +[] +let main argv = + Tests.runTestsInAssembly defaultConfig argv diff --git a/tests/Settings.fs b/tests/Settings.fs new file mode 100644 index 0000000..aff0a91 --- /dev/null +++ b/tests/Settings.fs @@ -0,0 +1,52 @@ +module Tests + +open Expecto +open Settings +open Types + + + +[] +let tests = + let parse = parseArgs ILog.Noop Settings.defaults + + testList "Parings command line arguments" [ + test "Matches /steamid id" { + let settings = parse [| "/steamid"; "123" |] + Expect.equal settings.Platform (Steam "123") "" + } + test "Ignores /steamid without id as next arg" { + let settings = parse [| "/steamid"; "/123" |] + Expect.equal settings.Platform Settings.defaults.Platform "" + } + test "Matches /oculus nonce" { + let settings = parse [| "/oculus"; "123" |] + Expect.equal settings.Platform (Oculus "123") "" + } + test "Ignores /oculus without nonce as next arg" { + let settings = parse [| "/oculus"; "/123" |] + Expect.equal settings.Platform Settings.defaults.Platform "" + } + test "Matches /noremotelogs" { + let settings = parse [| "/noremotelogs" |] + Expect.equal settings.RemoteLogging false "" + } + test "Matches /nowatchdog" { + let settings = parse [| "/nowatchdog" |] + Expect.equal settings.WatchForCrashes false "" + } + test "Matches /vr" { + let settings = parse [| "/vr" |] + Expect.equal settings.ProductMode Vr "" + } + test "Matches /autorun" { + let settings = parse [| "/autorun" |] + Expect.equal settings.AutoRun true "" + } + test "Matches /autoquit" { + let settings = parse [| "/autoquit" |] + Expect.equal settings.AutoQuit true "" + } + testProperty "Unknown arg doesn't change any values" <| + fun (args:string[]) -> parse args = Settings.defaults + ]