@@ -3362,9 +3362,15 @@ class ElectronLoader {
33623362 const gameDetails = gameDb [ profile . gameId ] ;
33633363 const gamePluginFormats = gameDetails ?. pluginFormats ?? [ ] ;
33643364
3365- const existingDataSubdirs = ( await fs . readdir ( gameModDir ) ) . filter ( ( existingModFile ) => {
3366- return fs . lstatSync ( path . join ( gameModDir , existingModFile ) ) . isDirectory ( ) ;
3367- } ) ;
3365+ // Build Map of all existing data subdirs for path normalization
3366+ /** @type {Map<string, string> } */ const existingDataSubdirs = new Map ( ) ;
3367+ if ( normalizePathCasing ) {
3368+ ( await fs . readdir ( gameModDir , { recursive : true } ) ) . forEach ( ( existingModFile ) => {
3369+ if ( fs . lstatSync ( path . join ( gameModDir , /** @type {string } */ ( existingModFile ) ) ) . isDirectory ( ) ) {
3370+ existingDataSubdirs . set ( /** @type {string } */ ( existingModFile ) . toLowerCase ( ) , existingModFile ) ;
3371+ }
3372+ } ) ;
3373+ }
33683374
33693375 // Copy all mods to the gameModDir for this profile
33703376 // (Copy mods in reverse with `overwrite: false` to follow load order and allow existing manual mods in the folder to be preserved)
@@ -3390,21 +3396,16 @@ class ElectronLoader {
33903396 modFile = modFile . toLowerCase ( ) ;
33913397 }
33923398
3393- if ( root ) {
3394- // Preserve capitalization of Data directory for root mods
3395- modFile = modFile . replace ( / ^ d a t a [ \\ / ] / , `Data${ path . sep } ` ) ;
3396- } else {
3397- // Apply capitalization rules of any existing Data subdirectories to ensure only one folder is created
3398- // TODO - Also do this for root mods
3399- // TODO - Do this recursively
3400- existingDataSubdirs . forEach ( ( existingDataSubdir ) => {
3401- existingDataSubdir = `${ existingDataSubdir } ${ path . sep } ` ;
3402- const lowerSubdir = existingDataSubdir . toLowerCase ( ) ;
3403-
3404- if ( modFile . startsWith ( lowerSubdir ) ) {
3405- modFile = modFile . replace ( lowerSubdir , existingDataSubdir ) ;
3406- }
3407- } ) ;
3399+ // Apply existing capitalization rules of mod data subdirectories to ensure only one folder is created
3400+ let modFileBase = path . dirname ( modFile ) ;
3401+ while ( modFileBase !== "." && modFileBase !== path . sep ) {
3402+ const existingBase = existingDataSubdirs . get ( modFileBase . toLowerCase ( ) ) ;
3403+ if ( existingBase ) {
3404+ modFile = modFile . replace ( modFileBase , existingBase ) ;
3405+ break ;
3406+ } else {
3407+ modFileBase = path . dirname ( modFileBase ) ;
3408+ }
34083409 }
34093410 }
34103411
@@ -3612,12 +3613,22 @@ class ElectronLoader {
36123613
36133614 // Some games require processing of plugin file timestamps to enforce load order
36143615 if ( ! ! gameDetails ?. pluginListType && profile . plugins && timestampedPluginTypes . includes ( gameDetails . pluginListType ) ) {
3615- const gameModDir = path . resolve ( this . #expandPath( profile . gameInstallation . modDir ) ) ;
3616+ let gamePluginDir = this . #expandPath( profile . gameInstallation . modDir ) ;
3617+
3618+ if ( gameDetails . pluginDataRoot ) {
3619+ gamePluginDir = path . join ( gamePluginDir , gameDetails . pluginDataRoot ) ;
3620+ }
3621+
36163622 let pluginTimestamp = Date . now ( ) / 1000 | 0 ;
36173623 profile . plugins . forEach ( ( pluginRef ) => {
3618- // Set plugin order using the plugin file's "last modified" timestamp
3619- fs . utimesSync ( path . join ( gameModDir , pluginRef . plugin ) , pluginTimestamp , pluginTimestamp ) ;
3620- ++ pluginTimestamp ;
3624+ const pluginPath = path . join ( gamePluginDir , pluginRef . plugin ) ;
3625+ if ( fs . existsSync ( pluginPath ) ) {
3626+ // Set plugin order using the plugin file's "last modified" timestamp
3627+ fs . utimesSync ( pluginPath , pluginTimestamp , pluginTimestamp ) ;
3628+ ++ pluginTimestamp ;
3629+ } else {
3630+ log . warn ( "Missing plugin file" , pluginPath ) ;
3631+ }
36213632 } ) ;
36223633 }
36233634
0 commit comments