diff --git a/.gitignore b/.gitignore
index 2b6225d4..39415124 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,6 +53,7 @@ Test.MMManaged/obj
Native/.vscode/settings.json
Native/.ionide/symbolCache.db
MMLaunch/obj
+MMLaunch/bin
.ionide/symbolCache.db
.rustc_info.json
Native/snaplib/target
diff --git a/MMAll.dotnet.sln b/MMAll.dotnet.sln
index 88547283..5f4de33c 100644
--- a/MMAll.dotnet.sln
+++ b/MMAll.dotnet.sln
@@ -9,26 +9,68 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MMManaged.Engine.dotnet", "
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MMTricks.dotnet", "MMTricks\MMTricks.dotnet.fsproj", "{AD894349-DD65-422F-80D1-0ED6F0052352}"
EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MMLaunch", "MMLaunch\MMLaunch.fsproj", "{3F2FB480-E609-4415-96A7-C4BE6DB13570}"
+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
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C6097DB3-F08B-4F18-9E9F-F87E4FE3C99B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C6097DB3-F08B-4F18-9E9F-F87E4FE3C99B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C6097DB3-F08B-4F18-9E9F-F87E4FE3C99B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C6097DB3-F08B-4F18-9E9F-F87E4FE3C99B}.Debug|x64.Build.0 = Debug|Any CPU
+ {C6097DB3-F08B-4F18-9E9F-F87E4FE3C99B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C6097DB3-F08B-4F18-9E9F-F87E4FE3C99B}.Debug|x86.Build.0 = Debug|Any CPU
{C6097DB3-F08B-4F18-9E9F-F87E4FE3C99B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C6097DB3-F08B-4F18-9E9F-F87E4FE3C99B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C6097DB3-F08B-4F18-9E9F-F87E4FE3C99B}.Release|x64.ActiveCfg = Release|Any CPU
+ {C6097DB3-F08B-4F18-9E9F-F87E4FE3C99B}.Release|x64.Build.0 = Release|Any CPU
+ {C6097DB3-F08B-4F18-9E9F-F87E4FE3C99B}.Release|x86.ActiveCfg = Release|Any CPU
+ {C6097DB3-F08B-4F18-9E9F-F87E4FE3C99B}.Release|x86.Build.0 = Release|Any CPU
{91E5FD7F-68BC-40F6-BFBE-549F0B82C671}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91E5FD7F-68BC-40F6-BFBE-549F0B82C671}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {91E5FD7F-68BC-40F6-BFBE-549F0B82C671}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {91E5FD7F-68BC-40F6-BFBE-549F0B82C671}.Debug|x64.Build.0 = Debug|Any CPU
+ {91E5FD7F-68BC-40F6-BFBE-549F0B82C671}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {91E5FD7F-68BC-40F6-BFBE-549F0B82C671}.Debug|x86.Build.0 = Debug|Any CPU
{91E5FD7F-68BC-40F6-BFBE-549F0B82C671}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91E5FD7F-68BC-40F6-BFBE-549F0B82C671}.Release|Any CPU.Build.0 = Release|Any CPU
+ {91E5FD7F-68BC-40F6-BFBE-549F0B82C671}.Release|x64.ActiveCfg = Release|Any CPU
+ {91E5FD7F-68BC-40F6-BFBE-549F0B82C671}.Release|x64.Build.0 = Release|Any CPU
+ {91E5FD7F-68BC-40F6-BFBE-549F0B82C671}.Release|x86.ActiveCfg = Release|Any CPU
+ {91E5FD7F-68BC-40F6-BFBE-549F0B82C671}.Release|x86.Build.0 = Release|Any CPU
{AD894349-DD65-422F-80D1-0ED6F0052352}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD894349-DD65-422F-80D1-0ED6F0052352}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AD894349-DD65-422F-80D1-0ED6F0052352}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AD894349-DD65-422F-80D1-0ED6F0052352}.Debug|x64.Build.0 = Debug|Any CPU
+ {AD894349-DD65-422F-80D1-0ED6F0052352}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AD894349-DD65-422F-80D1-0ED6F0052352}.Debug|x86.Build.0 = Debug|Any CPU
{AD894349-DD65-422F-80D1-0ED6F0052352}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD894349-DD65-422F-80D1-0ED6F0052352}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AD894349-DD65-422F-80D1-0ED6F0052352}.Release|x64.ActiveCfg = Release|Any CPU
+ {AD894349-DD65-422F-80D1-0ED6F0052352}.Release|x64.Build.0 = Release|Any CPU
+ {AD894349-DD65-422F-80D1-0ED6F0052352}.Release|x86.ActiveCfg = Release|Any CPU
+ {AD894349-DD65-422F-80D1-0ED6F0052352}.Release|x86.Build.0 = Release|Any CPU
+ {3F2FB480-E609-4415-96A7-C4BE6DB13570}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3F2FB480-E609-4415-96A7-C4BE6DB13570}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3F2FB480-E609-4415-96A7-C4BE6DB13570}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3F2FB480-E609-4415-96A7-C4BE6DB13570}.Debug|x64.Build.0 = Debug|Any CPU
+ {3F2FB480-E609-4415-96A7-C4BE6DB13570}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3F2FB480-E609-4415-96A7-C4BE6DB13570}.Debug|x86.Build.0 = Debug|Any CPU
+ {3F2FB480-E609-4415-96A7-C4BE6DB13570}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3F2FB480-E609-4415-96A7-C4BE6DB13570}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3F2FB480-E609-4415-96A7-C4BE6DB13570}.Release|x64.ActiveCfg = Release|Any CPU
+ {3F2FB480-E609-4415-96A7-C4BE6DB13570}.Release|x64.Build.0 = Release|Any CPU
+ {3F2FB480-E609-4415-96A7-C4BE6DB13570}.Release|x86.ActiveCfg = Release|Any CPU
+ {3F2FB480-E609-4415-96A7-C4BE6DB13570}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
diff --git a/MMDotNet.sln b/MMDotNet.sln
index 521b2a61..21ad84d8 100644
--- a/MMDotNet.sln
+++ b/MMDotNet.sln
@@ -20,10 +20,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
BlenderScripts\install.py = BlenderScripts\install.py
EndProjectSection
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfInteropSample", "MMLaunch\WpfInteropSample\WpfInteropSample.csproj", "{B7751FAB-AE42-457F-8D69-51BEE5DC080A}"
-EndProject
-Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MMLaunch", "MMLaunch\MMLaunch.fsproj", "{5FC4A84B-A153-4927-B6AA-015C78F6D03D}"
-EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MMView", "MMView\MMView.fsproj", "{404FA43F-143A-4EB1-AE53-24FC9F65A538}"
EndProject
Global
@@ -40,34 +36,18 @@ Global
{E87272E6-CE49-4D85-9E55-EAC6117D419A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E87272E6-CE49-4D85-9E55-EAC6117D419A}.Release|Any CPU.Build.0 = Release|Any CPU
{E87272E6-CE49-4D85-9E55-EAC6117D419A}.Release|x86.ActiveCfg = Release|Any CPU
- {0AAB4CB7-549E-46CF-A3B6-E853C5E381C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0AAB4CB7-549E-46CF-A3B6-E853C5E381C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0AAB4CB7-549E-46CF-A3B6-E853C5E381C8}.Debug|x86.ActiveCfg = Debug|Any CPU
- {0AAB4CB7-549E-46CF-A3B6-E853C5E381C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0AAB4CB7-549E-46CF-A3B6-E853C5E381C8}.Release|Any CPU.Build.0 = Release|Any CPU
- {0AAB4CB7-549E-46CF-A3B6-E853C5E381C8}.Release|x86.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Debug|x86.ActiveCfg = Debug|x86
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Debug|x86.Build.0 = Debug|x86
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Release|Any CPU.Build.0 = Release|Any CPU
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Release|x86.ActiveCfg = Release|x86
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Release|x86.Build.0 = Release|x86
- {5FC4A84B-A153-4927-B6AA-015C78F6D03D}.Debug|Any CPU.ActiveCfg = Debug|x86
- {5FC4A84B-A153-4927-B6AA-015C78F6D03D}.Debug|Any CPU.Build.0 = Debug|x86
- {5FC4A84B-A153-4927-B6AA-015C78F6D03D}.Debug|x86.ActiveCfg = Debug|x86
- {5FC4A84B-A153-4927-B6AA-015C78F6D03D}.Debug|x86.Build.0 = Debug|x86
- {5FC4A84B-A153-4927-B6AA-015C78F6D03D}.Release|Any CPU.ActiveCfg = Release|x86
- {5FC4A84B-A153-4927-B6AA-015C78F6D03D}.Release|Any CPU.Build.0 = Release|x86
- {5FC4A84B-A153-4927-B6AA-015C78F6D03D}.Release|x86.ActiveCfg = Release|x86
- {5FC4A84B-A153-4927-B6AA-015C78F6D03D}.Release|x86.Build.0 = Release|x86
+ {0AAB4CB7-549E-46CF-A3B6-E853C5E381C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0AAB4CB7-549E-46CF-A3B6-E853C5E381C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0AAB4CB7-549E-46CF-A3B6-E853C5E381C8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0AAB4CB7-549E-46CF-A3B6-E853C5E381C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0AAB4CB7-549E-46CF-A3B6-E853C5E381C8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0AAB4CB7-549E-46CF-A3B6-E853C5E381C8}.Release|x86.ActiveCfg = Release|Any CPU
{404FA43F-143A-4EB1-AE53-24FC9F65A538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{404FA43F-143A-4EB1-AE53-24FC9F65A538}.Debug|Any CPU.Build.0 = Debug|Any CPU
{404FA43F-143A-4EB1-AE53-24FC9F65A538}.Debug|x86.ActiveCfg = Debug|x86
diff --git a/MMLaunch/App.axaml b/MMLaunch/App.axaml
new file mode 100644
index 00000000..4831259d
--- /dev/null
+++ b/MMLaunch/App.axaml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/MMLaunch/App.axaml.fs b/MMLaunch/App.axaml.fs
new file mode 100644
index 00000000..771968f0
--- /dev/null
+++ b/MMLaunch/App.axaml.fs
@@ -0,0 +1,19 @@
+namespace MMLaunch
+
+open Avalonia
+open Avalonia.Controls.ApplicationLifetimes
+open Avalonia.Markup.Xaml
+
+type App() =
+ inherit Application()
+
+ override this.Initialize() =
+ AvaloniaXamlLoader.Load(this)
+
+ override this.OnFrameworkInitializationCompleted() =
+ match this.ApplicationLifetime with
+ | :? IClassicDesktopStyleApplicationLifetime as desktop ->
+ desktop.MainWindow <- MainWindow()
+ | _ -> ()
+
+ base.OnFrameworkInitializationCompleted()
diff --git a/MMLaunch/App.config b/MMLaunch/App.config
deleted file mode 100644
index e1a25799..00000000
--- a/MMLaunch/App.config
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/MMLaunch/App.fs b/MMLaunch/App.fs
deleted file mode 100644
index df840628..00000000
--- a/MMLaunch/App.fs
+++ /dev/null
@@ -1,11 +0,0 @@
-module main
-
-open System
-open FsXaml
-
-type App = XAML<"App.xaml">
-
-[]
-[]
-let main argv =
- App().Root.Run()
\ No newline at end of file
diff --git a/MMLaunch/App.xaml b/MMLaunch/App.xaml
deleted file mode 100644
index f61c4ba3..00000000
--- a/MMLaunch/App.xaml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/MMLaunch/AssemblyInfo.fs b/MMLaunch/AssemblyInfo.fs
deleted file mode 100644
index 625be1eb..00000000
--- a/MMLaunch/AssemblyInfo.fs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace System
-open System.Reflection
-open System.Runtime.InteropServices
-
-[]
-[]
-[]
-[]
-[]
-[]
-do ()
-
-module internal AssemblyVersionInformation =
- let [] Version = "1.2.0.0"
diff --git a/MMLaunch/BlenderUtil.fs b/MMLaunch/BlenderUtil.fs
index 24b671f7..d8015c04 100644
--- a/MMLaunch/BlenderUtil.fs
+++ b/MMLaunch/BlenderUtil.fs
@@ -63,7 +63,7 @@ module BlenderUtil =
if key = null then failwith "can't open reg key"
let bKey = key.OpenSubKey SubKey
- if bKey = null then failwith "can't open blender key"
+ if bKey = null then failwith "can't open blender registry key"
let v = bKey.GetValue(name,defVal)
if v = null then failwith "name not found"
@@ -163,9 +163,14 @@ module BlenderUtil =
let rawMsg = sprintf "\n\nTried to run: %s\n\nStdout:\n%s\n\nStderr:\n%s" cmd rawOut ""
failwithf "No addon paths detected; install script may not be compatible with this version of blender:%s" rawMsg
- let isWritable (p:string) =
+ let isWritable (p:string) =
+ // Try to write a probe file. Directory.GetAccessControl from
+ // .NET Framework no longer ships in core; this is a portable
+ // equivalent.
try
- Directory.GetAccessControl(p) |> ignore
+ let probe = Path.Combine(p, ".mmlaunch_probe_" + System.Guid.NewGuid().ToString("N"))
+ File.WriteAllText(probe, "")
+ File.Delete(probe)
true
with
| _ -> false
diff --git a/MMLaunch/BlenderWindow.axaml b/MMLaunch/BlenderWindow.axaml
new file mode 100644
index 00000000..530b61e6
--- /dev/null
+++ b/MMLaunch/BlenderWindow.axaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MMLaunch/BlenderWindow.axaml.fs b/MMLaunch/BlenderWindow.axaml.fs
new file mode 100644
index 00000000..1fab7eab
--- /dev/null
+++ b/MMLaunch/BlenderWindow.axaml.fs
@@ -0,0 +1,123 @@
+namespace MMLaunch
+
+open System
+open System.IO
+
+open Avalonia.Controls
+open Avalonia.Markup.Xaml
+
+open ModelMod
+
+type BlenderViewModel() as x =
+ inherit ViewModelBase()
+
+ let mutable detectedBlender = None
+ let mutable selectedBlender = None
+ let mutable scriptStatus = ""
+
+ let detectBlender () =
+ match BlenderUtil.detectInstallPath () with
+ | None -> None
+ | Some path ->
+ let exe = Path.Combine(path, BlenderUtil.BlenderExe)
+ if File.Exists exe then Some exe else None
+
+ let getScriptStatus (scriptdir: string option) =
+ let lastScriptDir =
+ defaultArg scriptdir (RegConfig.getGlobalValue RegKeys.LastScriptInstallDir "" :?> string)
+
+ match lastScriptDir with
+ | "" -> "Unknown: likely not installed, or was installed manually."
+ | s when not (Directory.Exists s) -> "Unknown"
+ | s ->
+ match BlenderUtil.checkScriptStatus s with
+ | Err msg -> sprintf "Failed to check status: %s" msg
+ | Ok BlenderUtil.NotFound -> "Not installed"
+ | Ok BlenderUtil.UpToDate -> "Up to date"
+ | Ok BlenderUtil.Diverged -> "Out of date or modified locally"
+
+ do
+ detectedBlender <- detectBlender ()
+ let lastBlender = RegConfig.getGlobalValue RegKeys.LastSelectedBlender "" :?> string
+
+ selectedBlender <-
+ match lastBlender with
+ | "" -> detectedBlender
+ | s when not (File.Exists s) -> detectedBlender
+ | s -> Some s
+
+ scriptStatus <- getScriptStatus None
+
+ member _.SelectedBlender
+ with get () =
+ match selectedBlender with
+ | None -> "Not found"
+ | Some s -> s
+ and set (value: string) =
+ if File.Exists value then
+ RegConfig.setGlobalValue RegKeys.LastSelectedBlender value |> ignore
+ selectedBlender <- Some value
+ x.RaisePropertyChanged "SelectedBlender"
+
+ member _.ScriptStatus
+ with get () = scriptStatus
+ and set value =
+ scriptStatus <- value
+ x.RaisePropertyChanged "ScriptStatus"
+
+ member _.Detect =
+ new RelayCommand((fun _ -> true), (fun _ ->
+ match detectBlender () with
+ | None -> ViewModelUtil.pushDialog "Blender not found; try using Browse to find it manually"
+ | Some s ->
+ match ViewModelUtil.pushOkCancelDialog (sprintf "Found blender:\n%s\nUse this?" s) with
+ | DialogResult.Yes -> x.SelectedBlender <- s
+ | _ -> ()))
+
+ member _.Browse =
+ new RelayCommand((fun _ -> true), (fun _ ->
+ let idir =
+ match selectedBlender with
+ | None -> Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
+ | Some exe -> Path.GetDirectoryName exe
+
+ match ViewModelUtil.pushSelectFileDialog (Some idir, "Executable files|*.exe") with
+ | None -> ()
+ | Some exe ->
+ if File.Exists exe then x.SelectedBlender <- exe))
+
+ member _.Check =
+ new RelayCommand((fun _ -> true), (fun _ ->
+ match selectedBlender with
+ | None -> ViewModelUtil.pushDialog "Please select a version of blender to use first."
+ | Some exe ->
+ match BlenderUtil.getAddonsPath exe with
+ | Err e -> ViewModelUtil.pushDialog (sprintf "Failed to get addons path: %s" e)
+ | Ok path ->
+ let path = Path.Combine(path, BlenderUtil.ModName)
+ if Directory.Exists path then
+ RegConfig.setGlobalValue RegKeys.LastScriptInstallDir path |> ignore
+ x.ScriptStatus <- getScriptStatus (Some path)))
+
+ member _.Install =
+ new RelayCommand((fun _ -> true), (fun _ ->
+ match selectedBlender with
+ | None -> ViewModelUtil.pushDialog "Please select a version of blender to use first."
+ | Some exe ->
+ match ViewModelUtil.pushOkCancelDialog
+ "This will install or update the MMObj blender scripts. If you have modified the files locally, your changes will be overwritten. Proceed?" with
+ | DialogResult.Yes ->
+ match BlenderUtil.installMMScripts exe with
+ | Ok dir ->
+ RegConfig.setGlobalValue RegKeys.LastScriptInstallDir dir |> ignore
+ ViewModelUtil.pushDialog (sprintf "Blender scripts installed and registered in '%s'" dir)
+ x.ScriptStatus <- getScriptStatus None
+ | Err s -> ViewModelUtil.pushDialog s
+ | _ -> ()))
+
+type BlenderWindow() as this =
+ inherit Window()
+
+ do
+ AvaloniaXamlLoader.Load(this)
+ this.DataContext <- BlenderViewModel()
diff --git a/MMLaunch/BlenderWindow.xaml b/MMLaunch/BlenderWindow.xaml
deleted file mode 100644
index 1b09473a..00000000
--- a/MMLaunch/BlenderWindow.xaml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/MMLaunch/BlenderWindow.xaml.fs b/MMLaunch/BlenderWindow.xaml.fs
deleted file mode 100644
index 3396b9a0..00000000
--- a/MMLaunch/BlenderWindow.xaml.fs
+++ /dev/null
@@ -1,156 +0,0 @@
-// ModelMod: 3d data snapshotting & substitution program.
-// Copyright(C) 2015,2016 John Quigley
-
-// This program is free software : you can redistribute it and / or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 2.1 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU Lesser General Public License
-// along with this program.If not, see .
-
-namespace MMLaunch
-
-open System
-open System.Diagnostics
-open System.Threading
-open System.Windows
-open System.Windows.Threading
-open System.IO
-open FSharp.ViewModule
-open FSharp.ViewModule.Validation
-open System.Windows.Input
-open System.ComponentModel
-open System.Collections.ObjectModel
-open Microsoft.Win32
-
-open FsXaml
-
-open ViewModelUtil
-open ModelMod
-
-type BlenderView = XAML<"BlenderWindow.xaml", true>
-
-type BlenderViewModel() =
- inherit ViewModelBase()
-
- let mutable detectedBlender = None
- let mutable selectedBlender = None
- let mutable scriptStatus = ""
-
- let detectBlender() =
- let path = BlenderUtil.detectInstallPath()
- match path with
- | None -> None
- | Some path ->
- let exe = Path.Combine(path,BlenderUtil.BlenderExe)
- if File.Exists exe then
- Some(exe)
- else
- None
-
- let getScriptStatus (scriptdir:string option) =
- let lastScriptDir = defaultArg scriptdir (RegConfig.getGlobalValue RegKeys.LastScriptInstallDir "" :?> string)
-
- match lastScriptDir with
- | "" -> "Unknown: likely not installed, or was installed manually."
- | s when not (Directory.Exists(s)) -> "Unknown"
- | s ->
- match BlenderUtil.checkScriptStatus s with
- | Err(s) -> sprintf "Failed to check status: %s" s
- | Ok(BlenderUtil.NotFound) -> "Not installed"
- | Ok(BlenderUtil.UpToDate) -> "Up to date"
- | Ok(BlenderUtil.Diverged) -> "Out of date or modified locally"
-
- do
- detectedBlender <- detectBlender()
- let lastBlender = RegConfig.getGlobalValue RegKeys.LastSelectedBlender "" :?> string
- selectedBlender <-
- match lastBlender with
- | "" -> detectedBlender
- | s when (not (File.Exists s)) -> detectedBlender
- | s -> Some(s)
- scriptStatus <- getScriptStatus None
-
- member x.SelectedBlender
- with get() =
- match selectedBlender with
- | None -> "Not found"
- | Some s -> s
- and set (value:string) =
- if File.Exists value then
- RegConfig.setGlobalValue RegKeys.LastSelectedBlender value |> ignore
- selectedBlender <- Some(value)
-
- x.RaisePropertyChanged("SelectedBlender")
-
- member x.ScriptStatus
- with get() =
- scriptStatus
- and set value =
- scriptStatus <- value
- x.RaisePropertyChanged("ScriptStatus")
-
- member x.Detect =
- new RelayCommand (
- (fun canExecute -> true),
- (fun action ->
- match detectBlender() with
- | None -> ViewModelUtil.pushDialog "Blender not found; try using Browse to find it manually"
- | Some s ->
- match ViewModelUtil.pushOkCancelDialog (sprintf "Found blender:\n%s\nUse this?" s) with
- | MessageBoxResult.Yes -> x.SelectedBlender <- s
- | _ -> ()
- ))
- member x.Browse =
- new RelayCommand (
- (fun canExecute -> true),
- (fun action ->
- let idir =
- match selectedBlender with
- | None -> Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
- | Some exe -> Path.GetDirectoryName(exe)
- match ViewModelUtil.pushSelectFileDialog (Some(idir),"Executable files (*.exe)|*.exe") with
- | None -> ()
- | Some exe ->
- if (File.Exists (exe)) then
- x.SelectedBlender <- exe
- ))
- member x.Check =
- new RelayCommand (
- (fun canExecute -> true),
- (fun action ->
- match selectedBlender with
- | None -> ViewModelUtil.pushDialog "Please select a version of blender to use first."
- | Some exe ->
- match (BlenderUtil.getAddonsPath exe) with
- | Err(e) -> ViewModelUtil.pushDialog (sprintf "Failed to get addons path: %s" e)
- | Ok(path) ->
- let path = Path.Combine(path,BlenderUtil.ModName)
- if Directory.Exists path then
- RegConfig.setGlobalValue RegKeys.LastScriptInstallDir path |> ignore
- x.ScriptStatus <- getScriptStatus (Some(path))
- ))
- member x.Install =
- new RelayCommand (
- (fun canExecute -> true),
- (fun action ->
- match selectedBlender with
- | None -> ViewModelUtil.pushDialog "Please select a version of blender to use first."
- | Some exe ->
- match ViewModelUtil.pushOkCancelDialog ("This will install or update the MMObj blender scripts. If you have modified the files locally, your changes will be overwritten. Proceed?") with
- | MessageBoxResult.Yes ->
- match BlenderUtil.installMMScripts(exe) with
- | Ok(dir) ->
- RegConfig.setGlobalValue RegKeys.LastScriptInstallDir dir |> ignore
- ViewModelUtil.pushDialog (sprintf "Blender scripts installed and registered in'%s'" dir)
- x.ScriptStatus <- getScriptStatus None
-
- | Err(s) -> ViewModelUtil.pushDialog s
- | _ -> ()
- ))
\ No newline at end of file
diff --git a/MMLaunch/ConfirmDialog.axaml b/MMLaunch/ConfirmDialog.axaml
new file mode 100644
index 00000000..22b7b8be
--- /dev/null
+++ b/MMLaunch/ConfirmDialog.axaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MMLaunch/ConfirmDialog.axaml.fs b/MMLaunch/ConfirmDialog.axaml.fs
new file mode 100644
index 00000000..f0583a10
--- /dev/null
+++ b/MMLaunch/ConfirmDialog.axaml.fs
@@ -0,0 +1,61 @@
+namespace MMLaunch
+
+open Avalonia.Controls
+open Avalonia.Markup.Xaml
+
+type ConfirmDialogViewModel() =
+ inherit ViewModelBase()
+
+ let mutable view: ConfirmDialog option = None
+ let mutable confirmed = false
+ let mutable displayText = ""
+ let mutable checkboxText = ""
+ let mutable checkboxChecked = false
+
+ member x.View
+ with get () = view
+ and set value =
+ view <- value
+
+ member x.Text
+ with get () = displayText
+ and set value =
+ displayText <- value
+ x.RaisePropertyChanged "Text"
+
+ member x.CheckBoxText
+ with get () = checkboxText
+ and set value =
+ checkboxText <- value
+ x.RaisePropertyChanged "CheckBoxText"
+ x.RaisePropertyChanged "CheckBoxVisible"
+
+ member x.CheckBoxVisible = checkboxText.Trim() <> ""
+
+ member x.CheckboxChecked
+ with get () = checkboxChecked
+ and set (value: bool) =
+ checkboxChecked <- value
+ x.RaisePropertyChanged "CheckboxChecked"
+
+ member x.Confirmed = confirmed
+
+ member x.Cancel =
+ new RelayCommand((fun _ -> true), (fun _ ->
+ view |> Option.iter (fun v -> v.Close())))
+
+ member x.Confirm =
+ new RelayCommand((fun _ -> true), (fun _ ->
+ confirmed <- true
+ view |> Option.iter (fun v -> v.Close())))
+
+and ConfirmDialog() as this =
+ inherit Window()
+
+ do
+ AvaloniaXamlLoader.Load(this)
+ let vm = ConfirmDialogViewModel()
+ vm.View <- Some this
+ this.DataContext <- vm
+
+ member x.ViewModel = x.DataContext :?> ConfirmDialogViewModel
diff --git a/MMLaunch/ConfirmDialog.xaml b/MMLaunch/ConfirmDialog.xaml
deleted file mode 100644
index 97acb386..00000000
--- a/MMLaunch/ConfirmDialog.xaml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/MMLaunch/ConfirmDialog.xaml.fs b/MMLaunch/ConfirmDialog.xaml.fs
deleted file mode 100644
index 55937691..00000000
--- a/MMLaunch/ConfirmDialog.xaml.fs
+++ /dev/null
@@ -1,88 +0,0 @@
-// ModelMod: 3d data snapshotting & substitution program.
-// Copyright(C) 2015,2016 John Quigley
-
-// This program is free software : you can redistribute it and / or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 2.1 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU Lesser General Public License
-// along with this program.If not, see .
-
-namespace MMLaunch
-
-open System
-open System.IO
-open System.Windows
-open FSharp.ViewModule
-open FSharp.ViewModule.Validation
-open System.Windows.Input
-open System.ComponentModel
-open System.Collections.ObjectModel
-open System.Windows.Controls
-open Microsoft.Win32
-
-open FsXaml
-
-open ViewModelUtil
-
-type ConfirmDialogView = XAML<"ConfirmDialog.xaml", true>
-
-type ConfirmDialogViewModel() =
- inherit ViewModelBase()
-
- let mutable view:ConfirmDialogView option = None
- let mutable confirmed = false
- let mutable displayText = ""
- let mutable checkboxText = ""
- let mutable checkboxChecked = false
-
- member x.View
- with get() =
- match view with
- | None -> null
- | Some view -> view
- and set value =
- view <- Some(value)
-
- member x.Text
- with get() = displayText
- and set value = displayText <- value; x.RaisePropertyChanged("Text")
-
- member x.CheckBoxText
- with get() = checkboxText
- and set value =
- checkboxText <- value
- x.RaisePropertyChanged("CheckBoxText")
- x.RaisePropertyChanged("CheckBoxVisibility")
-
- member x.CheckBoxVisibility
- with get() =
- if ViewModelUtil.DesignMode || checkboxText.Trim() <> "" then Visibility.Visible else Visibility.Hidden
-
- member x.CheckboxChecked
- with get () = checkboxChecked
- and set (value:bool) =
- checkboxChecked <- value
- x.RaisePropertyChanged("CheckboxChecked")
-
- member x.Confirmed
- with get() = confirmed
-
- member x.Cancel =
- new RelayCommand (
- (fun canExecute -> true),
- (fun action -> view |> Option.iter (fun v ->
- v.Root.Close() )))
-
- member x.Confirm =
- new RelayCommand (
- (fun canExecute -> true),
- (fun action -> view |> Option.iter (fun v ->
- confirmed <- true
- v.Root.Close() )))
diff --git a/MMLaunch/CreateModWindow.axaml b/MMLaunch/CreateModWindow.axaml
new file mode 100644
index 00000000..23af1bce
--- /dev/null
+++ b/MMLaunch/CreateModWindow.axaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MMLaunch/CreateModWindow.axaml.fs b/MMLaunch/CreateModWindow.axaml.fs
new file mode 100644
index 00000000..7ebd5836
--- /dev/null
+++ b/MMLaunch/CreateModWindow.axaml.fs
@@ -0,0 +1,201 @@
+namespace MMLaunch
+
+open System
+open System.Collections.ObjectModel
+open System.IO
+
+open Avalonia.Controls
+open Avalonia.Markup.Xaml
+open Avalonia.Threading
+
+open ModelMod
+
+[]
+type MMObjFileModel(fullPath: string) =
+ member x.Name = Path.GetFileName(fullPath)
+ member x.FullPath = fullPath
+
+type CreateModViewModel() as self =
+ inherit ViewModelBase()
+
+ let mutable snapDir = ""
+ let mutable dataDir = ""
+ let mutable targetMMObjFile: MMObjFileModel option = None
+ let mutable modName = ""
+ let mutable previewHost: MeshPreviewControl option = None
+ let mutable mmobjFiles = new ObservableCollection()
+ let mutable addToModIndex = true
+ let mutable convertTextures = true
+ let mutable removeSnapshotsFn = ignore
+
+ let mutable sdWriteTime = DateTime.Now
+
+ let timer = new DispatcherTimer()
+
+ do
+ timer.Interval <- TimeSpan(0, 0, 1)
+ timer.Tick.Add(fun _ ->
+ let sd = self.SnapshotDir
+ if Directory.Exists sd then
+ let writeTime = Directory.GetLastWriteTime sd
+ if writeTime <> sdWriteTime then
+ sdWriteTime <- writeTime
+ self.UpdateFileList())
+ timer.Start()
+
+ let validateModName (mn: string) : Result =
+ let illegalChars = [| '/'; '\\'; ':'; '*'; '?'; '"'; '<'; '>'; '|' |]
+ let mn = mn.Trim()
+
+ match mn with
+ | "" -> Err "Enter mod name"
+ | s when s.IndexOfAny illegalChars >= 0 ->
+ Err(sprintf "Mod name cannot contain any of: %A" illegalChars)
+ | s when s.Contains ".." -> Err "Mod name cannot contain .."
+ | s when Directory.Exists(Path.Combine(dataDir, mn)) ->
+ Err "Directory already exists, please choose a different mod name"
+ | _ -> Ok(Path.Combine(dataDir, mn))
+
+ member x.UpdateFileList() =
+ mmobjFiles.Clear()
+
+ if Directory.Exists snapDir then
+ Directory.GetFiles(snapDir, "*.mmobj")
+ |> Array.sortBy (fun f -> File.GetLastWriteTime(Path.Combine(snapDir, f)))
+ |> Array.rev
+ |> Array.map (fun f -> MMObjFileModel(f))
+ |> Array.iter (fun m -> mmobjFiles.Add m)
+
+ sdWriteTime <- Directory.GetLastWriteTime snapDir
+
+ x.TargetFileChanged()
+ x.RaisePropertyChanged "Files"
+ x.RaisePropertyChanged "RemoveSnapshots"
+
+ member x.SnapshotDir
+ with get () = snapDir
+ and set value =
+ snapDir <- value
+ x.UpdateFileList()
+
+ member x.DataDir
+ with get () = dataDir
+ and set value = dataDir <- value
+
+ member x.PreviewHost
+ with set value = previewHost <- value
+
+ member x.Files = mmobjFiles
+
+ member x.SelectedFile
+ with get () : MMObjFileModel =
+ match targetMMObjFile with
+ | None -> null
+ | Some f -> f
+ and set (value: MMObjFileModel) =
+ targetMMObjFile <- if isNull value then None else Some value
+ x.TargetFileChanged()
+
+ member x.ModName
+ with get () = modName
+ and set (value: string) =
+ modName <- value.Trim()
+ x.TargetFileChanged()
+
+ member x.ModDest =
+ match validateModName modName with
+ | Err s -> s
+ | Ok path -> path
+
+ member x.AddToModIndex
+ with get () = addToModIndex
+ and set value = addToModIndex <- value
+
+ member x.ConvertTextures
+ with get () = convertTextures
+ and set value = convertTextures <- value
+
+ member x.CanCreate =
+ let mnvalid =
+ match validateModName modName with
+ | Err _ -> false
+ | Ok _ -> true
+
+ match targetMMObjFile with
+ | None -> false
+ | Some f -> File.Exists f.FullPath && mnvalid
+
+ member x.RemoveSnapshotsFn
+ with set value = removeSnapshotsFn <- value
+
+ member x.BrowseFile =
+ ViewModelUtil.alwaysExecutable (fun _ ->
+ match ViewModelUtil.pushSelectFileDialog (Some snapDir, "MMObj files|*.mmobj") with
+ | None -> ()
+ | Some file ->
+ let found = mmobjFiles |> Seq.tryFind (fun m -> m.FullPath = file)
+
+ let model =
+ match found with
+ | Some m -> m
+ | None ->
+ let m = MMObjFileModel(file)
+ mmobjFiles.Add m
+ m
+
+ targetMMObjFile <- Some model
+ x.TargetFileChanged())
+
+ member x.TargetFileChanged() =
+ match previewHost with
+ | None -> ()
+ | Some host ->
+ match targetMMObjFile with
+ | None -> host.SelectedFile <- ""
+ | Some file ->
+ host.SelectedFile <- file.FullPath
+
+ x.RaisePropertyChanged "CanCreate"
+ x.RaisePropertyChanged "Create"
+ x.RaisePropertyChanged "ModDest"
+ x.RaisePropertyChanged "SelectedFile"
+
+ member x.RemoveSnapshots =
+ new RelayCommand((fun _ -> true), (fun _ -> removeSnapshotsFn ()))
+
+ member x.Create =
+ new RelayCommand(
+ (fun _ -> x.CanCreate),
+ (fun _ ->
+ match validateModName modName, targetMMObjFile with
+ | Err e, _ -> ViewModelUtil.pushDialog e
+ | _, None -> ()
+ | Ok _, Some file ->
+ match ModUtil.createMod dataDir modName convertTextures file.FullPath with
+ | Ok modFile ->
+ let createdMessage = sprintf "Import %s into blender to edit." modFile
+
+ let modIndexErr =
+ if addToModIndex then
+ match ModUtil.addToModIndex dataDir modFile with
+ | Err e -> sprintf "\n\nFailed to add mod to mod index, please add it manually: %s\n\n" e
+ | Ok _ -> ""
+ else ""
+
+ ViewModelUtil.pushDialog (sprintf "Mod created. %s%s" modIndexErr createdMessage)
+ | Err msg -> ViewModelUtil.pushDialog msg))
+
+type CreateModWindow() as this =
+ inherit Window()
+
+ do
+ AvaloniaXamlLoader.Load(this)
+ let vm = CreateModViewModel()
+ this.DataContext <- vm
+
+ // Wire the preview control found in XAML to the view-model.
+ let preview = this.FindControl("ModelPreview")
+ if not (isNull preview) then
+ vm.PreviewHost <- Some preview
+
+ member x.ViewModel = x.DataContext :?> CreateModViewModel
diff --git a/MMLaunch/CreateModWindow.xaml b/MMLaunch/CreateModWindow.xaml
deleted file mode 100644
index e3d2f872..00000000
--- a/MMLaunch/CreateModWindow.xaml
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/MMLaunch/CreateModWindow.xaml.fs b/MMLaunch/CreateModWindow.xaml.fs
deleted file mode 100644
index b4a6a350..00000000
--- a/MMLaunch/CreateModWindow.xaml.fs
+++ /dev/null
@@ -1,229 +0,0 @@
-// ModelMod: 3d data snapshotting & substitution program.
-// Copyright(C) 2015,2016 John Quigley
-
-// This program is free software : you can redistribute it and / or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 2.1 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU Lesser General Public License
-// along with this program.If not, see .
-
-namespace MMLaunch
-
-open System
-open System.Diagnostics
-open System.Threading
-open System.Windows
-open System.Windows.Threading
-open System.IO
-open FSharp.ViewModule
-open FSharp.ViewModule.Validation
-open System.Windows.Input
-open System.Windows.Controls
-open System.ComponentModel
-open System.Collections.ObjectModel
-open Microsoft.Win32
-
-open FsXaml
-
-open ViewModelUtil
-open ModelMod
-
-type CreateModView = XAML<"CreateModWindow.xaml", true>
-
-[] // For listbox selecteditem compatibility
-type MMObjFileModel(fullPath) =
- member x.Name
- with get() =
- Path.GetFileName(fullPath)
- member x.FullPath
- with get() = fullPath
-
-type CreateModViewModel() as self =
- inherit ViewModelBase()
-
- let mutable snapDir = ""
- let mutable dataDir = ""
- let mutable targetMMObjFile:MMObjFileModel option = None
- let mutable modName = ""
- let mutable previewHost: PreviewHost option = None
- let mutable modNameTB: TextBox option = None
- let mutable mmobjFiles = new ObservableCollection([])
- let mutable addToModIndex = true
- let mutable convertTextures = true
- let mutable removeSnapshotsFn = ignore
-
- let mutable sdWriteTime = DateTime.Now
-
- let timer = new DispatcherTimer()
- do
- timer.Interval <- new TimeSpan(0,0,1)
- timer.Tick.Add(fun (args) ->
- let sd = self.SnapshotDir
- if Directory.Exists(sd) then
- let writeTime = Directory.GetLastWriteTime(sd)
- if writeTime <> sdWriteTime then
- ()
- sdWriteTime <- writeTime
- self.UpdateFileList()
- )
- timer.Start()
-
- let validateModName (mn:string):Result =
- let illegalChars = [|'/'; '\\'; ':'; '*'; '?'; '"'; '<'; '>'; '|'|]
-
- let mn = mn.Trim()
-
- match mn with
- | "" -> Err("Enter mod name")
- | s when s.IndexOfAny(illegalChars) >= 0 -> Err(sprintf "Mod name cannot contain any of: %A" illegalChars)
- | s when s.Contains("..") -> Err("Mod name cannot contain ..")
- | s when Directory.Exists(Path.Combine(dataDir,mn)) -> Err("Directory already exists, please choose a different mod name")
- | s ->
- Ok(Path.Combine(dataDir,mn))
-
- member x.UpdateFileList() =
- mmobjFiles.Clear()
- if Directory.Exists snapDir then
- Directory.GetFiles (snapDir, "*.mmobj")
- |> Array.sortBy (fun f -> File.GetLastWriteTime(Path.Combine(snapDir,f) ))
- |> Array.rev
- |> Array.map (fun f -> MMObjFileModel(f))
- |> Array.iter (fun nt -> mmobjFiles.Add(nt))
- sdWriteTime <- Directory.GetLastWriteTime(snapDir)
-
- x.TargetFileChanged()
- x.RaisePropertyChanged("Files")
- x.RaisePropertyChanged("RemoveSnapshots")
-
- member x.SnapshotDir
- with get() = snapDir
- and set value =
- snapDir <- value
- x.UpdateFileList()
- member x.DataDir
- with get() = dataDir
- and set value = dataDir <- value
- member x.PreviewHost
- with set value = previewHost <- value
- member x.ModNameTB
- with set value =
- modNameTB <- value
- modNameTB |> Option.iter (fun tb -> tb.SelectionChanged.Add(x.ModNameChanged) )
-
- member x.Files
- with get() = mmobjFiles
-
- member x.SelectedFile
- with get() =
- match targetMMObjFile with
- | None -> null
- | Some(file) -> file
- and set value =
- if value = null then
- targetMMObjFile <- None
- else
- targetMMObjFile <- Some(value)
- x.TargetFileChanged()
-
- member x.ModNameChanged args =
- modNameTB |> Option.iter (fun tb ->
- modName <- tb.Text.Trim()
- x.TargetFileChanged()
- )
-
- member x.ModName
- with get() = modName
- and set (value:string) =
- modName <- value.Trim()
- x.TargetFileChanged()
-
- member x.ModDest
- with get() =
- match validateModName modName with
- | Err(s) -> s
- | Ok(path) -> path
-
- member x.BrowseFile = alwaysExecutable (fun action ->
- match ViewModelUtil.pushSelectFileDialog (Some(x.SnapshotDir),"MMObj files (*.mmobj)|*.mmobj") with
- | None -> ()
- | Some (file) ->
- // try to find existing model in collection
- let found = mmobjFiles |> Seq.tryFind (fun m -> m.FullPath = file)
- let model =
- match found with
- | None ->
- let model = new MMObjFileModel(file)
- mmobjFiles.Add(model)
- model
- | Some (model) -> model
- targetMMObjFile <- Some(model)
- x.TargetFileChanged())
-
- member x.TargetFileChanged() =
- match previewHost with
- | None -> ()
- | Some(host) ->
- match targetMMObjFile with
- | None -> host.SelectedFile <- ""
- | Some file ->
- host.SelectedFile <- file.FullPath
-
- x.RaisePropertyChanged("CanCreate")
- x.RaisePropertyChanged("Create")
- x.RaisePropertyChanged("ModDest")
- x.RaisePropertyChanged("SelectedFile")
-
- member x.AddToModIndex
- with get() = addToModIndex
- and set value = addToModIndex <- value
-
- member x.ConvertTextures
- with get() = convertTextures
- and set value = convertTextures <- value
-
- member x.CanCreate =
- let mnvalid =
- match validateModName(modName) with
- | Err(_) -> false
- | Ok(_) -> true
- match targetMMObjFile with
- | None -> false
- | Some (file) -> File.Exists(file.FullPath) && mnvalid
-
- member x.RemoveSnapshotsFn
- with set value = removeSnapshotsFn <- value
-
- member x.RemoveSnapshots =
- new RelayCommand (
- (fun canExecute -> true),
- (fun action -> removeSnapshotsFn() ))
-
- member x.Create =
- new RelayCommand (
- (fun canExecute -> x.CanCreate),
- (fun action ->
- match validateModName(modName),targetMMObjFile with
- | Err(e),_ -> ViewModelUtil.pushDialog(e)
- | _,None -> ()
- | Ok(_),Some(file) ->
- match (ModUtil.createMod dataDir modName convertTextures file.FullPath) with
- | Ok(modFile) ->
- let createdMessage = sprintf "Import %s into blender to edit." modFile
- let modIndexErr =
- if addToModIndex then
- match (ModUtil.addToModIndex dataDir modFile) with
- | Err(e) -> (sprintf "\n\nFailed to add mod to mod index, please add it manually: %s\n\n" e)
- | Ok(_) -> ""
- else
- ""
-
- ViewModelUtil.pushDialog(sprintf "Mod created. %s%s" modIndexErr createdMessage )
- | Err(msg) -> ViewModelUtil.pushDialog(msg)
- ))
diff --git a/MMLaunch/Domain.fs b/MMLaunch/Domain.fs
new file mode 100644
index 00000000..acecc528
--- /dev/null
+++ b/MMLaunch/Domain.fs
@@ -0,0 +1,10 @@
+// Slim domain types for the launcher. The full versions live in MMManaged but pull
+// in MonoGame/SharpDX (.NET Framework only); the launcher only needs the registry-
+// backed RunConfig/GameProfile records and a class shape compatible with
+// SnapshotProfile descriptions.
+
+namespace ModelMod
+
+open System
+
+// The actual Domain contents from MMManaged are included as files (see MMLaunch.fsproj file)
\ No newline at end of file
diff --git a/MMLaunch/GameProfileWindow.axaml b/MMLaunch/GameProfileWindow.axaml
new file mode 100644
index 00000000..8c2d1adb
--- /dev/null
+++ b/MMLaunch/GameProfileWindow.axaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/MMLaunch/GameProfileWindow.axaml.fs b/MMLaunch/GameProfileWindow.axaml.fs
new file mode 100644
index 00000000..b9450cad
--- /dev/null
+++ b/MMLaunch/GameProfileWindow.axaml.fs
@@ -0,0 +1,57 @@
+namespace MMLaunch
+
+open System
+
+open Avalonia.Controls
+open Avalonia.Markup.Xaml
+
+open ModelMod.ConfigTypes
+
+type GameProfileViewModel() =
+ inherit ViewModelBase()
+
+ let mutable profile = {
+ GameProfile.ReverseNormals = false
+ UpdateTangentSpace = true
+ CommandLineArguments = ""
+ DataPathName = ""
+ }
+
+ let mutable profileChangedCb: GameProfile -> unit = ignore
+
+ let updateProfile newProfile =
+ profile <- newProfile
+ profileChangedCb profile
+
+ member x.Profile
+ with get () = profile
+ and set value =
+ profile <- value
+ x.RaisePropertyChanged String.Empty
+
+ member x.ProfileChangedCb
+ with get () = profileChangedCb
+ and set value = profileChangedCb <- value
+
+ member x.ReverseNormals
+ with get () = profile.ReverseNormals
+ and set (value: bool) = updateProfile { profile with ReverseNormals = value }
+
+ member x.UpdateTangentSpace
+ with get () = profile.UpdateTangentSpace
+ and set (value: bool) = updateProfile { profile with UpdateTangentSpace = value }
+
+ member x.CommandLineArguments
+ with get () = profile.CommandLineArguments
+ and set (value: string) = updateProfile { profile with CommandLineArguments = value }
+
+ member x.DataPathName
+ with get () = profile.DataPathName
+ and set (value: string) = updateProfile { profile with DataPathName = value }
+
+type GameProfileWindow() as this =
+ inherit Window()
+
+ do
+ AvaloniaXamlLoader.Load(this)
+ this.DataContext <- GameProfileViewModel()
diff --git a/MMLaunch/GameProfileWindow.xaml b/MMLaunch/GameProfileWindow.xaml
deleted file mode 100644
index 2ed4a742..00000000
--- a/MMLaunch/GameProfileWindow.xaml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/MMLaunch/GameProfileWindow.xaml.fs b/MMLaunch/GameProfileWindow.xaml.fs
deleted file mode 100644
index 83dcfa46..00000000
--- a/MMLaunch/GameProfileWindow.xaml.fs
+++ /dev/null
@@ -1,79 +0,0 @@
-// ModelMod: 3d data snapshotting & substitution program.
-// Copyright(C) 2015,2016 John Quigley
-
-// This program is free software : you can redistribute it and / or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 2.1 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU Lesser General Public License
-// along with this program.If not, see .
-
-namespace MMLaunch
-
-open System
-open System.Diagnostics
-open System.Threading
-open System.Windows
-open System.Windows.Threading
-open System.IO
-open FSharp.ViewModule
-open FSharp.ViewModule.Validation
-open System.Windows.Input
-open System.ComponentModel
-open System.Collections.ObjectModel
-open Microsoft.Win32
-open FsXaml
-
-open ViewModelUtil
-open ModelMod
-open ModelMod.CoreTypes
-
-type GameProfileView = XAML<"GameProfileWindow.xaml", true>
-
-type GameProfileViewModel() =
- inherit ViewModelBase()
-
- // The viewmodel is also the actual Model for the GameProfile
- let mutable profile = {
- GameProfile.ReverseNormals = false
- UpdateTangentSpace = true
- CommandLineArguments = ""
- DataPathName = ""
- }
- let mutable profileChangedCb: GameProfile -> unit = ignore
-
- let updateProfile newProfile =
- profile <- newProfile
- profileChangedCb profile
-
- member x.Profile
- with get() = profile
- and set value =
- profile <- value
- x.RaisePropertyChanged(String.Empty)
-
- member x.ProfileChangedCb
- with get() = profileChangedCb
- and set value = profileChangedCb <- value
-
- member x.ReverseNormals
- with get () = profile.ReverseNormals
- and set (value:bool) = updateProfile { profile with ReverseNormals = value }
-
- member x.UpdateTangentSpace
- with get() = profile.UpdateTangentSpace
- and set (value:bool) = updateProfile { profile with UpdateTangentSpace = value }
-
- member x.CommandLineArguments
- with get () = profile.CommandLineArguments
- and set (value:string) = updateProfile { profile with CommandLineArguments = value}
-
- member x.DataPathName
- with get() = profile.DataPathName
- and set (value:string) = updateProfile { profile with DataPathName = value }
diff --git a/MMLaunch/KnownGames.fs b/MMLaunch/KnownGames.fs
index 5400404e..180a7335 100644
--- a/MMLaunch/KnownGames.fs
+++ b/MMLaunch/KnownGames.fs
@@ -15,7 +15,7 @@ module KnownGames =
let AllKnownGames = [
{
KnownGame.ExeBaseName = "gw2-64"
- D3DPaths = [D3D9(@"bin64"); D3D11(@"")]
+ D3DPaths = [D3D11(@"")]
Is64Bit = true
}
{
diff --git a/MMLaunch/MMLaunch.fsproj b/MMLaunch/MMLaunch.fsproj
index 67fd43dd..0faee532 100644
--- a/MMLaunch/MMLaunch.fsproj
+++ b/MMLaunch/MMLaunch.fsproj
@@ -1,187 +1,92 @@
-
-
+
+
- Debug
- x86
- 8.0.30703
- 2.0
- 5fc4a84b-a153-4927-b6aa-015c78f6d03d
- WinExe
+ Exe
+ WinExe
+ net8.0
MMLaunch
MMLaunch
- v4.6.2
- true
- 4.6.0.0
- MMLaunch
- .\Resource.res
-
+ 1.3.0.0
+ app.manifest
+ ModelMod.ico
+ true
+
+ $(NoWarn);FS0020;FS0025;FS0026;FS0040;FS0044;FS0046;FS0049;FS0052;FS0064;FS0067;FS0086;FS0760;FS0988;FS3370;FS3391;FS3559
-
- true
- full
- false
- false
- bin\Debug\
- DEBUG;TRACE
- 3
- x86
- bin\Debug\MMLaunch.XML
+
+
+ win-x64
+ true
+ true
+ true
+
+
-
- pdbonly
- true
- true
- bin\Release\
- TRACE
- 3
- x86
- bin\Release\MMLaunch.XML
-
-
- 11
-
-
-
-
- $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets
-
-
-
-
- F:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Microsoft\VisualStudio\v16.0\FSharp\Microsoft.FSharp.Targets
-
-
-
-
- C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\CommonExtensions\Microsoft\FSharp\Tools\Microsoft.FSharp.Targets
-
-
-
-
- $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Domain\Logging.fs
+
+
+ Domain\Util.fs
+
+
+
+ Domain\ConfigTypes.fs
+
+
+ Domain\Yaml.fs
+
+
+ Domain\SnapshotProfileLoad.fs
+
+
+
+ Domain\RegConfig.fs
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
- MMView
- {404fa43f-143a-4eb1-ae53-24fc9f65a538}
- True
-
-
- WpfInteropSample
- {b7751fab-ae42-457f-8d69-51bee5dc080a}
- True
-
-
-
- ..\FSharp.Core.4.4.3.0\FSharp.Core.dll
-
-
-
- ..\packages\MonoGame.Framework.WindowsDX\lib\net40\MonoGame.Framework.dll
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- MMManaged
- {e87272e6-ce49-4d85-9e55-eac6117d419a}
- True
-
-
- MMManaged.Engine
- {a1b2c3d4-e5f6-7890-abcd-ef1234567890}
- True
-
+
+
-
-
-
-
- ..\packages\Expression.Blend.Sdk\lib\net45\System.Windows.Interactivity.dll
- True
- True
-
-
-
-
-
-
-
-
- ..\packages\FSharp.ViewModule.Core\lib\net45\FSharp.ViewModule.Core.Wpf.dll
- True
- True
-
-
-
-
-
-
-
-
- ..\packages\FsXaml.Wpf\lib\net45\FsXaml.Wpf.dll
- True
- True
-
-
- ..\packages\FsXaml.Wpf\lib\net45\FsXaml.Wpf.TypeProvider.dll
- True
- True
-
-
-
-
-
-
-
-
- ..\packages\YamlDotNet\lib\net35\YamlDotNet.dll
- True
- True
-
-
-
-
-
\ No newline at end of file
+
+
diff --git a/MMLaunch/MainWindow.axaml b/MMLaunch/MainWindow.axaml
new file mode 100644
index 00000000..7f544258
--- /dev/null
+++ b/MMLaunch/MainWindow.axaml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MMLaunch/MainWindow.axaml.fs b/MMLaunch/MainWindow.axaml.fs
new file mode 100644
index 00000000..7ac69f8f
--- /dev/null
+++ b/MMLaunch/MainWindow.axaml.fs
@@ -0,0 +1,801 @@
+// ModelMod: 3d data snapshotting & substitution program.
+// Copyright(C) 2015,2016 John Quigley
+//
+// This program is free software : you can redistribute it and / or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+// GNU General Public License for more details.
+
+namespace MMLaunch
+
+open System
+open System.Collections.ObjectModel
+open System.Diagnostics
+open System.IO
+
+open Avalonia
+open Avalonia.Controls
+open Avalonia.Markup.Xaml
+open Avalonia.Media.Imaging
+open Avalonia.Threading
+
+open ModelMod
+open ModelMod.ConfigTypes
+
+// Helper module for using parameterized loc strings with failwith, etc
+module Formatters =
+ type String = Printf.StringFormat unit>
+ type StringRetString = Printf.StringFormat string>
+ type StringAnyRetString<'a> = Printf.StringFormat 'a -> string>
+ type AnyRetString<'a> = Printf.StringFormat<'a -> string>
+
+module LocStrings =
+ module Input =
+ let Header = "Input:"
+ let Desc1 = "Press CONTROL followed by the following keys."
+ let Desc2 = "There is no in-game UI that displays these, so try alt-tab if you forget them."
+ let Reload = "Load (or reload) modelmod managed code, configuration, and mods"
+ let ReloadMods = "Load (or reload) mods only"
+ let Toggle = "Toggle mod display"
+ let ClearTex = "Clear the active texture list (will be rebuilt from scene textures)"
+ let SelectNextTex = "Select Previous Texture"
+ let SelectPrevTex = "Select Next Texture"
+ let DoSnapshot = "Take snapshot of current selection"
+
+ module Snapshot =
+ let Header = "Snapshot Profile:"
+
+ module Errors =
+ let ProfileNameRequired = "Profile name may not be empty"
+ let NoFilesInDir = Formatters.AnyRetString("No files in %s")
+ let SnapshotDirNotFound = Formatters.StringRetString("Snapshot directory does not exist: %s")
+ let LoaderStartFailed = Formatters.StringAnyRetString("Start Failed: %s (target: %s)")
+ let LoaderUnknownExit = Formatters.AnyRetString("Unknown (code: %d)")
+ let NoInputDescription = "No Input description available"
+ let NoSnapshotDescription = "No Snapshot description available"
+ let BadExePath = "Cannot set exe path; it is already used by another profile"
+
+ module Misc =
+ let NewProfileName = "New Profile"
+ let PermanentRemove:Formatters.StringAnyRetString = Formatters.StringAnyRetString("Permanently remove files (no recycle bin) in %s?\n\nFiles:\n%s")
+ //let ConfirmRecycle = Formatters.StringAnyRetString("Remove files in %s?\n%A")
+ let ConfirmProfileRemove = Formatters.AnyRetString("Remove profile '%s'?\n\nNote: mod & snapshot files that are associated with this profile will not be removed.")
+ let RecycleMoar = Formatters.StringAnyRetString("%s\n...and %d more")
+ let LoaderNotStarted = "Not Started"
+ let LoaderStartPending = "Start Pending..."
+ let LoaderStopped = Formatters.StringAnyRetString("Exited with status: %s (target: %s)")
+ let LoaderStarted = Formatters.AnyRetString("Started; waiting for exit (target: %s)")
+ let ExeFilesFilter = "Executable files (*.exe)"
+ let StartCopy =
+ Formatters.StringAnyRetString(
+ "This version of ModelMod does not know how to start this game. " +
+ "Therefore ModelMod probably doesn't work with it.\n\nIf you want to try manually, " +
+ "first figure out whether the game is 32 or 64 bit and whether it uses d3d9 or 11." +
+ "\nThen you copy d3d9.dll or d3d11.dll into the game's executable directory, and then start the game manually." +
+ "\nThe destination may be the game's directory or a subdirectory (like 'bin64')." +
+ "\nIf the game is 32 bit, copy %s\\modelmod_32\\d3d9.dll. (or d3d11.dll)" +
+ "\nIf the game is 64 bit, copy %s\\modelmod_64\\d3d9.dll. (or d3d11.dll)" +
+ "\nIf you don't know where to copy the file, it is the same location Reshade would use." +
+ "\nRemove d3d9.dll (or d3d11.dll) from the game's directory to stop using ModelMod.")
+
+module ProfileText =
+ module Input =
+ let CommandOrder = [
+ LocStrings.Input.ReloadMods; LocStrings.Input.Toggle
+ LocStrings.Input.ClearTex
+ LocStrings.Input.SelectNextTex; LocStrings.Input.SelectPrevTex; LocStrings.Input.DoSnapshot
+ LocStrings.Input.Reload ]
+
+ let PunctKeys = [@"\"; "]"; ";"; ","; "."; "/"; "-"]
+ let FKeys = ["F1"; "F2"; "F6"; "F3"; "F4"; "F7"; "F10"]
+
+ let Descriptions =
+ let makeInputDesc keys =
+ List.fold2 (fun acc key text -> acc + (sprintf "%s\t%s\n" key text)) "" keys CommandOrder
+
+ Map.ofList [ (InputProfiles.PunctRock, makeInputDesc PunctKeys)
+ (InputProfiles.FItUp, makeInputDesc FKeys) ]
+
+/// Loads an exe file's icon as an Avalonia Bitmap. Falls back to null on
+/// failure. Uses System.Drawing's icon extraction; works on Windows only,
+/// which is fine since the launcher only runs there.
+module IconLoader =
+ let extractFromExe (exePath: string) : Bitmap option =
+ if String.IsNullOrEmpty exePath || not (File.Exists exePath) then None
+ else
+ try
+ use icon = System.Drawing.Icon.ExtractAssociatedIcon(exePath)
+ if isNull icon then None
+ else
+ use bmp = icon.ToBitmap()
+ use ms = new MemoryStream()
+ bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Png)
+ ms.Position <- 0L
+ Some(new Bitmap(ms))
+ with _ -> None
+
+/// Mutable wrapper around an immutable RunConfig.
+[] // ListBox bindings need a reference type that supports null
+type ProfileModel(config: ConfigTypes.RunConfig) =
+ let mutable config = config
+
+ // set defaults for empty profile values
+ let mutable config =
+ if config.SnapshotProfile.Trim() = "" then
+ { config with SnapshotProfile = ConfigTypes.DefaultSnapProfileName }
+ else config
+
+ let mutable config =
+ if config.InputProfile.Trim() = "" || not (InputProfiles.isValid config.InputProfile) then
+ { config with InputProfile = InputProfiles.DefaultProfile }
+ else config
+
+ let save () =
+ try RegConfig.saveProfile config
+ with e -> ViewModelUtil.pushDialog (sprintf "%s" e.Message)
+
+ let mutable iconSource: Bitmap option =
+ IconLoader.extractFromExe config.ExePath
+
+ member x.Config = config
+
+ member x.Icon
+ with get () =
+ match iconSource with
+ | Some b -> b :> Avalonia.Media.IImage
+ | None -> null
+
+ member x.ProfileKeyName = config.ProfileKeyName
+
+ member x.Name
+ with get () = config.ProfileName
+ and set (value: string) =
+ let value = value.Trim()
+ if value = "" then ViewModelUtil.pushDialog LocStrings.Errors.ProfileNameRequired
+ else
+ config <- { config with ProfileName = value }
+ save ()
+
+ member x.ExePath
+ with get () = config.ExePath
+ and set value =
+ config <- { config with ExePath = value }
+ iconSource <- IconLoader.extractFromExe value
+ save ()
+
+ member x.InputProfile
+ with get () = config.InputProfile
+ and set value =
+ config <- { config with InputProfile = value }
+ save ()
+
+ member x.SnapshotProfile
+ with get () = config.SnapshotProfile
+ and set value =
+ config <- { config with SnapshotProfile = value }
+ save ()
+
+ member x.LaunchWindow
+ with get () = config.LaunchWindow
+ and set value =
+ config <- { config with LaunchWindow = value }
+ save ()
+
+ member x.LoadModsOnStart
+ with get () = config.LoadModsOnStart
+ and set value =
+ config <- { config with LoadModsOnStart = value }
+ save ()
+
+ member x.GameProfile
+ with get () = config.GameProfile
+ and set value =
+ config <- { config with GameProfile = value }
+ save ()
+
+module MainViewUtil =
+ let pushSelectExecutableDialog (currentExe: string option) =
+ let initialDir =
+ match currentExe with
+ | None -> None
+ | Some exe when File.Exists exe -> Some(Directory.GetParent(exe).ToString())
+ | _ -> None
+
+ ViewModelUtil.pushSelectFileDialog (initialDir, LocStrings.Misc.ExeFilesFilter + "|*.exe")
+
+ let getDirLocator (profile: ProfileModel) =
+ let root = ProcessUtil.getMMRoot ()
+ State.DirLocator(root, profile.Config)
+
+ let getSnapshotDir (profile: ProfileModel) =
+ Path.GetFullPath(getDirLocator(profile).ExeSnapshotDir)
+
+ let getDataDir (profile: ProfileModel) =
+ Path.GetFullPath(getDirLocator(profile).ExeDataDir)
+
+ let pushDeleteProfileDialog (profile: ProfileModel) =
+ let msg = sprintf LocStrings.Misc.ConfirmProfileRemove profile.Name
+ match ViewModelUtil.pushOkCancelDialog msg with
+ | DialogResult.Yes -> true
+ | _ -> false
+
+ let private setOwner (child: Window) (parent: Window) =
+ child.WindowStartupLocation <- WindowStartupLocation.CenterOwner
+
+ let makeBlenderWindow (parentWin: Window) =
+ let view = BlenderWindow()
+ setOwner view parentWin
+ view, (view.DataContext :?> BlenderViewModel)
+
+ let makePreferencesWindow (parentWin: Window) =
+ let view = PreferencesWindow()
+ setOwner view parentWin
+ view, (view.DataContext :?> PreferencesViewModel)
+
+ let makeGameProfileWindow (parentWin: Window) =
+ let view = GameProfileWindow()
+ setOwner view parentWin
+ view, (view.DataContext :?> GameProfileViewModel)
+
+ let makeConfirmDialog (parentWin: Window) =
+ let view = ConfirmDialog()
+ setOwner view parentWin
+ view, view.ViewModel
+
+ let private showDialogSync (win: Window) (parent: Window) =
+ let task = win.ShowDialog(parent)
+ let frame = DispatcherFrame()
+ task.ContinueWith(fun _ -> frame.Continue <- false) |> ignore
+ Dispatcher.UIThread.PushFrame(frame)
+
+ let pushRemoveSnapshotsDialog (mainWin: Window) (profile: ProfileModel) =
+ let snapdir = getSnapshotDir profile
+
+ if not (Directory.Exists snapdir) then
+ ViewModelUtil.pushDialog (sprintf LocStrings.Errors.SnapshotDirNotFound snapdir)
+ else
+ let files = Directory.GetFiles snapdir
+
+ if files.Length = 0 then
+ ViewModelUtil.pushDialog (sprintf LocStrings.Errors.NoFilesInDir snapdir)
+ else
+ let display = 10
+ let take = Math.Min(files.Length, display)
+ let moar = files.Length - take
+ let preview = files |> Seq.take take |> Array.ofSeq
+ let preview = String.Join("\n", preview)
+
+ let baseMsg = sprintf LocStrings.Misc.PermanentRemove snapdir preview
+ let msg = if moar = 0 then baseMsg else sprintf LocStrings.Misc.RecycleMoar baseMsg moar
+
+ let view, vm = makeConfirmDialog mainWin
+ vm.Text <- msg
+ vm.CheckBoxText <- "" // recycle bin not available on .NET 8 cross-platform; always permanent delete
+ showDialogSync view mainWin
+
+ if vm.Confirmed then
+ for f in files do
+ try File.Delete f with _ -> ()
+
+ let makeCreateModDialog (parentWin: Window) (profile: ProfileModel) =
+ let view = CreateModWindow()
+ setOwner view parentWin
+
+ let vm = view.ViewModel
+ vm.SnapshotDir <- getSnapshotDir profile
+ vm.DataDir <- getDataDir profile
+
+ view, vm
+
+ let failValidation (msg: string) = ViewModelUtil.pushDialog msg
+
+ let profileDirHasFiles (p: ProfileModel) (dirSelector: ProfileModel -> string) =
+ try
+ if p.ExePath = "" then false
+ else
+ let sd = dirSelector p
+ if not (Directory.Exists sd) then false
+ else Directory.EnumerateFileSystemEntries(sd).GetEnumerator().MoveNext()
+ with _ -> false
+
+ let openModsDir (p: ProfileModel) =
+ if profileDirHasFiles p getDataDir then
+ let proc = new Process()
+ proc.StartInfo.UseShellExecute <- true
+ proc.StartInfo.FileName <- getDataDir p
+ proc.Start() |> ignore
+
+ let showDialog (win: Window) (parent: Window) = showDialogSync win parent
+
+/// Used for Snapshot and Input profiles, since they both basically just have a name
+/// and description as far as the UI is concerned.
+type SubProfileModel(name: string) =
+ member x.Name = name
+
+type LaunchWindowModel(name: string, time: int) =
+ member x.Name = name
+ member x.Time = time
+
+type GameExePath = string
+
+type LoaderState =
+ | NotStarted
+ | StartPending of GameExePath
+ | StartFailed of Exception * GameExePath
+ | Started of Process * GameExePath
+ | Stopped of Process * GameExePath
+
+type MainViewModel() as self =
+ inherit ViewModelBase()
+
+ let mutable selectedProfile: ProfileModel option = None
+ let mutable loaderState = NotStarted
+
+ do RegConfig.init ()
+
+ let snapshotProfileDefs, snapshotProfileNames =
+ try
+ let defs = SnapshotProfileLoad.GetAll(ProcessUtil.getMMRoot ())
+ let names = defs |> Map.toList |> List.map fst
+ defs, names
+ with _ -> Map.ofList [], []
+
+ let observableProfiles =
+ new ObservableCollection(
+ RegConfig.loadAll ()
+ |> Array.sortBy (fun gp -> gp.ProfileName.ToLowerInvariant().Trim())
+ |> Array.map (fun rc -> ProfileModel(rc)))
+
+ let timer = new DispatcherTimer()
+
+ do
+ timer.Interval <- TimeSpan(0, 0, 1)
+ timer.Tick.Add(fun _ -> self.PeriodicUpdate())
+ timer.Start()
+
+ let launchWindows =
+ [ LaunchWindowModel("5 Seconds", 5)
+ LaunchWindowModel("15 Seconds", 15)
+ LaunchWindowModel("30 Seconds", 30)
+ LaunchWindowModel("45 Seconds", 45) ]
+
+ let getSelectedProfileField (getter: ProfileModel -> 'a) (devVal: 'a) =
+ match selectedProfile with
+ | None -> devVal
+ | Some p -> getter p
+
+ let setSelectedProfileField (setter: ProfileModel -> unit) =
+ match selectedProfile with
+ | None -> ()
+ | Some p -> setter p
+
+ member x.PeriodicUpdate() =
+ try
+ let currentRoot = ProcessUtil.getMMRoot ()
+ let regRoot = RegConfig.getMMRoot ()
+ if currentRoot <> regRoot then RegConfig.setMMRoot currentRoot |> ignore
+ with _ -> ()
+
+ x.UpdateLoaderState
+ <| match loaderState with
+ | NotStarted
+ | StartPending _
+ | StartFailed _ -> loaderState
+ | Stopped (_, _) -> loaderState
+ | Started (proc, exe) ->
+ if proc.HasExited then
+ try
+ File.Delete(Path.Combine(Path.GetDirectoryName exe, "ModelModCLRAppDomain.dll"))
+ with _ -> ()
+
+ Stopped(proc, exe)
+ else loaderState
+
+ x.UpdateProfileButtons()
+
+ member x.LoaderStateText =
+ match loaderState with
+ | NotStarted -> LocStrings.Misc.LoaderNotStarted
+ | StartPending _ -> LocStrings.Misc.LoaderStartPending
+ | StartFailed (e, exe) -> sprintf LocStrings.Errors.LoaderStartFailed e.Message exe
+ | Stopped (proc, exe) ->
+ let exitReason =
+ ProcessUtil.getLoaderExitReason proc (sprintf LocStrings.Errors.LoaderUnknownExit proc.ExitCode)
+ sprintf LocStrings.Misc.LoaderStopped exitReason exe
+ | Started (_, exe) -> sprintf LocStrings.Misc.LoaderStarted exe
+
+ member x.Profiles = observableProfiles
+
+ member x.SnapshotProfiles =
+ new ObservableCollection(snapshotProfileNames |> List.map SubProfileModel)
+
+ member x.InputProfiles =
+ new ObservableCollection(InputProfiles.ValidProfiles |> List.map SubProfileModel)
+
+ member x.LaunchWindows = new ObservableCollection(launchWindows)
+
+ /// Bound to the listbox. ListBox can't bind to an option-typed property,
+ /// so we expose a plain ProfileModel-or-null adapter and translate.
+ member x.SelectedProfileRaw
+ with get () : ProfileModel =
+ match selectedProfile with
+ | None -> null
+ | Some p -> p
+ and set (value: ProfileModel) =
+ x.SelectedProfile <-
+ if isNull value then None else Some value
+
+ member x.SelectedProfile
+ with get () = selectedProfile
+ and set value =
+ selectedProfile <- value
+
+ x.RaisePropertyChanged "SelectedProfile"
+ x.RaisePropertyChanged "SelectedProfileRaw"
+ x.RaisePropertyChanged "SelectedProfileName"
+ x.RaisePropertyChanged "SelectedProfileExePath"
+ x.RaisePropertyChanged "SelectedProfileLoadModsOnStart"
+ x.RaisePropertyChanged "SelectedProfileLaunchWindow"
+ x.RaisePropertyChanged "SelectedInputProfile"
+ x.RaisePropertyChanged "SelectedSnapshotProfile"
+ x.RaisePropertyChanged "ProfileAreaVisible"
+ x.RaisePropertyChanged "ProfileDescription"
+ x.UpdateLaunchUI()
+ x.UpdateProfileButtons()
+
+ member x.SelectedProfileName
+ with get () = getSelectedProfileField (fun p -> p.Name) ""
+ and set (value: string) = setSelectedProfileField (fun p -> p.Name <- value)
+
+ member x.SelectedProfileExePath
+ with get () = getSelectedProfileField (fun p -> p.ExePath) ""
+ and set (value: string) = setSelectedProfileField (fun p -> p.ExePath <- value)
+
+ member x.SelectedProfileLoadModsOnStart
+ with get () = getSelectedProfileField (fun p -> p.LoadModsOnStart) ConfigTypes.DefaultRunConfig.LoadModsOnStart
+ and set (value: bool) = setSelectedProfileField (fun p -> p.LoadModsOnStart <- value)
+
+ member x.SelectedProfileLaunchWindow
+ with get () =
+ let time = getSelectedProfileField (fun p -> p.LaunchWindow) ConfigTypes.DefaultRunConfig.LaunchWindow
+
+ match launchWindows |> List.tryFind (fun lt -> lt.Time = time) with
+ | None -> launchWindows.Head.Time
+ | Some lw -> lw.Time
+ and set (value: int) = setSelectedProfileField (fun p -> p.LaunchWindow <- value)
+
+ member x.SelectedInputProfile
+ with get () = getSelectedProfileField (fun p -> p.InputProfile) ConfigTypes.DefaultRunConfig.InputProfile
+ and set (value: string) =
+ setSelectedProfileField (fun p -> p.InputProfile <- value)
+ x.RaisePropertyChanged "ProfileDescription"
+
+ member x.SelectedSnapshotProfile
+ with get () = getSelectedProfileField (fun p -> p.SnapshotProfile) ConfigTypes.DefaultRunConfig.SnapshotProfile
+ and set (value: string) =
+ setSelectedProfileField (fun p -> p.SnapshotProfile <- value)
+ x.RaisePropertyChanged "ProfileDescription"
+
+ member x.ProfileDescription =
+ match x.SelectedProfile with
+ | None -> ""
+ | Some profile ->
+ let inputText =
+ match ProfileText.Input.Descriptions |> Map.tryFind profile.InputProfile with
+ | None -> LocStrings.Errors.NoInputDescription
+ | Some text ->
+ LocStrings.Input.Header + "\n" + LocStrings.Input.Desc1 + "\n" + LocStrings.Input.Desc2 + "\n" + text
+
+ let snapshotText =
+ let stext =
+ snapshotProfileDefs
+ |> Map.tryFind profile.SnapshotProfile
+ |> function
+ | None -> LocStrings.Errors.NoSnapshotDescription
+ | Some p -> p.ToString()
+
+ LocStrings.Snapshot.Header + "\n" + stext
+
+ inputText + "\n" + snapshotText
+
+ member x.LauncherProfileIcon =
+ match loaderState with
+ | Started (_, exe)
+ | StartPending exe ->
+ match observableProfiles |> Seq.tryFind (fun p -> p.ExePath = exe) with
+ | None -> null
+ | Some p -> p.Icon
+ | NotStarted
+ | StartFailed _
+ | Stopped _ ->
+ match x.SelectedProfile with
+ | None -> null
+ | Some p -> p.Icon
+
+ member x.ProfileAreaVisible = selectedProfile.IsSome
+
+ member x.BrowseExe =
+ ViewModelUtil.alwaysExecutable (fun mainWin ->
+ selectedProfile
+ |> Option.iter (fun selectedProfile ->
+ match MainViewUtil.pushSelectExecutableDialog (Some selectedProfile.ExePath) with
+ | None -> ()
+ | Some exePath ->
+ let existingProfileKey = RegConfig.findProfilePath exePath
+
+ let ok =
+ match existingProfileKey with
+ | None -> true
+ | Some key -> key.EndsWith(selectedProfile.ProfileKeyName)
+
+ if ok then
+ selectedProfile.ExePath <- exePath
+
+ if selectedProfile.Name = ""
+ || selectedProfile.Name.StartsWith(LocStrings.Misc.NewProfileName) then
+ selectedProfile.Name <- RegConfig.getDefaultProfileName exePath
+
+ x.SelectedProfile <- x.SelectedProfile
+ else
+ MainViewUtil.failValidation LocStrings.Errors.BadExePath))
+
+ member x.UpdateLoaderState(newState) =
+ if newState <> loaderState then
+ loaderState <- newState
+ x.UpdateLaunchUI()
+
+ member x.UpdateLaunchUI() =
+ x.RaisePropertyChanged "LoaderStateText"
+ x.RaisePropertyChanged "LoaderIsStartable"
+ x.RaisePropertyChanged "StartInSnapshotMode"
+ x.RaisePropertyChanged "StartInDebugMode"
+ x.RaisePropertyChanged "LauncherProfileIcon"
+ x.RaisePropertyChanged "ViewInjectionLog"
+ x.RaisePropertyChanged "ViewModelModLog"
+
+ member x.UpdateProfileButtons() =
+ x.RaisePropertyChanged "DeleteProfile"
+ x.RaisePropertyChanged "RemoveSnapshots"
+ x.RaisePropertyChanged "CreateMod"
+ x.RaisePropertyChanged "OpenMods"
+
+ member x.HasModFiles =
+ match x.SelectedProfile with
+ | None -> false
+ | Some profile -> MainViewUtil.profileDirHasFiles profile MainViewUtil.getDataDir
+
+ member x.HasSnapshots =
+ match x.SelectedProfile with
+ | None -> false
+ | Some profile -> MainViewUtil.profileDirHasFiles profile MainViewUtil.getSnapshotDir
+
+ member x.LoaderIsStartable =
+ match loaderState with
+ | StartPending _
+ | Started _ -> false
+ | StartFailed _
+ | Stopped _
+ | NotStarted -> true
+
+ member x.NewProfile =
+ new RelayCommand(
+ (fun _ -> true),
+ (fun _ ->
+ let seqNextName =
+ seq {
+ let rec next count =
+ let nextName =
+ if count = 0 then LocStrings.Misc.NewProfileName
+ else sprintf "%s (%d)" LocStrings.Misc.NewProfileName count
+
+ match observableProfiles |> Seq.tryFind (fun pm -> pm.Name = nextName) with
+ | None -> nextName
+ | Some _ -> next (count + 1)
+ yield next 0
+ }
+
+ let profile =
+ ProfileModel({ RegConfig.loadDefaultProfile () with ProfileName = Seq.head seqNextName })
+
+ observableProfiles.Add profile
+
+ x.RaisePropertyChanged "Profiles"
+ x.SelectedProfile <- Some profile))
+
+ member x.DeleteProfile =
+ new RelayCommand(
+ (fun _ -> selectedProfile.IsSome),
+ (fun _ ->
+ x.SelectedProfile
+ |> Option.iter (fun profile ->
+ if MainViewUtil.pushDeleteProfileDialog profile then
+ try
+ if profile.Config.ProfileKeyName <> "" then
+ RegConfig.removeProfile profile.Config
+
+ if observableProfiles.Count <= 1 then
+ x.SelectedProfile <- None
+ observableProfiles.Clear()
+ else
+ x.SelectedProfile <- Some(observableProfiles.Item(observableProfiles.Count - 1))
+ x.UpdateLaunchUI()
+ x.UpdateProfileButtons()
+
+ observableProfiles.Remove profile |> ignore
+ with e -> ViewModelUtil.pushDialog e.Message)))
+
+ member x.DoRemoveSnapshots(mainWin: Window) =
+ x.SelectedProfile
+ |> Option.iter (fun profile -> MainViewUtil.pushRemoveSnapshotsDialog mainWin profile)
+
+ member x.RemoveSnapshots =
+ new RelayCommand(
+ (fun _ -> x.HasSnapshots),
+ (fun mainWin ->
+ match ViewModelUtil.asWindow mainWin with
+ | Some w -> x.DoRemoveSnapshots w
+ | None -> ()))
+
+ member x.CreateMod =
+ new RelayCommand(
+ (fun _ -> x.HasSnapshots),
+ (fun mainWin ->
+ match ViewModelUtil.asWindow mainWin with
+ | None -> ()
+ | Some w ->
+ x.SelectedProfile
+ |> Option.iter (fun profile ->
+ let view, vm = MainViewUtil.makeCreateModDialog w profile
+ vm.RemoveSnapshotsFn <- (fun _ -> x.DoRemoveSnapshots w)
+ MainViewUtil.showDialog view w)))
+
+ member x.OpenMods =
+ new RelayCommand(
+ (fun _ -> x.HasModFiles),
+ (fun _ -> x.SelectedProfile |> Option.iter MainViewUtil.openModsDir))
+
+ member x.SetupBlender =
+ new RelayCommand(
+ (fun _ -> true),
+ (fun mainWin ->
+ match ViewModelUtil.asWindow mainWin with
+ | None -> ()
+ | Some w ->
+ let view, _ = MainViewUtil.makeBlenderWindow w
+ MainViewUtil.showDialog view w))
+
+ member x.OpenPreferences =
+ new RelayCommand(
+ (fun _ -> true),
+ (fun mainWin ->
+ match ViewModelUtil.asWindow mainWin with
+ | None -> ()
+ | Some w ->
+ let view, _ = MainViewUtil.makePreferencesWindow w
+ MainViewUtil.showDialog view w))
+
+ member x.OpenGameProfile =
+ new RelayCommand(
+ (fun _ -> true),
+ (fun mainWin ->
+ match ViewModelUtil.asWindow mainWin with
+ | None -> ()
+ | Some w ->
+ let view, vm = MainViewUtil.makeGameProfileWindow w
+
+ if x.SelectedProfile.IsSome then
+ vm.Profile <- x.SelectedProfile.Value.GameProfile
+
+ vm.ProfileChangedCb <-
+ (fun gameProfile ->
+ x.SelectedProfile
+ |> Option.iter (fun profile -> profile.GameProfile <- gameProfile))
+
+ MainViewUtil.showDialog view w))
+
+ member private x.promptCopy(mainWin: Window, debugMode: bool, selectedProfile: ProfileModel) =
+ let debugText = """# DebugMode file created by MMLaunch on $DATE for $GAME
+
+# This file will be overwritten by MMLaunch whenever "Start(Debug)" is clicked.
+# It will be removed by MMLaunch whenever "Start" is clicked.
+# if you want to preserve this file make it read-only.
+
+# When this file exists, ModelMod will start in "DebugMode" which slows
+# its initialization and reports extra info in the log file,
+# to help catch errors. Please include the log file in any bugs
+# you report, especially if it relates to a crash or hang.
+# The following settings are available and can be set to
+# zero or one. When all these are
+# zero it is equivalent to running without DebugMode, but still runs
+# somewhat more slowly. For bug reports please ensure you
+# have these all set to 1.
+
+protect_mem=1
+defer_rehook=1
+defer_draw_hook=1
+add_ref_context=1
+add_ref_device=1
+"""
+
+ let res = ProcessUtil.preStartCopy selectedProfile.ExePath
+
+ let showMessage (msg: string) =
+ let view, vm = MainViewUtil.makeConfirmDialog mainWin
+ vm.CheckBoxText <- ""
+ vm.Text <- msg
+ MainViewUtil.showDialog view mainWin
+
+ match res with
+ | Ok ProcessUtil.PreStartCopyResult.Copied ->
+ let root = ProcessUtil.getMMRoot ()
+ let dmFile = Path.Combine(root, "DebugMode.txt")
+
+ try
+ let debugText =
+ debugText
+ .Replace(("$DATE": string), DateTime.Now.ToString())
+ .Replace("$GAME", selectedProfile.ExePath)
+
+ if debugMode then File.WriteAllText(dmFile, debugText)
+ elif File.Exists dmFile then File.Delete dmFile
+ with _ -> ()
+
+ match ProcessUtil.launch selectedProfile.ExePath with
+ | Ok _ -> ()
+ | Err e -> showMessage ("Start failed: " + e.Message)
+ | Ok ProcessUtil.PreStartCopyResult.UnknownExe ->
+ let binPath = Path.Combine(ProcessUtil.getMMRoot (), "Bin")
+ showMessage (sprintf LocStrings.Misc.StartCopy binPath binPath)
+ | Err e -> showMessage ("Start failed: " + e.Message)
+
+ member x.StartInSnapshotMode =
+ new RelayCommand(
+ (fun _ -> x.ProfileAreaVisible && x.LoaderIsStartable),
+ (fun mainWin ->
+ match ViewModelUtil.asWindow mainWin with
+ | None -> ()
+ | Some w ->
+ x.SelectedProfile
+ |> Option.iter (fun prof -> x.promptCopy(w, false, prof))))
+
+ member x.StartInDebugMode =
+ new RelayCommand(
+ (fun _ -> x.ProfileAreaVisible && x.LoaderIsStartable),
+ (fun mainWin ->
+ match ViewModelUtil.asWindow mainWin with
+ | None -> ()
+ | Some w ->
+ x.SelectedProfile
+ |> Option.iter (fun prof -> x.promptCopy(w, true, prof))))
+
+ member x.ViewInjectionLog =
+ new RelayCommand(
+ (fun _ -> x.ProfileAreaVisible && x.LoaderIsStartable),
+ (fun _ ->
+ x.SelectedProfile
+ |> Option.iter (fun profile ->
+ match ProcessUtil.openInjectionLog profile.ExePath with
+ | Ok _ -> ()
+ | Err e -> MainViewUtil.failValidation e.Message)))
+
+ member x.ViewModelModLog =
+ new RelayCommand(
+ (fun _ -> x.ProfileAreaVisible),
+ (fun _ ->
+ x.SelectedProfile
+ |> Option.iter (fun profile ->
+ match ProcessUtil.openModelModLog profile.ExePath with
+ | Ok _ -> ()
+ | Err e -> MainViewUtil.failValidation e.Message)))
+
+type MainWindow() as this =
+ inherit Window()
+
+ do
+ AvaloniaXamlLoader.Load(this)
+ this.DataContext <- MainViewModel()
diff --git a/MMLaunch/MainWindow.xaml b/MMLaunch/MainWindow.xaml
deleted file mode 100644
index 523c4de7..00000000
--- a/MMLaunch/MainWindow.xaml
+++ /dev/null
@@ -1,107 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/MMLaunch/MainWindow.xaml.fs b/MMLaunch/MainWindow.xaml.fs
deleted file mode 100644
index 508e196f..00000000
--- a/MMLaunch/MainWindow.xaml.fs
+++ /dev/null
@@ -1,920 +0,0 @@
-// ModelMod: 3d data snapshotting & substitution program.
-// Copyright(C) 2015,2016 John Quigley
-
-// This program is free software : you can redistribute it and / or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 2.1 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU Lesser General Public License
-// along with this program.If not, see .
-
-namespace MMLaunch
-
-open System
-open System.Diagnostics
-open System.Threading
-open System.Windows
-open System.Windows.Threading
-open System.IO
-open FSharp.ViewModule
-open FSharp.ViewModule.Validation
-open System.Windows.Controls
-open System.Windows.Input
-open System.ComponentModel
-open System.Collections.ObjectModel
-open Microsoft.Win32
-
-open Microsoft.VisualBasic.FileIO // for recycling bin thing
-
-open FsXaml
-
-open ViewModelUtil
-open ModelMod
-
-type MainView = XAML<"MainWindow.xaml", true>
-
-// Helper module for using parameterized loc strings with failwith, etc
-// (since we can't pass them directly as an argument):
-// http://stackoverflow.com/questions/18551851/why-does-fs-printfn-work-with-literal-strings-but-not-values-of-type-string
-module Formatters =
- type String = Printf.StringFormat unit>
- type StringRetString = Printf.StringFormat string>
- type StringAnyRetString<'a> = Printf.StringFormat 'a -> string>
- type AnyRetString<'a> = Printf.StringFormat<'a -> string>
-
-module LocStrings =
- // Ideally these strings would actually be localized someday using whatever method.
- module Input =
- let Header = "Input:"
- let Desc1 = "Press CONTROL followed by the following keys."
- let Desc2 = "There is no in-game UI that displays these, so try alt-tab if you forget them."
- let Reload = "Load (or reload) modelmod managed code, configuration, and mods"
- let ReloadMods = "Load (or reload) mods only"
- let Toggle = "Toggle mod display"
- let ClearTex = "Clear the active texture list (will be rebuilt from scene textures)"
- let SelectNextTex = "Select Previous Texture"
- let SelectPrevTex = "Select Next Texture"
- let DoSnapshot = "Take snapshot of current selection"
-
- module Snapshot =
- let Header = "Snapshot Profile:"
- let Desc1 = "The following profile settings will be applied"
- let PosLabel = "Position: "
- let UVLabel = "UV: "
-
- module Errors =
- let ProfileNameRequired = "Profile name may not be empty"
- let NoFilesInDir = Formatters.AnyRetString("No files in %s")
- let SnapshotDirNotFound = Formatters.StringRetString("Snapshot directory does not exist: %s")
- let CantFindLoader = Formatters.String("Unable to find loader path; check that %s is built for this configuration")
- let LoaderStartFailed = Formatters.StringAnyRetString("Start Failed: %s (target: %s)")
- let LoaderUnknownExit = Formatters.AnyRetString("Unknown (code: %d)")
- let NoInputDescription = "No Input description available"
- let NoSnapshotDescription = "No Snapshot description available"
- let BadExePath = "Cannot set exe path; it is already used by another profile"
- let NoCRuntime = "The Visual C++ Runtime was not detected; ModelMod cannot work without it. Would you open a browser to Microsoft's web site to download it?"
- let NoCRuntimeRestart = "Please install the visual C++ runtime and then restart ModelMod."
-
- module Misc =
- let NewProfileName = "New Profile"
- let ConfirmRecycle = Formatters.StringAnyRetString("Remove files in %s?\n%A")
- let ConfirmProfileRemove = Formatters.AnyRetString("Remove profile '%s'?\n\nNote: mod & snapshot files that are associated with this profile will not be removed.")
- let RecycleMoar = Formatters.StringAnyRetString("%s\n...and %d more")
- let RecycleSlow = "Use Recycle Bin (can be slow,blocks UI)"
- let LoaderNotStarted = "Not Started"
- let LoaderStartPending = "Start Pending..."
- let LoaderStopped = Formatters.StringAnyRetString("Exited with status: %s (target: %s)")
- let LoaderStarted = Formatters.AnyRetString("Started; waiting for exit (target: %s)")
- let ExeFilesFilter = "Executable files (*.exe)"
- let StartCopy =
- Formatters.StringAnyRetString(
- "This version of ModelMod does not know how to start this game. " +
- "Therefore ModelMod probably doesn't work with it.\n\nIf you want to try manually, " +
- "first figure out whether the game is 32 or 64 bit and whether it uses d3d9 or 11." +
- "\nThen you copy d3d9.dll or d3d11.dll into the game's executable directory, and then start the game manually." +
- "\nThe destination may be the game's directory or a subdirectory (like 'bin64')." +
- "\nIf the game is 32 bit, copy %s\modelmod_32\d3d9.dll. (or d3d11.dll)" +
- "\nIf the game is 64 bit, copy %s\modelmod_64\d3d9.dll. (or d3d11.dll)" +
- "\nIf you don't know where to copy the file, it is the same location Reshade would use." +
- "\nRemove d3d9.dll (or d3d11.dll) from the game's directory to stop using ModelMod.")
-
-module ProfileText =
- module Input =
- let CommandOrder = [
- LocStrings.Input.ReloadMods; LocStrings.Input.Toggle;
- LocStrings.Input.ClearTex;
- LocStrings.Input.SelectNextTex; LocStrings.Input.SelectPrevTex; LocStrings.Input.DoSnapshot
- LocStrings.Input.Reload]
- let PunctKeys = [@"\"; "]";
- ";";
- ","; "."; "/"; "-"]
- let FKeys = ["F1"; "F2";
- "F6";
- "F3"; "F4"; "F7"; "F10"]
-
- let Descriptions =
- let makeInputDesc keys =
- List.fold2 (fun acc key text ->
- acc + (sprintf "%s\t%s\n" key text)
- ) "" keys CommandOrder
-
- Map.ofList [ (InputProfiles.PunctRock, (makeInputDesc PunctKeys));
- (InputProfiles.FItUp, (makeInputDesc FKeys)); ]
-
-// Mutable wrapper around an immutable RunConfig; there are ways we could use RunConfig
-// directly, but we can also use this to store things that the run config won't
-// have, like logs and lists of mods.
-type ProfileModel(config:CoreTypes.RunConfig) =
- let mutable config = config
-
- // set defaults for empty profile values
- let mutable config =
- if config.SnapshotProfile.Trim() = ""
- then { config with SnapshotProfile = SnapshotProfile.DefaultProfileName } else config
- let mutable config =
- if config.InputProfile.Trim() = "" || not (InputProfiles.isValid config.InputProfile)
- then { config with InputProfile = InputProfiles.DefaultProfile} else config
-
- let save() =
- try
- RegConfig.saveProfile config
- with
- | e -> ViewModelUtil.pushDialog (sprintf "%s" e.Message)
-
- let mutable iconSource = null
-
- do
- if File.Exists (config.ExePath) then
- use icon = System.Drawing.Icon.ExtractAssociatedIcon(config.ExePath)
- iconSource <-
- System.Windows.Interop.Imaging.CreateBitmapSourceFromHIcon(
- icon.Handle,
- System.Windows.Int32Rect.Empty,
- System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions())
-
- member x.Config = config
-
- member x.Icon
- with get() = iconSource
-
- member x.ProfileKeyName
- with get() = config.ProfileKeyName
-
- member x.Name
- with get() = config.ProfileName
- and set (value:string) =
- let value = value.Trim()
- if value = "" then
- ViewModelUtil.pushDialog LocStrings.Errors.ProfileNameRequired
- else
- config <- {config with ProfileName = value }
- save()
-
- member x.ExePath
- with get() = config.ExePath
- and set value =
- config <- { config with ExePath = value }
- save()
-
- member x.InputProfile
- with get() = config.InputProfile
- and set value =
- config <- { config with InputProfile = value }
- save()
-
- member x.SnapshotProfile
- with get() = config.SnapshotProfile
- and set value =
- config <- { config with SnapshotProfile = value }
- save()
-
- member x.LaunchWindow
- with get() = config.LaunchWindow
- and set value =
- config <- { config with LaunchWindow = value }
- save()
-
- member x.LoadModsOnStart
- with get() = config.LoadModsOnStart
- and set value =
- config <- { config with LoadModsOnStart = value }
- save()
-
- member x.GameProfile
- with get() = config.GameProfile
- and set value =
- config <- { config with GameProfile = value }
- save()
-
-module MainViewUtil =
- let pushSelectExecutableDialog(currentExe:string option) =
- let initialDir =
- match currentExe with
- | None -> None
- | Some exe when File.Exists(exe) -> Some(Directory.GetParent(exe).ToString())
- | Some exe -> None
-
- ViewModelUtil.pushSelectFileDialog (initialDir,LocStrings.Misc.ExeFilesFilter + "|*.exe")
-
- let getDirLocator (profile:ProfileModel) =
- let root = ProcessUtil.getMMRoot()
- let dl = State.DirLocator(root, profile.Config)
- dl
-
- let getSnapshotDir (profile:ProfileModel) =
- Path.GetFullPath(getDirLocator(profile).ExeSnapshotDir)
-
- let getDataDir (profile:ProfileModel) =
- Path.GetFullPath(getDirLocator(profile).ExeDataDir)
-
- let pushDeleteProfileDialog (profile:ProfileModel) =
- let msg = sprintf LocStrings.Misc.ConfirmProfileRemove profile.Name
- match (ViewModelUtil.pushOkCancelDialog msg) with
- | MessageBoxResult.Yes -> true
- | _ -> false
-
- let makeBlenderWindow (parentWin:Window) =
- let view = new BlenderView()
- view.Root.Owner <- parentWin
- let vm = view.Root.DataContext :?> BlenderViewModel
- (view,vm)
-
- let makePreferencesWindow (parentWin:Window) =
- let view = new PreferencesView()
- view.Root.Owner <- parentWin
- let vm = view.Root.DataContext :?> PreferencesViewModel
- (view,vm)
-
- let makeGameProfileWindow (parentWin:Window) =
- let view = new GameProfileView()
- view.Root.Owner <- parentWin
- let vm = view.Root.DataContext :?> GameProfileViewModel
- (view,vm)
-
- let makeConfirmDialog (parentWin:Window) =
- let view = new ConfirmDialogView()
- view.Root.Owner <- parentWin
- let vm = view.Root.DataContext :?> ConfirmDialogViewModel
- vm.View <- view
- (view,vm)
-
- let pushRemoveSnapshotsDialog (mainWin:Window) (profile:ProfileModel) =
- let snapdir = getSnapshotDir profile
- if not (Directory.Exists snapdir) then
- ViewModelUtil.pushDialog (sprintf LocStrings.Errors.SnapshotDirNotFound snapdir)
- else
- let files = Directory.GetFiles(snapdir);
-
- let (ok,recycle) =
- let display = 5
- let take = Math.Min(files.Length,display)
- let moar = files.Length - take
-
- if files.Length > 0 then
- let files = files |> Seq.take take |> Array.ofSeq
-
- let msg = sprintf LocStrings.Misc.ConfirmRecycle snapdir files
- let msg = if moar = 0 then msg else (sprintf LocStrings.Misc.RecycleMoar msg moar)
- let view,vm = makeConfirmDialog mainWin
- vm.CheckBoxText <- LocStrings.Misc.RecycleSlow
- vm.Text <- msg
- vm.CheckboxChecked <- RegConfig.getGlobalValue RegKeys.RecycleSnapshots 1 |> (fun v -> v :?> int) |> RegUtil.dwordAsBool
- view.Root.ShowDialog() |> ignore
- vm.Confirmed,vm.CheckboxChecked
- else
- ViewModelUtil.pushDialog (sprintf LocStrings.Errors.NoFilesInDir snapdir)
- false,true
- if ok then
- // save the recycle pref
- RegConfig.setGlobalValue RegKeys.RecycleSnapshots (recycle |> RegUtil.boolAsDword) |> ignore
-
- if recycle then
- // invoke the power of VB!
- // dunno why this is sofa king slow, but probably we should be pushing a progress dialog or doing it async.
- // (if we do it async, and the user is on the create mods dialog, the user will get to watch the
- // snapshot list slowly drain away, which could be useful, or annoying)
- for f in files do
- FileSystem.DeleteFile(f, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin)
- else
- // smoke 'em
- for f in files do
- File.Delete(f)
-
- let makeCreateModDialog (parentWin:Window) (profile:ProfileModel) =
- let cw = new CreateModView()
-
- // put some stuff in its viewmodel
- let vm = cw.Root.DataContext :?> CreateModViewModel
- vm.SnapshotDir <- getSnapshotDir(profile)
- vm.DataDir <- getDataDir(profile)
- let modNameTB = cw.Root.FindName("ModName") :?> TextBox
- let previewHost = cw.Root.FindName("ModelPreview") :?> PreviewHost
- vm.PreviewHost <- Some(previewHost)
- vm.ModNameTB <- Some(modNameTB)
-
- cw.Root.Owner <- parentWin
- (cw,vm)
-
- let failValidation (msg:string) = ViewModelUtil.pushDialog msg
-
- let profileDirHasFiles (p:ProfileModel) (dirSelector: ProfileModel -> string) =
- // dir locator may throw exception if data dir does not exist
- try
- if p.ExePath = ""
- then false
- else
- let sd = dirSelector p
- if not (Directory.Exists sd)
- then false
- else Directory.EnumerateFileSystemEntries(sd).GetEnumerator().MoveNext()
- with
- | e -> false
-
- let openModsDir (p:ProfileModel) =
- let hasfiles = profileDirHasFiles p getDataDir
- if hasfiles then
- let proc = new Process()
- proc.StartInfo.UseShellExecute <- true
- proc.StartInfo.FileName <- getDataDir p
- proc.Start() |> ignore
-
- // utility method to faciliate various hacks with the Profiles list box
- let findProfilesListBox (mainWin:obj):ListBox option =
- let mainWin = mainWin :?> Window
- match mainWin with
- | null -> None
- | win ->
- let lb = win.FindName("ProfilesListBox") :?> System.Windows.Controls.ListBox
- match lb with
- | null -> None
- | _ -> Some(lb)
-
-/// Used for Snapshot and Input profiles, since they both basically just have a name
-/// and description as far as the UI is concerned.
-type SubProfileModel(name:string) =
- member x.Name with get() = name
-
-type LaunchWindowModel(name:string,time:int) =
- member x.Name with get() = name
- member x.Time with get() = time
-
-type GameExePath = string
-type LoaderState =
- NotStarted
- | StartPending of (GameExePath)
- | StartFailed of (Exception * GameExePath)
- | Started of (Process * GameExePath)
- | Stopped of (Process * GameExePath)
-
-type ProfileModelConverter() =
- inherit Converter(
- (fun value cparams ->
- match value with
- | None -> null
- | Some p -> box p),
- ProfileModel(CoreTypes.DefaultRunConfig),
- (fun value cparams ->
- match value with
- | null -> None
- | p -> Some(p :?> ProfileModel)),
- None)
-
-#nowarn "40"
-type MainViewModel() as self =
- inherit ViewModelBase()
-
- let emptyProfile = ProfileModel(CoreTypes.DefaultRunConfig)
-
- let mutable selectedProfile:ProfileModel option = None
- let mutable loaderState = NotStarted
-
- do
- RegConfig.init() // reg config requires init to set hive root
-
- let snapshotProfileDefs,snapshotProfileNames =
- try
- let defs = SnapshotProfile.GetAll (ProcessUtil.getMMRoot())
- let names = defs |> Map.toList |> List.map fst
- defs,names
- with
- | e -> Map.ofList [],[]
-
- let observableProfiles =
- new ObservableCollection(
- RegConfig.loadAll()
- |> Array.sortBy (fun gp -> gp.ProfileName.ToLowerInvariant().Trim())
- |> Array.fold (fun (acc: ResizeArray) rc -> acc.Add( ProfileModel(rc)); acc ) (new ResizeArray()))
-
- let timer = new DispatcherTimer()
- do
- timer.Interval <- new TimeSpan(0,0,1)
- timer.Tick.Add(fun (args) -> self.PeriodicUpdate())
- timer.Start()
-
- let launchWindows = [new LaunchWindowModel("5 Seconds", 5); new LaunchWindowModel("15 Seconds", 15); new LaunchWindowModel("30 Seconds", 30); new LaunchWindowModel("45 Seconds", 45);]
-
- let getSelectedProfileField (getter:ProfileModel -> 'a) (devVal:'a) =
- match selectedProfile with
- | None -> devVal
- | Some profile -> getter profile
- let setSelectedProfileField (setter:ProfileModel -> unit) =
- match selectedProfile with
- | None -> ()
- | Some profile -> setter profile
-
- member x.PeriodicUpdate() =
-
- // If the registry mmroot does not match the current load path, reset it.
- // catch this exception since in will get thrown in the VS designer
- // view for this form.
- try
- let currentRoot = ProcessUtil.getMMRoot()
- let regRoot = RegConfig.getMMRoot()
- if currentRoot <> regRoot then
- RegConfig.setMMRoot currentRoot |> ignore
- with
- | e -> ()
-
- x.UpdateLoaderState <|
- match loaderState with
- | NotStarted
- | StartPending(_)
- | StartFailed (_) -> loaderState
- | Stopped (proc,exe) -> loaderState
- | Started (proc,exe) ->
- if proc.HasExited
- then
- try
- File.Delete(Path.Combine(Path.GetDirectoryName(exe), @"ModelModCLRAppDomain.dll"))
- with
- | e -> ()
-
- Stopped (proc,exe)
- else loaderState
-
- x.UpdateProfileButtons()
-
- member x.LoaderStateText
- with get() =
- match loaderState with
- | NotStarted -> LocStrings.Misc.LoaderNotStarted
- | StartPending(_) -> LocStrings.Misc.LoaderStartPending
- | StartFailed (e,exe) -> sprintf LocStrings.Errors.LoaderStartFailed e.Message exe
- | Stopped (proc,exe) ->
- let exitReason =
- ProcessUtil.getLoaderExitReason proc (sprintf LocStrings.Errors.LoaderUnknownExit proc.ExitCode)
-
- sprintf LocStrings.Misc.LoaderStopped exitReason exe
- | Started (_,exe) -> sprintf LocStrings.Misc.LoaderStarted exe
-
- member x.Profiles =
- if ViewModelUtil.DesignMode then
- new ObservableCollection([||])
- else
- observableProfiles
-
- member x.SnapshotProfiles =
- new ObservableCollection
- (snapshotProfileNames |> List.map (fun p -> SubProfileModel(p)))
-
- member x.InputProfiles =
- new ObservableCollection
- (InputProfiles.ValidProfiles |> List.map (fun p -> SubProfileModel(p)))
-
- member x.LaunchWindows =
- new ObservableCollection(launchWindows)
-
- member x.SelectedProfile
- with get () = selectedProfile
- and set value =
- selectedProfile <- value
-
- x.RaisePropertyChanged("DeleteProfile")
- x.RaisePropertyChanged("SelectedProfile")
- x.RaisePropertyChanged("SelectedProfileName")
- x.RaisePropertyChanged("SelectedProfileExePath")
- x.RaisePropertyChanged("SelectedProfileLoadModsOnStart")
- x.RaisePropertyChanged("SelectedProfileLaunchWindow")
- x.RaisePropertyChanged("SelectedInputProfile")
- x.RaisePropertyChanged("SelectedSnapshotProfile")
- x.RaisePropertyChanged("ProfileAreaVisibility")
- x.RaisePropertyChanged("ProfileDescription")
- x.UpdateLaunchUI()
- x.UpdateProfileButtons()
-
- // Various boilerplate accessors for the selected profile: I put these in
- // because the XAML field notation (e.g. SelectedProfile.Name) won't
- // trigger the converter on SelectedProfile before looking for .Name,
- // so it fails, because SelectedProfile is an option type.
- // There is probably a better way to do this, since this kinda sucks.
- member x.SelectedProfileName
- with get () = getSelectedProfileField (fun profile -> profile.Name) ""
- and set (value:string) = setSelectedProfileField (fun profile -> profile.Name <- value)
-
- member x.SelectedProfileExePath
- with get () = getSelectedProfileField (fun profile -> profile.ExePath) ""
- and set (value:string) = setSelectedProfileField (fun profile -> profile.ExePath <- value)
-
- member x.SelectedProfileLoadModsOnStart
- with get () = getSelectedProfileField (fun profile -> profile.LoadModsOnStart) CoreTypes.DefaultRunConfig.LoadModsOnStart
- and set (value:bool) = setSelectedProfileField (fun profile -> profile.LoadModsOnStart <- value)
-
- member x.SelectedProfileLaunchWindow
- with get() =
- let time = getSelectedProfileField (fun profile -> profile.LaunchWindow) CoreTypes.DefaultRunConfig.LaunchWindow
- let found = launchWindows |> List.tryFind (fun lt -> lt.Time = time)
- match found with
- | None -> launchWindows.Head.Time
- | Some (lw) -> lw.Time
-
- and set (value:int) =
- setSelectedProfileField (fun profile -> profile.LaunchWindow <- value)
-
- member x.SelectedInputProfile
- with get () = getSelectedProfileField (fun profile -> profile.InputProfile) CoreTypes.DefaultRunConfig.InputProfile
- and set (value:string) =
- setSelectedProfileField (fun profile -> profile.InputProfile <- value)
- x.RaisePropertyChanged("ProfileDescription")
-
- member x.SelectedSnapshotProfile
- with get () = getSelectedProfileField (fun profile -> profile.SnapshotProfile) CoreTypes.DefaultRunConfig.SnapshotProfile
- and set (value:string) =
- setSelectedProfileField (fun profile -> profile.SnapshotProfile <- value)
- x.RaisePropertyChanged("ProfileDescription")
-
- member x.ProfileDescription
- with get () =
- match x.SelectedProfile with
- | None -> ""
- | Some profile ->
- let inputText =
- match (ProfileText.Input.Descriptions |> Map.tryFind profile.InputProfile) with
- | None -> LocStrings.Errors.NoInputDescription
- | Some (text) ->
- LocStrings.Input.Header + "\n" + LocStrings.Input.Desc1 + "\n" + LocStrings.Input.Desc2 + "\n" + text
- let snapshotText =
- let makeStringList (xforms:string list) = String.Join(", ", xforms)
-
- let stext =
- snapshotProfileDefs
- |> Map.tryFind profile.SnapshotProfile
- |> function
- | None -> LocStrings.Errors.NoSnapshotDescription
- | Some profile -> profile.ToString()
-
- LocStrings.Snapshot.Header + "\n" + stext
-
- inputText + "\n" + snapshotText
-
- member x.LauncherProfileIcon
- with get() =
- match loaderState with
- | Started (_,exe)
- | StartPending (exe) ->
- let profile = observableProfiles |> Seq.tryFind (fun p -> p.ExePath = exe)
- match profile with
- | None -> null
- | Some(p) -> p.Icon
- | NotStarted
- | StartFailed (_)
- | Stopped (_) ->
- match x.SelectedProfile with
- | None -> null
- | Some(p) -> p.Icon
-
- member x.ProfileAreaVisibility =
- if DesignMode ||
- selectedProfile.IsSome then
- Visibility.Visible
- else
- Visibility.Hidden
-
- member x.BrowseExe = alwaysExecutable (fun mainWin ->
- selectedProfile |> Option.iter (fun selectedProfile ->
- match MainViewUtil.pushSelectExecutableDialog(Some(selectedProfile.ExePath)) with
- | None -> ()
- | Some (exePath) ->
- // verify that the chosen path is not already claimed by another profile
- let existingProfileKey = RegConfig.findProfilePath exePath
- let ok =
- match existingProfileKey with
- | None -> true
- | Some (key) ->
- key.EndsWith(selectedProfile.ProfileKeyName)
-
- if ok then
- selectedProfile.ExePath <- exePath
- if selectedProfile.Name = "" || selectedProfile.Name.StartsWith(LocStrings.Misc.NewProfileName) then
- selectedProfile.Name <- RegConfig.getDefaultProfileName exePath
-
- // update the name in the list box using this heavy-handed method; raising changed
- // events doesn't seem to be enough
- MainViewUtil.findProfilesListBox mainWin |> Option.iter (fun lb -> lb.Items.Refresh())
-
- // force an update to the selectedProfile to update view model stuff (lame)
- x.SelectedProfile <- x.SelectedProfile
- else
- MainViewUtil.failValidation LocStrings.Errors.BadExePath))
-
-
- member x.UpdateLoaderState(newState) =
- if newState <> loaderState then
- loaderState <- newState
- x.UpdateLaunchUI()
-
- member x.UpdateLaunchUI() =
- x.RaisePropertyChanged("LoaderStateText")
- x.RaisePropertyChanged("LoaderIsStartable")
- x.RaisePropertyChanged("StartInSnapshotMode")
- x.RaisePropertyChanged("StartInDebugMode")
- x.RaisePropertyChanged("LauncherProfileIcon")
- x.RaisePropertyChanged("ViewInjectionLog")
- x.RaisePropertyChanged("ViewModelModLog")
-
- member x.UpdateProfileButtons() =
- x.RaisePropertyChanged("RemoveSnapshots")
- x.RaisePropertyChanged("CreateMod")
- x.RaisePropertyChanged("OpenMods")
-
- member x.HasModFiles
- with get() =
- match x.SelectedProfile with
- | None -> false
- | Some profile -> MainViewUtil.profileDirHasFiles profile (fun prof -> (MainViewUtil.getDataDir prof))
-
- member x.HasSnapshots
- with get() =
- match x.SelectedProfile with
- | None -> false
- | Some profile -> MainViewUtil.profileDirHasFiles profile (fun prof -> (MainViewUtil.getSnapshotDir prof))
-
- member x.LoaderIsStartable
- with get() =
- match loaderState with
- StartPending(_)
- | Started (_) -> false
- | StartFailed (_)
- | Stopped (_)
- | NotStarted -> true
-
- member x.NewProfile =
- new RelayCommand (
- (fun canExecute -> true),
- (fun mainWin ->
- // find temp profile name
- let seqNextName = seq {
- let rec next count =
- let nextName = if count = 0 then LocStrings.Misc.NewProfileName else (sprintf "%s (%d)" LocStrings.Misc.NewProfileName count)
- let found = observableProfiles |> Seq.tryFind (fun pm -> pm.Name = nextName)
- match found with
- | None -> nextName
- | Some v -> next (count+1)
- yield (next 0)
- }
-
- let profile = new ProfileModel( { RegConfig.loadDefaultProfile() with ProfileName = (Seq.head seqNextName) })
- observableProfiles.Add(profile)
-
- x.RaisePropertyChanged("Profiles")
- x.SelectedProfile <- Some(profile)
-
- // force the lb to scroll manually
- MainViewUtil.findProfilesListBox mainWin |> Option.iter (fun lb -> lb.ScrollIntoView(profile))))
-
- member x.DeleteProfile =
- new RelayCommand (
- (fun canExecute -> selectedProfile.IsSome ),
- (fun action ->
- x.SelectedProfile |> Option.iter (fun profile ->
- if (MainViewUtil.pushDeleteProfileDialog profile) then
- try
- if profile.Config.ProfileKeyName <> "" then
- RegConfig.removeProfile profile.Config
-
- if (observableProfiles.Count <= 1) then
- x.SelectedProfile <- None
- observableProfiles.Clear()
- else
- x.SelectedProfile <- Some(observableProfiles.Item(observableProfiles.Count - 1))
- x.UpdateLaunchUI()
- x.UpdateProfileButtons()
-
- observableProfiles.Remove(profile) |> ignore
- with
- | e ->
- ViewModelUtil.pushDialog e.Message)))
-
- member x.DoRemoveSnapshots (mainWin:Window) =
- x.SelectedProfile |> Option.iter (fun profile ->
- MainViewUtil.pushRemoveSnapshotsDialog mainWin profile)
-
- member x.RemoveSnapshots =
- new RelayCommand (
- (fun canExecute -> x.HasSnapshots),
- (fun mainWin -> x.DoRemoveSnapshots(mainWin :?> Window)))
-
- member x.CreateMod =
- new RelayCommand (
- (fun canExecute -> x.HasSnapshots),
- (fun mainWin ->
- let mainWin = mainWin :?> Window
- x.SelectedProfile |> Option.iter (fun profile ->
- let view,vm = MainViewUtil.makeCreateModDialog mainWin profile
- vm.RemoveSnapshotsFn <- ( fun _ -> x.DoRemoveSnapshots(mainWin))
- view.Root.ShowDialog() |> ignore)))
-
- member x.OpenMods =
- new RelayCommand (
- (fun canExecute -> x.HasModFiles),
- (fun action ->
- x.SelectedProfile |> Option.iter MainViewUtil.openModsDir))
-
- member x.SetupBlender =
- new RelayCommand (
- (fun canExecute -> true),
- (fun mainWin ->
- let mainWin = mainWin :?> Window
- let view,_ = MainViewUtil.makeBlenderWindow(mainWin)
- view.Root.ShowDialog() |> ignore
- ))
-
- member x.OpenPreferences =
- new RelayCommand (
- (fun canExecute -> true),
- (fun mainWin ->
- let mainWin = mainWin :?> Window
- let view,_ = MainViewUtil.makePreferencesWindow(mainWin)
- view.Root.ShowDialog() |> ignore
- ))
-
- member x.OpenGameProfile =
- new RelayCommand (
- (fun canExecute -> true),
- (fun mainWin ->
- let mainWin = mainWin :?> Window
- let view,vm = MainViewUtil.makeGameProfileWindow(mainWin)
- if x.SelectedProfile.IsSome then
- vm.Profile <- x.SelectedProfile.Value.GameProfile
-
- vm.ProfileChangedCb <- (fun gameProfile ->
- x.SelectedProfile |> Option.iter (fun profile ->
- profile.GameProfile <- gameProfile
- ))
- view.Root.ShowDialog() |> ignore
- ))
-
- member private x.promptCopy(mainWin, debugMode, (selectedProfile:ProfileModel)) =
- // if we are going to start in debug mode we'll need to create the init file
- // so make a wall of text for it.
- let debugText = """# DebugMode file created by MMLaunch on $DATE for $GAME
-
-# This file will be overwritten by MMLaunch whenever "Start(Debug)" is clicked.
-# It will be removed by MMLaunch whenever "Start" is clicked.
-# if you want to preserve this file make it read-only.
-
-# When this file exists, ModelMod will start in "DebugMode" which slows
-# its initialization and reports extra info in the log file,
-# to help catch errors. Please include the log file in any bugs
-# you report, especially if it relates to a crash or hang.
-# The following settings are available and can be set to
-# zero or one. When all these are
-# zero it is equivalent to running without DebugMode, but still runs
-# somewhat more slowly. For bug reports please ensure you
-# have these all set to 1.
-
-# whether to protect memory using the win32 functions before hooking
-protect_mem=1
-
-# whether to defer function rehook assignments until later in execution
-defer_rehook=1
-
-# whether to defer hooking the draw function. when this is 1 it also
-# delays initialization of managed code and mod loading since those
-# are triggered by the draw function.
-defer_draw_hook=1
-
-# whether to add ref on context after hook
-add_ref_context=1
-
-# whether to add ref on device after hook
-add_ref_device=1
-"""
-
-
- let res = ProcessUtil.preStartCopy (selectedProfile.ExePath)
- match res with
- | Ok(ProcessUtil.PreStartCopyResult.Copied) ->
- let root = ProcessUtil.getMMRoot()
- let dmFile = Path.Combine(root, "DebugMode.txt")
-
- try
- let debugText = debugText.Replace(("$DATE":string), DateTime.Now.ToString()).Replace("$GAME", selectedProfile.ExePath)
-
- if debugMode then
- File.WriteAllText(dmFile, debugText)
- else
- if File.Exists(dmFile) then
- File.Delete(dmFile)
- with
- | e -> ()
- match ProcessUtil.launch (selectedProfile.ExePath) with
- | Ok(proc) -> ()
- | Err(e) ->
- let msg = "Start failed: " + e.Message
- let view,vm = MainViewUtil.makeConfirmDialog mainWin
- vm.CheckBoxText <- ""
- vm.Text <- msg
- view.Root.ShowDialog() |> ignore
- | Ok(ProcessUtil.PreStartCopyResult.UnknownExe) ->
- // don't know how to handle this so tell user about i
- let binPath = Path.Combine(ProcessUtil.getMMRoot(), "Bin")
- let msg = sprintf LocStrings.Misc.StartCopy binPath binPath
- let view,vm = MainViewUtil.makeConfirmDialog mainWin
- vm.CheckBoxText <- ""
- vm.Text <- msg
- view.Root.ShowDialog() |> ignore
- | Err(e) ->
- let msg = "Start failed: " + e.Message
- let view,vm = MainViewUtil.makeConfirmDialog mainWin
- vm.CheckBoxText <- ""
- vm.Text <- msg
- view.Root.ShowDialog() |> ignore
-
- member x.StartInSnapshotMode =
- /// If we can automatically copy in our d3d files and start the exe, do that.
- /// Otherwise display the user prompt telling them how lame we are
- /// (they need to do it manually)
- let promptCopy mainWin (selectedProfile:ProfileModel) =
- x.promptCopy(mainWin, false, selectedProfile)
-
- // old function to start the MMLoader, which isn't used anymore.
- let startMMLoader (x:MainViewModel) (selectedProfile:ProfileModel) =
- x.UpdateLoaderState <|
- match loaderState with
- | Started (proc,exe) ->
- // kill even in stopped case in case poll didn't catch exit
- proc.Kill()
- proc.WaitForExit()
- Stopped(proc,exe)
- | StartPending(_) -> loaderState
- | Stopped (proc,exe) -> loaderState
- | StartFailed (_) -> loaderState
- | NotStarted -> loaderState
-
- x.UpdateLoaderState <| StartPending(selectedProfile.ExePath)
-
- try
- // make sure the data dir exists
- let dir = MainViewUtil.getDirLocator(selectedProfile).QueryBaseDataDir()
- if dir <> "" && not (Directory.Exists dir) then
- Directory.CreateDirectory(dir) |> ignore
-
- let dir = MainViewUtil.getDataDir selectedProfile
- if dir <> "" && not (Directory.Exists dir) then
- Directory.CreateDirectory(dir) |> ignore
-
- let launchWindow = selectedProfile.LaunchWindow
-
- // start it
- x.UpdateLoaderState <|
- match (ProcessUtil.launchWithLoader selectedProfile.ExePath selectedProfile.GameProfile.CommandLineArguments launchWindow) with
- | Ok(p) -> Started(p,selectedProfile.ExePath)
- | Err(e) ->
- MainViewUtil.failValidation e.Message
- StartFailed(e,selectedProfile.ExePath)
- with
- | e -> MainViewUtil.failValidation (sprintf "Failed to start: %s" e.Message)
-
- new RelayCommand (
- (fun canExecute -> x.ProfileAreaVisibility = Visibility.Visible && x.LoaderIsStartable),
- //(fun action -> x.SelectedProfile |> Option.iter (startMMLoader x)))
- (fun mainWin -> x.SelectedProfile |> Option.iter (promptCopy (mainWin :?> Window))))
-
- member x.StartInDebugMode =
- new RelayCommand (
- (fun canExecute -> x.ProfileAreaVisibility = Visibility.Visible && x.LoaderIsStartable),
- //(fun action -> x.SelectedProfile |> Option.iter (startMMLoader x)))
- (fun mainWin -> x.SelectedProfile |> Option.iter (fun prof -> x.promptCopy (mainWin :?> Window, true, prof) )))
-
- member x.ViewInjectionLog =
- new RelayCommand (
- (fun canExecute -> x.ProfileAreaVisibility = Visibility.Visible && x.LoaderIsStartable),
- (fun action ->
- x.SelectedProfile |> Option.iter (fun profile ->
- match ProcessUtil.openInjectionLog profile.ExePath with
- | Ok(_) -> ()
- | Err(e) -> MainViewUtil.failValidation e.Message)))
- member x.ViewModelModLog =
- new RelayCommand (
- (fun canExecute -> x.ProfileAreaVisibility = Visibility.Visible),
- (fun action ->
- x.SelectedProfile |> Option.iter (fun profile ->
- match ProcessUtil.openModelModLog profile.ExePath with
- | Ok(_) -> ()
- | Err(e) -> MainViewUtil.failValidation e.Message)))
\ No newline at end of file
diff --git a/MMLaunch/MeshPreviewControl.fs b/MMLaunch/MeshPreviewControl.fs
new file mode 100644
index 00000000..db081760
--- /dev/null
+++ b/MMLaunch/MeshPreviewControl.fs
@@ -0,0 +1,214 @@
+// Avalonia mesh preview control. Replaces the old WPF PreviewHost which
+// embedded a MonoGame D3D11 surface and instantiated MMView's MeshViewControl.
+//
+// To keep the launcher self-contained and free of the .NET-Framework-only
+// MonoGame/SharpDX dependencies that MMView pulls in, this is a small Skia
+// based wireframe renderer: it parses the .obj/.mmobj geometry inline,
+// projects each triangle through a fixed orbit camera, and draws the edges
+// using Avalonia's DrawingContext. Mouse drag rotates, mouse wheel zooms.
+
+namespace MMLaunch
+
+open System
+open System.IO
+open System.Numerics
+
+open Avalonia
+open Avalonia.Controls
+open Avalonia.Input
+open Avalonia.Media
+
+module private MmObj =
+ type Mesh = {
+ Positions: Vector3[]
+ Triangles: (int * int * int)[]
+ }
+
+ let private tryParseFloat (s: string) =
+ Single.TryParse(s, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture)
+
+ let private parseVec3 (parts: string[]) =
+ if parts.Length < 4 then None
+ else
+ let ok1, x = tryParseFloat parts.[1]
+ let ok2, y = tryParseFloat parts.[2]
+ let ok3, z = tryParseFloat parts.[3]
+ if ok1 && ok2 && ok3 then Some(Vector3(x, y, z)) else None
+
+ /// Returns the 1-based position index from a face vertex token like
+ /// "12", "12/4", "12/4/7", or "12//7". Negative indices (relative to
+ /// end of list) are also handled.
+ let private parseFaceIdx (token: string) (positionsCount: int) : int option =
+ let posStr =
+ match token.IndexOf('/') with
+ | -1 -> token
+ | i -> token.Substring(0, i)
+ match Int32.TryParse(posStr) with
+ | true, v when v > 0 -> Some(v - 1)
+ | true, v when v < 0 -> Some(positionsCount + v)
+ | _ -> None
+
+ let load (path: string) : Mesh =
+ let positions = ResizeArray()
+ let triangles = ResizeArray()
+
+ if File.Exists path then
+ let lines = File.ReadAllLines path
+ for line in lines do
+ let trimmed = line.Trim()
+ if trimmed.Length > 0 && not (trimmed.StartsWith "#") then
+ let parts = trimmed.Split([| ' '; '\t' |], StringSplitOptions.RemoveEmptyEntries)
+ if parts.Length > 0 then
+ match parts.[0] with
+ | "v" ->
+ match parseVec3 parts with
+ | Some v -> positions.Add(v)
+ | None -> ()
+ | "f" when parts.Length >= 4 ->
+ // Triangulate: fan from vertex 1
+ let idxs =
+ [| for i in 1 .. parts.Length - 1 -> parseFaceIdx parts.[i] positions.Count |]
+ if idxs |> Array.forall Option.isSome then
+ let resolved = idxs |> Array.map Option.get
+ for k in 1 .. resolved.Length - 2 do
+ triangles.Add((resolved.[0], resolved.[k], resolved.[k + 1]))
+ | _ -> ()
+
+ { Positions = positions.ToArray(); Triangles = triangles.ToArray() }
+
+ let bounds (m: Mesh) : Vector3 * Vector3 =
+ if m.Positions.Length = 0 then
+ Vector3.Zero, Vector3.One
+ else
+ let mutable lo = m.Positions.[0]
+ let mutable hi = m.Positions.[0]
+ for v in m.Positions do
+ lo <- Vector3.Min(lo, v)
+ hi <- Vector3.Max(hi, v)
+ lo, hi
+
+[] // FindControl returns null if not present; allow Option round-trip
+type MeshPreviewControl() as this =
+ inherit Control()
+
+ let mutable mesh: MmObj.Mesh = { Positions = [||]; Triangles = [||] }
+ let mutable selectedFile: string = ""
+
+ // Orbit camera state
+ let mutable yaw: float32 = 0.5f
+ let mutable pitch: float32 = -0.3f
+ let mutable distance: float32 = 3.5f
+ let mutable center: Vector3 = Vector3.Zero
+ let mutable modelScale: float32 = 1.0f
+
+ let mutable lastPointer: Point option = None
+
+ let strokeBrush: IBrush = SolidColorBrush(Color.FromRgb(60uy, 200uy, 255uy)) :> IBrush
+ let bgBrush: IBrush = SolidColorBrush(Color.FromRgb(28uy, 28uy, 32uy)) :> IBrush
+ let pen = Pen(SolidColorBrush(Color.FromRgb(60uy, 200uy, 255uy)), 1.0)
+
+ let recomputeFraming () =
+ let lo, hi = MmObj.bounds mesh
+ center <- (lo + hi) * 0.5f
+ let extent = hi - lo
+ let maxExtent = max (max extent.X extent.Y) extent.Z
+ modelScale <- if maxExtent > 0.0001f then 2.0f / maxExtent else 1.0f
+ distance <- 3.5f
+
+ do
+ this.ClipToBounds <- true
+ this.Focusable <- true
+
+ member x.SelectedFile
+ with get() = selectedFile
+ and set (value: string) =
+ selectedFile <- value
+ mesh <-
+ if String.IsNullOrEmpty value || not (File.Exists value) then
+ { Positions = [||]; Triangles = [||] }
+ else
+ MmObj.load value
+ recomputeFraming ()
+ x.InvalidateVisual()
+
+ override x.OnPointerPressed(e) =
+ base.OnPointerPressed(e)
+ lastPointer <- Some(e.GetPosition(x))
+ e.Pointer.Capture(x) |> ignore
+
+ override x.OnPointerMoved(e) =
+ base.OnPointerMoved(e)
+ match lastPointer with
+ | Some prev when e.GetCurrentPoint(x).Properties.IsLeftButtonPressed ->
+ let cur = e.GetPosition(x)
+ let dx = float32 (cur.X - prev.X)
+ let dy = float32 (cur.Y - prev.Y)
+ yaw <- yaw + dx * 0.01f
+ pitch <- pitch + dy * 0.01f
+ pitch <- max -1.5f (min 1.5f pitch)
+ lastPointer <- Some cur
+ x.InvalidateVisual()
+ | _ -> ()
+
+ override x.OnPointerReleased(e) =
+ base.OnPointerReleased(e)
+ lastPointer <- None
+ e.Pointer.Capture(null) |> ignore
+
+ override x.OnPointerWheelChanged(e) =
+ base.OnPointerWheelChanged(e)
+ let factor = if e.Delta.Y > 0.0 then 0.9f else 1.1f
+ distance <- max 0.5f (min 50.0f (distance * factor))
+ x.InvalidateVisual()
+
+ override x.Render(ctx: DrawingContext) =
+ let b = x.Bounds
+ ctx.FillRectangle(bgBrush, Rect(0.0, 0.0, b.Width, b.Height))
+
+ if mesh.Triangles.Length = 0 then
+ let fmt =
+ FormattedText(
+ (if String.IsNullOrEmpty selectedFile then "No mesh selected" else "No geometry"),
+ Globalization.CultureInfo.InvariantCulture,
+ FlowDirection.LeftToRight,
+ Typeface.Default,
+ 12.0,
+ Brushes.Gray)
+ ctx.DrawText(fmt, Point(10.0, 10.0))
+ else
+ let w = float32 b.Width
+ let h = float32 b.Height
+ if w > 1.0f && h > 1.0f then
+ let cosY = MathF.Cos(yaw)
+ let sinY = MathF.Sin(yaw)
+ let cosP = MathF.Cos(pitch)
+ let sinP = MathF.Sin(pitch)
+
+ // Project a model-space vertex into screen-space.
+ let project (v: Vector3) : Point =
+ let p = (v - center) * modelScale
+ // Yaw around Y, then pitch around X.
+ let x1 = p.X * cosY + p.Z * sinY
+ let z1 = -p.X * sinY + p.Z * cosY
+ let y2 = p.Y * cosP - z1 * sinP
+ let z2 = p.Y * sinP + z1 * cosP
+ // Perspective divide; eye at (0, 0, distance) looking toward -Z.
+ let ez = distance - z2
+ let f = 1.5f
+ let sx = if ez > 0.001f then x1 * f / ez else 0.0f
+ let sy = if ez > 0.001f then y2 * f / ez else 0.0f
+ let scale = min w h * 0.45f
+ Point(float (w * 0.5f + sx * scale), float (h * 0.5f - sy * scale))
+
+ let positions = mesh.Positions
+ let projected = Array.map project positions
+
+ for (a, ib, c) in mesh.Triangles do
+ if a >= 0 && ib >= 0 && c >= 0
+ && a < projected.Length && ib < projected.Length && c < projected.Length then
+ let pa = projected.[a]
+ let pb = projected.[ib]
+ let pc = projected.[c]
+ ctx.DrawLine(pen, pa, pb)
+ ctx.DrawLine(pen, pb, pc)
+ ctx.DrawLine(pen, pc, pa)
diff --git a/MMLaunch/ModUtil.fs b/MMLaunch/ModUtil.fs
index 3f70edea..5510b422 100644
--- a/MMLaunch/ModUtil.fs
+++ b/MMLaunch/ModUtil.fs
@@ -22,6 +22,7 @@ open System.IO
open YamlDotNet.RepresentationModel
open YamlDotNet.Serialization
+open ModelMod.ConfigTypes
open ViewModelUtil
/// Helper for DDS files, mostly LLM generated
@@ -193,7 +194,7 @@ module ModUtil =
VertDeclPath: string
ExpectedPrimCount: int
ExpectedVertCount: int
- Profile: ModelMod.CoreTypes.SnapProfile
+ Profile: ModelMod.ConfigTypes.SnapProfile
}
type YamlMod = {
@@ -201,7 +202,7 @@ module ModUtil =
Ref: string
ModType: string // subtype of the mod (gpureplacement, etc)
MeshPath: string
- Profile: ModelMod.CoreTypes.SnapProfile
+ Profile: ModelMod.ConfigTypes.SnapProfile
}
let getOutputPath modRoot modName = Path.GetFullPath(Path.Combine(modRoot, modName))
@@ -361,7 +362,7 @@ mods:"""
let p = sd.Deserialize(f)
p.Profile
else
- ModelMod.SnapshotProfile.EmptyProfile
+ ModelMod.ConfigTypes.EmptySnapProfile
// generate ref yaml
let refYamlFile =
diff --git a/MMLaunch/PreferencesWindow.axaml b/MMLaunch/PreferencesWindow.axaml
new file mode 100644
index 00000000..fb686e90
--- /dev/null
+++ b/MMLaunch/PreferencesWindow.axaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/MMLaunch/PreferencesWindow.axaml.fs b/MMLaunch/PreferencesWindow.axaml.fs
new file mode 100644
index 00000000..4d5c083e
--- /dev/null
+++ b/MMLaunch/PreferencesWindow.axaml.fs
@@ -0,0 +1,37 @@
+namespace MMLaunch
+
+open System.IO
+
+open Avalonia.Controls
+open Avalonia.Markup.Xaml
+
+open ModelMod
+
+type PreferencesViewModel() =
+ inherit ViewModelBase()
+
+ let mutable docRoot = RegConfig.getDocRoot ()
+
+ member x.DocRoot
+ with get () = docRoot
+ and set value =
+ if not (Directory.Exists value) then
+ ViewModelUtil.pushDialog (sprintf "Directory does not exist: %s" value)
+ else
+ docRoot <- value
+ RegConfig.setDocRoot docRoot |> ignore
+ x.RaisePropertyChanged "DocRoot"
+
+ member x.Browse =
+ ViewModelUtil.alwaysExecutable (fun _ ->
+ let initial = if Directory.Exists docRoot then Some docRoot else None
+ match ViewModelUtil.pushSelectFolderDialog initial with
+ | Some path -> x.DocRoot <- path
+ | None -> ())
+
+type PreferencesWindow() as this =
+ inherit Window()
+
+ do
+ AvaloniaXamlLoader.Load(this)
+ this.DataContext <- PreferencesViewModel()
diff --git a/MMLaunch/PreferencesWindow.xaml b/MMLaunch/PreferencesWindow.xaml
deleted file mode 100644
index 679dc341..00000000
--- a/MMLaunch/PreferencesWindow.xaml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/MMLaunch/PreferencesWindow.xaml.fs b/MMLaunch/PreferencesWindow.xaml.fs
deleted file mode 100644
index a60fede3..00000000
--- a/MMLaunch/PreferencesWindow.xaml.fs
+++ /dev/null
@@ -1,64 +0,0 @@
-// ModelMod: 3d data snapshotting & substitution program.
-// Copyright(C) 2015,2016 John Quigley
-
-// This program is free software : you can redistribute it and / or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 2.1 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU Lesser General Public License
-// along with this program.If not, see .
-
-namespace MMLaunch
-
-open System
-open System.Diagnostics
-open System.Threading
-open System.Windows
-open System.Windows.Threading
-open System.IO
-open FSharp.ViewModule
-open FSharp.ViewModule.Validation
-open System.Windows.Input
-open System.ComponentModel
-open System.Collections.ObjectModel
-open Microsoft.Win32
-
-open System.Windows.Forms // just for FolderBrowserDialog !
-open FsXaml
-
-open ViewModelUtil
-open ModelMod
-
-type PreferencesView = XAML<"PreferencesWindow.xaml", true>
-
-type PreferencesViewModel() =
- inherit ViewModelBase()
-
- let mutable docRoot = RegConfig.getDocRoot()
-
- member x.DocRoot
- with get() = docRoot
- and set value =
- if not (Directory.Exists value) then
- ViewModelUtil.pushDialog (sprintf "Directory does not exist: %s" value)
- else
- docRoot <- value
- RegConfig.setDocRoot docRoot |> ignore
- x.RaisePropertyChanged("DocRoot")
-
- member x.Browse = alwaysExecutable (fun action ->
- use fb = new FolderBrowserDialog()
-
- match fb.ShowDialog() with
- | DialogResult.OK ->
- if Directory.Exists fb.SelectedPath then
- x.DocRoot <- fb.SelectedPath
- | _ -> ()
- )
-
\ No newline at end of file
diff --git a/MMLaunch/PreviewHost.fs b/MMLaunch/PreviewHost.fs
deleted file mode 100644
index 91a0890f..00000000
--- a/MMLaunch/PreviewHost.fs
+++ /dev/null
@@ -1,69 +0,0 @@
-// ModelMod: 3d data snapshotting & substitution program.
-// Copyright(C) 2015,2016 John Quigley
-
-// This program is free software : you can redistribute it and / or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 2.1 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU Lesser General Public License
-// along with this program.If not, see .
-
-namespace MMLaunch
-
-open System.IO
-
-open WpfInteropSample
-open ModelMod.StartConf
-open ModelMod.CoreTypes
-
-type PreviewHost() =
- inherit WpfInteropSample.D3D11Host()
-
- let mutable selectedFile:string = ""
- let mutable control:MeshView.Main.MeshViewControl option = None
-
- member x.SelectedFile
- with get() = selectedFile
- and set value =
- selectedFile <- value
- x.Initialize()
-
- override x.Initialize() =
- x.Uninitialize()
-
- if (File.Exists selectedFile) then
- let conf = {
- Conf.ModIndexFile = None
- FilesToLoad = [selectedFile]
- AppSettings =
- Some({
- Window = None
- CamPosition = Some(Vec3F(0.f,3.75f,10.0f))
- MeshReadFlags = { ReadMaterialFile = true; ReverseTransform = false; AdjustBlendWeights = AdjustBlendWeightsDefault }
- })
- Conf.BinCacheDir = ""
- }
- control <- Some(new MeshView.Main.MeshViewControl(conf, x.GraphicsDevice))
- ()
-
- override x.Uninitialize() =
- match control with
- | None -> ()
- | Some ctrl -> (ctrl :> System.IDisposable).Dispose()
- control <- None
-
- override x.Render(time: System.TimeSpan) =
- match control with
- | None -> ()
- | Some (control) ->
-
- let gt = new Microsoft.Xna.Framework.GameTime(System.TimeSpan(0L), time)
- control.Update(gt)
- control.Draw(gt)
- ()
\ No newline at end of file
diff --git a/MMLaunch/ProcessUtil.fs b/MMLaunch/ProcessUtil.fs
index 5e1e3a7e..0404939e 100644
--- a/MMLaunch/ProcessUtil.fs
+++ b/MMLaunch/ProcessUtil.fs
@@ -37,6 +37,8 @@ module ProcessUtil =
let LoaderName = "MMLoader.exe"
+ let mutable lastRoot:string option = None
+
let getMMRoot() =
// MMRoot by convention its where one of the files/directories below lives.
// This value is also stored in the registry (PeriodicUpdate puts it there)
@@ -49,6 +51,8 @@ module ProcessUtil =
]
let rootFiles = [
"MMDotNet.sln"; // for dev runs
+ "TPLib"
+ "Logs"
"Bin"]
let root =
@@ -57,12 +61,19 @@ module ProcessUtil =
rootFiles
|> List.map (fun filepath -> Path.Combine(rootpath, filepath))
|> List.tryPick (fun filepath ->
- eprintfn "try %A" (Path.GetFullPath(filepath))
+ //printfn "try %A" (Path.GetFullPath(filepath))
if (Directory.Exists(filepath) || File.Exists(filepath)) then Some(filepath) else None)
)
- match root with
- | None -> failwithf "Unable to find MM root from working dir %A" (System.Environment.CurrentDirectory)
- | Some(dir) -> Path.GetFullPath(Path.GetDirectoryName(dir))
+
+ let fullRoot =
+ match root with
+ | None -> failwithf "Unable to find MM root from working dir %A" (System.Environment.CurrentDirectory)
+ | Some(dir) -> Path.GetFullPath(Path.GetDirectoryName(dir))
+
+ if lastRoot <> Some(fullRoot) then
+ lastRoot <- Some(fullRoot)
+ printfn "MM root changed: %A" root
+ fullRoot
/// Loader isn't used anymore so this is just a placeholder until I remove the related code.
let getLoaderPath() =
@@ -217,6 +228,22 @@ module ProcessUtil =
let openModelModLog (exePath:string): Result =
getModelModLog exePath |> openTextFile
+ open System.IO
+ open System.Reflection.PortableExecutable
+
+ let getBitness (path: string) =
+ use stream = File.OpenRead(path)
+ use pe = new PEReader(stream)
+ let header = pe.PEHeaders.CoffHeader
+
+ match header.Machine with
+ | Machine.I386 -> (32, "32-bit (x86)")
+ | Machine.Amd64 -> (64, "64-bit (x64)")
+ | Machine.Arm64 -> (64, "64-bit (ARM64)")
+ | Machine.Arm -> (32, "32-bit (ARM)")
+ | Machine.IA64 -> (64, "64-bit (Itanium)")
+ | m -> (0, sprintf "unknown (%A)" m)
+
type PreStartCopyResult =
| Copied
| UnknownExe
@@ -225,7 +252,7 @@ module ProcessUtil =
/// the `Copied` value as a result. Otherwise return `UnknownExe`. If a problem happens
/// return the exception. This function also checks if any existing target d3d dlls
/// belong to modelmod, if not, it does not overwrite them (and returns an exception).
- let preStartCopy(exePath): Result =
+ let preStartCopy(exePath:string): Result =
try
let fileName = Path.GetFileNameWithoutExtension(exePath)
let mmRoot = getMMRoot()
@@ -252,20 +279,57 @@ module ProcessUtil =
failwithf "Can't overwrite file that doesn't belong to modelmod: %A is %A" targFile fvi.FileDescription
toCheck |> List.iter (function | D3D11(somefile) | D3D9(somefile) -> checkIsMM somefile)
+ let (bitness,bitStr) = getBitness exePath
+ if bitness = 0 then
+ failwithf "Can't determine exe type for target: %A (%A)" exePath bitStr
+ let is64 = (bitness = 64)
+ printfn "target %A bitness: %A" exePath bitness
+
// if we get here then one or both paths are ok to overwrite
// source files could be in either of these dirs depending on where we are running from
- let srcBits = if prof.Is64Bit then "modelmod_64" else "modelmod_32"
- let dirs = ["Bin"; "Release"]
- let dirs = dirs |> List.map (fun d -> Path.Combine(mmRoot, d, srcBits))
+ let bitDirs =
+ [
+ if is64 then "modelmod_64" else "modelmod_32";
+ ""
+ ]
+
+ // copy from working dir if we are running from there, otherwise use release package folders
+ let workDirOut = @"Native\target\release"
+ let dirs = [ workDirOut; "Bin"; "Release";]
+ let dirs = bitDirs |> List.map (fun srcBits ->
+ dirs |> List.map (fun d -> Path.Combine(mmRoot, d, srcBits)))
+ |> List.concat
+
let sourceDir = dirs |> List.tryFind(fun d -> Directory.Exists(d))
let sourceDir =
match sourceDir with
| None -> failwithf "Can't find source files for copy (root %A, looked in %A)" mmRoot dirs
| Some(sd) -> sd
- let copyD11 outpath =
+ let copyD11 (outpath:string) =
+
let baseDllName = Path.GetFileName(outpath)
- File.Copy(Path.Combine(sourceDir, baseDllName), outpath, true)
+ let src = Path.Combine(sourceDir, baseDllName)
+ let src =
+ if not (File.Exists src) && src.ToLowerInvariant().Contains(workDirOut.ToLowerInvariant()) then
+
+
+ let hookCore = Path.Combine(sourceDir, "hook_core.dll")
+ if File.Exists(hookCore) then
+ // make sure bitness on hook core matches as it can be 32 or 64
+ let hcBitness = fst (getBitness hookCore)
+ if hcBitness <> bitness then
+ failwithf "built hook_core.dll %A (%d) does not match target bitness %A (%d)"
+ hookCore hcBitness exePath bitness
+ else
+ hookCore
+ else
+ failwithf "cannot find source file for copy: %A" src
+ else
+ src
+
+ printfn "copy %A => %A" src outpath
+ File.Copy(src, outpath, true)
toCheck |> List.iter (function | D3D9(outpath) | D3D11(outpath) -> copyD11 outpath)
Ok(Copied)
diff --git a/MMLaunch/Program.fs b/MMLaunch/Program.fs
new file mode 100644
index 00000000..a5a21ef9
--- /dev/null
+++ b/MMLaunch/Program.fs
@@ -0,0 +1,17 @@
+namespace MMLaunch
+
+open System
+open Avalonia
+
+module Program =
+ let buildAvaloniaApp () : AppBuilder =
+ AppBuilder
+ .Configure()
+ .UsePlatformDetect()
+ .WithInterFont()
+
+ []
+ []
+ let main argv =
+ let builder = buildAvaloniaApp ()
+ builder.StartWithClassicDesktopLifetime(argv)
diff --git a/MMLaunch/Resource.rc b/MMLaunch/Resource.rc
deleted file mode 100644
index 078f50b6..00000000
--- a/MMLaunch/Resource.rc
+++ /dev/null
@@ -1 +0,0 @@
-1 ICON "..\ModelMod.ico"
\ No newline at end of file
diff --git a/MMLaunch/Resource.res b/MMLaunch/Resource.res
deleted file mode 100644
index d28ef74a..00000000
Binary files a/MMLaunch/Resource.res and /dev/null differ
diff --git a/MMLaunch/SnapshotProfile.fs b/MMLaunch/SnapshotProfile.fs
new file mode 100644
index 00000000..707ce1f6
--- /dev/null
+++ b/MMLaunch/SnapshotProfile.fs
@@ -0,0 +1,25 @@
+// Slim SnapshotProfile loader for the launcher (the engine version is in
+// MMManaged/SnapshotProfile.fs and additionally produces interop structs).
+
+namespace ModelMod
+
+open System
+open System.IO
+
+open ConfigTypes
+
+/// Minimal Snapshot.SnapMeta surface used by ModUtil for reading/writing
+/// snapshot meta yaml files. The full Snapshot module in MMManaged also
+/// contains the actual snapshot-writing pipeline; the launcher only needs
+/// the metadata record shape.
+module Snapshot =
+ type SnapMeta() =
+ let mutable profile: ConfigTypes.SnapProfile = ConfigTypes.EmptySnapProfile
+ let mutable context: string = ""
+ let mutable vbChecksumAlgo: string = ""
+ let mutable vbChecksum: string = ""
+
+ member x.Profile with get () = profile and set v = profile <- v
+ member x.Context with get () = context and set v = context <- v
+ member x.VBChecksumAlgo with get () = vbChecksumAlgo and set v = vbChecksumAlgo <- v
+ member x.VBChecksum with get () = vbChecksum and set v = vbChecksum <- v
diff --git a/MMLaunch/State.fs b/MMLaunch/State.fs
new file mode 100644
index 00000000..36dbd7e7
--- /dev/null
+++ b/MMLaunch/State.fs
@@ -0,0 +1,55 @@
+// Slim State module: only DirLocator is needed by the launcher to derive the
+// per-game data and snapshot directories. The full module in MMManaged also
+// owns the loaded mod database and snapshot-profile cache; the launcher does
+// not.
+
+namespace ModelMod
+
+open System.IO
+
+open ConfigTypes
+
+module State =
+ let private defaultDataDir = "Data"
+
+ type DirLocator(rootDir: string, conf: RunConfig) =
+ override x.ToString() =
+ sprintf "" rootDir conf
+
+ member x.QueryBaseDataDir() =
+ if conf.DocRoot <> "" then conf.DocRoot
+ else Path.Combine(rootDir, defaultDataDir)
+
+ member x.BaseDataDir
+ with get() =
+ let dataDir = x.QueryBaseDataDir()
+ if not (Directory.Exists dataDir) then
+ failwithf "Data directory does not exist: %s" dataDir
+ dataDir
+
+ member x.ExeBaseName
+ with get() = Path.GetFileNameWithoutExtension(conf.ExePath.ToLowerInvariant())
+
+ member x.ExeDataDir
+ with get() =
+ let dd = Path.Combine(x.BaseDataDir, x.ExeBaseName)
+ let gameProfDP = conf.GameProfile.DataPathName
+
+ let dirChecks =
+ [ (fun () -> if Directory.Exists dd then Some dd else None)
+ (fun () ->
+ if gameProfDP <> "" && Path.IsPathRooted(gameProfDP) && Directory.Exists(gameProfDP) then
+ Some gameProfDP
+ else None)
+ (fun () ->
+ if gameProfDP <> "" then
+ let bdSub = Path.Combine(x.BaseDataDir, gameProfDP)
+ if Directory.Exists bdSub then Some bdSub else None
+ else None) ]
+
+ match dirChecks |> List.tryPick (fun check -> check()) with
+ | Some path -> path
+ | None -> dd
+
+ member x.ExeSnapshotDir = Path.Combine(x.ExeDataDir, "snapshots")
+ member x.RootDir = rootDir
diff --git a/MMLaunch/TestFSI.fsx b/MMLaunch/TestFSI.fsx
deleted file mode 100644
index 7b87959a..00000000
--- a/MMLaunch/TestFSI.fsx
+++ /dev/null
@@ -1,6 +0,0 @@
-
-#load "BlenderUtil.fs"
-
-open MMLaunch
-
-BlenderUtil.findInstallPath()
\ No newline at end of file
diff --git a/MMLaunch/ViewModelBase.fs b/MMLaunch/ViewModelBase.fs
new file mode 100644
index 00000000..0e76fe11
--- /dev/null
+++ b/MMLaunch/ViewModelBase.fs
@@ -0,0 +1,32 @@
+// Hand-rolled MVVM primitives for the launcher: an INotifyPropertyChanged base
+// and an ICommand impl. The original WPF launcher used FSharp.ViewModule for
+// these; FSharp.ViewModule is WPF-only, so we replace it with this ~50 line
+// equivalent rather than take a full dependency on ReactiveUI.
+
+namespace MMLaunch
+
+open System
+open System.ComponentModel
+open System.Windows.Input
+
+type ViewModelBase() =
+ let propertyChanged = Event()
+
+ interface INotifyPropertyChanged with
+ []
+ member _.PropertyChanged = propertyChanged.Publish
+
+ member x.RaisePropertyChanged(propertyName: string) =
+ propertyChanged.Trigger(x, PropertyChangedEventArgs(propertyName))
+
+type RelayCommand(canExecute: obj -> bool, action: obj -> unit) =
+ let canExecuteChanged = Event()
+
+ member _.RaiseCanExecuteChanged() =
+ canExecuteChanged.Trigger(null, EventArgs.Empty)
+
+ interface ICommand with
+ []
+ member _.CanExecuteChanged = canExecuteChanged.Publish
+ member _.CanExecute(arg) = canExecute arg
+ member _.Execute(arg) = action arg
diff --git a/MMLaunch/ViewModelUtil.fs b/MMLaunch/ViewModelUtil.fs
index 597a0750..7b9c60c4 100644
--- a/MMLaunch/ViewModelUtil.fs
+++ b/MMLaunch/ViewModelUtil.fs
@@ -1,72 +1,221 @@
-// ModelMod: 3d data snapshotting & substitution program.
+// ModelMod: 3d data snapshotting & substitution program.
// Copyright(C) 2015,2016 John Quigley
-
+//
// This program is free software : you can redistribute it and / or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or
// (at your option) any later version.
-
+//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
// GNU General Public License for more details.
-// You should have received a copy of the GNU Lesser General Public License
-// along with this program.If not, see .
-
namespace MMLaunch
open System
-open System.ComponentModel
-open System.Windows
-open FSharp.ViewModule
-open FSharp.ViewModule.Validation
-open System.Windows.Input
-
-open Microsoft.Win32
-
-// When used with Exception as the fail type, this
-// encourages the idiom of using pattern matching to
+open System.Collections.Generic
+open System.Threading.Tasks
+
+open Avalonia
+open Avalonia.Controls
+open Avalonia.Controls.ApplicationLifetimes
+open Avalonia.Layout
+open Avalonia.Media
+open Avalonia.Platform.Storage
+open Avalonia.Threading
+
+// When used with Exception as the fail type, this
+// encourages the idiom of using pattern matching to
// handle errors, rather than require try blocks in random places.
-type Result<'SuccessType,'FailType> =
- Ok of 'SuccessType
+type Result<'SuccessType, 'FailType> =
+ | Ok of 'SuccessType
| Err of 'FailType
-
-module ViewModelUtil =
- let DesignMode = DesignerProperties.GetIsInDesignMode(new DependencyObject())
-
- type RelayCommand (canExecute:(obj -> bool), action:(obj -> unit)) =
- let event = new DelegateEvent()
- interface ICommand with
- []
- member x.CanExecuteChanged = event.Publish
- member x.CanExecute arg = canExecute(arg)
- member x.Execute arg = action(arg)
- let alwaysExecutable (action:(obj -> unit)) =
- new RelayCommand ((fun canExecute -> true), action)
+/// Avalonia replacement for WPF's MessageBoxResult enum. The launcher only
+/// distinguishes "Yes" from anything else.
+type DialogResult =
+ | Yes
+ | No
+ | Cancel
- let pushDialog(msg:string) =
- MessageBox.Show(msg) |> ignore
-
- let pushOkCancelDialog(msg:string) =
- MessageBox.Show(msg, "Confirm", MessageBoxButton.YesNo)
-
- let pushSelectFileDialog(initialDir:string option,filter:string) =
- let dlg = new OpenFileDialog()
-
- match initialDir with
+module ViewModelUtil =
+ /// Avalonia has no global "design mode" flag like WPF's DesignerProperties;
+ /// the design-time XAML previewer sets Design.IsDesignMode on the view, but
+ /// our view-models construct themselves at runtime so this is always false.
+ let DesignMode = false
+
+ let alwaysExecutable (action: obj -> unit) =
+ new RelayCommand((fun _ -> true), action)
+
+ // ----- Window discovery helpers -----------------------------------------
+
+ /// Returns the first open window from the classic desktop lifetime that can
+ /// be used as a dialog owner, or None if no windows are open yet.
+ let private firstOpenWindow () : Window option =
+ match Application.Current with
+ | null -> None
+ | app ->
+ match app.ApplicationLifetime with
+ | :? IClassicDesktopStyleApplicationLifetime as desktop ->
+ let windows = desktop.Windows
+ if windows = null || windows.Count = 0 then
+ match desktop.MainWindow with
+ | null -> None
+ | mw -> Some mw
+ else
+ Some windows.[0]
+ | _ -> None
+
+ /// Coerce a CommandParameter (which is sometimes a Window, sometimes null
+ /// in design mode) into a Window option. Falls back to whatever window is
+ /// currently open if the parameter is null.
+ let asWindow (param: obj) : Window option =
+ match param with
+ | :? Window as w -> Some w
+ | _ -> firstOpenWindow ()
+
+ // ----- Dialogs (simple modal Window built on the fly) -------------------
+
+ /// Synchronously pump the Avalonia dispatcher until `task` completes.
+ /// Used so the existing synchronous command code paths can call into the
+ /// async Avalonia APIs (StorageProvider, ShowDialog) without rewriting all
+ /// the call sites.
+ let private waitFor<'T> (task: Task<'T>) : 'T =
+ Dispatcher.UIThread.RunJobs()
+ let frame = DispatcherFrame()
+ task.ContinueWith(fun (_: Task<'T>) -> frame.Continue <- false)
+ |> ignore
+ Dispatcher.UIThread.PushFrame(frame)
+ task.Result
+
+ let private waitForVoid (task: Task) : unit =
+ let frame = DispatcherFrame()
+ task.ContinueWith(fun (_: Task) -> frame.Continue <- false)
+ |> ignore
+ Dispatcher.UIThread.PushFrame(frame)
+
+ let private showMessageDialog (title: string) (msg: string) (buttons: (string * DialogResult) list) : DialogResult =
+ let owner = firstOpenWindow ()
+
+ let mutable result = DialogResult.Cancel
+
+ let win = Window()
+ win.Title <- title
+ win.Width <- 460.0
+ win.SizeToContent <- SizeToContent.Height
+ win.CanResize <- false
+ win.WindowStartupLocation <- WindowStartupLocation.CenterOwner
+ match owner with
+ | Some w -> win.Icon <- w.Icon
| None -> ()
- | Some dir ->
- dlg.InitialDirectory <- dir
-
- dlg.Filter <- filter
- dlg.FilterIndex <- 0
- dlg.RestoreDirectory <- true
-
- let res = dlg.ShowDialog()
- if res.HasValue && res.Value then
- Some (dlg.FileName)
- else
- None
+ let panel = StackPanel(Margin = Thickness(20.0), Spacing = 12.0)
+
+ let text = TextBlock(Text = msg, TextWrapping = TextWrapping.Wrap)
+ panel.Children.Add(text)
+
+ let btnRow = StackPanel(Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Spacing = 8.0)
+ for (label, value) in buttons do
+ let b = Button(Content = label, MinWidth = 80.0)
+ b.Click.Add(fun _ ->
+ result <- value
+ win.Close())
+ btnRow.Children.Add(b)
+ panel.Children.Add(btnRow)
+
+ win.Content <- panel
+
+ match owner with
+ | Some o ->
+ let task = win.ShowDialog(o)
+ waitForVoid task
+ | None ->
+ // No owner means we're being called before the main window is shown
+ // (e.g. very early startup). Nothing else can be on screen, so just
+ // skip the dialog rather than crash.
+ result <- DialogResult.Cancel
+
+ result
+
+ let pushDialog (msg: string) =
+ showMessageDialog "ModelMod" msg [ ("OK", DialogResult.Yes) ] |> ignore
+
+ let pushOkCancelDialog (msg: string) : DialogResult =
+ showMessageDialog "Confirm" msg [ ("Yes", DialogResult.Yes); ("No", DialogResult.No) ]
+
+ // ----- File / folder pickers --------------------------------------------
+
+ let private exeFileTypes =
+ let ft = FilePickerFileType("Executable files")
+ ft.Patterns <- [| "*.exe" |]
+ ft
+
+ let private mmObjFileTypes =
+ let ft = FilePickerFileType("MMObj files")
+ ft.Patterns <- [| "*.mmobj" |]
+ ft
+
+ let private parseFilter (filter: string) : FilePickerFileType[] =
+ // WPF filter strings are pipe-separated "Description|*.ext" pairs.
+ // Avalonia uses FilePickerFileType objects; convert.
+ let parts = filter.Split('|')
+ let pairs =
+ [| for i in 0 .. 2 .. parts.Length - 2 ->
+ let desc = parts.[i]
+ let pats = parts.[i + 1].Split(';') |> Array.map (fun s -> s.Trim())
+ let ft = FilePickerFileType(desc)
+ ft.Patterns <- pats
+ ft |]
+ if pairs.Length = 0 then [| FilePickerFileType("All files", Patterns = [| "*" |]) |] else pairs
+
+ let pushSelectFileDialog (initialDir: string option, filter: string) : string option =
+ match firstOpenWindow () with
+ | None -> None
+ | Some owner ->
+ let topLevel = TopLevel.GetTopLevel(owner)
+ if isNull topLevel then None
+ else
+ let opts = FilePickerOpenOptions()
+ opts.AllowMultiple <- false
+ opts.FileTypeFilter <- parseFilter filter
+
+ match initialDir with
+ | Some dir when System.IO.Directory.Exists dir ->
+ let folderTask = topLevel.StorageProvider.TryGetFolderFromPathAsync(Uri(dir))
+ let folder = waitFor folderTask
+ if not (isNull folder) then opts.SuggestedStartLocation <- folder
+ | _ -> ()
+
+ let task = topLevel.StorageProvider.OpenFilePickerAsync(opts)
+ let files = waitFor task
+ if files = null || files.Count = 0 then None
+ else
+ let f = files.[0]
+ let path = f.TryGetLocalPath()
+ if String.IsNullOrEmpty path then None else Some path
+
+ let pushSelectFolderDialog (initialDir: string option) : string option =
+ match firstOpenWindow () with
+ | None -> None
+ | Some owner ->
+ let topLevel = TopLevel.GetTopLevel(owner)
+ if isNull topLevel then None
+ else
+ let opts = FolderPickerOpenOptions()
+ opts.AllowMultiple <- false
+
+ match initialDir with
+ | Some dir when System.IO.Directory.Exists dir ->
+ let folderTask = topLevel.StorageProvider.TryGetFolderFromPathAsync(Uri(dir))
+ let folder = waitFor folderTask
+ if not (isNull folder) then opts.SuggestedStartLocation <- folder
+ | _ -> ()
+
+ let task = topLevel.StorageProvider.OpenFolderPickerAsync(opts)
+ let folders = waitFor task
+ if folders = null || folders.Count = 0 then None
+ else
+ let f = folders.[0]
+ let path = f.TryGetLocalPath()
+ if String.IsNullOrEmpty path then None else Some path
diff --git a/MMLaunch/WpfInteropSample/App.xaml b/MMLaunch/WpfInteropSample/App.xaml
deleted file mode 100644
index cf63bfd9..00000000
--- a/MMLaunch/WpfInteropSample/App.xaml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
diff --git a/MMLaunch/WpfInteropSample/App.xaml.cs b/MMLaunch/WpfInteropSample/App.xaml.cs
deleted file mode 100644
index de7a4d20..00000000
--- a/MMLaunch/WpfInteropSample/App.xaml.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace WpfInteropSample
-{
- public partial class App
- {
- }
-}
diff --git a/MMLaunch/WpfInteropSample/D3D11Host.cs b/MMLaunch/WpfInteropSample/D3D11Host.cs
deleted file mode 100644
index eb9e435a..00000000
--- a/MMLaunch/WpfInteropSample/D3D11Host.cs
+++ /dev/null
@@ -1,448 +0,0 @@
-using System;
-using System.ComponentModel;
-using System.Diagnostics;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Media;
-using Microsoft.Xna.Framework;
-using Microsoft.Xna.Framework.Graphics;
-using Color = Microsoft.Xna.Framework.Color;
-using Matrix = Microsoft.Xna.Framework.Matrix;
-
-
-namespace WpfInteropSample
-{
- ///
- /// Host a Direct3D 11 scene.
- ///
- public class D3D11Host : Image
- {
- #region Fields
- // The Direct3D 11 device (shared by all D3D11Host elements):
- private static GraphicsDevice _graphicsDevice;
- private static int _referenceCount;
- private static readonly object _graphicsDeviceLock = new object();
-
- // Image source:
- private RenderTarget2D _renderTarget;
- private D3D11Image _d3D11Image;
- private bool _resetBackBuffer;
-
- // Render timing:
- private readonly Stopwatch _timer;
- private TimeSpan _lastRenderingTime;
- #endregion
-
- #region Properties
- ///
- /// Gets a value indicating whether the controls runs in the context of a designer (e.g.
- /// Visual Studio Designer or Expression Blend).
- ///
- ///
- /// if controls run in design mode; otherwise,
- /// .
- ///
- public static bool IsInDesignMode
- {
- get
- {
- if (!_isInDesignMode.HasValue)
- _isInDesignMode = (bool)DependencyPropertyDescriptor.FromProperty(DesignerProperties.IsInDesignModeProperty, typeof(FrameworkElement)).Metadata.DefaultValue;
-
- return _isInDesignMode.Value;
- }
- }
- private static bool? _isInDesignMode;
-
-
- ///
- /// Gets the graphics device.
- ///
- /// The graphics device.
- public GraphicsDevice GraphicsDevice
- {
- get { return _graphicsDevice; }
- }
- #endregion
-
-
- #region Constructors
- ///
- /// Initializes a new instance of the class.
- ///
- public D3D11Host()
- {
- _timer = new Stopwatch();
- Loaded += OnLoaded;
- Unloaded += OnUnloaded;
- }
- #endregion
-
-
- #region Methods
- private void OnLoaded(object sender, RoutedEventArgs eventArgs)
- {
- if (IsInDesignMode)
- return;
-
- InitializeGraphicsDevice();
- InitializeImageSource();
- Initialize();
- StartRendering();
- }
-
-
- private void OnUnloaded(object sender, RoutedEventArgs eventArgs)
- {
- if (IsInDesignMode)
- return;
-
- StopRendering();
- Uninitialize();
- UnitializeImageSource();
- UninitializeGraphicsDevice();
- }
-
-
- private static void InitializeGraphicsDevice()
- {
- lock (_graphicsDeviceLock)
- {
- _referenceCount++;
- if (_referenceCount == 1)
- {
- // Create Direct3D 11 device.
- var presentationParameters = new PresentationParameters
- {
- // Do not associate graphics device with window.
- DeviceWindowHandle = IntPtr.Zero,
- };
- // JMQ: changed this for modelmod
- _graphicsDevice = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, GraphicsProfile.Reach, presentationParameters);
- //_graphicsDevice = new GraphicsDevice(GraphicsProfile.HiDef, presentationParameters);
- }
- }
- }
-
-
- private static void UninitializeGraphicsDevice()
- {
- lock (_graphicsDeviceLock)
- {
- _referenceCount--;
- if (_referenceCount == 0)
- {
- _graphicsDevice.Dispose();
- _graphicsDevice = null;
- }
- }
- }
-
-
- private void InitializeImageSource()
- {
- _d3D11Image = new D3D11Image();
- _d3D11Image.IsFrontBufferAvailableChanged += OnIsFrontBufferAvailableChanged;
- CreateBackBuffer();
- Source = _d3D11Image;
- }
-
-
- private void UnitializeImageSource()
- {
- _d3D11Image.IsFrontBufferAvailableChanged -= OnIsFrontBufferAvailableChanged;
- Source = null;
-
- if (_d3D11Image != null)
- {
- _d3D11Image.Dispose();
- _d3D11Image = null;
- }
- if (_renderTarget != null)
- {
- _renderTarget.Dispose();
- _renderTarget = null;
- }
- }
-
-
- private void CreateBackBuffer()
- {
- _d3D11Image.SetBackBuffer(null);
- if (_renderTarget != null)
- {
- _renderTarget.Dispose();
- _renderTarget = null;
- }
-
- int width = Math.Max((int)ActualWidth, 1);
- int height = Math.Max((int)ActualHeight, 1);
- _renderTarget = new RenderTarget2D(_graphicsDevice, width, height, false, SurfaceFormat.Bgr32, DepthFormat.Depth24Stencil8, 0, RenderTargetUsage.DiscardContents, true);
- _d3D11Image.SetBackBuffer(_renderTarget);
- }
-
-
- private void StartRendering()
- {
- if (_timer.IsRunning)
- return;
-
- CompositionTarget.Rendering += OnRendering;
- _timer.Start();
- }
-
-
- private void StopRendering()
- {
- if (!_timer.IsRunning)
- return;
-
- CompositionTarget.Rendering -= OnRendering;
- _timer.Stop();
- }
-
-
- private void OnRendering(object sender, EventArgs eventArgs)
- {
- if (!_timer.IsRunning)
- return;
-
- // Recreate back buffer if necessary.
- if (_resetBackBuffer)
- CreateBackBuffer();
-
- // CompositionTarget.Rendering event may be raised multiple times per frame
- // (e.g. during window resizing).
- var renderingEventArgs = (RenderingEventArgs)eventArgs;
- if (_lastRenderingTime != renderingEventArgs.RenderingTime || _resetBackBuffer)
- {
- _lastRenderingTime = renderingEventArgs.RenderingTime;
-
- GraphicsDevice.SetRenderTarget(_renderTarget);
- Render(_timer.Elapsed);
- GraphicsDevice.Flush();
- }
-
- _d3D11Image.Invalidate(); // Always invalidate D3DImage to reduce flickering
- // during window resizing.
-
- _resetBackBuffer = false;
- }
-
-
- ///
- /// Raises the event, using the specified
- /// information as part of the eventual event data.
- ///
- /// Details of the old and new size involved in the change.
- protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
- {
- _resetBackBuffer = true;
- base.OnRenderSizeChanged(sizeInfo);
- }
-
-
- private void OnIsFrontBufferAvailableChanged(object sender, DependencyPropertyChangedEventArgs eventArgs)
- {
- if (_d3D11Image.IsFrontBufferAvailable)
- {
- StartRendering();
- _resetBackBuffer = true;
- }
- else
- {
- StopRendering();
- }
- }
-
-
- #region ----- Example Scene -----
-
- // Source: http://msdn.microsoft.com/en-us/library/bb203926(v=xnagamestudio.40).aspx
-
- // Note: This is just an example. To improve the D3D11Host make the methods
- // Initialize(), Unitialize(), and Render() protected virtual or call an
- // external "renderer".
-
- Matrix _worldMatrix;
- Matrix _viewMatrix;
- Matrix _projectionMatrix;
- VertexDeclaration vertexDeclaration;
- VertexBuffer _vertexBuffer;
- BasicEffect _basicEffect;
-
-
- public virtual void Initialize()
- {
- float tilt = MathHelper.ToRadians(0); // 0 degree angle
- // Use the world matrix to tilt the cube along x and y axes.
- _worldMatrix = Matrix.CreateRotationX(tilt) * Matrix.CreateRotationY(tilt);
- _viewMatrix = Matrix.CreateLookAt(new Vector3(5, 5, 5), Vector3.Zero, Vector3.Up);
-
- _projectionMatrix = Matrix.CreatePerspectiveFieldOfView(
- MathHelper.ToRadians(45), // 45 degree angle
- (float)GraphicsDevice.Viewport.Width /
- (float)GraphicsDevice.Viewport.Height,
- 1.0f, 100.0f);
-
- _basicEffect = new BasicEffect(GraphicsDevice);
-
- _basicEffect.World = _worldMatrix;
- _basicEffect.View = _viewMatrix;
- _basicEffect.Projection = _projectionMatrix;
-
- // primitive color
- _basicEffect.AmbientLightColor = new Vector3(0.1f, 0.1f, 0.1f);
- _basicEffect.DiffuseColor = new Vector3(1.0f, 1.0f, 1.0f);
- _basicEffect.SpecularColor = new Vector3(0.25f, 0.25f, 0.25f);
- _basicEffect.SpecularPower = 5.0f;
- _basicEffect.Alpha = 1.0f;
-
- _basicEffect.LightingEnabled = true;
- if (_basicEffect.LightingEnabled)
- {
- _basicEffect.DirectionalLight0.Enabled = true; // enable each light individually
- if (_basicEffect.DirectionalLight0.Enabled)
- {
- // x direction
- _basicEffect.DirectionalLight0.DiffuseColor = new Vector3(1, 0, 0); // range is 0 to 1
- _basicEffect.DirectionalLight0.Direction = Vector3.Normalize(new Vector3(-1, 0, 0));
- // points from the light to the origin of the scene
- _basicEffect.DirectionalLight0.SpecularColor = Vector3.One;
- }
-
- _basicEffect.DirectionalLight1.Enabled = true;
- if (_basicEffect.DirectionalLight1.Enabled)
- {
- // y direction
- _basicEffect.DirectionalLight1.DiffuseColor = new Vector3(0, 0.75f, 0);
- _basicEffect.DirectionalLight1.Direction = Vector3.Normalize(new Vector3(0, -1, 0));
- _basicEffect.DirectionalLight1.SpecularColor = Vector3.One;
- }
-
- _basicEffect.DirectionalLight2.Enabled = true;
- if (_basicEffect.DirectionalLight2.Enabled)
- {
- // z direction
- _basicEffect.DirectionalLight2.DiffuseColor = new Vector3(0, 0, 0.5f);
- _basicEffect.DirectionalLight2.Direction = Vector3.Normalize(new Vector3(0, 0, -1));
- _basicEffect.DirectionalLight2.SpecularColor = Vector3.One;
- }
- }
-
- vertexDeclaration = new VertexDeclaration(new[]
- {
- new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
- new VertexElement(12, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0),
- new VertexElement(24, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0)
- });
-
- Vector3 topLeftFront = new Vector3(-1.0f, 1.0f, 1.0f);
- Vector3 bottomLeftFront = new Vector3(-1.0f, -1.0f, 1.0f);
- Vector3 topRightFront = new Vector3(1.0f, 1.0f, 1.0f);
- Vector3 bottomRightFront = new Vector3(1.0f, -1.0f, 1.0f);
- Vector3 topLeftBack = new Vector3(-1.0f, 1.0f, -1.0f);
- Vector3 topRightBack = new Vector3(1.0f, 1.0f, -1.0f);
- Vector3 bottomLeftBack = new Vector3(-1.0f, -1.0f, -1.0f);
- Vector3 bottomRightBack = new Vector3(1.0f, -1.0f, -1.0f);
-
- Vector2 textureTopLeft = new Vector2(0.0f, 0.0f);
- Vector2 textureTopRight = new Vector2(1.0f, 0.0f);
- Vector2 textureBottomLeft = new Vector2(0.0f, 1.0f);
- Vector2 textureBottomRight = new Vector2(1.0f, 1.0f);
-
- Vector3 frontNormal = new Vector3(0.0f, 0.0f, 1.0f);
- Vector3 backNormal = new Vector3(0.0f, 0.0f, -1.0f);
- Vector3 topNormal = new Vector3(0.0f, 1.0f, 0.0f);
- Vector3 bottomNormal = new Vector3(0.0f, -1.0f, 0.0f);
- Vector3 leftNormal = new Vector3(-1.0f, 0.0f, 0.0f);
- Vector3 rightNormal = new Vector3(1.0f, 0.0f, 0.0f);
-
- var cubeVertices = new VertexPositionNormalTexture[36];
-
- // Front face.
- cubeVertices[0] = new VertexPositionNormalTexture(topLeftFront, frontNormal, textureTopLeft);
- cubeVertices[1] = new VertexPositionNormalTexture(bottomLeftFront, frontNormal, textureBottomLeft);
- cubeVertices[2] = new VertexPositionNormalTexture(topRightFront, frontNormal, textureTopRight);
- cubeVertices[3] = new VertexPositionNormalTexture(bottomLeftFront, frontNormal, textureBottomLeft);
- cubeVertices[4] = new VertexPositionNormalTexture(bottomRightFront, frontNormal, textureBottomRight);
- cubeVertices[5] = new VertexPositionNormalTexture(topRightFront, frontNormal, textureTopRight);
-
- // Back face.
- cubeVertices[6] = new VertexPositionNormalTexture(topLeftBack, backNormal, textureTopRight);
- cubeVertices[7] = new VertexPositionNormalTexture(topRightBack, backNormal, textureTopLeft);
- cubeVertices[8] = new VertexPositionNormalTexture(bottomLeftBack, backNormal, textureBottomRight);
- cubeVertices[9] = new VertexPositionNormalTexture(bottomLeftBack, backNormal, textureBottomRight);
- cubeVertices[10] = new VertexPositionNormalTexture(topRightBack, backNormal, textureTopLeft);
- cubeVertices[11] = new VertexPositionNormalTexture(bottomRightBack, backNormal, textureBottomLeft);
-
- // Top face.
- cubeVertices[12] = new VertexPositionNormalTexture(topLeftFront, topNormal, textureBottomLeft);
- cubeVertices[13] = new VertexPositionNormalTexture(topRightBack, topNormal, textureTopRight);
- cubeVertices[14] = new VertexPositionNormalTexture(topLeftBack, topNormal, textureTopLeft);
- cubeVertices[15] = new VertexPositionNormalTexture(topLeftFront, topNormal, textureBottomLeft);
- cubeVertices[16] = new VertexPositionNormalTexture(topRightFront, topNormal, textureBottomRight);
- cubeVertices[17] = new VertexPositionNormalTexture(topRightBack, topNormal, textureTopRight);
-
- // Bottom face.
- cubeVertices[18] = new VertexPositionNormalTexture(bottomLeftFront, bottomNormal, textureTopLeft);
- cubeVertices[19] = new VertexPositionNormalTexture(bottomLeftBack, bottomNormal, textureBottomLeft);
- cubeVertices[20] = new VertexPositionNormalTexture(bottomRightBack, bottomNormal, textureBottomRight);
- cubeVertices[21] = new VertexPositionNormalTexture(bottomLeftFront, bottomNormal, textureTopLeft);
- cubeVertices[22] = new VertexPositionNormalTexture(bottomRightBack, bottomNormal, textureBottomRight);
- cubeVertices[23] = new VertexPositionNormalTexture(bottomRightFront, bottomNormal, textureTopRight);
-
- // Left face.
- cubeVertices[24] = new VertexPositionNormalTexture(topLeftFront, leftNormal, textureTopRight);
- cubeVertices[25] = new VertexPositionNormalTexture(bottomLeftBack, leftNormal, textureBottomLeft);
- cubeVertices[26] = new VertexPositionNormalTexture(bottomLeftFront, leftNormal, textureBottomRight);
- cubeVertices[27] = new VertexPositionNormalTexture(topLeftBack, leftNormal, textureTopLeft);
- cubeVertices[28] = new VertexPositionNormalTexture(bottomLeftBack, leftNormal, textureBottomLeft);
- cubeVertices[29] = new VertexPositionNormalTexture(topLeftFront, leftNormal, textureTopRight);
-
- // Right face.
- cubeVertices[30] = new VertexPositionNormalTexture(topRightFront, rightNormal, textureTopLeft);
- cubeVertices[31] = new VertexPositionNormalTexture(bottomRightFront, rightNormal, textureBottomLeft);
- cubeVertices[32] = new VertexPositionNormalTexture(bottomRightBack, rightNormal, textureBottomRight);
- cubeVertices[33] = new VertexPositionNormalTexture(topRightBack, rightNormal, textureTopRight);
- cubeVertices[34] = new VertexPositionNormalTexture(topRightFront, rightNormal, textureTopLeft);
- cubeVertices[35] = new VertexPositionNormalTexture(bottomRightBack, rightNormal, textureBottomRight);
-
- _vertexBuffer = new VertexBuffer(GraphicsDevice, vertexDeclaration, cubeVertices.Length, BufferUsage.None);
- _vertexBuffer.SetData(cubeVertices);
- }
-
-
- public virtual void Uninitialize()
- {
- _vertexBuffer.Dispose();
- _vertexBuffer = null;
-
- vertexDeclaration.Dispose();
- vertexDeclaration = null;
-
- _basicEffect.Dispose();
- _basicEffect = null;
- }
-
-
- public virtual void Render(TimeSpan time)
- {
- GraphicsDevice.Clear(Color.SteelBlue);
- GraphicsDevice.RasterizerState = RasterizerState.CullNone;
- GraphicsDevice.SetVertexBuffer(_vertexBuffer);
-
- // Rotate cube around up-axis.
- _basicEffect.World = Matrix.CreateRotationY((float)time.Milliseconds / 1000 * MathHelper.TwoPi) * _worldMatrix;
-
- foreach (var pass in _basicEffect.CurrentTechnique.Passes)
- {
- pass.Apply();
- GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, 12);
- }
- }
- #endregion
-
- #endregion
- }
-}
\ No newline at end of file
diff --git a/MMLaunch/WpfInteropSample/D3D11Image.cs b/MMLaunch/WpfInteropSample/D3D11Image.cs
deleted file mode 100644
index 89d71187..00000000
--- a/MMLaunch/WpfInteropSample/D3D11Image.cs
+++ /dev/null
@@ -1,190 +0,0 @@
-using System;
-using System.Windows;
-using System.Windows.Interop;
-using Microsoft.Xna.Framework.Graphics;
-using SharpDX.Direct3D9;
-using Texture = SharpDX.Direct3D9.Texture;
-
-
-namespace WpfInteropSample
-{
- ///
- /// Wraps the to make it compatible with Direct3D 11.
- ///
- ///
- /// The should be disposed if no longer needed!
- ///
- internal class D3D11Image : D3DImage, IDisposable
- {
- #region Fields
- // Use a Direct3D 9 device for interoperability. The device is shared by
- // all D3D11Images.
- private static D3D9 _d3D9;
- private static int _referenceCount;
- private static readonly object _d3d9Lock = new object();
-
- private bool _disposed;
- private Texture _backBuffer;
- #endregion
-
-
- #region Creation & Cleanup
- ///
- /// Initializes a new instance of the class.
- ///
- public D3D11Image()
- {
- InitializeD3D9();
- }
-
-
- ///
- /// Releases unmanaged resources before an instance of the class is
- /// reclaimed by garbage collection.
- ///
- ///
- /// This method releases unmanaged resources by calling the virtual
- /// method, passing in .
- ///
- ~D3D11Image()
- {
- Dispose(false);
- }
-
-
- ///
- /// Releases all resources used by an instance of the class.
- ///
- ///
- /// This method calls the virtual method, passing in
- /// , and then suppresses finalization of the instance.
- ///
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
-
- ///
- /// Releases the unmanaged resources used by an instance of the class
- /// and optionally releases the managed resources.
- ///
- ///
- /// to release both managed and unmanaged resources;
- /// to release only unmanaged resources.
- ///
- protected virtual void Dispose(bool disposing)
- {
- if (!_disposed)
- {
- if (disposing)
- {
- // Dispose managed resources.
- SetBackBuffer(null);
- if (_backBuffer != null)
- {
- _backBuffer.Dispose();
- _backBuffer = null;
- }
- }
-
- // Release unmanaged resources.
- UninitializeD3D9();
- _disposed = true;
- }
- }
- #endregion
-
-
- #region Methods
- ///
- /// Initializes the Direct3D 9 device.
- ///
- private static void InitializeD3D9()
- {
- lock (_d3d9Lock)
- {
- _referenceCount++;
- if (_referenceCount == 1)
- _d3D9 = new D3D9();
- }
- }
-
-
- ///
- /// Un-initializes the Direct3D 9 device, if no longer needed.
- ///
- private static void UninitializeD3D9()
- {
- lock (_d3d9Lock)
- {
- _referenceCount--;
- if (_referenceCount == 0)
- {
- _d3D9.Dispose();
- _d3D9 = null;
- }
- }
- }
-
-
- private void ThrowIfDisposed()
- {
- if (_disposed)
- throw new ObjectDisposedException(GetType().FullName);
- }
-
-
- ///
- /// Invalidates the front buffer. (Needs to be called when the back buffer has changed.)
- ///
- public void Invalidate()
- {
- ThrowIfDisposed();
-
- if (_backBuffer != null)
- {
- Lock();
- AddDirtyRect(new Int32Rect(0, 0, PixelWidth, PixelHeight));
- Unlock();
- }
- }
-
-
- ///
- /// Sets the back buffer of the .
- ///
- /// The Direct3D 11 texture to be used as the back buffer.
- public void SetBackBuffer(Texture2D texture)
- {
- ThrowIfDisposed();
-
- var previousBackBuffer = _backBuffer;
-
- // Create shared texture on Direct3D 9 device.
- _backBuffer = _d3D9.GetSharedTexture(texture);
- if (_backBuffer != null)
- {
- // Set texture as new back buffer.
- using (Surface surface = _backBuffer.GetSurfaceLevel(0))
- {
- Lock();
- SetBackBuffer(D3DResourceType.IDirect3DSurface9, surface.NativePointer);
- Unlock();
- }
- }
- else
- {
- // Reset back buffer.
- Lock();
- SetBackBuffer(D3DResourceType.IDirect3DSurface9, IntPtr.Zero);
- Unlock();
- }
-
- if (previousBackBuffer != null)
- previousBackBuffer.Dispose();
- }
- #endregion
- }
-}
\ No newline at end of file
diff --git a/MMLaunch/WpfInteropSample/D3D9.cs b/MMLaunch/WpfInteropSample/D3D9.cs
deleted file mode 100644
index f86ce882..00000000
--- a/MMLaunch/WpfInteropSample/D3D9.cs
+++ /dev/null
@@ -1,178 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using Microsoft.Xna.Framework.Graphics;
-using SharpDX.Direct3D9;
-using DeviceType = SharpDX.Direct3D9.DeviceType;
-using PresentInterval = SharpDX.Direct3D9.PresentInterval;
-using Texture = SharpDX.Direct3D9.Texture;
-
-
-namespace WpfInteropSample
-{
- ///
- /// Represents a Direct3D 9 device required for Direct3D 11 interoperability.
- ///
- ///
- /// It is not possible to set a Direct3D 11 resource (e.g. a texture or render target) in WPF
- /// directly because WPF requires Direct3D 9. The class creates a new
- /// Direct3D 9 device which can be used for sharing resources between Direct3D 11 and Direct3D
- /// 9. Call to convert a texture from Direct3D 11 to Direct3D 9.
- ///
- internal class D3D9 : IDisposable
- {
- // The code requires Windows Vista and up using the Windows Display Driver Model (WDDM).
- // It does not work with the Windows 2000 Display Driver Model (XDDM).
-
- #region Fields
- private bool _disposed;
- private Direct3DEx _direct3D;
- private DeviceEx _device;
- #endregion
-
-
- #region Creation & Cleanup
- ///
- /// Initializes a new instance of the class.
- ///
- public D3D9()
- {
- // Create Direct3DEx device on Windows Vista/7/8 with a display configured to use
- // the Windows Display Driver Model (WDDM). Use Direct3D on any other platform.
- _direct3D = new Direct3DEx();
-
- PresentParameters presentparams = new PresentParameters
- {
- Windowed = true,
- SwapEffect = SwapEffect.Discard,
- PresentationInterval = PresentInterval.Default,
-
- // The device back buffer is not used.
- BackBufferFormat = Format.Unknown,
- BackBufferWidth = 1,
- BackBufferHeight = 1,
-
- // Use dummy window handle.
- DeviceWindowHandle = GetDesktopWindow()
- };
-
-
- _device = new DeviceEx(_direct3D, 0, DeviceType.Hardware, IntPtr.Zero,
- CreateFlags.HardwareVertexProcessing | CreateFlags.Multithreaded | CreateFlags.FpuPreserve,
- presentparams);
- }
-
-
- ///
- /// Releases unmanaged resources before an instance of the class is
- /// reclaimed by garbage collection.
- ///
- ///
- /// This method releases unmanaged resources by calling the virtual
- /// method, passing in .
- ///
- ~D3D9()
- {
- Dispose(false);
- }
-
-
- ///
- /// Releases all resources used by an instance of the class.
- ///
- ///
- /// This method calls the virtual method, passing in
- /// , and then suppresses finalization of the instance.
- ///
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
-
- ///
- /// Releases the unmanaged resources used by an instance of the class
- /// and optionally releases the managed resources.
- ///
- ///
- /// to release both managed and unmanaged resources;
- /// to release only unmanaged resources.
- ///
- protected virtual void Dispose(bool disposing)
- {
- if (!_disposed)
- {
- if (disposing)
- {
- // Dispose managed resources.
- if (_device != null)
- {
- _device.Dispose();
- _device = null;
- }
- if (_direct3D != null)
- {
- _direct3D.Dispose();
- _direct3D = null;
- }
- }
-
- // Release unmanaged resources.
- _disposed = true;
- }
- }
- #endregion
-
-
- #region Methods
-
- [DllImport("user32.dll", SetLastError = false)]
- private static extern IntPtr GetDesktopWindow();
-
-
- private void ThrowIfDisposed()
- {
- if (_disposed)
- throw new ObjectDisposedException(GetType().FullName);
- }
-
-
- ///
- /// Creates Direct3D 9 texture from the specified Direct3D 11 texture.
- /// (The content is shared between the devices.)
- ///
- /// The Direct3D 11 texture.
- /// The Direct3D 9 texture.
- ///
- /// The Direct3D 11 texture is not a shared resource, or the texture format is not
- /// supported.
- ///
- public Texture GetSharedTexture(Texture2D renderTarget)
- {
- ThrowIfDisposed();
-
- if (renderTarget == null)
- return null;
-
- IntPtr handle = renderTarget.GetSharedHandle();
- if (handle == IntPtr.Zero)
- throw new ArgumentException("Unable to access resource. The texture needs to be created as a shared resource.", "renderTarget");
-
- Format format;
- switch (renderTarget.Format)
- {
- case SurfaceFormat.Bgr32:
- format = Format.X8R8G8B8;
- break;
- case SurfaceFormat.Bgra32:
- format = Format.A8R8G8B8;
- break;
- default:
- throw new ArgumentException("Unexpected surface format. Supported formats are: SurfaceFormat.Bgr32, SurfaceFormat.Bgra32.", "renderTarget");
- }
-
- return new Texture(_device, renderTarget.Width, renderTarget.Height, 1, Usage.RenderTarget, format, Pool.Default, ref handle);
- }
- #endregion
- }
-}
diff --git a/MMLaunch/WpfInteropSample/MainWindow.xaml b/MMLaunch/WpfInteropSample/MainWindow.xaml
deleted file mode 100644
index f0c2392c..00000000
--- a/MMLaunch/WpfInteropSample/MainWindow.xaml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/MMLaunch/WpfInteropSample/MainWindow.xaml.cs b/MMLaunch/WpfInteropSample/MainWindow.xaml.cs
deleted file mode 100644
index 78aa93f0..00000000
--- a/MMLaunch/WpfInteropSample/MainWindow.xaml.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace WpfInteropSample
-{
- public partial class MainWindow
- {
- public MainWindow()
- {
- InitializeComponent();
- }
- }
-}
diff --git a/MMLaunch/WpfInteropSample/Properties/AssemblyInfo.cs b/MMLaunch/WpfInteropSample/Properties/AssemblyInfo.cs
deleted file mode 100644
index 81834caa..00000000
--- a/MMLaunch/WpfInteropSample/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using System.Reflection;
-using System.Resources;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Windows;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("WpfInteropSample")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("WpfInteropSample")]
-[assembly: AssemblyCopyright("Copyright © 2013")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-//In order to begin building localizable applications, set
-//CultureYouAreCodingWith in your .csproj file
-//inside a . For example, if you are using US english
-//in your source files, set the to en-US. Then uncomment
-//the NeutralResourceLanguage attribute below. Update the "en-US" in
-//the line below to match the UICulture setting in the project file.
-
-//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
-
-
-[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)
-)]
-
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/MMLaunch/WpfInteropSample/Properties/Resources.Designer.cs b/MMLaunch/WpfInteropSample/Properties/Resources.Designer.cs
deleted file mode 100644
index 183dc121..00000000
--- a/MMLaunch/WpfInteropSample/Properties/Resources.Designer.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// 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.
-//
-//------------------------------------------------------------------------------
-
-namespace WpfInteropSample.Properties {
- using System;
-
-
- ///
- /// A strongly-typed resource class, for looking up localized strings, etc.
- ///
- // This class was auto-generated by the StronglyTypedResourceBuilder
- // class via a tool like ResGen or Visual Studio.
- // To add or remove a member, edit your .ResX file then rerun ResGen
- // with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.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() {
- }
-
- ///
- /// Returns the cached ResourceManager instance used by this class.
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Resources.ResourceManager ResourceManager {
- get {
- if (object.ReferenceEquals(resourceMan, null)) {
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WpfInteropSample.Properties.Resources", typeof(Resources).Assembly);
- resourceMan = temp;
- }
- return resourceMan;
- }
- }
-
- ///
- /// Overrides the current thread's CurrentUICulture property for all
- /// resource lookups using this strongly typed resource class.
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Globalization.CultureInfo Culture {
- get {
- return resourceCulture;
- }
- set {
- resourceCulture = value;
- }
- }
- }
-}
diff --git a/MMLaunch/WpfInteropSample/Properties/Resources.resx b/MMLaunch/WpfInteropSample/Properties/Resources.resx
deleted file mode 100644
index af7dbebb..00000000
--- a/MMLaunch/WpfInteropSample/Properties/Resources.resx
+++ /dev/null
@@ -1,117 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 2.0
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
\ No newline at end of file
diff --git a/MMLaunch/WpfInteropSample/Properties/Settings.Designer.cs b/MMLaunch/WpfInteropSample/Properties/Settings.Designer.cs
deleted file mode 100644
index d87b99e5..00000000
--- a/MMLaunch/WpfInteropSample/Properties/Settings.Designer.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// 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.
-//
-//------------------------------------------------------------------------------
-
-namespace WpfInteropSample.Properties {
-
-
- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.10.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;
- }
- }
- }
-}
diff --git a/MMLaunch/WpfInteropSample/Properties/Settings.settings b/MMLaunch/WpfInteropSample/Properties/Settings.settings
deleted file mode 100644
index 033d7a5e..00000000
--- a/MMLaunch/WpfInteropSample/Properties/Settings.settings
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/MMLaunch/WpfInteropSample/ReadMe.txt b/MMLaunch/WpfInteropSample/ReadMe.txt
deleted file mode 100644
index 55dd8651..00000000
--- a/MMLaunch/WpfInteropSample/ReadMe.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-This sample shows how to display MonoGame graphics in a WPF D3DImage.
-
-This sample requires SharpDX >= v2.5.0!
\ No newline at end of file
diff --git a/MMLaunch/WpfInteropSample/WpfInteropSample.csproj b/MMLaunch/WpfInteropSample/WpfInteropSample.csproj
deleted file mode 100644
index dbb93f13..00000000
--- a/MMLaunch/WpfInteropSample/WpfInteropSample.csproj
+++ /dev/null
@@ -1,204 +0,0 @@
-
-
-
- Debug
- x86
- 8.0.30703
- 2.0
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}
- WinExe
- Properties
- WpfInteropSample
- WpfInteropSample
- v4.6.2
-
-
- 512
- {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- 4
-
-
- x86
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
- false
-
-
- x86
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
- false
-
-
- true
- bin\Debug\
- DEBUG;TRACE
- full
- AnyCPU
- prompt
- MinimumRecommendedRules.ruleset
- false
-
-
- bin\Release\
- TRACE
- true
- pdbonly
- AnyCPU
- prompt
- MinimumRecommendedRules.ruleset
- false
-
-
-
- ..\..\FSharp.Core.4.4.3.0\FSharp.Core.dll
-
-
- ..\..\packages\MonoGame.Framework.WindowsDX\lib\net40\MonoGame.Framework.dll
-
-
- ..\..\packages\MonoGame.Framework.WindowsDX\lib\net40\SharpDX.dll
-
-
- ..\..\packages\MonoGame.Framework.WindowsDX\lib\net40\SharpDX.Direct3D11.dll
-
-
- ..\..\packages\MonoGame.Framework.WindowsDX\lib\net40\SharpDX.Direct3D9.dll
-
-
- ..\..\packages\MonoGame.Framework.WindowsDX\lib\net40\SharpDX.DXGI.dll
-
-
-
-
-
-
-
-
-
- 4.0
-
-
-
-
-
-
-
- MSBuild:Compile
- Designer
-
-
- MSBuild:Compile
- Designer
-
-
- App.xaml
- Code
-
-
-
-
-
- MainWindow.xaml
- Code
-
-
-
-
- Code
-
-
- True
- True
- Resources.resx
-
-
- True
- Settings.settings
- True
-
-
- ResXFileCodeGenerator
- Resources.Designer.cs
-
-
-
- SettingsSingleFileGenerator
- Settings.Designer.cs
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ..\..\packages\Expression.Blend.Sdk\lib\net45\System.Windows.Interactivity.dll
- True
- True
-
-
-
-
-
-
-
-
- ..\..\packages\FSharp.ViewModule.Core\lib\net45\FSharp.ViewModule.Core.Wpf.dll
- True
- True
-
-
-
-
-
-
-
-
- ..\..\packages\FsXaml.Wpf\lib\net45\FsXaml.Wpf.dll
- True
- True
-
-
- ..\..\packages\FsXaml.Wpf\lib\net45\FsXaml.Wpf.TypeProvider.dll
- True
- True
-
-
-
-
-
-
-
-
- ..\..\packages\YamlDotNet\lib\net35\YamlDotNet.dll
- True
- True
-
-
-
-
-
\ No newline at end of file
diff --git a/MMLaunch/WpfInteropSample/WpfInteropSample.sln b/MMLaunch/WpfInteropSample/WpfInteropSample.sln
deleted file mode 100644
index c548509d..00000000
--- a/MMLaunch/WpfInteropSample/WpfInteropSample.sln
+++ /dev/null
@@ -1,47 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 11.00
-# Visual Studio 2010
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{0A3E2DFD-8744-4C43-A60C-2213386B1CF4}"
- ProjectSection(SolutionItems) = preProject
- ..\..\paket.dependencies = ..\..\paket.dependencies
- EndProjectSection
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfInteropSample", "WpfInteropSample.csproj", "{B7751FAB-AE42-457F-8D69-51BEE5DC080A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Framework.Windows", "..\..\MonoGame.Framework\MonoGame.Framework.Windows.csproj", "{7DE47032-A904-4C29-BD22-2D235E8D91BA}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Debug|Mixed Platforms = Debug|Mixed Platforms
- Debug|x86 = Debug|x86
- Release|Any CPU = Release|Any CPU
- Release|Mixed Platforms = Release|Mixed Platforms
- Release|x86 = Release|x86
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Debug|Any CPU.ActiveCfg = Debug|x86
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Debug|Mixed Platforms.Build.0 = Debug|x86
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Debug|x86.ActiveCfg = Debug|x86
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Debug|x86.Build.0 = Debug|x86
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Release|Any CPU.ActiveCfg = Release|x86
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Release|Mixed Platforms.ActiveCfg = Release|x86
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Release|Mixed Platforms.Build.0 = Release|x86
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Release|x86.ActiveCfg = Release|x86
- {B7751FAB-AE42-457F-8D69-51BEE5DC080A}.Release|x86.Build.0 = Release|x86
- {7DE47032-A904-4C29-BD22-2D235E8D91BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7DE47032-A904-4C29-BD22-2D235E8D91BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7DE47032-A904-4C29-BD22-2D235E8D91BA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
- {7DE47032-A904-4C29-BD22-2D235E8D91BA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
- {7DE47032-A904-4C29-BD22-2D235E8D91BA}.Debug|x86.ActiveCfg = Debug|Any CPU
- {7DE47032-A904-4C29-BD22-2D235E8D91BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7DE47032-A904-4C29-BD22-2D235E8D91BA}.Release|Any CPU.Build.0 = Release|Any CPU
- {7DE47032-A904-4C29-BD22-2D235E8D91BA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
- {7DE47032-A904-4C29-BD22-2D235E8D91BA}.Release|Mixed Platforms.Build.0 = Release|Any CPU
- {7DE47032-A904-4C29-BD22-2D235E8D91BA}.Release|x86.ActiveCfg = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
-EndGlobal
diff --git a/MMLaunch/WpfInteropSample/app.config b/MMLaunch/WpfInteropSample/app.config
deleted file mode 100644
index 2a0024f7..00000000
--- a/MMLaunch/WpfInteropSample/app.config
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/MMLaunch/app.manifest b/MMLaunch/app.manifest
new file mode 100644
index 00000000..056a2c14
--- /dev/null
+++ b/MMLaunch/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ true
+ PerMonitorV2
+
+
+
+
+
+
+
+
+
+
diff --git a/MMLaunch/paket.references b/MMLaunch/paket.references
deleted file mode 100644
index 22a0e10a..00000000
--- a/MMLaunch/paket.references
+++ /dev/null
@@ -1,4 +0,0 @@
-Expression.Blend.Sdk
-FSharp.ViewModule.Core
-FsXaml.Wpf
-YamlDotNet
\ No newline at end of file
diff --git a/MMManaged.Engine/MMManaged.Engine.dotnet.fsproj b/MMManaged.Engine/MMManaged.Engine.dotnet.fsproj
index ece5423f..3dafa432 100644
--- a/MMManaged.Engine/MMManaged.Engine.dotnet.fsproj
+++ b/MMManaged.Engine/MMManaged.Engine.dotnet.fsproj
@@ -59,9 +59,13 @@
MemoryCache.fs
-
- SnapshotProfile.fs
-
+
+
+ SnapshotProfileLoad.fs
+
+
+ SnapshotProfileInterop.fs
+
MeshDiskCache.fs
diff --git a/MMManaged.Engine/MMManaged.Engine.fsproj b/MMManaged.Engine/MMManaged.Engine.fsproj
index 920df1ea..a06ccbd9 100644
--- a/MMManaged.Engine/MMManaged.Engine.fsproj
+++ b/MMManaged.Engine/MMManaged.Engine.fsproj
@@ -78,8 +78,11 @@
MemoryCache.fs
-
- SnapshotProfile.fs
+
+ SnapshotProfileLoad.fs
+
+
+ SnapshotProfileInterop.fs
MeshDiskCache.fs
diff --git a/MMManaged/ConfigTypes.fs b/MMManaged/ConfigTypes.fs
new file mode 100644
index 00000000..1881eca2
--- /dev/null
+++ b/MMManaged/ConfigTypes.fs
@@ -0,0 +1,143 @@
+namespace ModelMod
+
+open System
+
+module ConfigTypes =
+ /// Contains the name of all available input profiles. An input profile is just a set of keybindings for
+ /// controlling ModelMod in games. Different games and systems require different input layouts, so that
+ /// ModelMod doesn't interfere too much with the game. Some games make heavy use of the F keys, for instance,
+ /// so the punctuation layout is a better choice. Its expected that some games won't work well with either of
+ /// these, and some new layouts will need to be defined.
+ /// The managed code generally doesn't care about the precise definition of each layout, with the exception of
+ /// The launcher app, which describes the layout in the UI. Native code (specificaly RenderState.cpp) is
+ /// responsible for actually setting up the bindings.
+ module InputProfiles =
+ let PunctRock = "PunctuationKeys"
+ let FItUp = "FKeys"
+
+ let ValidProfiles = [ PunctRock; FItUp ]
+
+ let DefaultProfile = FItUp
+
+ let isValid (profile:string) =
+ ValidProfiles |> List.exists (fun p -> p.ToLowerInvariant() = profile.ToLowerInvariant())
+
+ /// Contains settings specific to a particular game. Usually these settings relate to how a game lays out geometry data
+ /// in D3D memory.
+ type GameProfile = {
+ /// Controls the order in which normal vector components are written to D3D buffers.
+ /// False: XYZW; True: ZYXW
+ ReverseNormals: bool
+ /// Controls whether tangent space updates are globally enabled or disabled. Whatever this setting is, mods
+ /// can opt in or out by setting "UpdateTangentSpace" to true or false in their yaml files.
+ UpdateTangentSpace: bool
+ /// Command line arguments that should be passed to the game when launched.
+ CommandLineArguments: string
+ /// An alternate name for the game data directory data directory in case the exe base name does not map to any extant directory.
+ /// Can also be a full absolute path.
+ DataPathName: string
+ }
+
+ let DefaultGameProfile = {
+ ReverseNormals = false
+ UpdateTangentSpace = true
+ CommandLineArguments = ""
+ DataPathName = ""
+ }
+
+ /// A run config for modelmod. These are stored in the registry.
+ type RunConfig = {
+ /// Reg key that this profile is stored under, e.g "Profile0000"
+ ProfileKeyName: string
+ /// Friendly name for profile (if missing, defaults to exe base name)
+ ProfileName: string
+ /// Path to exe
+ ExePath: string
+ /// If true, mods will be load and displayed on startup; otherwise they must be loaded
+ /// and displayed manually with keyboard commands
+ LoadModsOnStart: bool
+ /// Whether the current/next run is in full (snapshot) mode or playback only. This is really for
+ /// future use, since right now we don't have a separate "playback" mode (the idea is that we could be
+ /// slightly more efficient by disabling certain things, like shader constant tracking,
+ /// when we know we are only playing back mods).
+ RunModeFull: bool
+ /// Input profile (i.e.: input key layout)
+ InputProfile: string
+ /// Snapshot profile (i.e.: model transforms for snapshot)
+ SnapshotProfile: string
+ /// Game profile
+ GameProfile: GameProfile
+ /// Doc root for this profile. Currently ignored.
+ DocRoot: string
+ /// Period of time that the Loader will wait for the game to start before exiting.
+ LaunchWindow: int
+ /// MinimumFPS desired. Below this number, modelmod will temporarily shut off mod rendering in an effort to improve FPS.
+ MinimumFPS: int
+ }
+
+ /// When no run configuration is available in the registry, this is what is used. The Input and Snapshot
+ /// modules define their own defaults. The default DocRoot is \ModelMod.
+ let DefaultRunConfig = {
+ ProfileKeyName = ""
+ ProfileName = ""
+ ExePath = ""
+ RunConfig.RunModeFull = true
+ LoadModsOnStart = true
+ InputProfile = ""
+ SnapshotProfile = ""
+ GameProfile = DefaultGameProfile
+ DocRoot = System.IO.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments),"ModelMod")
+ LaunchWindow = 15
+ MinimumFPS = 28
+ }
+
+ /// Value for AdjustBlendWeights that applies the blend weight sum-to-1.0 fix (adds deficit to X component).
+ let AdjustBlendWeightsDefault = "addx"
+
+ /// A snapshot profile controls what types of data transformations
+ /// (typically vertex position and uv coordinates) that are applied by the snapshotter. These are typically used to
+ /// position the snapshotted mesh in a location that is convenient for use in a 3D tool (therefore, different tools may
+ /// need different profiles for the same game). The transforms are automatically reversed on load so that the data is
+ /// in the correct space for the game.
+ /// More recently the profile has been extended to specify how certain parts of the mesh data (e.g tangent space vectors)
+ /// should be interpreted, both during snapshot and mod load.
+ type SnapProfile() =
+ let mutable name:string = "";
+ let mutable posX:ResizeArray = new ResizeArray();
+ let mutable uvX:ResizeArray = new ResizeArray();
+ let mutable flipTangent:bool = false;
+ let mutable vecEncoding:string = "";
+ let mutable blendIndexInColor1:bool = false;
+ let mutable blendWeightInColor2:bool = false;
+ let mutable adjustBlendWeights:string = AdjustBlendWeightsDefault;
+
+ static member Create(name,posX,uvX,flipTangent,vecEncoding:string,blendIndexInColor1:bool,blendWeightInColor2:bool):SnapProfile =
+ let p = new SnapProfile()
+ p.Name <- name
+ p.PosXForm <- posX
+ p.UVXForm <- uvX
+ p.FlipTang <- flipTangent
+ p.VecEncoding <- vecEncoding
+ p.BlendIndexInColor1 <- blendIndexInColor1
+ p.BlendWeightInColor2 <- blendWeightInColor2
+ p
+
+ member x.Name with get() = name and set v = name <- v
+ member x.PosXForm with get() = posX and set v = posX <- v
+ member x.UVXForm with get() = uvX and set v = uvX <- v
+ member x.FlipTang with get() = flipTangent and set v = flipTangent <- v
+ member x.VecEncoding with get() = vecEncoding and set v = vecEncoding <- v
+ member x.BlendIndexInColor1 with get() = blendIndexInColor1 and set v = blendIndexInColor1 <- v
+ member x.BlendWeightInColor2 with get() = blendWeightInColor2 and set v = blendWeightInColor2 <- v
+ member x.AdjustBlendWeights with get() = adjustBlendWeights and set v = adjustBlendWeights <- v
+
+ member x.IsPackedVec() = vecEncoding.Trim().ToLowerInvariant() = "packed"
+ member x.IsOctaVec() = vecEncoding.Trim().ToLowerInvariant() = "octa"
+ override x.ToString() =
+ sprintf "[SnapshotProfile: %s; pos: %A; uv: %A, fliptangent: %A, vecencoding: %A, blendindexincolor1: %A, blendweightincolor2: %A, adjustblendweights: %A]" name posX uvX flipTangent vecEncoding blendIndexInColor1 blendWeightInColor2 adjustBlendWeights
+
+ let EmptySnapProfile = SnapProfile.Create("",new ResizeArray(),new ResizeArray(),false,"",false,false)
+
+ /// This profile should always exist in SnapshotProfiles.yaml.
+ /// If it does not, new game profiles will be created with an empty snapshot profile (not an error, but not desirable either)
+ let DefaultSnapProfileName = "Profile1"
\ No newline at end of file
diff --git a/MMManaged/CoreTypes.fs b/MMManaged/CoreTypes.fs
index 6f46b35e..a83eabff 100644
--- a/MMManaged/CoreTypes.fs
+++ b/MMManaged/CoreTypes.fs
@@ -29,25 +29,6 @@ module CoreState =
// Run time DLL context, set by Interop.Main
let mutable Context = ""
-/// Contains the name of all available input profiles. An input profile is just a set of keybindings for
-/// controlling ModelMod in games. Different games and systems require different input layouts, so that
-/// ModelMod doesn't interfere too much with the game. Some games make heavy use of the F keys, for instance,
-/// so the punctuation layout is a better choice. Its expected that some games won't work well with either of
-/// these, and some new layouts will need to be defined.
-/// The managed code generally doesn't care about the precise definition of each layout, with the exception of
-/// The launcher app, which describes the layout in the UI. Native code (specificaly RenderState.cpp) is
-/// responsible for actually setting up the bindings.
-module InputProfiles =
- let PunctRock = "PunctuationKeys"
- let FItUp = "FKeys"
-
- let ValidProfiles = [ PunctRock; FItUp ]
-
- let DefaultProfile = FItUp
-
- let isValid (profile:string) =
- ValidProfiles |> List.exists (fun p -> p.ToLowerInvariant() = profile.ToLowerInvariant())
-
module CoreTypes =
// ------------------------------------------------------------------------
@@ -68,78 +49,6 @@ module CoreTypes =
member v.Z = z
member v.W = w
- // ------------------------------------------------------------------------
- // Configuration types
-
- /// Contains settings specific to a particular game. Usually these settings relate to how a game lays out geometry data
- /// in D3D memory.
- type GameProfile = {
- /// Controls the order in which normal vector components are written to D3D buffers.
- /// False: XYZW; True: ZYXW
- ReverseNormals: bool
- /// Controls whether tangent space updates are globally enabled or disabled. Whatever this setting is, mods
- /// can opt in or out by setting "UpdateTangentSpace" to true or false in their yaml files.
- UpdateTangentSpace: bool
- /// Command line arguments that should be passed to the game when launched.
- CommandLineArguments: string
- /// An alternate name for the game data directory data directory in case the exe base name does not map to any extant directory.
- /// Can also be a full absolute path.
- DataPathName: string
- }
-
- let DefaultGameProfile = {
- ReverseNormals = false
- UpdateTangentSpace = true
- CommandLineArguments = ""
- DataPathName = ""
- }
-
- /// A run config for modelmod. These are stored in the registry.
- type RunConfig = {
- /// Reg key that this profile is stored under, e.g "Profile0000"
- ProfileKeyName: string
- /// Friendly name for profile (if missing, defaults to exe base name)
- ProfileName: string
- /// Path to exe
- ExePath: string
- /// If true, mods will be load and displayed on startup; otherwise they must be loaded
- /// and displayed manually with keyboard commands
- LoadModsOnStart: bool
- /// Whether the current/next run is in full (snapshot) mode or playback only. This is really for
- /// future use, since right now we don't have a separate "playback" mode (the idea is that we could be
- /// slightly more efficient by disabling certain things, like shader constant tracking,
- /// when we know we are only playing back mods).
- RunModeFull: bool
- /// Input profile (i.e.: input key layout)
- InputProfile: string
- /// Snapshot profile (i.e.: model transforms for snapshot)
- SnapshotProfile: string
- /// Game profile
- GameProfile: GameProfile
- /// Doc root for this profile. Currently ignored.
- DocRoot: string
- /// Period of time that the Loader will wait for the game to start before exiting.
- LaunchWindow: int
- /// MinimumFPS desired. Below this number, modelmod will temporarily shut off mod rendering in an effort to improve FPS.
- MinimumFPS: int
- }
-
- /// When no run configuration is available in the registry, this is what is used. The Input and Snapshot
- /// modules define their own defaults. The default DocRoot is \ModelMod.
- let DefaultRunConfig = {
- ProfileKeyName = ""
- ProfileName = ""
- ExePath = ""
- RunConfig.RunModeFull = true
- LoadModsOnStart = true
- InputProfile = ""
- SnapshotProfile = ""
- GameProfile = DefaultGameProfile
- DocRoot = System.IO.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments),"ModelMod")
- LaunchWindow = 15
- MinimumFPS = 28
- }
-
// ------------------------------------------------------------------------
// Mod and ref data
@@ -217,59 +126,13 @@ module CoreTypes =
AdjustBlendWeights: string
}
- /// Value for AdjustBlendWeights that applies the blend weight sum-to-1.0 fix (adds deficit to X component).
- let AdjustBlendWeightsDefault = "addx"
-
/// Default read flags; used when no overriding flags are specified.
let DefaultReadFlags = {
ReadMaterialFile = false
ReverseTransform = true
- AdjustBlendWeights = AdjustBlendWeightsDefault
+ AdjustBlendWeights = ConfigTypes.AdjustBlendWeightsDefault
}
- /// A snapshot profile controls what types of data transformations
- /// (typically vertex position and uv coordinates) that are applied by the snapshotter. These are typically used to
- /// position the snapshotted mesh in a location that is convenient for use in a 3D tool (therefore, different tools may
- /// need different profiles for the same game). The transforms are automatically reversed on load so that the data is
- /// in the correct space for the game.
- /// More recently the profile has been extended to specify how certain parts of the mesh data (e.g tangent space vectors)
- /// should be interpreted, both during snapshot and mod load.
- type SnapProfile() =
- let mutable name:string = "";
- let mutable posX:ResizeArray = new ResizeArray();
- let mutable uvX:ResizeArray = new ResizeArray();
- let mutable flipTangent:bool = false;
- let mutable vecEncoding:string = "";
- let mutable blendIndexInColor1:bool = false;
- let mutable blendWeightInColor2:bool = false;
- let mutable adjustBlendWeights:string = AdjustBlendWeightsDefault;
-
- static member Create(name,posX,uvX,flipTangent,vecEncoding:string,blendIndexInColor1:bool,blendWeightInColor2:bool):SnapProfile =
- let p = new SnapProfile()
- p.Name <- name
- p.PosXForm <- posX
- p.UVXForm <- uvX
- p.FlipTang <- flipTangent
- p.VecEncoding <- vecEncoding
- p.BlendIndexInColor1 <- blendIndexInColor1
- p.BlendWeightInColor2 <- blendWeightInColor2
- p
-
- member x.Name with get() = name and set v = name <- v
- member x.PosXForm with get() = posX and set v = posX <- v
- member x.UVXForm with get() = uvX and set v = uvX <- v
- member x.FlipTang with get() = flipTangent and set v = flipTangent <- v
- member x.VecEncoding with get() = vecEncoding and set v = vecEncoding <- v
- member x.BlendIndexInColor1 with get() = blendIndexInColor1 and set v = blendIndexInColor1 <- v
- member x.BlendWeightInColor2 with get() = blendWeightInColor2 and set v = blendWeightInColor2 <- v
- member x.AdjustBlendWeights with get() = adjustBlendWeights and set v = adjustBlendWeights <- v
-
- member x.IsPackedVec() = vecEncoding.Trim().ToLowerInvariant() = "packed"
- member x.IsOctaVec() = vecEncoding.Trim().ToLowerInvariant() = "octa"
-
- override x.ToString() =
- sprintf "[SnapshotProfile: %s; pos: %A; uv: %A, fliptangent: %A, vecencoding: %A, blendindexincolor1: %A, blendweightincolor2: %A, adjustblendweights: %A]" name posX uvX flipTangent vecEncoding blendIndexInColor1 blendWeightInColor2 adjustBlendWeights
-
/// Basic storage for everything that we consider to be "mesh data". This is intentionally pretty close to the
/// renderer level; i.e. we don't have fields like "NormalMap" because the texture stage used for will vary
/// across games or even within the same game. Generally if you want to customize a texture its up to you to make
@@ -365,7 +228,7 @@ module CoreTypes =
/// regenerates even if tangent space is globally disabled in the game profile. When left unspecified the global default is used.
UpdateTangentSpace: bool option
/// Snapshot profile; optional since many older mods will not have this.
- Profile: SnapProfile option
+ Profile: ConfigTypes.SnapProfile option
/// Optional CRC32 of the stream-0 vertex buffer bound at the time of
/// the original snapshot (algorithm: `crc32-full`). Can be used as a
/// secondary mesh identifier to disambiguate mods that share the same
diff --git a/MMManaged/MMManaged.dotnet.fsproj b/MMManaged/MMManaged.dotnet.fsproj
index 57c47b45..8bf54b1b 100644
--- a/MMManaged/MMManaged.dotnet.fsproj
+++ b/MMManaged/MMManaged.dotnet.fsproj
@@ -36,6 +36,7 @@
+
diff --git a/MMManaged/MMManaged.fsproj b/MMManaged/MMManaged.fsproj
index 82568682..ca3ce605 100644
--- a/MMManaged/MMManaged.fsproj
+++ b/MMManaged/MMManaged.fsproj
@@ -62,6 +62,7 @@
+
diff --git a/MMManaged/MeshUtil.fs b/MMManaged/MeshUtil.fs
index 3e0898a4..5bb478ba 100644
--- a/MMManaged/MeshUtil.fs
+++ b/MMManaged/MeshUtil.fs
@@ -127,7 +127,7 @@ module MeshUtil =
let shouldAdjustBlendWeights =
let abw = flags.AdjustBlendWeights.Trim().ToLowerInvariant()
- abw = "" || abw = AdjustBlendWeightsDefault
+ abw = "" || abw = ConfigTypes.AdjustBlendWeightsDefault
let makeBlendVectors (components:(int32 * float32)[] option) =
match components with
diff --git a/MMManaged/ModDB.fs b/MMManaged/ModDB.fs
index 0fcb6a3d..fa559739 100644
--- a/MMManaged/ModDB.fs
+++ b/MMManaged/ModDB.fs
@@ -31,7 +31,7 @@ open StartConf
open CoreTypes
open InteropTypes
open VertexTypes
-open SnapshotProfile
+open SnapshotProfileInterop
/// Contains the "Mod Database"; functions for reading yaml, mmobj, and other files and storing them in memory.
module ModDB =
@@ -199,13 +199,13 @@ module ModDB =
// load profile early so it can influence mesh read flags
let profile =
match node |> Yaml.getOptionalValue "Profile" |> Yaml.toOptionalMapping with
- | Some(profile) -> Some(loadSingleProfile "" profile)
+ | Some(profile) -> Some(SnapshotProfileLoad.loadSingleProfile "" profile)
| None -> None
let mutable numOverrideTextures = 0
let mutable fullMeshPath = ""
let meshReadFlags =
- let abw = match profile with Some(p) -> p.AdjustBlendWeights | None -> AdjustBlendWeightsDefault
+ let abw = match profile with Some(p) -> p.AdjustBlendWeights | None -> ConfigTypes.AdjustBlendWeightsDefault
{ CoreTypes.DefaultReadFlags with AdjustBlendWeights = abw }
let mesh,modType,weightMode,attrs =
let sType = (node |> Yaml.getFirstValue ["modtype"; "meshtype"] |> Yaml.toString).ToLower().Trim()
@@ -418,11 +418,11 @@ module ModDB =
let profile =
match node |> Yaml.getOptionalValue "Profile" |> Yaml.toOptionalMapping with
- | Some(profile) -> Some(loadSingleProfile "" profile)
+ | Some(profile) -> Some(SnapshotProfileLoad.loadSingleProfile "" profile)
| None -> None
let meshReadFlags =
- let abw = match profile with Some(p) -> p.AdjustBlendWeights | None -> AdjustBlendWeightsDefault
+ let abw = match profile with Some(p) -> p.AdjustBlendWeights | None -> ConfigTypes.AdjustBlendWeightsDefault
{ CoreTypes.DefaultReadFlags with AdjustBlendWeights = abw }
let doMeshLoad() =
let mesh = MeshDiskCache.loadMesh (meshFullPath,ModType.Reference,meshReadFlags,binCacheDir)
diff --git a/MMManaged/ModDBInterop.fs b/MMManaged/ModDBInterop.fs
index f5777fa5..ae2f9b12 100644
--- a/MMManaged/ModDBInterop.fs
+++ b/MMManaged/ModDBInterop.fs
@@ -110,10 +110,10 @@ module ModDBInterop =
| e ->
log.Error "%A" e
{
- RunModeFull = CoreTypes.DefaultRunConfig.RunModeFull
- LoadModsOnStart = CoreTypes.DefaultRunConfig.LoadModsOnStart
- InputProfile = CoreTypes.DefaultRunConfig.InputProfile
- MinimumFPS = CoreTypes.DefaultRunConfig.MinimumFPS
+ RunModeFull = ConfigTypes.DefaultRunConfig.RunModeFull
+ LoadModsOnStart = ConfigTypes.DefaultRunConfig.LoadModsOnStart
+ InputProfile = ConfigTypes.DefaultRunConfig.InputProfile
+ MinimumFPS = ConfigTypes.DefaultRunConfig.MinimumFPS
ProfileKey = ""
}
@@ -263,7 +263,7 @@ module ModDBInterop =
let profile =
match meshrel.DBMod.Profile with
| None -> EmptyModSnapProfile
- | Some(p) -> SnapshotProfile.toInteropStruct p
+ | Some(p) -> SnapshotProfileInterop.toInteropStruct p
let vbChecksum,vbChecksumSet =
match meshrel.DBMod.VBChecksum with
diff --git a/MMManaged/RegConfig.fs b/MMManaged/RegConfig.fs
index 9a2a73a7..2ab520a3 100644
--- a/MMManaged/RegConfig.fs
+++ b/MMManaged/RegConfig.fs
@@ -20,7 +20,7 @@ open System
open System.IO
open Microsoft.Win32
-open CoreTypes
+open ConfigTypes
/// List of all the reg value names that we set (in HKCU/Software/ModelMod)
module RegKeys =
@@ -231,7 +231,7 @@ module RegConfig =
({
ProfileKeyName = profKey
ProfileName = profSave RegKeys.ProfName conf.ProfileName
- CoreTypes.RunConfig.ExePath = profSave RegKeys.ProfExePath conf.ExePath
+ ConfigTypes.RunConfig.ExePath = profSave RegKeys.ProfExePath conf.ExePath
RunModeFull = profSave RegKeys.ProfRunModeFull (boolAsDword conf.RunModeFull) |> dwordAsBool
LoadModsOnStart = profSave RegKeys.ProfLoadModsOnStart (boolAsDword conf.LoadModsOnStart) |> dwordAsBool
InputProfile = profSave RegKeys.ProfInputProfile conf.InputProfile
@@ -293,7 +293,7 @@ module RegConfig =
ProfileKeyName = profileKeyName
ProfileName = regget(profPath,RegKeys.ProfName,DefaultRunConfig.ProfileName) :?> string
- CoreTypes.RunConfig.ExePath = regget(profPath,RegKeys.ProfExePath,DefaultRunConfig.ExePath) :?> string
+ ConfigTypes.RunConfig.ExePath = regget(profPath,RegKeys.ProfExePath,DefaultRunConfig.ExePath) :?> string
RunModeFull = dwordAsBool ( regget(profPath,RegKeys.ProfRunModeFull, (boolAsDword DefaultRunConfig.RunModeFull)) :?> int )
LoadModsOnStart = dwordAsBool ( regget(profPath,RegKeys.ProfLoadModsOnStart, (boolAsDword DefaultRunConfig.LoadModsOnStart)) :?> int)
InputProfile = regget(profPath,RegKeys.ProfInputProfile, DefaultRunConfig.InputProfile) :?> string
diff --git a/MMManaged/Snapshot.fs b/MMManaged/Snapshot.fs
index 1d830e49..1479ff1e 100644
--- a/MMManaged/Snapshot.fs
+++ b/MMManaged/Snapshot.fs
@@ -39,6 +39,7 @@ type Device11 = SharpDX.Direct3D11.Device
open YamlDotNet.Serialization
open YamlDotNet.Serialization.NamingConventions
+open ConfigTypes
open CoreTypes
open FSharp.Core
@@ -150,7 +151,7 @@ module Snapshot =
}
type SnapMeta() =
- let mutable profile:CoreTypes.SnapProfile = SnapshotProfile.EmptyProfile
+ let mutable profile:ConfigTypes.SnapProfile = ConfigTypes.EmptySnapProfile
let mutable context:string = ""
let mutable vbChecksumAlgo:string = ""
let mutable vbChecksum:string = ""
@@ -173,7 +174,7 @@ module Snapshot =
/// Reads a vertex element. Uses the read output functions to pipe the data to an appropriate handler
/// function, depending on the type.
- let private readElement (snapProfile:CoreTypes.SnapProfile) (fns:ReadOutputFunctions) (ignoreFns:ReadOutputFunctions) reader (el:VertexTypes.MMVertexElement) =
+ let private readElement (snapProfile:ConfigTypes.SnapProfile) (fns:ReadOutputFunctions) (ignoreFns:ReadOutputFunctions) reader (el:VertexTypes.MMVertexElement) =
let outputFns = fns // keep reference to real output functions
let fns =
if el.SemanticIndex = 0 then
@@ -718,7 +719,7 @@ module Snapshot =
|> function
| None ->
log.Warn "No snap profile found for: '%A', using empty profile (no transforms)" State.Data.Conf.SnapshotProfile
- SnapshotProfile.EmptyProfile
+ ConfigTypes.EmptySnapProfile
| Some s ->
log.Info "Applying snap profile: %A" s
s
diff --git a/MMManaged/SnapshotProfile.fs b/MMManaged/SnapshotProfile.fs
deleted file mode 100644
index ea9267eb..00000000
--- a/MMManaged/SnapshotProfile.fs
+++ /dev/null
@@ -1,140 +0,0 @@
-namespace ModelMod
-
-open FSharp.Core
-
-open System
-open System.IO
-
-open CoreTypes
-
-/// SnapshotProfiles are loaded from yaml files in the "SnapshotProfiles" subdirectory of the modelmod installation folder.
-module SnapshotProfile =
- let private log = Logging.getLogger("SnapshotProfile")
-
- let EmptyProfile = SnapProfile.Create("",new ResizeArray(),new ResizeArray(),false,"",false,false)
-
- /// This profile should always exist in SnapshotProfiles.yaml.
- /// If it does not, new game profiles will be created with an empty snapshot profile (not an error, but not desirable either)
- let DefaultProfileName = "Profile1"
-
- let loadSingleProfile (name:string) (values:YamlDotNet.RepresentationModel.YamlMappingNode) =
- let getStrArray def key =
- values
- |> Yaml.getOptionalValue key
- |> Yaml.toOptionalSequence
- |> function
- | None -> def
- | Some s -> s |> Seq.map Yaml.toString |> Array.ofSeq |> fun a -> new ResizeArray(a)
-
- let mutable posX = getStrArray (new ResizeArray()) "pos"
- if posX.Count = 0 then
- posX <- getStrArray (new ResizeArray()) "PosXForm"
- let mutable uvX = getStrArray (new ResizeArray()) "uv"
- if uvX.Count = 0 then
- uvX <- getStrArray (new ResizeArray()) "UVXForm"
-
- let mutable flipTang = values |> Yaml.getOptionalBool "flipTangent"
- if flipTang.IsNone then
- flipTang <- values |> Yaml.getOptionalBool "FlipTang"
- let flipTang = Option.defaultValue false flipTang
-
- let mutable vecEncoding = values |> Yaml.getOptionalString "vecEncoding"
- let vecEncoding = Option.defaultValue "" vecEncoding
-
- let blendIndexInColor1 = values |> Yaml.getOptionalBool "BlendIndexInColor1" |> Option.defaultValue false
- let blendWeightInColor2 = values |> Yaml.getOptionalBool "BlendWeightInColor2" |> Option.defaultValue false
-
- let adjustBlendWeights = values |> Yaml.getOptionalString "AdjustBlendWeights" |> Option.defaultValue AdjustBlendWeightsDefault
-
- let pname = values |> Yaml.getOptionalString "Name" |> Option.defaultValue name
-
- let p = SnapProfile.Create(pname,posX,uvX,flipTang,vecEncoding,blendIndexInColor1,blendWeightInColor2)
- p.AdjustBlendWeights <- adjustBlendWeights
- p
-
- let toInteropStruct (profile: CoreTypes.SnapProfile): InteropTypes.ModSnapProfile =
- // Constants for field size limits
- let maxSnapProfileStrLength = 255
-
- // Helper function to trim a string and log a warning if exceeds the max length
- let mutable somethingTrimmed = false
- let trimAndLog maxSize fieldName (s: string) =
- if s.Length > maxSize then
- somethingTrimmed <- true
- log.Error "Field '%s' exceeded max length of %d characters." fieldName maxSize
- s.Substring(0, maxSize)
- else
- s
-
- // Trim the necessary fields
- let trimmedName = trimAndLog maxSnapProfileStrLength "Name" profile.Name
- let trimmedVecEncoding = trimAndLog maxSnapProfileStrLength "VecEncoding" profile.VecEncoding
-
- // Transformations for PosX and UVX
- let processTransforms transforms maxLength =
- transforms
- |> Seq.map InteropTypes.makeXFormString
- |> Seq.toArray
- |> fun arr ->
- if arr.Length < maxLength then
- // Create an array of empty transform strings to fill up to the max length
- let fillArray = Array.init (maxLength - arr.Length) (fun _ -> InteropTypes.makeXFormString "")
- // Concatenate the two arrays
- Array.append arr fillArray
- else
- arr
-
- let posXTransformed = processTransforms profile.PosXForm InteropTypes.MaxModSnapProfileXFormLen
- let uvXTransformed = processTransforms profile.UVXForm InteropTypes.MaxModSnapProfileXFormLen
-
- // Check if any transform exceeds the capacity
- let isValid =
- not somethingTrimmed &&
- posXTransformed.Length <= InteropTypes.MaxModSnapProfileXFormLen &&
- uvXTransformed.Length <= InteropTypes.MaxModSnapProfileXFormLen
-
- // If valid, build the struct; otherwise, return an invalid struct
- if isValid then
- {
- Valid = true
- Name = trimmedName
- PosXLength = posXTransformed.Length
- PosX = posXTransformed
- UVXLength = uvXTransformed.Length
- UVX = uvXTransformed
- FlipTangent = profile.FlipTang
- VecEncoding = trimmedVecEncoding
- BlendIndexInColor1 = profile.BlendIndexInColor1
- BlendWeightInColor2 = profile.BlendWeightInColor2
- }
- else
- log.Error "Profile has too many transforms and will be marked as invalid."
- {
- InteropTypes.EmptyModSnapProfile with
- Valid = false
- Name = trimmedName
- }
-
- /// Returns a map of all available profiles. Throws exception if the profiles cannot be loaded.
- let GetAll rootDir =
- let pDir = Path.Combine(rootDir, "SnapshotProfiles")
- if not (Directory.Exists(pDir)) then
- failwithf "Profile directory does not exist %A" pDir
- let profiles = new ResizeArray<_>()
-
- Directory.GetFiles(pDir)
- |> Array.filter (fun fn -> Path.GetExtension(fn).ToLowerInvariant() = ".yaml")
- |> Array.iter (fun fn ->
- let docs = Yaml.load fn
-
- for d in docs do
- let rootMap = Yaml.toMapping "expected a sequence" d.RootNode
- // each file can contain multiple profiles, walk each
- for p in rootMap.Children do
- let pname = p.Key |> Yaml.toString
- let pvals = p.Value |> Yaml.toMapping "expected a mapping"
- let prof = loadSingleProfile pname pvals
- profiles.Add(pname,prof)
- )
- profiles.ToArray() |> Map.ofArray
-
diff --git a/MMManaged/SnapshotProfileInterop.fs b/MMManaged/SnapshotProfileInterop.fs
new file mode 100644
index 00000000..bcafbcd8
--- /dev/null
+++ b/MMManaged/SnapshotProfileInterop.fs
@@ -0,0 +1,82 @@
+namespace ModelMod
+
+open FSharp.Core
+
+open System
+open System.IO
+
+open ConfigTypes
+
+/// SnapshotProfiles are loaded from yaml files in the "SnapshotProfiles" subdirectory of the modelmod installation folder.
+/// This module serializes it to a native interop structure.
+/// It is separate from the loader (`SnapshotProfileLoad`) so that MMLaunch can include that directly; it doesn't need this
+/// and can't include it since this depends indirectly on CoreTypes and therefore SharpDX/Monogame which don't build under
+/// MMLaunch
+module SnapshotProfileInterop =
+ let private log = Logging.getLogger("SnapshotProfileInterop")
+
+ let toInteropStruct (profile: SnapProfile): InteropTypes.ModSnapProfile =
+ // Constants for field size limits
+ let maxSnapProfileStrLength = 255
+
+ // Helper function to trim a string and log a warning if exceeds the max length
+ let mutable somethingTrimmed = false
+ let trimAndLog maxSize fieldName (s: string) =
+ if s.Length > maxSize then
+ somethingTrimmed <- true
+ log.Error "Field '%s' exceeded max length of %d characters." fieldName maxSize
+ s.Substring(0, maxSize)
+ else
+ s
+
+ // Trim the necessary fields
+ let trimmedName = trimAndLog maxSnapProfileStrLength "Name" profile.Name
+ let trimmedVecEncoding = trimAndLog maxSnapProfileStrLength "VecEncoding" profile.VecEncoding
+
+ // Transformations for PosX and UVX
+ let processTransforms transforms maxLength =
+ transforms
+ |> Seq.map InteropTypes.makeXFormString
+ |> Seq.toArray
+ |> fun arr ->
+ if arr.Length < maxLength then
+ // Create an array of empty transform strings to fill up to the max length
+ let fillArray = Array.init (maxLength - arr.Length) (fun _ -> InteropTypes.makeXFormString "")
+ // Concatenate the two arrays
+ Array.append arr fillArray
+ else
+ arr
+
+ let posXTransformed = processTransforms profile.PosXForm InteropTypes.MaxModSnapProfileXFormLen
+ let uvXTransformed = processTransforms profile.UVXForm InteropTypes.MaxModSnapProfileXFormLen
+
+ // Check if any transform exceeds the capacity
+ let isValid =
+ not somethingTrimmed &&
+ posXTransformed.Length <= InteropTypes.MaxModSnapProfileXFormLen &&
+ uvXTransformed.Length <= InteropTypes.MaxModSnapProfileXFormLen
+
+ // If valid, build the struct; otherwise, return an invalid struct
+ if isValid then
+ {
+ Valid = true
+ Name = trimmedName
+ PosXLength = posXTransformed.Length
+ PosX = posXTransformed
+ UVXLength = uvXTransformed.Length
+ UVX = uvXTransformed
+ FlipTangent = profile.FlipTang
+ VecEncoding = trimmedVecEncoding
+ BlendIndexInColor1 = profile.BlendIndexInColor1
+ BlendWeightInColor2 = profile.BlendWeightInColor2
+ }
+ else
+ log.Error "Profile has too many transforms and will be marked as invalid."
+ {
+ InteropTypes.EmptyModSnapProfile with
+ Valid = false
+ Name = trimmedName
+ }
+
+
+
diff --git a/MMManaged/SnapshotProfileLoad.fs b/MMManaged/SnapshotProfileLoad.fs
new file mode 100644
index 00000000..efbe0e13
--- /dev/null
+++ b/MMManaged/SnapshotProfileLoad.fs
@@ -0,0 +1,71 @@
+namespace ModelMod
+
+open FSharp.Core
+
+open System
+open System.IO
+
+open ConfigTypes
+open Yaml
+
+/// SnapshotProfiles are loaded from yaml files in the "SnapshotProfiles" subdirectory of the modelmod installation folder.
+module SnapshotProfileLoad =
+ let private log = Logging.getLogger("SnapshotProfileLoad")
+
+ let loadSingleProfile (name:string) (values:YamlDotNet.RepresentationModel.YamlMappingNode) =
+ let getStrArray def key =
+ values
+ |> Yaml.getOptionalValue key
+ |> Yaml.toOptionalSequence
+ |> function
+ | None -> def
+ | Some s -> s |> Seq.map Yaml.toString |> Array.ofSeq |> fun a -> new ResizeArray(a)
+
+ let mutable posX = getStrArray (new ResizeArray()) "pos"
+ if posX.Count = 0 then
+ posX <- getStrArray (new ResizeArray()) "PosXForm"
+ let mutable uvX = getStrArray (new ResizeArray()) "uv"
+ if uvX.Count = 0 then
+ uvX <- getStrArray (new ResizeArray()) "UVXForm"
+
+ let mutable flipTang = values |> Yaml.getOptionalBool "flipTangent"
+ if flipTang.IsNone then
+ flipTang <- values |> Yaml.getOptionalBool "FlipTang"
+ let flipTang = Option.defaultValue false flipTang
+
+ let mutable vecEncoding = values |> Yaml.getOptionalString "vecEncoding"
+ let vecEncoding = Option.defaultValue "" vecEncoding
+
+ let blendIndexInColor1 = values |> Yaml.getOptionalBool "BlendIndexInColor1" |> Option.defaultValue false
+ let blendWeightInColor2 = values |> Yaml.getOptionalBool "BlendWeightInColor2" |> Option.defaultValue false
+
+ let adjustBlendWeights = values |> Yaml.getOptionalString "AdjustBlendWeights" |> Option.defaultValue AdjustBlendWeightsDefault
+
+ let pname = values |> Yaml.getOptionalString "Name" |> Option.defaultValue name
+
+ let p = SnapProfile.Create(pname,posX,uvX,flipTang,vecEncoding,blendIndexInColor1,blendWeightInColor2)
+ p.AdjustBlendWeights <- adjustBlendWeights
+ p
+
+ /// Returns a map of all available profiles. Throws exception if the profiles cannot be loaded.
+ let GetAll rootDir =
+ let pDir = Path.Combine(rootDir, "SnapshotProfiles")
+ if not (Directory.Exists(pDir)) then
+ failwithf "Profile directory does not exist %A" pDir
+ let profiles = new ResizeArray<_>()
+
+ Directory.GetFiles(pDir)
+ |> Array.filter (fun fn -> Path.GetExtension(fn).ToLowerInvariant() = ".yaml")
+ |> Array.iter (fun fn ->
+ let docs = Yaml.load fn
+
+ for d in docs do
+ let rootMap = Yaml.toMapping "expected a sequence" d.RootNode
+ // each file can contain multiple profiles, walk each
+ for p in rootMap.Children do
+ let pname = p.Key |> Yaml.toString
+ let pvals = p.Value |> Yaml.toMapping "expected a mapping"
+ let prof = loadSingleProfile pname pvals
+ profiles.Add(pname,prof)
+ )
+ profiles.ToArray() |> Map.ofArray
\ No newline at end of file
diff --git a/MMManaged/State.fs b/MMManaged/State.fs
index 4ac289a6..ac9eabb4 100644
--- a/MMManaged/State.fs
+++ b/MMManaged/State.fs
@@ -18,7 +18,7 @@ namespace ModelMod
open System.IO
-open CoreTypes
+open ConfigTypes
/// Contains mutable state, including the current configuration and data for all loaded mods.
/// This is stored here so that we don't have to pass it all over the interop barrier, which
@@ -90,10 +90,10 @@ module State =
// various muties
let mutable private _moddb = new ModDB.ModDB([],[],[])
let mutable private _rootDir = "."
- let mutable private _conf = CoreTypes.DefaultRunConfig
+ let mutable private _conf = ConfigTypes.DefaultRunConfig
let mutable private _locator = DirLocator(_rootDir,_conf)
let mutable private _loadState = InteropTypes.AsyncLoadState.NotStarted
- let mutable private _snapProfiles:Map = Map.ofList []
+ let mutable private _snapProfiles:Map = Map.ofList []
// access to the muties out side of the module goes through this, via the "Data" field below.
type StateDateAccessor() =
@@ -113,7 +113,7 @@ module State =
/// Verify the specified confiuration and install it in state. Does not load the Moddb.
/// Throws exception if confiuration is invalid.
- let validateAndSetConf (rootDir:string) (conf:CoreTypes.RunConfig): CoreTypes.RunConfig =
+ let validateAndSetConf (rootDir:string) (conf:ConfigTypes.RunConfig): ConfigTypes.RunConfig =
if not (Directory.Exists rootDir) then
failwithf "Root directory does not exist: %s" rootDir
@@ -123,7 +123,7 @@ module State =
let snapProfile =
try
- let sprofiles = SnapshotProfile.GetAll(_rootDir)
+ let sprofiles = SnapshotProfileLoad.GetAll(_rootDir)
_snapProfiles <- sprofiles
snapProfileVal <- sprofiles |> Map.tryFind conf.SnapshotProfile
@@ -153,7 +153,7 @@ module State =
/// Reload snapshot profiles from disk, using the current root dir and conf.
let reloadSnapshotProfiles() =
try
- let sprofiles = SnapshotProfile.GetAll(_rootDir)
+ let sprofiles = SnapshotProfileLoad.GetAll(_rootDir)
_snapProfiles <- sprofiles
let snapProfileVal = sprofiles |> Map.tryFind _conf.SnapshotProfile
@@ -167,7 +167,7 @@ module State =
log().Error "Error reloading snapshot profiles: %A" e
/// For use by tets
- let testSetConf(c:CoreTypes.RunConfig) =
+ let testSetConf(c:ConfigTypes.RunConfig) =
_conf <- c
_locator <- DirLocator(_rootDir,_conf)
diff --git a/MMView/Main.fs b/MMView/Main.fs
index 947440ec..9eec2111 100644
--- a/MMView/Main.fs
+++ b/MMView/Main.fs
@@ -255,7 +255,7 @@ module Main =
({
AppSettings.Window = winSettings
CamPosition = camPos
- MeshReadFlags = { ReadMaterialFile = true; ReverseTransform = transform; AdjustBlendWeights = AdjustBlendWeightsDefault }
+ MeshReadFlags = { ReadMaterialFile = true; ReverseTransform = transform; AdjustBlendWeights = ConfigTypes.AdjustBlendWeightsDefault }
})
let run(argv:string[]) =
diff --git a/build.fsx b/build.fsx
index d8f58077..ec50448a 100644
--- a/build.fsx
+++ b/build.fsx
@@ -140,13 +140,8 @@ Target "BuildNative" (fun _ ->
Target "UpdateAssembyInfo" (fun _ ->
printfn "version is %A" version
- CreateFSharpAssemblyInfo "./MMLaunch/AssemblyInfo.fs"
- [Attribute.Title "ModelMod launcher application"
- Attribute.Description ""
- Attribute.Guid "2ce8e338-7143-4f97-ab39-3e90ca50bdf2"
- Attribute.Product "ModelMod"
- Attribute.Version version
- Attribute.FileVersion version]
+ // MMLaunch is now SDK-style; the SDK auto-generates assembly attributes
+ // from in the .fsproj, so no AssemblyInfo.fs is needed/wanted.
CreateFSharpAssemblyInfo "./MMManaged/AssemblyInfo.fs"
[Attribute.Title "ModelMod managed code library"
@@ -207,10 +202,33 @@ Target "BuildFS" (fun _ ->
!! "**/*.fsproj"
-- "**/*dotnet.fsproj"
-- "**/Test.*"
+ // MMLaunch is now an SDK-style net8.0 / Avalonia project, built by
+ // BuildMMLaunch via `dotnet build`. The legacy MSBuildRelease toolchain
+ // here only knows how to build .NET Framework projects.
+ -- "MMLaunch/MMLaunch.fsproj"
|> MSBuildRelease buildBin "Build"
|> Log "BuildFS-Output: "
)
+Target "BuildMMLaunch" (fun _ ->
+ let proj = "MMLaunch/MMLaunch.fsproj"
+ let result = Shell.Exec("dotnet", sprintf "build -c Release \"%s\"" proj)
+ if result <> 0 then failwithf "dotnet build failed for %s (exit %d)" proj result
+
+ // Drop the build output into the standard buildBin so CopyStuff/Zip pick it up.
+ if not (Directory.Exists buildBin) then Directory.CreateDirectory buildBin |> ignore
+ let mmLaunchOut = "MMLaunch/bin/Release/net8.0"
+ if Directory.Exists mmLaunchOut then
+ !! (mmLaunchOut + "/*.*")
+ -- "**/*.xml"
+ -- "**/*.pdb"
+ |> CopyFiles buildBin
+ // Bring along the `runtimes/` folder for native bits like SkiaSharp/HarfBuzz/ANGLE.
+ if Directory.Exists (mmLaunchOut + "/runtimes") then
+ let files = !! (mmLaunchOut + "/runtimes/**/*.*")
+ CopyWithSubfoldersTo buildBin [files]
+)
+
Target "BuildTest" (fun _ ->
!! "**/Test.*.fsproj"
-- "**/*dotnet.fsproj"
@@ -315,8 +333,8 @@ Target "SignBuild" (fun _ ->
"Bin\ModelMod.dll";
//"Bin\MMLoader.exe";
"Bin\MeshView.exe";
- "Bin\WpfInteropSample.exe";
"Bin\MMLaunch.exe";
+ "Bin\MMLaunch.dll";
"Bin\MMManaged.dll";
"Bin\MMManaged.Engine.dll";
//"Bin\ModelModCLRAppDomain.dll";
@@ -392,6 +410,7 @@ Target "CheckConf" ignore
==> "Clean"
==> "BuildCS"
==> "BuildFS"
+ ==> "BuildMMLaunch"
==> "BuildNative"
==> "AppveyorBuild"
@@ -478,7 +497,7 @@ let doCISetup() =
let usePath = vspath @@ msbuildFSTargets
let updateList = [
- "MMLaunch/MMLaunch.fsproj"
+ // MMLaunch is SDK-style now and doesn't need the FSharpTargetsPath rewrite.
"MMManaged/MMManaged.fsproj"
"MMManaged.Engine/MMManaged.Engine.fsproj"
"MMView/MMView.fsproj"