<# .SYNOPSIS Separe les modules optionnels d'un build empaquete (Docs/Migration/15_Modules_Optionnels.md). .DESCRIPTION Les modules de Content/Modules sont livres en fichiers nus a cote du jeu. Ce script garde dans le build les modules listes comme essentiels dans le fichier de configuration (Tools/modules/.json) et deplace tous les autres dans une archive zip, avec l'arborescence LifePlay\Content\Modules\\..., a extraire dans le dossier du jeu (celui de LifePlay.exe). Une notice LISEZMOI / README est ajoutee a l'archive. Le jeu gere l'absence d'un module : au demarrage, GI.removeDeletedMods retire de GI.ModEnableds tout module dont le fichier .lpmod est absent, et GI.IsModEnabled repond alors faux. Controles : chaque module essentiel doit exister ; nombre de fichiers et d'octets deplaces = contenu de l'archive (7-Zip, test d'integrite) ; aucun fichier de module optionnel ne reste dans le build. Les fichiers deplaces sont conserves dans \..\_modules_optionnels (rien n'est supprime). .PARAMETER (configuration "per_module": true, depuis la publication qui suit la V1.4) Une archive par module optionnel, -for-LifePlay-.zip (LifePlay\Content\Modules\ + notice bilingue tiree du .lpmod), dans \ ; un index INDEX.txt (nom, archive, taille, SHA-256) sert au message de diffusion (Discord). Les mods d'assets (Tools/BuildMod.ps1) se deposent dans le meme dossier. .EXAMPLE .\Tools\SplitModules.ps1 -Build D:\Game\LifePlay_Build_5.5_Shipping_V1.5 -Config Tools\modules ext.json -Out D:\Game\LifePlay_Publication .EXAMPLE .\Tools\SplitModules.ps1 -Build D:\Game\LifePlay_Build_5.5_Shipping_V1.4 -Config Tools\modules\V1.4.json -Out D:\Game\LifePlay_Publication #> param( [Parameter(Mandatory)] [string] $Build, [Parameter(Mandatory)] [string] $Config, [Parameter(Mandatory)] [string] $Out ) $ErrorActionPreference = 'Stop' $SevenZip = 'C:\Program Files\7-Zip\7z.exe' $cfg = Get-Content $Config -Raw | ConvertFrom-Json $modules = Join-Path $Build 'Windows\LifePlay\Content\Modules' if (-not (Test-Path $modules)) { throw "Dossier des modules introuvable : $modules" } # Un build de distribution ne contient aucun mod d'assets installe (sa partie texte serait livree sans ses paquets) : # ces mods sont livres par Tools/BuildMod.ps1. $assetMods = @(Get-ChildItem (Join-Path $Build 'Windows\LifePlay\Mods') -Directory -ErrorAction SilentlyContinue | ForEach-Object Name) if ($assetMods) { throw "Mod(s) d'assets installe(s) dans le build ($($assetMods -join ', ')) : utiliser un build neuf, ces mods sont livres par BuildMod.ps1" } foreach ($m in $cfg.essential) { if (-not (Test-Path (Join-Path $modules "$m\$m.lpmod"))) { throw "Module essentiel absent du build : $m" } } $optional = Get-ChildItem $modules -Directory | Where-Object { (Test-Path (Join-Path $_.FullName "$($_.Name).lpmod")) -and $_.Name -notin $cfg.essential } if (-not $optional) { Write-Host 'Aucun module optionnel dans le build.'; return } # Arborescence de l'archive : LifePlay\Content\Modules\ $stage = Join-Path (Split-Path $Build -Parent) ((Split-Path $Build -Leaf) + '_modules_optionnels') $target = Join-Path $stage 'LifePlay\Content\Modules' New-Item -ItemType Directory -Force $target | Out-Null $files = 0; $bytes = 0 foreach ($m in $optional) { $items = Get-ChildItem $m.FullName -Recurse -File $files += $items.Count; $bytes += ($items | Measure-Object Length -Sum).Sum Move-Item $m.FullName (Join-Path $target $m.Name) } $list = ($optional.Name | Sort-Object) -join ', ' if ($cfg.per_module) { # Une archive par module (publication suivant la V1.4). $dir = Join-Path $Out $(if ($cfg.out_dir) { $cfg.out_dir } else { "Modules_$($cfg.version)" }) New-Item -ItemType Directory -Force $dir | Out-Null $index = @("LifePlay Revival $($cfg.version) - optional modules / modules optionnels", '', 'Install: extract the zip into the game folder (the one containing LifePlay.exe), merging the LifePlay folder, then enable', 'the module in "Mod Manager" (title screen). Installation : extraire le zip dans le dossier du jeu, puis activer le module.', 'Adult content, adults only / contenu adulte, reserve aux adultes.', '') # Paquets : un groupe de la configuration ("groups" : name, pattern, title, description) reunit ses modules dans une # seule archive (ex. vin_* = modules de base du jeu, decision du 2026-10-03) ; chaque autre module a la sienne. $packages = @() $grouped = @() foreach ($g in @($cfg.groups)) { if (-not $g) { continue } $members = @($optional | Where-Object { $_.Name -like $g.pattern } | Sort-Object Name) if ($members) { $packages += [pscustomobject]@{ Name = $g.name; Title = $g.title; Description = $g.description; Members = $members }; $grouped += $members.Name } } foreach ($m in ($optional | Where-Object { $_.Name -notin $grouped } | Sort-Object Name)) { $packages += [pscustomobject]@{ Name = $m.Name; Title = ''; Description = ''; Members = @($m) } } $zipped = 0 foreach ($p in $packages) { $one = Join-Path $stage "_zip\$($p.Name)" New-Item -ItemType Directory -Force (Join-Path $one 'LifePlay\Content\Modules') | Out-Null $lines = @(); $expected = 0 foreach ($m in $p.Members) { $src = Join-Path $target $m.Name $lp = Get-Content (Join-Path $src "$($m.Name).lpmod") -Encoding utf8 $val = { param($k) $l = $lp | Where-Object { $_ -match "^$k\s*:" } | Select-Object -First 1; if ($l) { ($l -split ':', 2)[1].Trim() } else { '' } } $title = & $val 'MODULE_NAME'; $desc = & $val 'MODULE_DESCRIPTION'; $req = & $val 'MODULE_REQUIREMENTS' Copy-Item $src (Join-Path $one "LifePlay\Content\Modules\$($m.Name)") -Recurse $expected += @(Get-ChildItem $src -Recurse -File).Count $lines += "- $title ($($m.Name))$(if ($req) { ", requires / necessite : $req" })" if ($p.Members.Count -eq 1) { $p.Title = $title; $p.Description = $desc } } $modulesDir = if ($p.Members.Count -eq 1) { "LifePlay\Content\Modules\$($p.Members[0].Name)" } else { 'LifePlay\Content\Modules\' } $note = @("$($p.Title) - LifePlay Revival $($cfg.version)", '', $p.Description, '') + $lines + @('', 'EN: extract this archive into the game folder (the one containing LifePlay.exe), merging the LifePlay folder, then', "enable the module(s) in `"Mod Manager`" (title screen). To remove one, delete its folder $modulesDir.", 'FR : extraire cette archive dans le dossier du jeu (celui de LifePlay.exe) en fusionnant le dossier LifePlay, puis', "activer le ou les modules dans `"Mod Manager`" (ecran titre). Pour en retirer un : supprimer $modulesDir.", 'Adult content, adults only / contenu adulte, reserve aux adultes.') [IO.File]::WriteAllText((Join-Path $one 'README.txt'), ($note -join "`r`n") + "`r`n", (New-Object Text.UTF8Encoding $false)) $zip = Join-Path $dir "$($p.Name)-for-LifePlay-$($cfg.version).zip" if (Test-Path $zip) { throw "L'archive existe deja : $zip" } Push-Location $one & $SevenZip a -tzip -mx=7 $zip 'LifePlay' 'README.txt' | Out-Null Pop-Location & $SevenZip t $zip | Out-Null if ($LASTEXITCODE -ne 0) { throw "Test 7-Zip en echec : $zip" } $n = (& $SevenZip l -slt $zip | Select-String '^Folder = -').Count - 1 if ($n -ne $expected) { throw "$($p.Name) : $n fichier(s) dans l'archive pour $expected" } $zipped += $n $h = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower() $index += ('{0} ({1}{2}) : {3}, {4:N0} bytes, SHA-256 {5}' -f $p.Title, $p.Name, $(if ($p.Members.Count -gt 1) { ': ' + ($p.Members.Name -join ', ') } else { '' }), (Split-Path $zip -Leaf), (Get-Item $zip).Length, $h) Write-Host " $($p.Name) : $(Split-Path $zip -Leaf) ($($p.Members.Count) module(s), $n fichiers)" } [IO.File]::WriteAllText((Join-Path $dir 'INDEX.txt'), ($index -join "`r`n") + "`r`n", (New-Object Text.UTF8Encoding $false)) $left = Get-ChildItem $modules -Directory | Where-Object { $_.Name -in $optional.Name } Write-Host "Modules gardes dans le build : $($cfg.essential -join ', ')" Write-Host "Modules optionnels : $($optional.Count) en $($packages.Count) archive(s) dans $dir ; fichiers deplaces $files, archives $zipped ; restes dans le build $(@($left).Count)" if ($zipped -ne $files -or @($left).Count -ne 0) { throw 'Controle en echec : contenu des archives different des fichiers deplaces' } return } $readme = @" LifePlay Revival $($cfg.version) - modules optionnels / optional modules FR : extraire cette archive dans le dossier du jeu (celui qui contient LifePlay.exe), en fusionnant le dossier LifePlay. Les modules apparaissent ensuite dans le gestionnaire de mods du jeu, ou ils s'activent et se desactivent. vin_NTR est desactive par defaut. Pour retirer un module : supprimer son dossier LifePlay\Content\Modules\. EN: extract this archive into the game folder (the one containing LifePlay.exe), merging the LifePlay folder. The modules then appear in the in-game mod manager, where they can be enabled or disabled. vin_NTR is disabled by default. To remove a module, delete its LifePlay\Content\Modules\ folder. Modules : $list Contenu adulte, reserve aux adultes / adult content, adults only. "@ Set-Content (Join-Path $stage 'LISEZMOI_README.txt') $readme -Encoding utf8 New-Item -ItemType Directory -Force $Out | Out-Null $zip = Join-Path $Out $cfg.zip if (Test-Path $zip) { throw "L'archive existe deja : $zip" } Push-Location $stage & $SevenZip a -tzip -mx=7 $zip 'LifePlay' 'LISEZMOI_README.txt' | Out-Null Pop-Location & $SevenZip t $zip | Out-Null if ($LASTEXITCODE -ne 0) { throw "Test 7-Zip en echec : $zip" } $zipFiles = (& $SevenZip l -slt $zip | Select-String '^Folder = -').Count - 1 # moins la notice $left = Get-ChildItem $modules -Directory | Where-Object { $_.Name -in $optional.Name } $hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower() Write-Host "Modules gardes dans le build : $($cfg.essential -join ', ')" Write-Host "Modules optionnels : $($optional.Count) ($list)" Write-Host "Fichiers deplaces : $files ($bytes octets) ; dans l'archive : $zipFiles fichier(s) de module" Write-Host "Restes dans le build : $(@($left).Count)" Write-Host "Archive : $zip ($((Get-Item $zip).Length) octets), SHA-256 $hash" if ($zipFiles -ne $files -or @($left).Count -ne 0) { throw 'Controle en echec : contenu de l''archive different des fichiers deplaces' }