DC3 : atelier de mods hors du jeu, dans l'addon LifePlay-Sandbox (CHG-236)
Retrait de BuildMod, NewMod, EditMod, ModSource, HairPipeline et leurs .py, Tools/anim, Tools/hairs, Tools/mods, ModSources, AnimSources, Config/LPSandbox.ini, lanceurs sandbox et guide des moddeurs (57 fichiers suivis ; contenu disque deplace vers la sauvegarde DC3). References et docs mises a jour. Pin addon 79f0205. Validation : DC3-VAL PASS (cuisson lpr_anim_freeanims depuis l'addon identique au mod publie, tournee identique a SB2-REF). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2cf416cb2d
commit
e46093c81c
77 changed files with 316 additions and 9513 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -45,9 +45,9 @@ Plugins/McpAutomationBridge/
|
|||
# des mods (comme Content/Heavy : livres par l'archive des sources).
|
||||
/Releases/
|
||||
/Mods/
|
||||
/ModSources/*/Saved/
|
||||
/ModSources/*/Intermediate/
|
||||
/ModSources/*/Content/
|
||||
# Lot DC3 : sources de mods et atelier de mods dans le depot addon LifePlay-Sandbox
|
||||
/ModSources/
|
||||
/AnimSources/
|
||||
|
||||
# Journaux et dumps de debug des lots (restent en local ; .md et .csv restent suivis)
|
||||
/Docs/Migration/etat/**/*.txt
|
||||
|
|
|
|||
|
|
@ -1,187 +0,0 @@
|
|||
{
|
||||
"pack": "freeanims",
|
||||
"prefixe": "fap",
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"auteur": "SoerGame",
|
||||
"licence": "CC BY 4.0",
|
||||
"url": "https://www.fab.com/listings/9e93d929-36e8-45f9-b925-325db1b9385d",
|
||||
"url_auteur": "https://www.fab.com/sellers/SoerGame",
|
||||
"fichiers": [
|
||||
"Content/FreeAnimationsPack (assets Unreal)"
|
||||
],
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"exclus": [],
|
||||
"chaine": {
|
||||
"methode": "auto",
|
||||
"profil_attendu": "ue5_mannequin",
|
||||
"source": {
|
||||
"type": "uasset",
|
||||
"clips": "/Game/FreeAnimationsPack/Animations",
|
||||
"maillage": "/Game/FreeAnimationsPack/Demo/Characters/Mannequins/Meshes/SKM_Manny.SKM_Manny",
|
||||
"clip_reference": "AS_Rest"
|
||||
},
|
||||
"sortie": {
|
||||
"clips": "/Game/AnimPacks/FreeAnimationsPack",
|
||||
"rigs": "/Game/Retarget_ue5"
|
||||
},
|
||||
"corps": [
|
||||
{
|
||||
"nom": "Bridget",
|
||||
"maillage": "/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp",
|
||||
"prefixe": "B_",
|
||||
"gabarit": "/Game/Heavy/Daz/Anims/Idle_F_Daz"
|
||||
},
|
||||
{
|
||||
"nom": "Michael",
|
||||
"maillage": "/Game/Heavy/Daz/Michael/michael.michael",
|
||||
"prefixe": "M_",
|
||||
"gabarit": "/Game/Heavy/Daz/Anims/idle_Daz"
|
||||
}
|
||||
],
|
||||
"mod": "lpr_anim_freeanims",
|
||||
"travail": "Saved/AnimPipeline/freeanims",
|
||||
"sandbox": "AnimPipeline/freeanims/sandbox"
|
||||
},
|
||||
"animations": {
|
||||
"fap_as_airkiss": {
|
||||
"clip": "AS_AirKiss",
|
||||
"nom": {
|
||||
"fr": "Baiser envoyé",
|
||||
"en": "Blow a kiss"
|
||||
},
|
||||
"categorie": "interaction",
|
||||
"accessoire": "",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Geste ponctuel (probablement joué une fois) d'envoi de baiser, joué sur place ; utile pour une scène de flirt ou d'adieu.",
|
||||
"en": "One-shot gesture (probably played once) of blowing a kiss, played in place; useful for a flirting or farewell scene."
|
||||
}
|
||||
},
|
||||
"fap_as_attackinlevitation2": {
|
||||
"clip": "AS_AttackInLevitation2",
|
||||
"nom": {
|
||||
"fr": "Attaque en lévitation",
|
||||
"en": "Levitation attack"
|
||||
},
|
||||
"categorie": "magie",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Attaque lancée en lévitation, probablement jouée une fois, sur place ; pour un sort ou une attaque surnaturelle.",
|
||||
"en": "Attack performed while levitating, probably played once, in place; for a spell or supernatural attack."
|
||||
}
|
||||
},
|
||||
"fap_as_chilldriving": {
|
||||
"clip": "AS_ChillDriving",
|
||||
"nom": {
|
||||
"fr": "Conduite détendue",
|
||||
"en": "Relaxed driving"
|
||||
},
|
||||
"categorie": "conduite",
|
||||
"accessoire": "volant",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Pose de conduite détendue, probablement en boucle, jouée sur place ; à utiliser avec un véhicule ou un volant.",
|
||||
"en": "Relaxed driving pose, probably looping, played in place; to use with a vehicle or steering wheel."
|
||||
}
|
||||
},
|
||||
"fap_as_combohands": {
|
||||
"clip": "AS_ComboHands",
|
||||
"nom": {
|
||||
"fr": "Enchaînement de poings",
|
||||
"en": "Punch combo"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Enchaînement de coups de poing, probablement joué une fois, sur place ; pour une bagarre ou un entraînement.",
|
||||
"en": "Sequence of punches, probably played once, in place; for a fight or training scene."
|
||||
}
|
||||
},
|
||||
"fap_as_eatingonparty": {
|
||||
"clip": "AS_EatingOnParty",
|
||||
"nom": {
|
||||
"fr": "Manger en soirée",
|
||||
"en": "Eating at party"
|
||||
},
|
||||
"categorie": "interaction",
|
||||
"accessoire": "",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Personnage mangeant debout lors d'une soirée, probablement en boucle, joué sur place ; pour un buffet ou une fête.",
|
||||
"en": "Character eating while standing at a party, probably looping, played in place; for a buffet or party scene."
|
||||
}
|
||||
},
|
||||
"fap_as_flyingup": {
|
||||
"clip": "AS_FlyingUp",
|
||||
"nom": {
|
||||
"fr": "Envol",
|
||||
"en": "Take off"
|
||||
},
|
||||
"categorie": "magie",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Décollage vers le haut, probablement joué une fois, sur place ; pour un envol ou une lévitation.",
|
||||
"en": "Upward take-off, probably played once, in place; for flying off or levitating."
|
||||
}
|
||||
},
|
||||
"fap_as_goforward": {
|
||||
"clip": "AS_GoForward",
|
||||
"nom": {
|
||||
"fr": "Marche avant",
|
||||
"en": "Walk forward"
|
||||
},
|
||||
"categorie": "locomotion",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Cycle de marche vers l'avant, probablement en boucle, joué sur place ; à combiner avec un déplacement du personnage.",
|
||||
"en": "Forward walk cycle, probably looping, played in place; combine with a character movement."
|
||||
}
|
||||
},
|
||||
"fap_as_rest": {
|
||||
"clip": "AS_Rest",
|
||||
"nom": {
|
||||
"fr": "Repos",
|
||||
"en": "Rest"
|
||||
},
|
||||
"categorie": "attente",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Posture de repos debout, probablement en boucle, jouée sur place ; pose d'attente par défaut.",
|
||||
"en": "Standing rest posture, probably looping, played in place; default waiting pose."
|
||||
}
|
||||
},
|
||||
"fap_as_rightlegkick2": {
|
||||
"clip": "AS_RightLegKick2",
|
||||
"nom": {
|
||||
"fr": "Coup de pied droit",
|
||||
"en": "Right leg kick"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Coup de pied de la jambe droite, probablement joué une fois, sur place ; pour un combat ou un entraînement.",
|
||||
"en": "Right leg kick, probably played once, in place; for a fight or training scene."
|
||||
}
|
||||
},
|
||||
"fap_as_talk": {
|
||||
"clip": "AS_Talk",
|
||||
"nom": {
|
||||
"fr": "Parler",
|
||||
"en": "Talk"
|
||||
},
|
||||
"categorie": "conversation",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Personnage qui parle debout, probablement en boucle, joué sur place ; pour un dialogue ou une conversation.",
|
||||
"en": "Character talking while standing, probably looping, played in place; for a dialogue or conversation."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,640 +0,0 @@
|
|||
{
|
||||
"pack": "quaternius",
|
||||
"prefixe": "qua",
|
||||
"source": "Quaternius Universal Animation Library (Standard)",
|
||||
"auteur": "Quaternius",
|
||||
"licence": "CC0 1.0",
|
||||
"url": "https://quaternius.com",
|
||||
"fichiers": [
|
||||
"D:/Game/Universal Animation Library[Standard]/Unreal-Godot/UAL1_Standard.glb"
|
||||
],
|
||||
"credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com",
|
||||
"exclus": [
|
||||
"A_TPose"
|
||||
],
|
||||
"chaine": {
|
||||
"methode": "auto",
|
||||
"profil_attendu": "ue4_mannequin",
|
||||
"source": {
|
||||
"type": "glb",
|
||||
"fichier": "D:/Game/Universal Animation Library[Standard]/Unreal-Godot/UAL1_Standard.glb",
|
||||
"import": "Tools/ImportQuaternius.py",
|
||||
"clips": "/Game/Quaternius/UAL",
|
||||
"maillage": "/Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard",
|
||||
"clip_reference": "A_TPose"
|
||||
},
|
||||
"sortie": {
|
||||
"clips": "/Game/Quaternius/UAL",
|
||||
"rigs": "/Game/Quaternius/Retarget"
|
||||
},
|
||||
"corps": [
|
||||
{
|
||||
"nom": "Bridget",
|
||||
"maillage": "/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp",
|
||||
"prefixe": "B_",
|
||||
"gabarit": "/Game/Heavy/Daz/Anims/Idle_F_Daz"
|
||||
},
|
||||
{
|
||||
"nom": "Michael",
|
||||
"maillage": "/Game/Heavy/Daz/Michael/michael.michael",
|
||||
"prefixe": "M_",
|
||||
"gabarit": "/Game/Heavy/Daz/Anims/idle_Daz"
|
||||
}
|
||||
],
|
||||
"mod": "lpr_anim_quaternius",
|
||||
"travail": "Saved/AnimPipeline/quaternius",
|
||||
"sandbox": "Measurement"
|
||||
},
|
||||
"animations": {
|
||||
"qua_crouch_fwd_loop": {
|
||||
"clip": "Crouch_Fwd_Loop",
|
||||
"nom": {
|
||||
"fr": "Marche accroupie",
|
||||
"en": "Crouch walk"
|
||||
},
|
||||
"categorie": "locomotion",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de marche accroupie sur place ; à combiner avec un déplacement pour une scène furtive.",
|
||||
"en": "Looping crouched walk in place; combine with a character movement for a sneaking scene."
|
||||
}
|
||||
},
|
||||
"qua_crouch_idle_loop": {
|
||||
"clip": "Crouch_Idle_Loop",
|
||||
"nom": {
|
||||
"fr": "Attente accroupie",
|
||||
"en": "Crouch idle"
|
||||
},
|
||||
"categorie": "attente",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle d'attente accroupie, pour se cacher ou observer discrètement.",
|
||||
"en": "Looping crouched idle, for hiding or watching discreetly."
|
||||
}
|
||||
},
|
||||
"qua_dance_loop": {
|
||||
"clip": "Dance_Loop",
|
||||
"nom": {
|
||||
"fr": "Danse",
|
||||
"en": "Dance"
|
||||
},
|
||||
"categorie": "danse",
|
||||
"accessoire": "",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Boucle de danse libre, pour une scène de fête ou de club.",
|
||||
"en": "Looping free dance, for a party or club scene."
|
||||
}
|
||||
},
|
||||
"qua_death01": {
|
||||
"clip": "Death01",
|
||||
"nom": {
|
||||
"fr": "Mort",
|
||||
"en": "Death"
|
||||
},
|
||||
"categorie": "chute",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : le personnage s'effondre et reste au sol ; à utiliser pour une mort ou un évanouissement.",
|
||||
"en": "Played once: the character collapses and stays down; use for a death or a faint."
|
||||
}
|
||||
},
|
||||
"qua_driving_loop": {
|
||||
"clip": "Driving_Loop",
|
||||
"nom": {
|
||||
"fr": "Conduite",
|
||||
"en": "Driving"
|
||||
},
|
||||
"categorie": "conduite",
|
||||
"accessoire": "volant",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de conduite, mains sur le volant ; pour un personnage assis dans un véhicule.",
|
||||
"en": "Looping driving pose, hands on the wheel; for a character seated in a vehicle."
|
||||
}
|
||||
},
|
||||
"qua_fixing_kneeling": {
|
||||
"clip": "Fixing_Kneeling",
|
||||
"nom": {
|
||||
"fr": "Réparer à genoux",
|
||||
"en": "Fix kneeling"
|
||||
},
|
||||
"categorie": "interaction",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de bricolage à genoux, pour une réparation ou un travail au sol.",
|
||||
"en": "Looping kneeling repair work, for fixing something or working at ground level."
|
||||
}
|
||||
},
|
||||
"qua_hit_chest": {
|
||||
"clip": "Hit_Chest",
|
||||
"nom": {
|
||||
"fr": "Coup à la poitrine",
|
||||
"en": "Hit in chest"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : réaction à un coup reçu au torse ; à placer après une attaque.",
|
||||
"en": "Played once: reaction to a blow to the chest; place it after an attack."
|
||||
}
|
||||
},
|
||||
"qua_hit_head": {
|
||||
"clip": "Hit_Head",
|
||||
"nom": {
|
||||
"fr": "Coup à la tête",
|
||||
"en": "Hit in head"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : réaction à un coup reçu à la tête ; à placer après une attaque.",
|
||||
"en": "Played once: reaction to a blow to the head; place it after an attack."
|
||||
}
|
||||
},
|
||||
"qua_idle_loop": {
|
||||
"clip": "Idle_Loop",
|
||||
"nom": {
|
||||
"fr": "Attente",
|
||||
"en": "Idle"
|
||||
},
|
||||
"categorie": "attente",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle d'attente debout par défaut, pour un personnage qui patiente.",
|
||||
"en": "Default standing idle loop, for a waiting character."
|
||||
}
|
||||
},
|
||||
"qua_idle_talking_loop": {
|
||||
"clip": "Idle_Talking_Loop",
|
||||
"nom": {
|
||||
"fr": "Attente en parlant",
|
||||
"en": "Idle talking"
|
||||
},
|
||||
"categorie": "conversation",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle debout avec gestes de la main, pour un personnage qui parle dans un dialogue.",
|
||||
"en": "Standing loop with hand gestures, for a character speaking in a dialogue."
|
||||
}
|
||||
},
|
||||
"qua_idle_torch_loop": {
|
||||
"clip": "Idle_Torch_Loop",
|
||||
"nom": {
|
||||
"fr": "Attente avec torche",
|
||||
"en": "Idle with torch"
|
||||
},
|
||||
"categorie": "attente",
|
||||
"accessoire": "torche",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle d'attente debout en tenant une torche ; pour une scène de nuit ou d'exploration.",
|
||||
"en": "Standing idle loop holding a torch; for a night or exploration scene."
|
||||
}
|
||||
},
|
||||
"qua_interact": {
|
||||
"clip": "Interact",
|
||||
"nom": {
|
||||
"fr": "Interagir",
|
||||
"en": "Interact"
|
||||
},
|
||||
"categorie": "interaction",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : geste pour actionner un objet (bouton, porte, levier).",
|
||||
"en": "Played once: gesture to operate an object (button, door, lever)."
|
||||
}
|
||||
},
|
||||
"qua_jog_fwd_loop": {
|
||||
"clip": "Jog_Fwd_Loop",
|
||||
"nom": {
|
||||
"fr": "Jogging",
|
||||
"en": "Jog"
|
||||
},
|
||||
"categorie": "locomotion",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de course légère sur place ; à combiner avec un déplacement du personnage.",
|
||||
"en": "Looping light jog in place; combine with a character movement."
|
||||
}
|
||||
},
|
||||
"qua_jump_land": {
|
||||
"clip": "Jump_Land",
|
||||
"nom": {
|
||||
"fr": "Réception de saut",
|
||||
"en": "Jump landing"
|
||||
},
|
||||
"categorie": "chute",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : réception au sol ; dernière phase d'un saut (après la boucle en l'air).",
|
||||
"en": "Played once: landing on the ground; last phase of a jump (after the airborne loop)."
|
||||
}
|
||||
},
|
||||
"qua_jump_loop": {
|
||||
"clip": "Jump_Loop",
|
||||
"nom": {
|
||||
"fr": "Saut en l'air",
|
||||
"en": "Jump airborne"
|
||||
},
|
||||
"categorie": "chute",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de la phase en l'air d'un saut, entre le départ et la réception.",
|
||||
"en": "Looping airborne phase of a jump, between the start and the landing."
|
||||
}
|
||||
},
|
||||
"qua_jump_start": {
|
||||
"clip": "Jump_Start",
|
||||
"nom": {
|
||||
"fr": "Départ de saut",
|
||||
"en": "Jump start"
|
||||
},
|
||||
"categorie": "chute",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : impulsion au départ d'un saut ; première phase avant la boucle en l'air.",
|
||||
"en": "Played once: take-off impulse; first phase before the airborne loop."
|
||||
}
|
||||
},
|
||||
"qua_pickup_table": {
|
||||
"clip": "PickUp_Table",
|
||||
"nom": {
|
||||
"fr": "Ramasser sur une table",
|
||||
"en": "Pick up from table"
|
||||
},
|
||||
"categorie": "interaction",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : le personnage prend un objet posé sur une table.",
|
||||
"en": "Played once: the character picks up an object lying on a table."
|
||||
}
|
||||
},
|
||||
"qua_pistol_aim_down": {
|
||||
"clip": "Pistol_Aim_Down",
|
||||
"nom": {
|
||||
"fr": "Viser vers le bas",
|
||||
"en": "Aim down"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "pistolet",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Pose de visée pistolet vers le bas ; à enchaîner avec les autres visées ou le tir.",
|
||||
"en": "Pistol aiming pose pointing down; chain with the other aims or the shot."
|
||||
}
|
||||
},
|
||||
"qua_pistol_aim_neutral": {
|
||||
"clip": "Pistol_Aim_Neutral",
|
||||
"nom": {
|
||||
"fr": "Viser droit",
|
||||
"en": "Aim straight"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "pistolet",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Pose de visée pistolet à l'horizontale ; pose de base avant un tir.",
|
||||
"en": "Pistol aiming pose at level; base pose before a shot."
|
||||
}
|
||||
},
|
||||
"qua_pistol_aim_up": {
|
||||
"clip": "Pistol_Aim_Up",
|
||||
"nom": {
|
||||
"fr": "Viser vers le haut",
|
||||
"en": "Aim up"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "pistolet",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Pose de visée pistolet vers le haut ; à enchaîner avec les autres visées ou le tir.",
|
||||
"en": "Pistol aiming pose pointing up; chain with the other aims or the shot."
|
||||
}
|
||||
},
|
||||
"qua_pistol_idle_loop": {
|
||||
"clip": "Pistol_Idle_Loop",
|
||||
"nom": {
|
||||
"fr": "Attente au pistolet",
|
||||
"en": "Pistol idle"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "pistolet",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Boucle d'attente debout, pistolet en main ; pour un personnage armé qui guette.",
|
||||
"en": "Standing idle loop with a pistol in hand; for an armed character on guard."
|
||||
}
|
||||
},
|
||||
"qua_pistol_reload": {
|
||||
"clip": "Pistol_Reload",
|
||||
"nom": {
|
||||
"fr": "Recharger le pistolet",
|
||||
"en": "Pistol reload"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "pistolet",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : rechargement du pistolet ; à placer entre deux tirs.",
|
||||
"en": "Played once: pistol reload; place it between two shots."
|
||||
}
|
||||
},
|
||||
"qua_pistol_shoot": {
|
||||
"clip": "Pistol_Shoot",
|
||||
"nom": {
|
||||
"fr": "Tir au pistolet",
|
||||
"en": "Pistol shot"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "pistolet",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : un tir de pistolet avec recul.",
|
||||
"en": "Played once: one pistol shot with recoil."
|
||||
}
|
||||
},
|
||||
"qua_punch_cross": {
|
||||
"clip": "Punch_Cross",
|
||||
"nom": {
|
||||
"fr": "Direct",
|
||||
"en": "Cross punch"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : coup de poing direct ; pour une bagarre.",
|
||||
"en": "Played once: cross punch; for a fight."
|
||||
}
|
||||
},
|
||||
"qua_punch_jab": {
|
||||
"clip": "Punch_Jab",
|
||||
"nom": {
|
||||
"fr": "Jab",
|
||||
"en": "Jab"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : coup de poing rapide ; pour une bagarre.",
|
||||
"en": "Played once: quick jab punch; for a fight."
|
||||
}
|
||||
},
|
||||
"qua_push_loop": {
|
||||
"clip": "Push_Loop",
|
||||
"nom": {
|
||||
"fr": "Pousser",
|
||||
"en": "Push"
|
||||
},
|
||||
"categorie": "interaction",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de poussée à deux mains sur place ; pour pousser un meuble ou une porte lourde.",
|
||||
"en": "Looping two-handed push in place; for pushing furniture or a heavy door."
|
||||
}
|
||||
},
|
||||
"qua_roll": {
|
||||
"clip": "Roll",
|
||||
"nom": {
|
||||
"fr": "Roulade",
|
||||
"en": "Roll"
|
||||
},
|
||||
"categorie": "sport",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : roulade au sol ; pour esquiver ou plonger.",
|
||||
"en": "Played once: ground roll; for dodging or diving."
|
||||
}
|
||||
},
|
||||
"qua_sitting_enter": {
|
||||
"clip": "Sitting_Enter",
|
||||
"nom": {
|
||||
"fr": "S'asseoir",
|
||||
"en": "Sit down"
|
||||
},
|
||||
"categorie": "assis",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : entrée, le personnage passe de debout à assis ; à suivre d'une boucle assise.",
|
||||
"en": "Played once: entry, the character goes from standing to seated; follow with a seated loop."
|
||||
}
|
||||
},
|
||||
"qua_sitting_exit": {
|
||||
"clip": "Sitting_Exit",
|
||||
"nom": {
|
||||
"fr": "Se lever",
|
||||
"en": "Stand up"
|
||||
},
|
||||
"categorie": "assis",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : sortie, le personnage passe d'assis à debout ; à placer après une boucle assise.",
|
||||
"en": "Played once: exit, the character goes from seated to standing; place after a seated loop."
|
||||
}
|
||||
},
|
||||
"qua_sitting_idle_loop": {
|
||||
"clip": "Sitting_Idle_Loop",
|
||||
"nom": {
|
||||
"fr": "Assis, attente",
|
||||
"en": "Seated idle"
|
||||
},
|
||||
"categorie": "assis",
|
||||
"accessoire": "",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Boucle d'attente assise, après « s'asseoir » ; pour un personnage installé sur une chaise.",
|
||||
"en": "Seated idle loop, after the sit-down clip; for a character settled on a chair."
|
||||
}
|
||||
},
|
||||
"qua_sitting_talking_loop": {
|
||||
"clip": "Sitting_Talking_Loop",
|
||||
"nom": {
|
||||
"fr": "Assis, en parlant",
|
||||
"en": "Seated talking"
|
||||
},
|
||||
"categorie": "assis",
|
||||
"accessoire": "",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Boucle assise avec gestes de la main, pour un dialogue entre personnages assis.",
|
||||
"en": "Seated loop with hand gestures, for a dialogue between seated characters."
|
||||
}
|
||||
},
|
||||
"qua_spell_simple_enter": {
|
||||
"clip": "Spell_Simple_Enter",
|
||||
"nom": {
|
||||
"fr": "Début d'incantation",
|
||||
"en": "Spell start"
|
||||
},
|
||||
"categorie": "magie",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : mise en position pour lancer un sort ; à suivre de la boucle d'incantation.",
|
||||
"en": "Played once: stance to begin casting a spell; follow with the spell idle loop."
|
||||
}
|
||||
},
|
||||
"qua_spell_simple_exit": {
|
||||
"clip": "Spell_Simple_Exit",
|
||||
"nom": {
|
||||
"fr": "Fin d'incantation",
|
||||
"en": "Spell end"
|
||||
},
|
||||
"categorie": "magie",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : retour à la posture normale après un sort.",
|
||||
"en": "Played once: return to normal stance after a spell."
|
||||
}
|
||||
},
|
||||
"qua_spell_simple_idle_loop": {
|
||||
"clip": "Spell_Simple_Idle_Loop",
|
||||
"nom": {
|
||||
"fr": "Incantation",
|
||||
"en": "Spell idle"
|
||||
},
|
||||
"categorie": "magie",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de concentration, mains levées, entre l'entrée et le lancer du sort.",
|
||||
"en": "Looping concentration pose, hands raised, between the entry and the cast."
|
||||
}
|
||||
},
|
||||
"qua_spell_simple_shoot": {
|
||||
"clip": "Spell_Simple_Shoot",
|
||||
"nom": {
|
||||
"fr": "Lancer un sort",
|
||||
"en": "Cast spell"
|
||||
},
|
||||
"categorie": "magie",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : le geste qui lance le sort ; à placer après la boucle d'incantation.",
|
||||
"en": "Played once: the gesture that casts the spell; place after the spell idle loop."
|
||||
}
|
||||
},
|
||||
"qua_sprint_loop": {
|
||||
"clip": "Sprint_Loop",
|
||||
"nom": {
|
||||
"fr": "Sprint",
|
||||
"en": "Sprint"
|
||||
},
|
||||
"categorie": "locomotion",
|
||||
"accessoire": "",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Boucle de course rapide sur place ; à combiner avec un déplacement pour une fuite ou une poursuite.",
|
||||
"en": "Looping fast run in place; combine with a movement for an escape or chase."
|
||||
}
|
||||
},
|
||||
"qua_swim_fwd_loop": {
|
||||
"clip": "Swim_Fwd_Loop",
|
||||
"nom": {
|
||||
"fr": "Nage",
|
||||
"en": "Swim"
|
||||
},
|
||||
"categorie": "nage",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de nage en avant ; à combiner avec un déplacement dans l'eau.",
|
||||
"en": "Looping forward swim; combine with a movement in water."
|
||||
},
|
||||
"posture": "dans l'eau"
|
||||
},
|
||||
"qua_swim_idle_loop": {
|
||||
"clip": "Swim_Idle_Loop",
|
||||
"nom": {
|
||||
"fr": "Surplace dans l'eau",
|
||||
"en": "Treading water"
|
||||
},
|
||||
"categorie": "nage",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de flottaison sur place dans l'eau.",
|
||||
"en": "Looping treading water in place."
|
||||
},
|
||||
"posture": "dans l'eau"
|
||||
},
|
||||
"qua_sword_attack": {
|
||||
"clip": "Sword_Attack",
|
||||
"nom": {
|
||||
"fr": "Attaque à l'épée",
|
||||
"en": "Sword attack"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "épée",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Joué une fois : un coup d'épée ; à enchaîner après l'attente à l'épée.",
|
||||
"en": "Played once: a sword strike; chain after the sword idle."
|
||||
}
|
||||
},
|
||||
"qua_sword_idle": {
|
||||
"clip": "Sword_Idle",
|
||||
"nom": {
|
||||
"fr": "Attente à l'épée",
|
||||
"en": "Sword idle"
|
||||
},
|
||||
"categorie": "combat",
|
||||
"accessoire": "épée",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Boucle d'attente debout, épée en main, en garde.",
|
||||
"en": "Standing idle loop, sword in hand, on guard."
|
||||
}
|
||||
},
|
||||
"qua_walk_formal_loop": {
|
||||
"clip": "Walk_Formal_Loop",
|
||||
"nom": {
|
||||
"fr": "Marche formelle",
|
||||
"en": "Formal walk"
|
||||
},
|
||||
"categorie": "locomotion",
|
||||
"accessoire": "",
|
||||
"style": "formel",
|
||||
"usage": {
|
||||
"fr": "Boucle de marche tenue et digne sur place ; à combiner avec un déplacement (cérémonie, travail).",
|
||||
"en": "Looping upright, dignified walk in place; combine with a movement (ceremony, office)."
|
||||
}
|
||||
},
|
||||
"qua_walk_loop": {
|
||||
"clip": "Walk_Loop",
|
||||
"nom": {
|
||||
"fr": "Marche",
|
||||
"en": "Walk"
|
||||
},
|
||||
"categorie": "locomotion",
|
||||
"accessoire": "",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Boucle de marche sur place ; à combiner avec un déplacement du personnage.",
|
||||
"en": "Looping walk in place; combine with a character movement."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
; Config/LPSandbox.ini
|
||||
; Contrat S1-T1 — chemins et options du bac a sable LPPairSandbox.
|
||||
; Toute cle absente => valeur par defaut du code (identique a celle-ci).
|
||||
; Chemins "/Game/..." : format LoadObject (/Package/Nom.Nom ou /Package/BP.BP_C).
|
||||
; Chemins relatifs (sans "/") : relatifs a Content/ sauf [Sandbox.Output] (relatif a Saved/).
|
||||
|
||||
[Sandbox.Mode]
|
||||
; Mode initial du menu : Pair | Interaction | Mesure
|
||||
; (Saved/sandbox_mode.txt, s'il existe, reste prioritaire et est consomme.)
|
||||
Mode=Pair
|
||||
|
||||
[Sandbox.Assets]
|
||||
PersonBP=/Game/BPs/Person.Person_C
|
||||
BridgetMesh=/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp
|
||||
MichaelMesh=/Game/Heavy/Daz/Michael/michael.michael
|
||||
QuaterniusMannequinMesh=/Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard
|
||||
DazIdleFemale=/Game/Heavy/Daz/Anims/Idle_F_Daz.Idle_F_Daz
|
||||
DazIdleMale=/Game/Heavy/Daz/Anims/idle_Daz.idle_Daz
|
||||
FurnitureWallMaterial=/Game/Heavy/New2/Furniture/Materials/FntWall.FntWall
|
||||
|
||||
[Sandbox.Roots]
|
||||
; Racines de packages UE (sans nom d'asset final)
|
||||
FurnitureRoot=/Game/Heavy/New2/Furniture
|
||||
DazAnimRoot=/Game/Heavy/Daz/Anims
|
||||
QuaterniusRoot=/Game/Quaternius
|
||||
QuaterniusUALRoot=/Game/Quaternius/UAL
|
||||
|
||||
[Sandbox.Content]
|
||||
; Chemins disque, relatifs a Content/
|
||||
ModuleRoot=Modules/vin_Base
|
||||
WorldFile=Modules/vin_Base/World/work___vin01.lpworld
|
||||
ChatAction=Modules/vin_Base/Interactions/chat.lpaction
|
||||
QuaterniusUALDisk=Quaternius/UAL
|
||||
|
||||
[Sandbox.Sky]
|
||||
; Optionnel : cubemap du SkyLight du sandbox
|
||||
SkyCubemap=/Game/Heavy/New2/Buildings/Mats/urban_street_03_1k.urban_street_03_1k
|
||||
SkyIntensity=0.6
|
||||
|
||||
[Sandbox.Output]
|
||||
; Relatif a Saved/
|
||||
MeasureOutputRoot=Measurement
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
> Toutes les commandes se lancent depuis `D:\Game\LifePlay-master 5.5`, sous PowerShell 7. Explications :
|
||||
> `08_Guide_Developpeur.md`. Méthode : `10_Protocole_Operations.md`.
|
||||
>
|
||||
> **Atelier de mods (DC3, 2026-10-07)** : `BuildMod`, `NewMod`, `EditMod`, `HairPipeline`, `AnimPipeline`, `DeliverAnims`, `ModSource` et les lanceurs sandbox se lancent depuis `D:\Game\LifePlay-Sandbox` (`.\Tools\...`), avec `-GameRoot` facultatif (défaut : `[Workshop] GameRoot` de `Config\LPSandbox.ini` de l'addon) ; `ModSources\`, `AnimSources\` et `Tools\{anim,hairs,mods}` y sont aussi. Dans ce document, les chemins `.\Tools\<script de mod>` et `<projet>/Tools/Hair*.py` s'entendent donc dans l'addon.
|
||||
|
||||
```powershell
|
||||
$UE = 'D:\Game\Launcher\Engine\UE_5.5\Engine'
|
||||
|
|
@ -211,6 +213,7 @@ cmd /c '"<UE>\Build\BatchFiles\RunUAT.bat" BuildCookRun -project="<projet>\LifeP
|
|||
Get-FileHash Releases\<version>\Windows\LifePlay-Windows.utoc, <build>\Windows\LifePlay\Content\Paks\LifePlay-Windows.utoc
|
||||
# 5. Releases\ ne garde que la version publiée (les précédentes vers LifePlay_Backup_5.5\Releases) : l'archive des
|
||||
# sources la contient (≈ 1,3 Go) ; exclure aussi les produits de cuisson des mods : -x!ModSources\*\Saved
|
||||
# 6. (zips de mods d'assets : produits par LifePlay-Sandbox\Tools\BuildMod.ps1, lancé depuis D:\Game\LifePlay-Sandbox)
|
||||
# 6. Mods officiels recuits contre la nouvelle version : .\Tools\BuildMod.ps1 -Name lpr_hair_pocolov
|
||||
# Modules livrés à part : Tools\modules\next.json (essentiels : vin_Base, vin_VanillaRealistic ; lp_Revival retiré en H3d)
|
||||
# Un zip par module (15_Modules_Optionnels.md §5), dans un même dossier que les mods d'assets ; diffusion sur Discord :
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# 10 — Protocole suivi pendant les opérations
|
||||
|
||||
> Depuis le lot DC3 (2026-10-07), ces scripts, `ModSources/` et `AnimSources/` sont dans le dépôt addon `LifePlay-Sandbox` (même disposition relative ; lancer depuis l'addon, `D:\Game\LifePlay-Sandbox`).
|
||||
|
||||
> Règles appliquées du 2026-09-29 au 2026-10-02 pour toute modification du projet, et leçons tirées des
|
||||
> erreurs commises. Elles valent pour toute reprise du projet, par un développeur comme par un agent.
|
||||
> Détail des tests : `03_Plan_Migration_Tests.md` ; correctifs de contenu : `05_Correctifs_Isoles.md`.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# 13. Protocole de conversion d'une coiffure
|
||||
|
||||
> Depuis le lot DC3 (2026-10-07), ces scripts, `ModSources/` et `AnimSources/` sont dans le dépôt addon `LifePlay-Sandbox` (même disposition relative ; lancer depuis l'addon, `D:\Game\LifePlay-Sandbox`).
|
||||
|
||||
Ce protocole décrit comment ajouter au jeu une coiffure livrée en **maillage statique** (FBX de Fab ou d'ailleurs),
|
||||
sans modifier aucun Blueprint. Il a été établi sur *Pocolov Hair 17* (lot O7e, `11_Audit_Optimisation.md` §16.8) puis
|
||||
rejoué de bout en bout avec les outils ci-dessous : résultat identique au millimètre.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# 16. H3 : assets apportés par les mods (étude, 2026-10-03)
|
||||
|
||||
> Depuis le lot DC3 (2026-10-07), ces scripts, `ModSources/` et `AnimSources/` sont dans le dépôt addon `LifePlay-Sandbox` (même disposition relative ; lancer depuis l'addon, `D:\Game\LifePlay-Sandbox`).
|
||||
|
||||
Objectif : qu'un mod apporte ses propres maillages, textures et matériaux (coiffures d'abord, puis vêtements, meubles,
|
||||
animations) **sans recompiler ni recuire le jeu**. Aujourd'hui, un mod `Content/Modules/<mod>` ne contient que du texte :
|
||||
tout asset doit être cuit avec le jeu (lots H1, H2).
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# 20. Chaîne des animations : de l'achat du pack au mod livré (étude, 2026-10-06)
|
||||
|
||||
> Depuis le lot DC3 (2026-10-07), ces scripts, `ModSources/` et `AnimSources/` sont dans le dépôt addon `LifePlay-Sandbox` (même disposition relative ; lancer depuis l'addon, `D:\Game\LifePlay-Sandbox`).
|
||||
|
||||
Objectif : qu'un pack d'animations, quel que soit son squelette d'origine, soit converti pour les corps du jeu
|
||||
(Genesis 8 : Bridget, Michael), évalué et livré en mod **sans intervention manuelle**, avec un contrôle bloquant à
|
||||
chaque étape. Base technique : la méthode validée dans la sandbox Mesure (CHG-188 à 204, mémoire « transfert de
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ Le lot S1 fixe le premier contrat jeu ↔ addon : **tous les chemins d'assets et
|
|||
`Config/LPSandbox.ini`**, lu par un point d'accès unique. Détail : `etat/sandbox/S1/S1-SPEC.md` ; implémentation
|
||||
`S1-IMPL.md` ; audit `S1-CART.md` ; validation `S1-VAL.md` (PASS statique ; compilation et runtime à confirmer).
|
||||
|
||||
- **Fichier** : `Config/LPSandbox.ini` (versionné, pas dans `Saved/`), lecture directe non hiérarchique (`FConfigFile::Read`).
|
||||
- **Fichier** : `Config/LPSandbox.ini` (versionné, pas dans `Saved/` ; depuis DC3 il vit dans l'addon, avec sa section `[Workshop]` `GameRoot`), lecture directe non hiérarchique (`FConfigFile::Read`).
|
||||
- **API** : `struct LIFEPLAY_API FLPSandboxConfig` (`Source/LifePlay/LPSandboxConfig.h/.cpp`), `static const FLPSandboxConfig& Get()`, chargé une fois. `LPPairSandbox.cpp` et `LPAnimRedirect::ClipPath` n'utilisent plus de littéral `/Game/...`.
|
||||
- **Repli** : fichier, clé ou valeur vide absents => default = littéral historique (comportement identique sans ini). Un asset introuvable garde le journal `LogLifePlay` d'origine, sans repli silencieux.
|
||||
- **19 clés, 6 sections** : `Sandbox.Mode` (`Mode` = `Pair` | `Interaction` | `Mesure`, `Saved/sandbox_mode.txt` reste prioritaire) ; `Sandbox.Assets` (`PersonBP`, `BridgetMesh`, `MichaelMesh`, `QuaterniusMannequinMesh`, `DazIdleFemale`, `DazIdleMale`, `FurnitureWallMaterial`) ; `Sandbox.Roots` (`FurnitureRoot`, `DazAnimRoot`, `QuaterniusRoot`, `QuaterniusUALRoot`) ; `Sandbox.Content`, chemins relatifs à `Content/` (`ModuleRoot`, `WorldFile`, `ChatAction`, `QuaterniusUALDisk`) ; `Sandbox.Sky` (`SkyCubemap`, `SkyIntensity`) ; `Sandbox.Output`, relatif à `Saved/` (`MeasureOutputRoot`).
|
||||
|
|
@ -124,12 +124,12 @@ D:\Game\
|
|||
|
||||
- **Liaison** : `LifePlay.uproject` déclare `"AdditionalPluginDirectories": ["../LifePlay-Sandbox/Plugins"]` (chemin relatif au `.uproject`) et active `LPSandbox` dans `Plugins`. UBT et l'éditeur y trouvent `LPSandbox.uplugin` (modules `LPSandbox` et `LPAnimMeasure`) ; les DLL du plugin sont compilées avec le jeu (`Build.bat LifePlayEditor Win64 Development`).
|
||||
- **Pourquoi pas un submodule** : l'addon est un `uproject` mince qui contient le plugin sous `Plugins/LPSandbox/`. Un submodule monté sur `Plugins/LPSandbox` du jeu donnerait `Plugins/LPSandbox/Plugins/LPSandbox/` (plugin imbriqué, non scanné au bon niveau) ; un submodule `External/LifePlay-Sandbox` dupliquerait l'addon à côté du sibling et rendrait ambigu l'exemplaire chargé. Un clone sibling évite les deux problèmes.
|
||||
- **Pin** : le jeu consomme un commit précis de l'addon, consigné à la main dans `S3-INT.md` (aujourd'hui `97c0d134cd1388c3735add79d77ea98817e89fbe`, `lot/DC2-modtools`, DC2 module éditeur `LPModTools` et chemins propres à l'addon ; précédents : `3e9c32f1f27b1abdaf7731dc90496c27ee061718` DC1 `LPAnimRedirect` et `LPCoupleSim` rapatriés dans le plugin `LPAnimCore` du jeu, `16905c68070992f5139dab907b41b461fb765957` SB4 ouverture par déclencheur éphémère non mémorisée, `3d858527fccfdddd8711c7c7448fa001442e0b8a` SB2-O1 correction de `ShowRoom`, `fece7e9a0e63b3058b5d24d96d669a3c9c0e9add` SB2 bibliothèques de scène et de personnages, caméra du socle, état des modes sorti vers les mods, `82dae58380b76c45f60541fd7cb5a0a409700e10` SB1 registre de mods et barre latérale, `3b21fd180c04f0fa49a1b5c9ef1b517ade1cb362` SB0 conception de la sandbox modulaire, `f09c42e5a2bc613cf269cce76c878461ca41c8b7` S7-T2 uplugin, `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` S5-T2 `LPCoupleSim`, `f8102647ccdebad37e4977ccbf2c51e583823a80`, S4-T3). Le pin est mis à jour (hash dans `S3-INT.md` et README de l'addon) à chaque évolution de l'addon que le jeu doit prendre. Reproduire :
|
||||
- **Pin** : le jeu consomme un commit précis de l'addon, consigné à la main dans `S3-INT.md` (aujourd'hui `79f0205b7281152dde7cd088086d8c813e0b20f5`, `lot/DC3-atelier`, DC3 atelier de mods (scripts, `ModSources`, `AnimSources`) dans l'addon ; précédents : `97c0d134cd1388c3735add79d77ea98817e89fbe` DC2 module éditeur `LPModTools` et chemins propres à l'addon, `3e9c32f1f27b1abdaf7731dc90496c27ee061718` DC1 `LPAnimRedirect` et `LPCoupleSim` rapatriés dans le plugin `LPAnimCore` du jeu, `16905c68070992f5139dab907b41b461fb765957` SB4 ouverture par déclencheur éphémère non mémorisée, `3d858527fccfdddd8711c7c7448fa001442e0b8a` SB2-O1 correction de `ShowRoom`, `fece7e9a0e63b3058b5d24d96d669a3c9c0e9add` SB2 bibliothèques de scène et de personnages, caméra du socle, état des modes sorti vers les mods, `82dae58380b76c45f60541fd7cb5a0a409700e10` SB1 registre de mods et barre latérale, `3b21fd180c04f0fa49a1b5c9ef1b517ade1cb362` SB0 conception de la sandbox modulaire, `f09c42e5a2bc613cf269cce76c878461ca41c8b7` S7-T2 uplugin, `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` S5-T2 `LPCoupleSim`, `f8102647ccdebad37e4977ccbf2c51e583823a80`, S4-T3). Le pin est mis à jour (hash dans `S3-INT.md` et README de l'addon) à chaque évolution de l'addon que le jeu doit prendre. Reproduire :
|
||||
|
||||
```powershell
|
||||
cd D:\Game
|
||||
git clone https://github.com/edujiro5972/LifePlay-Sandbox.git
|
||||
git -C LifePlay-Sandbox checkout 97c0d134cd1388c3735add79d77ea98817e89fbe
|
||||
git -C LifePlay-Sandbox checkout 79f0205b7281152dde7cd088086d8c813e0b20f5
|
||||
# le jeu doit être cloné à côté : D:\Game\LifePlay-master 5.5
|
||||
```
|
||||
|
||||
|
|
@ -188,13 +188,14 @@ Le lot S5 ferme la **dérive de fork** entre la sandbox (`LPSandbox/LPCoupleSim.
|
|||
- [x] **DC0** : cartographie et SPEC du découplage jeu / sandbox (`etat/sandbox/DC0/DC0-SPEC.md`, approuvée par l'utilisateur 2026-10-07) ; plan `24_Plan_Decouplage_Sandbox.md`.
|
||||
- [x] **DC1** : Plugin LPAnimCore dans le jeu, LPAnimMeasure réduit à la mesure (DC1-T1..T5) (DC1-VAL PASS, contrôle visuel utilisateur 2026-10-07 ; tag addon lot-DC1-valide).
|
||||
- [x] **DC2** : Commandlets de mods vers un module éditeur de la sandbox (DC2-T1..T5) (DC2-VAL PASS ; tag addon lot-DC2-valide ; le jeu compile et démarre sans l'addon).
|
||||
- [ ] **DC3** : Scripts, données et sources de mods vers la sandbox (DC3-T1..T5).
|
||||
- [x] **DC3** : Scripts, données et sources de mods vers la sandbox (DC3-T1..T5) (DC3-VAL PASS ; tag addon lot-DC3-valide ; atelier de mods dans l'addon).
|
||||
- [ ] **DC4** : Preuve « jeu seul », docs, évaluation (DC4-T1..T4).
|
||||
- [ ] Intégration des déplacements dans le jeu à améliorer (remarque utilisateur au contrôle visuel DC1, 2026-10-07) ; lot propriétaire : à définir, hors DC.
|
||||
- [x] Reports DC0 (lot propriétaire **DC1**, traité : texte corrigé en § Rupture du couplage) : § Rupture du couplage : la mention d'un appel à SetUALRoot par FLifePlayModule::StartupModule() est périmée ; seul l'addon l'appelle (LPSandbox.cpp:17, LPSandboxMeasure.cpp:39) — voir DC0-CART b2.
|
||||
- [ ] Report DC2 : `LPAnimCatalog` ignore `-DryRun` et a des valeurs par défaut (pack quaternius) : lancé sans arguments, il réécrit `Saved/AnimPipeline/quaternius/fiches_calculees.json` (DC2-VAL anomalie 1) — lot propriétaire DC3.
|
||||
- [x] Report DC2 (traité en DC3 : `-Livraison=` obligatoire) : `LPAnimCatalog` ignore `-DryRun` et a des valeurs par défaut (pack quaternius) : lancé sans arguments, il réécrit `Saved/AnimPipeline/quaternius/fiches_calculees.json` (DC2-VAL anomalie 1) — lot propriétaire DC3.
|
||||
- [ ] Report DC2 : `LifePlay.Build.cs` garde `Json` avec un commentaire AP1 périmé (plus aucun usage dans `LifePlay`) — lot propriétaire DC4.
|
||||
- [ ] Report DC2 : sans addon, l'absence est silencieuse au runtime ; l'éditeur/UBT affiche `AdditionalPluginDirectory … not found` (avertissement, non bloquant) — pour information, DC4 docs.
|
||||
- [ ] Report DC3 : après un test sans addon, UBT réutilise un `Makefile.bin` sans les modules de l'addon : le déplacer avant de recompiler (DC3-BUILD) — à écrire dans `09`/`18` — lot propriétaire DC4.
|
||||
## Lots (S0 … S8)
|
||||
|
||||
| Lot | Objet (une ligne) |
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# 23. Cycle de vie d'un mod (2026-10-07)
|
||||
|
||||
> Depuis le lot DC3 (2026-10-07), ces scripts, `ModSources/` et `AnimSources/` sont dans le dépôt addon `LifePlay-Sandbox` (même disposition relative ; lancer depuis l'addon, `D:\Game\LifePlay-Sandbox`).
|
||||
|
||||
Référence pour les développeurs du jeu et des outils de mods : ce qu'est un mod LifePlay, comment il est fabriqué,
|
||||
livré, découvert, activé et consommé, quelles propriétés sont réellement lues, et ce qui pourrait évoluer.
|
||||
Constats relevés dans le code du jeu, des Blueprints (`GI`) et du moteur UE 5.5 installé
|
||||
|
|
|
|||
|
|
@ -1131,6 +1131,7 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol
|
|||
| CHG-155 | 2026-10-05 | ShowCouple | Un lit est reconnu par son nom. Il ne bloque un déplacement que vers le bas, pour se poser ; le retour horizontal n'est plus arrêté et le contact ne renvoie plus la boîte | après avoir quitté le lit, la boîte ne pouvait plus y revenir | revenir au test `DoesPackageExist` et au filtre du flanc seul |
|
||||
| CHG-156 | 2026-10-05 | Clavier | `LPKeyboardSubsystem` : bouton « Clavier » ajouté à l'exécution dans `MainMenu.SettingsScrollBox` ; panneau pour changer, ajouter ou retirer une touche (actions et axes clavier). Écrit dans `Saved/Config/Windows`, pas dans `GameSettings.sav` | pouvoir réaffecter les touches, dont Q, sans modifier les Blueprints | retirer `LPKeyboardSubsystem.h/.cpp` et la dépendance `UMG` |
|
||||
| CHG-158 | 2026-10-05 | Réglages | `LPSettingsSubsystem` : onglets Audio, Video, Lewd, General, Keyboard, Misc au-dessus de `SettingsScrollBox`. Les lignes du Blueprint restent en place et sont montrées selon leur texte ; le reste va dans Misc. Keyboard affiche la liste des touches | la liste unique ne montrait pas le clavier | retirer `LPSettingsSubsystem.h/.cpp` |
|
||||
| CHG-236 | 2026-10-07 | DC3 | jeu : retrait (57 fichiers suivis) de `Tools/BuildMod.ps1`, `NewMod.ps1`, `EditMod.ps1`, `ModSource.ps1`, `HairPipeline.ps1`, `HairImportFbx.py`, `HairMaterials.py`, `FbxMaterialMap.py`, `ImportQuaternius.py`, `DiagnosePose.py`, `Tools/anim/`, `Tools/hairs/`, `Tools/mods/`, `ModSources/`, `AnimSources/`, `Config/LPSandbox.ini`, `Tools/debug/*_sandbox.bat`, `Docs/Modding/FullGuides/Creating_Asset_Mods.md`, `Docs/Modding/ShortGuides/explanation.lphair` (contenu disque déplacé, non supprimé, vers `D:\Game\LifePlay_Backup_5.5\DC3\deplaces_du_jeu`) ; références `SplitModules.ps1`, `LPModInfoSubsystem.h`, `BuildDebug.ps1`, `explanation.lpmod`, `README.md`, `.gitignore` ; docs (`21_Sandbox_Addon.md`, `09_Commandes_Utiles.md`, `13_Protocole_Coiffure.md`, `16_H3_Mods_Paquets.md`, `10_Protocole_Operations.md`, `23_Cycle_De_Vie_Mod.md`, `20_Chaine_Animations.md`, `README.md`, `etat/sandbox/S3/S3-INT.md`, `etat/sandbox/DC3/*`) ; addon `D:\Game\LifePlay-Sandbox` : atelier de mods (`Tools/*`, `Tools/Roots.ps1` nouveau, `ModSources`, `AnimSources`, lanceurs, `Docs/Modding`, `Config/LPSandbox.ini` section `[Workshop]`, `.gitignore`), `LPAnimCatalogCommandlet.cpp` (`-Livraison=` obligatoire), `LPAnimMeasurePaths.cpp/.h` et `LPSandboxConfig.cpp` (replis vers le jeu retirés) (commits `1f4ec1f` et `79f0205b7281152dde7cd088086d8c813e0b20f5`, branche `lot/DC3-atelier`, tag `lot-DC3-valide`) | lot DC3 : l'atelier de mods vit dans l'addon ; cuisson de `lpr_anim_freeanims` depuis l'addon identique au mod publié (29 fichiers, SHA-256) ; tournée freeanims identique à SB2-REF (DC3-VAL PASS) ; piège UBT `Makefile.bin` (cache généré sans addon) traité. Motif : le jeu public ne doit plus embarquer l'outillage ni les sources de mods du dépôt privé `LifePlay-Sandbox` | pin `97c0d13` → `79f0205` (hash complet `79f0205b7281152dde7cd088086d8c813e0b20f5`) | `.\Tools\Restore.ps1 -Lot DC3` (jeu ; remettre aussi le contenu de `DC3\deplaces_du_jeu`) et `.\Tools\Restore.ps1 -Lot DC3-addon -ProjectRoot D:\Game\LifePlay-Sandbox` ; pin `97c0d13` |
|
||||
| CHG-235 | 2026-10-07 | DC2 | jeu : `Source/LifePlayEditorTools/` (13 sources retirées : `LPAnimEvalCommandlet`, `LPAnimTwistCommandlet`, `LPAnimCatalogCommandlet`, `LPAnimIdentifyCommandlet`, `LPAnimChecks`, `LPHairFromStaticMeshCommandlet` en `.h/.cpp`, `LPAnimMethodSelect.h`), `LifePlayEditorTools.Build.cs`, docs (`21_Sandbox_Addon.md`, `20_Chaine_Animations.md`, `09_Commandes_Utiles.md`, `08_Guide_Developpeur.md`, `etat/sandbox/S3/S3-INT.md`, `etat/sandbox/DC2/*`) ; addon `D:\Game\LifePlay-Sandbox` : module `LPModTools` (nouveau), `LPAnimMeasurePaths.h/.cpp` (nouveau), `LPSkeletonProfile.cpp`, `LPPoseMeasure.cpp`, `LPAnimMeasure.Build.cs`, `LPSandboxConfig.cpp`, `LPSandbox.uplugin` (commit `97c0d134cd1388c3735add79d77ea98817e89fbe`, branche `lot/DC2-modtools`, tag `lot-DC2-valide`) | lot DC2 : les commandlets de mods (`LPAnimEval`, `LPAnimTwist`, `LPAnimCatalog`, `LPAnimIdentify`, `LPHairFromStaticMesh`) passent dans le module éditeur `LPModTools` de l'addon ; le jeu compile sans l'addon (DC2-BUILD) et démarre sans lui (DC2-VAL E5) ; résultats identiques à SB2-REF (DC2-VAL PASS). Motif : le jeu public ne doit plus dépendre du dépôt privé `LifePlay-Sandbox` pour son outillage éditeur | pin `3e9c32f` → `97c0d13` (hash complet `97c0d134cd1388c3735add79d77ea98817e89fbe`) | `.\Tools\Restore.ps1 -Lot DC2` (jeu) et `.\Tools\Restore.ps1 -Lot DC2-addon -ProjectRoot D:\Game\LifePlay-Sandbox` ; pin `3e9c32f` |
|
||||
| CHG-234 | 2026-10-07 | DC1 | jeu : `Plugins/LPAnimCore/*` (nouveau plugin : `LPAnimRedirect.*`, `LPCoupleSim.h`, `LPCoupleSimCore.cpp`), `LifePlay.uproject`, `Source/LifePlay/LifePlay.Build.cs`, docs (`21_Sandbox_Addon.md`, `20_Chaine_Animations.md`, `07_Architecture_5.5.md`, `etat/sandbox/S3/S3-INT.md`, `etat/sandbox/DC1/*`) ; addon `D:\Game\LifePlay-Sandbox` : `LPSandbox.uproject`, `LPSandbox.uplugin`, `LPAnimMeasure.Build.cs`, `LPAnimMeasure.h`, `LPSandbox.Build.cs`, suppression de `LPAnimRedirect.*`, `LPCoupleSim.h`, `LPCoupleSimCore.cpp` (commit `3e9c32f1f27b1abdaf7731dc90496c27ee061718`, branche `lot/DC1-animcore`, tag `lot-DC1-valide`) | lot DC1 : `LPAnimRedirect` et `LPCoupleSim` rapatriés dans le plugin `LPAnimCore` du jeu, `LPAnimMeasure` réduit à la mesure ; compilation PASS, DC1-VAL PASS, contrôle visuel utilisateur 2026-10-07 ; report DC0 SetUALRoot traité. Motif : le jeu public doit compiler sans le dépôt privé `LifePlay-Sandbox` (premier pas du découplage) | pin `16905c6` → `3e9c32f` (hash complet `3e9c32f1f27b1abdaf7731dc90496c27ee061718`) | `.\Tools\Restore.ps1 -Lot DC1` (jeu) et `.\Tools\Restore.ps1 -Lot DC1-addon -ProjectRoot D:\Game\LifePlay-Sandbox` ; pin `16905c6` |
|
||||
| CHG-233 | 2026-10-07 | DC0 | `Docs/Migration/24_Plan_Decouplage_Sandbox.md` (nouveau), `Docs/Migration/etat/sandbox/DC0/DC0-CART.md`, `DC0-SPEC.md`, `DC0-VAL.md` (nouveaux) | lot DC0 : plan du découplage jeu / sandbox (lots DC0 à DC4), cartographie et SPEC approuvées par l'utilisateur le 2026-10-07 (plugin `LPAnimCore` dans le jeu, module éditeur `LPModTools` et tout l'outillage de mods dans l'addon, dépôt Sandbox privé) ; documentation seule, aucun code. Motif : le jeu public doit compiler sans le dépôt privé `LifePlay-Sandbox` (`LifePlay` et `LifePlayEditorTools` dépendent de `LPAnimMeasure`) | aucun pin (documentation seule) | supprimer ces fichiers (documentation seule) ; rien n'est commité dans le dépôt du jeu ni poussé |
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# Migration LifePlay 4.27 → 5.5 — point d'entrée
|
||||
|
||||
> Depuis le lot DC3 (2026-10-07), ces scripts, `ModSources/` et `AnimSources/` sont dans le dépôt addon `LifePlay-Sandbox` (même disposition relative ; lancer depuis l'addon, `D:\Game\LifePlay-Sandbox`).
|
||||
|
||||
La migration a été reprise le 2026-09-29, après l'échec d'un premier portage.
|
||||
|
||||
**État au 2026-10-02** :
|
||||
|
|
|
|||
105
Docs/Migration/etat/sandbox/DC3/DC3-BUILD.md
Normal file
105
Docs/Migration/etat/sandbox/DC3/DC3-BUILD.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# DC3-BUILD — Compilation éditeur LifePlay
|
||||
|
||||
**Fiche** : DC3-T3a | **Modèle prévu** : Haiku | **Modèle utilisé** : Haiku
|
||||
**Agent** : A-BUILD | **Date** : 2026-10-07
|
||||
|
||||
---
|
||||
|
||||
## Commande
|
||||
|
||||
```powershell
|
||||
& 'D:\Game\Launcher\Engine\UE_5.5\Engine\Build\BatchFiles\Build.bat' LifePlayEditor Win64 Development `
|
||||
-Project='D:\Game\LifePlay-master 5.5\LifePlay.uproject' -WaitMutex `
|
||||
*> 'D:\Game\LifePlay-master 5.5\Docs\Migration\etat\sandbox\DC3\DC3_Build_LifePlayEditor.txt'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Résultats
|
||||
|
||||
### Code de sortie
|
||||
**0** — Compilation réussie sans erreurs de build.
|
||||
|
||||
### Erreurs
|
||||
Aucune
|
||||
|
||||
### Avertissements
|
||||
Aucun
|
||||
|
||||
### Fichiers compilés
|
||||
```
|
||||
[1/4] Compile [x64] Module.LifePlay.cpp
|
||||
[2/4] Link [x64] UnrealEditor-LifePlay.lib
|
||||
[3/4] Link [x64] UnrealEditor-LifePlay.dll
|
||||
[4/4] WriteMetadata LifePlayEditor.target
|
||||
```
|
||||
|
||||
**Note** : Le système de compilation incrémental (adaptive non-unity build) a détecté et compilé `Module.LifePlay.cpp`. Les fichiers `LPAnimMeasurePaths.cpp`, `LPSandboxConfig.cpp`, `LPAnimCatalogCommandlet.cpp` n'apparaissent pas dans le journal de compilation.
|
||||
|
||||
### DLL générées
|
||||
| Fichier | LastWriteTime |
|
||||
|---------|---------------|
|
||||
| UnrealEditor-LPAnimMeasure.dll | 07/10/2026 18:34:36 |
|
||||
| UnrealEditor-LPModTools.dll | 07/10/2026 18:34:50 |
|
||||
| UnrealEditor-LPSandbox.dll | 07/10/2026 18:34:55 |
|
||||
|
||||
### Dossier Binaries de l'addon
|
||||
`D:\Game\LifePlay-Sandbox\Plugins\LPSandbox\Binaries` : **N'existe pas** ✓
|
||||
|
||||
### Journal complet
|
||||
Chemin : `D:\Game\LifePlay-master 5.5\Docs\Migration\etat\sandbox\DC3\DC3_Build_LifePlayEditor.txt`
|
||||
|
||||
---
|
||||
|
||||
## Verdict
|
||||
|
||||
✓ **Compilation réussie** — Code de sortie 0, pas d'erreurs, DLL à jour, structure attendue.
|
||||
|
||||
---
|
||||
|
||||
## Relance après déplacement du Makefile.bin (DC3-T3a-fix) — Modèle prévu haiku | utilisé haiku
|
||||
|
||||
### Commande
|
||||
```powershell
|
||||
& 'D:\Game\Launcher\Engine\UE_5.5\Engine\Build\BatchFiles\Build.bat' LifePlayEditor Win64 Development -Project='D:\Game\LifePlay-master 5.5\LifePlay.uproject' -WaitMutex *> 'D:\Game\LifePlay-master 5.5\Docs\Migration\etat\sandbox\DC3\DC3_Build_LifePlayEditor_2.txt'
|
||||
```
|
||||
|
||||
### Code de sortie
|
||||
**0** — Compilation réussie.
|
||||
|
||||
### Verdict
|
||||
Succès. Les modules de l'addon (LPAnimMeasure, LPSandbox, LPModTools) ont été recompilés et les DLL ont été reliées après suppression du cache Makefile.bin.
|
||||
|
||||
### Erreurs
|
||||
Aucune
|
||||
|
||||
### Avertissements
|
||||
Aucun
|
||||
|
||||
### Lignes Compile [x64]
|
||||
```
|
||||
[1/11] Compile [x64] LPAnimCatalogCommandlet.cpp
|
||||
[2/11] Compile [x64] LPAnimIdentifyCommandlet.cpp
|
||||
[4/11] Compile [x64] Module.LPAnimMeasure.cpp
|
||||
[8/11] Compile [x64] Module.LPSandbox.cpp
|
||||
```
|
||||
|
||||
### Lignes Link [x64]
|
||||
```
|
||||
[3/11] Link [x64] UnrealEditor-LPModTools.lib
|
||||
[5/11] Link [x64] UnrealEditor-LPAnimMeasure.lib
|
||||
[6/11] Link [x64] UnrealEditor-LPAnimMeasure.dll
|
||||
[7/11] Link [x64] UnrealEditor-LPModTools.dll
|
||||
[9/11] Link [x64] UnrealEditor-LPSandbox.lib
|
||||
[10/11] Link [x64] UnrealEditor-LPSandbox.dll
|
||||
```
|
||||
|
||||
### DLL générées
|
||||
| Fichier | LastWriteTime |
|
||||
|---------|---------------|
|
||||
| UnrealEditor-LPAnimMeasure.dll | 07/10/2026 19:36:42 |
|
||||
| UnrealEditor-LPModTools.dll | 07/10/2026 19:36:42 |
|
||||
| UnrealEditor-LPSandbox.dll | 07/10/2026 19:36:45 |
|
||||
|
||||
### Journal complet
|
||||
Chemin : `D:\Game\LifePlay-master 5.5\Docs\Migration\etat\sandbox\DC3\DC3_Build_LifePlayEditor_2.txt`
|
||||
29
Docs/Migration/etat/sandbox/DC3/DC3-DOC.md
Normal file
29
Docs/Migration/etat/sandbox/DC3/DC3-DOC.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# DC3-DOC — Pin et documentation du lot DC3
|
||||
|
||||
**Fiche DC3-T4 (A-INT, partie pin) et DC3-T5 (A-DOC) — Modèle prévu sonnet | utilisé sonnet**
|
||||
|
||||
- Date : 2026-10-07. Aucune commande git d'écriture, aucun fichier de code. Les fins de ligne (LF) des fichiers sont conservées.
|
||||
- Nouveau pin de l'addon : `79f0205b7281152dde7cd088086d8c813e0b20f5` (`79f0205`, branche `lot/DC3-atelier`, tag `lot-DC3-valide`, local, non poussé ; commits `1f4ec1f` et `79f0205`). Pin précédent : `97c0d134cd1388c3735add79d77ea98817e89fbe` (DC2).
|
||||
|
||||
## DC3-T4 (partie pin)
|
||||
|
||||
| Fichier | Lignes touchées |
|
||||
|---|---|
|
||||
| `Docs/Migration/etat/sandbox/S3/S3-INT.md` | 11 (commit pin), 13 (DC2 ajouté en tête des pins précédents), 49 (checkout) |
|
||||
| `Docs/Migration/21_Sandbox_Addon.md` | 127 (paragraphe Pin : nouveau hash en tête, DC2 en tête des précédents), 132 (checkout) |
|
||||
| `D:\Game\LifePlay-Sandbox\README.md` | commit pin, pin précédent, checkout |
|
||||
|
||||
## DC3-T5 (documentation)
|
||||
|
||||
| Fichier | Modification |
|
||||
|---|---|
|
||||
| `Docs/Migration/Journal_Modifications.md` | 1134 : CHG-236 (7 colonnes), juste au-dessus de CHG-235 |
|
||||
| `Docs/Migration/21_Sandbox_Addon.md` | `Config/LPSandbox.ini` : précision « vit dans l'addon » (§ S1) ; DC3 coché ; report DC2 `LPAnimCatalog` coché (traité en DC3) ; nouveau report DC3 non coché (`Makefile.bin` d'UBT, lot propriétaire DC4, à écrire dans `09`/`18`) |
|
||||
| `Docs/Migration/09_Commandes_Utiles.md` | note en tête (commandes de l'atelier lancées depuis `D:\Game\LifePlay-Sandbox`, `-GameRoot`, `[Workshop] GameRoot`) ; § 7 : les zips de mods d'assets viennent de `LifePlay-Sandbox\Tools\BuildMod.ps1` |
|
||||
| `Docs/Migration/13_Protocole_Coiffure.md`, `16_H3_Mods_Paquets.md`, `10_Protocole_Operations.md`, `23_Cycle_De_Vie_Mod.md`, `20_Chaine_Animations.md`, `README.md` | note courte en tête (scripts, `ModSources/` et `AnimSources/` dans l'addon ; lancer depuis l'addon) ; aucun chemin réécrit |
|
||||
| `D:\Game\LifePlay-Sandbox\README.md` | section « Atelier de mods (lot DC3) » ; mentions périmées des lanceurs « restant dans le jeu » corrigées |
|
||||
| `Docs/Migration/etat/sandbox/DC3/DC3-DOC.md` | nouveau |
|
||||
|
||||
## Limites
|
||||
|
||||
Les chemins `.\Tools\...`, `<projet>/Tools/...` et `ModSources\...` des documents 09, 13, 16, 20, 23 ne sont pas réécrits un à un : la note de tête indique qu'ils s'entendent dans l'addon. Le piège du `Makefile.bin` n'est pas encore écrit dans `09`/`18` (report DC4).
|
||||
68
Docs/Migration/etat/sandbox/DC3/DC3-IMPL.md
Normal file
68
Docs/Migration/etat/sandbox/DC3/DC3-IMPL.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# DC3-IMPL — Atelier de mods dans l'addon
|
||||
|
||||
- **Fiches** : DC3-T1 (A-IMPL-H, scripts structurants) et DC3-T2 (A-IMPL-L, déplacements simples) — **Modèle** :
|
||||
M3 opus | opus pour les deux (DC3-T2 fait dans la foulée par la session principale, motif : les déplacements et
|
||||
les chemins des scripts sont imbriqués, un second agent aurait dû relire tout le contexte).
|
||||
- **Dépend de** : DC0-SPEC § 4. **Instantané addon** : `D:\Game\LifePlay_Backup_5.5\DC3-addon` (cibles absentes +
|
||||
`Config\LPSandbox.ini`, `.gitignore`, `README.md`). **Branche addon** : `lot/DC3-atelier` (depuis `lot/DC2-modtools`).
|
||||
- **Jeu** : côté jeu, rien n'est encore retiré (attente de la fin de la réécriture d'historique PUB1, autre session).
|
||||
|
||||
## Copie jeu → addon (même disposition relative)
|
||||
|
||||
746 fichiers comparés par SHA-256 : 0 différence (copie brute, avant adaptation). Une erreur de copie
|
||||
(`Tools/anim/methodes/*.json` arrivés dans `Tools/anim/`) corrigée aussitôt, contenu vérifié identique.
|
||||
|
||||
| Élément | Addon |
|
||||
|---|---|
|
||||
| `Tools/BuildMod.ps1`, `NewMod.ps1`, `EditMod.ps1`, `ModSource.ps1`, `HairPipeline.ps1` | `Tools/` (adaptés, voir ci-dessous) |
|
||||
| `Tools/HairImportFbx.py`, `HairMaterials.py`, `FbxMaterialMap.py`, `ImportQuaternius.py`, `DiagnosePose.py` | `Tools/` (inchangés) |
|
||||
| `Tools/anim/` (`AnimPipeline.ps1`, `DeliverAnims.ps1` adaptés ; `.py`, `profiles/`, `methodes/` inchangés) | `Tools/anim/` — les 2 résidus non suivis `Tools/anim/21_Sandbox_Addon.md`, `Journal_Modifications.md` du jeu **non copiés** |
|
||||
| `Tools/hairs/`, `Tools/mods/` | idem |
|
||||
| `ModSources/` (3 mods, `*/Content` compris), `AnimSources/` | idem ; `ModSources/*/Content/` ignoré par git (comme dans le jeu) |
|
||||
| `Tools/debug/LifePlay_{pair,interaction,measure}_sandbox.bat` | `Tools/debug/` (adaptés) |
|
||||
| `Docs/Modding/FullGuides/Creating_Asset_Mods.md`, `ShortGuides/explanation.lphair` | `Docs/Modding/` |
|
||||
|
||||
## Deux racines
|
||||
|
||||
- Nouveau `Tools/Roots.ps1` : `$LPSbRoot` (addon, déduit de l'emplacement de `Roots.ps1`) et `Resolve-LPRoots
|
||||
[-GameRoot]` → racine du jeu (paramètre, sinon `Config/LPSandbox.ini` `[Workshop] GameRoot=..\LifePlay-master 5.5`,
|
||||
sinon erreur ; contrôle de `LifePlay.uproject`). Testé : `D:\Game\LifePlay-master 5.5` / `D:\Game\LifePlay-Sandbox`.
|
||||
- `Config/LPSandbox.ini` de l'addon : section `[Workshop]` ajoutée (la copie du jeu reste lue par repli jusqu'à son retrait).
|
||||
- Chaque script reçoit `-GameRoot` (facultatif) ; `$Root` = jeu ; passent à `$LPSbRoot` : `ModSources`, `AnimSources`,
|
||||
`Tools/mods`, `Tools/anim/*.py`, `RetargetPack.py`, `ImportQuaternius.py` (`source.import` des `pack.json`),
|
||||
lanceur d'interaction, `BuildMod`/`DeliverAnims` appelés par le pilote (avec `-GameRoot $Root`) ; restent au jeu :
|
||||
projet, `Config/DefaultGame.ini`, `Releases`, `Mods`, `Content`, `Saved` (dont `travail` des `pack.json`),
|
||||
`Licenses`, `Tools/RunCommandlet.ps1`, `Tools/BuildCollections.py` (`HairPipeline` : chemin explicite vers le jeu).
|
||||
- `ModSource.ps1` : `Source = $LPSbRoot\ModSources\<Mod>`, `Mounted = <jeu>\Mods\<Mod>` (signature inchangée).
|
||||
- Lanceurs `.bat` : `GAME` = `%LP_GAMEROOT%` sinon `%~dp0..\..\..\LifePlay-master 5.5` ; `PROJ`, `ROOT` (Mesure : `Saved\sandbox_mode.txt`) en dérivent.
|
||||
- Analyse syntaxique PowerShell (`Parser::ParseFile`) : 6 scripts sans erreur.
|
||||
|
||||
## Reste (après la fin de PUB1)
|
||||
|
||||
Jeu : `git rm` des éléments copiés (scripts, `Tools/anim|hairs|mods`, `Config/LPSandbox.ini`, lanceurs, guide,
|
||||
`ModSources` suivi) ; `ModSources/`, `AnimSources/` et fichiers non suivis **déplacés** vers la sauvegarde (jamais
|
||||
supprimés) ; références (`SplitModules.ps1`, `LPModInfoSubsystem.h:8`, `BuildDebug.ps1:39`, `explanation.lpmod`,
|
||||
README, `.gitignore`) ; retrait des replis `ProjectDir()` / `ProjectConfigDir()` de `LPAnimMeasurePaths` et
|
||||
`LPSandboxConfig` (addon) — **reporté après DC3-T3** pour garder un repli pendant la validation.
|
||||
|
||||
## Report DC2 traité
|
||||
|
||||
`LPModTools/LPAnimCatalogCommandlet.cpp` : `-Livraison=` devient obligatoire (erreur d'usage, code 1) ; l'ancien défaut
|
||||
(pack quaternius) faisait réécrire `Saved/AnimPipeline/quaternius/fiches_calculees.json` par un lancement sans
|
||||
argument (DC2-VAL, anomalie 1). Le pilote passe toujours `-Livraison=` (inchangé). Recompilation nécessaire (DC3-T3).
|
||||
|
||||
## Après PUB1 : retrait côté jeu (2026-10-07)
|
||||
|
||||
- Addon commité d'abord : `1f4ec1f` (copie + adaptations, 164 fichiers).
|
||||
- Instantané jeu `D:\Game\LifePlay_Backup_5.5\DC3` (59 fichiers : les 53 suivis retirés + `SplitModules.ps1`,
|
||||
`LPModInfoSubsystem.h`, `BuildDebug.ps1`, `explanation.lpmod`, `README.md`, `.gitignore`).
|
||||
- `git rm --cached` de 57 fichiers suivis (dont `AnimSources/*/pack.json`, `Tools/DiagnosePose.py`,
|
||||
`Tools/ImportQuaternius.py`, suivis entre-temps par les commits de phase) ; **tout** le contenu disque correspondant
|
||||
(754 fichiers, dont `ModSources` 133 Mo et les 2 résidus de `Tools/anim`) **déplacé** vers
|
||||
`D:\Game\LifePlay_Backup_5.5\DC3\deplaces_du_jeu` (rien supprimé). Copie addon vérifiée identique (`pack.json`).
|
||||
- Références : `SplitModules.ps1:21,41`, `LPModInfoSubsystem.h:8`, `BuildDebug.ps1:39`, `explanation.lpmod:17`
|
||||
(dépôt de l'atelier), `README.md` (outils et guide dans un dépôt d'atelier séparé ; le jeu se construit sans lui),
|
||||
`.gitignore` (`/ModSources/*/…` → `/ModSources/`, `/AnimSources/`). Seul reste un commentaire périmé dans
|
||||
`LifePlay.Build.cs:30` (report DC4, avec `Json`).
|
||||
- Addon : replis vers le jeu retirés (`LPAnimMeasurePaths::ToolsAnimDir()` → `<addon>/Tools/anim` seul ;
|
||||
`LPSandboxConfig` → `<addon>/Config/LPSandbox.ini` seul).
|
||||
77
Docs/Migration/etat/sandbox/DC3/DC3-VAL.md
Normal file
77
Docs/Migration/etat/sandbox/DC3/DC3-VAL.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# DC3-VAL — Validation automatique du lot DC3 (DC3-T3b)
|
||||
|
||||
**Fiche DC3-T3b — Modèle prévu sonnet | utilisé sonnet** (agent A-VAL)
|
||||
|
||||
- Date : 2026-10-07. Build testé : celui de `DC3-BUILD.md` (code 0, non recompilé). Lot DC3 : atelier de mods dans l'addon `D:\Game\LifePlay-Sandbox`, nouveau `Tools\Roots.ps1`, `LPAnimCatalog` exigeant `-Livraison=`.
|
||||
- Méthode : celle de `DC2-VAL.md` (pwsh 7, aucun éditeur avant chaque lancement, comparaison SHA-256 contre `SB2-REF`). Aucune commande git hors `status`, aucun script/source/ini modifié, aucune recompilation, rien supprimé.
|
||||
- Journaux et pièces : `D:\Game\LifePlay_Backup_5.5\DC3-VAL\` (`v4.log`, `v5.log`, `v5.cmd`, `v6.log`, `v7.log`, `V5_LifePlay.log`, `V6_LifePlay.log`, `V8_LifePlay.log`, `DC3_LPAnimCatalog_20261007-194818.log`, `mods\` zip produit, `x\` extraction des deux zips, `dc3_test_mod\` mod de test déplacé).
|
||||
|
||||
## Tableau des essais
|
||||
|
||||
| Essai | Attendu | Constaté | Verdict |
|
||||
|---|---|---|---|
|
||||
| V0 préalable | `<jeu>\Mods` vide ; `Tools\BuildMod.ps1`, `Tools\anim`, `ModSources`, `AnimSources`, `Config\LPSandbox.ini` absents du jeu | `Mods` présent et vide ; les 5 `Test-Path` = False. 0 UnrealEditor. | PASS |
|
||||
| V1 racines | `D:\Game\LifePlay-master 5.5` puis `D:\Game\LifePlay-Sandbox` ; erreur explicite si racine invalide | `D:\Game\LifePlay-master 5.5` / `D:\Game\LifePlay-Sandbox`. `Resolve-LPRoots 'D:\Nexiste\Pas'` : `Exception: ...\Tools\Roots.ps1:37 ... LifePlay.uproject introuvable dans D:\Nexiste\Pas (racine du jeu)` | PASS |
|
||||
| V2 ModSource | montage/démontage, addon retrouvé, `Mods` vide | `Mod lpr_anim_freeanims monte dans Mods\lpr_anim_freeanims` ; `Get-StrayMods` : `lpr_anim_freeanims` ; `Mod lpr_anim_freeanims remis dans ModSources\lpr_anim_freeanims`. `ModSources\lpr_anim_freeanims\lpr_anim_freeanims.uplugin` = True ; `<jeu>\Mods` : 0 entrée. | PASS |
|
||||
| V3 NewMod | 3 fichiers + `Tools\mods\dc3_test_mod.json` dans l'addon ; déplacé ; git propre | `Mod cree : D:\Game\LifePlay-Sandbox\ModSources\dc3_test_mod` ; `.uplugin`, `Module\dc3_test_mod.lpmod`, `Module\CREDITS.txt`, `Tools\mods\dc3_test_mod.json` tous True (dans l'addon). Déplacés (Move-Item) vers `DC3-VAL\dc3_test_mod\`. `git status --short` de l'addon : plus aucune trace de `dc3_test_mod` (restent les 3 `M` DC3 attendus, voir V9). `<jeu>\Mods` vide. | PASS |
|
||||
| V4 cuisson de mod | code 0, même liste de fichiers | code 0, 28 s (voir détail). Zip 4 416 299 octets, 29 fichiers, SHA-256 `F4D011F9...C62DE`. `<jeu>\Mods` vide ; `ModSources\lpr_anim_freeanims\lpr_anim_freeanims.uplugin` revenu. | PASS |
|
||||
| V5 conversion à blanc des rigs | `RETARGET rigs prets jeton ...` | `Conversion des clips de /Game/Quaternius/UAL vers Bridget et Michael (methode ue4, rigs)` / ` RETARGET rigs prets jeton 5a32ccdcbe35` / `Cuisson non lancee`. 59 s. Journal éditeur : `LogLPAnimMeasure: PATHS racine addon D:/Game/LifePlay-Sandbox` ; `LogPython: Warning: RETARGET methode ue4 profil ue4_mannequin calques T` ; `RETARGET profil UAL1_Standard -> ue4_mannequin (100%)`. Plantage à la fermeture : non observé, 0 éditeur après. | PASS |
|
||||
| V6 tournée freeanims `-From 5 -To 5` | code 0, `ok`, 73 fichiers comparés | code 0, 424 s, `verdict : ok (424 s)`, `[ok] une vignette <Clip>.png par clip`, `[ok] une planche <Clip>.jpg + .json par clip`. Voir détail. | PASS |
|
||||
| V7 LPAnimCatalog sans argument | `Usage`, code 1, `fiches_calculees.json` inchangé | `LogLPAnimCatalog: Error: Usage : -Livraison=<livraison.json> [-Out=<dossier>] [-SeuilBoucle=1] [-SeuilSurPlace=5]`, `code=1 duree=12s erreurs=2`. SHA-256 de `Saved\AnimPipeline\quaternius\fiches_calculees.json` avant = après = `5DBBA632FAF3821415D0F7EF9CD74226EA694501BCC69B96565170194F9417AE` (date de modification aussi inchangée, 00:33:24). | PASS |
|
||||
| V8 lanceur pair | `Ouvert :`, Modules, PATHS | `LifePlay_pair_sandbox.bat` ; `Ouvert :` en moins de 60 s. `LogLPAnimMeasure: PATHS racine addon D:/Game/LifePlay-Sandbox` ; `LogLPSandboxMods: Display: Modules : 7 sandbox, 16 jeu ; ordre : lps_system_core, lps_system_animmeasure, lps_lib_scene, lps_lib_actors, lps_couple, lps_interaction, lps_asset_integration` ; `LogLPSandboxMods: Display: Ouvert : lps_couple`. Processus UnrealEditor arrêté (`Stop-Process`). | PASS |
|
||||
| V9 état final | `Mods` vide, 0 éditeur, `Open=lps_couple`, git status | `<jeu>\Mods` : 0 ; 0 UnrealEditor ; `Saved\sandbox_mods.ini` : `Order=lps_couple,lps_interaction,lps_asset_integration`, `Open=lps_couple`, 3 modules `true`. Git : voir ci-dessous. | PASS |
|
||||
|
||||
## Détail V4 : cuisson `lpr_anim_freeanims`
|
||||
|
||||
- Version lue dans le jeu 2026.10.1.6, journal de cuisson `Saved\Logs\portage\mods\lpr_anim_freeanims_20261007_193840.log`. Sorties : `20 paquet(s) cuit(s)`, `conteneur : 20 fichier(s), tous sous Mods/lpr_anim_freeanims/`, `Archive : ...\DC3-VAL\mods\lpr_anim_freeanims-1.0-for-LifePlay-2026.10.1.6.zip (4 416 299 octets, 29 fichiers ...)`. Pas de dossier `precedent_*` (sortie vers un dossier neuf).
|
||||
- Comparaison avec `D:\Game\LifePlay_Publication\Mods\lpr_anim_freeanims-1.0-for-LifePlay-2026.10.1.6.zip` (7-Zip, les deux zips extraits) : 41 entrées (dossiers compris) de chaque côté, 29 fichiers de chaque côté, **liste identique** (aucun fichier en plus ni en moins), taille de l'archive identique (4 416 299 octets).
|
||||
- Comparaison fichier par fichier (SHA-256) : **les 29 fichiers sont octet pour octet identiques**, y compris `lpr_anim_freeanimsLifePlay-Windows.pak` (2 718 o), `.ucas` (642 192 o), `.utoc` (2 485 o) ; seul l'horodatage du système de fichiers du `.pak` diffère (non compris dans le contenu). Aucun écart de taille.
|
||||
- `.lpmod` livré : SHA-256 identique (`0B1B3CAE...4DD9`), diff texte vide (`MODULE_PACKAGED: 2026-10-07` et `MODULE_GAME_VERSION: 2026.10.1.6` identiques dans les deux).
|
||||
- Le SHA-256 du zip précédent est `F518B0EB...EAC` contre `F4D011F9...62DE` : différence due à l'enveloppe zip (horodatages des entrées), pas au contenu.
|
||||
|
||||
## Détail V6 : comparaison complète contre `SB2-REF\freeanims\`
|
||||
|
||||
Règle de DC2-VAL : SHA-256 de chaque fichier commun ; `5_evaluation.json` et `2_identification.json` comparés en JSON hors `debut`/`fin`/`duree_s` ; `etat.json` exclu ; ligne « Généré le » de `rapport.html` exclue.
|
||||
|
||||
- **73 fichiers comparés, liste 73 / 73 identique** (`Compare-Object` vide).
|
||||
- 50 fichiers au SHA-256 identique. 23 différents :
|
||||
- `etat.json` (même taille 1 960 o), `rapport.html` (seule ligne différente : `Généré le` 07/10/2026 12:31:18 → 19:41:00), `etapes\2_identification.json` (JSON égal hors dates), `etapes\5_evaluation.json` (JSON égal hors dates ; verdict `ok` des deux côtés, 0 avertissement des deux côtés ; `duree_s` 424 vs 435).
|
||||
- **19 images** (10 planches `.jpg` + 9 vignettes `.png`), toutes dans les 20 images connues non déterministes (AirKiss, AttackInLevitation2, ChillDriving, ComboHands, EatingOnParty, FlyingUp, GoForward, Rest, RightLegKick2, Talk), écart de taille maximal 0,16 % (`AS_Talk.jpg` 144 201 → 144 435). `AS_ChillDriving.png` est cette fois identique, d'où 19 sur 20.
|
||||
- Tous les fichiers de données (`rapport.json`, CSV, `retarget_pose_*.txt`, etc.) identiques.
|
||||
- Journal d'éditeur (`V6_LifePlay.log`) : `LogLPAnimMeasure: PATHS racine addon D:/Game/LifePlay-Sandbox` ; `METHODE ue5 (calques en pose A, profil source ue5_mannequin)` ; `PROFIL squelette SK_Mannequin (161 os) -> ue5_mannequin, 100% des os du profil` ; `PROFIL squelette bridget_gp_Skeleton (191 os) -> genesis8, 100% des os du profil` ; `PROFIL squelette michael_Skeleton (183 os) -> genesis8, 100%` ; `LogLPSandbox: Warning: TOURNEE debut 10 clips, 30 etapes` ; `LogLPSandboxMods: Display: Ouvert : lps_asset_integration (declencheur, non memorise)`. Aucune erreur « profil » ou « methode » introuvable (les seules lignes trouvées par la recherche `profil` sont `Deviceprofile LinuxArm64... not found`, sans rapport, du moteur). Seul `Error` du journal : `LogLifePlay: Error: Coiffures : GI.HairstyleTable ou GI.MorphableHairs introuvable sur GameInstance_0, table laissee telle quelle` (connu, propre à la sandbox, déjà noté à DC2-VAL).
|
||||
- `Open=lps_couple` conservé après la tournée.
|
||||
|
||||
## État final (V9)
|
||||
|
||||
- `git -C D:\Game\LifePlay-Sandbox status --short` (changements DC3 non commités, attendu : retrait des replis vers le jeu) :
|
||||
```
|
||||
M Plugins/LPSandbox/Source/LPAnimMeasure/LPAnimMeasurePaths.cpp
|
||||
M Plugins/LPSandbox/Source/LPAnimMeasure/LPAnimMeasurePaths.h
|
||||
M Plugins/LPSandbox/Source/LPSandbox/LPSandboxConfig.cpp
|
||||
```
|
||||
- `git -C "D:\Game\LifePlay-master 5.5" status --short` : 69 lignes, inchangées par cette validation (retraits indexés `D ` de `ModSources/`, `AnimSources/`, `Tools/BuildMod.ps1`, `NewMod.ps1`, `EditMod.ps1`, `ModSource.ps1`, `HairPipeline.ps1`, `Tools/anim/*`, `Tools/hairs`, `Tools/mods`, `Tools/debug/*_sandbox.bat`, `Config/LPSandbox.ini`, `Docs/Modding/...` ; modifiés ` M` `.gitignore`, `README.md`, `Docs/Modding/ShortGuides/explanation.lpmod`, `Source/LifePlay/LPModInfoSubsystem.h`, `Tools/BuildDebug.ps1`, `Tools/SplitModules.ps1` ; non suivis `.cursor/`, `Docs/Migration/etat/PUB1/`, `Docs/Migration/etat/commit_plan/CP-RUN.md`, `Docs/Migration/etat/sandbox/DC3/`, `LPSandboxMeasure.cpp`, `LPSandboxMeasureTour.cpp`).
|
||||
|
||||
## Anomalies
|
||||
|
||||
1. `Tools\RunCommandlet.ps1` écrit son résumé `D:\Game\LifePlay-master 5.5\Docs\Migration\etat\DC3_LPAnimCatalog_20261007-194818.txt` (comportement du script, non listé par git status, sans doute ignoré) : à déplacer ou ignorer si indésirable.
|
||||
2. `Saved\sandbox_mode.txt` contient `Pair` en fin de validation (valeur écrite par le lanceur pair en V8 ; la valeur d'origine avant V6 n'avait pas été relevée, le pilote la restaure lui-même après V6).
|
||||
3. Compte des entrées de zip : 41 entrées dont dossiers pour 29 fichiers (la fiche ne précisait pas la différence) ; sans conséquence.
|
||||
4. Le dossier `D:\Game\LifePlay_Backup_5.5\DC3-VAL\x\` contient les deux extractions de zip (rien supprimé) ; peut être effacé par l'utilisateur.
|
||||
5. Aucune anomalie sur les chemins : l'addon est utilisé seul pour profils/méthodes (V6, V5 réussis sans repli), et le jeu n'a plus aucun des éléments d'atelier.
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS** sur V0–V9. Critères : V4 zip produit avec la même liste de fichiers (et contenus octet pour octet identiques) ; V6 73 fichiers comparés ; V7 code 1 sans écriture ; `<jeu>\Mods` vide à la fin.
|
||||
|
||||
## Reste à valider par l'utilisateur
|
||||
|
||||
Rien de visuel, sauf s'il souhaite ouvrir EditMod (`D:\Game\LifePlay-Sandbox\Tools\EditMod.ps1`, lancement de l'éditeur sur un mod, non testé ici).
|
||||
|
||||
## Modèles par fiche
|
||||
|
||||
| Fiche | Modèle prévu | Modèle utilisé |
|
||||
|---|---|---|
|
||||
| DC3-T1 | opus | opus |
|
||||
| DC3-T2 | opus | opus |
|
||||
| DC3-T3a | haiku | haiku |
|
||||
| DC3-T3b | sonnet | sonnet |
|
||||
|
|
@ -8,9 +8,9 @@ Date : 2026-10-06 — Role A-INT
|
|||
|---|---|
|
||||
| Repo addon | https://github.com/edujiro5972/LifePlay-Sandbox.git |
|
||||
| Branche | `main` |
|
||||
| **Commit pin** | `97c0d134cd1388c3735add79d77ea98817e89fbe` (`97c0d13`, DC2 : module editeur `LPModTools` et chemins propres a l'addon, branche `lot/DC2-modtools`, tag `lot-DC2-valide`, **local, non pousse** au 2026-10-07) |
|
||||
| **Commit pin** | `79f0205b7281152dde7cd088086d8c813e0b20f5` (`79f0205`, DC3 : atelier de mods dans l'addon, replis vers le jeu retires, branche `lot/DC3-atelier`, tag `lot-DC3-valide`, **local, non pousse** au 2026-10-07) |
|
||||
| Contenu du commit | S5-T2 : `LPCoupleSim` (géométrie du couple partagée jeu/addon) ; inclut S4-T3 (retrait GM Interaction legacy), S4-T2 (découpe `LPPairSandbox.cpp`) et S3-T4 (`StartupModule` → `SetUALRoot`) |
|
||||
| Pins précédents | `3e9c32f1f27b1abdaf7731dc90496c27ee061718` (DC1), `16905c68070992f5139dab907b41b461fb765957` (SB4), `3d858527fccfdddd8711c7c7448fa001442e0b8a` (SB2-O1), `82dae58380b76c45f60541fd7cb5a0a409700e10` (SB1), `f09c42e5a2bc613cf269cce76c878461ca41c8b7` (S7-T2), `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` (S5-T2), `f8102647ccdebad37e4977ccbf2c51e583823a80` (S4-T3), `4de1f0b6764ed3bc575bb687630d3f070e1be221` (S3-T4) |
|
||||
| Pins précédents | `97c0d134cd1388c3735add79d77ea98817e89fbe` (DC2), `3e9c32f1f27b1abdaf7731dc90496c27ee061718` (DC1), `16905c68070992f5139dab907b41b461fb765957` (SB4), `3d858527fccfdddd8711c7c7448fa001442e0b8a` (SB2-O1), `82dae58380b76c45f60541fd7cb5a0a409700e10` (SB1), `f09c42e5a2bc613cf269cce76c878461ca41c8b7` (S7-T2), `c1fdd29e2c1036c7d6365ba6389eff5d2f8c8c96` (S5-T2), `f8102647ccdebad37e4977ccbf2c51e583823a80` (S4-T3), `4de1f0b6764ed3bc575bb687630d3f070e1be221` (S3-T4) |
|
||||
|
||||
## Structure
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ sur la copie effectivement chargee. Pin manuel a la place (ci-dessous).
|
|||
```powershell
|
||||
cd D:\Game
|
||||
git clone https://github.com/edujiro5972/LifePlay-Sandbox.git
|
||||
git -C LifePlay-Sandbox checkout 97c0d134cd1388c3735add79d77ea98817e89fbe
|
||||
git -C LifePlay-Sandbox checkout 79f0205b7281152dde7cd088086d8c813e0b20f5
|
||||
# Le jeu doit etre clone a cote : D:\Game\LifePlay-master 5.5
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,177 +0,0 @@
|
|||
# Creating asset mods (hairstyles, and more)
|
||||
|
||||
*LifePlay Revival addition (UE 5.5). Available from the first release published with its reference version (`Releases/<version>` in the full source archive), that is the release after LifePlay 2026.10.4 (V1.4).*
|
||||
|
||||
Text mods (scenes, actions, stats, presets...) work as before: a folder in `LifePlay/Content/Modules`, see
|
||||
[Getting_Started.txt](Getting_Started.txt). This guide is about mods that bring **new 3D assets** (meshes, textures,
|
||||
materials) without rebuilding the game. The first supported use is **hairstyles**.
|
||||
|
||||
## 1. How it works
|
||||
|
||||
An asset mod has two parts, installed side by side in the game folder:
|
||||
|
||||
```
|
||||
LifePlay/Mods/<Name>/<Name>.uplugin plugin descriptor
|
||||
LifePlay/Mods/<Name>/Content/Paks/Windows/<Name>LifePlay-Windows.pak/.utoc/.ucas cooked assets
|
||||
LifePlay/Content/Modules/<Name>/<Name>.lpmod LifePlay module (text, as usual)
|
||||
LifePlay/Content/Modules/<Name>/Hairstyles/*.lphair hairstyle declarations
|
||||
LifePlay/Content/Modules/<Name>/CREDITS.txt credits and licenses
|
||||
```
|
||||
|
||||
- The engine mounts every plugin found in `LifePlay/Mods/` at startup. Its assets live under `/<Name>/...`.
|
||||
- The text module makes the mod visible in the **Mod Manager** (title screen, or Ctrl-M in game). The hairstyles are
|
||||
available only while the module is enabled.
|
||||
- Cooked assets are tied to a game version: a mod is built against the **reference version** of a release
|
||||
(`Releases/<version>`, shipped with the full source archive) and is made for that version. Another game version
|
||||
may require rebuilding the mod.
|
||||
|
||||
## 2. Requirements
|
||||
|
||||
- Windows, Unreal Engine **5.5** (Epic Games Launcher), Visual Studio 2022 is not needed for asset mods.
|
||||
- The **LifePlay Revival full source archive** of the game version you target, including `Releases/<version>/`
|
||||
(for example `Releases/2026.10.4/` for LifePlay 2026.10.4). Open `LifePlay.uproject` once to let the editor build
|
||||
its caches.
|
||||
- 7-Zip (`C:\Program Files\7-Zip\7z.exe`) to package the mod.
|
||||
- Assets you are **allowed to redistribute**: CC0 or CC BY (credit the author). Store licenses such as the Fab
|
||||
"Standard License" forbid redistributing the source files and are not suitable.
|
||||
|
||||
The scripts below are in `Tools/` and are run from a PowerShell window in the project folder. The engine path
|
||||
(`D:\Game\Launcher\Engine\UE_5.5`) is set at the top of each script: adjust it if your engine is elsewhere.
|
||||
|
||||
## 3. Create the mod
|
||||
|
||||
```powershell
|
||||
.\Tools\NewMod.ps1 -Name MyHairPack -Title "My Hair Pack" -Author "Me" -Hairs
|
||||
```
|
||||
|
||||
This creates the mod sources in `ModSources/MyHairPack/`:
|
||||
|
||||
```
|
||||
ModSources/MyHairPack/MyHairPack.uplugin content plugin
|
||||
ModSources/MyHairPack/Content/ your assets (filled by the editor or the hair pipeline)
|
||||
ModSources/MyHairPack/Module/MyHairPack.lpmod
|
||||
ModSources/MyHairPack/Module/CREDITS.txt
|
||||
Tools/mods/MyHairPack.json hair pipeline configuration (with -Hairs)
|
||||
```
|
||||
|
||||
Mod sources stay in `ModSources/`. The tools move a mod into the project's `Mods/` folder only while they work on it
|
||||
(import, cook) and move it back afterwards: a plugin left in `Mods/` would be cooked into the base game.
|
||||
|
||||
The name is the plugin name, the asset root (`/MyHairPack/`) and the module ID: letters, digits and underscores,
|
||||
starting with a letter, unique.
|
||||
|
||||
## 4. Add hairstyles
|
||||
|
||||
Hairstyles must be skinned to the Genesis 8 skeleton used by the game characters and use the game's hair material.
|
||||
The hair pipeline does this for static hairstyles distributed as FBX (for example the free Pocolov hairs on Fab):
|
||||
it fits the hair to the default head, transfers skin weights from the body (head, neck, chest) so long hair follows
|
||||
the character without cutting through the skin, and creates `DazHair` material instances.
|
||||
|
||||
1. Download the hairstyle archives (zip/rar/7z with an FBX and its textures).
|
||||
2. Add one entry per hairstyle in `Tools/mods/MyHairPack.json` (see `Tools/mods/lpr_hair_pocolov.json`, the 12 Pocolov
|
||||
hairstyles, for a complete example):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "MyHair01",
|
||||
"archive": "D:/Downloads/my-hair-01.zip",
|
||||
"title": "My Hair 01",
|
||||
"url": "https://source/of/the/asset",
|
||||
"author": "Original author",
|
||||
"license": "CC BY 4.0",
|
||||
"sex": "F",
|
||||
"mesh": "auto", "head": "auto", "cap_slots": "auto", "diffuse": "auto", "strands_opacity": "auto",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1
|
||||
}
|
||||
```
|
||||
|
||||
3. Inspect, read the report, fix the JSON if a guess is wrong, then apply:
|
||||
|
||||
```powershell
|
||||
.\Tools\HairPipeline.ps1 -Config Tools\mods\MyHairPack.json -Step Inspect
|
||||
.\Tools\HairPipeline.ps1 -Config Tools\mods\MyHairPack.json -Step Apply
|
||||
```
|
||||
|
||||
`Apply` writes the assets to `ModSources/MyHairPack/Content/Hairs/`, one `.lphair` per hairstyle in
|
||||
`Module/Hairstyles/` and one line per hairstyle in `Module/CREDITS.txt`. To redo a hairstyle: `-Step Inspect` then
|
||||
`-Step Apply -Rebuild -Only MyHair01` (the previous version is moved to the backup folder, never deleted).
|
||||
|
||||
Fitting options per hairstyle (`margin`, `scale`, `offset_z`, `offset_y`, `skin`, `gap`) and the detailed protocol
|
||||
are described (in French) in `Docs/Migration/13_Protocole_Coiffure.md`.
|
||||
|
||||
A `.lphair` file can also be written by hand for a hairstyle made another way (see
|
||||
[explanation.lphair](../ShortGuides/explanation.lphair)):
|
||||
|
||||
```
|
||||
HAIRSTYLE: /MyHairPack/Hairs/MyHair01.MyHair01
|
||||
SEX: F
|
||||
DAZ: true
|
||||
```
|
||||
|
||||
## 4b. Mod metadata
|
||||
|
||||
The first three lines of `<Name>.lpmod` are read **by position** by the game and must stay first, in this order:
|
||||
`MODULE_UNIQUEID`, `MODULE_NAME`, `MODULE_AUTHOR`. The other lines are free; the Mod Manager shows the whole file in its
|
||||
detail panel, so every line below is visible to players.
|
||||
|
||||
| Key | Written by | Meaning |
|
||||
|---|---|---|
|
||||
| `MODULE_VERSION` | you (`NewMod.ps1 -ModVersion`, default 1.0) | version of the mod; also in the archive name and the plugin `VersionName` |
|
||||
| `MODULE_CREATED` | `NewMod.ps1` | creation date (YYYY-MM-DD) |
|
||||
| `MODULE_LICENSE` | you (`-License`) | license of the mod as a whole (each asset's license stays in `CREDITS.txt`) |
|
||||
| `MODULE_AUTHOR_LINK` | you (`-AuthorLink`) | author page; also the plugin `CreatedByURL` |
|
||||
| `MODULE_PRICE` | you (`-Price`, default Free) | information only, the game sells nothing. A non-free mod is refused by `BuildMod.ps1` if `CREDITS.txt` mentions a non-commercial or store license (`NC`, `Standard License`, `Personal`) |
|
||||
| `MODULE_DEFAULT_ENABLED` | you | `true`: enabled the first time the game finds it; otherwise a new mod starts disabled |
|
||||
| `MODULE_PACKAGED` | `BuildMod.ps1` (shipped copy only) | packaging date |
|
||||
| `MODULE_GAME_VERSION` | `BuildMod.ps1` (shipped copy only) | game version the assets were cooked for. If an **enabled** mod declares another version than the game, the game shows a "Mod version warning" window (once per session) |
|
||||
|
||||
## 5. Edit the mod in the editor
|
||||
|
||||
```powershell
|
||||
.\Tools\EditMod.ps1 -Name MyHairPack
|
||||
```
|
||||
|
||||
Opens the editor with the mod mounted: its assets appear in the Content Browser under **MyHairPack Content**
|
||||
(`/MyHairPack/`). Plugin content is hidden by default: in the Content Browser
|
||||
settings (gear icon), tick **Show Plugin Content**. The search box matches asset names, not folder names (search
|
||||
`MyHair`, not `MyHairPack`). Close the editor to move the mod back to `ModSources/`. Keep every asset of the mod inside
|
||||
`/MyHairPack/`; referencing game assets (skeletons, the `DazHair` material...) is fine, modifying them is not
|
||||
(changes to game assets are not shipped by the mod).
|
||||
|
||||
## 6. Build and test
|
||||
|
||||
```powershell
|
||||
.\Tools\BuildMod.ps1 -Name MyHairPack -Install <test game folder>\Windows
|
||||
```
|
||||
|
||||
`BuildMod.ps1`:
|
||||
|
||||
1. checks the plugin, the module and the reference version (`Releases/<version>`, version read from
|
||||
`Config/DefaultGame.ini`, or `-Version`);
|
||||
2. cooks only the mod against the reference version (log in `Saved/Logs/portage/mods/`);
|
||||
3. checks that the cooked container holds nothing outside `Mods/MyHairPack/` (no game asset is shipped);
|
||||
4. builds `MyHairPack-for-LifePlay-<version>.zip` (with an install README) in `D:\Game\LifePlay_Publication\Mods`
|
||||
(`-Out` to change it);
|
||||
5. with `-Install`, copies the mod into a test game (the folder that contains `LifePlay.exe`).
|
||||
|
||||
Then start the game, open **Mod Manager** on the title screen, enable the mod, close the manager and start or load a
|
||||
game: the new hairstyles are at the end of the hairstyle list (for the sex declared in `SEX`).
|
||||
|
||||
## 7. Distribute
|
||||
|
||||
Share the zip. Players extract it into the game folder (the one that contains `LifePlay.exe`), merging the `LifePlay`
|
||||
folder, then enable the mod in the Mod Manager. To uninstall, they delete `LifePlay/Mods/<Name>` and
|
||||
`LifePlay/Content/Modules/<Name>`. Always say which LifePlay version the mod was built for, and keep the credits of
|
||||
every asset in `CREDITS.txt`.
|
||||
|
||||
## 8. Troubleshooting
|
||||
|
||||
| Symptom | Cause, fix |
|
||||
|---|---|
|
||||
| The mod is not listed in the Mod Manager | `LifePlay/Content/Modules/<Name>/<Name>.lpmod` missing, or `MODULE_UNIQUEID` different from the folder name |
|
||||
| The mod is listed but the hairstyle does not appear | module not enabled; `HAIRSTYLE` path wrong (`/<Name>/Hairs/<Asset>.<Asset>`); `LifePlay/Mods/<Name>/Content/Paks/Windows` missing; wrong `SEX` |
|
||||
| `Mods\<Name> existe deja` when running a tool | a previous run was interrupted: move `Mods/<Name>` back to `ModSources/<Name>` by hand |
|
||||
| `Version de reference absente` | the source archive lacks `Releases/<version>`: use the source archive of the targeted game version |
|
||||
| `Autres plugins dans Mods\` | another mod is still mounted: move it back to `ModSources/` |
|
||||
| The hairstyle cuts through the shoulders or floats | adjust `gap`, `margin`, `offset_z`, `offset_y` and rebuild the hairstyle |
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
LifePlay Revival addition. Lphair files declare hairstyles, beards and body hair. Put them anywhere inside your mod folder (for example Content\Modules\<your mod>\Hairstyles\). They are read at startup; the hairstyles of a mod are available only while the mod is enabled in the Mod Manager.
|
||||
|
||||
HAIRSTYLE: /MyHairPack/Hairs/MyHair01.MyHair01 // full path of a skeletal mesh skinned to the Genesis 8 skeleton (an asset of the game, or of an asset mod: see Docs/Modding/FullGuides/Creating_Asset_Mods.md), or NONE for "no hair"
|
||||
SEX: F // F or M (default F): which characters can use it
|
||||
DAZ: true // true for hairstyles made for the Daz Genesis 8 characters (all current ones)
|
||||
FIT_MORPH: MorphName 0.5 // optional: morph target applied to the hair, with its value, to fit the head
|
||||
|
||||
The // comments at the end of the lines above are explanations only: in a real file, a comment must be on its own line.
|
||||
|
||||
Several hairstyles can be declared in one file: each HAIRSTYLE line starts a new hairstyle, the SEX, DAZ and FIT_MORPH lines that follow apply to it. Lines starting with // are comments.
|
||||
|
||||
Beards and body hair are textures painted on the skin, listed by name (the texture must exist in the game folder used by the game for that list):
|
||||
|
||||
FACIAL_HAIR: Facial_Hair__1_ // beard (male)
|
||||
BODY_HAIR_M: JPBH78-TorsoNG-01 // male body hair
|
||||
BODY_HAIR_F: Pubes01 // female body hair
|
||||
|
||||
The base game list is Content\Modules\vin_Base\Hairstyles\vin_Base.lphair (MODE: BASE): do not copy that line into your own files.
|
||||
|
|
@ -14,5 +14,5 @@ LifePlay Revival addition:
|
|||
MODULE_DEFAULT_ENABLED: true // optional: the mod is enabled the first time the game finds it (otherwise a new mod starts disabled until the player ticks it in the Mod Manager). Used by lpr_hair_pocolov.
|
||||
The first three lines (MODULE_UNIQUEID, MODULE_NAME, MODULE_AUTHOR) are read by position: keep them first, in this order.
|
||||
Other optional keys, shown as-is in the Mod Manager: MODULE_VERSION, MODULE_CREATED, MODULE_LICENSE, MODULE_AUTHOR_LINK, MODULE_PRICE,
|
||||
MODULE_PACKAGED and MODULE_GAME_VERSION (written by Tools/BuildMod.ps1; the game warns when an enabled mod was built for another
|
||||
MODULE_PACKAGED and MODULE_GAME_VERSION (written by BuildMod.ps1 of the mod workshop, a separate repository; the game warns when an enabled mod was built for another
|
||||
game version). See Docs/Modding/FullGuides/Creating_Asset_Mods.md, section 4b.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,761 +0,0 @@
|
|||
{
|
||||
"pack": "freeanims",
|
||||
"mod": "lpr_anim_freeanims",
|
||||
"version": "1.0",
|
||||
"genere": "2026-10-07T02:38:49",
|
||||
"nombre_animations": 10,
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"animations": [
|
||||
{
|
||||
"cle": "fap_as_airkiss",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_airkiss)",
|
||||
"action": "ANIMATION: fap_as_airkiss"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Baiser envoyé",
|
||||
"en": "Blow a kiss"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_airkiss_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_airkiss_M"
|
||||
},
|
||||
"duree_s": 10.3,
|
||||
"images": 310,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_airkiss.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 2.3
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 1262,
|
||||
"masculin": 1253,
|
||||
"total": 2515
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 36.57
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 8,
|
||||
"cm_s": 1,
|
||||
"sens": "cote"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 100
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "repos",
|
||||
"droite": "mouvement"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "interaction",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Geste ponctuel (probablement joué une fois) d'envoi de baiser, joué sur place ; utile pour une scène de flirt ou d'adieu.",
|
||||
"en": "One-shot gesture (probably played once) of blowing a kiss, played in place; useful for a flirting or farewell scene."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "FFBE444E7D7717C9DFD6FD4E3BD7DD706F8BB600E8AB05239CBEF9078652693A"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_attackinlevitation2",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_attackinlevitation2)",
|
||||
"action": "ANIMATION: fap_as_attackinlevitation2"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Attaque en lévitation",
|
||||
"en": "Levitation attack"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_attackinlevitation2_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_attackinlevitation2_M"
|
||||
},
|
||||
"duree_s": 7.07,
|
||||
"images": 213,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_attackinlevitation2.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 3.4
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 944,
|
||||
"masculin": 934,
|
||||
"total": 1878
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 6.74
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 6,
|
||||
"cm_s": 1,
|
||||
"sens": "cote"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 0,
|
||||
"droite": 0
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "mouvement",
|
||||
"droite": "levee"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "magie",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Attaque lancée en lévitation, probablement jouée une fois, sur place ; pour un sort ou une attaque surnaturelle.",
|
||||
"en": "Attack performed while levitating, probably played once, in place; for a spell or supernatural attack."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.01
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "D909B0E97115A819BCCA61CC8B1D4EE819DB91CF6D471EFCF8E87C6987E8D0F1"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_chilldriving",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_chilldriving)",
|
||||
"action": "ANIMATION: fap_as_chilldriving"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Conduite détendue",
|
||||
"en": "Relaxed driving"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_chilldriving_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_chilldriving_M"
|
||||
},
|
||||
"duree_s": 6.7,
|
||||
"images": 202,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_chilldriving.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 3.6
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 908,
|
||||
"masculin": 898,
|
||||
"total": 1806
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 3.88
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 1,
|
||||
"cm_s": 0,
|
||||
"sens": "sur place"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "assis",
|
||||
"posture_origine": "calculee",
|
||||
"hauteur_assise_cm": 40
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 100
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "repos",
|
||||
"droite": "levee"
|
||||
}
|
||||
},
|
||||
"accessoire": "volant",
|
||||
"categorie": "conduite",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Pose de conduite détendue, probablement en boucle, jouée sur place ; à utiliser avec un véhicule ou un volant.",
|
||||
"en": "Relaxed driving pose, probably looping, played in place; to use with a vehicle or steering wheel."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "5A74F7DA765AB938991A14FFF3E77DB6A18C12D025D810D598DBDD936EB67E24"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_combohands",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_combohands)",
|
||||
"action": "ANIMATION: fap_as_combohands"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Enchaînement de poings",
|
||||
"en": "Punch combo"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_combohands_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_combohands_M"
|
||||
},
|
||||
"duree_s": 2.2,
|
||||
"images": 67,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_combohands.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 10.9
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 517,
|
||||
"masculin": 507,
|
||||
"total": 1025
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 56.24
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 2,
|
||||
"cm_s": 1,
|
||||
"sens": "sur place"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 93
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "mouvement",
|
||||
"droite": "mouvement"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "combat",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Enchaînement de coups de poing, probablement joué une fois, sur place ; pour une bagarre ou un entraînement.",
|
||||
"en": "Sequence of punches, probably played once, in place; for a fight or training scene."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "012073AFF1B80B99F9C15CBAA1BA428CCCB1B16C3C21746E3182956A4BE967A4"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_eatingonparty",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_eatingonparty)",
|
||||
"action": "ANIMATION: fap_as_eatingonparty"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Manger en soirée",
|
||||
"en": "Eating at party"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_eatingonparty_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_eatingonparty_M"
|
||||
},
|
||||
"duree_s": 23.7,
|
||||
"images": 712,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_eatingonparty.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 1.0
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 2581,
|
||||
"masculin": 2571,
|
||||
"total": 5152
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 19.28
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 10,
|
||||
"cm_s": 0,
|
||||
"sens": "cote"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 100
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "repos",
|
||||
"droite": "mouvement"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "interaction",
|
||||
"style": "détendu",
|
||||
"usage": {
|
||||
"fr": "Personnage mangeant debout lors d'une soirée, probablement en boucle, joué sur place ; pour un buffet ou une fête.",
|
||||
"en": "Character eating while standing at a party, probably looping, played in place; for a buffet or party scene."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "8D06644A66882A9CD6A5D9CF112353409FADAF453A397DBBAC99E177C0753FBA"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_flyingup",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_flyingup)",
|
||||
"action": "ANIMATION: fap_as_flyingup"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Envol",
|
||||
"en": "Take off"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_flyingup_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_flyingup_M"
|
||||
},
|
||||
"duree_s": 2.7,
|
||||
"images": 82,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_flyingup.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 8.9
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 514,
|
||||
"masculin": 504,
|
||||
"total": 1018
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 68.13
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 2,
|
||||
"cm_s": 1,
|
||||
"sens": "sur place"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 43,
|
||||
"droite": 43
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "levee",
|
||||
"droite": "levee"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "magie",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Décollage vers le haut, probablement joué une fois, sur place ; pour un envol ou une lévitation.",
|
||||
"en": "Upward take-off, probably played once, in place; for flying off or levitating."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "327291EFF0B154B719CE85A07AD11B4DED45F4DB16787A5DE1BA3960092BA8C6"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_goforward",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_goforward)",
|
||||
"action": "ANIMATION: fap_as_goforward"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Marche avant",
|
||||
"en": "Walk forward"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_goforward_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_goforward_M"
|
||||
},
|
||||
"duree_s": 3.37,
|
||||
"images": 102,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_goforward.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 7.1
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 579,
|
||||
"masculin": 569,
|
||||
"total": 1149
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 4.29
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 0,
|
||||
"cm_s": 0,
|
||||
"sens": "sur place"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 100
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "mouvement",
|
||||
"droite": "levee"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "locomotion",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Cycle de marche vers l'avant, probablement en boucle, joué sur place ; à combiner avec un déplacement du personnage.",
|
||||
"en": "Forward walk cycle, probably looping, played in place; combine with a character movement."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "44363245CBE7C209178B3292D49B97B43F2DF0DBCC4FBC5B58EA230B8C48E469"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_rest",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_rest)",
|
||||
"action": "ANIMATION: fap_as_rest"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Repos",
|
||||
"en": "Rest"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_rest_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_rest_M"
|
||||
},
|
||||
"duree_s": 11.47,
|
||||
"images": 345,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_rest.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 2.1
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 1377,
|
||||
"masculin": 1357,
|
||||
"total": 2734
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 4.26
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 1,
|
||||
"cm_s": 0,
|
||||
"sens": "sur place"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "allonge",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 100
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "repos",
|
||||
"droite": "repos"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "attente",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Posture de repos debout, probablement en boucle, jouée sur place ; pose d'attente par défaut.",
|
||||
"en": "Standing rest posture, probably looping, played in place; default waiting pose."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "513BEEE6DC9431E52D66DC07D26C88621C6A6DFF465DEF42F81E711E2A5AE3DF"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_rightlegkick2",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_rightlegkick2)",
|
||||
"action": "ANIMATION: fap_as_rightlegkick2"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Coup de pied droit",
|
||||
"en": "Right leg kick"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_rightlegkick2_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_rightlegkick2_M"
|
||||
},
|
||||
"duree_s": 1.1,
|
||||
"images": 34,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_rightlegkick2.jpg",
|
||||
"images": 13,
|
||||
"taille": 256,
|
||||
"ips": 11.8
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 387,
|
||||
"masculin": 377,
|
||||
"total": 764
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 55.01
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 11,
|
||||
"cm_s": 10,
|
||||
"sens": "avant"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 0,
|
||||
"droite": 38
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "mouvement",
|
||||
"droite": "mouvement"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "combat",
|
||||
"style": "agressif",
|
||||
"usage": {
|
||||
"fr": "Coup de pied de la jambe droite, probablement joué une fois, sur place ; pour un combat ou un entraînement.",
|
||||
"en": "Right leg kick, probably played once, in place; for a fight or training scene."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.0
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "FA818881DD479BA94192C8C1BA8A8220E0E2D94432D1E04E12E59F998F0129F0"
|
||||
},
|
||||
{
|
||||
"cle": "fap_as_talk",
|
||||
"appel": {
|
||||
"script": "Actor.animate(fap_as_talk)",
|
||||
"action": "ANIMATION: fap_as_talk"
|
||||
},
|
||||
"nom": {
|
||||
"fr": "Parler",
|
||||
"en": "Talk"
|
||||
},
|
||||
"assets": {
|
||||
"feminin": "/lpr_anim_freeanims/Anims/Female/fap_as_talk_F",
|
||||
"masculin": "/lpr_anim_freeanims/Anims/Male/fap_as_talk_M"
|
||||
},
|
||||
"duree_s": 14.07,
|
||||
"images": 423,
|
||||
"planche": {
|
||||
"fichier": "planches/fap_as_talk.jpg",
|
||||
"images": 24,
|
||||
"taille": 256,
|
||||
"ips": 1.7
|
||||
},
|
||||
"cadence": 30,
|
||||
"taille_ko": {
|
||||
"feminin": 1633,
|
||||
"masculin": 1623,
|
||||
"total": 3256
|
||||
},
|
||||
"taille_cuite": false,
|
||||
"boucle": {
|
||||
"oui": false,
|
||||
"ecart_pct": 28.35
|
||||
},
|
||||
"deplacement": {
|
||||
"cm": 2,
|
||||
"cm_s": 0,
|
||||
"sens": "sur place"
|
||||
},
|
||||
"posture": {
|
||||
"principale": "debout",
|
||||
"posture_origine": "calculee"
|
||||
},
|
||||
"contacts": {
|
||||
"pieds_au_sol_pct": {
|
||||
"gauche": 100,
|
||||
"droite": 100
|
||||
},
|
||||
"mains": {
|
||||
"gauche": "mouvement",
|
||||
"droite": "mouvement"
|
||||
}
|
||||
},
|
||||
"accessoire": "",
|
||||
"categorie": "conversation",
|
||||
"style": "neutre",
|
||||
"usage": {
|
||||
"fr": "Personnage qui parle debout, probablement en boucle, joué sur place ; pour un dialogue ou une conversation.",
|
||||
"en": "Character talking while standing, probably looping, played in place; for a dialogue or conversation."
|
||||
},
|
||||
"corps": [
|
||||
"feminin",
|
||||
"masculin"
|
||||
],
|
||||
"qualite": {
|
||||
"verdict": "accepté",
|
||||
"motifs": [],
|
||||
"pire_brut_pct": 0.0,
|
||||
"pire_torsion_deg": 0.01
|
||||
},
|
||||
"source": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"licence": "CC BY 4.0",
|
||||
"credit": "Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay",
|
||||
"version": "1.0",
|
||||
"empreinte": "5C9CB1D57C27D542A2DC131F4C53EDA4171BD96F8FD5B9CD831566AA86563A75"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
Credits and licenses of the assets in Free Animations Pack (SoerGame, Fab ; mannequin UE5) (one line per asset: title, author, license, source URL, changes).
|
||||
- Animations : Free Animations Pack par SoerGame (Fab), licence Creative Commons Attribution (CC BY 4.0), converties pour LifePlay, https://www.fab.com/listings/9e93d929-36e8-45f9-b925-325db1b9385d . Changes: retargeted to the Genesis 8 skeletons (Bridget, Michael), renamed fap_<clip>_F / fap_<clip>_M.
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
MODULE_UNIQUEID: lpr_anim_freeanims
|
||||
MODULE_NAME: Free Animations Pack (SoerGame, Fab ; mannequin UE5)
|
||||
MODULE_AUTHOR: LifePlay Revival (animations by SoerGame)
|
||||
MODULE_LINK: https://github.com/edujiro5972/LifePlay-Revival
|
||||
MODULE_DESCRIPTION: Free animation clips from Free Animations Pack (SoerGame, Fab ; mannequin UE5) (CC BY 4.0), retargeted for LifePlay
|
||||
MODULE_REQUIREMENTS: vin_Base
|
||||
MODULE_VERSION: 1.0
|
||||
MODULE_CREATED: 2026-10-07
|
||||
MODULE_LICENSE: CC BY 4.0
|
||||
MODULE_AUTHOR_LINK: https://www.fab.com/sellers/SoerGame
|
||||
MODULE_PRICE: Free
|
||||
MODULE_DEFAULT_ENABLED: true
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0",
|
||||
"FriendlyName": "Free Animations Pack (SoerGame, Fab ; mannequin UE5)",
|
||||
"Description": "Free animation clips from Free Animations Pack (SoerGame, Fab ; mannequin UE5) (CC BY 4.0), retargeted for LifePlay",
|
||||
"Category": "LifePlay Mods",
|
||||
"CreatedBy": "LifePlay Revival (animations by SoerGame)",
|
||||
"CanContainContent": true,
|
||||
"IsBetaVersion": false,
|
||||
"Installed": false,
|
||||
"CreatedByURL": "https://www.fab.com/sellers/SoerGame"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +0,0 @@
|
|||
Credits and licenses of the assets in Quaternius Animations (one line per asset: title, author, license, source URL, changes).
|
||||
- Universal Animation Library by Quaternius, CC0 1.0, https://quaternius.com . Changes: retargeted to the Genesis 8 skeletons (Bridget, Michael), renamed qua_<clip>_F / qua_<clip>_M.
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
MODULE_UNIQUEID: lpr_anim_quaternius
|
||||
MODULE_NAME: Quaternius Animations
|
||||
MODULE_AUTHOR: LifePlay Revival (animations by Quaternius)
|
||||
MODULE_LINK: https://github.com/edujiro5972/LifePlay-Revival
|
||||
MODULE_DESCRIPTION: Free animation clips from the Quaternius Universal Animation Library (CC0), retargeted for LifePlay
|
||||
MODULE_REQUIREMENTS: vin_Base
|
||||
MODULE_VERSION: 1.0
|
||||
MODULE_CREATED: 2026-10-06
|
||||
MODULE_LICENSE: CC0
|
||||
MODULE_AUTHOR_LINK: https://quaternius.com
|
||||
MODULE_PRICE: Free
|
||||
MODULE_DEFAULT_ENABLED: true
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0",
|
||||
"FriendlyName": "Quaternius Animations",
|
||||
"Description": "Free animation clips from the Quaternius Universal Animation Library (CC0), retargeted for LifePlay",
|
||||
"Category": "LifePlay Mods",
|
||||
"CreatedBy": "LifePlay Revival (animations by Quaternius)",
|
||||
"CanContainContent": true,
|
||||
"IsBetaVersion": false,
|
||||
"Installed": false,
|
||||
"CreatedByURL": "https://quaternius.com"
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
Credits and licenses of the assets in Pocolov Hairstyles (one line per asset: title, author, license, source URL, changes).
|
||||
- Pocolov Hair 03 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/2257098d-b77c-4eef-9132-85f1454f0ae3 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/lpr_hair_pocolov/Hairs/Pocolov03*).
|
||||
- Pocolov Hair 05 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/4025749f-d597-4332-91aa-16e355b94974 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/lpr_hair_pocolov/Hairs/Pocolov05*).
|
||||
- Pocolov Hair 06 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/14c3cd52-02f9-4bfe-bb40-b5c3acb1f428 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/lpr_hair_pocolov/Hairs/Pocolov06*).
|
||||
- Pocolov Hair 11 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/c31c219e-bc62-4957-91c5-720440375f79 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/lpr_hair_pocolov/Hairs/Pocolov11*).
|
||||
- Pocolov Hair 15 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/ed8e016a-0bf0-4a6c-94b0-2760083b879d . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/lpr_hair_pocolov/Hairs/Pocolov15*).
|
||||
- Pocolov Hair 16 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/802dc956-6211-4b83-8ba1-37beb3cb338f . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/lpr_hair_pocolov/Hairs/Pocolov16*).
|
||||
- Pocolov Hair 17 by Pocolov (Pocolov Studio 2019, artist Thang), CC BY 4.0, https://www.fab.com/listings/7f6f687d-bb13-40c6-8b69-64044ededc1d . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/lpr_hair_pocolov/Hairs/Pocolov17*).
|
||||
- Pocolov Hair 07 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/4d1ba957-b420-4bdc-8984-b528a73b1d58 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/lpr_hair_pocolov/Hairs/Pocolov07*).
|
||||
- Pocolov Hair 09 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/80383c46-f491-4db2-a63d-48d279f35782 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/lpr_hair_pocolov/Hairs/Pocolov09*).
|
||||
- Pocolov Hair 10 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/40a91685-76dc-4384-91f1-0040f44e8958 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/lpr_hair_pocolov/Hairs/Pocolov10*).
|
||||
- Pocolov Hair 13 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/a3933681-f5e3-4d93-99f0-c5fff444d405 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/lpr_hair_pocolov/Hairs/Pocolov13*).
|
||||
- Pocolov Hair 14 by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, https://www.fab.com/listings/4911f6b9-d548-45d7-86be-a6971da97c54 . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials (/lpr_hair_pocolov/Hairs/Pocolov14*).
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Pocolov Hair 03 - Pocolov (Pocolov Studio, artist Thang) - licence CC BY 4.0
|
||||
// Source : https://www.fab.com/listings/2257098d-b77c-4eef-9132-85f1454f0ae3
|
||||
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
|
||||
HAIRSTYLE: /lpr_hair_pocolov/Hairs/Pocolov03.Pocolov03
|
||||
SEX: F
|
||||
DAZ: true
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Pocolov Hair 05 - Pocolov (Pocolov Studio, artist Thang) - licence CC BY 4.0
|
||||
// Source : https://www.fab.com/listings/4025749f-d597-4332-91aa-16e355b94974
|
||||
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
|
||||
HAIRSTYLE: /lpr_hair_pocolov/Hairs/Pocolov05.Pocolov05
|
||||
SEX: F
|
||||
DAZ: true
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Pocolov Hair 06 - Pocolov (Pocolov Studio, artist Thang) - licence CC BY 4.0
|
||||
// Source : https://www.fab.com/listings/14c3cd52-02f9-4bfe-bb40-b5c3acb1f428
|
||||
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
|
||||
HAIRSTYLE: /lpr_hair_pocolov/Hairs/Pocolov06.Pocolov06
|
||||
SEX: F
|
||||
DAZ: true
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Pocolov Hair 07 - Pocolov (Pocolov Studio, artist Thang) - licence CC BY 4.0
|
||||
// Source : https://www.fab.com/listings/4d1ba957-b420-4bdc-8984-b528a73b1d58
|
||||
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
|
||||
HAIRSTYLE: /lpr_hair_pocolov/Hairs/Pocolov07.Pocolov07
|
||||
SEX: F
|
||||
DAZ: true
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Pocolov Hair 09 - Pocolov (Pocolov Studio, artist Thang) - licence CC BY 4.0
|
||||
// Source : https://www.fab.com/listings/80383c46-f491-4db2-a63d-48d279f35782
|
||||
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
|
||||
HAIRSTYLE: /lpr_hair_pocolov/Hairs/Pocolov09.Pocolov09
|
||||
SEX: F
|
||||
DAZ: true
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Pocolov Hair 10 - Pocolov (Pocolov Studio, artist Thang) - licence CC BY 4.0
|
||||
// Source : https://www.fab.com/listings/40a91685-76dc-4384-91f1-0040f44e8958
|
||||
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
|
||||
HAIRSTYLE: /lpr_hair_pocolov/Hairs/Pocolov10.Pocolov10
|
||||
SEX: F
|
||||
DAZ: true
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Pocolov Hair 11 - Pocolov (Pocolov Studio, artist Thang) - licence CC BY 4.0
|
||||
// Source : https://www.fab.com/listings/c31c219e-bc62-4957-91c5-720440375f79
|
||||
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
|
||||
HAIRSTYLE: /lpr_hair_pocolov/Hairs/Pocolov11.Pocolov11
|
||||
SEX: F
|
||||
DAZ: true
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Pocolov Hair 13 - Pocolov (Pocolov Studio, artist Thang) - licence CC BY 4.0
|
||||
// Source : https://www.fab.com/listings/a3933681-f5e3-4d93-99f0-c5fff444d405
|
||||
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
|
||||
HAIRSTYLE: /lpr_hair_pocolov/Hairs/Pocolov13.Pocolov13
|
||||
SEX: F
|
||||
DAZ: true
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Pocolov Hair 14 - Pocolov (Pocolov Studio, artist Thang) - licence CC BY 4.0
|
||||
// Source : https://www.fab.com/listings/4911f6b9-d548-45d7-86be-a6971da97c54
|
||||
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
|
||||
HAIRSTYLE: /lpr_hair_pocolov/Hairs/Pocolov14.Pocolov14
|
||||
SEX: F
|
||||
DAZ: true
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Pocolov Hair 15 - Pocolov (Pocolov Studio, artist Thang) - licence CC BY 4.0
|
||||
// Source : https://www.fab.com/listings/ed8e016a-0bf0-4a6c-94b0-2760083b879d
|
||||
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
|
||||
HAIRSTYLE: /lpr_hair_pocolov/Hairs/Pocolov15.Pocolov15
|
||||
SEX: F
|
||||
DAZ: true
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Pocolov Hair 16 - Pocolov (Pocolov Studio, artist Thang) - licence CC BY 4.0
|
||||
// Source : https://www.fab.com/listings/802dc956-6211-4b83-8ba1-37beb3cb338f
|
||||
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
|
||||
HAIRSTYLE: /lpr_hair_pocolov/Hairs/Pocolov16.Pocolov16
|
||||
SEX: F
|
||||
DAZ: true
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Pocolov Hair 17 - Pocolov (Pocolov Studio 2019, artist Thang) - licence CC BY 4.0
|
||||
// Source : https://www.fab.com/listings/7f6f687d-bb13-40c6-8b69-64044ededc1d
|
||||
// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.
|
||||
HAIRSTYLE: /lpr_hair_pocolov/Hairs/Pocolov17.Pocolov17
|
||||
SEX: F
|
||||
DAZ: true
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
MODULE_UNIQUEID: lpr_hair_pocolov
|
||||
MODULE_NAME: Pocolov Hairstyles
|
||||
MODULE_AUTHOR: LifePlay Revival (hairstyles by Pocolov)
|
||||
MODULE_LINK: https://github.com/edujiro5972/LifePlay-Revival
|
||||
MODULE_DESCRIPTION: 12 free hairstyles by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, converted for LifePlay
|
||||
MODULE_REQUIREMENTS: vin_Base
|
||||
MODULE_VERSION: 1.0
|
||||
MODULE_CREATED: 2026-10-03
|
||||
MODULE_LICENSE: CC BY 4.0
|
||||
MODULE_AUTHOR_LINK: https://www.fab.com/sellers/Pocolov
|
||||
MODULE_PRICE: Free
|
||||
MODULE_DEFAULT_ENABLED: true
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 1,
|
||||
"VersionName": "1.0",
|
||||
"FriendlyName": "Pocolov Hairstyles",
|
||||
"Description": "12 free hairstyles by Pocolov (Pocolov Studio, artist Thang), CC BY 4.0, converted for LifePlay",
|
||||
"Category": "LifePlay Mods",
|
||||
"CreatedBy": "LifePlay Revival (hairstyles by Pocolov)",
|
||||
"CanContainContent": true,
|
||||
"IsBetaVersion": false,
|
||||
"Installed": false,
|
||||
"CreatedByURL": "https://www.fab.com/sellers/Pocolov"
|
||||
}
|
||||
|
|
@ -44,15 +44,15 @@ Mods can now bring their own 3D assets, and the Mod Manager is available from th
|
|||
- **Asset mods.** A mod can ship cooked 3D assets (meshes, textures, materials) in `LifePlay/Mods/<mod>/` next to its
|
||||
text module in `LifePlay/Content/Modules/<mod>/`, without rebuilding the game. Mods are cooked against the
|
||||
**reference version** of the release (`Releases/2026.10.1.5`, included in the full source archive). Tools:
|
||||
`Tools/NewMod.ps1`, `Tools/EditMod.ps1`, `Tools/HairPipeline.ps1`, `Tools/BuildMod.ps1`. Guide (English):
|
||||
[`Docs/Modding/FullGuides/Creating_Asset_Mods.md`](Docs/Modding/FullGuides/Creating_Asset_Mods.md).
|
||||
`NewMod.ps1`, `EditMod.ps1`, `HairPipeline.ps1`, `BuildMod.ps1` and the modding guide, which now live in a separate
|
||||
mod workshop repository (not part of this repository; the game builds and runs without it).
|
||||
- **Mod Manager on the title screen.** Choose your mods before starting or loading a game; the choice is saved. A mod
|
||||
that the game does not know yet starts disabled (as in the original game), unless its `.lpmod` declares
|
||||
`MODULE_DEFAULT_ENABLED: true`.
|
||||
- **Mod metadata and version warning.** New optional `.lpmod` keys (version, dates, license, author link, price, game
|
||||
version), shown in the Mod Manager; the game warns when an enabled mod was built for another game version.
|
||||
- **Pocolov hairstyles as a mod.** The Pocolov hairstyles left the base game: they are now the asset mod
|
||||
`lpr_hair_pocolov` (12 hairstyles, CC BY 4.0; sources in `ModSources/lpr_hair_pocolov`, credits in its
|
||||
`lpr_hair_pocolov` (12 hairstyles, CC BY 4.0; sources in the mod workshop repository, credits in its
|
||||
`CREDITS.txt`).
|
||||
- **Game build.** The packaged game only contains the base modules `vin_Base` and `vin_VanillaRealistic`. The source
|
||||
archive and this repository keep all modules.
|
||||
|
|
@ -70,7 +70,7 @@ Moddable hairstyles, first new content, and a lighter game build. Verified in ga
|
|||
- **6 new hairstyles** (Pocolov Hair 03, 06, 11, 15, 16, 17 — CC BY 4.0, credits in
|
||||
`Licenses/Free_3D_Models_Credits.txt`), in the new `lp_Revival` module. They follow the head, neck and chest
|
||||
(weights transferred from the body) and do not cut through the skin.
|
||||
- **Hairstyle import pipeline** for developers: `Tools/HairPipeline.ps1` converts a static FBX hairstyle (Fab or
|
||||
- **Hairstyle import pipeline** for developers (now in the separate mod workshop repository): `HairPipeline.ps1` converts a static FBX hairstyle (Fab or
|
||||
elsewhere) in two commands; protocol in
|
||||
[`Docs/Migration/13_Protocole_Coiffure.md`](Docs/Migration/13_Protocole_Coiffure.md) (French).
|
||||
- **Main menu.** Original author credited (Vinfamy); Donate links to the LifePlay Revival Patreon; new Discord link;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
// affiche le fichier entier. Cles ajoutees (facultatives, apres la ligne 3) :
|
||||
// MODULE_VERSION, MODULE_CREATED, MODULE_PACKAGED (dates AAAA-MM-JJ), MODULE_LICENSE, MODULE_AUTHOR_LINK,
|
||||
// MODULE_PRICE, MODULE_GAME_VERSION (version du jeu contre laquelle les assets du mod ont ete cuits, ecrite par
|
||||
// Tools/BuildMod.ps1), MODULE_DEFAULT_ENABLED (lu par LPHairstyleSubsystem).
|
||||
// LifePlay-Sandbox/Tools/BuildMod.ps1, atelier de mods de l'addon), MODULE_DEFAULT_ENABLED (lu par LPHairstyleSubsystem).
|
||||
//
|
||||
// Avertissement : un module ACTIF dont MODULE_GAME_VERSION differe de la version du jeu (ProjectVersion) declenche une
|
||||
// fenetre (Slate, independante des Blueprints, aussi sur l'ecran titre), une fois par module et par session.
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ $buildFlag = ''
|
|||
if (-not $SkipBuild) { $buildFlag = '-build ' }
|
||||
|
||||
# Pas de -nodebuginfo. -pak -iostore -compressed : reglages courants du projet (lot O2).
|
||||
# Fichier .cmd : PowerShell couperait une ligne UAT mal quotee (meme parade que BuildMod.ps1).
|
||||
# Fichier .cmd : PowerShell couperait une ligne UAT mal quotee (meme parade que BuildMod.ps1 de l'addon LifePlay-Sandbox).
|
||||
$batch = [IO.Path]::ChangeExtension($log, '.cmd')
|
||||
$cmd = "@call `"$uat`" BuildCookRun -project=`"$proj`" -platform=Win64 -clientconfig=$Config $buildFlag-cook -iterate -stage -pak -iostore -compressed -archive -archivedirectory=`"$ArchiveDir`" -unattended -utf8output -nop4 > `"$log`" 2>&1"
|
||||
[IO.File]::WriteAllText($batch, $cmd + "`r`n@exit /b %ERRORLEVEL%`r`n", (New-Object Text.UTF8Encoding $false))
|
||||
|
|
|
|||
|
|
@ -1,320 +0,0 @@
|
|||
<#
|
||||
.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\ ;
|
||||
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 ;
|
||||
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 ;
|
||||
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
|
||||
.\Tools\BuildMod.ps1 -Name lpr_hair_pocolov
|
||||
.\Tools\BuildMod.ps1 -Name lpr_hair_pocolov -Retarget bake
|
||||
.\Tools\BuildMod.ps1 -Name lpr_hair_pocolov -Retarget bake -RetargetOnly
|
||||
.\Tools\BuildMod.ps1 -Anim freeanims
|
||||
.\Tools\BuildMod.ps1 -Target sandbox -Open
|
||||
.\Tools\BuildMod.ps1 -Target game -Open
|
||||
.\Tools\BuildMod.ps1 -Name lpr_hair_pocolov -Install D:\Game\LifePlay_Build_5.5_H3d\Windows
|
||||
#>
|
||||
param(
|
||||
[string] $Name = '',
|
||||
[string] $Version = '',
|
||||
[string] $Out = 'D:\Game\LifePlay_Publication\Mods',
|
||||
[string] $Install = '',
|
||||
[string] $Retarget = '',
|
||||
[string] $RetargetConfig = '',
|
||||
[switch] $RetargetOnly,
|
||||
[string] $Target = '',
|
||||
[switch] $Open,
|
||||
[string] $Anim = '',
|
||||
[int] $From = 0,
|
||||
[switch] $SansCuisson
|
||||
)
|
||||
|
||||
$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'
|
||||
|
||||
# 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 = '') {
|
||||
$running = @(Get-Process UnrealEditor, UnrealEditor-Cmd -ErrorAction SilentlyContinue)
|
||||
if ($running) { throw "UnrealEditor est ouvert (pid $($running[0].Id)) : le fermer avant la conversion" }
|
||||
$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' }
|
||||
$editor = Join-Path $UE 'Binaries\Win64\UnrealEditor.exe'
|
||||
$project = Join-Path $ProjectRoot 'LifePlay.uproject'
|
||||
$script = Join-Path $ProjectRoot 'Tools\anim\RetargetPack.py'
|
||||
$logDir = Join-Path $ProjectRoot 'Saved\Logs'
|
||||
# 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
|
||||
$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) {
|
||||
if ($file.LastWriteTime -lt $launched) { continue }
|
||||
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() }
|
||||
$found = Find-RetargetLine $text $Mode $jeton
|
||||
if ($found.Hit) { $hit = $found.Hit }
|
||||
if ($found.Failure) { $failure = $found.Failure }
|
||||
}
|
||||
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" }
|
||||
# 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 }
|
||||
$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`nTarget=$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) {
|
||||
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()
|
||||
# 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()))" }
|
||||
}
|
||||
$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" }
|
||||
}
|
||||
|
||||
# 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 }
|
||||
if ($Retarget) { Invoke-AnimToGenesis $Root $Retarget $RetargetConfig }
|
||||
if ($Open) {
|
||||
Open-AnimTarget $Root $Target
|
||||
exit 0
|
||||
}
|
||||
if (-not $bCook) {
|
||||
Write-Host "Cuisson non lancee"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 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
|
||||
$base = if ($modVersion) { "$Name-$modVersion-for-LifePlay-$Version" } else { "$Name-for-LifePlay-$Version" }
|
||||
$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
|
||||
# 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))
|
||||
$readme = @(
|
||||
"$title ($Name)$(if ($modVersion) { " $modVersion" }) - built for LifePlay $Version",
|
||||
'',
|
||||
'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)
|
||||
|
||||
# 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"
|
||||
}
|
||||
|
||||
# 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"
|
||||
}
|
||||
# Code de sortie : succes (robocopy renvoie 1 a 7 quand il a copie).
|
||||
exit 0
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
# Mesure la pose Idle (image 0) : mannequin, Bridget, Michael.
|
||||
# But : voir si la main passe derriere le buste, et de quel angle le bras est roule.
|
||||
|
||||
import unreal
|
||||
|
||||
PAIRS = (
|
||||
("source", "/Game/Quaternius/UAL/Idle_Loop.Idle_Loop", "/Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard",
|
||||
("pelvis", "spine_03", "clavicle_l", "upperarm_l", "lowerarm_l", "hand_l",
|
||||
"clavicle_r", "upperarm_r", "lowerarm_r", "hand_r")),
|
||||
("bridget", "/Game/Quaternius/UAL/B_Idle_Loop.B_Idle_Loop", "/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp",
|
||||
("pelvis", "chestUpper", "lCollar", "lShldrBend", "lForearmBend", "lHand",
|
||||
"rCollar", "rShldrBend", "rForearmBend", "rHand")),
|
||||
("michael", "/Game/Quaternius/UAL/M_Idle_Loop.M_Idle_Loop", "/Game/Heavy/Daz/Michael/michael.michael",
|
||||
("pelvis", "chestUpper", "lCollar", "lShldrBend", "lForearmBend", "lHand",
|
||||
"rCollar", "rShldrBend", "rForearmBend", "rHand")),
|
||||
)
|
||||
|
||||
FINGERS = (
|
||||
"lThumb1", "lIndex1", "lMid1", "lRing1", "lPinky1",
|
||||
"thumb_01_l", "index_01_l", "middle_01_l", "pinky_01_l",
|
||||
"hand_l", "lHand",
|
||||
)
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return unreal.MathLibrary.subtract_vector_vector(a, b)
|
||||
|
||||
|
||||
def norm(v):
|
||||
return unreal.MathLibrary.normal(v)
|
||||
|
||||
|
||||
def cross(a, b):
|
||||
return unreal.MathLibrary.cross_vector_vector(a, b)
|
||||
|
||||
|
||||
def fmt(v):
|
||||
return "(%.1f %.1f %.1f)" % (v.x, v.y, v.z)
|
||||
|
||||
|
||||
def bone_names(mesh):
|
||||
pose = mesh.skeleton.get_reference_pose()
|
||||
names = unreal.AnimPoseExtensions.get_bone_names(pose)
|
||||
return [str(n) for n in names]
|
||||
|
||||
|
||||
def pose_at_zero(sequence):
|
||||
for call in (
|
||||
lambda: unreal.AnimPoseExtensions.get_anim_pose_at_time(sequence, 0.0),
|
||||
lambda: unreal.AnimPoseExtensions.get_anim_pose_at_frame(sequence, 0),
|
||||
):
|
||||
try:
|
||||
return call()
|
||||
except Exception as exc:
|
||||
unreal.log_warning("POSE api %s" % exc)
|
||||
raise RuntimeError("pas de lecture de pose")
|
||||
|
||||
|
||||
def bone_tm(pose, name):
|
||||
return unreal.AnimPoseExtensions.get_bone_pose(pose, name, unreal.AnimPoseSpaces.WORLD)
|
||||
|
||||
|
||||
def main():
|
||||
unreal.log_warning("POSE debut")
|
||||
for label, anim_path, mesh_path, bones in PAIRS:
|
||||
anim = unreal.load_asset(anim_path)
|
||||
mesh = unreal.load_asset(mesh_path)
|
||||
if not anim or not mesh:
|
||||
unreal.log_error("POSE absent %s" % label)
|
||||
continue
|
||||
names = bone_names(mesh)
|
||||
pose = pose_at_zero(anim)
|
||||
got = {}
|
||||
for bone in bones:
|
||||
if bone not in names:
|
||||
unreal.log_warning("POSE %s os absent %s" % (label, bone))
|
||||
continue
|
||||
got[bone] = bone_tm(pose, bone).translation
|
||||
pelvis = got.get("pelvis")
|
||||
chest_name = "spine_03" if "spine_03" in got else "chestUpper"
|
||||
chest = got.get(chest_name)
|
||||
if not pelvis or not chest:
|
||||
unreal.log_error("POSE %s sans buste" % label)
|
||||
continue
|
||||
up = norm(sub(chest, pelvis))
|
||||
# Avant approximatif : produit du haut du buste et de l'axe gauche-droite des epaules.
|
||||
left = got.get("clavicle_l") or got.get("upperarm_l") or got.get("lCollar") or got.get("lShldrBend")
|
||||
right = got.get("clavicle_r") or got.get("upperarm_r") or got.get("rCollar") or got.get("rShldrBend")
|
||||
if not left or not right:
|
||||
unreal.log_error("POSE %s sans epaules" % label)
|
||||
continue
|
||||
side = norm(sub(left, right))
|
||||
forward = norm(cross(side, up))
|
||||
unreal.log_warning("POSE %s avant %s" % (label, fmt(forward)))
|
||||
for side_name, hand_name, elbow_name in (
|
||||
("gauche", "hand_l" if "hand_l" in got else "lHand", "lowerarm_l" if "lowerarm_l" in got else "lForearmBend"),
|
||||
("droite", "hand_r" if "hand_r" in got else "rHand", "lowerarm_r" if "lowerarm_r" in got else "rForearmBend"),
|
||||
):
|
||||
hand = got.get(hand_name)
|
||||
elbow = got.get(elbow_name)
|
||||
if not hand:
|
||||
continue
|
||||
rel = sub(hand, chest)
|
||||
ahead = dot(rel, forward)
|
||||
unreal.log_warning(
|
||||
"POSE %s main %s %s devant %.1f pos %s" % (label, side_name, hand_name, ahead, fmt(hand)))
|
||||
if elbow:
|
||||
unreal.log_warning("POSE %s coude %s %s" % (label, side_name, fmt(elbow)))
|
||||
present = [n for n in FINGERS if n in names]
|
||||
unreal.log_warning("POSE %s doigts %s" % (label, " ".join(present)))
|
||||
unreal.log_warning("POSE fin")
|
||||
unreal.SystemLibrary.quit_editor()
|
||||
|
||||
|
||||
main()
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Ouvre l'editeur avec un mod d'assets monte (lot H3c, Docs/Modding/FullGuides/Creating_Asset_Mods.md).
|
||||
|
||||
.DESCRIPTION
|
||||
Monte ModSources\<Nom> dans Mods\<Nom> (Tools/ModSource.ps1), lance l'editeur et attend sa fermeture, puis remet le
|
||||
mod dans ModSources\<Nom> (meme si l'editeur plante). Les assets du mod apparaissent dans le navigateur de contenu
|
||||
sous "<Nom> Content" (chemin /<Nom>/) ; un editeur deja ouvert ne voit pas le mod : le fermer d'abord.
|
||||
|
||||
.EXAMPLE
|
||||
.\Tools\EditMod.ps1 -Name lpr_hair_pocolov
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $Name
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = Split-Path $PSScriptRoot -Parent
|
||||
. (Join-Path $PSScriptRoot 'ModSource.ps1')
|
||||
$Editor = 'D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe'
|
||||
|
||||
if (Get-Process UnrealEditor -ErrorAction SilentlyContinue) {
|
||||
throw "Un editeur est deja ouvert : le fermer (il ne verrait pas le mod), puis relancer"
|
||||
}
|
||||
$stray = Get-StrayMods $Root
|
||||
if ($stray) { throw "Autres plugins dans Mods\ ($($stray -join ', ')) : les remettre dans ModSources d'abord" }
|
||||
Mount-ModSource $Root $Name | Out-Null
|
||||
try {
|
||||
Write-Host "Editeur ouvert avec /$Name/ ; le mod sera remis dans ModSources a sa fermeture."
|
||||
Start-Process -FilePath $Editor -ArgumentList "`"$Root\LifePlay.uproject`"" -Wait
|
||||
}
|
||||
finally {
|
||||
Dismount-ModSource $Root $Name
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
# Lecture des associations materiau -> textures d'un FBX binaire, sans SDK (Tools/HairPipeline.ps1).
|
||||
#
|
||||
# Un FBX binaire est un arbre de noeuds ; Objects contient les Material, Texture (RelativeFilename) et Video, et
|
||||
# Connections les liens "OP" (texture -> propriete d'un materiau : DiffuseColor, TransparentColor, OpacityMap...) ou
|
||||
# "OO" (video -> texture). Ce script les relie et ecrit, pour chaque materiau, le fichier de chaque canal.
|
||||
#
|
||||
# Usage : python Tools/FbxMaterialMap.py <fichier.fbx> [<sortie.json>]
|
||||
# Sortie : {"<materiau>": {"DiffuseColor": "Hair_01_Gray.png", "TransparentColor": "Hair_01A1.jpg", ...}, ...}
|
||||
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
|
||||
def read_nodes(data, offset, end, wide):
|
||||
nodes = []
|
||||
while offset < end:
|
||||
if wide:
|
||||
end_offset, num_props, _ = struct.unpack_from("<QQQ", data, offset)
|
||||
offset += 24
|
||||
else:
|
||||
end_offset, num_props, _ = struct.unpack_from("<III", data, offset)
|
||||
offset += 12
|
||||
name_len = data[offset]
|
||||
offset += 1
|
||||
if end_offset == 0:
|
||||
break
|
||||
name = data[offset:offset + name_len].decode("ascii", "replace")
|
||||
offset += name_len
|
||||
props = []
|
||||
for _ in range(num_props):
|
||||
code = chr(data[offset])
|
||||
offset += 1
|
||||
if code in "YCIFDL":
|
||||
fmt = {"Y": "<h", "C": "<?", "I": "<i", "F": "<f", "D": "<d", "L": "<q"}[code]
|
||||
props.append(struct.unpack_from(fmt, data, offset)[0])
|
||||
offset += struct.calcsize(fmt)
|
||||
elif code in "SR":
|
||||
length = struct.unpack_from("<I", data, offset)[0]
|
||||
offset += 4
|
||||
raw = data[offset:offset + length]
|
||||
props.append(raw.decode("utf-8", "replace") if code == "S" else raw)
|
||||
offset += length
|
||||
elif code in "fdlib":
|
||||
count, encoding, length = struct.unpack_from("<III", data, offset)
|
||||
offset += 12 + length
|
||||
props.append(None) # tableaux inutiles ici
|
||||
else:
|
||||
raise ValueError("type de propriete FBX inconnu : " + code)
|
||||
children = read_nodes(data, offset, end_offset, wide) if offset < end_offset else []
|
||||
nodes.append((name, props, children))
|
||||
offset = end_offset
|
||||
return nodes
|
||||
|
||||
|
||||
def object_name(prop):
|
||||
# "Nom\x00\x01Classe" -> "Nom"
|
||||
return prop.split("\x00")[0] if isinstance(prop, str) else str(prop)
|
||||
|
||||
|
||||
def material_map(path):
|
||||
data = open(path, "rb").read()
|
||||
if not data.startswith(b"Kaydara FBX Binary"):
|
||||
raise ValueError("FBX ASCII non gere : " + path)
|
||||
version = struct.unpack_from("<I", data, 23)[0]
|
||||
nodes = read_nodes(data, 27, len(data), version >= 7500)
|
||||
objects = next((c for n, p, c in nodes if n == "Objects"), [])
|
||||
connections = next((c for n, p, c in nodes if n == "Connections"), [])
|
||||
materials, textures, videos = {}, {}, {}
|
||||
for name, props, children in objects:
|
||||
if not props:
|
||||
continue
|
||||
oid = props[0]
|
||||
if name == "Material":
|
||||
materials[oid] = object_name(props[1])
|
||||
elif name in ("Texture", "Video"):
|
||||
filename = next((c[1][0] for c in children if c[0] in ("RelativeFilename", "FileName") and c[1]), "")
|
||||
target = textures if name == "Texture" else videos
|
||||
target[oid] = re.split(r"[\\/]", filename)[-1] if filename else object_name(props[1])
|
||||
result = {}
|
||||
for name, props, _ in connections:
|
||||
if name != "C" or len(props) < 3:
|
||||
continue
|
||||
if props[0] == "OP" and props[1] in textures and props[2] in materials:
|
||||
channel = props[3] if len(props) > 3 else "?"
|
||||
result.setdefault(materials[props[2]], {})[channel] = textures[props[1]]
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mapping = material_map(sys.argv[1])
|
||||
text = json.dumps(mapping, indent=1, ensure_ascii=False)
|
||||
if len(sys.argv) > 2:
|
||||
open(sys.argv[2], "w", encoding="utf-8").write(text)
|
||||
print(text)
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
# Protocole de conversion d'une coiffure (Docs/Migration/13_Protocole_Coiffure.md), etape 2 : import du FBX.
|
||||
#
|
||||
# Importe les maillages d'un FBX en maillages statiques separes (sans materiaux ni textures) dans un dossier de travail,
|
||||
# puis affiche pour chacun : nom, boite englobante, triangles, emplacements de materiaux. Ces informations servent a
|
||||
# choisir le maillage de la coiffure (-Static) et la tete de reference (-FitHead) de LPHairFromStaticMesh.
|
||||
#
|
||||
# Usage :
|
||||
# UnrealEditor-Cmd.exe <projet> -run=pythonscript "-script=<projet>/Tools/HairImportFbx.py fbx=<fichier.fbx> dest=/Game/Heavy/New5/Hairs/_Work" -unattended -nopause -nosplash
|
||||
# Sortie : lignes "HAIRIMPORT" du journal.
|
||||
|
||||
import sys
|
||||
import unreal
|
||||
|
||||
args = dict(a.split("=", 1) for a in sys.argv[1:] if "=" in a)
|
||||
fbx = args.get("fbx")
|
||||
dest = args.get("dest", "/Game/Heavy/New5/Hairs/_Work")
|
||||
if not fbx:
|
||||
unreal.log_error("HAIRIMPORT usage : fbx=<fichier.fbx> [dest=/Game/...]")
|
||||
else:
|
||||
task = unreal.AssetImportTask()
|
||||
task.filename = fbx.replace("\\", "/")
|
||||
task.destination_path = dest
|
||||
task.automated = True
|
||||
task.replace_existing = True
|
||||
task.save = True
|
||||
ui = unreal.FbxImportUI()
|
||||
ui.import_mesh = True
|
||||
ui.import_as_skeletal = False
|
||||
ui.import_materials = False
|
||||
ui.import_textures = False
|
||||
ui.mesh_type_to_import = unreal.FBXImportType.FBXIT_STATIC_MESH
|
||||
ui.static_mesh_import_data.combine_meshes = False
|
||||
ui.static_mesh_import_data.auto_generate_collision = False
|
||||
task.options = ui
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
count = 0
|
||||
meshes = []
|
||||
for path in task.imported_object_paths:
|
||||
asset = unreal.load_asset(path)
|
||||
if isinstance(asset, unreal.StaticMesh):
|
||||
box = asset.get_bounding_box()
|
||||
size = box.max - box.min
|
||||
slots = [str(m.material_slot_name) for m in asset.static_materials]
|
||||
unreal.log_warning("HAIRIMPORT %s : %d triangles, taille X=%.1f Y=%.1f Z=%.1f cm, min Z=%.1f max Z=%.1f, emplacements %s"
|
||||
% (path, asset.get_num_triangles(0), size.x, size.y, size.z, box.min.z, box.max.z, slots))
|
||||
meshes.append({"path": path.split(".")[0], "name": asset.get_name(), "triangles": asset.get_num_triangles(0),
|
||||
"min": [box.min.x, box.min.y, box.min.z], "max": [box.max.x, box.max.y, box.max.z], "slots": slots})
|
||||
count += 1
|
||||
unreal.log_warning("HAIRIMPORT %d maillage(s) statique(s) importe(s) dans %s" % (count, dest))
|
||||
# Rapport lisible par Tools/HairPipeline.ps1 (option report=<fichier.json>).
|
||||
if args.get("report"):
|
||||
import json
|
||||
with open(args["report"], "w", encoding="utf-8") as f:
|
||||
json.dump({"fbx": fbx, "dest": dest, "meshes": meshes}, f, indent=1)
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
# Protocole de conversion d'une coiffure (Docs/Migration/13_Protocole_Coiffure.md), etape 4 : textures et materiaux.
|
||||
#
|
||||
# Importe les textures de la coiffure, cree deux materiaux enfants de /Game/Heavy/Daz/Bridget/Hairs/DazHair (le
|
||||
# materiau des coiffures Daz : parametres HairColor et HairThickness pilotes par le jeu) et les affecte aux
|
||||
# emplacements du SkeletalMesh produit par LPHairFromStaticMesh :
|
||||
# <Nom>_Cap : emplacements listes dans cap= (calotte) ; opacite cap_opacity= ou "white" (calotte opaque)
|
||||
# <Nom>_Strands : tous les autres emplacements (meches) ; opacite strands_opacity=
|
||||
# Controle final : dependances du SkeletalMesh (squelette et ces deux materiaux seulement).
|
||||
#
|
||||
# Usage :
|
||||
# UnrealEditor-Cmd.exe <projet> -run=pythonscript "-script=<projet>/Tools/HairMaterials.py mesh=/Game/Heavy/New5/Hairs/Nom
|
||||
# diffuse=<couleur.png> strands_opacity=<masque.jpg> cap_opacity=white cap=Hair_Base [thicken=1]" -unattended -nopause -nosplash
|
||||
# Les fichiers de textures sont des chemins sur disque ; les textures sont creees a cote du SkeletalMesh.
|
||||
# Sortie : lignes "HAIRMAT" du journal.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unreal
|
||||
|
||||
eal = unreal.EditorAssetLibrary
|
||||
mel = unreal.MaterialEditingLibrary
|
||||
tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||
args = dict(a.split("=", 1) for a in sys.argv[1:] if "=" in a)
|
||||
|
||||
|
||||
def import_texture(file, folder, name):
|
||||
# Texture deja importee depuis le meme fichier : reutilisee. Une autre source dans un plugin (mod, H3) : refus,
|
||||
# HairPipeline -Rebuild deplace d'abord les textures d'un mod vers la sauvegarde.
|
||||
path = folder + "/" + name
|
||||
if eal.does_asset_exist(path):
|
||||
existing = unreal.load_asset(path)
|
||||
data = existing.get_editor_property("asset_import_data")
|
||||
source = data.get_first_filename().replace("\\", "/") if data else ""
|
||||
if source.lower() == file.replace("\\", "/").lower():
|
||||
unreal.log_warning("HAIRMAT texture %s reutilisee (meme source)" % name)
|
||||
return existing
|
||||
if not folder.startswith("/Game/"):
|
||||
raise RuntimeError("HAIRMAT texture %s existe deja avec une autre source (%s) : relancer HairPipeline avec -Rebuild" % (path, source))
|
||||
task = unreal.AssetImportTask()
|
||||
task.filename = file.replace("\\", "/")
|
||||
task.destination_path = folder
|
||||
task.destination_name = name
|
||||
task.automated = True
|
||||
task.replace_existing = True
|
||||
task.save = True
|
||||
tools.import_asset_tasks([task])
|
||||
return unreal.load_asset(folder + "/" + name)
|
||||
|
||||
|
||||
def make_mic(folder, name, parent, diffuse, opacity, thicken):
|
||||
path = folder + "/" + name
|
||||
mic = unreal.load_asset(path) if eal.does_asset_exist(path) else tools.create_asset(
|
||||
name, folder, unreal.MaterialInstanceConstant, unreal.MaterialInstanceConstantFactoryNew())
|
||||
mel.set_material_instance_parent(mic, parent)
|
||||
mel.set_material_instance_texture_parameter_value(mic, "MapDiffuse", diffuse)
|
||||
mel.set_material_instance_texture_parameter_value(mic, "MapOpacity", opacity)
|
||||
mel.set_material_instance_scalar_parameter_value(mic, "Thicken", thicken)
|
||||
mel.update_material_instance(mic)
|
||||
eal.save_loaded_asset(mic, only_if_is_dirty=False)
|
||||
return mic
|
||||
|
||||
|
||||
mesh_path = args.get("mesh")
|
||||
if not mesh_path or not args.get("diffuse") or not args.get("strands_opacity"):
|
||||
unreal.log_error("HAIRMAT usage : mesh=/Game/... diffuse=<fichier> strands_opacity=<fichier> [cap_opacity=<fichier>|white] [cap=Slot1,Slot2] [thicken=1]")
|
||||
else:
|
||||
folder, name = mesh_path.rsplit("/", 1)
|
||||
# Dans un plugin (mod, H3), le registre de la commandlet ne connait pas encore les assets du dossier : sans cette
|
||||
# analyse, does_asset_exist repond faux, la creation d'un asset existant ouvre une boite de dialogue et plante.
|
||||
unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous([folder], True)
|
||||
thicken = float(args.get("thicken", "1"))
|
||||
cap_slots = [s for s in args.get("cap", "").split(",") if s]
|
||||
parent = unreal.load_asset("/Game/Heavy/Daz/Bridget/Hairs/DazHair")
|
||||
diffuse = import_texture(args["diffuse"], folder, name + "_Diffuse")
|
||||
strands_opacity = import_texture(args["strands_opacity"], folder, name + "_StrandsOpacity")
|
||||
strands = make_mic(folder, name + "_Strands", parent, diffuse, strands_opacity, thicken)
|
||||
# Calotte : seulement si un emplacement lui est attribue (Pocolov 11 : emplacement unique, pas de calotte).
|
||||
cap = None
|
||||
if cap_slots:
|
||||
cap_arg = args.get("cap_opacity", "white")
|
||||
cap_opacity = unreal.load_asset("/Engine/EngineResources/WhiteSquareTexture") if cap_arg == "white" else import_texture(cap_arg, folder, name + "_CapOpacity")
|
||||
cap = make_mic(folder, name + "_Cap", parent, diffuse, cap_opacity, 1.0)
|
||||
|
||||
# Affectation : modifier les elements puis reaffecter le tableau entier et sauvegarder sans condition
|
||||
# (une affectation Python ne marque pas l'asset modifie).
|
||||
mesh = unreal.load_asset(mesh_path)
|
||||
materials = []
|
||||
used = set()
|
||||
for slot in mesh.get_editor_property("materials"):
|
||||
slot_name = str(slot.get_editor_property("material_slot_name"))
|
||||
slot.set_editor_property("material_interface", cap if slot_name in cap_slots else strands)
|
||||
materials.append(slot)
|
||||
used.add(folder + "/" + (cap if slot_name in cap_slots else strands).get_name())
|
||||
unreal.log_warning("HAIRMAT emplacement %s -> %s" % (slot_name, (cap if slot_name in cap_slots else strands).get_name()))
|
||||
mesh.set_editor_property("materials", materials)
|
||||
eal.save_loaded_asset(mesh, only_if_is_dirty=False)
|
||||
|
||||
registry = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
# Chemin de paquet (et non de fichier) : vaut aussi pour un plugin de mod (/<Mod>/..., H3).
|
||||
registry.scan_paths_synchronous([folder], force_rescan=True)
|
||||
options = unreal.AssetRegistryDependencyOptions(include_soft_package_references=True, include_hard_package_references=True)
|
||||
deps = [str(d) for d in registry.get_dependencies(mesh_path, options) if not str(d).startswith("/Script/")]
|
||||
expected = {mesh_path + "_Skeleton"} | used
|
||||
unreal.log_warning("HAIRMAT dependances de %s : %s -> %s" % (name, deps, "OK" if set(deps) == expected else "A VERIFIER"))
|
||||
|
|
@ -1,284 +0,0 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Chaine industrialisee de conversion des coiffures (Docs/Migration/13_Protocole_Coiffure.md, section 7).
|
||||
|
||||
.DESCRIPTION
|
||||
Pilotee par un fichier JSON (un lot de coiffures). Deux etapes :
|
||||
|
||||
-Step Inspect : pour chaque coiffure
|
||||
1. extrait l'archive (zips imbriques compris) dans <backup>\<nom>\src ; copie le FBX et les textures sous des
|
||||
noms sans espaces ;
|
||||
2. importe le FBX en maillages statiques (Tools/HairImportFbx.py) dans <folder>/_Work_<nom> ;
|
||||
3. propose : maillage de la coiffure, tete de reference (nom contenant "head"), emplacements de calotte (nom
|
||||
contenant base, cap ou scalp), texture de couleur et masque des meches (analyse des images : niveaux de gris,
|
||||
proportion de pixels noirs/blancs, orientation) ;
|
||||
4. ecrit la proposition dans le fichier JSON (champs "auto" remplaces) et un rapport.
|
||||
-> relire le rapport, corriger le JSON si besoin.
|
||||
|
||||
-Step Apply : pour chaque coiffure inspectee
|
||||
1. LPHairFromStaticMesh (essai a blanc, controle des echelles, puis conversion) ;
|
||||
2. Tools/HairMaterials.py (textures, materiaux DazHair, controle des dependances) ;
|
||||
3. dossier _Work_<nom> deplace vers <backup>\<nom>\work (jamais supprime) ;
|
||||
4. Modules/<module>/Hairstyles/<nom>.lphair (attribution comprise) ; credits dans
|
||||
Licenses/Free_3D_Models_Credits.txt (une ligne par coiffure, sans doublon) ;
|
||||
puis Tools/BuildCollections.py et un bilan. Le build et le controle en jeu restent manuels (protocole, etapes 6 et 7).
|
||||
|
||||
Une coiffure dont le SkeletalMesh existe deja est sautee (rien n'est ecrase), sauf avec -Rebuild : la coiffure et son
|
||||
squelette precedents sont alors deplaces dans <backup>\<nom>\precedente_<date> avant reconstruction.
|
||||
Options de LPHairFromStaticMesh par coiffure dans le JSON : margin, scale, offset_z, offset_y, skin (transfer|rigid),
|
||||
gap (cm).
|
||||
|
||||
Mod d'assets (H3b) : avec "mod": "<Nom>" et "folder": "/<Nom>/Hairs", les sources ModSources\<Nom> sont montees dans
|
||||
Mods\<Nom> pendant le travail (Tools/ModSource.ps1) puis remises en place ; les .lphair et les credits vont dans
|
||||
ModSources\<Nom>\Module ; -Rebuild deplace aussi les textures du mod vers la sauvegarde (pas de remplacement de
|
||||
texture dans un plugin). Le mod se cuit ensuite avec Tools/BuildMod.ps1.
|
||||
|
||||
.EXAMPLE
|
||||
.\Tools\HairPipeline.ps1 -Config Tools\hairs\HAIRS1.json -Step Inspect
|
||||
.\Tools\HairPipeline.ps1 -Config Tools\hairs\HAIRS1.json -Step Apply
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $Config,
|
||||
[Parameter(Mandatory)] [ValidateSet('Inspect', 'Apply')] [string] $Step,
|
||||
[string] $Only = '',
|
||||
[switch] $Rebuild
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = Split-Path $PSScriptRoot -Parent
|
||||
$Proj = Join-Path $Root 'LifePlay.uproject'
|
||||
$UE = 'D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
|
||||
$Tools = ($PSScriptRoot -replace '\\', '/')
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
. (Join-Path $PSScriptRoot 'ModSource.ps1')
|
||||
|
||||
$cfg = Get-Content $Config -Raw | ConvertFrom-Json
|
||||
$backup = $cfg.backup
|
||||
$folder = $cfg.folder
|
||||
$mod = $cfg.mod
|
||||
New-Item -ItemType Directory -Force $backup | Out-Null
|
||||
|
||||
function Invoke-UE([string[]] $Arguments, [string] $Filter) {
|
||||
$out = & $UE $Proj @Arguments -unattended -nopause -nosplash 2>&1 | ForEach-Object { "$_" }
|
||||
$out | Where-Object { $_ -match $Filter -and $_ -notmatch 'LogInit: Display' }
|
||||
}
|
||||
|
||||
# Chemin disque d'un chemin de paquet : /Game/X -> Content/X ; /<Plugin>/X -> Mods/<Plugin>/Content/X (mod, H3)
|
||||
# ou Plugins/<Plugin>/Content/X.
|
||||
function Get-DiskPath([string] $PackagePath) {
|
||||
$parts = $PackagePath.TrimStart('/').Split('/', 2)
|
||||
if ($parts[0] -eq 'Game') { return Join-Path $Root ('Content\' + ($parts[1] -replace '/', '\')) }
|
||||
foreach ($base in 'Mods', 'Plugins') {
|
||||
$pluginDir = Join-Path $Root "$base\$($parts[0])"
|
||||
if (Test-Path $pluginDir) { return Join-Path $pluginDir ('Content\' + ($parts[1] -replace '/', '\')) }
|
||||
}
|
||||
throw "Dossier de contenu introuvable pour $PackagePath"
|
||||
}
|
||||
|
||||
$SevenZip = 'C:\Program Files\7-Zip\7z.exe'
|
||||
function Expand-One([string] $Archive, [string] $Dest) {
|
||||
# 7-Zip lit zip, rar et 7z (Pocolov 03 : source/Hair3.rar) ; Expand-Archive en secours pour les zip.
|
||||
if (Test-Path $SevenZip) { & $SevenZip x -y "-o$Dest" $Archive | Out-Null }
|
||||
else { Expand-Archive -LiteralPath $Archive -DestinationPath $Dest -Force }
|
||||
}
|
||||
|
||||
function Expand-All([string] $Archive, [string] $Dest) {
|
||||
Expand-One $Archive $Dest
|
||||
# Archives imbriquees (Pocolov : source/<nom>.zip ou .rar)
|
||||
for ($pass = 0; $pass -lt 3; $pass++) {
|
||||
$inner = Get-ChildItem $Dest -Recurse -Include *.zip, *.rar, *.7z | Where-Object { -not (Test-Path ($_.FullName + '.done')) }
|
||||
if (-not $inner) { break }
|
||||
foreach ($z in $inner) {
|
||||
Expand-One $z.FullName (Join-Path $z.DirectoryName ($z.BaseName -replace '\s', '_'))
|
||||
New-Item -ItemType File ($z.FullName + '.done') | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Statistiques d'une image : saturation moyenne, part de pixels tres sombres / tres clairs, part sombre du bas.
|
||||
function Get-ImageStats([string] $Path) {
|
||||
try { $bmp = [System.Drawing.Bitmap]::FromFile($Path) } catch { return $null }
|
||||
try {
|
||||
$step = [Math]::Max(1, [int]([Math]::Sqrt($bmp.Width * $bmp.Height / 20000)))
|
||||
$n = 0; $sat = 0.0; $dark = 0; $light = 0; $bottomDark = 0; $bottomN = 0; $sideDark = 0; $sideN = 0
|
||||
for ($y = 0; $y -lt $bmp.Height; $y += $step) {
|
||||
for ($x = 0; $x -lt $bmp.Width; $x += $step) {
|
||||
$c = $bmp.GetPixel($x, $y)
|
||||
$max = [Math]::Max($c.R, [Math]::Max($c.G, $c.B)); $min = [Math]::Min($c.R, [Math]::Min($c.G, $c.B))
|
||||
$sat += ($max - $min) / 255.0
|
||||
$l = ($c.R + $c.G + $c.B) / 765.0
|
||||
if ($l -lt 0.15) { $dark++ } elseif ($l -gt 0.85) { $light++ }
|
||||
if ($y -gt $bmp.Height * 0.8) { $bottomN++; if ($l -lt 0.3) { $bottomDark++ } }
|
||||
if ($x -lt $bmp.Width * 0.05 -or $x -ge $bmp.Width * 0.95) { $sideN++; if ($l -lt 0.3) { $sideDark++ } }
|
||||
$n++
|
||||
}
|
||||
}
|
||||
[pscustomobject]@{
|
||||
File = $Path; Width = $bmp.Width; Height = $bmp.Height
|
||||
Saturation = [Math]::Round($sat / $n, 3)
|
||||
Binary = [Math]::Round(($dark + $light) / $n, 3)
|
||||
Dark = [Math]::Round($dark / $n, 3)
|
||||
BottomDark = if ($bottomN) { [Math]::Round($bottomDark / $bottomN, 3) } else { 0 }
|
||||
SideDark = if ($sideN) { [Math]::Round($sideDark / $sideN, 3) } else { 0 }
|
||||
}
|
||||
} finally { $bmp.Dispose() }
|
||||
}
|
||||
|
||||
function Select-Textures($all, [string[]] $StrandSlots) {
|
||||
# Archive avec une tete complete (Pocolov 03 : peau, yeux, sourcils) : ne garder que les textures nommees "hair"
|
||||
# s'il y en a.
|
||||
$stats = @($all | Where-Object { (Split-Path $_.File -Leaf) -match 'hair' })
|
||||
if (-not $stats) { $stats = $all }
|
||||
# Masque : niveaux de gris, majoritairement noir/blanc. Masque des meches : vertical (hauteur > largeur ; un masque
|
||||
# carre est en general la lisiere de la calotte, Pocolov 16) ; a
|
||||
# egalite, celui dont le nom contient un emplacement des meches (Hair_01 -> Hair_01A1), puis l'ordre alphabetique
|
||||
# (A1 avant A2). Couleur : le reste ; de preference grise (teintee par HairColor), sinon la plus grande.
|
||||
$masks = $stats | Where-Object { $_.Saturation -lt 0.05 -and $_.Binary -gt 0.55 }
|
||||
$matchesSlot = { param($f) foreach ($s in $StrandSlots) { if ($s -and (Split-Path $f -Leaf) -match [regex]::Escape($s)) { return $true } }; $false }
|
||||
# Puis bords lateraux sombres (carte de meches a bords transparents : Hair_01A1 plutot que Hair_01A, Pocolov 03).
|
||||
$strands = $masks | Where-Object { $_.Height -gt $_.Width } | Sort-Object @{ e = { & $matchesSlot $_.File }; Descending = $true }, @{ e = { $_.SideDark -gt 0.5 }; Descending = $true }, @{ e = { Split-Path $_.File -Leaf } } | Select-Object -First 1
|
||||
$colors = $stats | Where-Object { $_ -notin $masks }
|
||||
$diffuse = $colors | Sort-Object @{ e = { $_.Saturation -lt 0.05 }; Descending = $true }, @{ e = { $_.Height * $_.Width }; Descending = $true } | Select-Object -First 1
|
||||
[pscustomobject]@{ Diffuse = $diffuse; Strands = $strands }
|
||||
}
|
||||
|
||||
$report = @()
|
||||
if ($mod) {
|
||||
if ($folder -notlike "/$mod/*") { throw "folder ($folder) doit etre dans /$mod/" }
|
||||
Mount-ModSource $Root $mod | Out-Null
|
||||
}
|
||||
try {
|
||||
foreach ($h in $cfg.hairs) {
|
||||
if ($Only -and $h.name -ne $Only) { continue }
|
||||
$name = $h.name
|
||||
$dir = Join-Path $backup $name
|
||||
$meshPath = "$folder/$name"
|
||||
$work = "$folder/_Work_$name"
|
||||
|
||||
if ($Step -eq 'Inspect') {
|
||||
Write-Host "== $name : inspection"
|
||||
if (-not (Test-Path $h.archive)) { $report += "$name : archive absente ($($h.archive))"; continue }
|
||||
Remove-Item (Join-Path $dir 'src') -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Expand-All $h.archive (Join-Path $dir 'src')
|
||||
$norm = Join-Path $dir 'norm'
|
||||
New-Item -ItemType Directory -Force $norm | Out-Null
|
||||
$fbx = Get-ChildItem (Join-Path $dir 'src') -Recurse -Filter *.fbx | Sort-Object Length -Descending | Select-Object -First 1
|
||||
if (-not $fbx) { $report += "$name : aucun FBX dans l'archive"; continue }
|
||||
Copy-Item $fbx.FullName (Join-Path $norm 'hair.fbx') -Force
|
||||
$i = 0; $stats = @()
|
||||
foreach ($t in Get-ChildItem (Join-Path $dir 'src') -Recurse -Include *.png, *.jpg, *.jpeg, *.tga, *.bmp) {
|
||||
$i++
|
||||
$copy = Join-Path $norm ("tex{0:D2}_{1}{2}" -f $i, ($t.BaseName -replace '[^A-Za-z0-9_-]', '_'), $t.Extension.ToLower())
|
||||
Copy-Item $t.FullName $copy -Force
|
||||
$s = Get-ImageStats $copy
|
||||
if ($s) { $stats += $s }
|
||||
}
|
||||
$json = Join-Path $dir 'import.json'
|
||||
$fbxArg = (Join-Path $norm 'hair.fbx') -replace '\\', '/'
|
||||
Invoke-UE @('-run=pythonscript', "-script=$Tools/HairImportFbx.py fbx=$fbxArg dest=$work report=$($json -replace '\\','/')") 'HAIRIMPORT' | Write-Host
|
||||
if (-not (Test-Path $json)) { $report += "$name : import du FBX echoue"; continue }
|
||||
$imp = Get-Content $json -Raw | ConvertFrom-Json
|
||||
$head = $imp.meshes | Where-Object { $_.name -match 'head' } | Sort-Object triangles -Descending | Select-Object -First 1
|
||||
$hairMeshes = @($imp.meshes | Where-Object { $_ -ne $head })
|
||||
$hair = $hairMeshes | Sort-Object triangles -Descending | Select-Object -First 1
|
||||
# Champs detectes : ceux valant "auto" a la premiere inspection (memorises dans auto_fields, recalcules a chaque
|
||||
# inspection) ; une valeur saisie a la main n'est jamais remplacee.
|
||||
if (-not $h.auto_fields) { $h | Add-Member auto_fields @('mesh', 'head', 'cap_slots', 'diffuse', 'strands_opacity' | Where-Object { $h.$_ -eq 'auto' }) -Force }
|
||||
$auto = { param($f) $f -in $h.auto_fields }
|
||||
if (& $auto 'mesh') { $h.mesh = $hair.path }
|
||||
if (& $auto 'head') { $h.head = if ($head) { $head.path } else { $hair.path } }
|
||||
# Associations materiau -> textures declarees dans le FBX (Tools/FbxMaterialMap.py), pour le rapport et le cas
|
||||
# d'un emplacement unique.
|
||||
$fbxMap = $null
|
||||
try { $fbxMap = (& python (Join-Path $PSScriptRoot 'FbxMaterialMap.py') (Join-Path $norm 'hair.fbx')) -join "`n" | ConvertFrom-Json } catch { }
|
||||
$findCopy = { param($fileName) if ($fileName) { $b = [IO.Path]::GetFileNameWithoutExtension($fileName) -replace '[^A-Za-z0-9_-]', '_'; ($stats | Where-Object { (Split-Path $_.File -Leaf) -match ('^tex\d+_' + [regex]::Escape($b) + '\.') } | Select-Object -First 1).File } }
|
||||
# Calotte : emplacement nomme base/cap/scalp, seulement s'il reste au moins un autre emplacement pour les meches.
|
||||
if (& $auto 'cap_slots') { $h.cap_slots = if ($hair.slots.Count -gt 1) { (@($hair.slots | Where-Object { $_ -match 'base|cap|scalp' }) -join ',') } else { '' } }
|
||||
$strandSlots = @($hair.slots | Where-Object { $_ -notin ($h.cap_slots -split ',') })
|
||||
$sel = Select-Textures $stats $strandSlots
|
||||
if (& $auto 'diffuse') { $h.diffuse = if ($sel.Diffuse) { $sel.Diffuse.File -replace '\\', '/' } else { '' } }
|
||||
if (& $auto 'strands_opacity') {
|
||||
# Emplacement unique (Pocolov 11) : masque declare par le FBX pour cet emplacement ; sinon masque vertical.
|
||||
$declared = if ($strandSlots.Count -eq 1 -and $hair.slots.Count -eq 1 -and $fbxMap) { & $findCopy $fbxMap.($strandSlots[0]).TransparentColor }
|
||||
$h.strands_opacity = if ($declared) { $declared -replace '\\', '/' } elseif ($sel.Strands) { $sel.Strands.File -replace '\\', '/' } else { '' }
|
||||
}
|
||||
$notes = @()
|
||||
if ($hairMeshes.Count -gt 1) { $notes += "$($hairMeshes.Count) maillages hors tete : seul '$($hair.name)' est converti" }
|
||||
if (-not $head) { $notes += 'pas de tete de reference : ajustement sur la coiffure elle-meme (margin 1.0)'; if (-not $h.margin) { $h | Add-Member margin '1.0' -Force } }
|
||||
if (-not $h.diffuse -or -not $h.strands_opacity) { $notes += 'textures non identifiees : renseigner diffuse / strands_opacity' }
|
||||
$h | Add-Member inspected $true -Force
|
||||
$report += "$name : coiffure $($hair.name) ($($hair.triangles) tri., emplacements $($hair.slots -join ',')), tete $(if ($head) { $head.name } else { '-' }), calotte [$($h.cap_slots)], couleur $(Split-Path $h.diffuse -Leaf), masque $(Split-Path $h.strands_opacity -Leaf)$(if ($notes) { ' ; ' + ($notes -join ' ; ') })"
|
||||
if ($fbxMap) { $report += ($fbxMap.PSObject.Properties | ForEach-Object { " FBX $($_.Name) : " + (($_.Value.PSObject.Properties | ForEach-Object { "$($_.Name)=$($_.Value)" }) -join ', ') }) }
|
||||
$report += ($stats | ForEach-Object { " texture $(Split-Path $_.File -Leaf) : $($_.Width)x$($_.Height), saturation $($_.Saturation), noir/blanc $($_.Binary), bas sombre $($_.BottomDark), bords sombres $($_.SideDark)" })
|
||||
}
|
||||
else {
|
||||
Write-Host "== $name : application"
|
||||
if (-not $h.inspected) { $report += "$name : non inspectee, sautee"; continue }
|
||||
$existing = (Get-DiskPath $meshPath) + '.uasset'
|
||||
if (Test-Path $existing) {
|
||||
if (-not $Rebuild) { $report += "$name : $meshPath existe deja, sautee (-Rebuild pour la refaire)"; continue }
|
||||
# -Rebuild : coiffure et squelette precedents deplaces vers la sauvegarde (materiaux et textures mis a jour sur place).
|
||||
$previous = Join-Path $dir ('precedente_' + (Get-Date -Format 'yyyyMMdd_HHmmss'))
|
||||
New-Item -ItemType Directory -Force $previous | Out-Null
|
||||
Move-Item $existing $previous
|
||||
Move-Item ($existing -replace '\.uasset$', '_Skeleton.uasset') $previous -ErrorAction SilentlyContinue
|
||||
if ($mod) {
|
||||
foreach ($suffix in '_Diffuse', '_StrandsOpacity', '_CapOpacity') {
|
||||
Move-Item ($existing -replace '\.uasset$', "$suffix.uasset") $previous -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
$common = @("-Static=$($h.mesh)", "-FitHead=$($h.head)", "-Out=$meshPath")
|
||||
if ($h.margin) { $common += "-Margin=$($h.margin)" }
|
||||
foreach ($k in 'scale', 'offset_z', 'offset_y', 'skin', 'gap') { if ($h.$k) { $common += ('-' + @{ scale = 'Scale'; offset_z = 'OffsetZ'; offset_y = 'OffsetY'; skin = 'Skin'; gap = 'Gap' }[$k] + '=' + $h.$k) } }
|
||||
$dry = Invoke-UE (@('-run=LPHairFromStaticMesh') + $common + '-DryRun') 'LogLPHairFromStatic: (Warning|Error)'
|
||||
$dry | Write-Host
|
||||
$scaleLine = $dry | Where-Object { $_ -match 'echelle X=([\d.]+) Y=([\d.]+)' } | Select-Object -First 1
|
||||
if (-not $scaleLine) { $report += "$name : essai a blanc en echec"; continue }
|
||||
$sx = [double]$Matches[1]; $sy = [double]$Matches[2]
|
||||
if ($sx -lt 0.6 -or $sx -gt 1.8 -or $sy -lt 0.6 -or $sy -gt 1.8) { $report += "$name : echelle hors limites (X=$sx Y=$sy), sautee : verifier la tete de reference"; continue }
|
||||
$conv = Invoke-UE (@('-run=LPHairFromStaticMesh') + $common) 'LogLPHairFromStatic: (Warning|Error)'
|
||||
if (-not ($conv -match 'sauvegarde :.*' + [regex]::Escape($name) + '\.uasset')) { $conv | Write-Host; $report += "$name : conversion en echec"; continue }
|
||||
$placed = ($conv | Where-Object { $_ -match 'bornes' }) -replace '.*bornes ', ''
|
||||
$mat = Invoke-UE @('-run=pythonscript', "-script=$Tools/HairMaterials.py mesh=$meshPath diffuse=$($h.diffuse) strands_opacity=$($h.strands_opacity) cap_opacity=$($h.cap_opacity) cap=$($h.cap_slots) thicken=$($h.thicken)") 'HAIRMAT'
|
||||
$mat | Write-Host
|
||||
$depOk = [bool]($mat -match 'dependances.*-> OK')
|
||||
# Nettoyage : dossier de travail deplace vers la sauvegarde.
|
||||
$workDir = Get-DiskPath $work
|
||||
if (Test-Path $workDir) { Move-Item $workDir (Join-Path $dir 'work') -Force }
|
||||
# Declaration et credits.
|
||||
# Mod (H3b) : module texte du mod, dans ses sources (montees dans Mods\<mod> pendant le travail).
|
||||
$moduleDir = if ($mod) { Join-Path $Root "Mods\$mod\Module" } else { Join-Path $Root "Content\Modules\$($cfg.module)" }
|
||||
$lphairDir = Join-Path $moduleDir 'Hairstyles'
|
||||
New-Item -ItemType Directory -Force $lphairDir | Out-Null
|
||||
$credit = "$($h.title) by $($h.author), $($h.license), $($h.url)"
|
||||
$lines = @(
|
||||
"// $($h.title) - $($h.author) - licence $($h.license)",
|
||||
"// Source : $($h.url)",
|
||||
'// Modifications (Tools/HairPipeline.ps1) : liee au squelette Genesis 8 par transfert de poids depuis le corps (tete, cou, poitrine), ajustee a la tete de Bridget, materiaux DazHair.',
|
||||
"HAIRSTYLE: $meshPath.$name", "SEX: $($h.sex)", 'DAZ: true')
|
||||
[IO.File]::WriteAllText((Join-Path $lphairDir "$($name.ToLower()).lphair"), ($lines -join "`r`n") + "`r`n", (New-Object Text.UTF8Encoding $false))
|
||||
# Credits : fichier du jeu, ou Module\CREDITS.txt d'un mod.
|
||||
$credits = if ($mod) { Join-Path $moduleDir 'CREDITS.txt' } else { Join-Path $Root 'Licenses\Free_3D_Models_Credits.txt' }
|
||||
if (-not (Test-Path $credits)) { New-Item -ItemType File -Force $credits | Out-Null }
|
||||
if (-not (Select-String -LiteralPath $credits -SimpleMatch $h.url -Quiet)) {
|
||||
Add-Content -LiteralPath $credits -Value ("- $credit . Changes: skinned to the Genesis 8 skeleton by weight transfer from the body (head, neck, chest), fitted to the Bridget head, DazHair materials ($meshPath*).") -Encoding utf8
|
||||
}
|
||||
$report += "$name : OK, echelle X=$sx Y=$sy, bornes $placed, dependances $(if ($depOk) { 'OK' } else { 'A VERIFIER' })"
|
||||
}
|
||||
}
|
||||
|
||||
if ($Step -eq 'Inspect') {
|
||||
$cfg | ConvertTo-Json -Depth 6 | Set-Content $Config -Encoding utf8
|
||||
}
|
||||
else {
|
||||
Invoke-UE @('-run=pythonscript', "-script=$Tools/BuildCollections.py") 'collection\(s\) ecrite' | Write-Host
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if ($mod) { Dismount-ModSource $Root $mod }
|
||||
}
|
||||
$reportFile = Join-Path $backup "rapport_$Step.txt"
|
||||
$report | Set-Content $reportFile -Encoding utf8
|
||||
Write-Host "`n=== Bilan ($Step) : $reportFile"
|
||||
$report | Write-Host
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# Importe le mannequin et les animations CC0 de Quaternius (Universal Animation Library, sans root motion).
|
||||
#
|
||||
# Usage :
|
||||
# UnrealEditor-Cmd.exe <projet> -run=pythonscript "-script=<projet>/Tools/ImportQuaternius.py" -unattended -nopause -nosplash
|
||||
# Sortie : lignes "QUATERNIUS" du journal.
|
||||
|
||||
import unreal
|
||||
|
||||
glb = "D:/Game/Universal Animation Library[Standard]/Unreal-Godot/UAL1_Standard.glb"
|
||||
dest = "/Game/Quaternius/UAL"
|
||||
|
||||
task = unreal.AssetImportTask()
|
||||
task.filename = glb
|
||||
task.destination_path = dest
|
||||
task.automated = True
|
||||
task.replace_existing = True
|
||||
task.save = True
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
reported = list(task.imported_object_paths)
|
||||
unreal.log_warning("QUATERNIUS tache %d objet(s)" % len(reported))
|
||||
for path in reported:
|
||||
unreal.log_warning("QUATERNIUS PATH %s" % path)
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_directory_exist(dest):
|
||||
unreal.log_error("QUATERNIUS dossier absent %s" % dest)
|
||||
else:
|
||||
unreal.EditorAssetLibrary.save_directory(dest, False, True)
|
||||
assets = unreal.EditorAssetLibrary.list_assets(dest, True, False)
|
||||
unreal.log_warning("QUATERNIUS dossier %d asset(s)" % len(assets))
|
||||
for path in assets:
|
||||
asset = unreal.load_asset(path)
|
||||
kind = asset.get_class().get_name() if asset else "null"
|
||||
extra = ""
|
||||
if isinstance(asset, unreal.SkeletalMesh):
|
||||
box = asset.get_bounds()
|
||||
extra = " origin=(%.1f %.1f %.1f) extent=(%.1f %.1f %.1f)" % (
|
||||
box.origin.x, box.origin.y, box.origin.z,
|
||||
box.box_extent.x, box.box_extent.y, box.box_extent.z)
|
||||
unreal.log_warning("QUATERNIUS ASSET %s %s%s" % (kind, path, extra))
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Montage temporaire d'un mod d'assets (lot H3b, Docs/Migration/16_H3_Mods_Paquets.md).
|
||||
|
||||
.DESCRIPTION
|
||||
Les sources d'un mod vivent dans ModSources\<Nom>\ :
|
||||
<Nom>.uplugin plugin de contenu (CanContainContent)
|
||||
Content\ assets du mod (non versionnes, comme Content/Heavy)
|
||||
Module\ module LifePlay en texte (<Nom>.lpmod, Hairstyles\*.lphair, CREDITS.txt)
|
||||
L'editeur ne monte /<Nom>/ que si le plugin est dans <projet>\Mods\ ; mais un plugin laisse dans Mods\ serait cuit
|
||||
avec le jeu de base. Mount-ModSource deplace donc ModSources\<Nom> vers Mods\<Nom> le temps d'un travail (import,
|
||||
cuisson DLC) et Dismount-ModSource le remet en place (deplacements seulement, rien n'est copie ni supprime).
|
||||
|
||||
A charger par dot-sourcing : . (Join-Path $PSScriptRoot 'ModSource.ps1')
|
||||
#>
|
||||
|
||||
function Get-ModSourcePaths([string] $Root, [string] $Name) {
|
||||
[pscustomobject]@{
|
||||
Source = Join-Path $Root "ModSources\$Name"
|
||||
Mounted = Join-Path $Root "Mods\$Name"
|
||||
}
|
||||
}
|
||||
|
||||
function Mount-ModSource([string] $Root, [string] $Name) {
|
||||
$p = Get-ModSourcePaths $Root $Name
|
||||
if (Test-Path $p.Mounted) {
|
||||
throw "Mods\$Name existe deja (travail precedent interrompu ?) : le remettre dans ModSources\$Name a la main avant de relancer"
|
||||
}
|
||||
if (-not (Test-Path (Join-Path $p.Source "$Name.uplugin"))) {
|
||||
throw "ModSources\$Name\$Name.uplugin introuvable"
|
||||
}
|
||||
New-Item -ItemType Directory -Force (Split-Path $p.Mounted -Parent) | Out-Null
|
||||
Move-Item $p.Source $p.Mounted
|
||||
Write-Host "Mod $Name monte dans Mods\$Name"
|
||||
$p.Mounted
|
||||
}
|
||||
|
||||
function Dismount-ModSource([string] $Root, [string] $Name) {
|
||||
$p = Get-ModSourcePaths $Root $Name
|
||||
if (Test-Path $p.Mounted) {
|
||||
Move-Item $p.Mounted $p.Source
|
||||
Write-Host "Mod $Name remis dans ModSources\$Name"
|
||||
}
|
||||
}
|
||||
|
||||
# Autres plugins presents dans Mods\ (ils seraient cuits avec le jeu de base).
|
||||
function Get-StrayMods([string] $Root, [string] $Except = '') {
|
||||
$mods = Join-Path $Root 'Mods'
|
||||
if (-not (Test-Path $mods)) { return @() }
|
||||
@(Get-ChildItem $mods -Directory | Where-Object { $_.Name -ne $Except } | ForEach-Object Name)
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Cree le squelette d'un mod d'assets (lot H3c, Docs/Modding/FullGuides/Creating_Asset_Mods.md).
|
||||
|
||||
.DESCRIPTION
|
||||
Cree ModSources\<Nom>\ :
|
||||
<Nom>.uplugin plugin de contenu (assets cuits a part du jeu)
|
||||
Content\ assets du mod (a remplir dans l'editeur : Tools/EditMod.ps1)
|
||||
Module\<Nom>.lpmod module LifePlay (MODULE_UNIQUEID = <Nom>, active depuis le gestionnaire de mods)
|
||||
Module\CREDITS.txt credits et licences des assets
|
||||
et, avec -Hairs, Tools\mods\<Nom>.json (configuration de Tools/HairPipeline.ps1, liste de coiffures vide).
|
||||
Refuse un nom deja pris (sources, module du jeu ou plugin).
|
||||
|
||||
.EXAMPLE
|
||||
.\Tools\NewMod.ps1 -Name MyHairPack -Title "My Hair Pack" -Author "Me" -License "CC BY 4.0" -AuthorLink https://example.org -Hairs
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)] [ValidatePattern('^[A-Za-z][A-Za-z0-9_]{2,40}$')] [string] $Name,
|
||||
[Parameter(Mandatory)] [string] $Title,
|
||||
[Parameter(Mandatory)] [string] $Author,
|
||||
[string] $Description = '',
|
||||
[string] $Link = '',
|
||||
[string] $ModVersion = '1.0',
|
||||
[string] $License = '',
|
||||
[string] $AuthorLink = '',
|
||||
[string] $Price = 'Free',
|
||||
[switch] $Hairs
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = Split-Path $PSScriptRoot -Parent
|
||||
$src = Join-Path $Root "ModSources\$Name"
|
||||
foreach ($taken in $src, (Join-Path $Root "Mods\$Name"), (Join-Path $Root "Content\Modules\$Name"), (Join-Path $Root "Plugins\$Name")) {
|
||||
if (Test-Path $taken) { throw "Nom deja pris : $taken" }
|
||||
}
|
||||
$utf8 = New-Object Text.UTF8Encoding $false
|
||||
New-Item -ItemType Directory -Force (Join-Path $src 'Content'), (Join-Path $src 'Module') | Out-Null
|
||||
|
||||
$plugin = [ordered]@{
|
||||
FileVersion = 3; Version = 1; VersionName = $ModVersion
|
||||
FriendlyName = $Title
|
||||
Description = $(if ($Description) { $Description } else { "LifePlay mod: $Title" })
|
||||
Category = 'LifePlay Mods'; CreatedBy = $Author
|
||||
CanContainContent = $true; IsBetaVersion = $false; Installed = $false
|
||||
}
|
||||
if ($AuthorLink) { $plugin.CreatedByURL = $AuthorLink }
|
||||
[IO.File]::WriteAllText((Join-Path $src "$Name.uplugin"), ($plugin | ConvertTo-Json) + "`r`n", $utf8)
|
||||
|
||||
$lpmod = @(
|
||||
"MODULE_UNIQUEID: $Name",
|
||||
"MODULE_NAME: $Title",
|
||||
"MODULE_AUTHOR: $Author",
|
||||
"MODULE_LINK: $Link",
|
||||
"MODULE_DESCRIPTION: $(if ($Description) { $Description } else { $Title })",
|
||||
'MODULE_REQUIREMENTS: vin_Base',
|
||||
# Metadonnees (H3d) ; MODULE_PACKAGED et MODULE_GAME_VERSION sont ecrites par BuildMod.ps1 dans la copie livree.
|
||||
"MODULE_VERSION: $ModVersion",
|
||||
"MODULE_CREATED: $(Get-Date -Format 'yyyy-MM-dd')",
|
||||
"MODULE_LICENSE: $License",
|
||||
"MODULE_AUTHOR_LINK: $AuthorLink",
|
||||
"MODULE_PRICE: $Price")
|
||||
[IO.File]::WriteAllText((Join-Path $src "Module\$Name.lpmod"), ($lpmod -join "`r`n") + "`r`n", $utf8)
|
||||
[IO.File]::WriteAllText((Join-Path $src 'Module\CREDITS.txt'), "Credits and licenses of the assets in $Title (one line per asset: title, author, license, source URL, changes).`r`n", $utf8)
|
||||
|
||||
if ($Hairs) {
|
||||
$cfgPath = Join-Path $Root "Tools\mods\$Name.json"
|
||||
if (Test-Path $cfgPath) { throw "$cfgPath existe deja" }
|
||||
$cfg = [ordered]@{
|
||||
mod = $Name; module = $Name; folder = "/$Name/Hairs"
|
||||
backup = "D:/Game/LifePlay_Backup_5.5/mods/$Name"
|
||||
hairs = @()
|
||||
}
|
||||
New-Item -ItemType Directory -Force (Split-Path $cfgPath -Parent) | Out-Null
|
||||
[IO.File]::WriteAllText($cfgPath, ($cfg | ConvertTo-Json -Depth 4) + "`r`n", $utf8)
|
||||
Write-Host "Configuration des coiffures : $cfgPath (ajouter les entrees de 'hairs', voir Tools/mods/lpr_hair_pocolov.json)"
|
||||
}
|
||||
Write-Host "Mod cree : $src"
|
||||
Write-Host "Suite : .\Tools\EditMod.ps1 -Name $Name (assets dans l'editeur), puis .\Tools\BuildMod.ps1 -Name $Name"
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
.PARAMETER (configuration "per_module": true, depuis la publication qui suit la V1.4)
|
||||
Une archive par module optionnel, <module>-for-LifePlay-<version>.zip (LifePlay\Content\Modules\<module> + notice
|
||||
bilingue tiree du .lpmod), dans <Out>\<out_dir> ; 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.
|
||||
de diffusion (Discord). Les mods d'assets (LifePlay-Sandbox/Tools/BuildMod.ps1, atelier de mods de l'addon, lot DC3) se deposent dans le meme dossier.
|
||||
|
||||
.EXAMPLE
|
||||
.\Tools\SplitModules.ps1 -Build D:\Game\LifePlay_Build_5.5_Shipping_V1.5 -Config Tools\modules
|
||||
|
|
@ -38,7 +38,7 @@ $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.
|
||||
# ces mods sont livres par LifePlay-Sandbox/Tools/BuildMod.ps1 (addon, lot DC3).
|
||||
$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" }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,563 +0,0 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Pilote de la chaine d'animations d'un pack, de la source au mod livre (lot AP7, Docs/Migration/20_Chaine_Animations.md, section 3).
|
||||
|
||||
.DESCRIPTION
|
||||
-Pack <nom> : dossier AnimSources\<nom> ; la chaine est decrite par le bloc "chaine" de AnimSources\<nom>\pack.json.
|
||||
Huit etapes, un controle bloquant a chacune, arret au premier controle en echec :
|
||||
0 inventaire pack.json complet, licence / credit / auteur renseignes (refus de "A COMPLETER"), sources trouvees,
|
||||
empreintes SHA-256 des .uasset de la source
|
||||
1 import glb : script d'import (saute si clips presents et glb inchange) ; uasset : rien. Clips > 0
|
||||
2 identif. LPAnimIdentify sur le clip de reference ; valide, methode non factice ; la methode est retenue
|
||||
3 calque LPAnimEval -PoseOnly (pose T des corps) ; rapport.json valide
|
||||
4 conversion BuildMod -Retarget bake (cmd /c start /wait) ; ligne RETARGET bridget n michael n (n = clips) ;
|
||||
puis LPAnimTwist ; rapport_torsion.json valide
|
||||
5 evaluation LPAnimEval complet (un clip rejete bloque), rapport.html, puis sandbox (vignettes et planches)
|
||||
6 catalogue copie des clips dans le mod (DeliverAnims.ps1, ecrit livraison.json), LPAnimCatalog (fiches),
|
||||
Catalogue.py (DeliverAnims.ps1 -CatalogueOnly). La copie precede les fiches : le controle exige
|
||||
fiches_calculees.json plus recent que livraison.json
|
||||
7 livraison cuisson DLC du mod (BuildMod.ps1 -Name <mod>) ; sans version de reference Releases\<version>\Windows :
|
||||
avertissement, sans echec ; -SansCuisson la saute
|
||||
Rapports : <travail>\etapes\<n>_<nom>.json (debut, fin, duree, commandes, code, controles, verdict) et
|
||||
<travail>\etat.json (avancement, methode retenue, empreintes). -From <n> reprend a l'etape n (etat.json lu ;
|
||||
etapes precedentes non rejouees) ; -To <n> s'arrete apres l'etape n. Un passage complet (sans -From) deplace
|
||||
l'ancien contenu de <travail> dans <travail>\precedent_<date> (jamais supprime).
|
||||
-DryRun : affiche le plan (commandes exactes, dans l'ordre), ne lance ni ne cree rien ; l'etape 0 y est evaluee
|
||||
pour de bon et ses echecs sont montres comme "echec prevu" sans arreter le plan.
|
||||
|
||||
.EXAMPLE
|
||||
.\Tools\anim\AnimPipeline.ps1 -Pack quaternius -DryRun
|
||||
.\Tools\anim\AnimPipeline.ps1 -Pack freeanims
|
||||
.\Tools\anim\AnimPipeline.ps1 -Pack freeanims -From 5 -SansCuisson
|
||||
.\Tools\BuildMod.ps1 -Anim freeanims
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)][string] $Pack,
|
||||
[ValidateRange(0, 7)][int] $From = 0,
|
||||
[ValidateRange(0, 7)][int] $To = 7,
|
||||
[switch] $DryRun,
|
||||
[switch] $SansCuisson
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$Editor = 'D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
|
||||
$Pwsh = (Get-Process -Id $PID).Path
|
||||
$Utf8 = New-Object Text.UTF8Encoding $false
|
||||
$StepNames = 'inventaire', 'import', 'identification', 'calque', 'conversion', 'evaluation', 'catalogue', 'livraison'
|
||||
|
||||
# ---------------------------------------------------------------- etat du passage
|
||||
$script:DryRun = [bool] $DryRun
|
||||
$script:Cfg = $null # pack.json
|
||||
$script:Ch = $null # bloc chaine
|
||||
$script:Work = '' # dossier de travail (absolu, barres obliques)
|
||||
$script:Method = '' # methode retenue (etape 2) ; vide tant qu'inconnue
|
||||
$script:Clips = @() # clips source (noms), hors B_/M_ et exclus
|
||||
$script:Etat = [ordered]@{ pack = $Pack; etapes = [ordered]@{} }
|
||||
$script:PrevEtat = $null
|
||||
$script:St = $null # etape en cours
|
||||
$script:Planned = @() # echecs prevus (essai a blanc)
|
||||
|
||||
function Show-Rel([string] $Path) {
|
||||
$p = $Path -replace '/', '\'
|
||||
if ($p.StartsWith($Root, [StringComparison]::OrdinalIgnoreCase)) { return $p.Substring($Root.Length).TrimStart('\') }
|
||||
$p
|
||||
}
|
||||
function Add-Cmd([string] $Text) {
|
||||
$script:St.Cmds += $Text
|
||||
Write-Host " > $Text"
|
||||
}
|
||||
function Add-Check([string] $Name, [bool] $Ok, [string] $Detail = '') {
|
||||
$script:St.Checks += [ordered]@{ nom = $Name; ok = $Ok; detail = $Detail }
|
||||
if ($Ok) { Write-Host " [ok] $Name$(if ($Detail) { " : $Detail" })" }
|
||||
elseif ($script:DryRun) { Write-Host " [ECHEC PREVU] $Name$(if ($Detail) { " : $Detail" })" -ForegroundColor Yellow; $script:Planned += "etape $($script:St.N) : $Name$(if ($Detail) { " : $Detail" })" }
|
||||
else { Write-Host " [ECHEC] $Name$(if ($Detail) { " : $Detail" })" -ForegroundColor Red }
|
||||
}
|
||||
function Add-Warn([string] $Text) {
|
||||
$script:St.Warns += $Text
|
||||
Write-Host " [avertissement] $Text" -ForegroundColor Yellow
|
||||
}
|
||||
function Read-Json([string] $Path) {
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return $null }
|
||||
try { Get-Content -LiteralPath $Path -Raw -Encoding utf8 | ConvertFrom-Json } catch { $null }
|
||||
}
|
||||
function Write-Text([string] $Path, [string] $Text) {
|
||||
New-Item -ItemType Directory -Force (Split-Path $Path -Parent) | Out-Null
|
||||
[IO.File]::WriteAllText($Path, $Text, $Utf8)
|
||||
}
|
||||
function ConvertTo-Disk([string] $GamePath) {
|
||||
if ($GamePath -notmatch '^/Game/(.+)$') { throw "chemin Unreal invalide (attendu /Game/...) : $GamePath" }
|
||||
Join-Path $Root ('Content\' + ($Matches[1] -replace '/', '\'))
|
||||
}
|
||||
function Get-SourceClips {
|
||||
$dir = ConvertTo-Disk $script:Ch.source.clips
|
||||
if (-not (Test-Path $dir)) { return @() }
|
||||
$excl = @($script:Cfg.exclus | Where-Object { $_ })
|
||||
# Hors maillage et squelette importes avec les clips (glb : <maillage>, <maillage>_Skeleton, <maillage>_PhysicsAsset).
|
||||
$mesh = ([string] $script:Ch.source.maillage -replace '^.*/', '' -replace '\..*$', '')
|
||||
@(Get-ChildItem $dir -File -Filter '*.uasset' | ForEach-Object { $_.BaseName } |
|
||||
Where-Object { $_ -notlike 'B_*' -and $_ -notlike 'M_*' -and $_ -notin $excl -and -not ($mesh -and $_ -like "$mesh*") -and $_ -notlike '*_Skeleton' -and $_ -notlike '*_PhysicsAsset' } | Sort-Object)
|
||||
}
|
||||
function Test-Placeholder($Value) {
|
||||
$s = [string] $Value
|
||||
(-not $s.Trim()) -or ($s -match 'A COMPLETER')
|
||||
}
|
||||
function Test-EditorClosed {
|
||||
$running = @(Get-Process UnrealEditor, UnrealEditor-Cmd -ErrorAction SilentlyContinue)
|
||||
Add-Check 'editeur ferme' ($running.Count -eq 0) $(if ($running) { "UnrealEditor ouvert (pid $($running[0].Id))" } else { '' })
|
||||
$running.Count -eq 0
|
||||
}
|
||||
function Get-MethodArg { if ($script:Method) { $script:Method } elseif ($script:Ch.methode -and $script:Ch.methode -ne 'auto') { $script:Ch.methode } else { '<methode-etape-2>' } }
|
||||
function Get-WorkRel { Show-Rel $script:Work }
|
||||
|
||||
# Commandlet via Tools\RunCommandlet.ps1 ; rend le code de sortie.
|
||||
function Invoke-Commandlet([string] $Commandlet, [string] $CmdArgs) {
|
||||
Add-Cmd "Tools\RunCommandlet.ps1 -Lot AP7 -Commandlet $Commandlet -Args '$CmdArgs'"
|
||||
if ($script:DryRun) { return 0 }
|
||||
$global:LASTEXITCODE = 0
|
||||
& (Join-Path $Root 'Tools\RunCommandlet.ps1') -Lot AP7 -Commandlet $Commandlet -Args $CmdArgs | Out-Host
|
||||
[int] $LASTEXITCODE
|
||||
}
|
||||
|
||||
# Arguments communs a LPAnimEval / LPAnimTwist.
|
||||
function Get-EvalArgs {
|
||||
$bodies = (@($script:Ch.corps | ForEach-Object { '{0}:{1}' -f $_.nom, $_.prefixe })) -join ','
|
||||
$templates = (@($script:Ch.corps | ForEach-Object { '{0}:{1}' -f $_.nom, $_.gabarit })) -join ','
|
||||
'-Methode={0} -ClipDir={1} -RefClip={2} -BodyClipDir={3} -Templates={4} -Bodies={5}' -f (Get-MethodArg), $script:Ch.source.clips, $script:Ch.source.clip_reference, $script:Ch.sortie.clips, $templates, $bodies
|
||||
}
|
||||
function Get-WorkArg([string] $Name) { '-{0}="{1}"' -f $Name, $script:Work }
|
||||
|
||||
function Test-Fresh([string] $Path, [datetime] $Since) {
|
||||
(Test-Path -LiteralPath $Path) -and ((Get-Item -LiteralPath $Path).LastWriteTime -ge $Since.AddSeconds(-2))
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- etapes
|
||||
function Step-Inventaire {
|
||||
$packFile = Join-Path $Root "AnimSources\$Pack\pack.json"
|
||||
$script:Cfg = Read-Json $packFile
|
||||
Add-Check 'pack.json lisible' ($null -ne $script:Cfg) (Show-Rel $packFile)
|
||||
if (-not $script:Cfg) { throw "pack.json illisible : $packFile" }
|
||||
$c = $script:Cfg.chaine
|
||||
$script:Ch = $c
|
||||
Add-Check 'bloc chaine present' ($null -ne $c)
|
||||
if (-not $c) { throw 'bloc "chaine" absent de pack.json' }
|
||||
$missing = @()
|
||||
foreach ($k in 'methode', 'profil_attendu', 'mod', 'travail', 'sandbox') { if (Test-Placeholder $c.$k) { $missing += "chaine.$k" } }
|
||||
foreach ($k in 'type', 'clips', 'maillage', 'clip_reference') { if (Test-Placeholder $c.source.$k) { $missing += "chaine.source.$k" } }
|
||||
foreach ($k in 'clips', 'rigs') { if (Test-Placeholder $c.sortie.$k) { $missing += "chaine.sortie.$k" } }
|
||||
if ($c.source.type -eq 'glb') { foreach ($k in 'fichier', 'import') { if (Test-Placeholder $c.source.$k) { $missing += "chaine.source.$k" } } }
|
||||
elseif ($c.source.type -ne 'uasset') { $missing += "chaine.source.type ($($c.source.type) : glb ou uasset)" }
|
||||
if (@($c.corps).Count -eq 0) { $missing += 'chaine.corps' }
|
||||
foreach ($b in @($c.corps)) { foreach ($k in 'nom', 'maillage', 'prefixe', 'gabarit') { if (Test-Placeholder $b.$k) { $missing += "chaine.corps[$($b.nom)].$k" } } }
|
||||
if (Test-Placeholder $script:Cfg.prefixe) { $missing += 'prefixe' }
|
||||
Add-Check 'bloc chaine complet' ($missing.Count -eq 0) ($missing -join ', ')
|
||||
$legal = @()
|
||||
foreach ($k in 'licence', 'credit', 'auteur') { if (Test-Placeholder $script:Cfg.$k) { $legal += "$k = '$($script:Cfg.$k)'" } }
|
||||
Add-Check 'licence, credit et auteur renseignes' ($legal.Count -eq 0) $(if ($legal) { 'refus : ' + ($legal -join ' ; ') } else { "$($script:Cfg.licence) / $($script:Cfg.auteur)" })
|
||||
if ($missing.Count -gt 0) { throw 'bloc chaine incomplet' }
|
||||
$script:Work = (Join-Path $Root ($c.travail -replace '/', '\')).TrimEnd('\') -replace '\\', '/'
|
||||
|
||||
$files = @()
|
||||
if ($c.source.type -eq 'glb') {
|
||||
$glb = [string] $c.source.fichier
|
||||
$ok = Test-Path -LiteralPath $glb
|
||||
Add-Check 'fichier glb trouve' $ok $glb
|
||||
if ($ok) { $script:Etat.glb_sha256 = (Get-FileHash -LiteralPath $glb -Algorithm SHA256).Hash }
|
||||
$imp = Join-Path $Root ($c.source.import -replace '/', '\')
|
||||
Add-Check 'script d''import trouve' (Test-Path $imp) (Show-Rel $imp)
|
||||
}
|
||||
$dir = ConvertTo-Disk $c.source.clips
|
||||
$dirOk = Test-Path $dir
|
||||
if ($c.source.type -eq 'uasset') { Add-Check 'dossier des clips trouve' $dirOk (Show-Rel $dir) }
|
||||
elseif (-not $dirOk) { Write-Host " (dossier des clips absent : cree par l'import) $(Show-Rel $dir)" }
|
||||
$hashes = [ordered]@{}
|
||||
if ($dirOk) {
|
||||
foreach ($clip in Get-SourceClips) {
|
||||
$hashes["$clip.uasset"] = (Get-FileHash -LiteralPath (Join-Path $dir "$clip.uasset") -Algorithm SHA256).Hash
|
||||
}
|
||||
}
|
||||
if ($c.source.type -eq 'uasset') { Add-Check 'empreintes SHA-256 des .uasset' ($hashes.Count -gt 0) "$($hashes.Count) fichier(s)" }
|
||||
else { Write-Host " empreintes SHA-256 : $($hashes.Count) .uasset deja presents + glb" }
|
||||
$script:Etat.empreintes = $hashes
|
||||
$script:St.Extra.empreintes = $hashes
|
||||
if ($script:Etat.glb_sha256) { $script:St.Extra.glb_sha256 = $script:Etat.glb_sha256 }
|
||||
}
|
||||
|
||||
function Step-Import {
|
||||
$c = $script:Ch
|
||||
if ($c.source.type -eq 'uasset') {
|
||||
$script:St.Skipped = $true
|
||||
Write-Host ' source uasset : rien a importer'
|
||||
}
|
||||
else {
|
||||
$dir = ConvertTo-Disk $c.source.clips
|
||||
$have = @(Get-SourceClips).Count
|
||||
$prev = if ($script:PrevEtat) { $script:PrevEtat.glb_sha256 } else { $null }
|
||||
$now = if ($script:Etat.glb_sha256) { $script:Etat.glb_sha256 } elseif (Test-Path -LiteralPath $c.source.fichier) { (Get-FileHash -LiteralPath $c.source.fichier -Algorithm SHA256).Hash } else { $null }
|
||||
$script:Etat.glb_sha256 = $now
|
||||
if ($have -gt 0 -and $prev -and $prev -eq $now) {
|
||||
$script:St.Skipped = $true
|
||||
Write-Host " import saute : $have clip(s) presents, glb inchange (SHA-256 $($now.Substring(0, 12))...)"
|
||||
}
|
||||
else {
|
||||
$py = (Join-Path $Root ($c.source.import -replace '/', '\')) -replace '\\', '/'
|
||||
$log = Join-Path $Root ('Saved\Logs\portage\AP7_Import_{0}.log' -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
|
||||
Add-Cmd "UnrealEditor-Cmd.exe `"$Root\LifePlay.uproject`" -run=pythonscript `"-script=$py`" -unattended -nopause -nosplash -stdout -FullStdOutLogOutput > $(Show-Rel $log)"
|
||||
if (-not $script:DryRun) {
|
||||
if (-not (Test-EditorClosed)) { throw 'editeur ouvert' }
|
||||
New-Item -ItemType Directory -Force (Split-Path $log -Parent) | Out-Null
|
||||
& $Editor (Join-Path $Root 'LifePlay.uproject') -run=pythonscript "-script=$py" -unattended -nopause -nosplash -stdout -FullStdOutLogOutput *> $log
|
||||
$script:St.Code = $LASTEXITCODE
|
||||
$imported = Select-String -Path $log -Pattern 'QUATERNIUS dossier \d+ asset' -Quiet
|
||||
Add-Check 'import termine (journal)' $imported (Show-Rel $log)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $script:DryRun) {
|
||||
$n = @(Get-SourceClips).Count
|
||||
Add-Check 'clips source > 0' ($n -gt 0) "$n clip(s)"
|
||||
$script:Clips = Get-SourceClips
|
||||
}
|
||||
else { Write-Host " clips source actuellement : $(@(Get-SourceClips).Count)" }
|
||||
}
|
||||
|
||||
function Step-Identification {
|
||||
$c = $script:Ch
|
||||
$clip = '{0}/{1}' -f $c.source.clips.TrimEnd('/'), $c.source.clip_reference
|
||||
$out = "$($script:Work)/identification.json"
|
||||
$code = Invoke-Commandlet 'LPAnimIdentify' ('-Clip={0} -Out="{1}" -Attendu={2}' -f $clip, $out, $c.profil_attendu)
|
||||
if ($script:DryRun) {
|
||||
if ($c.methode -and $c.methode -ne 'auto') { Write-Host " methode imposee par pack.json : $($c.methode)" } else { Write-Host ' methode retenue : celle de identification.json (champ methode)' }
|
||||
return
|
||||
}
|
||||
$script:St.Code = $code
|
||||
Add-Check 'LPAnimIdentify code 0' ($code -eq 0) "code $code"
|
||||
$r = Read-Json $out
|
||||
Add-Check 'identification.json valide' ($r -and $r.valide -eq $true) $(if ($r) { "profil $($r.profil), methode $($r.methode)" } else { 'rapport absent' })
|
||||
Add-Check 'methode non factice' ($r -and $r.methode -and $r.methode_factice -ne $true) $(if ($r) { "methode '$($r.methode)'" })
|
||||
if ($c.methode -and $c.methode -ne 'auto') { $script:Method = [string] $c.methode; Write-Host " methode imposee par pack.json : $($script:Method)" }
|
||||
elseif ($r -and $r.methode) { $script:Method = [string] $r.methode }
|
||||
$script:Etat.methode = $script:Method
|
||||
$script:St.Extra.methode = $script:Method
|
||||
}
|
||||
|
||||
function Step-Calque {
|
||||
if (-not $script:DryRun -and -not (Test-EditorClosed)) { throw 'editeur ouvert' }
|
||||
$a = (Get-EvalArgs) + " -PoseOnly -PoseDir=`"$($script:Work)/pose`" " + (Get-WorkArg 'Out')
|
||||
$code = Invoke-Commandlet 'LPAnimEval' $a
|
||||
if ($script:DryRun) { return }
|
||||
$script:St.Code = $code
|
||||
$r = Read-Json (Join-Path $script:Work 'rapport.json')
|
||||
Add-Check 'rapport.json valide (pose T)' ($r -and $r.valide -eq $true) $(if ($r -and $r.echecs) { (@($r.echecs) -join '; ') } elseif (-not $r) { 'rapport absent' } else { "code $code" })
|
||||
foreach ($b in @($script:Ch.corps)) {
|
||||
Add-Check "retarget_pose_$($b.nom).txt ecrit" (Test-Path (Join-Path $script:Work "pose\retarget_pose_$($b.nom).txt"))
|
||||
}
|
||||
}
|
||||
|
||||
function Step-Conversion {
|
||||
$c = $script:Ch
|
||||
$cfgFile = "$($script:Work)/anim_retarget.json"
|
||||
$cfg = [ordered]@{
|
||||
mode = 'bake'; methode = (Get-MethodArg); source_mesh = $c.source.maillage; clips_dir = $c.source.clips
|
||||
out_dir = $c.sortie.clips; dest = $c.sortie.rigs
|
||||
corps = @($c.corps | ForEach-Object { [ordered]@{ nom = $_.nom; maillage = $_.maillage; prefixe = $_.prefixe } })
|
||||
exclure = @($script:Cfg.exclus | Where-Object { $_ })
|
||||
pose_dir = "$($script:Work)/pose"
|
||||
}
|
||||
$log = "$($script:Work)/etapes/4_conversion_bake.log"
|
||||
$bat = "$($script:Work)/etapes/4_conversion_bake.cmd"
|
||||
$buildMod = Join-Path $Root 'Tools\BuildMod.ps1'
|
||||
$line = "@`"$Pwsh`" -NoProfile -ExecutionPolicy Bypass -File `"$buildMod`" -Retarget bake -RetargetOnly -RetargetConfig `"$($cfgFile -replace '/', '\')`" > `"$($log -replace '/', '\')`" 2>&1"
|
||||
Write-Host " config de cuisson : $(Show-Rel $cfgFile)"
|
||||
Add-Cmd "cmd /c start `"`" /wait `"$(Show-Rel $bat)`" (contenu : $(Show-Rel $buildMod) -Retarget bake -RetargetOnly -RetargetConfig $(Show-Rel $cfgFile) > $(Show-Rel $log))"
|
||||
if (-not $script:DryRun) {
|
||||
if (-not (Test-EditorClosed)) { throw 'editeur ouvert' }
|
||||
Write-Text $cfgFile (($cfg | ConvertTo-Json -Depth 10) + "`r`n")
|
||||
Write-Text $bat ($line + "`r`n@exit /b %ERRORLEVEL%`r`n")
|
||||
# start /wait sur un .cmd laisse sa console ouverte (cmd /K) et ne rend jamais la main : passer par cmd /c (AP7).
|
||||
$p = Start-Process cmd -ArgumentList ('/c start "" /wait cmd /c "{0}"' -f ($bat -replace '/', '\')) -Wait -PassThru -NoNewWindow
|
||||
$script:St.Code = $p.ExitCode
|
||||
$text = if (Test-Path $log) { Get-Content $log -Raw -Encoding utf8 } else { '' }
|
||||
$n = @(Get-SourceClips).Count
|
||||
$m = [regex]::Match($text, 'RETARGET bridget (\d+) michael (\d+)')
|
||||
Add-Check 'conversion : code 0' ($script:St.Code -eq 0) "code $($script:St.Code)"
|
||||
Add-Check "ligne RETARGET bridget $n michael $n" ($m.Success -and [int] $m.Groups[1].Value -eq $n -and [int] $m.Groups[2].Value -eq $n) $(if ($m.Success) { $m.Value } else { 'ligne absente : voir ' + (Show-Rel $log) })
|
||||
if (-not $m.Success) { throw 'conversion non confirmee' }
|
||||
}
|
||||
$a = (Get-EvalArgs) + ' ' + (Get-WorkArg 'Out')
|
||||
$code = Invoke-Commandlet 'LPAnimTwist' $a
|
||||
if ($script:DryRun) { return }
|
||||
$r = Read-Json (Join-Path $script:Work 'rapport_torsion.json')
|
||||
Add-Check 'rapport_torsion.json valide' ($r -and $r.valide -eq $true) $(if (-not $r) { "rapport absent (code $code)" } else { '' })
|
||||
}
|
||||
|
||||
# Sandbox : vignettes et planches. Fichiers de Saved\ remplaces le temps de la tournee, puis deplaces ou restaures.
|
||||
function Invoke-Sandbox {
|
||||
$c = $script:Ch
|
||||
$saved = Join-Path $Root 'Saved'
|
||||
$ini = Join-Path $saved 'sandbox_pack.ini'
|
||||
$modeF = Join-Path $saved 'sandbox_mode.txt'
|
||||
$tourF = Join-Path $saved 'sandbox_tour.txt'
|
||||
$bat = Join-Path $Root 'Tools\debug\LifePlay_interaction_sandbox.bat'
|
||||
$sbRel = ($c.sortie.clips -replace '^/Game/', '')
|
||||
$clipRel = ($c.source.clips -replace '^/Game/', '')
|
||||
$sbRoot = [string] $c.sandbox
|
||||
$iniText = @(
|
||||
'; Ecrit par Tools\anim\AnimPipeline.ps1 (lot AP7)', '[Sandbox.Pack]', "Name=$Pack", "Method=$(Get-MethodArg)",
|
||||
"ClipRoot=$($c.source.clips)", "ClipDisk=$clipRel", "BodyClipRoot=$($c.sortie.clips)", "BodyClipDisk=$sbRel",
|
||||
"RefClip=$($c.source.clip_reference)", "MannequinMesh=$($c.source.maillage)", '', '[Sandbox.Output]', "MeasureOutputRoot=$sbRoot") -join "`r`n"
|
||||
$outDir = Join-Path $saved ($sbRoot -replace '/', '\')
|
||||
Add-Cmd "ecrire Saved\sandbox_pack.ini (Name=$Pack, Method=$(Get-MethodArg), ClipRoot=$($c.source.clips), BodyClipRoot=$($c.sortie.clips), RefClip=$($c.source.clip_reference), MeasureOutputRoot=$sbRoot)"
|
||||
Add-Cmd 'ecrire Saved\sandbox_mode.txt = Mesure ; Saved\sandbox_tour.txt = vignettes planches quitter'
|
||||
Add-Cmd "Tools\debug\LifePlay_interaction_sandbox.bat (attente de la fermeture de UnrealEditor, 40 min maximum)"
|
||||
Add-Cmd "deplacer Saved\sandbox_pack.ini vers $(Get-WorkRel)\sandbox_pack.ini ; restaurer sandbox_mode.txt d'origine"
|
||||
if ($script:DryRun) { Write-Host " controle : <Clip>.png dans $(Show-Rel (Join-Path $outDir 'vignettes')) ; <Clip>.jpg et <Clip>.json dans $(Show-Rel (Join-Path $outDir 'planches')), par clip"; return }
|
||||
if (-not (Test-EditorClosed)) { throw 'editeur ouvert' }
|
||||
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$t0 = Get-Date
|
||||
New-Item -ItemType Directory -Force $script:Work | Out-Null
|
||||
$origMode = if (Test-Path $modeF) { Get-Content $modeF -Raw } else { $null }
|
||||
foreach ($f in $ini, $tourF) {
|
||||
if (Test-Path $f) { Move-Item $f (Join-Path $script:Work ("{0}.avant_{1}" -f (Split-Path $f -Leaf), $stamp)) }
|
||||
}
|
||||
try {
|
||||
Write-Text $ini ($iniText + "`r`n")
|
||||
Write-Text $modeF 'Mesure'
|
||||
Write-Text $tourF 'vignettes planches quitter'
|
||||
# Sans tuyau ni attente : avec `& $bat | Out-Null`, PowerShell attendait la fin de l'editeur lance par le .bat
|
||||
# (heritier de la sortie), puis ne le trouvait plus et concluait a tort « pas lancee » (AP7).
|
||||
Start-Process -FilePath 'cmd.exe' -ArgumentList ('/c "{0}"' -f $bat) -WindowStyle Hidden | Out-Null
|
||||
$deadline = (Get-Date).AddMinutes(3)
|
||||
while (-not (Get-Process UnrealEditor -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 3 }
|
||||
if (-not (Get-Process UnrealEditor -ErrorAction SilentlyContinue)) { throw 'la sandbox ne s''est pas lancee (3 min)' }
|
||||
$deadline = (Get-Date).AddMinutes(40)
|
||||
while ((Get-Process UnrealEditor -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 10 }
|
||||
if (Get-Process UnrealEditor -ErrorAction SilentlyContinue) { throw 'la sandbox ne s''est pas fermee en 40 min (laissee ouverte)' }
|
||||
}
|
||||
finally {
|
||||
if (Test-Path $ini) { Move-Item $ini (Join-Path $script:Work 'sandbox_pack.ini') -Force }
|
||||
if (Test-Path $tourF) { Move-Item $tourF (Join-Path $script:Work 'sandbox_tour.txt') -Force }
|
||||
if (Test-Path $modeF) { Move-Item $modeF (Join-Path $script:Work 'sandbox_mode.txt') -Force }
|
||||
if ($null -ne $origMode) { Write-Text $modeF $origMode }
|
||||
}
|
||||
$missV = @(); $missP = @()
|
||||
foreach ($clip in (Get-SourceClips)) {
|
||||
if (-not (Test-Fresh (Join-Path $outDir "vignettes\$clip.png") $t0)) { $missV += $clip }
|
||||
if (-not ((Test-Fresh (Join-Path $outDir "planches\$clip.jpg") $t0) -and (Test-Fresh (Join-Path $outDir "planches\$clip.json") $t0))) { $missP += $clip }
|
||||
}
|
||||
Add-Check 'une vignette <Clip>.png par clip' ($missV.Count -eq 0) $(if ($missV) { 'manquantes : ' + (($missV | Select-Object -First 8) -join ', ') })
|
||||
Add-Check 'une planche <Clip>.jpg + .json par clip' ($missP.Count -eq 0) $(if ($missP) { 'manquantes : ' + (($missP | Select-Object -First 8) -join ', ') })
|
||||
}
|
||||
|
||||
function Step-Evaluation {
|
||||
if (-not $script:DryRun -and -not (Test-EditorClosed)) { throw 'editeur ouvert' }
|
||||
$a = (Get-EvalArgs) + " -PoseDir=`"$($script:Work)/pose`" " + (Get-WorkArg 'Out')
|
||||
$code = Invoke-Commandlet 'LPAnimEval' $a
|
||||
Add-Cmd "python Tools\anim\RapportControles.py $(Get-WorkRel)"
|
||||
if (-not $script:DryRun) {
|
||||
$script:St.Code = $code
|
||||
$r = Read-Json (Join-Path $script:Work 'rapport.json')
|
||||
Add-Check 'rapport.json valide (aucun clip rejete)' ($r -and $r.valide -eq $true) $(if ($r -and $r.echecs) { (@($r.echecs) -join '; ') } elseif (-not $r) { 'rapport absent' })
|
||||
if ($r) { foreach ($x in @($r.a_revoir)) { Add-Warn "a revoir : $x" } }
|
||||
if (-not ($r -and $r.valide -eq $true)) { throw 'evaluation en echec' }
|
||||
& python (Join-Path $Root 'Tools\anim\RapportControles.py') $script:Work | Out-Host
|
||||
Add-Check 'rapport.html ecrit' ((Test-Path (Join-Path $script:Work 'rapport.html')) -and $LASTEXITCODE -eq 0)
|
||||
}
|
||||
Invoke-Sandbox
|
||||
}
|
||||
|
||||
function Get-DeliverParams([switch] $CatalogueOnly) {
|
||||
$c = $script:Ch
|
||||
$outDir = Join-Path (Join-Path $Root 'Saved') (($c.sandbox -replace '/', '\'))
|
||||
$p = [ordered]@{
|
||||
Name = $c.mod; Source = $c.sortie.clips; Prefix = $script:Cfg.prefixe; Exclude = (@($script:Cfg.exclus | Where-Object { $_ }) -join ',')
|
||||
Pipeline = (Get-WorkRel); Pack = (Join-Path $Root "AnimSources\$Pack\pack.json")
|
||||
}
|
||||
if ($CatalogueOnly) {
|
||||
$p.Vignettes = Join-Path $outDir 'vignettes'; $p.Planches = Join-Path $outDir 'planches'; $p.CatalogueOnly = $true
|
||||
}
|
||||
$p
|
||||
}
|
||||
function Format-Params($P) { (@($P.GetEnumerator() | ForEach-Object { if ($_.Value -is [bool]) { "-$($_.Key)" } else { "-$($_.Key) '$($_.Value)'" } })) -join ' ' }
|
||||
function Invoke-Deliver($P) {
|
||||
Add-Cmd "Tools\anim\DeliverAnims.ps1 $(Format-Params $P)"
|
||||
if ($script:DryRun) { return 0 }
|
||||
$global:LASTEXITCODE = 0
|
||||
& (Join-Path $Root 'Tools\anim\DeliverAnims.ps1') @P | Out-Host
|
||||
[int] $LASTEXITCODE
|
||||
}
|
||||
|
||||
function Step-Catalogue {
|
||||
$c = $script:Ch
|
||||
$liv = Join-Path $script:Work 'livraison.json'
|
||||
$fic = Join-Path $script:Work 'fiches_calculees.json'
|
||||
$t0 = Get-Date
|
||||
# a. copie des clips dans le mod (ecrit livraison.json)
|
||||
$code = Invoke-Deliver (Get-DeliverParams)
|
||||
if (-not $script:DryRun) {
|
||||
$script:St.Code = $code
|
||||
Add-Check 'livraison (copie dans le mod) : code 0' ($code -eq 0) "code $code"
|
||||
Add-Check 'livraison.json ecrit' (Test-Fresh $liv $t0)
|
||||
if ($code -ne 0 -or -not (Test-Fresh $liv $t0)) { throw 'livraison en echec' }
|
||||
if (-not (Test-EditorClosed)) { throw 'editeur ouvert' }
|
||||
}
|
||||
# b. fiches calculees
|
||||
$code = Invoke-Commandlet 'LPAnimCatalog' ('-Livraison="{0}" -Out="{1}"' -f "$($script:Work)/livraison.json", $script:Work)
|
||||
if (-not $script:DryRun) {
|
||||
$f = Read-Json $fic
|
||||
$newer = (Test-Path $fic) -and (Test-Path $liv) -and ((Get-Item $fic).LastWriteTime -gt (Get-Item $liv).LastWriteTime)
|
||||
Add-Check 'fiches_calculees.json plus recent que livraison.json' $newer
|
||||
Add-Check 'fiches calculees sans echec' ($f -and @($f.echecs).Count -eq 0 -and $code -eq 0) $(if ($f -and $f.echecs) { (@($f.echecs) -join '; ') } else { "code $code" })
|
||||
if (-not $newer) { throw 'fiches non calculees' }
|
||||
}
|
||||
# c. catalogue (Catalogue.py)
|
||||
$code = Invoke-Deliver (Get-DeliverParams -CatalogueOnly)
|
||||
if (-not $script:DryRun) {
|
||||
$r = Read-Json (Join-Path $script:Work 'catalogue_rapport.json')
|
||||
$cat = Join-Path $Root "ModSources\$($c.mod)\Module\Animations\catalogue.json"
|
||||
Add-Check 'Catalogue.py : code 0' ($code -eq 0) "code $code"
|
||||
Add-Check 'catalogue_rapport.json ok' ($r -and $r.ok -eq $true) $(if ($r) { "$($r.fiches) fiche(s)" })
|
||||
Add-Check 'catalogue.json dans le mod' (Test-Path $cat) (Show-Rel $cat)
|
||||
}
|
||||
}
|
||||
|
||||
function Step-Livraison {
|
||||
$c = $script:Ch
|
||||
$version = (Select-String -Path (Join-Path $Root 'Config\DefaultGame.ini') -Pattern '^ProjectVersion=(.+)$' | Select-Object -First 1).Matches[0].Groups[1].Value.Trim()
|
||||
$release = Join-Path $Root "Releases\$version\Windows\AssetRegistry.bin"
|
||||
$log = "$($script:Work)/etapes/7_livraison_cuisson.log"
|
||||
if ($SansCuisson) {
|
||||
$script:St.Skipped = $true
|
||||
Write-Host ' -SansCuisson : cuisson DLC sautee'
|
||||
return
|
||||
}
|
||||
if (-not (Test-Path $release)) {
|
||||
Add-Cmd "(saute) Tools\BuildMod.ps1 -Name $($c.mod) version de reference absente : $(Show-Rel $release)"
|
||||
Add-Warn 'version de reference absente : cuisson DLC non faite'
|
||||
return
|
||||
}
|
||||
Add-Cmd "$(Split-Path $Pwsh -Leaf) -NoProfile -File Tools\BuildMod.ps1 -Name $($c.mod) > $(Show-Rel $log) (version de reference $version)"
|
||||
if ($script:DryRun) { return }
|
||||
New-Item -ItemType Directory -Force (Split-Path $log -Parent) | Out-Null
|
||||
& $Pwsh -NoProfile -ExecutionPolicy Bypass -File (Join-Path $Root 'Tools\BuildMod.ps1') -Name $c.mod *>&1 | Tee-Object -FilePath $log | Out-Host
|
||||
$script:St.Code = $LASTEXITCODE
|
||||
$text = Get-Content $log -Raw -Encoding utf8
|
||||
$m = [regex]::Match($text, 'Archive : (.+?\.zip) \(')
|
||||
Add-Check 'BuildMod : code 0' ($script:St.Code -eq 0) "code $($script:St.Code)"
|
||||
Add-Check 'archive .zip produite' ($m.Success -and (Test-Path $m.Groups[1].Value)) $(if ($m.Success) { $m.Groups[1].Value })
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- moteur
|
||||
function Save-Etat {
|
||||
if ($script:DryRun) { return }
|
||||
$script:Etat.maj = Get-Date -Format s
|
||||
Write-Text (Join-Path $script:Work 'etat.json') (($script:Etat | ConvertTo-Json -Depth 10) + "`r`n")
|
||||
}
|
||||
function Move-Previous {
|
||||
$w = $script:Work
|
||||
$keep = @(Get-ChildItem $w -Force -ErrorAction SilentlyContinue | Where-Object { -not ($_.PSIsContainer -and $_.Name -like 'precedent_*') })
|
||||
if (-not $keep.Count) { return }
|
||||
$dest = Join-Path $w ("precedent_{0}" -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
|
||||
New-Item -ItemType Directory -Force $dest | Out-Null
|
||||
foreach ($k in $keep) { Move-Item -LiteralPath $k.FullName -Destination $dest }
|
||||
Write-Host " resultat precedent deplace : $($keep.Count) element(s) -> $(Show-Rel $dest)"
|
||||
}
|
||||
function Invoke-Step([int] $N, [scriptblock] $Block) {
|
||||
if ($N -lt $From -or $N -gt $To) { return }
|
||||
$name = $StepNames[$N]
|
||||
Write-Host ''
|
||||
Write-Host "== Etape $N : $name"
|
||||
$script:St = [ordered]@{ N = $N; Cmds = @(); Checks = @(); Warns = @(); Extra = [ordered]@{}; Code = $null; Skipped = $false }
|
||||
$t0 = Get-Date
|
||||
try { $null = & $Block }
|
||||
catch {
|
||||
$msg = $_.Exception.Message
|
||||
$script:St.Checks += [ordered]@{ nom = 'erreur'; ok = $false; detail = $msg }
|
||||
if ($script:DryRun) { $script:Planned += "etape $N : $msg"; Write-Host " [ECHEC PREVU] $msg" -ForegroundColor Yellow }
|
||||
else { Write-Host " [ECHEC] $msg" -ForegroundColor Red }
|
||||
}
|
||||
$bad = @($script:St.Checks | Where-Object { $_.ok -eq $false })
|
||||
$verdict = if ($bad.Count) { 'echec' } elseif ($script:St.Skipped) { 'saute' } elseif ($script:St.Warns.Count) { 'avertissement' } else { 'ok' }
|
||||
if ($script:DryRun) { return $(if ($N -eq 0 -and -not $script:Ch) { 'stop' }) }
|
||||
if (-not $script:Work) { throw "Etape $N ($name) en echec : $((@($bad | ForEach-Object { $_.detail })) -join ' ; ')" }
|
||||
# Passage complet : l'ancien resultat est deplace une fois l'inventaire accepte (un refus ne touche a rien).
|
||||
$recName = "${N}_$name"
|
||||
if ($N -eq 0 -and $From -eq 0) {
|
||||
if ($bad.Count) { $recName = "${N}_${name}_refus_$(Get-Date -Format 'yyyyMMdd-HHmmss')" } else { Move-Previous }
|
||||
}
|
||||
$t1 = Get-Date
|
||||
$rec = [ordered]@{
|
||||
etape = $N; nom = $name; debut = $t0.ToString('s'); fin = $t1.ToString('s'); duree_s = [int] ($t1 - $t0).TotalSeconds
|
||||
commandes = $script:St.Cmds; code = $script:St.Code; controles = $script:St.Checks; avertissements = $script:St.Warns; verdict = $verdict
|
||||
}
|
||||
foreach ($k in $script:St.Extra.Keys) { $rec[$k] = $script:St.Extra[$k] }
|
||||
Write-Text (Join-Path $script:Work "etapes\$recName.json") (($rec | ConvertTo-Json -Depth 10) + "`r`n")
|
||||
$script:Etat.etapes["$N"] = [ordered]@{ nom = $name; verdict = $verdict; fin = $t1.ToString('s') }
|
||||
Save-Etat
|
||||
Write-Host " verdict : $verdict ($($rec.duree_s) s)"
|
||||
if ($verdict -eq 'echec') {
|
||||
throw "Etape $N ($name) en echec : " + ((@($bad | ForEach-Object { "$($_.nom)$(if ($_.detail) { " ($($_.detail))" })" })) -join ' ; ')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if ($Pack -notmatch '^[A-Za-z0-9_-]+$') { throw "nom de pack invalide : $Pack" }
|
||||
if ($From -gt $To) { throw "-From ($From) apres -To ($To)" }
|
||||
Write-Host "Pilote AnimPipeline : pack $Pack, etapes $From a $To$(if ($DryRun) { ' (A BLANC : rien lance, rien cree)' })"
|
||||
|
||||
# Le bloc chaine est lu des l'etape 0 ; pour -From > 0 il faut le charger sans rejouer l'inventaire.
|
||||
if ($From -gt 0) {
|
||||
$script:Cfg = Read-Json (Join-Path $Root "AnimSources\$Pack\pack.json")
|
||||
if (-not $script:Cfg -or -not $script:Cfg.chaine) { throw "pack.json illisible ou sans bloc chaine (AnimSources\$Pack)" }
|
||||
$script:Ch = $script:Cfg.chaine
|
||||
$script:Work = (Join-Path $Root ($script:Ch.travail -replace '/', '\')).TrimEnd('\') -replace '\\', '/'
|
||||
$prev = if (Test-Path (Join-Path $script:Work 'etat.json')) { Get-Content (Join-Path $script:Work 'etat.json') -Raw -Encoding utf8 | ConvertFrom-Json -AsHashtable } else { $null }
|
||||
if ($prev) { $script:Etat = $prev; $script:PrevEtat = $prev; if ($prev.methode) { $script:Method = [string] $prev.methode } }
|
||||
if (-not $script:Method -and $script:Ch.methode -and $script:Ch.methode -ne 'auto') { $script:Method = [string] $script:Ch.methode }
|
||||
for ($i = 0; $i -lt $From; $i++) {
|
||||
$e = if ($prev -and $prev.etapes) { $prev.etapes["$i"] } else { $null }
|
||||
if (-not $e -or $e.verdict -notin 'ok', 'saute', 'avertissement') {
|
||||
if ($DryRun) { Write-Host " (etape $i : pas de verdict enregistre)" } else { Write-Warning "etape $i : pas de verdict enregistre dans etat.json (reprise sur la foi de l'utilisateur)" }
|
||||
}
|
||||
}
|
||||
if ($From -ge 3 -and -not $script:Method -and -not $DryRun) { throw "methode inconnue (etat.json sans methode et pack.json = auto) : reprendre depuis l'etape 2" }
|
||||
$script:Clips = Get-SourceClips
|
||||
}
|
||||
else {
|
||||
# Passage complet : etat precedent lu avant tout deplacement.
|
||||
$cfgPeek = Read-Json (Join-Path $Root "AnimSources\$Pack\pack.json")
|
||||
if ($cfgPeek -and $cfgPeek.chaine.travail) {
|
||||
$w = (Join-Path $Root ($cfgPeek.chaine.travail -replace '/', '\')).TrimEnd('\')
|
||||
$ef = Join-Path $w 'etat.json'
|
||||
if (Test-Path $ef) { $script:PrevEtat = Get-Content $ef -Raw -Encoding utf8 | ConvertFrom-Json -AsHashtable }
|
||||
$keep = @(Get-ChildItem $w -Force -ErrorAction SilentlyContinue | Where-Object { -not ($_.PSIsContainer -and $_.Name -like 'precedent_*') })
|
||||
if ($keep.Count) { Write-Host "Resultat precedent : $($keep.Count) element(s) de $(Show-Rel $w) a deplacer dans $(Show-Rel $w)\precedent_<date> apres l'inventaire accepte" }
|
||||
}
|
||||
}
|
||||
|
||||
$stop = $null
|
||||
$stop = Invoke-Step 0 { Step-Inventaire }
|
||||
if ($stop -eq 'stop') { throw 'plan interrompu : pack.json illisible ou sans bloc chaine' }
|
||||
Invoke-Step 1 { Step-Import }
|
||||
Invoke-Step 2 { Step-Identification }
|
||||
Invoke-Step 3 { Step-Calque }
|
||||
Invoke-Step 4 { Step-Conversion }
|
||||
Invoke-Step 5 { Step-Evaluation }
|
||||
Invoke-Step 6 { Step-Catalogue }
|
||||
Invoke-Step 7 { Step-Livraison }
|
||||
|
||||
Write-Host ''
|
||||
if ($DryRun) {
|
||||
if ($script:Planned.Count) {
|
||||
Write-Host "A blanc : plan complet affiche ; $($script:Planned.Count) echec(s) prevu(s) :" -ForegroundColor Yellow
|
||||
$script:Planned | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
|
||||
}
|
||||
else { Write-Host 'A blanc : plan complet affiche, aucun echec prevu' }
|
||||
}
|
||||
else { Write-Host "Chaine terminee : pack $Pack, etapes $From a $To (rapports : $(Get-WorkRel)\etapes)" }
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
Write-Host "ECHEC : $($_.Exception.Message)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -1,463 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Catalogue des animations d'un mod (lot AP6) : catalogue.json, catalogue.html, vignettes.
|
||||
|
||||
Entrees : pack.json (--pack), livraison.json + fiches_calculees.json + rapport.json + csv de qualite (dossier --pipeline),
|
||||
vignettes <Clip>.png (--vignettes, carres 512 px, defaut Saved/Measurement/vignettes), planches <Clip>.jpg + .json
|
||||
(--planches, defaut Saved/Measurement/planches ; bande de N images carrees de 256 px, animee au survol),
|
||||
clips du mod ModSources/<mod>/Content/Anims (ou --mod-dir). Collisions de cles : jeu de base, scenarios du jeu et
|
||||
catalogues de tous les autres mods de ModSources/*/Module/Animations/catalogue.json.
|
||||
Controles bloquants : code de sortie 1 et liste des causes ; rien n'est ecrit dans --out en cas d'echec.
|
||||
Python 3 standard, sans dependance.
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import datetime
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
BASE_KEYS = ["martialart", "sneak", "dance", "smoke", "write", "drinkbottle", "drinkglass", "drinktea",
|
||||
"camera", "call", "browse", "makeup", "eat", "sweep", "fightlost", "gun", "knockedout"]
|
||||
TRIAL_MODULE = "lpr_anim_essai"
|
||||
SENS_FR = {"sur place": "sur place", "avant": "avance", "arriere": "recule", "cote": "de côté"}
|
||||
|
||||
|
||||
def read_json(path):
|
||||
with open(path, encoding="utf-8-sig") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def sha256_of(paths):
|
||||
h = hashlib.sha256()
|
||||
for p in paths:
|
||||
with open(p, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(1 << 20), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest().upper()
|
||||
|
||||
|
||||
def read_csv_rows(path):
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
with open(path, encoding="utf-8-sig", newline="") as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def num(v):
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def game_keys(root):
|
||||
"""Cles deja utilisees par le jeu (hors module d'essai) : ANIMATION: <cle> et animate(<cle>)."""
|
||||
keys = set()
|
||||
mods = os.path.join(root, "Content", "Modules")
|
||||
pat = re.compile(r"ANIMATION:\s*([A-Za-z0-9_]+)|animate\(\s*([A-Za-z0-9_]+)\s*\)")
|
||||
for dp, dn, fn in os.walk(mods):
|
||||
rel = os.path.relpath(dp, mods).replace("\\", "/")
|
||||
if rel.split("/")[0] == TRIAL_MODULE:
|
||||
dn[:] = []
|
||||
continue
|
||||
for name in fn:
|
||||
if name.lower().endswith((".lpaction", ".lpscene")):
|
||||
try:
|
||||
with open(os.path.join(dp, name), encoding="utf-8-sig", errors="replace") as f:
|
||||
text = f.read()
|
||||
except OSError:
|
||||
continue
|
||||
for m in pat.finditer(text):
|
||||
keys.add((m.group(1) or m.group(2)).lower())
|
||||
return keys
|
||||
|
||||
|
||||
def other_catalogue_keys(root, mod, out_dir):
|
||||
keys = {}
|
||||
base = os.path.join(root, "ModSources")
|
||||
if not os.path.isdir(base):
|
||||
return keys
|
||||
own = os.path.normcase(os.path.abspath(os.path.join(out_dir, "catalogue.json")))
|
||||
for d in sorted(os.listdir(base)):
|
||||
p = os.path.join(base, d, "Module", "Animations", "catalogue.json")
|
||||
if d == mod or not os.path.isfile(p) or os.path.normcase(os.path.abspath(p)) == own:
|
||||
continue
|
||||
try:
|
||||
for a in read_json(p).get("animations", []):
|
||||
keys[str(a.get("cle", "")).lower()] = d
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
return keys
|
||||
|
||||
|
||||
def build(args, errors):
|
||||
root = os.path.abspath(args.root)
|
||||
pipe = os.path.abspath(args.pipeline)
|
||||
pack_path = os.path.abspath(args.pack)
|
||||
out = os.path.abspath(args.out)
|
||||
mod_dir = os.path.abspath(args.mod_dir) if args.mod_dir else os.path.join(root, "ModSources", args.mod)
|
||||
vign = os.path.abspath(args.vignettes) if args.vignettes else os.path.join(root, "Saved", "Measurement", "vignettes")
|
||||
plan = os.path.abspath(args.planches) if args.planches else os.path.join(root, "Saved", "Measurement", "planches")
|
||||
|
||||
def load(path, what):
|
||||
if not os.path.isfile(path):
|
||||
errors.append("%s introuvable : %s" % (what, path))
|
||||
return None
|
||||
try:
|
||||
return read_json(path)
|
||||
except ValueError as e:
|
||||
errors.append("%s illisible : %s (%s)" % (what, path, e))
|
||||
return None
|
||||
|
||||
pack = load(pack_path, "pack.json")
|
||||
liv_path = os.path.join(pipe, "livraison.json")
|
||||
fic_path = os.path.join(pipe, "fiches_calculees.json")
|
||||
liv = load(liv_path, "livraison.json")
|
||||
fic = load(fic_path, "fiches_calculees.json")
|
||||
rap = load(os.path.join(pipe, "rapport.json"), "rapport.json")
|
||||
if None in (pack, liv, fic, rap):
|
||||
return None
|
||||
|
||||
# Rapport et fiches calculees.
|
||||
if rap.get("valide") is not True:
|
||||
errors.append("rapport.json non valide : %s" % "; ".join(map(str, rap.get("echecs") or [])))
|
||||
if fic.get("echecs"):
|
||||
errors.append("fiches_calculees.json contient des echecs : %s" % "; ".join(map(str, fic["echecs"])))
|
||||
if os.path.getmtime(fic_path) <= os.path.getmtime(liv_path):
|
||||
errors.append("fiches_calculees.json (%s) n'est pas plus recent que livraison.json (%s) : "
|
||||
"le catalogue ne decrit pas les clips livres"
|
||||
% (datetime.datetime.fromtimestamp(os.path.getmtime(fic_path)).isoformat(" ", "seconds"),
|
||||
datetime.datetime.fromtimestamp(os.path.getmtime(liv_path)).isoformat(" ", "seconds")))
|
||||
fiches = fic.get("fiches", {})
|
||||
declared = pack.get("animations", {})
|
||||
|
||||
# Cles : uniques, sans collision.
|
||||
keys = [str(e.get("cle", "")) for e in liv]
|
||||
seen = set()
|
||||
for k in keys:
|
||||
if k in seen:
|
||||
errors.append("cle en double dans livraison.json : %s" % k)
|
||||
seen.add(k)
|
||||
taken = {k: "jeu de base" for k in BASE_KEYS}
|
||||
for k in game_keys(root):
|
||||
taken.setdefault(k, "scenario du jeu")
|
||||
for k, d in other_catalogue_keys(root, args.mod, out).items():
|
||||
taken.setdefault(k, "catalogue de %s" % d)
|
||||
for k in keys:
|
||||
if k.lower() in taken:
|
||||
errors.append("collision de cle : %s deja utilisee (%s)" % (k, taken[k.lower()]))
|
||||
|
||||
# Qualite.
|
||||
a_revoir = {}
|
||||
for line in rap.get("a_revoir") or []:
|
||||
parts = [p.strip() for p in str(line).split(" : ", 2)]
|
||||
if len(parts) == 3:
|
||||
a_revoir.setdefault(parts[1], []).append("%s : %s" % (parts[0], parts[2]))
|
||||
qual = {}
|
||||
for corps in ("Bridget", "Michael"):
|
||||
for r in read_csv_rows(os.path.join(pipe, "eval_%s.csv" % corps)):
|
||||
q = qual.setdefault(r["clip"], {"brut": 0.0, "torsion": 0.0})
|
||||
q["brut"] = max(q["brut"], num(r.get("brut_pire")))
|
||||
q["torsion"] = max(q["torsion"], num(r.get("torsion_deg")))
|
||||
|
||||
animations = []
|
||||
thumbs = []
|
||||
for e in liv:
|
||||
k = str(e.get("cle", ""))
|
||||
d = declared.get(k)
|
||||
f = fiches.get(k)
|
||||
if d is None:
|
||||
errors.append("%s : absente de pack.json" % k)
|
||||
if f is None:
|
||||
errors.append("%s : fiche calculee absente" % k)
|
||||
if d is None or f is None:
|
||||
continue
|
||||
clip = d.get("clip", "")
|
||||
miss = []
|
||||
for name, val in (("clip", clip), ("nom.fr", (d.get("nom") or {}).get("fr")), ("nom.en", (d.get("nom") or {}).get("en")),
|
||||
("categorie", d.get("categorie")), ("style", d.get("style")),
|
||||
("usage.fr", (d.get("usage") or {}).get("fr")), ("usage.en", (d.get("usage") or {}).get("en"))):
|
||||
if not val:
|
||||
miss.append(name)
|
||||
for name in ("images", "cadence", "duree_s", "boucle", "deplacement_cm", "vitesse_cm_s", "deplacement_sens",
|
||||
"posture", "pieds_au_sol_pct", "mains"):
|
||||
if f.get(name) in (None, "", {}):
|
||||
miss.append("fiche." + name)
|
||||
for name in ("source", "licence", "credit"):
|
||||
if not pack.get({"source": "source", "licence": "licence", "credit": "credit"}[name]):
|
||||
miss.append("pack." + name)
|
||||
for name in ("asset_F", "asset_M"):
|
||||
if not e.get(name):
|
||||
miss.append("livraison." + name)
|
||||
if miss:
|
||||
errors.append("%s : champs obligatoires vides : %s" % (k, ", ".join(miss)))
|
||||
thumb = os.path.join(vign, "%s.png" % clip)
|
||||
if not clip or not os.path.isfile(thumb):
|
||||
errors.append("%s : vignette absente (%s)" % (k, thumb))
|
||||
pj = os.path.join(plan, "%s.jpg" % clip)
|
||||
pm = os.path.join(plan, "%s.json" % clip)
|
||||
pinfo = None
|
||||
if not clip or not os.path.isfile(pj):
|
||||
errors.append("%s : planche absente (%s)" % (k, pj))
|
||||
elif not os.path.isfile(pm):
|
||||
errors.append("%s : description de planche absente (%s)" % (k, pm))
|
||||
else:
|
||||
try:
|
||||
pinfo = read_json(pm)
|
||||
n_img = pinfo["images"]
|
||||
if isinstance(n_img, bool) or not isinstance(n_img, int) or n_img < 2:
|
||||
errors.append("%s : planche invalide, images = %r (au moins 2 attendues) : %s" % (k, n_img, pm))
|
||||
pinfo = None
|
||||
elif pinfo.get("taille") != 256:
|
||||
errors.append("%s : planche invalide, taille = %r (256 attendu) : %s" % (k, pinfo.get("taille"), pm))
|
||||
pinfo = None
|
||||
except (ValueError, KeyError, TypeError, AttributeError) as e:
|
||||
errors.append("%s : description de planche illisible : %s (%r)" % (k, pm, e))
|
||||
pinfo = None
|
||||
files = [os.path.join(mod_dir, "Content", "Anims", "Female", k + "_F.uasset"),
|
||||
os.path.join(mod_dir, "Content", "Anims", "Male", k + "_M.uasset")]
|
||||
for p in files:
|
||||
if not os.path.isfile(p):
|
||||
errors.append("%s : clip du mod absent : %s" % (k, p))
|
||||
if miss or not os.path.isfile(files[0]) or not os.path.isfile(files[1]) or not os.path.isfile(thumb) or pinfo is None:
|
||||
continue # causes deja listees ci-dessus
|
||||
|
||||
tf, tm = os.path.getsize(files[0]), os.path.getsize(files[1])
|
||||
motifs = a_revoir.get(clip, [])
|
||||
q = qual.get(clip, {"brut": 0.0, "torsion": 0.0})
|
||||
decl = d.get("posture")
|
||||
if decl is not None and (not isinstance(decl, str) or not decl.strip()):
|
||||
errors.append("%s : posture declaree invalide dans pack.json" % k)
|
||||
continue
|
||||
posture = {"principale": decl if decl else f["posture"],
|
||||
"posture_origine": "declaree" if decl else "calculee"}
|
||||
if decl:
|
||||
posture["posture_calculee"] = f["posture"]
|
||||
if f.get("posture_debut") != f.get("posture_fin"):
|
||||
posture["debut"], posture["fin"] = f.get("posture_debut"), f.get("posture_fin")
|
||||
# Hauteur d'assise seulement pour une posture assise (une chute traverse des images "assis").
|
||||
if f.get("hauteur_assise_cm") is not None and posture["principale"] == "assis":
|
||||
posture["hauteur_assise_cm"] = f["hauteur_assise_cm"]
|
||||
dur_p = float(f["duree_s"]) or num(pinfo.get("duree_s"))
|
||||
if dur_p <= 0:
|
||||
errors.append("%s : duree nulle, cadence de planche incalculable" % k)
|
||||
continue
|
||||
animations.append({
|
||||
"cle": k,
|
||||
"appel": {"script": "Actor.animate(%s)" % k, "action": "ANIMATION: %s" % k},
|
||||
"nom": {"fr": d["nom"]["fr"], "en": d["nom"]["en"]},
|
||||
"assets": {"feminin": e["asset_F"], "masculin": e["asset_M"]},
|
||||
"duree_s": round(float(f["duree_s"]), 2),
|
||||
"images": f["images"],
|
||||
"planche": {"fichier": "planches/%s.jpg" % k, "images": pinfo["images"], "taille": 256,
|
||||
"ips": round(pinfo["images"] / dur_p, 1)},
|
||||
"cadence": f["cadence"],
|
||||
"taille_ko": {"feminin": round(tf / 1024), "masculin": round(tm / 1024), "total": round((tf + tm) / 1024)},
|
||||
"taille_cuite": False,
|
||||
"boucle": {"oui": bool(f["boucle"]), "ecart_pct": round(f.get("boucle_ecart_pct") or 0.0, 2)},
|
||||
"deplacement": {"cm": f["deplacement_cm"], "cm_s": f["vitesse_cm_s"], "sens": f["deplacement_sens"]},
|
||||
"posture": posture,
|
||||
"contacts": {"pieds_au_sol_pct": f["pieds_au_sol_pct"], "mains": f["mains"]},
|
||||
"accessoire": d.get("accessoire", ""),
|
||||
"categorie": d["categorie"],
|
||||
"style": d["style"],
|
||||
"usage": {"fr": d["usage"]["fr"], "en": d["usage"]["en"]},
|
||||
"corps": ["feminin", "masculin"],
|
||||
"qualite": {"verdict": "à revoir" if motifs else "accepté", "motifs": motifs,
|
||||
"pire_brut_pct": round(q["brut"], 2), "pire_torsion_deg": round(q["torsion"], 2)},
|
||||
"source": pack.get("source", ""),
|
||||
"licence": pack.get("licence", ""),
|
||||
"credit": pack.get("credit", ""),
|
||||
"version": args.version,
|
||||
"empreinte": sha256_of(files),
|
||||
})
|
||||
thumbs.append((k, thumb, pj))
|
||||
if errors:
|
||||
return None
|
||||
header = {
|
||||
"pack": pack.get("pack", ""), "mod": args.mod, "version": args.version,
|
||||
"genere": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||
"nombre_animations": len(animations), "credit": pack.get("credit", ""),
|
||||
"animations": animations,
|
||||
}
|
||||
return header, thumbs, out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------- HTML
|
||||
|
||||
CSS = """
|
||||
:root{--bg:#f6f7f9;--fg:#1d2330;--mut:#5d6677;--card:#fff;--bd:#d9dde5;--acc:#2057c9;--ok:#1d7a3f;--warn:#a85b00;--chip:#eef1f6}
|
||||
@media (prefers-color-scheme:dark){:root{--bg:#14171d;--fg:#e6e9ef;--mut:#9aa3b4;--card:#1d222b;--bd:#323a48;--acc:#7aa7ff;--ok:#5fcf8a;--warn:#f0a850;--chip:#262c37}}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.45 system-ui,Segoe UI,sans-serif;overflow-wrap:anywhere}
|
||||
header{padding:16px;max-width:1200px;margin:0 auto}
|
||||
h1{margin:0 0 4px;font-size:1.4rem}
|
||||
.sub{color:var(--mut);font-size:.9rem}
|
||||
.bar{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
|
||||
.bar input,.bar select{font:inherit;padding:6px 8px;border:1px solid var(--bd);border-radius:6px;background:var(--card);color:var(--fg);min-width:0;max-width:100%}
|
||||
.bar input{flex:1 1 220px}
|
||||
main{max-width:1200px;margin:0 auto;padding:0 16px 24px;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr));gap:12px}
|
||||
.card{background:var(--card);border:1px solid var(--bd);border-radius:10px;overflow:hidden;display:flex;flex-direction:column}
|
||||
.card[hidden],.card img[hidden],.card .anim[hidden]{display:none}
|
||||
.card img{width:100%;aspect-ratio:1/1;object-fit:contain;background:var(--chip);display:block}
|
||||
.card .anim{width:100%;aspect-ratio:1/1;background-color:var(--chip);background-repeat:no-repeat}
|
||||
.in{padding:10px 12px;display:flex;flex-direction:column;gap:6px}
|
||||
.in h2{margin:0;font-size:1.05rem}
|
||||
.key{font-family:Consolas,monospace;background:var(--chip);border:1px solid var(--bd);border-radius:6px;padding:3px 6px;cursor:pointer;text-align:left;color:var(--fg);font-size:.85rem}
|
||||
.call{font-family:Consolas,monospace;font-size:.8rem;color:var(--mut)}
|
||||
.tags{display:flex;flex-wrap:wrap;gap:4px}
|
||||
.tags span{background:var(--chip);border-radius:10px;padding:1px 8px;font-size:.78rem}
|
||||
.use{font-size:.88rem}
|
||||
.q-ok{color:var(--ok)}.q-rev{color:var(--warn);font-weight:600}
|
||||
footer{max-width:1200px;margin:0 auto;padding:0 16px 24px;color:var(--mut);font-size:.8rem}
|
||||
#none{display:none;padding:16px;max-width:1200px;margin:0 auto}
|
||||
"""
|
||||
|
||||
JS = """
|
||||
const A=JSON.parse(document.getElementById('data').textContent).animations;
|
||||
const $=id=>document.getElementById(id);
|
||||
const cap=s=>s.charAt(0).toUpperCase()+s.slice(1);
|
||||
function dur(a){return a.duree_s<2?'courte':(a.duree_s>5?'longue':'moyenne')}
|
||||
function fill(sel,vals){[...new Set(vals)].sort().forEach(v=>{const o=document.createElement('option');o.value=v;o.textContent=v;sel.appendChild(o)})}
|
||||
fill($('fcat'),A.map(a=>a.categorie));fill($('fpos'),A.map(a=>a.posture.principale));
|
||||
const SENS={'sur place':'sur place','avant':'avance','arriere':'recule','cote':'de côté'};
|
||||
function esc(s){return String(s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]))}
|
||||
function card(a){
|
||||
const dp=a.deplacement,p=a.posture;
|
||||
let pos=p.principale;if(p.debut)pos+=' ('+p.debut+' → '+p.fin+')';if(p.hauteur_assise_cm!=null)pos+=', assise '+p.hauteur_assise_cm+' cm';
|
||||
const mv=dp.sens==='sur place'?'sur place':(SENS[dp.sens]||dp.sens)+' '+Math.round(dp.cm)+' cm ('+Math.round(dp.cm_s)+' cm/s)';
|
||||
const q=a.qualite,qc=q.verdict==='accepté'?'q-ok':'q-rev';
|
||||
const el=document.createElement('article');el.className='card';
|
||||
el.innerHTML='<img loading="lazy" src="vignettes/'+esc(a.cle)+'.png" alt="'+esc(a.nom.fr)+'">'
|
||||
+'<div class="in"><h2>'+esc(a.nom.fr)+' <span class="sub">/ '+esc(a.nom.en)+'</span></h2>'
|
||||
+'<button class="key" type="button" title="Copier la clé">'+esc(a.cle)+'</button>'
|
||||
+'<div class="call">'+esc(a.appel.script)+'<br>'+esc(a.appel.action)+'</div>'
|
||||
+'<div class="tags"><span>'+esc(a.categorie)+'</span><span>'+esc(pos)+'</span><span>'+a.duree_s.toString().replace('.',',')+' s ('+dur(a)+')</span>'
|
||||
+'<span>'+(a.boucle.oui?'boucle':'une fois')+'</span><span>'+esc(mv)+'</span>'+(a.accessoire?'<span>'+esc(a.accessoire)+'</span>':'')+'</div>'
|
||||
+'<div class="use">'+esc(a.usage.fr)+'</div>'
|
||||
+'<div class="'+qc+'">Qualité : '+esc(q.verdict)+(q.motifs.length?' ('+esc(q.motifs.join(' ; '))+')':'')+'</div></div>';
|
||||
hover(el,a);
|
||||
const b=el.querySelector('.key');
|
||||
b.onclick=()=>{const done=()=>{b.textContent='copié';setTimeout(()=>b.textContent=a.cle,900)};
|
||||
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(a.cle).then(done,()=>sel(b))}else sel(b)};
|
||||
return el}
|
||||
const reduce=matchMedia('(prefers-reduced-motion: reduce)');
|
||||
function hover(el,a){
|
||||
const pl=a.planche,img=el.querySelector('img');let div=null,timer=null,ready=false,want=false,last='mouse';
|
||||
function show(){if(!want||!ready)return;img.hidden=true;div.hidden=false;let i=0;div.style.backgroundPosition='0% 0%';
|
||||
clearInterval(timer);timer=setInterval(()=>{i=(i+1)%pl.images;div.style.backgroundPosition=(i*100/(pl.images-1))+'% 0%'},1000/pl.ips)}
|
||||
function start(){want=true;
|
||||
if(!div){div=document.createElement('div');div.className='anim';div.hidden=true;div.setAttribute('role','img');div.setAttribute('aria-label',a.nom.fr);
|
||||
div.style.backgroundSize=(pl.images*100)+'% 100%';img.parentNode.insertBefore(div,img);
|
||||
const im=new Image();im.onload=()=>{div.style.backgroundImage='url("'+pl.fichier+'")';ready=true;show()};im.src=pl.fichier}
|
||||
else show()}
|
||||
function stop(){want=false;clearInterval(timer);timer=null;if(div)div.hidden=true;img.hidden=false}
|
||||
el.addEventListener('pointerenter',e=>{if(e.pointerType!=='touch'&&!reduce.matches)start()});
|
||||
el.addEventListener('pointerleave',e=>{if(e.pointerType!=='touch')stop()});
|
||||
el.addEventListener('pointerdown',e=>{last=e.pointerType});
|
||||
el.addEventListener('click',e=>{if(!e.target.closest('img,.anim'))return;if(last==='touch'||reduce.matches){want?stop():start()}})}
|
||||
function sel(b){const r=document.createRange();r.selectNodeContents(b);const s=getSelection();s.removeAllRanges();s.addRange(r)}
|
||||
const main=$('list'),cards=A.map(a=>({a,el:card(a)}));cards.forEach(c=>main.appendChild(c.el));
|
||||
function apply(){
|
||||
const t=$('q').value.trim().toLowerCase(),c=$('fcat').value,p=$('fpos').value,d=$('fdur').value;let n=0;
|
||||
cards.forEach(x=>{const a=x.a;
|
||||
const hay=(a.cle+' '+a.nom.fr+' '+a.nom.en+' '+a.usage.fr+' '+a.usage.en+' '+a.accessoire+' '+a.style).toLowerCase();
|
||||
const ok=(!t||hay.includes(t))&&(!c||a.categorie===c)&&(!p||a.posture.principale===p)&&(!d||dur(a)===d);
|
||||
x.el.hidden=!ok;if(ok)n++});
|
||||
$('count').textContent=n+' / '+A.length+' animation(s)';$('none').style.display=n?'none':'block'}
|
||||
['q','fcat','fpos','fdur'].forEach(i=>$(i).addEventListener('input',apply));apply();
|
||||
"""
|
||||
|
||||
|
||||
def render_html(cat):
|
||||
data = json.dumps(cat, ensure_ascii=False).replace("</", "<\\/")
|
||||
title = "Animations — %s" % cat["mod"]
|
||||
return """<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>%(title)s</title>
|
||||
<style>%(css)s</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>%(title)s</h1>
|
||||
<div class="sub">Version %(version)s — %(n)d animations — généré le %(date)s. Copiez la clé puis écrivez <code>Actor.animate(clé)</code> dans une scène ou <code>ANIMATION: clé</code> dans une action. Survolez une vignette pour voir le mouvement.</div>
|
||||
<div class="bar">
|
||||
<input id="q" type="search" placeholder="Rechercher (nom, clé, usage…)" aria-label="Recherche">
|
||||
<select id="fcat" aria-label="Catégorie"><option value="">Toutes catégories</option></select>
|
||||
<select id="fpos" aria-label="Posture"><option value="">Toutes postures</option></select>
|
||||
<select id="fdur" aria-label="Durée"><option value="">Toutes durées</option><option value="courte">Courte (< 2 s)</option><option value="moyenne">Moyenne (2 à 5 s)</option><option value="longue">Longue (> 5 s)</option></select>
|
||||
</div>
|
||||
<div id="count" class="sub" style="margin-top:8px"></div>
|
||||
</header>
|
||||
<div id="none">Aucune animation ne correspond aux filtres.</div>
|
||||
<main id="list"></main>
|
||||
<footer>%(credit)s</footer>
|
||||
<script id="data" type="application/json">%(data)s</script>
|
||||
<script>%(js)s</script>
|
||||
</body>
|
||||
</html>
|
||||
""" % {"title": html.escape(title), "css": CSS, "js": JS, "data": data, "version": html.escape(cat["version"]),
|
||||
"n": cat["nombre_animations"], "date": html.escape(cat["genere"][:10]), "credit": html.escape(cat["credit"])}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--pack", required=True)
|
||||
ap.add_argument("--pipeline", required=True)
|
||||
ap.add_argument("--mod", required=True)
|
||||
ap.add_argument("--version", required=True)
|
||||
ap.add_argument("--out", required=True)
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
ap.add_argument("--root", default=os.path.dirname(os.path.dirname(here)), help="racine du projet")
|
||||
ap.add_argument("--mod-dir", default="", help="dossier du mod (defaut : <root>/ModSources/<mod>)")
|
||||
ap.add_argument("--planches", default="", help="dossier des planches <Clip>.jpg/.json (defaut : <root>/Saved/Measurement/planches)")
|
||||
ap.add_argument("--vignettes", default="", help="dossier des vignettes <Clip>.png (defaut : <root>/Saved/Measurement/vignettes)")
|
||||
args = ap.parse_args()
|
||||
|
||||
errors = []
|
||||
result = None
|
||||
try:
|
||||
result = build(args, errors)
|
||||
except Exception as e: # noqa: BLE001
|
||||
errors.append("erreur inattendue : %r" % (e,))
|
||||
rapport = os.path.join(os.path.abspath(args.pipeline), "catalogue_rapport.json")
|
||||
info = {"date": datetime.datetime.now().isoformat(timespec="seconds"), "mod": args.mod, "version": args.version,
|
||||
"ok": result is not None and not errors, "fiches": len(result[0]["animations"]) if result else 0,
|
||||
"echecs": errors}
|
||||
if result is None or errors:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(rapport), exist_ok=True)
|
||||
with open(rapport, "w", encoding="utf-8", newline="\n") as f:
|
||||
json.dump(info, f, ensure_ascii=False, indent=2)
|
||||
except OSError:
|
||||
pass
|
||||
print("CATALOGUE ECHEC : %d cause(s)" % len(errors), file=sys.stderr)
|
||||
for e in errors:
|
||||
print(" - " + e, file=sys.stderr)
|
||||
return 1
|
||||
cat, thumbs, out = result
|
||||
os.makedirs(os.path.join(out, "vignettes"), exist_ok=True)
|
||||
os.makedirs(os.path.join(out, "planches"), exist_ok=True)
|
||||
for k, src, psrc in thumbs:
|
||||
shutil.copyfile(src, os.path.join(out, "vignettes", k + ".png"))
|
||||
shutil.copyfile(psrc, os.path.join(out, "planches", k + ".jpg"))
|
||||
with open(os.path.join(out, "catalogue.json"), "w", encoding="utf-8", newline="\n") as f:
|
||||
json.dump(cat, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
with open(os.path.join(out, "catalogue.html"), "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(render_html(cat))
|
||||
with open(rapport, "w", encoding="utf-8", newline="\n") as f:
|
||||
json.dump(info, f, ensure_ascii=False, indent=2)
|
||||
print("CATALOGUE OK : %d fiche(s) -> %s" % (len(cat["animations"]), out))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Copie les clips d'animation convertis dans un mod d'assets, renommes a la convention du jeu (lot AP5).
|
||||
|
||||
.DESCRIPTION
|
||||
/Game/Quaternius/UAL/B_<Clip> -> /<Nom>/Anims/Female/qua_<clip>_F ; M_<Clip> -> /<Nom>/Anims/Male/qua_<clip>_M.
|
||||
Le jeu appelle Actor.animate(<cle>) et cherche <cle>_F et <cle>_M (cle = ce qui precede le dernier _).
|
||||
1. controle bloquant : rapport.json existe et valide = vrai (les a_revoir sont affiches, non bloquants) ;
|
||||
2. controles : editeur ferme, aucun autre plugin dans Mods\ ;
|
||||
3. table cle / sources / destinations calculee depuis le dossier disque de -Source (/Game/X/Y -> Content\X\Y), B_*.uasset et M_*.uasset ;
|
||||
4. -DryRun : affiche la table et s'arrete (rien n'est lance ni cree) ;
|
||||
5. sinon : cree ModSources\<Nom> s'il manque (jamais d'ecrasement), monte le mod, lance Tools\anim\DeliverAnims.py
|
||||
dans UnrealEditor-Cmd (journal Saved\Logs\portage\AP5_DeliverAnims_<horodatage>.log), remet le mod en place,
|
||||
controle la sortie (LIVRAISON copies 2n erreurs 0, n fichiers .uasset par sexe). Code de sortie 1 au moindre ecart.
|
||||
|
||||
-Catalogue : apres une livraison reussie, lance Tools\anim\Catalogue.py (lot AP6).
|
||||
-CatalogueOnly : saute la copie et ne produit que le catalogue (rapport valide exige), sans lancer Unreal.
|
||||
|
||||
Parametres (defaut = pack Quaternius) : -Pipeline (dossier des rapports, defaut de -Report, lieu de livraison.json ; sous Saved\),
|
||||
-Pack (pack.json ; donne aussi, quand il est passe explicitement, les textes du mod cree : source, auteur, licence, credit, url ;
|
||||
refus si licence, credit ou auteur est vide ou "A COMPLETER" ; sans -Pack, textes Quaternius d'origine), -Vignettes / -Planches (dossiers transmis a Catalogue.py), -Source (clips convertis B_/M_), -Name, -Prefix, -Exclude.
|
||||
|
||||
.EXAMPLE
|
||||
.\Tools\anim\DeliverAnims.ps1 -DryRun
|
||||
.\Tools\anim\DeliverAnims.ps1 -DryRun -Pipeline Saved\AnimPipeline\freeanims -Source /Game/AnimPacks/FreeAnimationsPack -Name lpr_anim_freeanims -Prefix fap -Exclude ''
|
||||
.\Tools\anim\DeliverAnims.ps1
|
||||
#>
|
||||
param(
|
||||
[string] $Name = 'lpr_anim_quaternius',
|
||||
[string] $Source = '/Game/Quaternius/UAL',
|
||||
[string] $Prefix = 'qua',
|
||||
[string] $Exclude = 'A_TPose',
|
||||
[string] $Report = '',
|
||||
[string] $Pipeline = 'Saved\AnimPipeline\quaternius',
|
||||
[string] $Pack = 'AnimSources\quaternius\pack.json',
|
||||
[string] $Vignettes = '',
|
||||
[string] $Planches = '',
|
||||
[switch] $DryRun,
|
||||
[switch] $Catalogue,
|
||||
[switch] $CatalogueOnly
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
. (Join-Path $Root 'Tools\ModSource.ps1')
|
||||
$Editor = 'D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
|
||||
# -Pipeline : dossier des rapports du pack (relatif a la racine du projet, ou absolu) ; defaut de -Report, lieu de livraison.json.
|
||||
# -Report seul (sans -Pipeline) : le dossier du rapport fait office de dossier de pipeline (comportement d'avant).
|
||||
if ($Report -and -not $PSBoundParameters.ContainsKey('Pipeline')) { $Pipeline = Split-Path $Report -Parent }
|
||||
if (-not [IO.Path]::IsPathRooted($Pipeline)) { $Pipeline = Join-Path $Root $Pipeline }
|
||||
$Pipeline = $Pipeline.TrimEnd('\', '/')
|
||||
if (-not $Report) { $Report = Join-Path $Pipeline 'rapport.json' }
|
||||
if (-not [IO.Path]::IsPathRooted($Pack)) { $Pack = Join-Path $Root $Pack }
|
||||
|
||||
function New-IfMissing([string] $Path, [string] $Text) {
|
||||
if (Test-Path $Path) { return }
|
||||
New-Item -ItemType Directory -Force (Split-Path $Path -Parent) | Out-Null
|
||||
[IO.File]::WriteAllText($Path, $Text, (New-Object Text.UTF8Encoding $false))
|
||||
Write-Host " cree : $Path"
|
||||
}
|
||||
|
||||
# Lot AP6 : catalogue du mod (Tools\anim\Catalogue.py), version lue dans le .lpmod.
|
||||
function Invoke-Catalogue {
|
||||
$lpmodPath = Join-Path $Root "ModSources\$Name\Module\$Name.lpmod"
|
||||
if (-not (Test-Path $lpmodPath)) { throw "lpmod introuvable : $lpmodPath" }
|
||||
$line = Select-String -Path $lpmodPath -Pattern '^MODULE_VERSION:\s*(\S+)' | Select-Object -First 1
|
||||
if (-not $line) { throw "MODULE_VERSION absente de $lpmodPath" }
|
||||
$version = $line.Matches[0].Groups[1].Value
|
||||
$out = Join-Path $Root "ModSources\$Name\Module\Animations"
|
||||
Write-Host "Catalogue : version $version -> $out"
|
||||
$extra = @()
|
||||
if ($Vignettes) { $extra += @('--vignettes', $Vignettes) }
|
||||
if ($Planches) { $extra += @('--planches', $Planches) }
|
||||
& python (Join-Path $Root 'Tools\anim\Catalogue.py') --pack $Pack `
|
||||
--pipeline $Pipeline --mod $Name --version $version --out $out @extra
|
||||
if ($LASTEXITCODE -ne 0) { throw "Catalogue.py a echoue (code $LASTEXITCODE)" }
|
||||
}
|
||||
|
||||
try {
|
||||
# 1. Verdict des controles qualite.
|
||||
if (-not (Test-Path $Report)) { throw "rapport.json introuvable : $Report" }
|
||||
$rapport = Get-Content $Report -Raw -Encoding utf8 | ConvertFrom-Json
|
||||
if ($rapport.valide -ne $true) {
|
||||
$echecs = @($rapport.echecs) -join '; '
|
||||
throw "Rapport non valide ($Report). Echecs : $echecs"
|
||||
}
|
||||
foreach ($r in @($rapport.a_revoir)) { Write-Warning "a revoir : $r" }
|
||||
|
||||
if ($CatalogueOnly -and -not $DryRun) {
|
||||
Invoke-Catalogue
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 2. Controles d'environnement.
|
||||
$running = @(Get-Process UnrealEditor, UnrealEditor-Cmd -ErrorAction SilentlyContinue)
|
||||
if ($running) { throw "UnrealEditor est ouvert (pid $($running[0].Id)) : le fermer avant la livraison" }
|
||||
$stray = Get-StrayMods $Root
|
||||
if ($stray) { throw "Autres plugins dans Mods\ ($($stray -join ', ')) : les remettre dans ModSources" }
|
||||
|
||||
# 3. Table des cles.
|
||||
if ($Source -notmatch '^/Game/(.+)$') { throw "-Source doit commencer par /Game/ (recu : $Source)" }
|
||||
$srcDir = Join-Path $Root ('Content\' + ($Matches[1] -replace '/', '\'))
|
||||
if (-not (Test-Path $srcDir)) { throw "Dossier source introuvable : $srcDir" }
|
||||
$excluded = @($Exclude -split ',' | Where-Object { $_ })
|
||||
$fem = @{}; $mal = @{}
|
||||
foreach ($f in Get-ChildItem $srcDir -File -Filter '*.uasset') {
|
||||
$base = $f.BaseName
|
||||
if ($base -like 'B_*') { $fem[$base.Substring(2)] = $Source.TrimEnd('/') + '/' + $base }
|
||||
elseif ($base -like 'M_*') { $mal[$base.Substring(2)] = $Source.TrimEnd('/') + '/' + $base }
|
||||
}
|
||||
$clips = @(($fem.Keys + $mal.Keys) | Sort-Object -Unique | Where-Object { $_ -notin $excluded })
|
||||
$table = @()
|
||||
$seen = @{}
|
||||
foreach ($clip in $clips) {
|
||||
if (-not $fem.ContainsKey($clip)) { throw "Clip $clip : source B_ absente" }
|
||||
if (-not $mal.ContainsKey($clip)) { throw "Clip $clip : source M_ absente" }
|
||||
$cle = ($Prefix + '_' + $clip).ToLowerInvariant()
|
||||
if ($cle -notmatch '^[a-z0-9_]+$') { throw "Cle invalide (caracteres hors [a-z0-9_]) : $cle" }
|
||||
if ($seen.ContainsKey($cle)) { throw "Cle en double : $cle ($clip et $($seen[$cle]))" }
|
||||
$seen[$cle] = $clip
|
||||
$table += [pscustomobject]@{
|
||||
Cle = $cle; SourceF = $fem[$clip]; SourceM = $mal[$clip]
|
||||
AssetF = "/$Name/Anims/Female/${cle}_F"; AssetM = "/$Name/Anims/Male/${cle}_M"
|
||||
}
|
||||
}
|
||||
if ($table.Count -eq 0) { throw "Aucun clip a livrer dans $srcDir" }
|
||||
Write-Host "Table : $($table.Count) cle(s) pour /$Name/ (exclus : $($excluded -join ', '))"
|
||||
$table | Select-Object -First 5 | ForEach-Object { Write-Host (" {0} : {1} -> {2} ; {3} -> {4}" -f $_.Cle, $_.SourceF, $_.AssetF, $_.SourceM, $_.AssetM) }
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "A blanc : rien lance, rien cree"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 5. Sources du mod (jamais d'ecrasement). Textes : pack.json si -Pack est donne, sinon ceux de Quaternius.
|
||||
$paths = Get-ModSourcePaths $Root $Name
|
||||
$src = $paths.Source
|
||||
$title = 'Quaternius Animations'
|
||||
$desc = 'Free animation clips from the Quaternius Universal Animation Library (CC0), retargeted for LifePlay'
|
||||
$author = 'LifePlay Revival (animations by Quaternius)'
|
||||
$licence = 'CC0'
|
||||
$link = 'https://quaternius.com'
|
||||
$credits = "Credits and licenses of the assets in Quaternius Animations (one line per asset: title, author, license, source URL, changes).`r`n" +
|
||||
"- Universal Animation Library by Quaternius, CC0 1.0, https://quaternius.com . Changes: retargeted to the Genesis 8 skeletons (Bridget, Michael), renamed qua_<clip>_F / qua_<clip>_M.`r`n"
|
||||
if ($PSBoundParameters.ContainsKey('Pack')) {
|
||||
$pk = Get-Content $Pack -Raw -Encoding utf8 | ConvertFrom-Json
|
||||
foreach ($k in 'source', 'auteur', 'licence', 'credit') {
|
||||
if (-not ([string] $pk.$k).Trim() -or ([string] $pk.$k) -match 'A COMPLETER') { throw "pack.json : '$k' vide ou 'A COMPLETER' (mod non cree)" }
|
||||
}
|
||||
$title = [string] $pk.source
|
||||
$desc = "Free animation clips from $($pk.source) ($($pk.licence)), retargeted for LifePlay"
|
||||
$author = "LifePlay Revival (animations by $($pk.auteur))"
|
||||
$licence = [string] $pk.licence
|
||||
# Lien de l'auteur : url_auteur (page de l'auteur) s'il est donne, sinon url (page du pack).
|
||||
$authorUrl = if ($pk.PSObject.Properties['url_auteur']) { [string] $pk.url_auteur } else { [string] $pk.url }
|
||||
$link = if ($authorUrl.Trim() -and $authorUrl -notmatch 'A COMPLETER') { $authorUrl } else { '' }
|
||||
$credits = "Credits and licenses of the assets in $title (one line per asset: title, author, license, source URL, changes).`r`n" +
|
||||
"- $($pk.credit)$(if ($pk.url -and ([string] $pk.url) -notmatch 'A COMPLETER') { ', ' + $pk.url }) . Changes: retargeted to the Genesis 8 skeletons (Bridget, Michael), renamed $($pk.prefixe)_<clip>_F / $($pk.prefixe)_<clip>_M.`r`n"
|
||||
}
|
||||
$uplugin = [ordered]@{
|
||||
FileVersion = 3; Version = 1; VersionName = '1.0'; FriendlyName = $title
|
||||
Description = $desc; Category = 'LifePlay Mods'; CreatedBy = $author
|
||||
CanContainContent = $true; IsBetaVersion = $false; Installed = $false
|
||||
}
|
||||
if ($link) { $uplugin.CreatedByURL = $link }
|
||||
New-IfMissing (Join-Path $src "$Name.uplugin") (($uplugin | ConvertTo-Json) + "`r`n")
|
||||
$lpmod = @(
|
||||
"MODULE_UNIQUEID: $Name", "MODULE_NAME: $title", "MODULE_AUTHOR: $author",
|
||||
'MODULE_LINK: https://github.com/edujiro5972/LifePlay-Revival', "MODULE_DESCRIPTION: $desc",
|
||||
'MODULE_REQUIREMENTS: vin_Base', 'MODULE_VERSION: 1.0', "MODULE_CREATED: $(Get-Date -Format 'yyyy-MM-dd')",
|
||||
"MODULE_LICENSE: $licence") + @($(if ($link) { "MODULE_AUTHOR_LINK: $link" })) + @('MODULE_PRICE: Free', 'MODULE_DEFAULT_ENABLED: true')
|
||||
New-IfMissing (Join-Path $src "Module\$Name.lpmod") (($lpmod -join "`r`n") + "`r`n")
|
||||
New-IfMissing (Join-Path $src 'Module\CREDITS.txt') $credits
|
||||
New-Item -ItemType Directory -Force (Join-Path $src 'Content') | Out-Null
|
||||
|
||||
# Lancement d'Unreal, mod monte.
|
||||
$logDir = Join-Path $Root 'Saved\Logs\portage'
|
||||
New-Item -ItemType Directory -Force $logDir | Out-Null
|
||||
$log = Join-Path $logDir ('AP5_DeliverAnims_{0}.log' -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
|
||||
$py = (Join-Path $Root 'Tools\anim\DeliverAnims.py') -replace '\\', '/'
|
||||
$mounted = Mount-ModSource $Root $Name
|
||||
try {
|
||||
# Le script Python decoupe ses arguments sur les espaces : pipeline= est donne relatif a Saved\ (sans espace).
|
||||
$savedDir = (Join-Path $Root 'Saved').TrimEnd('\') + '\'
|
||||
if (-not $Pipeline.StartsWith($savedDir, [StringComparison]::OrdinalIgnoreCase)) { throw "-Pipeline doit etre sous $savedDir (recu : $Pipeline)" }
|
||||
$pipeRel = $Pipeline.Substring($savedDir.Length) -replace '\\', '/'
|
||||
if ($pipeRel -match '\s') { throw "-Pipeline ne doit pas contenir d'espace sous Saved\ (recu : $pipeRel)" }
|
||||
$script = "-script=$py mod=$Name src=$Source prefix=$Prefix exclude=$Exclude pipeline=$pipeRel"
|
||||
Write-Host "Livraison dans /$Name/ (journal : $log)"
|
||||
& $Editor (Join-Path $Root 'LifePlay.uproject') -run=pythonscript $script -unattended -nopause -nosplash -stdout -FullStdOutLogOutput *> $log
|
||||
}
|
||||
finally {
|
||||
Dismount-ModSource $Root $Name
|
||||
}
|
||||
|
||||
# Controle de sortie.
|
||||
$n = $table.Count
|
||||
if (-not (Select-String -Path $log -Pattern "LIVRAISON copies $($n * 2) erreurs 0\b" -Quiet)) {
|
||||
$last = Select-String -Path $log -Pattern 'LIVRAISON' | Select-Object -Last 3 | ForEach-Object { $_.Line }
|
||||
throw "Ligne 'LIVRAISON copies $($n * 2) erreurs 0' absente : $($last -join ' | ') (voir $log)"
|
||||
}
|
||||
foreach ($sexe in 'Female', 'Male') {
|
||||
$dir = Join-Path $src "Content\Anims\$sexe"
|
||||
$count = @(Get-ChildItem $dir -Filter '*.uasset' -ErrorAction SilentlyContinue).Count
|
||||
if ($count -ne $n) { throw "$dir : $count fichier(s) .uasset pour $n cle(s)" }
|
||||
}
|
||||
Write-Host "Livraison terminee : $n cle(s), $($n * 2) copies"
|
||||
if ($Catalogue) { Invoke-Catalogue }
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
Write-Host "ECHEC : $($_.Exception.Message)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
# Livraison des clips d'animation convertis dans un mod d'assets (lot AP5).
|
||||
# Lancement :
|
||||
# UnrealEditor-Cmd.exe <projet> -run=pythonscript "-script=<chemin>/Tools/anim/DeliverAnims.py mod=<Nom> src=/Game/Quaternius/UAL prefix=qua exclude=A_TPose pipeline=AnimPipeline/quaternius"
|
||||
# Copie <src>/B_<Clip> -> /<mod>/Anims/Female/<prefix>_<clip>_F
|
||||
# <src>/M_<Clip> -> /<mod>/Anims/Male/<prefix>_<clip>_M (src = dossier des clips convertis B_/M_)
|
||||
# Les sources ne sont jamais deplacees ni renommees. Seules des copies du mod peuvent etre supprimees.
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import unreal
|
||||
|
||||
# Arguments : mod=... src=... prefix=... exclude=A,B (le script peut recevoir les arguments en un seul bloc)
|
||||
# pipeline= : dossier de livraison.json, relatif a Saved/ (ou absolu) ; sans espace (arguments decoupes sur les espaces).
|
||||
args = {"mod": "lpr_anim_quaternius", "src": "/Game/Quaternius/UAL", "prefix": "qua", "exclude": "A_TPose",
|
||||
"pipeline": "AnimPipeline/quaternius"}
|
||||
tokens = []
|
||||
for a in sys.argv[1:]:
|
||||
tokens.extend(a.split())
|
||||
for t in tokens:
|
||||
if "=" in t:
|
||||
k, v = t.split("=", 1)
|
||||
args[k.strip().lower()] = v.strip()
|
||||
|
||||
MOD = args["mod"]
|
||||
SRC = args["src"].rstrip("/")
|
||||
PREFIX = args["prefix"]
|
||||
EXCLUDE = set(x for x in args["exclude"].split(",") if x)
|
||||
erreurs = []
|
||||
livraison = []
|
||||
|
||||
|
||||
def erreur(msg):
|
||||
erreurs.append(msg)
|
||||
unreal.log_error("LIVRAISON erreur : " + msg)
|
||||
|
||||
|
||||
def lister(dossier, debut):
|
||||
noms = set()
|
||||
for p in unreal.EditorAssetLibrary.list_assets(dossier, recursive=False, include_folder=False):
|
||||
nom = p.split("/")[-1].split(".")[0]
|
||||
if nom.startswith(debut):
|
||||
noms.add(nom[len(debut):])
|
||||
return noms
|
||||
|
||||
|
||||
def infos(asset):
|
||||
return asset.get_skeleton(), asset.get_editor_property("number_of_sampled_keys"), asset.get_editor_property("sequence_length")
|
||||
|
||||
|
||||
# Garde-fou : on ne supprime et n'ecrit que dans le mod.
|
||||
def sous_mod(chemin):
|
||||
return chemin.startswith("/" + MOD + "/")
|
||||
|
||||
|
||||
# En commandlet, le registre n'a pas encore explore le dossier du mod : sans ce scan, les copies d'une passe
|
||||
# precedente sont ignorees par does_asset_exist et duplicate_asset echoue (AP5, deuxieme passe).
|
||||
unreal.AssetRegistryHelpers.get_asset_registry().scan_paths_synchronous(["/" + MOD], True)
|
||||
|
||||
clips = sorted((lister(SRC, "B_") & lister(SRC, "M_")) - EXCLUDE)
|
||||
unreal.log("LIVRAISON %d clip(s) a livrer vers /%s/" % (len(clips), MOD))
|
||||
|
||||
for clip in clips:
|
||||
cle = PREFIX + "_" + clip.lower()
|
||||
if not re.match(r"^[a-z0-9_]+$", cle):
|
||||
erreur("cle invalide : " + cle)
|
||||
continue
|
||||
sources = {"F": SRC + "/B_" + clip, "M": SRC + "/M_" + clip}
|
||||
dests = {"F": "/%s/Anims/Female/%s_F" % (MOD, cle), "M": "/%s/Anims/Male/%s_M" % (MOD, cle)}
|
||||
ref = {}
|
||||
ok = True
|
||||
for sexe in ("F", "M"):
|
||||
s, d = sources[sexe], dests[sexe]
|
||||
if not sous_mod(d):
|
||||
erreur("destination hors du mod : " + d)
|
||||
ok = False
|
||||
continue
|
||||
asrc = unreal.EditorAssetLibrary.load_asset(s)
|
||||
if not isinstance(asrc, unreal.AnimSequence):
|
||||
erreur("source absente ou pas une AnimSequence : " + s)
|
||||
ok = False
|
||||
continue
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(d):
|
||||
if not unreal.EditorAssetLibrary.delete_asset(d):
|
||||
erreur("suppression de la copie precedente impossible : " + d)
|
||||
ok = False
|
||||
continue
|
||||
copie = unreal.EditorAssetLibrary.duplicate_asset(s, d)
|
||||
if copie is None:
|
||||
erreur("copie impossible : %s -> %s" % (s, d))
|
||||
ok = False
|
||||
continue
|
||||
# Un mod DLC ne peut pas referencer du contenu moteur non cuit : les clips d'un pack UE5 pointent
|
||||
# explicitement sur /Engine/Animation/DefaultAnim*CompressionSettings (cuisson refusee, AP7). Sans
|
||||
# reglage (None), la compression par defaut du projet s'applique, comme pour les clips Quaternius.
|
||||
for prop in ("bone_compression_settings", "curve_compression_settings"):
|
||||
valeur = copie.get_editor_property(prop)
|
||||
if valeur is not None and valeur.get_path_name().startswith("/Engine/"):
|
||||
copie.set_editor_property(prop, None)
|
||||
if not unreal.EditorAssetLibrary.save_asset(d, only_if_is_dirty=False):
|
||||
erreur("enregistrement impossible : " + d)
|
||||
ok = False
|
||||
continue
|
||||
# Controles : existe, AnimSequence, meme squelette, meme nombre d'images, meme duree.
|
||||
acopie = unreal.EditorAssetLibrary.load_asset(d)
|
||||
if not unreal.EditorAssetLibrary.does_asset_exist(d) or not isinstance(acopie, unreal.AnimSequence):
|
||||
erreur("copie absente ou pas une AnimSequence : " + d)
|
||||
ok = False
|
||||
continue
|
||||
sk0, n0, l0 = infos(asrc)
|
||||
sk1, n1, l1 = infos(acopie)
|
||||
if sk0 != sk1:
|
||||
erreur("squelette different : " + d)
|
||||
ok = False
|
||||
if n0 != n1:
|
||||
erreur("nombre d'images different (%s / %s) : %s" % (n0, n1, d))
|
||||
ok = False
|
||||
if abs(l0 - l1) > 1e-4:
|
||||
erreur("duree differente (%s / %s) : %s" % (l0, l1, d))
|
||||
ok = False
|
||||
ref[sexe] = (n1, l1)
|
||||
if ok:
|
||||
livraison.append({
|
||||
"cle": cle,
|
||||
"source_F": sources["F"], "source_M": sources["M"],
|
||||
"asset_F": dests["F"], "asset_M": dests["M"],
|
||||
"images": ref["F"][0], "duree_s": ref["F"][1],
|
||||
})
|
||||
|
||||
sortie = os.path.join(unreal.Paths.project_saved_dir(), args["pipeline"], "livraison.json") # join : absolu prime
|
||||
os.makedirs(os.path.dirname(sortie), exist_ok=True)
|
||||
with open(sortie, "w", encoding="utf-8") as f:
|
||||
json.dump(livraison, f, indent=2, ensure_ascii=False)
|
||||
|
||||
unreal.log("LIVRAISON copies %d erreurs %d" % (len(livraison) * 2, len(erreurs)))
|
||||
|
|
@ -1,359 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Genere un rapport HTML statique a partir des sorties du commandlet
|
||||
d'evaluation d'animations (rapport.json, rapport_poseT_*.json, eval_*.csv,
|
||||
controles_*.csv).
|
||||
|
||||
Usage :
|
||||
python RapportControles.py [dossier_resultats]
|
||||
|
||||
Par defaut : D:\\Game\\LifePlay-master 5.5\\Saved\\AnimPipeline\\quaternius
|
||||
Sortie : <dossier>/rapport.html (une seule page, CSS integre, hors ligne).
|
||||
Bibliotheque standard uniquement.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Defaut = dossier du pack Quaternius ; tout autre pack : passer son dossier en argument.
|
||||
DOSSIER_DEFAUT = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||
"Saved", "AnimPipeline", "quaternius")
|
||||
|
||||
# (cle CSV valeur, cle CSV os, titre, unite, seuil)
|
||||
MESURES = [
|
||||
("sol_cm", "sol_os", "Sol", "cm", 3.0),
|
||||
("glissement_cm", "glissement_os", "Glissement", "cm/image", 3.0),
|
||||
("genou_cm", "genou_os", "Genou", "cm", 1.0),
|
||||
("flexion_deg", "flexion_os", "Flexion", "\u00b0", 5.0),
|
||||
("saut_deg", "saut_os", "Saut", "\u00b0", 10.0),
|
||||
("penetration_cm", "penetration_os", "Penetration", "cm", 1.0),
|
||||
]
|
||||
FACTEUR_REJET = 3.0
|
||||
ORDRE_VERDICT = {"rejete": 0, "a revoir": 1, "accepte": 2}
|
||||
|
||||
|
||||
def esc(texte):
|
||||
return html.escape(str(texte), quote=True)
|
||||
|
||||
|
||||
def lire_json(chemin):
|
||||
# utf-8-sig : tolere un eventuel BOM
|
||||
with open(chemin, "r", encoding="utf-8-sig") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def lire_csv(chemin):
|
||||
with open(chemin, "r", encoding="utf-8-sig", newline="") as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
|
||||
def nombre(valeur, defaut=0.0):
|
||||
try:
|
||||
return float(valeur)
|
||||
except (TypeError, ValueError):
|
||||
return defaut
|
||||
|
||||
|
||||
def fmt(valeur, decimales=2):
|
||||
"""Formate un nombre ; notation scientifique pour les tres petites valeurs."""
|
||||
try:
|
||||
x = float(valeur)
|
||||
except (TypeError, ValueError):
|
||||
return "-"
|
||||
if x != 0 and abs(x) < 0.005:
|
||||
return "%.1e" % x
|
||||
return ("%." + str(decimales) + "f") % x
|
||||
|
||||
|
||||
def classe_mesure(valeur, seuil):
|
||||
if valeur > seuil * FACTEUR_REJET:
|
||||
return "m-rejet"
|
||||
if valeur > seuil:
|
||||
return "m-revoir"
|
||||
return ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- sections
|
||||
|
||||
def html_entete(rapport, date_txt):
|
||||
valide = bool(rapport.get("valide"))
|
||||
cls = "ok" if valide else "ko"
|
||||
texte = "VALIDE" if valide else "REFUS\u00c9"
|
||||
return (
|
||||
'<header>\n'
|
||||
'<h1>Rapport des contr\u00f4les d\'animations</h1>\n'
|
||||
'<div class="verdict %s" role="status">%s</div>\n'
|
||||
'<dl class="meta">\n'
|
||||
'<div><dt>G\u00e9n\u00e9r\u00e9 le</dt><dd>%s</dd></div>\n'
|
||||
'<div><dt>Dossier des clips</dt><dd><code>%s</code></dd></div>\n'
|
||||
'<div><dt>Clips</dt><dd>%s</dd></div>\n'
|
||||
'<div><dt>Instants par clip</dt><dd>%s</dd></div>\n'
|
||||
'</dl>\n</header>\n'
|
||||
) % (cls, texte, esc(date_txt), esc(rapport.get("dossier_clips", "?")),
|
||||
esc(rapport.get("clips", "?")), esc(rapport.get("instants", "?")))
|
||||
|
||||
|
||||
def html_echecs_revoir(rapport):
|
||||
echecs = rapport.get("echecs") or []
|
||||
revoir = rapport.get("a_revoir") or []
|
||||
sortie = ['<section>\n<h2>\u00c9checs</h2>\n']
|
||||
if echecs:
|
||||
sortie.append('<ul class="liste ko-liste">\n')
|
||||
sortie += ["<li>%s</li>\n" % esc(e) for e in echecs]
|
||||
sortie.append("</ul>\n")
|
||||
else:
|
||||
sortie.append('<p class="vide">Aucun \u00e9chec.</p>\n')
|
||||
sortie.append("</section>\n<section>\n<h2>Clips \u00e0 revoir</h2>\n")
|
||||
if revoir:
|
||||
sortie.append('<ul class="liste">\n')
|
||||
sortie += ["<li>%s</li>\n" % esc(e) for e in revoir]
|
||||
sortie.append("</ul>\n")
|
||||
else:
|
||||
sortie.append('<p class="vide">Aucun clip \u00e0 revoir.</p>\n')
|
||||
sortie.append("</section>\n")
|
||||
return "".join(sortie)
|
||||
|
||||
|
||||
def html_legende():
|
||||
items = []
|
||||
for _, _, titre, unite, seuil in MESURES:
|
||||
items.append("<li><b>%s</b> %s %s</li>" % (
|
||||
esc(titre), esc(fmt(seuil, 0)), esc(unite)))
|
||||
return (
|
||||
'<section class="legende">\n<h2>L\u00e9gende des seuils</h2>\n'
|
||||
'<ul class="seuils">%s</ul>\n'
|
||||
'<p>Une mesure au-del\u00e0 du seuil donne <span class="puce revoir">'
|
||||
'\u00e0 revoir</span> ; au-del\u00e0 de %s fois le seuil, '
|
||||
'<span class="puce rejete">rejet\u00e9</span>. '
|
||||
'Dans chaque cellule : la valeur, puis l\'os concern\u00e9 en petit.</p>\n'
|
||||
'</section>\n'
|
||||
) % ("".join(items), esc(fmt(FACTEUR_REJET, 0)))
|
||||
|
||||
|
||||
def html_resume_corps(c):
|
||||
cartes = [
|
||||
("Formule pire", fmt(c.get("formule_pire"), 3) + " %"),
|
||||
("Clip pire", fmt(c.get("clip_pire"), 3) + " %"),
|
||||
("Torsion pire", fmt(c.get("torsion_pire_deg"), 3) + " \u00b0"),
|
||||
("Clips \u00e0 revoir", str(c.get("clips_a_revoir_controles", "?"))),
|
||||
("Instants mesur\u00e9s", str(c.get("instants_mesures", "?"))),
|
||||
("Instants hors seuil", str(c.get("instants_hors_seuil", "?"))),
|
||||
]
|
||||
return '<div class="cartes">%s</div>\n' % "".join(
|
||||
'<div class="carte"><span class="k">%s</span><span class="v">%s</span></div>'
|
||||
% (esc(k), esc(v)) for k, v in cartes)
|
||||
|
||||
|
||||
def html_controles(lignes):
|
||||
lignes = sorted(
|
||||
lignes, key=lambda r: (ORDRE_VERDICT.get(r.get("verdict", ""), 3),
|
||||
r.get("clip", "")))
|
||||
tete = "<th>Clip</th><th>Images</th>" + "".join(
|
||||
"<th>%s <small>(%s)</small></th>" % (esc(t), esc(u))
|
||||
for _, _, t, u, _ in MESURES) + "<th>Verdict</th>"
|
||||
corps = []
|
||||
for r in lignes:
|
||||
verdict = r.get("verdict", "")
|
||||
cls = {"rejete": "v-rejete", "a revoir": "v-revoir"}.get(verdict, "v-accepte")
|
||||
cases = ['<td class="clip">%s</td>' % esc(r.get("clip", "")),
|
||||
'<td class="num">%s</td>' % esc(r.get("images", ""))]
|
||||
for cle_val, cle_os, _, _, seuil in MESURES:
|
||||
v = nombre(r.get(cle_val))
|
||||
os_ = r.get(cle_os, "")
|
||||
cases.append(
|
||||
'<td class="num %s">%s%s</td>' % (
|
||||
classe_mesure(v, seuil), esc(fmt(v)),
|
||||
('<small class="os">%s</small>' % esc(os_)) if os_ else ""))
|
||||
cases.append('<td><span class="puce %s">%s</span></td>' % (
|
||||
esc(verdict.replace(" ", "")), esc(verdict)))
|
||||
corps.append('<tr class="%s">%s</tr>' % (cls, "".join(cases)))
|
||||
return ('<div class="defile"><table class="controles">\n<thead><tr>%s</tr></thead>\n'
|
||||
'<tbody>\n%s\n</tbody></table></div>\n') % (tete, "\n".join(corps))
|
||||
|
||||
|
||||
def html_pose(pose):
|
||||
if not pose:
|
||||
return '<p class="vide">Pas de rapport de pose T pour ce corps.</p>\n'
|
||||
seg = pose.get("segments") or []
|
||||
lignes = []
|
||||
for s in seg:
|
||||
cls = "ajuste" if s.get("ajuste") else ""
|
||||
lignes.append(
|
||||
'<tr class="%s"><td>%s</td><td class="num">%s</td><td class="num">%s</td>'
|
||||
'<td>%s</td></tr>' % (
|
||||
cls, esc(s.get("segment", "")), esc(fmt(s.get("avant"))),
|
||||
esc(fmt(s.get("apres"))), "ajust\u00e9" if s.get("ajuste") else ""))
|
||||
reglages = pose.get("reglages") or []
|
||||
lig_reg = ['<tr><td>%s</td><td class="num">%s</td></tr>' % (
|
||||
esc(r.get("os", "")), esc(fmt(r.get("degres")))) for r in reglages]
|
||||
valide = pose.get("valide")
|
||||
return (
|
||||
'<p>Profil : <b>%s</b> \u00b7 sym\u00e9trie max : <b>%s</b> \u00b7 pose T : '
|
||||
'<span class="puce %s">%s</span></p>\n'
|
||||
'<div class="deux">\n'
|
||||
'<div><h4>Segments (\u00e9cart en %% de 180\u00b0, avant / apr\u00e8s ajustement)</h4>'
|
||||
'<div class="defile"><table><thead><tr><th>Segment</th><th>Avant</th>'
|
||||
'<th>Apr\u00e8s</th><th></th></tr></thead><tbody>%s</tbody></table></div></div>\n'
|
||||
'<div><h4>R\u00e9glages appliqu\u00e9s (degr\u00e9s)</h4>'
|
||||
'<div class="defile"><table><thead><tr><th>Os</th><th>Degr\u00e9s</th></tr>'
|
||||
'</thead><tbody>%s</tbody></table></div></div>\n</div>\n'
|
||||
) % (esc(pose.get("profil", "?")), esc(fmt(pose.get("ecart_symetrie_max"), 4)),
|
||||
"accepte" if valide else "rejete", "valide" if valide else "refus\u00e9",
|
||||
"".join(lignes), "".join(lig_reg) or
|
||||
'<tr><td colspan="2">Aucun r\u00e9glage.</td></tr>')
|
||||
|
||||
|
||||
def html_ecarts(lignes):
|
||||
tete = ("<th>Clip</th><th>Instant (s)</th><th>Formule (%)</th><th>Clip brut (%)</th>"
|
||||
"<th>Torsion (\u00b0)</th>")
|
||||
corps = []
|
||||
for r in lignes:
|
||||
corps.append(
|
||||
'<tr><td class="clip">%s</td><td class="num">%s</td>'
|
||||
'<td class="num">%s<small class="os">%s</small></td>'
|
||||
'<td class="num">%s<small class="os">%s</small></td>'
|
||||
'<td class="num">%s<small class="os">%s</small></td></tr>' % (
|
||||
esc(r.get("clip", "")), esc(r.get("instant", "")),
|
||||
esc(fmt(r.get("formule_pire"))), esc(r.get("formule_os", "")),
|
||||
esc(fmt(r.get("brut_pire"))), esc(r.get("brut_os", "")),
|
||||
esc(fmt(r.get("torsion_deg"))), esc(r.get("torsion_os", ""))))
|
||||
return (
|
||||
'<details><summary>\u00c9carts formule / clip / torsion par clip et par '
|
||||
'instant (%d lignes)</summary>\n<div class="defile"><table class="ecarts">'
|
||||
'<thead><tr>%s</tr></thead><tbody>\n%s\n</tbody></table></div></details>\n'
|
||||
) % (len(lignes), tete, "\n".join(corps))
|
||||
|
||||
|
||||
CSS = """
|
||||
:root{--fond:#f6f7f9;--carte:#fff;--texte:#1b1f24;--doux:#5b6470;--bord:#d7dce2;
|
||||
--ok:#1a7f37;--ok-fond:#dafbe1;--ko:#cf222e;--ko-fond:#ffebe9;
|
||||
--revoir:#9a6700;--revoir-fond:#fff3c4;--rejet-fond:#ffd8d3;--entete:#eef0f3}
|
||||
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--fond:#0f1317;
|
||||
--carte:#171c22;--texte:#e6e9ed;--doux:#98a2b0;--bord:#2d353f;--ok:#56d364;
|
||||
--ok-fond:#12351d;--ko:#ff7b72;--ko-fond:#3d1518;--revoir:#e3b341;
|
||||
--revoir-fond:#3a2d0b;--rejet-fond:#4a1a1a;--entete:#1f262e}}
|
||||
*{box-sizing:border-box}
|
||||
html{-webkit-text-size-adjust:100%}
|
||||
body{margin:0;background:var(--fond);color:var(--texte);
|
||||
font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
||||
main{max-width:1100px;margin:0 auto;padding:16px}
|
||||
h1{font-size:1.5rem;margin:.2em 0 .5em}
|
||||
h2{font-size:1.25rem;margin:1.6em 0 .5em;border-bottom:1px solid var(--bord);padding-bottom:.2em}
|
||||
h3{font-size:1.1rem;margin:1.2em 0 .4em}
|
||||
h4{font-size:.95rem;margin:.8em 0 .3em;color:var(--doux)}
|
||||
code{font-family:ui-monospace,Consolas,monospace;font-size:.9em;overflow-wrap:anywhere}
|
||||
.verdict{display:inline-block;font-size:2rem;font-weight:800;letter-spacing:.06em;
|
||||
padding:.2em .7em;border-radius:10px;border:3px solid}
|
||||
.verdict.ok{color:var(--ok);background:var(--ok-fond);border-color:var(--ok)}
|
||||
.verdict.ko{color:var(--ko);background:var(--ko-fond);border-color:var(--ko)}
|
||||
.meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:8px 16px;margin:16px 0 0}
|
||||
.meta div{background:var(--carte);border:1px solid var(--bord);border-radius:8px;padding:8px 12px;min-width:0}
|
||||
.meta dt{font-size:.8rem;color:var(--doux)}
|
||||
.meta dd{margin:0;font-weight:600;overflow-wrap:anywhere}
|
||||
.liste{margin:.3em 0;padding-left:1.3em}
|
||||
.liste li{overflow-wrap:anywhere}
|
||||
.ko-liste li{color:var(--ko)}
|
||||
.vide{color:var(--doux);font-style:italic}
|
||||
.seuils{display:flex;flex-wrap:wrap;gap:6px 14px;list-style:none;padding:0;margin:.3em 0}
|
||||
.seuils li{background:var(--carte);border:1px solid var(--bord);border-radius:6px;padding:2px 10px}
|
||||
.cartes{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin:.6em 0}
|
||||
.carte{background:var(--carte);border:1px solid var(--bord);border-radius:8px;padding:8px 12px;
|
||||
display:flex;flex-direction:column}
|
||||
.carte .k{font-size:.8rem;color:var(--doux)}
|
||||
.carte .v{font-size:1.2rem;font-weight:700}
|
||||
.defile{overflow-x:auto;max-width:100%;border:1px solid var(--bord);border-radius:8px;background:var(--carte)}
|
||||
table{border-collapse:collapse;width:100%;font-size:.88rem}
|
||||
th,td{padding:4px 8px;border-bottom:1px solid var(--bord);text-align:left;white-space:nowrap}
|
||||
th{background:var(--entete);position:sticky;top:0}
|
||||
th small{font-weight:400;color:var(--doux)}
|
||||
td.num{text-align:right;font-variant-numeric:tabular-nums}
|
||||
td.clip{font-weight:600}
|
||||
small.os{display:block;font-size:.7rem;color:var(--doux);line-height:1.1}
|
||||
tr.v-rejete td{background:var(--rejet-fond)}
|
||||
tr.v-revoir td{background:var(--revoir-fond)}
|
||||
td.m-revoir{color:var(--revoir);font-weight:700}
|
||||
td.m-rejet{color:var(--ko);font-weight:800}
|
||||
tr.ajuste td{background:var(--revoir-fond)}
|
||||
.puce{display:inline-block;padding:0 .6em;border-radius:999px;font-size:.8rem;font-weight:700;
|
||||
border:1px solid var(--bord)}
|
||||
.puce.accepte{color:var(--ok);background:var(--ok-fond);border-color:var(--ok)}
|
||||
.puce.arevoir,.puce.revoir{color:var(--revoir);background:var(--revoir-fond);border-color:var(--revoir)}
|
||||
.puce.rejete{color:var(--ko);background:var(--ko-fond);border-color:var(--ko)}
|
||||
.deux{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}
|
||||
.deux>div{min-width:0}
|
||||
details{margin:.8em 0}
|
||||
summary{cursor:pointer;font-weight:600;padding:4px 0}
|
||||
.ecarts th{position:static}
|
||||
footer{margin:2em 0 1em;color:var(--doux);font-size:.8rem}
|
||||
"""
|
||||
|
||||
|
||||
def construire(dossier):
|
||||
rapport = lire_json(os.path.join(dossier, "rapport.json"))
|
||||
date_txt = datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
||||
parts = ["<!DOCTYPE html>\n<html lang=\"fr\">\n<head>\n<meta charset=\"utf-8\">\n"
|
||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
|
||||
"<meta name=\"color-scheme\" content=\"light dark\">\n"
|
||||
"<title>Rapport des contr\u00f4les d'animations</title>\n"
|
||||
"<style>%s</style>\n</head>\n<body>\n<main>\n" % CSS]
|
||||
parts.append(html_entete(rapport, date_txt))
|
||||
parts.append(html_echecs_revoir(rapport))
|
||||
parts.append(html_legende())
|
||||
|
||||
stats = {}
|
||||
for c in rapport.get("corps", []):
|
||||
nom = c.get("corps", "?")
|
||||
chemin_ctrl = os.path.join(dossier, "controles_%s.csv" % nom)
|
||||
chemin_eval = os.path.join(dossier, "eval_%s.csv" % nom)
|
||||
chemin_pose = os.path.join(dossier, "rapport_poseT_%s.json" % nom)
|
||||
ctrl = lire_csv(chemin_ctrl) if os.path.isfile(chemin_ctrl) else []
|
||||
evals = lire_csv(chemin_eval) if os.path.isfile(chemin_eval) else []
|
||||
pose = lire_json(chemin_pose) if os.path.isfile(chemin_pose) else None
|
||||
stats[nom] = (len(ctrl), len(evals))
|
||||
|
||||
parts.append('<section class="corps" id="corps-%s">\n<h2>Corps : %s</h2>\n'
|
||||
% (esc(nom), esc(nom)))
|
||||
parts.append(html_resume_corps(c))
|
||||
parts.append("<h3>Contr\u00f4les par clip</h3>\n")
|
||||
if ctrl:
|
||||
parts.append(html_controles(ctrl))
|
||||
else:
|
||||
parts.append('<p class="vide">Fichier controles_%s.csv absent ou vide.</p>\n'
|
||||
% esc(nom))
|
||||
parts.append("<h3>Pose T</h3>\n")
|
||||
parts.append(html_pose(pose))
|
||||
parts.append("<h3>\u00c9carts d\u00e9taill\u00e9s</h3>\n")
|
||||
if evals:
|
||||
parts.append(html_ecarts(evals))
|
||||
else:
|
||||
parts.append('<p class="vide">Fichier eval_%s.csv absent ou vide.</p>\n'
|
||||
% esc(nom))
|
||||
parts.append("</section>\n")
|
||||
|
||||
parts.append('<footer>Rapport g\u00e9n\u00e9r\u00e9 par RapportControles.py \u00b7 '
|
||||
'source : <code>%s</code></footer>\n</main>\n</body>\n</html>\n'
|
||||
% esc(dossier))
|
||||
sortie = os.path.join(dossier, "rapport.html")
|
||||
with open(sortie, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write("".join(parts))
|
||||
return sortie, stats
|
||||
|
||||
|
||||
def main(argv):
|
||||
dossier = argv[1] if len(argv) > 1 else DOSSIER_DEFAUT
|
||||
if not os.path.isfile(os.path.join(dossier, "rapport.json")):
|
||||
print("rapport.json introuvable dans : %s" % dossier, file=sys.stderr)
|
||||
return 1
|
||||
sortie, stats = construire(dossier)
|
||||
print("Rapport ecrit : %s" % sortie)
|
||||
for nom, (nc, ne) in stats.items():
|
||||
print(" %s : %d lignes de controles, %d lignes d'ecarts" % (nom, nc, ne))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
@ -1,463 +0,0 @@
|
|||
# Recopie les clips d'un pack d'animations sur Bridget et Michael (squelettes Genesis 8 du jeu).
|
||||
# Les chaines portent le meme nom des deux cotes pour que le retarget les aligne.
|
||||
# Les maillages bridget_gp et michael ne sont pas reimportes.
|
||||
# Lot AP7 : script generique (ex-RetargetQuaternius.py), pilote par une methode de transformation
|
||||
# (Tools/anim/methodes/<id>.json ; "ue4" = ancienne methode Quaternius) et par Saved/anim_retarget.json.
|
||||
#
|
||||
# Lance par Tools/BuildMod.ps1 -Retarget (UnrealEditor.exe : la copie a besoin de Slate).
|
||||
# Saved/anim_retarget.json (absent : configuration Quaternius par defaut), champs et defauts :
|
||||
# mode "bake"|"rigs" (defaut "bake" ; a defaut de "mode", Saved/anim_to_genesis_step.txt est lu comme avant),
|
||||
# methode "ue4", source_mesh /Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard, clips_dir /Game/Quaternius/UAL,
|
||||
# out_dir = clips_dir, dest /Game/Quaternius/Retarget (ue4) ou /Game/Retarget_<methode>,
|
||||
# corps [{nom,maillage,prefixe}] Bridget (B_) et Michael (M_), exclure [], pose_dir Saved/Measurement,
|
||||
# jeton (ecrit par BuildMod, ajoute en fin des lignes de resultat et d'echec : "... jeton <jeton>").
|
||||
# Methode factice ou copie, ou profil du maillage source different de profil_source : ligne
|
||||
# "RETARGET echec : <cause>" puis arret.
|
||||
#
|
||||
# Phase C (CHG-204) : la pose de retarget de chaque corps est la pose ajustee sur le mannequin
|
||||
# par la sandbox Mesure (LPPoseMeasure::WriteRetargetPose) : Saved/Measurement/retarget_pose_<Corps>.txt.
|
||||
# Colonne et cou ont plus d'os chez Genesis : chaines Interpolated. Les os Twist restent hors chaine.
|
||||
# AP1 : les chaines et la racine viennent des profils Tools/anim/profiles (identification par les noms d'os).
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
|
||||
import unreal
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"mode": None,
|
||||
"methode": "ue4",
|
||||
"source_mesh": "/Game/Quaternius/UAL/UAL1_Standard.UAL1_Standard",
|
||||
"clips_dir": "/Game/Quaternius/UAL",
|
||||
"out_dir": None,
|
||||
"dest": None,
|
||||
"corps": [
|
||||
{"nom": "Bridget", "maillage": "/Game/Heavy/Daz/Bridget/bridget_gp.bridget_gp", "prefixe": "B_"},
|
||||
{"nom": "Michael", "maillage": "/Game/Heavy/Daz/Michael/michael.michael", "prefixe": "M_"},
|
||||
],
|
||||
"exclure": [],
|
||||
"pose_dir": None,
|
||||
"jeton": None,
|
||||
}
|
||||
NONE = unreal.Name()
|
||||
CFG = {}
|
||||
JETON = None
|
||||
|
||||
# Chaines derivees des profils de squelettes (lot AP1) : Tools/anim/profiles/*.json, lus aussi par la
|
||||
# sandbox (LPSkeletonProfile). Une chaine ne traverse jamais un os *Twist (hors chaine, il suit l'os Bend).
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PROFILE_DIR = os.path.join(HERE, "profiles")
|
||||
METHOD_DIR = os.path.join(HERE, "methodes")
|
||||
SIDES = (("l", "Left"), ("r", "Right"))
|
||||
FINGERS = (("thumb", "Thumb"), ("index", "Index"), ("middle", "Mid"), ("ring", "Ring"), ("pinky", "Pinky"))
|
||||
|
||||
|
||||
class Refusal(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def load_config(saved_dir):
|
||||
# Configuration par defaut = comportement Quaternius ; Saved/anim_retarget.json la complete champ par champ.
|
||||
config = json.loads(json.dumps(DEFAULT_CONFIG))
|
||||
path = os.path.join(saved_dir, "anim_retarget.json")
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8-sig") as handle:
|
||||
given = json.load(handle)
|
||||
global JETON
|
||||
JETON = str(given["jeton"]) if given.get("jeton") else None
|
||||
for key, value in given.items():
|
||||
if key not in DEFAULT_CONFIG:
|
||||
raise Refusal("champ inconnu %s dans %s" % (key, path))
|
||||
config[key] = value
|
||||
if config["mode"] is None:
|
||||
config["mode"] = "bake"
|
||||
legacy = os.path.join(saved_dir, "anim_to_genesis_step.txt")
|
||||
if os.path.exists(legacy):
|
||||
with open(legacy, "r", encoding="utf-8") as handle:
|
||||
config["mode"] = handle.read().strip().lower() or "bake"
|
||||
config["mode"] = str(config["mode"]).strip().lower()
|
||||
if config["mode"] not in ("bake", "rigs"):
|
||||
raise Refusal("etape inconnue %s" % config["mode"])
|
||||
config["clips_dir"] = config["clips_dir"].rstrip("/")
|
||||
config["out_dir"] = (config["out_dir"] or config["clips_dir"]).rstrip("/")
|
||||
if not config["dest"]:
|
||||
config["dest"] = "/Game/Quaternius/Retarget" if config["methode"] == "ue4" else "/Game/Retarget_%s" % config["methode"]
|
||||
config["dest"] = config["dest"].rstrip("/")
|
||||
if not config["pose_dir"]:
|
||||
config["pose_dir"] = os.path.join(saved_dir, "Measurement")
|
||||
if not config["corps"]:
|
||||
raise Refusal("aucun corps dans la configuration")
|
||||
return config
|
||||
|
||||
|
||||
def load_method(method_id, method_dir=None):
|
||||
path = os.path.join(method_dir or METHOD_DIR, "%s.json" % method_id)
|
||||
if not os.path.exists(path):
|
||||
raise Refusal("methode inconnue %s" % method_id)
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
method = json.load(handle)
|
||||
if method.get("factice") or method.get("copie"):
|
||||
raise Refusal("methode %s non implementee (factice ou copie)" % method_id)
|
||||
return method
|
||||
|
||||
|
||||
def suffix(config):
|
||||
# Les assets de la methode ue4 gardent leurs noms historiques ; les autres sont suffixes par la methode.
|
||||
return "" if config["methode"] == "ue4" else "_" + config["methode"]
|
||||
|
||||
|
||||
def source_rig_name(config):
|
||||
return "IK_Quaternius" if config["methode"] == "ue4" else "IK_Source" + suffix(config)
|
||||
|
||||
|
||||
def load_profiles():
|
||||
profiles = []
|
||||
for name in sorted(os.listdir(PROFILE_DIR)):
|
||||
if name.endswith(".json"):
|
||||
with open(os.path.join(PROFILE_DIR, name), "r", encoding="utf-8") as handle:
|
||||
profiles.append(json.load(handle))
|
||||
if not profiles:
|
||||
raise RuntimeError("aucun profil de squelette dans %s" % PROFILE_DIR)
|
||||
return profiles
|
||||
|
||||
|
||||
def role(profile, name, index=0):
|
||||
value = profile["roles"].get(name)
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, list):
|
||||
return value[index] if -len(value) <= index < len(value) else None
|
||||
return value if index in (0, -1) else None
|
||||
|
||||
|
||||
def identify_names(names, label="?"):
|
||||
# Meme regle que LPSkeletonProfile::Identify : au moins 95 % des os du profil presents ; a egalite de
|
||||
# score, le profil qui reconnait le plus d'os (le mannequin UE5 contient tous les noms d'os UE4).
|
||||
names = set(names)
|
||||
best, best_score, best_hits = None, 0.0, 0
|
||||
for profile in load_profiles():
|
||||
bones = []
|
||||
for value in profile["roles"].values():
|
||||
bones.extend(value if isinstance(value, list) else [value])
|
||||
hits = sum(1 for bone in bones if bone in names)
|
||||
score = round(hits / float(len(bones)), 6)
|
||||
if score > best_score or (score == best_score and hits > best_hits):
|
||||
best, best_score, best_hits = profile, score, hits
|
||||
if best is None or best_score < 0.95:
|
||||
raise RuntimeError("squelette inconnu %s (meilleur profil %.0f%%)" % (label, best_score * 100.0))
|
||||
return best, best_score
|
||||
|
||||
|
||||
def identify(mesh):
|
||||
best, best_score = identify_names(bone_names(mesh), mesh.get_name())
|
||||
unreal.log_warning("RETARGET profil %s -> %s (%.0f%%)" % (mesh.get_name(), best["id"], best_score * 100.0))
|
||||
return best
|
||||
|
||||
|
||||
def chains(profile):
|
||||
# (nom de chaine, premier os, dernier os) : memes noms des deux cotes pour auto_map_chains.
|
||||
made = [("Spine", role(profile, "spine", 0), role(profile, "spine", -1)),
|
||||
("Neck", role(profile, "neck", 0), role(profile, "head"))]
|
||||
for side, word in SIDES:
|
||||
made += [
|
||||
(word + "Collar", role(profile, "clavicle_" + side), role(profile, "upperarm_" + side)),
|
||||
(word + "Forearm", role(profile, "lowerarm_" + side), role(profile, "lowerarm_" + side)),
|
||||
(word + "Hand", role(profile, "hand_" + side), role(profile, "hand_" + side)),
|
||||
(word + "Thigh", role(profile, "thigh_" + side), role(profile, "thigh_" + side)),
|
||||
(word + "Calf", role(profile, "calf_" + side), role(profile, "calf_" + side)),
|
||||
(word + "Foot", role(profile, "foot_" + side), role(profile, "foot_" + side)),
|
||||
(word + "Toe", role(profile, "toe_" + side), role(profile, "toe_" + side)),
|
||||
]
|
||||
for finger, daz in FINGERS:
|
||||
made.append((word + daz, role(profile, finger + "_" + side, 0), role(profile, finger + "_" + side, -1)))
|
||||
missing = [name for name, start, end in made if not start or not end]
|
||||
if missing:
|
||||
raise RuntimeError("profil %s : chaines sans os %s" % (profile["id"], ", ".join(missing)))
|
||||
return made
|
||||
|
||||
|
||||
INTERPOLATED = ("Spine", "Neck")
|
||||
|
||||
|
||||
def make_factory(factory_class):
|
||||
factory = factory_class()
|
||||
factory.set_editor_property("edit_after_new", False)
|
||||
return factory
|
||||
|
||||
|
||||
def vsub(a, b):
|
||||
return unreal.MathLibrary.subtract_vector_vector(a, b)
|
||||
|
||||
|
||||
def vnorm(v):
|
||||
return unreal.MathLibrary.normal(v)
|
||||
|
||||
|
||||
def vdot(a, b):
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z
|
||||
|
||||
|
||||
def vcross(a, b):
|
||||
return unreal.MathLibrary.cross_vector_vector(a, b)
|
||||
|
||||
|
||||
def bone_names(mesh):
|
||||
pose = mesh.skeleton.get_reference_pose()
|
||||
return [str(name) for name in unreal.AnimPoseExtensions.get_bone_names(pose)]
|
||||
|
||||
|
||||
def pose_file(body):
|
||||
return os.path.join(CFG["pose_dir"], "retarget_pose_%s.txt" % body)
|
||||
|
||||
|
||||
def read_pose(body):
|
||||
path = pose_file(body)
|
||||
if not os.path.exists(path):
|
||||
raise RuntimeError("pose de retarget absente %s : ouvrir la sandbox Mesure une fois" % path)
|
||||
offsets = []
|
||||
with open(path, "r", encoding="utf-8-sig") as handle:
|
||||
for line in handle:
|
||||
cells = line.split()
|
||||
if len(cells) != 5 or cells[0].startswith("#"):
|
||||
continue
|
||||
offsets.append((cells[0], unreal.Quat(float(cells[1]), float(cells[2]), float(cells[3]), float(cells[4]))))
|
||||
return offsets
|
||||
|
||||
|
||||
def apply_pose(ctrl, mesh, body):
|
||||
side = unreal.RetargetSourceOrTarget.TARGET
|
||||
names = set(bone_names(mesh))
|
||||
applied = 0
|
||||
for bone, quat in read_pose(body):
|
||||
if bone not in names:
|
||||
unreal.log_warning("RETARGET pose %s os absent %s" % (body, bone))
|
||||
continue
|
||||
ctrl.set_rotation_offset_for_retarget_pose_bone(bone, quat, side)
|
||||
applied += 1
|
||||
unreal.log_warning("RETARGET pose %s %d os (pose %s ajustee)" % (body, applied, CFG["pose"]))
|
||||
|
||||
|
||||
def make_rig(name, mesh_path):
|
||||
asset_path = CFG["dest"] + "/" + name
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(asset_path):
|
||||
unreal.EditorAssetLibrary.delete_asset(asset_path)
|
||||
mesh = unreal.load_asset(mesh_path)
|
||||
profile = identify(mesh)
|
||||
root = role(profile, "hips")
|
||||
rig_chains = chains(profile)
|
||||
rig = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
||||
name, CFG["dest"], unreal.IKRigDefinition, make_factory(unreal.IKRigDefinitionFactory))
|
||||
ctrl = unreal.IKRigController.get_controller(rig)
|
||||
ctrl.set_skeletal_mesh(mesh)
|
||||
ctrl.set_retarget_root(root)
|
||||
for chain, start, end in rig_chains:
|
||||
made = str(ctrl.add_retarget_chain(chain, start, end, NONE))
|
||||
unreal.log_warning("RETARGET chaine %s %s %s -> %s" % (name, chain, start, made))
|
||||
if made == "None":
|
||||
raise RuntimeError("chaine impossible %s %s %s" % (name, chain, start))
|
||||
unreal.EditorAssetLibrary.save_loaded_asset(rig)
|
||||
return rig, mesh, rig_chains
|
||||
|
||||
|
||||
def make_retargeter(name, source_rig, target_rig, source_mesh, target_mesh, body, source_chains):
|
||||
asset_path = CFG["dest"] + "/" + name
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(asset_path):
|
||||
unreal.EditorAssetLibrary.delete_asset(asset_path)
|
||||
retargeter = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
|
||||
name, CFG["dest"], unreal.IKRetargeter, make_factory(unreal.IKRetargetFactory))
|
||||
ctrl = unreal.IKRetargeterController.get_controller(retargeter)
|
||||
ctrl.set_ik_rig(unreal.RetargetSourceOrTarget.SOURCE, source_rig)
|
||||
ctrl.set_ik_rig(unreal.RetargetSourceOrTarget.TARGET, target_rig)
|
||||
ctrl.set_preview_mesh(unreal.RetargetSourceOrTarget.SOURCE, source_mesh)
|
||||
ctrl.set_preview_mesh(unreal.RetargetSourceOrTarget.TARGET, target_mesh)
|
||||
ctrl.auto_map_chains(unreal.AutoMapChainType.EXACT, True)
|
||||
# Pose de retarget : la pose ajustee de la sandbox, et non plus l'alignement par le maillage,
|
||||
# les reglages Daz et les demi-tours d'epaule (pole / roll180), qu'elle remplace.
|
||||
apply_pose(ctrl, target_mesh, body)
|
||||
for chain_name, _, _ in source_chains:
|
||||
settings = ctrl.get_retarget_chain_settings(chain_name)
|
||||
fk = settings.get_editor_property("fk")
|
||||
mode = unreal.RetargetRotationMode.INTERPOLATED if chain_name in INTERPOLATED else unreal.RetargetRotationMode.ONE_TO_ONE
|
||||
fk.set_editor_property("rotation_mode", mode)
|
||||
fk.set_editor_property("translation_mode", unreal.RetargetTranslationMode.NONE)
|
||||
fk.set_editor_property("pole_vector_matching", 0.0)
|
||||
settings.set_editor_property("fk", fk)
|
||||
ctrl.set_retarget_chain_settings(chain_name, settings)
|
||||
unreal.EditorAssetLibrary.save_loaded_asset(retargeter)
|
||||
return retargeter
|
||||
|
||||
|
||||
def source_clips():
|
||||
prefixes = tuple(body["prefixe"] for body in CFG["corps"])
|
||||
clips = []
|
||||
for path in unreal.EditorAssetLibrary.list_assets(CFG["clips_dir"], False, False):
|
||||
asset = unreal.load_asset(path)
|
||||
if not isinstance(asset, unreal.AnimSequence):
|
||||
continue
|
||||
name = asset.get_name()
|
||||
if name.startswith(prefixes) or name in CFG["exclure"]:
|
||||
continue
|
||||
clips.append(unreal.EditorAssetLibrary.find_asset_data(path))
|
||||
return clips
|
||||
|
||||
|
||||
def delete_copies(names):
|
||||
# Seulement les copies des clips source. Pas tout asset dont le nom commence par un prefixe de corps.
|
||||
for name in names:
|
||||
for body in CFG["corps"]:
|
||||
for folder in (CFG["out_dir"] + "/", "/Game/"):
|
||||
path = folder + body["prefixe"] + name
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(path):
|
||||
unreal.EditorAssetLibrary.delete_asset(path)
|
||||
|
||||
|
||||
def tag(line):
|
||||
# AP7 : BuildMod ne retient que les lignes de resultat portant son jeton (cle "jeton" de anim_retarget.json).
|
||||
return "%s jeton %s" % (line, JETON) if JETON else line
|
||||
|
||||
|
||||
def finish(line):
|
||||
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
|
||||
unreal.log_warning(tag(line))
|
||||
unreal.SystemLibrary.quit_editor()
|
||||
|
||||
|
||||
def anim_pose(sequence, time):
|
||||
try:
|
||||
options = unreal.AnimPoseEvaluationOptions()
|
||||
for prop in ("should_retarget", "b_should_retarget"):
|
||||
try:
|
||||
options.set_editor_property(prop, False)
|
||||
except Exception:
|
||||
pass
|
||||
return unreal.AnimPoseExtensions.get_anim_pose_at_time(sequence, time, options)
|
||||
except Exception as exc:
|
||||
unreal.log_warning("RETARGET pose api %s" % exc)
|
||||
return unreal.AnimPoseExtensions.get_anim_pose_at_time(sequence, time)
|
||||
|
||||
|
||||
def hand_ahead(sequence, chest_name, hand_name, left_name, right_name, time, pelvis_name="pelvis"):
|
||||
pose = anim_pose(sequence, time)
|
||||
space = unreal.AnimPoseSpaces.WORLD
|
||||
|
||||
def loc(bone):
|
||||
return unreal.AnimPoseExtensions.get_bone_pose(pose, bone, space).translation
|
||||
|
||||
chest = loc(chest_name)
|
||||
pelvis = loc(pelvis_name)
|
||||
up = vnorm(vsub(chest, pelvis))
|
||||
side = vnorm(vsub(loc(left_name), loc(right_name)))
|
||||
forward = vnorm(vcross(side, up))
|
||||
return vdot(vsub(loc(hand_name), chest), forward)
|
||||
|
||||
|
||||
def log_hands(label):
|
||||
# Releve seulement : main devant (+) ou derriere (-) le buste sur le premier clip cuit du pack.
|
||||
# La mesure fine est la tournee de la sandbox. Sans clip cuit ou sans roles : on saute.
|
||||
profile = CFG["source_profile"]
|
||||
chest_src = role(profile, "spine", -1)
|
||||
hips_src = role(profile, "hips")
|
||||
hands_src = (role(profile, "hand_l"), role(profile, "hand_r"))
|
||||
collars_src = (role(profile, "clavicle_l"), role(profile, "clavicle_r"))
|
||||
if not (chest_src and hips_src and all(hands_src) and all(collars_src)):
|
||||
unreal.log_warning("RETARGET mesure sautee : roles du profil source incomplets")
|
||||
return
|
||||
first = None
|
||||
for body in CFG["corps"]:
|
||||
for data in source_clips():
|
||||
name = str(data.asset_name)
|
||||
if unreal.EditorAssetLibrary.does_asset_exist(CFG["out_dir"] + "/" + body["prefixe"] + name):
|
||||
first = name
|
||||
break
|
||||
if first:
|
||||
break
|
||||
if first is None:
|
||||
unreal.log_warning("RETARGET mesure sautee : aucun clip cuit")
|
||||
return
|
||||
spec = [(CFG["clips_dir"] + "/" + first, chest_src, hips_src, hands_src, collars_src)]
|
||||
for body in CFG["corps"]:
|
||||
spec.append((CFG["out_dir"] + "/" + body["prefixe"] + first, "chestUpper", "pelvis", ("lHand", "rHand"), ("lCollar", "rCollar")))
|
||||
for path, chest, pelvis, hands, collars in spec:
|
||||
leaf = path.split("/")[-1]
|
||||
sequence = unreal.load_asset(path + "." + leaf)
|
||||
if not sequence:
|
||||
unreal.log_warning("RETARGET mesure absente %s" % path)
|
||||
continue
|
||||
for hand in hands:
|
||||
ahead = hand_ahead(sequence, chest, hand, collars[0], collars[1], 0.0, pelvis)
|
||||
unreal.log_warning("RETARGET main %s %s %s devant %.1f" % (label, leaf, hand, ahead))
|
||||
|
||||
|
||||
def bake_named(clips, source_mesh, bodies):
|
||||
# bodies : [(corps, maillage, retargeter)] ; renvoie (nombre de copies par corps, nombre deplaces).
|
||||
made = []
|
||||
for body, mesh, retargeter in bodies:
|
||||
made.append(unreal.IKRetargetBatchOperation.duplicate_and_retarget(
|
||||
clips, source_mesh, mesh, retargeter, "", "", body["prefixe"], "", True))
|
||||
moved = 0
|
||||
for data in [item for group in made for item in group]:
|
||||
package = str(data.package_name)
|
||||
name = str(data.asset_name)
|
||||
dest = CFG["out_dir"] + "/" + name
|
||||
if package == dest or unreal.EditorAssetLibrary.rename_asset(package, dest):
|
||||
moved += 1
|
||||
else:
|
||||
unreal.log_error("RETARGET deplacement rate %s" % package)
|
||||
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
|
||||
return [len(group) for group in made], moved
|
||||
|
||||
|
||||
def run():
|
||||
global CFG
|
||||
CFG = load_config(unreal.Paths.project_saved_dir())
|
||||
method = load_method(CFG["methode"])
|
||||
CFG["pose"] = method["pose"]
|
||||
mode = CFG["mode"]
|
||||
unreal.log_warning("RETARGET etape %s" % mode)
|
||||
suf = suffix(CFG)
|
||||
probe = unreal.load_asset(CFG["source_mesh"])
|
||||
if not probe:
|
||||
raise Refusal("maillage source introuvable %s" % CFG["source_mesh"])
|
||||
source_profile, _ = identify_names(bone_names(probe), probe.get_name())
|
||||
if source_profile["id"] != method["profil_source"]:
|
||||
raise Refusal("profil du maillage source %s (%s) different de profil_source %s de la methode %s" % (
|
||||
probe.get_name(), source_profile["id"], method["profil_source"], CFG["methode"]))
|
||||
CFG["source_profile"] = source_profile
|
||||
unreal.log_warning("RETARGET methode %s profil %s calques %s" % (CFG["methode"], source_profile["id"], method["pose"]))
|
||||
source_rig, source_mesh, source_chains = make_rig(source_rig_name(CFG), CFG["source_mesh"])
|
||||
bodies = []
|
||||
for body in CFG["corps"]:
|
||||
rig, mesh, _ = make_rig("IK_" + body["nom"] + suf, body["maillage"])
|
||||
retargeter = make_retargeter("RTG_" + body["nom"] + suf, source_rig, rig, source_mesh, mesh, body["nom"], source_chains)
|
||||
bodies.append((body, mesh, retargeter))
|
||||
if mode == "rigs":
|
||||
finish("RETARGET rigs prets")
|
||||
return
|
||||
names = [str(data.asset_name) for data in source_clips()]
|
||||
delete_copies(names)
|
||||
clips = source_clips()
|
||||
unreal.log_warning("RETARGET clips source %d" % len(clips))
|
||||
counts, moved = bake_named(clips, source_mesh, bodies)
|
||||
try:
|
||||
log_hands("final")
|
||||
except Exception as exc:
|
||||
unreal.log_warning("RETARGET mesure finale ratee %s" % exc)
|
||||
done = " ".join("%s %d" % (body["nom"].lower(), count) for (body, _, _), count in zip(bodies, counts))
|
||||
finish("RETARGET %s deplaces %d pose %s ajustee" % (done, moved, method["pose"]))
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
run()
|
||||
except Refusal as exc:
|
||||
unreal.log_warning(tag("RETARGET echec : %s" % exc))
|
||||
unreal.SystemLibrary.quit_editor()
|
||||
except Exception as exc:
|
||||
unreal.log_warning(tag("RETARGET echec : %s" % exc))
|
||||
unreal.SystemLibrary.quit_editor()
|
||||
raise
|
||||
|
||||
|
||||
if not os.environ.get("LP_RETARGET_NOMAIN"):
|
||||
main()
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"id": "genesis_direct",
|
||||
"nom": "Genesis 8 vers Genesis 8 (copie)",
|
||||
"profil_source": "genesis8",
|
||||
"pose": "A",
|
||||
"copie": true,
|
||||
"factice": true,
|
||||
"description": "Source Daz (DUF) sur le meme squelette que le corps : copie sans transformation. Methode declaree, non implementee : la chaine la refuse avec un message clair.",
|
||||
"exemples": [
|
||||
"poses et animations Daz Studio (.duf)"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"id": "ue4",
|
||||
"nom": "Mannequin UE4 vers Genesis 8 (T vers T)",
|
||||
"profil_source": "ue4_mannequin",
|
||||
"pose": "T",
|
||||
"description": "Le mannequin UE4 est livre en pose T : calque d'origine = repos du mannequin ; calque du corps = repos Genesis + reglages Daz A vers T du profil genesis8, ajuste a la morphologie (phase A). Ancienne methode 'Quaternius'.",
|
||||
"exemples": [
|
||||
"Quaternius Universal Animation Library"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"id": "ue5",
|
||||
"nom": "Mannequin UE5 vers Genesis 8 (A vers A)",
|
||||
"profil_source": "ue5_mannequin",
|
||||
"pose": "A",
|
||||
"description": "Le mannequin UE5 est livre en pose A, comme Bridget et Michael : calque d'origine = repos du mannequin ; calque du corps = repos Genesis (A), ajuste a la morphologie (phase A). Aucun reglage A vers T.",
|
||||
"exemples": [
|
||||
"FreeAnimationsPack (SK_Mannequin Manny / Quinn)"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
{
|
||||
"id": "genesis8",
|
||||
"nom": "Daz Genesis 8 (Bridget, Michael)",
|
||||
"description": "Repos en pose A. Le pelvis Genesis est un enfant de hip, frere d'abdomenLower : la colonne part d'abdomenLower. Les os Twist (epaule, avant-bras, cuisse) recoivent la torsion.",
|
||||
"pose_t": {
|
||||
"type": "reglages",
|
||||
"source": "molettes Daz publiees, pose A Genesis 8 vers T (CHG-183)",
|
||||
"reglages": [
|
||||
{ "os": "lCollar", "enfant": "lShldrBend", "cible": "plat", "degres": 2.5 },
|
||||
{ "os": "rCollar", "enfant": "rShldrBend", "cible": "plat", "degres": 2.5 },
|
||||
{ "os": "lShldrBend", "enfant": "lForearmBend", "cible": "plat", "degres": 43.5 },
|
||||
{ "os": "rShldrBend", "enfant": "rForearmBend", "cible": "plat", "degres": 43.5 },
|
||||
{ "os": "lThighBend", "enfant": "lShin", "cible": "bas", "degres": 6.0 },
|
||||
{ "os": "rThighBend", "enfant": "rShin", "cible": "bas", "degres": 6.0 }
|
||||
]
|
||||
},
|
||||
"roles": {
|
||||
"hips": "hip",
|
||||
"pelvis": "pelvis",
|
||||
"spine": ["abdomenLower", "abdomenUpper", "chestLower", "chestUpper"],
|
||||
"neck": ["neckLower", "neckUpper"],
|
||||
"head": "head",
|
||||
"clavicle_l": "lCollar",
|
||||
"upperarm_l": "lShldrBend",
|
||||
"upperarm_twist_l": "lShldrTwist",
|
||||
"lowerarm_l": "lForearmBend",
|
||||
"lowerarm_twist_l": "lForearmTwist",
|
||||
"hand_l": "lHand",
|
||||
"thumb_l": ["lThumb1", "lThumb2", "lThumb3"],
|
||||
"index_l": ["lIndex1", "lIndex2", "lIndex3"],
|
||||
"middle_l": ["lMid1", "lMid2", "lMid3"],
|
||||
"ring_l": ["lRing1", "lRing2", "lRing3"],
|
||||
"pinky_l": ["lPinky1", "lPinky2", "lPinky3"],
|
||||
"thigh_l": "lThighBend",
|
||||
"thigh_twist_l": "lThighTwist",
|
||||
"calf_l": "lShin",
|
||||
"foot_l": "lFoot",
|
||||
"toe_l": "lToe",
|
||||
"clavicle_r": "rCollar",
|
||||
"upperarm_r": "rShldrBend",
|
||||
"upperarm_twist_r": "rShldrTwist",
|
||||
"lowerarm_r": "rForearmBend",
|
||||
"lowerarm_twist_r": "rForearmTwist",
|
||||
"hand_r": "rHand",
|
||||
"thumb_r": ["rThumb1", "rThumb2", "rThumb3"],
|
||||
"index_r": ["rIndex1", "rIndex2", "rIndex3"],
|
||||
"middle_r": ["rMid1", "rMid2", "rMid3"],
|
||||
"ring_r": ["rRing1", "rRing2", "rRing3"],
|
||||
"pinky_r": ["rPinky1", "rPinky2", "rPinky3"],
|
||||
"thigh_r": "rThighBend",
|
||||
"thigh_twist_r": "rThighTwist",
|
||||
"calf_r": "rShin",
|
||||
"foot_r": "rFoot",
|
||||
"toe_r": "rToe"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"id": "ue4_mannequin",
|
||||
"nom": "Mannequin UE4 (nomenclature SK_Mannequin) : Quaternius Universal Animation Library",
|
||||
"description": "Repos en pose T. Roles : un nom d'os, ou une liste ordonnee de la racine vers le bout pour une chaine.",
|
||||
"pose_t": { "type": "repos", "reglages": [] },
|
||||
"roles": {
|
||||
"hips": "pelvis",
|
||||
"pelvis": "pelvis",
|
||||
"spine": ["spine_01", "spine_02", "spine_03"],
|
||||
"neck": ["neck_01"],
|
||||
"head": "Head",
|
||||
"clavicle_l": "clavicle_l",
|
||||
"upperarm_l": "upperarm_l",
|
||||
"lowerarm_l": "lowerarm_l",
|
||||
"hand_l": "hand_l",
|
||||
"thumb_l": ["thumb_01_l", "thumb_02_l", "thumb_03_l"],
|
||||
"index_l": ["index_01_l", "index_02_l", "index_03_l"],
|
||||
"middle_l": ["middle_01_l", "middle_02_l", "middle_03_l"],
|
||||
"ring_l": ["ring_01_l", "ring_02_l", "ring_03_l"],
|
||||
"pinky_l": ["pinky_01_l", "pinky_02_l", "pinky_03_l"],
|
||||
"thigh_l": "thigh_l",
|
||||
"calf_l": "calf_l",
|
||||
"foot_l": "foot_l",
|
||||
"toe_l": "ball_l",
|
||||
"clavicle_r": "clavicle_r",
|
||||
"upperarm_r": "upperarm_r",
|
||||
"lowerarm_r": "lowerarm_r",
|
||||
"hand_r": "hand_r",
|
||||
"thumb_r": ["thumb_01_r", "thumb_02_r", "thumb_03_r"],
|
||||
"index_r": ["index_01_r", "index_02_r", "index_03_r"],
|
||||
"middle_r": ["middle_01_r", "middle_02_r", "middle_03_r"],
|
||||
"ring_r": ["ring_01_r", "ring_02_r", "ring_03_r"],
|
||||
"pinky_r": ["pinky_01_r", "pinky_02_r", "pinky_03_r"],
|
||||
"thigh_r": "thigh_r",
|
||||
"calf_r": "calf_r",
|
||||
"foot_r": "foot_r",
|
||||
"toe_r": "ball_r"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
{
|
||||
"id": "ue5_mannequin",
|
||||
"nom": "Mannequin UE5 (SK_Mannequin Manny / Quinn) : FreeAnimationsPack et packs Epic UE5",
|
||||
"description": "Repos en pose A (bras 54,8 deg sous l'horizontale). Colonne a 5 os, cou a 2 os. Os de torsion, metacarpes et correctifs hors roles (pilotes, non transferes). Roles : un nom d'os, ou une liste ordonnee de la racine vers le bout pour une chaine.",
|
||||
"pose_t": {
|
||||
"type": "reglages",
|
||||
"source": "Reglages A vers T utilises seulement par une methode en pose T (comparaison) ; la methode ue5 (pose A) prend le repos tel quel. repos SK_Mannequin releve par -run=LPAnimIdentify (AP7) ; cibles = pose T du mannequin Quaternius (clavicule 4,42 deg sous l'horizontale, cuisse 0,29 deg) ; reglages cumules du parent a l'enfant (bras 54,80 - 4,29)",
|
||||
"reglages": [
|
||||
{
|
||||
"os": "clavicle_l",
|
||||
"enfant": "upperarm_l",
|
||||
"cible": "plat",
|
||||
"degres": 4.29
|
||||
},
|
||||
{
|
||||
"os": "clavicle_r",
|
||||
"enfant": "upperarm_r",
|
||||
"cible": "plat",
|
||||
"degres": 4.29
|
||||
},
|
||||
{
|
||||
"os": "upperarm_l",
|
||||
"enfant": "lowerarm_l",
|
||||
"cible": "plat",
|
||||
"degres": 50.51
|
||||
},
|
||||
{
|
||||
"os": "upperarm_r",
|
||||
"enfant": "lowerarm_r",
|
||||
"cible": "plat",
|
||||
"degres": 50.51
|
||||
},
|
||||
{
|
||||
"os": "thigh_l",
|
||||
"enfant": "calf_l",
|
||||
"cible": "bas",
|
||||
"degres": 2.76
|
||||
},
|
||||
{
|
||||
"os": "thigh_r",
|
||||
"enfant": "calf_r",
|
||||
"cible": "bas",
|
||||
"degres": 2.76
|
||||
}
|
||||
]
|
||||
},
|
||||
"roles": {
|
||||
"hips": "pelvis",
|
||||
"pelvis": "pelvis",
|
||||
"spine": [
|
||||
"spine_01",
|
||||
"spine_02",
|
||||
"spine_03",
|
||||
"spine_04",
|
||||
"spine_05"
|
||||
],
|
||||
"neck": [
|
||||
"neck_01",
|
||||
"neck_02"
|
||||
],
|
||||
"head": "head",
|
||||
"clavicle_l": "clavicle_l",
|
||||
"upperarm_l": "upperarm_l",
|
||||
"lowerarm_l": "lowerarm_l",
|
||||
"hand_l": "hand_l",
|
||||
"thumb_l": [
|
||||
"thumb_01_l",
|
||||
"thumb_02_l",
|
||||
"thumb_03_l"
|
||||
],
|
||||
"index_l": [
|
||||
"index_01_l",
|
||||
"index_02_l",
|
||||
"index_03_l"
|
||||
],
|
||||
"middle_l": [
|
||||
"middle_01_l",
|
||||
"middle_02_l",
|
||||
"middle_03_l"
|
||||
],
|
||||
"ring_l": [
|
||||
"ring_01_l",
|
||||
"ring_02_l",
|
||||
"ring_03_l"
|
||||
],
|
||||
"pinky_l": [
|
||||
"pinky_01_l",
|
||||
"pinky_02_l",
|
||||
"pinky_03_l"
|
||||
],
|
||||
"thigh_l": "thigh_l",
|
||||
"calf_l": "calf_l",
|
||||
"foot_l": "foot_l",
|
||||
"toe_l": "ball_l",
|
||||
"clavicle_r": "clavicle_r",
|
||||
"upperarm_r": "upperarm_r",
|
||||
"lowerarm_r": "lowerarm_r",
|
||||
"hand_r": "hand_r",
|
||||
"thumb_r": [
|
||||
"thumb_01_r",
|
||||
"thumb_02_r",
|
||||
"thumb_03_r"
|
||||
],
|
||||
"index_r": [
|
||||
"index_01_r",
|
||||
"index_02_r",
|
||||
"index_03_r"
|
||||
],
|
||||
"middle_r": [
|
||||
"middle_01_r",
|
||||
"middle_02_r",
|
||||
"middle_03_r"
|
||||
],
|
||||
"ring_r": [
|
||||
"ring_01_r",
|
||||
"ring_02_r",
|
||||
"ring_03_r"
|
||||
],
|
||||
"pinky_r": [
|
||||
"pinky_01_r",
|
||||
"pinky_02_r",
|
||||
"pinky_03_r"
|
||||
],
|
||||
"thigh_r": "thigh_r",
|
||||
"calf_r": "calf_r",
|
||||
"foot_r": "foot_r",
|
||||
"toe_r": "ball_r"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
@echo off
|
||||
rem Meme chambre que la sandbox pair. Le menu en haut a gauche choisit Interaction ou Pair.
|
||||
rem L'instance de jeu du Blueprint n'est pas chargee. Aucun Blueprint n'est modifie.
|
||||
setlocal
|
||||
set "UE=D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe"
|
||||
set "PROJ=%~dp0..\..\LifePlay.uproject"
|
||||
start "" "%UE%" "%PROJ%" "/Engine/Maps/Entry?game=/Script/LPSandbox.LPPairSandboxGameMode" -game -windowed -ResX=1600 -ResY=900 "-ini:Engine:[/Script/EngineSettings.GameMapsSettings]:GameInstanceClass=/Script/Engine.GameInstance"
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
@echo off
|
||||
rem Chambre vide measurement : mannequin, Bridget et Michael, pose figee.
|
||||
rem L'instance de jeu du Blueprint n'est pas chargee. Aucun Blueprint n'est modifie.
|
||||
setlocal
|
||||
set "ROOT=%~dp0..\.."
|
||||
echo Mesure>"%ROOT%\Saved\sandbox_mode.txt"
|
||||
set "UE=D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe"
|
||||
set "PROJ=%~dp0..\..\LifePlay.uproject"
|
||||
start "" "%UE%" "%PROJ%" "/Engine/Maps/Entry?game=/Script/LPSandbox.LPPairSandboxGameMode" -game -windowed -ResX=1600 -ResY=900 "-ini:Engine:[/Script/EngineSettings.GameMapsSettings]:GameInstanceClass=/Script/Engine.GameInstance"
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
@echo off
|
||||
rem Fenetre a part de la partie : chambre du bureau. Menu : Pair ou Interaction.
|
||||
rem L'instance de jeu du Blueprint n'est pas chargee. Aucun Blueprint n'est modifie.
|
||||
setlocal
|
||||
set "UE=D:\Game\Launcher\Engine\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe"
|
||||
set "PROJ=%~dp0..\..\LifePlay.uproject"
|
||||
start "" "%UE%" "%PROJ%" "/Engine/Maps/Entry?game=/Script/LPSandbox.LPPairSandboxGameMode" -game -windowed -ResX=1600 -ResY=900 "-ini:Engine:[/Script/EngineSettings.GameMapsSettings]:GameInstanceClass=/Script/Engine.GameInstance"
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
{
|
||||
"module": "lp_Revival",
|
||||
"folder": "/Game/Heavy/New5/Hairs",
|
||||
"backup": "D:/Game/LifePlay_Backup_5.5/HAIRS1",
|
||||
"hairs": [
|
||||
{
|
||||
"name": "Pocolov03",
|
||||
"archive": "D:/Game/pocolov-hair-03.zip",
|
||||
"title": "Pocolov Hair 03",
|
||||
"url": "https://www.fab.com/listings/2257098d-b77c-4eef-9132-85f1454f0ae3",
|
||||
"mesh": "/Game/Heavy/New5/Hairs/_Work_Pocolov03/Box001",
|
||||
"head": "/Game/Heavy/New5/Hairs/_Work_Pocolov03/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov03/norm/tex10_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov03/norm/tex12_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov06",
|
||||
"archive": "D:/Game/pocolov-hair-06.zip",
|
||||
"title": "Pocolov Hair 06",
|
||||
"url": "https://www.fab.com/listings/14c3cd52-02f9-4bfe-bb40-b5c3acb1f428",
|
||||
"mesh": "/Game/Heavy/New5/Hairs/_Work_Pocolov06/Hair_06",
|
||||
"head": "/Game/Heavy/New5/Hairs/_Work_Pocolov06/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov06/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov06/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov11",
|
||||
"archive": "D:/Game/pocolov-hair-11.zip",
|
||||
"title": "Pocolov Hair 11",
|
||||
"url": "https://www.fab.com/listings/c31c219e-bc62-4957-91c5-720440375f79",
|
||||
"mesh": "/Game/Heavy/New5/Hairs/_Work_Pocolov11/Hair_11",
|
||||
"head": "/Game/Heavy/New5/Hairs/_Work_Pocolov11/Head",
|
||||
"cap_slots": "",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov11/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov11/norm/tex04_Hair_Texture_A.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov15",
|
||||
"archive": "D:/Game/pocolov-hair-15.zip",
|
||||
"title": "Pocolov Hair 15",
|
||||
"url": "https://www.fab.com/listings/ed8e016a-0bf0-4a6c-94b0-2760083b879d",
|
||||
"mesh": "/Game/Heavy/New5/Hairs/_Work_Pocolov15/Hair_15",
|
||||
"head": "/Game/Heavy/New5/Hairs/_Work_Pocolov15/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov15/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov15/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov16",
|
||||
"archive": "D:/Game/pocolov-hair-16.zip",
|
||||
"title": "Pocolov Hair 16",
|
||||
"url": "https://www.fab.com/listings/802dc956-6211-4b83-8ba1-37beb3cb338f",
|
||||
"mesh": "/Game/Heavy/New5/Hairs/_Work_Pocolov16/Hair_16",
|
||||
"head": "/Game/Heavy/New5/Hairs/_Work_Pocolov16/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov16/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov16/norm/tex04_Hair_01A.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov17",
|
||||
"archive": "D:/Game/pocolov-hair-17.zip",
|
||||
"title": "Pocolov Hair 17",
|
||||
"url": "https://www.fab.com/listings/7f6f687d-bb13-40c6-8b69-64044ededc1d",
|
||||
"mesh": "/Game/Heavy/New5/Hairs/_Work_Pocolov17/Hair_17",
|
||||
"head": "/Game/Heavy/New5/Hairs/_Work_Pocolov17/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov17/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/HAIRS1/Pocolov17/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio 2019, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
{
|
||||
"mod": "lpr_hair_pocolov",
|
||||
"module": "lpr_hair_pocolov",
|
||||
"folder": "/lpr_hair_pocolov/Hairs",
|
||||
"backup": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov",
|
||||
"hairs": [
|
||||
{
|
||||
"name": "Pocolov03",
|
||||
"archive": "D:/Game/pocolov-hair-03.zip",
|
||||
"title": "Pocolov Hair 03",
|
||||
"url": "https://www.fab.com/listings/2257098d-b77c-4eef-9132-85f1454f0ae3",
|
||||
"mesh": "/lpr_hair_pocolov/Hairs/_Work_Pocolov03/Box001",
|
||||
"head": "/lpr_hair_pocolov/Hairs/_Work_Pocolov03/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov03/norm/tex10_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov03/norm/tex12_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov05",
|
||||
"archive": "D:/Game/pocolov-hair-05.zip",
|
||||
"title": "Pocolov Hair 05",
|
||||
"url": "https://www.fab.com/listings/4025749f-d597-4332-91aa-16e355b94974",
|
||||
"mesh": "/lpr_hair_pocolov/Hairs/_Work_Pocolov05/Box001",
|
||||
"head": "/lpr_hair_pocolov/Hairs/_Work_Pocolov05/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov05/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov05/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov06",
|
||||
"archive": "D:/Game/pocolov-hair-06.zip",
|
||||
"title": "Pocolov Hair 06",
|
||||
"url": "https://www.fab.com/listings/14c3cd52-02f9-4bfe-bb40-b5c3acb1f428",
|
||||
"mesh": "/lpr_hair_pocolov/Hairs/_Work_Pocolov06/Hair_06",
|
||||
"head": "/lpr_hair_pocolov/Hairs/_Work_Pocolov06/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov06/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov06/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov07",
|
||||
"archive": "D:/Game/pocolov-hair-07.zip",
|
||||
"title": "Pocolov Hair 07",
|
||||
"url": "https://www.fab.com/listings/4d1ba957-b420-4bdc-8984-b528a73b1d58",
|
||||
"mesh": "/lpr_hair_pocolov/Hairs/_Work_Pocolov07/Hair_07",
|
||||
"head": "/lpr_hair_pocolov/Hairs/_Work_Pocolov07/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov07/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov07/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov09",
|
||||
"archive": "D:/Game/pocolov-hair-09.zip",
|
||||
"title": "Pocolov Hair 09",
|
||||
"url": "https://www.fab.com/listings/80383c46-f491-4db2-a63d-48d279f35782",
|
||||
"mesh": "/lpr_hair_pocolov/Hairs/_Work_Pocolov09/Hair_09",
|
||||
"head": "/lpr_hair_pocolov/Hairs/_Work_Pocolov09/Head",
|
||||
"cap_slots": "",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov09/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov09/norm/tex04_Hair_Texture_A.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov10",
|
||||
"archive": "D:/Game/pocolov-hair-10.zip",
|
||||
"title": "Pocolov Hair 10",
|
||||
"url": "https://www.fab.com/listings/40a91685-76dc-4384-91f1-0040f44e8958",
|
||||
"mesh": "/lpr_hair_pocolov/Hairs/_Work_Pocolov10/Hair_10",
|
||||
"head": "/lpr_hair_pocolov/Hairs/_Work_Pocolov10/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov10/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov10/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov11",
|
||||
"archive": "D:/Game/pocolov-hair-11.zip",
|
||||
"title": "Pocolov Hair 11",
|
||||
"url": "https://www.fab.com/listings/c31c219e-bc62-4957-91c5-720440375f79",
|
||||
"mesh": "/lpr_hair_pocolov/Hairs/_Work_Pocolov11/Hair_11",
|
||||
"head": "/lpr_hair_pocolov/Hairs/_Work_Pocolov11/Head",
|
||||
"cap_slots": "",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov11/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov11/norm/tex04_Hair_Texture_A.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov13",
|
||||
"archive": "D:/Game/pocolov-hair-13.zip",
|
||||
"title": "Pocolov Hair 13",
|
||||
"url": "https://www.fab.com/listings/a3933681-f5e3-4d93-99f0-c5fff444d405",
|
||||
"mesh": "/lpr_hair_pocolov/Hairs/_Work_Pocolov13/Hair_13",
|
||||
"head": "/lpr_hair_pocolov/Hairs/_Work_Pocolov13/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov13/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov13/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov14",
|
||||
"archive": "D:/Game/pocolov-hair-14.zip",
|
||||
"title": "Pocolov Hair 14",
|
||||
"url": "https://www.fab.com/listings/4911f6b9-d548-45d7-86be-a6971da97c54",
|
||||
"mesh": "/lpr_hair_pocolov/Hairs/_Work_Pocolov14/Hair_14",
|
||||
"head": "/lpr_hair_pocolov/Hairs/_Work_Pocolov14/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov14/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov14/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov15",
|
||||
"archive": "D:/Game/pocolov-hair-15.zip",
|
||||
"title": "Pocolov Hair 15",
|
||||
"url": "https://www.fab.com/listings/ed8e016a-0bf0-4a6c-94b0-2760083b879d",
|
||||
"mesh": "/lpr_hair_pocolov/Hairs/_Work_Pocolov15/Hair_15",
|
||||
"head": "/lpr_hair_pocolov/Hairs/_Work_Pocolov15/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov15/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov15/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov16",
|
||||
"archive": "D:/Game/pocolov-hair-16.zip",
|
||||
"title": "Pocolov Hair 16",
|
||||
"url": "https://www.fab.com/listings/802dc956-6211-4b83-8ba1-37beb3cb338f",
|
||||
"mesh": "/lpr_hair_pocolov/Hairs/_Work_Pocolov16/Hair_16",
|
||||
"head": "/lpr_hair_pocolov/Hairs/_Work_Pocolov16/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov16/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov16/norm/tex04_Hair_01A.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
},
|
||||
{
|
||||
"name": "Pocolov17",
|
||||
"archive": "D:/Game/pocolov-hair-17.zip",
|
||||
"title": "Pocolov Hair 17",
|
||||
"url": "https://www.fab.com/listings/7f6f687d-bb13-40c6-8b69-64044ededc1d",
|
||||
"mesh": "/lpr_hair_pocolov/Hairs/_Work_Pocolov17/Hair_17",
|
||||
"head": "/lpr_hair_pocolov/Hairs/_Work_Pocolov17/Head",
|
||||
"cap_slots": "Hair_Base",
|
||||
"diffuse": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov17/norm/tex01_Hair_01_Gray.png",
|
||||
"strands_opacity": "D:/Game/LifePlay_Backup_5.5/mods/lpr_hair_pocolov/Pocolov17/norm/tex02_Hair_01A1.jpg",
|
||||
"cap_opacity": "white",
|
||||
"thicken": 1,
|
||||
"sex": "F",
|
||||
"license": "CC BY 4.0",
|
||||
"author": "Pocolov (Pocolov Studio 2019, artist Thang)",
|
||||
"auto_fields": [
|
||||
"mesh",
|
||||
"head",
|
||||
"cap_slots",
|
||||
"diffuse",
|
||||
"strands_opacity"
|
||||
],
|
||||
"inspected": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue