2026-10-03 07:06:48 +00:00
<#
. SYNOPSIS
Cuit un mod d 'assets et l' assemble en archive installable ( lot H3b , Docs / Migration / 16_H3_Mods_Paquets . md ) .
. DESCRIPTION
Sources : ModSources \ < Nom > \ ( < Nom > . uplugin , Content \ , Module \ ; voir Tools / ModSource . ps1 ) .
1 . controles : plugin et module ( MODULE_UNIQUEID = < Nom > ) , version de reference Releases \ < Version > \ Windows , aucun
autre plugin dans Mods \ ;
2026-10-07 13:49:56 +00:00
0 . -Anim < pack > : chaine complete d ' animations du pack ( Tools \ anim \ AnimPipeline . ps1 -Pack < pack > , lot AP7 ) ;
-From < etape > et -SansCuisson lui sont transmis ; rien d 'autre n' est execute ;
1b . -Retarget bake | rigs : recopie les clips Quaternius sur Bridget et Michael ( Tools / anim / RetargetPack . py
dans UnrealEditor . exe , Slate requis ; -RetargetConfig < json > : copie vers Saved \ anim_retarget . json , methode et pack ) . L 'editeur doit etre ferme. -RetargetOnly s' arrete avant la cuisson ;
2026-10-06 19:10:53 +00:00
1c . -Target sandbox | game ecrit Saved \ Config \ Windows \ LPAnimTarget . ini . -Open lance cette cible
( sandbox : chambre d ' essai ; game : partie , carte par defaut ) et ne cuit pas ;
2026-10-03 07:06:48 +00:00
2 . montage du mod dans Mods \ < Nom > , cuisson DLC contre la version de reference ( BuildCookRun -dlcname
-basedonreleaseversion , via cmd / c : appelee directement depuis PowerShell , la version est coupee en deux ) ,
journal dans Saved \ Logs \ portage \ mods ; remise en place dans ModSources \ < Nom > dans tous les cas ;
3 . controle du conteneur : chaque paquet est sous Mods / < Nom > / ( aucun asset du jeu embarque ) ;
4 . assemblage < Out > \ < Nom > -for -LifePlay - < Version > \ LifePlay \ { Mods \ < Nom > , Content \ Modules \ < Nom > } + notice bilingue ,
archive . zip testee ( 7 -Zip ) ; un assemblage precedent du meme nom est deplace ( precedent_ < date > ) , jamais supprime ;
5 . -Install < dossier du jeu > : copie le mod dans un jeu de test ( dossier contenant LifePlay . exe ) .
. EXAMPLE
H3d : metadonnees des mods, avertissement de version, paquet lpr_hair_pocolov (12 coiffures)
- LPModInfoSubsystem : metadonnees des .lpmod ; fenetre d'avertissement quand un mod actif
a ete construit pour une autre version du jeu (Slate, sans Blueprint).
- NewMod/BuildMod : MODULE_VERSION, _CREATED, _LICENSE, _AUTHOR_LINK, _PRICE, _PACKAGED,
_GAME_VERSION ; ordre des 3 premieres lignes controle ; prix et licences controles.
- lpr_hair_pocolov 1.0 : les 12 coiffures Pocolov CC BY ; lp_Revival et les 6 Pocolov
retires du jeu de base ; mod d'essai LPHairSample retire.
Valide en jeu le 2026-10-03 (CHG-106, CHG-107).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:14:37 +00:00
. \ Tools \ BuildMod . ps1 -Name lpr_hair_pocolov
2026-10-06 19:10:53 +00:00
. \ Tools \ BuildMod . ps1 -Name lpr_hair_pocolov -Retarget bake
. \ Tools \ BuildMod . ps1 -Name lpr_hair_pocolov -Retarget bake -RetargetOnly
2026-10-07 13:49:56 +00:00
. \ Tools \ BuildMod . ps1 -Anim freeanims
2026-10-06 19:10:53 +00:00
. \ Tools \ BuildMod . ps1 -Target sandbox -Open
. \ Tools \ BuildMod . ps1 -Target game -Open
H3d : metadonnees des mods, avertissement de version, paquet lpr_hair_pocolov (12 coiffures)
- LPModInfoSubsystem : metadonnees des .lpmod ; fenetre d'avertissement quand un mod actif
a ete construit pour une autre version du jeu (Slate, sans Blueprint).
- NewMod/BuildMod : MODULE_VERSION, _CREATED, _LICENSE, _AUTHOR_LINK, _PRICE, _PACKAGED,
_GAME_VERSION ; ordre des 3 premieres lignes controle ; prix et licences controles.
- lpr_hair_pocolov 1.0 : les 12 coiffures Pocolov CC BY ; lp_Revival et les 6 Pocolov
retires du jeu de base ; mod d'essai LPHairSample retire.
Valide en jeu le 2026-10-03 (CHG-106, CHG-107).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:14:37 +00:00
. \ Tools \ BuildMod . ps1 -Name lpr_hair_pocolov -Install D: \ Game \ LifePlay_Build_5 . 5_H3d \ Windows
2026-10-03 07:06:48 +00:00
#>
param (
2026-10-06 19:10:53 +00:00
[ string ] $Name = '' ,
2026-10-03 07:06:48 +00:00
[ string ] $Version = '' ,
[ string ] $Out = 'D:\Game\LifePlay_Publication\Mods' ,
2026-10-06 19:10:53 +00:00
[ string ] $Install = '' ,
[ string ] $Retarget = '' ,
2026-10-07 13:49:56 +00:00
[ string ] $RetargetConfig = '' ,
2026-10-06 19:10:53 +00:00
[ switch ] $RetargetOnly ,
[ string ] $Target = '' ,
2026-10-07 13:49:56 +00:00
[ switch ] $Open ,
[ string ] $Anim = '' ,
[ int ] $From = 0 ,
[ switch ] $SansCuisson
2026-10-03 07:06:48 +00:00
)
$ErrorActionPreference = 'Stop'
$Root = Split-Path $PSScriptRoot -Parent
. ( Join-Path $PSScriptRoot 'ModSource.ps1' )
$UE = 'D:\Game\Launcher\Engine\UE_5.5\Engine'
$SevenZip = 'C:\Program Files\7-Zip\7z.exe'
2026-10-07 13:49:56 +00:00
# AP7 : chaine d'animations d'un pack.
if ( $Anim ) {
$pipe = @ { Pack = $Anim }
if ( $PSBoundParameters . ContainsKey ( 'From' ) ) { $pipe . From = $From }
if ( $SansCuisson ) { $pipe . SansCuisson = $true }
& ( Join-Path $PSScriptRoot 'anim\AnimPipeline.ps1' ) @pipe
exit $LASTEXITCODE
}
# AP7 : un resultat ou un echec de conversion n'est retenu que s'il porte le jeton de ce lancement
# (un ancien journal, ex. LifePlay-backup-<date>.log relu depuis l'octet 0, n'a jamais ce jeton).
function Find-RetargetLine([string ] $Text , [ string ] $Mode , [ string ] $Jeton ) {
$tok = [ regex ] :: Escape ( $Jeton )
$success = if ( $Mode -eq 'rigs' ) { " RETARGET rigs prets jeton $tok (?![0-9A-Za-z]) " } else { " RETARGET bridget \d+ michael \d+ deplaces \d+[^\r\n]*? jeton $tok (?![0-9A-Za-z]) " }
$ok = [ regex ] :: Match ( $Text , $success )
$bad = [ regex ] :: Match ( $Text , " RETARGET echec[^\r\n]*? jeton $tok (?![0-9A-Za-z]) " )
[ pscustomobject ] @ {
Hit = if ( $ok . Success ) { $ok . Value . Trim ( ) } else { $null }
Failure = if ( $bad . Success ) { $bad . Value . Trim ( ) } else { $null }
}
}
function Invoke-AnimToGenesis([string ] $ProjectRoot , [ string ] $Mode , [ string ] $ConfigFile = '' ) {
2026-10-06 19:10:53 +00:00
$running = @ ( Get-Process UnrealEditor , UnrealEditor-Cmd -ErrorAction SilentlyContinue )
if ( $running ) { throw " UnrealEditor est ouvert (pid $( $running [ 0 ] . Id ) ) : le fermer avant la conversion " }
2026-10-07 13:49:56 +00:00
$configTarget = Join-Path $ProjectRoot 'Saved\anim_retarget.json'
New-Item -ItemType Directory -Force ( Split-Path $configTarget ) | Out-Null
$jeton = [ guid ] :: NewGuid ( ) . ToString ( 'N' ) . Substring ( 0 , 12 )
if ( $ConfigFile ) {
if ( -not ( Test-Path $ConfigFile ) ) { throw " -RetargetConfig : fichier introuvable $ConfigFile " }
$cfg = Get-Content -LiteralPath $ConfigFile -Raw -Encoding utf8 | ConvertFrom-Json
$cfg | Add-Member -NotePropertyName jeton -NotePropertyValue $jeton -Force
[ IO.File ] :: WriteAllText ( $configTarget , ( ( $cfg | ConvertTo-Json -Depth 20 ) + " `r `n " ) , ( New-Object Text . UTF8Encoding $false ) )
}
else {
[ IO.File ] :: WriteAllText ( $configTarget , ( '{"mode": "' + $Mode . Trim ( ) . ToLower ( ) + '", "jeton": "' + $jeton + '"}' + " `r `n " ) , ( New-Object Text . UTF8Encoding $false ) )
}
$effective = Get-Content -LiteralPath $configTarget -Raw -Encoding utf8 | ConvertFrom-Json
$methode = if ( $effective . methode ) { [ string ] $effective . methode } else { 'ue4' }
$clipsDir = if ( $effective . clips_dir ) { [ string ] $effective . clips_dir } else { '/Game/Quaternius/UAL' }
2026-10-06 19:10:53 +00:00
$editor = Join-Path $UE 'Binaries\Win64\UnrealEditor.exe'
$project = Join-Path $ProjectRoot 'LifePlay.uproject'
2026-10-07 13:49:56 +00:00
$script = Join-Path $ProjectRoot 'Tools\anim\RetargetPack.py'
2026-10-06 19:10:53 +00:00
$logDir = Join-Path $ProjectRoot 'Saved\Logs'
2026-10-07 13:49:56 +00:00
# Journaux relus en entier s'ils ont change depuis le lancement : le jeton suffit a ecarter les lignes d'une ancienne
# cuisson. (AP7 : l'ancienne lecture a partir de la taille d'avant sautait le nouveau LifePlay.log, recree plus petit
# par l'editeur au demarrage, et ne voyait jamais la ligne de resultat.)
$launched = Get-Date
Write-Host " Conversion des clips de $clipsDir vers Bridget et Michael (methode $methode , $Mode ) "
# Chemins entre guillemets : le dossier du projet contient une espace (AP7 : sans guillemets, l'editeur
# recevait un chemin de projet coupe et se fermait sans journal).
$proc = Start-Process -FilePath $editor -ArgumentList " `" $project `" -ExecutePythonScript= `" $script `" -nosplash -unattended " -WorkingDirectory $ProjectRoot -PassThru
2026-10-06 19:10:53 +00:00
$deadline = ( Get-Date ) . AddMinutes ( 25 )
$hit = $null
$failure = $null
while ( ( Get-Date ) -lt $deadline ) {
Start-Sleep -Seconds 5
$logs = @ ( Get-ChildItem $logDir -Filter 'LifePlay*.log' -ErrorAction SilentlyContinue )
foreach ( $file in $logs ) {
2026-10-07 13:49:56 +00:00
if ( $file . LastWriteTime -lt $launched ) { continue }
2026-10-06 19:10:53 +00:00
try {
$stream = [ IO.File ] :: Open ( $file . FullName , [ IO.FileMode ] :: Open , [ IO.FileAccess ] :: Read , [ IO.FileShare ] :: ReadWrite )
}
catch [ System.IO.IOException ] { continue }
try {
$reader = New-Object IO . StreamReader ( $stream )
$text = $reader . ReadToEnd ( )
}
finally { $stream . Close ( ) }
2026-10-07 13:49:56 +00:00
$found = Find-RetargetLine $text $Mode $jeton
if ( $found . Hit ) { $hit = $found . Hit }
if ( $found . Failure ) { $failure = $found . Failure }
2026-10-06 19:10:53 +00:00
}
if ( $hit ) { break }
$editorGone = -not ( Get-Process UnrealEditor -ErrorAction SilentlyContinue )
if ( $editorGone -and $proc . HasExited ) { break }
}
if ( $failure -and -not $hit ) { throw " Conversion en echec : $failure " }
if ( -not $hit ) { throw " Conversion sans ligne RETARGET : voir Saved\Logs\LifePlay.log " }
2026-10-07 13:49:56 +00:00
# L'editeur se ferme seul apres la conversion (RetargetPack.py), mais l'enregistrement des assets peut prendre
# plusieurs minutes (AP7 : plus de 2 min pour 84 clips) : attendre jusqu'a 10 min que plus aucun editeur ne tourne.
$quiet = ( Get-Date ) . AddMinutes ( 10 )
while ( ( Get-Date ) -lt $quiet -and @ ( Get-Process UnrealEditor -ErrorAction SilentlyContinue ) . Count -gt 0 ) { Start-Sleep -Seconds 2 }
2026-10-06 19:10:53 +00:00
$still = @ ( Get-Process UnrealEditor -ErrorAction SilentlyContinue )
if ( $still ) { throw " Conversion terminee ( $hit ) mais UnrealEditor est encore ouvert : le fermer avant la cuisson " }
Write-Host " $hit "
}
function Write-AnimTarget([string ] $ProjectRoot , [ string ] $Which ) {
$dir = Join-Path $ProjectRoot 'Saved\Config\Windows'
New-Item -ItemType Directory -Force $dir | Out-Null
$ini = Join-Path $dir 'LPAnimTarget.ini'
[ IO.File ] :: WriteAllText ( $ini , " [LifePlay.Anim] `r `n Target= $Which `r `n " , ( New-Object Text . UTF8Encoding $false ) )
Write-Host " Cible des animations : $Which ( $ini ) "
}
function Open-AnimTarget([string ] $ProjectRoot , [ string ] $Which ) {
$running = @ ( Get-Process UnrealEditor , UnrealEditor-Cmd -ErrorAction SilentlyContinue )
if ( $running ) { throw " UnrealEditor est ouvert (pid $( $running [ 0 ] . Id ) ) : le fermer avant d'ouvrir la cible " }
$editor = Join-Path $UE 'Binaries\Win64\UnrealEditor.exe'
$project = Join-Path $ProjectRoot 'LifePlay.uproject'
if ( $Which -eq 'sandbox' ) {
# Le .bat quote l'argument -ini. Start-Process le coupe sur les deux-points.
& ( Join-Path $ProjectRoot 'Tools\debug\LifePlay_interaction_sandbox.bat' )
}
else {
Start-Process -FilePath $editor -ArgumentList @ ( $project , '-game' , '-windowed' , '-ResX=1600' , '-ResY=900' ) -WorkingDirectory $ProjectRoot | Out-Null
}
Write-Host " Ouvert : $Which "
}
# 1. Controles. Cuisson seulement : -Target -Open peut partir sans mod.
if ( $Open -and -not $Target ) { throw " -Open demande -Target sandbox ou -Target game " }
if ( $Target -and $Target -notin 'sandbox' , 'game' ) { throw " -Target vaut sandbox ou game (recu : $Target ) " }
$bCook = -not $RetargetOnly -and -not $Open
if ( $bCook -and -not $Name ) { throw " Preciser -Name du mod " }
if ( -not $bCook -and -not $Target -and -not $Retarget ) { throw " Preciser -Name du mod, ou -Target sandbox|game " }
if ( $bCook ) {
2026-10-03 07:06:48 +00:00
if ( -not $Version ) {
$line = Select-String -Path ( Join-Path $Root 'Config\DefaultGame.ini' ) -Pattern '^ProjectVersion=(.+)$' | Select-Object -First 1
if ( -not $line ) { throw 'ProjectVersion absent de Config\DefaultGame.ini : preciser -Version' }
$Version = $line . Matches [ 0 ] . Groups [ 1 ] . Value . Trim ( )
}
$src = Join-Path $Root " ModSources\ $Name "
$lpmod = Join-Path $src " Module\ $Name .lpmod "
if ( -not ( Test-Path ( Join-Path $src " $Name .uplugin " ) ) ) { throw " ModSources\ $Name \ $Name .uplugin introuvable " }
if ( -not ( Test-Path $lpmod ) ) { throw " ModSources\ $Name \Module\ $Name .lpmod introuvable " }
$id = ( Select-String -Path $lpmod -Pattern '^MODULE_UNIQUEID:\s*(.+)$' | Select-Object -First 1 ) . Matches [ 0 ] . Groups [ 1 ] . Value . Trim ( )
if ( $id -ne $Name ) { throw " MODULE_UNIQUEID ( $id ) different du nom du mod ( $Name ) " }
$title = ( Select-String -Path $lpmod -Pattern '^MODULE_NAME:\s*(.+)$' | Select-Object -First 1 ) . Matches [ 0 ] . Groups [ 1 ] . Value . Trim ( )
H3d : metadonnees des mods, avertissement de version, paquet lpr_hair_pocolov (12 coiffures)
- LPModInfoSubsystem : metadonnees des .lpmod ; fenetre d'avertissement quand un mod actif
a ete construit pour une autre version du jeu (Slate, sans Blueprint).
- NewMod/BuildMod : MODULE_VERSION, _CREATED, _LICENSE, _AUTHOR_LINK, _PRICE, _PACKAGED,
_GAME_VERSION ; ordre des 3 premieres lignes controle ; prix et licences controles.
- lpr_hair_pocolov 1.0 : les 12 coiffures Pocolov CC BY ; lp_Revival et les 6 Pocolov
retires du jeu de base ; mod d'essai LPHairSample retire.
Valide en jeu le 2026-10-03 (CHG-106, CHG-107).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:14:37 +00:00
# Metadonnees (H3d) : le jeu lit les trois premieres lignes par position.
$lpmodLines = @ ( Get-Content $lpmod -Encoding utf8 )
$expected = 'MODULE_UNIQUEID:' , 'MODULE_NAME:' , 'MODULE_AUTHOR:'
for ( $i = 0 ; $i -lt 3 ; $i + + ) {
if ( $lpmodLines . Count -le $i -or -not $lpmodLines [ $i ] . StartsWith ( $expected [ $i ] ) ) { throw " $Name .lpmod : la ligne $( $i + 1 ) doit commencer par $( $expected [ $i ] ) (le jeu lit ces lignes par position) " }
}
function Get-LpmodValue([string ] $Key ) {
$m = $lpmodLines | Select-String -Pattern " ^ $( [ regex ] :: Escape ( $Key ) ) :\s*(.*) $ " | Select-Object -First 1
if ( $m ) { $m . Matches [ 0 ] . Groups [ 1 ] . Value . Trim ( ) } else { '' }
}
$modVersion = Get-LpmodValue 'MODULE_VERSION'
# Mod payant : aucune licence d'asset non commerciale ou de magasin dans les credits.
$price = Get-LpmodValue 'MODULE_PRICE'
if ( $price -and $price -notmatch '^(free|gratuit|0+([.,]0+)?)$' ) {
$blocking = Select-String -Path ( Join-Path $src 'Module\CREDITS.txt' ) -Pattern '\bNC\b|NonCommercial|Non-Commercial|Standard License|Personal' -ErrorAction SilentlyContinue
if ( $blocking ) { throw " MODULE_PRICE ' $price ' : licence(s) incompatible(s) avec la vente dans CREDITS.txt ( $( $blocking [ 0 ] . Line . Trim ( ) ) ) " }
}
2026-10-03 07:06:48 +00:00
$release = Join-Path $Root " Releases\ $Version \Windows "
if ( -not ( Test-Path ( Join-Path $release 'AssetRegistry.bin' ) ) ) { throw " Version de reference absente : $release (build de publication avec -createreleaseversion= $Version ) " }
$stray = Get-StrayMods $Root
if ( $stray ) { throw " Autres plugins dans Mods\ ( $( $stray -join ', ' ) ) : les remettre dans ModSources avant de cuire " }
2026-10-06 19:10:53 +00:00
}
# 1b. Conversion, puis redirecteur sandbox ou game.
if ( $RetargetOnly -and -not $Retarget ) { $Retarget = 'bake' }
if ( $Retarget -and $Retarget -notin 'bake' , 'rigs' ) { throw " -Retarget vaut bake ou rigs (recu : $Retarget ) " }
if ( $Target ) { Write-AnimTarget $Root $Target }
2026-10-07 13:49:56 +00:00
if ( $Retarget ) { Invoke-AnimToGenesis $Root $Retarget $RetargetConfig }
2026-10-06 19:10:53 +00:00
if ( $Open ) {
Open-AnimTarget $Root $Target
exit 0
}
if ( -not $bCook ) {
Write-Host " Cuisson non lancee "
exit 0
}
2026-10-03 07:06:48 +00:00
# 2. Cuisson DLC.
$logDir = Join-Path $Root 'Saved\Logs\portage\mods'
New-Item -ItemType Directory -Force $logDir | Out-Null
$log = Join-Path $logDir ( " {0}_{1}.log " -f $Name , ( Get-Date -Format 'yyyyMMdd_HHmmss' ) )
$mounted = Mount-ModSource $Root $Name
try {
$uplugin = Join-Path $mounted " $Name .uplugin "
# Commande ecrite dans un .cmd a cote du journal (guillemets intacts, commande exacte conservee).
$batch = [ IO.Path ] :: ChangeExtension ( $log , '.cmd' )
$cmd = " @call `" $UE \Build\BatchFiles\RunUAT.bat `" BuildCookRun -project= `" $Root \LifePlay.uproject `" -platform=Win64 -clientconfig=Shipping -cook -stage -pak -iostore -compressed -dlcname= `" $uplugin `" -basedonreleaseversion= $Version -nodebuginfo -unattended -utf8output -nop4 > `" $log `" 2>&1 "
[ IO.File ] :: WriteAllText ( $batch , $cmd + " `r `n @exit /b %ERRORLEVEL% `r `n " , ( New-Object Text . UTF8Encoding $false ) )
Write-Host " Cuisson DLC de $Name contre $Version (journal : $log ) "
& cmd / c $batch
$exit = $LASTEXITCODE
$cooked = Select-String -Path $log -Pattern 'Packages Cooked: (\d+)' | Select-Object -Last 1
if ( $exit -ne 0 -or -not $cooked ) { throw " Cuisson en echec (code $exit ) : voir $log " }
Write-Host " $( $cooked . Matches [ 0 ] . Groups [ 1 ] . Value ) paquet(s) cuit(s) "
$paks = Join-Path $mounted " Saved\StagedBuilds\Windows\LifePlay\Mods\ $Name \Content\Paks\Windows "
$files = @ ( Get-ChildItem $paks -File | Where-Object { $_ . LastWriteTime -gt ( Get-Item $log ) . CreationTime } )
if ( $files . Count -lt 3 ) { throw " Paquets du mod absents ou anciens dans $paks " }
# 3. Contenu du conteneur : rien hors de Mods/<Nom>/.
$utoc = ( $files | Where-Object Extension -eq '.utoc' | Select-Object -First 1 ) . FullName
$entries = @ ( & " $UE \Binaries\Win64\UnrealPak.exe " $utoc -List 2 > & 1 | ForEach-Object { " $_ " } | Where-Object { $_ -match '"\.\./\.\./\.\./' } | ForEach-Object { ( $_ -split '"' ) [ 1 ] } )
$outside = @ ( $entries | Where-Object { $_ -notlike " ../../../LifePlay/Mods/ $Name /* " } )
if ( -not $entries -or $outside ) { throw " Conteneur : $( $entries . Count ) entree(s), $( $outside . Count ) hors de Mods/ $Name ( $( $outside | Select-Object -First 3 ) ) " }
Write-Host " conteneur : $( $entries . Count ) fichier(s), tous sous Mods/ $Name / "
# 4. Assemblage.
New-Item -ItemType Directory -Force $Out | Out-Null
H3d : metadonnees des mods, avertissement de version, paquet lpr_hair_pocolov (12 coiffures)
- LPModInfoSubsystem : metadonnees des .lpmod ; fenetre d'avertissement quand un mod actif
a ete construit pour une autre version du jeu (Slate, sans Blueprint).
- NewMod/BuildMod : MODULE_VERSION, _CREATED, _LICENSE, _AUTHOR_LINK, _PRICE, _PACKAGED,
_GAME_VERSION ; ordre des 3 premieres lignes controle ; prix et licences controles.
- lpr_hair_pocolov 1.0 : les 12 coiffures Pocolov CC BY ; lp_Revival et les 6 Pocolov
retires du jeu de base ; mod d'essai LPHairSample retire.
Valide en jeu le 2026-10-03 (CHG-106, CHG-107).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:14:37 +00:00
$base = if ( $modVersion ) { " $Name - $modVersion -for-LifePlay- $Version " } else { " $Name -for-LifePlay- $Version " }
2026-10-03 07:06:48 +00:00
$stage = Join-Path $Out $base
$zip = " $stage .zip "
$stamp = Get-Date -Format 'yyyyMMdd_HHmmss'
if ( Test-Path $stage ) { Move-Item $stage " $stage .precedent_ $stamp " }
if ( Test-Path $zip ) { Move-Item $zip " $stage .precedent_ $stamp .zip " }
$modDir = Join-Path $stage " LifePlay\Mods\ $Name "
New-Item -ItemType Directory -Force ( Join-Path $modDir 'Content\Paks\Windows' ) | Out-Null
Copy-Item $uplugin $modDir
$files | Copy-Item -Destination ( Join-Path $modDir 'Content\Paks\Windows' )
Copy-Item ( Join-Path $mounted 'Module' ) ( Join-Path $stage " LifePlay\Content\Modules\ $Name " ) -Recurse
H3d : metadonnees des mods, avertissement de version, paquet lpr_hair_pocolov (12 coiffures)
- LPModInfoSubsystem : metadonnees des .lpmod ; fenetre d'avertissement quand un mod actif
a ete construit pour une autre version du jeu (Slate, sans Blueprint).
- NewMod/BuildMod : MODULE_VERSION, _CREATED, _LICENSE, _AUTHOR_LINK, _PRICE, _PACKAGED,
_GAME_VERSION ; ordre des 3 premieres lignes controle ; prix et licences controles.
- lpr_hair_pocolov 1.0 : les 12 coiffures Pocolov CC BY ; lp_Revival et les 6 Pocolov
retires du jeu de base ; mod d'essai LPHairSample retire.
Valide en jeu le 2026-10-03 (CHG-106, CHG-107).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:14:37 +00:00
# Copie livree (pas les sources) : date de paquet et version du jeu visee (avertissement en jeu si elle differe).
$stagedLpmod = Join-Path $stage " LifePlay\Content\Modules\ $Name \ $Name .lpmod "
$stamped = @ ( $lpmodLines | Where-Object { $_ -notmatch '^MODULE_(PACKAGED|GAME_VERSION):' } ) + " MODULE_PACKAGED: $( Get-Date -Format 'yyyy-MM-dd' ) " + " MODULE_GAME_VERSION: $Version "
[ IO.File ] :: WriteAllText ( $stagedLpmod , ( $stamped -join " `r `n " ) + " `r `n " , ( New-Object Text . UTF8Encoding $false ) )
# Descripteur livre : version du mod et lien de l'auteur dans les champs natifs du plugin.
$stagedPlugin = Join-Path $modDir " $Name .uplugin "
$desc = Get-Content $stagedPlugin -Raw | ConvertFrom-Json
if ( $modVersion ) { $desc . VersionName = $modVersion }
$authorLink = Get-LpmodValue 'MODULE_AUTHOR_LINK'
if ( $authorLink ) { $desc | Add-Member CreatedByURL $authorLink -Force }
[ IO.File ] :: WriteAllText ( $stagedPlugin , ( $desc | ConvertTo-Json ) + " `r `n " , ( New-Object Text . UTF8Encoding $false ) )
2026-10-03 07:06:48 +00:00
$readme = @ (
H3d : metadonnees des mods, avertissement de version, paquet lpr_hair_pocolov (12 coiffures)
- LPModInfoSubsystem : metadonnees des .lpmod ; fenetre d'avertissement quand un mod actif
a ete construit pour une autre version du jeu (Slate, sans Blueprint).
- NewMod/BuildMod : MODULE_VERSION, _CREATED, _LICENSE, _AUTHOR_LINK, _PRICE, _PACKAGED,
_GAME_VERSION ; ordre des 3 premieres lignes controle ; prix et licences controles.
- lpr_hair_pocolov 1.0 : les 12 coiffures Pocolov CC BY ; lp_Revival et les 6 Pocolov
retires du jeu de base ; mod d'essai LPHairSample retire.
Valide en jeu le 2026-10-03 (CHG-106, CHG-107).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:14:37 +00:00
" $title ( $Name ) $( if ( $modVersion ) { " $modVersion " } ) - built for LifePlay $Version " ,
2026-10-03 07:06:48 +00:00
'' ,
'INSTALL: extract this archive into the game folder (the one containing LifePlay.exe), merging the LifePlay folder.' ,
" Then open `" Mod Manager `" on the title screen and enable the mod. To uninstall, delete LifePlay\Mods\ $Name " ,
" and LifePlay\Content\Modules\ $Name . Credits and licenses: LifePlay\Content\Modules\ $Name \CREDITS.txt. " ,
" This mod contains cooked assets: it is made for LifePlay $Version and may need a rebuild for other versions. " ,
'' ,
'INSTALLATION : extraire cette archive dans le dossier du jeu (celui de LifePlay.exe) en fusionnant le dossier' ,
'LifePlay, puis activer le mod dans "Mod Manager" sur l''ecran titre. Desinstallation : supprimer' ,
" LifePlay\Mods\ $Name et LifePlay\Content\Modules\ $Name . Credits et licences : CREDITS.txt du module. " ,
" Ce mod contient des assets cuits pour LifePlay $Version : une autre version du jeu peut demander de le recuire. " )
[ IO.File ] :: WriteAllText ( ( Join-Path $stage 'README.txt' ) , ( $readme -join " `r `n " ) + " `r `n " , ( New-Object Text . UTF8Encoding $false ) )
}
finally {
Dismount-ModSource $Root $Name
}
Push-Location $stage
try {
& $SevenZip a -tzip $zip '.\*' | Out-Null
& $SevenZip t $zip | Out-Null
if ( $LASTEXITCODE -ne 0 ) { throw " Archive $zip invalide " }
}
finally { Pop-Location }
$staged = @ ( Get-ChildItem $stage -Recurse -File ) . Count
$listed = @ ( & $SevenZip l -slt $zip | Where-Object { $_ -match '^Attributes = [^D]*$' } ) . Count
if ( $staged -ne $listed ) { throw " Archive : $listed fichier(s) pour $staged assembles " }
$hash = ( Get-FileHash $zip -Algorithm SHA256 ) . Hash
Write-Host ( " Archive : {0} ({1:N0} octets, {2} fichiers, SHA-256 {3}) " -f $zip , ( Get-Item $zip ) . Length , $listed , $hash )
2026-10-03 09:19:44 +00:00
# Index de diffusion (Tools/SplitModules.ps1, mode per_module) : ligne du mod ajoutee ou remplacee.
$index = Join-Path $Out 'INDEX.txt'
if ( Test-Path $index ) {
$entry = '{0} ({1}) : {2}, {3:N0} bytes, SHA-256 {4}' -f $title , $Name , ( Split-Path $zip -Leaf ) , ( Get-Item $zip ) . Length , $hash . ToLower ( )
$kept = @ ( Get-Content $index -Encoding utf8 | Where-Object { $_ -notlike " * ( $Name ) : * " } )
[ IO.File ] :: WriteAllText ( $index , ( ( $kept + $entry ) -join " `r `n " ) + " `r `n " , ( New-Object Text . UTF8Encoding $false ) )
Write-Host " Index mis a jour : $index "
}
2026-10-03 07:06:48 +00:00
# 5. Installation dans un jeu de test.
if ( $Install ) {
if ( -not ( Test-Path ( Join-Path $Install 'LifePlay.exe' ) ) ) { throw " $Install ne contient pas LifePlay.exe " }
robocopy ( Join-Path $stage 'LifePlay' ) ( Join-Path $Install 'LifePlay' ) / E / NFL / NDL / NJH / NJS / NP | Out-Null
if ( $LASTEXITCODE -ge 8 ) { throw " Copie vers $Install en echec (robocopy $LASTEXITCODE ) " }
Write-Host " Installe dans $Install "
}
H3d : metadonnees des mods, avertissement de version, paquet lpr_hair_pocolov (12 coiffures)
- LPModInfoSubsystem : metadonnees des .lpmod ; fenetre d'avertissement quand un mod actif
a ete construit pour une autre version du jeu (Slate, sans Blueprint).
- NewMod/BuildMod : MODULE_VERSION, _CREATED, _LICENSE, _AUTHOR_LINK, _PRICE, _PACKAGED,
_GAME_VERSION ; ordre des 3 premieres lignes controle ; prix et licences controles.
- lpr_hair_pocolov 1.0 : les 12 coiffures Pocolov CC BY ; lp_Revival et les 6 Pocolov
retires du jeu de base ; mod d'essai LPHairSample retire.
Valide en jeu le 2026-10-03 (CHG-106, CHG-107).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:14:37 +00:00
# Code de sortie : succes (robocopy renvoie 1 a 7 quand il a copie).
exit 0