From 0b966b557a7e7d186d8fcf973df6306285aa1e93 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 01:30:52 +0000 Subject: [PATCH 1/2] Authored by Claude: fix MMLaunch resetting MMRoot to Z:\ under Proton getMMRoot() located the ModelMod root by matching generic marker dirs (Bin/Logs/TPLib) relative to the current working directory. Under Wine/Proton the working dir can resolve to the unix root (drive Z:\), whose case-insensitive /bin matches the "Bin" marker, so getMMRoot() returned "Z:\". PeriodicUpdate then overwrote the registry MMRoot with that bogus value and the native loader could not find MMManaged.dll. Require a candidate root to actually contain a loadable MMManaged.dll (mirroring the native get_managed_dll_path search: "", Bin, Release, Debug) before accepting it; MMDotNet.sln is still trusted directly for dev checkouts. Invalid candidates now fall through to the existing registry value, so the correct root is preserved. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VL1pRPGKznkc7W3ZSPsAc2 --- MMLaunch/ProcessUtil.fs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/MMLaunch/ProcessUtil.fs b/MMLaunch/ProcessUtil.fs index baf1eb3..2bc1a46 100644 --- a/MMLaunch/ProcessUtil.fs +++ b/MMLaunch/ProcessUtil.fs @@ -57,6 +57,26 @@ module ProcessUtil = "Logs" "Bin"] + // A candidate directory is only a real MM root if the native loader could + // actually load MMManaged.dll from it. This mirrors get_managed_dll_path in + // the native util crate, which searches these same subdirs. Requiring the + // managed dll rejects false positives from generic markers -- most importantly, + // under Wine/Proton the working dir can resolve to the unix root (drive Z:\), + // whose case-insensitive "/bin" matches the "Bin" marker and previously + // produced a bogus root of "Z:\" that PeriodicUpdate wrote to the registry. + let managedDllSubdirs = [""; "Bin"; "Release"; "Debug"] + let hasManagedDll (dir:string) = + let dir = if String.IsNullOrEmpty dir then "." else dir + managedDllSubdirs + |> List.exists (fun sub -> File.Exists(Path.Combine(dir, sub, "MMManaged.dll"))) + + // MMDotNet.sln uniquely identifies a source checkout (ModelMod-specific, so no + // risk of matching a system directory), so trust it directly. Every other + // marker must be backed by a loadable MMManaged.dll. + let isValidCandidate (markerPath:string) = + markerPath.EndsWith("MMDotNet.sln") + || hasManagedDll (Path.GetDirectoryName(markerPath)) + let root = rootSearchPath |> List.tryPick (fun rootpath -> @@ -64,7 +84,7 @@ module ProcessUtil = |> List.map (fun filepath -> Path.Combine(rootpath, filepath)) |> List.tryPick (fun filepath -> //printfn "try %A" (Path.GetFullPath(filepath)) - if (Directory.Exists(filepath) || File.Exists(filepath)) then Some(filepath) else None) + if (Directory.Exists(filepath) || File.Exists(filepath)) && isValidCandidate filepath then Some(filepath) else None) ) let fullRoot = @@ -80,9 +100,9 @@ module ProcessUtil = //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 + if lastRoot <> Some(fullRoot) then lastRoot <- Some(fullRoot) - printfn "MM root changed: %A" root + printfn "MM root changed: %A" fullRoot fullRoot /// Loader isn't used anymore so this is just a placeholder until I remove the related code. From b367907247f7767402bde6c669fc79a6d862ba7f Mon Sep 17 00:00:00 2001 From: John Quigley Date: Tue, 14 Jul 2026 10:55:55 -0400 Subject: [PATCH 2/2] MMLaunch: tweak root detection fix - Warn if the determined root doesn't have the dll - Do the detection just once at startup --- MMLaunch/MainWindow.axaml.fs | 27 ++++++++++++++++++--------- MMLaunch/ProcessUtil.fs | 28 ++++++++++++++-------------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/MMLaunch/MainWindow.axaml.fs b/MMLaunch/MainWindow.axaml.fs index cef5c75..340e661 100644 --- a/MMLaunch/MainWindow.axaml.fs +++ b/MMLaunch/MainWindow.axaml.fs @@ -377,16 +377,25 @@ type MainViewModel() as self = | Some p -> setter p let mutable warnedNoRoot = false + let mutable warnedNoMMDLL = false + + let mutable rootInitted = false member x.PeriodicUpdate() = - try - let currentRoot = ProcessUtil.getMMRoot () - let regRoot = RegConfig.getMMRoot () - if currentRoot <> regRoot then RegConfig.setMMRoot currentRoot |> ignore - with ex -> - if not warnedNoRoot then - warnedNoRoot <- true - ViewModelUtil.pushDialog (sprintf "Unable to determine root dir: %A" ex) - () + if not rootInitted then + rootInitted <- true + try + let currentRoot = ProcessUtil.getMMRoot () + let regRoot = RegConfig.getMMRoot () + if currentRoot <> regRoot then RegConfig.setMMRoot currentRoot |> ignore + if not warnedNoMMDLL && not (ProcessUtil.hasManagedDll currentRoot) then + warnedNoMMDLL <- true + ViewModelUtil.pushDialog + (sprintf "Warning, current root %A has no managed DLL, the native code will not be able to load from it. Consider using regedit to clear the root in the registry" currentRoot) + with ex -> + if not warnedNoRoot then + warnedNoRoot <- true + ViewModelUtil.pushDialog (sprintf "Unable to determine root dir: %A" ex) + () x.UpdateLoaderState <| match loaderState with diff --git a/MMLaunch/ProcessUtil.fs b/MMLaunch/ProcessUtil.fs index 2bc1a46..5b3607e 100644 --- a/MMLaunch/ProcessUtil.fs +++ b/MMLaunch/ProcessUtil.fs @@ -41,6 +41,16 @@ module ProcessUtil = let mutable lastRoot:string option = None + // A candidate directory is only a real MM root if the native loader could + // actually load MMManaged.dll from it. This mirrors get_managed_dll_path in + // the native util crate, which searches these same subdirs. Requiring the specific + // DLL to exist reduces the risk of false positives. + let private managedDllSubdirs = [""; "Bin"; "Release"; "Debug"] + let hasManagedDll (dir:string) = + let dir = if String.IsNullOrEmpty dir then "." else dir + managedDllSubdirs + |> List.exists (fun sub -> File.Exists(Path.Combine(dir, sub, "MMManaged.dll"))) + 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) @@ -57,18 +67,7 @@ module ProcessUtil = "Logs" "Bin"] - // A candidate directory is only a real MM root if the native loader could - // actually load MMManaged.dll from it. This mirrors get_managed_dll_path in - // the native util crate, which searches these same subdirs. Requiring the - // managed dll rejects false positives from generic markers -- most importantly, - // under Wine/Proton the working dir can resolve to the unix root (drive Z:\), - // whose case-insensitive "/bin" matches the "Bin" marker and previously - // produced a bogus root of "Z:\" that PeriodicUpdate wrote to the registry. - let managedDllSubdirs = [""; "Bin"; "Release"; "Debug"] - let hasManagedDll (dir:string) = - let dir = if String.IsNullOrEmpty dir then "." else dir - managedDllSubdirs - |> List.exists (fun sub -> File.Exists(Path.Combine(dir, sub, "MMManaged.dll"))) + // MMDotNet.sln uniquely identifies a source checkout (ModelMod-specific, so no // risk of matching a system directory), so trust it directly. Every other @@ -92,9 +91,10 @@ module ProcessUtil = | None -> let regRoot = RegConfig.getMMRoot () if Directory.Exists(regRoot) - then regRoot + then + regRoot else - failwithf "Unable to find MM Root in filesystem from working dir %A, and reg root not set in register or does not exist: %A" + failwithf "Unable to find MM Root in filesystem from working dir %A, and reg root (%A) not set in registry or does not exist. Consider restarting this program in ModelMod root dir." (System.Environment.CurrentDirectory) regRoot //failwithf "Unable to find MM root from working dir %A" (System.Environment.CurrentDirectory)