diff --git a/.gitignore b/.gitignore index a1790bf..89a3f10 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/AnimSources/freeanims/pack.json b/AnimSources/freeanims/pack.json deleted file mode 100644 index f861701..0000000 --- a/AnimSources/freeanims/pack.json +++ /dev/null @@ -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." - } - } - } -} diff --git a/AnimSources/quaternius/pack.json b/AnimSources/quaternius/pack.json deleted file mode 100644 index de652ff..0000000 --- a/AnimSources/quaternius/pack.json +++ /dev/null @@ -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." - } - } - } -} diff --git a/Config/LPSandbox.ini b/Config/LPSandbox.ini deleted file mode 100644 index 333a510..0000000 --- a/Config/LPSandbox.ini +++ /dev/null @@ -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 diff --git a/Docs/Migration/09_Commandes_Utiles.md b/Docs/Migration/09_Commandes_Utiles.md index db91af4..c256646 100644 --- a/Docs/Migration/09_Commandes_Utiles.md +++ b/Docs/Migration/09_Commandes_Utiles.md @@ -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\ - - - diff --git a/ModSources/lpr_anim_freeanims/Module/Animations/catalogue.json b/ModSources/lpr_anim_freeanims/Module/Animations/catalogue.json deleted file mode 100644 index 2cff1c0..0000000 --- a/ModSources/lpr_anim_freeanims/Module/Animations/catalogue.json +++ /dev/null @@ -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" - } - ] -} diff --git a/ModSources/lpr_anim_freeanims/Module/CREDITS.txt b/ModSources/lpr_anim_freeanims/Module/CREDITS.txt deleted file mode 100644 index 15aa2ed..0000000 --- a/ModSources/lpr_anim_freeanims/Module/CREDITS.txt +++ /dev/null @@ -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__F / fap__M. diff --git a/ModSources/lpr_anim_freeanims/Module/lpr_anim_freeanims.lpmod b/ModSources/lpr_anim_freeanims/Module/lpr_anim_freeanims.lpmod deleted file mode 100644 index a5997ef..0000000 --- a/ModSources/lpr_anim_freeanims/Module/lpr_anim_freeanims.lpmod +++ /dev/null @@ -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 diff --git a/ModSources/lpr_anim_freeanims/lpr_anim_freeanims.uplugin b/ModSources/lpr_anim_freeanims/lpr_anim_freeanims.uplugin deleted file mode 100644 index 6680103..0000000 --- a/ModSources/lpr_anim_freeanims/lpr_anim_freeanims.uplugin +++ /dev/null @@ -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" -} diff --git a/ModSources/lpr_anim_quaternius/Module/Animations/catalogue.html b/ModSources/lpr_anim_quaternius/Module/Animations/catalogue.html deleted file mode 100644 index 3d5211b..0000000 --- a/ModSources/lpr_anim_quaternius/Module/Animations/catalogue.html +++ /dev/null @@ -1,106 +0,0 @@ - - - - - -Animations — lpr_anim_quaternius - - - -
-

Animations — lpr_anim_quaternius

-
Version 1.0 — 42 animations — généré le 2026-10-07. Copiez la clé puis écrivez Actor.animate(clé) dans une scène ou ANIMATION: clé dans une action. Survolez une vignette pour voir le mouvement.
-
- - - - -
-
-
-
Aucune animation ne correspond aux filtres.
-
-
Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com
- - - - diff --git a/ModSources/lpr_anim_quaternius/Module/Animations/catalogue.json b/ModSources/lpr_anim_quaternius/Module/Animations/catalogue.json deleted file mode 100644 index 2b474c3..0000000 --- a/ModSources/lpr_anim_quaternius/Module/Animations/catalogue.json +++ /dev/null @@ -1,3187 +0,0 @@ -{ - "pack": "quaternius", - "mod": "lpr_anim_quaternius", - "version": "1.0", - "genere": "2026-10-07T00:33:28", - "nombre_animations": 42, - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "animations": [ - { - "cle": "qua_crouch_fwd_loop", - "appel": { - "script": "Actor.animate(qua_crouch_fwd_loop)", - "action": "ANIMATION: qua_crouch_fwd_loop" - }, - "nom": { - "fr": "Marche accroupie", - "en": "Crouch walk" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_crouch_fwd_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_crouch_fwd_loop_M" - }, - "duree_s": 2.0, - "images": 61, - "planche": { - "fichier": "planches/qua_crouch_fwd_loop.jpg", - "images": 24, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 531, - "masculin": 521, - "total": 1052 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "variable", - "posture_origine": "calculee" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 79, - "droite": 74 - }, - "mains": { - "gauche": "repos", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "locomotion", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "5BF28444BC7AFA908501B19C131EFF428F4C2FCE8C4DBEFBD33BF236C93776FC" - }, - { - "cle": "qua_crouch_idle_loop", - "appel": { - "script": "Actor.animate(qua_crouch_idle_loop)", - "action": "ANIMATION: qua_crouch_idle_loop" - }, - "nom": { - "fr": "Attente accroupie", - "en": "Crouch idle" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_crouch_idle_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_crouch_idle_loop_M" - }, - "duree_s": 2.93, - "images": 89, - "planche": { - "fichier": "planches/qua_crouch_idle_loop.jpg", - "images": 24, - "taille": 256, - "ips": 8.2 - }, - "cadence": 30, - "taille_ko": { - "feminin": 817, - "masculin": 808, - "total": 1625 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "accroupi", - "posture_origine": "calculee" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 100, - "droite": 100 - }, - "mains": { - "gauche": "repos", - "droite": "repos" - } - }, - "accessoire": "", - "categorie": "attente", - "style": "neutre", - "usage": { - "fr": "Boucle d'attente accroupie, pour se cacher ou observer discrètement.", - "en": "Looping crouched idle, for hiding or watching discreetly." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "6DC0257A33153B969048CA7182204B0567CBECA88645FF8019D68707B2D560FC" - }, - { - "cle": "qua_dance_loop", - "appel": { - "script": "Actor.animate(qua_dance_loop)", - "action": "ANIMATION: qua_dance_loop" - }, - "nom": { - "fr": "Danse", - "en": "Dance" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_dance_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_dance_loop_M" - }, - "duree_s": 1.0, - "images": 31, - "planche": { - "fichier": "planches/qua_dance_loop.jpg", - "images": 12, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 500, - "masculin": 492, - "total": 992 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "mouvement" - } - }, - "accessoire": "", - "categorie": "danse", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "173C48D072A801D11A4230C25CD7C7FA6ACBB4EBF3C2DDC946828BEFF012BCBF" - }, - { - "cle": "qua_death01", - "appel": { - "script": "Actor.animate(qua_death01)", - "action": "ANIMATION: qua_death01" - }, - "nom": { - "fr": "Mort", - "en": "Death" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_death01_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_death01_M" - }, - "duree_s": 2.4, - "images": 73, - "planche": { - "fichier": "planches/qua_death01.jpg", - "images": 24, - "taille": 256, - "ips": 10.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 786, - "masculin": 777, - "total": 1563 - }, - "taille_cuite": false, - "boucle": { - "oui": false, - "ecart_pct": 95.63 - }, - "deplacement": { - "cm": 53, - "cm_s": 22, - "sens": "arriere" - }, - "posture": { - "principale": "variable", - "posture_origine": "calculee", - "debut": "debout", - "fin": "allonge" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 86, - "droite": 81 - }, - "mains": { - "gauche": "mouvement", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "chute", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "à revoir", - "motifs": [ - "Bridget : sol cm 5.6 (lFoot)", - "Michael : sol cm 5.6 (lFoot)" - ], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "1CF9ACECB34851BB084E9D312002B93BE1E05DFCA5DA661F085B542E6126FD75" - }, - { - "cle": "qua_driving_loop", - "appel": { - "script": "Actor.animate(qua_driving_loop)", - "action": "ANIMATION: qua_driving_loop" - }, - "nom": { - "fr": "Conduite", - "en": "Driving" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_driving_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_driving_loop_M" - }, - "duree_s": 1.67, - "images": 51, - "planche": { - "fichier": "planches/qua_driving_loop.jpg", - "images": 20, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 470, - "masculin": 460, - "total": 930 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "assis", - "posture_origine": "calculee", - "hauteur_assise_cm": 45 - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 100, - "droite": 100 - }, - "mains": { - "gauche": "repos", - "droite": "repos" - } - }, - "accessoire": "volant", - "categorie": "conduite", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "242E0C0D694B03CE3469D08A24A32F7C68C7510B047BBCD55697EB20715DCD0E" - }, - { - "cle": "qua_fixing_kneeling", - "appel": { - "script": "Actor.animate(qua_fixing_kneeling)", - "action": "ANIMATION: qua_fixing_kneeling" - }, - "nom": { - "fr": "Réparer à genoux", - "en": "Fix kneeling" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_fixing_kneeling_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_fixing_kneeling_M" - }, - "duree_s": 5.2, - "images": 157, - "planche": { - "fichier": "planches/qua_fixing_kneeling.jpg", - "images": 24, - "taille": 256, - "ips": 4.6 - }, - "cadence": 30, - "taille_ko": { - "feminin": 1231, - "masculin": 1221, - "total": 2452 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.03 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "a genoux", - "posture_origine": "calculee" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 100, - "droite": 94 - }, - "mains": { - "gauche": "mouvement", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "interaction", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "à revoir", - "motifs": [ - "Bridget : sol cm 5.2 (rFoot)", - "Michael : sol cm 4.4 (rFoot)" - ], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "F3258795367F1D835232BD8AEBC74B8923255F1481794505DCA480FFBA57FC87" - }, - { - "cle": "qua_hit_chest", - "appel": { - "script": "Actor.animate(qua_hit_chest)", - "action": "ANIMATION: qua_hit_chest" - }, - "nom": { - "fr": "Coup à la poitrine", - "en": "Hit in chest" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_hit_chest_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_hit_chest_M" - }, - "duree_s": 0.33, - "images": 11, - "planche": { - "fichier": "planches/qua_hit_chest.jpg", - "images": 4, - "taille": 256, - "ips": 12.1 - }, - "cadence": 30, - "taille_ko": { - "feminin": 445, - "masculin": 436, - "total": 881 - }, - "taille_cuite": false, - "boucle": { - "oui": false, - "ecart_pct": 15.27 - }, - "deplacement": { - "cm": 3, - "cm_s": 9, - "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": "combat", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.01, - "pire_torsion_deg": 0.02 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "FB7950F28F88AD367751B1AEAFAE3389CEE57CE7883C22796C87F7A8215B4E07" - }, - { - "cle": "qua_hit_head", - "appel": { - "script": "Actor.animate(qua_hit_head)", - "action": "ANIMATION: qua_hit_head" - }, - "nom": { - "fr": "Coup à la tête", - "en": "Hit in head" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_hit_head_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_hit_head_M" - }, - "duree_s": 0.43, - "images": 14, - "planche": { - "fichier": "planches/qua_hit_head.jpg", - "images": 5, - "taille": 256, - "ips": 11.6 - }, - "cadence": 30, - "taille_ko": { - "feminin": 459, - "masculin": 449, - "total": 908 - }, - "taille_cuite": false, - "boucle": { - "oui": false, - "ecart_pct": 5.01 - }, - "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": "mouvement" - } - }, - "accessoire": "", - "categorie": "combat", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "176D3A1E9E1E60730390E753233F66D3D880507B19441BA9377A41D33CB6C5CE" - }, - { - "cle": "qua_idle_loop", - "appel": { - "script": "Actor.animate(qua_idle_loop)", - "action": "ANIMATION: qua_idle_loop" - }, - "nom": { - "fr": "Attente", - "en": "Idle" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_idle_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_idle_loop_M" - }, - "duree_s": 2.5, - "images": 76, - "planche": { - "fichier": "planches/qua_idle_loop.jpg", - "images": 24, - "taille": 256, - "ips": 9.6 - }, - "cadence": 30, - "taille_ko": { - "feminin": 570, - "masculin": 560, - "total": 1130 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.19 - }, - "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": "repos", - "droite": "repos" - } - }, - "accessoire": "", - "categorie": "attente", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "C3501B164243DDC82BF77DEFE7F5D404638E24D3822F2BDC782CF2B0A2E465B8" - }, - { - "cle": "qua_idle_talking_loop", - "appel": { - "script": "Actor.animate(qua_idle_talking_loop)", - "action": "ANIMATION: qua_idle_talking_loop" - }, - "nom": { - "fr": "Attente en parlant", - "en": "Idle talking" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_idle_talking_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_idle_talking_loop_M" - }, - "duree_s": 2.93, - "images": 89, - "planche": { - "fichier": "planches/qua_idle_talking_loop.jpg", - "images": 24, - "taille": 256, - "ips": 8.2 - }, - "cadence": 30, - "taille_ko": { - "feminin": 792, - "masculin": 780, - "total": 1573 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "repos" - } - }, - "accessoire": "", - "categorie": "conversation", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "61E2C5D612C06F777604212E71FE6640878AA8917B5DFAB75B328BEA7BCBA3FE" - }, - { - "cle": "qua_idle_torch_loop", - "appel": { - "script": "Actor.animate(qua_idle_torch_loop)", - "action": "ANIMATION: qua_idle_torch_loop" - }, - "nom": { - "fr": "Attente avec torche", - "en": "Idle with torch" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_idle_torch_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_idle_torch_loop_M" - }, - "duree_s": 1.27, - "images": 39, - "planche": { - "fichier": "planches/qua_idle_torch_loop.jpg", - "images": 15, - "taille": 256, - "ips": 11.8 - }, - "cadence": 30, - "taille_ko": { - "feminin": 526, - "masculin": 516, - "total": 1042 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "repos", - "droite": "repos" - } - }, - "accessoire": "torche", - "categorie": "attente", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "A86E9DE73F030F6534C7FE2D91E5DB894829E0B24E6440925401624031954DFC" - }, - { - "cle": "qua_interact", - "appel": { - "script": "Actor.animate(qua_interact)", - "action": "ANIMATION: qua_interact" - }, - "nom": { - "fr": "Interagir", - "en": "Interact" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_interact_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_interact_M" - }, - "duree_s": 2.0, - "images": 61, - "planche": { - "fichier": "planches/qua_interact.jpg", - "images": 24, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 542, - "masculin": 531, - "total": 1073 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "repos" - } - }, - "accessoire": "", - "categorie": "interaction", - "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)." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "C649C49CB844ADD53BE596BBF1A3C9E1CEB4D40D0FBA97F16E3166E11E25E5CB" - }, - { - "cle": "qua_jog_fwd_loop", - "appel": { - "script": "Actor.animate(qua_jog_fwd_loop)", - "action": "ANIMATION: qua_jog_fwd_loop" - }, - "nom": { - "fr": "Jogging", - "en": "Jog" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_jog_fwd_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_jog_fwd_loop_M" - }, - "duree_s": 0.93, - "images": 29, - "planche": { - "fichier": "planches/qua_jog_fwd_loop.jpg", - "images": 11, - "taille": 256, - "ips": 11.8 - }, - "cadence": 30, - "taille_ko": { - "feminin": 488, - "masculin": 477, - "total": 965 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.26 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "debout", - "posture_origine": "calculee" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 17, - "droite": 17 - }, - "mains": { - "gauche": "mouvement", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "locomotion", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.06, - "pire_torsion_deg": 0.11 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "6914EEB47F8F5D5063B11C64016510AAD1C7A998D8C6E3BB7A64358FAB06EC97" - }, - { - "cle": "qua_jump_land", - "appel": { - "script": "Actor.animate(qua_jump_land)", - "action": "ANIMATION: qua_jump_land" - }, - "nom": { - "fr": "Réception de saut", - "en": "Jump landing" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_jump_land_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_jump_land_M" - }, - "duree_s": 1.27, - "images": 39, - "planche": { - "fichier": "planches/qua_jump_land.jpg", - "images": 15, - "taille": 256, - "ips": 11.8 - }, - "cadence": 30, - "taille_ko": { - "feminin": 598, - "masculin": 588, - "total": 1186 - }, - "taille_cuite": false, - "boucle": { - "oui": false, - "ecart_pct": 31.62 - }, - "deplacement": { - "cm": 3, - "cm_s": 3, - "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": "chute", - "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)." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.02, - "pire_torsion_deg": 0.03 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "01EB23A73501C6BC352888D32DD864DBB9DA44EE5F2E49CBE0EBAB1691A064B7" - }, - { - "cle": "qua_jump_loop", - "appel": { - "script": "Actor.animate(qua_jump_loop)", - "action": "ANIMATION: qua_jump_loop" - }, - "nom": { - "fr": "Saut en l'air", - "en": "Jump airborne" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_jump_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_jump_loop_M" - }, - "duree_s": 2.5, - "images": 76, - "planche": { - "fichier": "planches/qua_jump_loop.jpg", - "images": 24, - "taille": 256, - "ips": 9.6 - }, - "cadence": 30, - "taille_ko": { - "feminin": 606, - "masculin": 594, - "total": 1199 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "debout", - "posture_origine": "calculee" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 0, - "droite": 100 - }, - "mains": { - "gauche": "repos", - "droite": "repos" - } - }, - "accessoire": "", - "categorie": "chute", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "A2D7E1D8D497FB4C04B2E337F899DECF52CBDA7A3B786593866A7F8C099166A9" - }, - { - "cle": "qua_jump_start", - "appel": { - "script": "Actor.animate(qua_jump_start)", - "action": "ANIMATION: qua_jump_start" - }, - "nom": { - "fr": "Départ de saut", - "en": "Jump start" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_jump_start_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_jump_start_M" - }, - "duree_s": 1.33, - "images": 41, - "planche": { - "fichier": "planches/qua_jump_start.jpg", - "images": 16, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 599, - "masculin": 590, - "total": 1189 - }, - "taille_cuite": false, - "boucle": { - "oui": false, - "ecart_pct": 37.45 - }, - "deplacement": { - "cm": 10, - "cm_s": 7, - "sens": "avant" - }, - "posture": { - "principale": "debout", - "posture_origine": "calculee", - "debut": "accroupi", - "fin": "debout" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 12, - "droite": 56 - }, - "mains": { - "gauche": "mouvement", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "chute", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "à revoir", - "motifs": [ - "Bridget : saut deg 23.1 (lForearmBend)", - "Michael : saut deg 23.1 (lForearmBend)" - ], - "pire_brut_pct": 0.01, - "pire_torsion_deg": 0.02 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "B444B1F6443E46E1B3F9B421CEF6BED44745D9DD04E2AA11D33D91F48197D4B2" - }, - { - "cle": "qua_pickup_table", - "appel": { - "script": "Actor.animate(qua_pickup_table)", - "action": "ANIMATION: qua_pickup_table" - }, - "nom": { - "fr": "Ramasser sur une table", - "en": "Pick up from table" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_pickup_table_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_pickup_table_M" - }, - "duree_s": 0.83, - "images": 26, - "planche": { - "fichier": "planches/qua_pickup_table.jpg", - "images": 10, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 481, - "masculin": 471, - "total": 951 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "mouvement" - } - }, - "accessoire": "", - "categorie": "interaction", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.02, - "pire_torsion_deg": 0.05 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "E6481AC8E95369EC2B9590414487136B6BEE77982AF27FD9D40ADDBC7A36E276" - }, - { - "cle": "qua_pistol_aim_down", - "appel": { - "script": "Actor.animate(qua_pistol_aim_down)", - "action": "ANIMATION: qua_pistol_aim_down" - }, - "nom": { - "fr": "Viser vers le bas", - "en": "Aim down" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_pistol_aim_down_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_pistol_aim_down_M" - }, - "duree_s": 0.17, - "images": 6, - "planche": { - "fichier": "planches/qua_pistol_aim_down.jpg", - "images": 4, - "taille": 256, - "ips": 23.5 - }, - "cadence": 30, - "taille_ko": { - "feminin": 392, - "masculin": 382, - "total": 774 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "repos", - "droite": "repos" - } - }, - "accessoire": "pistolet", - "categorie": "combat", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "C88CA6AEE605B16B0F842E6C91CB160D5E69019E5B3D820E3CFD6C14AE9D8A1C" - }, - { - "cle": "qua_pistol_aim_neutral", - "appel": { - "script": "Actor.animate(qua_pistol_aim_neutral)", - "action": "ANIMATION: qua_pistol_aim_neutral" - }, - "nom": { - "fr": "Viser droit", - "en": "Aim straight" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_pistol_aim_neutral_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_pistol_aim_neutral_M" - }, - "duree_s": 0.17, - "images": 6, - "planche": { - "fichier": "planches/qua_pistol_aim_neutral.jpg", - "images": 4, - "taille": 256, - "ips": 23.5 - }, - "cadence": 30, - "taille_ko": { - "feminin": 392, - "masculin": 382, - "total": 774 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "repos", - "droite": "repos" - } - }, - "accessoire": "pistolet", - "categorie": "combat", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "3BB9CA9A4A5CCBAFB1E46A67BB014B127CB1A7DFC0D79DD0D26BA261A333C7B3" - }, - { - "cle": "qua_pistol_aim_up", - "appel": { - "script": "Actor.animate(qua_pistol_aim_up)", - "action": "ANIMATION: qua_pistol_aim_up" - }, - "nom": { - "fr": "Viser vers le haut", - "en": "Aim up" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_pistol_aim_up_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_pistol_aim_up_M" - }, - "duree_s": 0.17, - "images": 6, - "planche": { - "fichier": "planches/qua_pistol_aim_up.jpg", - "images": 4, - "taille": 256, - "ips": 23.5 - }, - "cadence": 30, - "taille_ko": { - "feminin": 392, - "masculin": 382, - "total": 774 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "levee", - "droite": "levee" - } - }, - "accessoire": "pistolet", - "categorie": "combat", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "CBD5C628AF2AFDB1801153554CF5CD233662BA792FC689324479BC86A175BA89" - }, - { - "cle": "qua_pistol_idle_loop", - "appel": { - "script": "Actor.animate(qua_pistol_idle_loop)", - "action": "ANIMATION: qua_pistol_idle_loop" - }, - "nom": { - "fr": "Attente au pistolet", - "en": "Pistol idle" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_pistol_idle_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_pistol_idle_loop_M" - }, - "duree_s": 1.67, - "images": 51, - "planche": { - "fichier": "planches/qua_pistol_idle_loop.jpg", - "images": 20, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 473, - "masculin": 464, - "total": 937 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "repos", - "droite": "repos" - } - }, - "accessoire": "pistolet", - "categorie": "combat", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "E4C04AEF22B8EB66983FAEB811379F5B52B4DB9387D6BD563D7A2254BC3DAFFC" - }, - { - "cle": "qua_pistol_reload", - "appel": { - "script": "Actor.animate(qua_pistol_reload)", - "action": "ANIMATION: qua_pistol_reload" - }, - "nom": { - "fr": "Recharger le pistolet", - "en": "Pistol reload" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_pistol_reload_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_pistol_reload_M" - }, - "duree_s": 1.67, - "images": 51, - "planche": { - "fichier": "planches/qua_pistol_reload.jpg", - "images": 20, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 633, - "masculin": 621, - "total": 1254 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "mouvement" - } - }, - "accessoire": "pistolet", - "categorie": "combat", - "style": "agressif", - "usage": { - "fr": "Joué une fois : rechargement du pistolet ; à placer entre deux tirs.", - "en": "Played once: pistol reload; place it between two shots." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.16, - "pire_torsion_deg": 0.3 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "AE0E297125CC353DBB6A6E09B2BA0431825EF51F75A291E7519C10F305C64EEA" - }, - { - "cle": "qua_pistol_shoot", - "appel": { - "script": "Actor.animate(qua_pistol_shoot)", - "action": "ANIMATION: qua_pistol_shoot" - }, - "nom": { - "fr": "Tir au pistolet", - "en": "Pistol shot" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_pistol_shoot_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_pistol_shoot_M" - }, - "duree_s": 0.63, - "images": 20, - "planche": { - "fichier": "planches/qua_pistol_shoot.jpg", - "images": 8, - "taille": 256, - "ips": 12.7 - }, - "cadence": 30, - "taille_ko": { - "feminin": 439, - "masculin": 430, - "total": 869 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "mouvement" - } - }, - "accessoire": "pistolet", - "categorie": "combat", - "style": "agressif", - "usage": { - "fr": "Joué une fois : un tir de pistolet avec recul.", - "en": "Played once: one pistol shot with recoil." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.01 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "DEA155DB01D349F6A1CD0ABD2E82CDA3C1B1FD645EA5F4F3F1C6DC457BB160C2" - }, - { - "cle": "qua_punch_cross", - "appel": { - "script": "Actor.animate(qua_punch_cross)", - "action": "ANIMATION: qua_punch_cross" - }, - "nom": { - "fr": "Direct", - "en": "Cross punch" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_punch_cross_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_punch_cross_M" - }, - "duree_s": 1.0, - "images": 31, - "planche": { - "fichier": "planches/qua_punch_cross.jpg", - "images": 12, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 498, - "masculin": 489, - "total": 987 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "mouvement" - } - }, - "accessoire": "", - "categorie": "combat", - "style": "agressif", - "usage": { - "fr": "Joué une fois : coup de poing direct ; pour une bagarre.", - "en": "Played once: cross punch; for a fight." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "24CAA42A311A1C9893A7F180ADC2EC2293AC3C56657237F9D3FB33FC264CBE2F" - }, - { - "cle": "qua_punch_jab", - "appel": { - "script": "Actor.animate(qua_punch_jab)", - "action": "ANIMATION: qua_punch_jab" - }, - "nom": { - "fr": "Jab", - "en": "Jab" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_punch_jab_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_punch_jab_M" - }, - "duree_s": 0.87, - "images": 27, - "planche": { - "fichier": "planches/qua_punch_jab.jpg", - "images": 10, - "taille": 256, - "ips": 11.5 - }, - "cadence": 30, - "taille_ko": { - "feminin": 472, - "masculin": 462, - "total": 934 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "mouvement" - } - }, - "accessoire": "", - "categorie": "combat", - "style": "agressif", - "usage": { - "fr": "Joué une fois : coup de poing rapide ; pour une bagarre.", - "en": "Played once: quick jab punch; for a fight." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.01 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "45EA97BFA3E178EBAC27CCD20BBD0C1AB96DF7BC6D4EB2D2673381D0298C0FA7" - }, - { - "cle": "qua_push_loop", - "appel": { - "script": "Actor.animate(qua_push_loop)", - "action": "ANIMATION: qua_push_loop" - }, - "nom": { - "fr": "Pousser", - "en": "Push" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_push_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_push_loop_M" - }, - "duree_s": 2.67, - "images": 81, - "planche": { - "fichier": "planches/qua_push_loop.jpg", - "images": 24, - "taille": 256, - "ips": 9.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 584, - "masculin": 577, - "total": 1161 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "debout", - "posture_origine": "calculee" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 78, - "droite": 80 - }, - "mains": { - "gauche": "repos", - "droite": "repos" - } - }, - "accessoire": "", - "categorie": "interaction", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "EAF3BFF8FB507A6787A716B07FFF39E5CF9D4D373EB2776C15275145A8FDD73A" - }, - { - "cle": "qua_roll", - "appel": { - "script": "Actor.animate(qua_roll)", - "action": "ANIMATION: qua_roll" - }, - "nom": { - "fr": "Roulade", - "en": "Roll" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_roll_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_roll_M" - }, - "duree_s": 1.47, - "images": 45, - "planche": { - "fichier": "planches/qua_roll.jpg", - "images": 18, - "taille": 256, - "ips": 12.2 - }, - "cadence": 30, - "taille_ko": { - "feminin": 628, - "masculin": 618, - "total": 1247 - }, - "taille_cuite": false, - "boucle": { - "oui": false, - "ecart_pct": 29.75 - }, - "deplacement": { - "cm": 11, - "cm_s": 8, - "sens": "avant" - }, - "posture": { - "principale": "variable", - "posture_origine": "calculee", - "debut": "accroupi", - "fin": "debout" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 53, - "droite": 53 - }, - "mains": { - "gauche": "mouvement", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "sport", - "style": "agressif", - "usage": { - "fr": "Joué une fois : roulade au sol ; pour esquiver ou plonger.", - "en": "Played once: ground roll; for dodging or diving." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "à revoir", - "motifs": [ - "Bridget : sol cm 4.8 (rFoot)", - "Michael : sol cm 5.0 (rFoot)" - ], - "pire_brut_pct": 0.19, - "pire_torsion_deg": 0.49 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "E8789A4A8E24E589F556A4FA297112F5040ECF881F2B25106EFE679A26AEF6B5" - }, - { - "cle": "qua_sitting_enter", - "appel": { - "script": "Actor.animate(qua_sitting_enter)", - "action": "ANIMATION: qua_sitting_enter" - }, - "nom": { - "fr": "S'asseoir", - "en": "Sit down" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_sitting_enter_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_sitting_enter_M" - }, - "duree_s": 1.3, - "images": 40, - "planche": { - "fichier": "planches/qua_sitting_enter.jpg", - "images": 16, - "taille": 256, - "ips": 12.3 - }, - "cadence": 30, - "taille_ko": { - "feminin": 597, - "masculin": 587, - "total": 1184 - }, - "taille_cuite": false, - "boucle": { - "oui": false, - "ecart_pct": 52.14 - }, - "deplacement": { - "cm": 28, - "cm_s": 22, - "sens": "arriere" - }, - "posture": { - "principale": "variable", - "posture_origine": "calculee", - "debut": "debout", - "fin": "assis" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 100, - "droite": 90 - }, - "mains": { - "gauche": "mouvement", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "assis", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "A1ACDCA70C74EF3AAD992ADCA9FB24025002DDE8B357FDE7CBF9B3AEBC731C74" - }, - { - "cle": "qua_sitting_exit", - "appel": { - "script": "Actor.animate(qua_sitting_exit)", - "action": "ANIMATION: qua_sitting_exit" - }, - "nom": { - "fr": "Se lever", - "en": "Stand up" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_sitting_exit_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_sitting_exit_M" - }, - "duree_s": 1.03, - "images": 32, - "planche": { - "fichier": "planches/qua_sitting_exit.jpg", - "images": 12, - "taille": 256, - "ips": 11.7 - }, - "cadence": 30, - "taille_ko": { - "feminin": 556, - "masculin": 546, - "total": 1102 - }, - "taille_cuite": false, - "boucle": { - "oui": false, - "ecart_pct": 52.14 - }, - "deplacement": { - "cm": 28, - "cm_s": 27, - "sens": "avant" - }, - "posture": { - "principale": "variable", - "posture_origine": "calculee", - "debut": "assis", - "fin": "debout" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 100, - "droite": 94 - }, - "mains": { - "gauche": "mouvement", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "assis", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.01 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "932A9D24652DAE1F71A6DC21FD669064AE2AC1F62B69751B08AA8D76A387D18A" - }, - { - "cle": "qua_sitting_idle_loop", - "appel": { - "script": "Actor.animate(qua_sitting_idle_loop)", - "action": "ANIMATION: qua_sitting_idle_loop" - }, - "nom": { - "fr": "Assis, attente", - "en": "Seated idle" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_sitting_idle_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_sitting_idle_loop_M" - }, - "duree_s": 1.67, - "images": 51, - "planche": { - "fichier": "planches/qua_sitting_idle_loop.jpg", - "images": 20, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 558, - "masculin": 548, - "total": 1106 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "assis", - "posture_origine": "calculee", - "hauteur_assise_cm": 45 - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 100, - "droite": 100 - }, - "mains": { - "gauche": "repos", - "droite": "repos" - } - }, - "accessoire": "", - "categorie": "assis", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "91261070E46467CF15F78F3A02D4C3E15C4BE91C453AB6AFEB31515F9DDAE2D8" - }, - { - "cle": "qua_sitting_talking_loop", - "appel": { - "script": "Actor.animate(qua_sitting_talking_loop)", - "action": "ANIMATION: qua_sitting_talking_loop" - }, - "nom": { - "fr": "Assis, en parlant", - "en": "Seated talking" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_sitting_talking_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_sitting_talking_loop_M" - }, - "duree_s": 2.93, - "images": 89, - "planche": { - "fichier": "planches/qua_sitting_talking_loop.jpg", - "images": 24, - "taille": 256, - "ips": 8.2 - }, - "cadence": 30, - "taille_ko": { - "feminin": 770, - "masculin": 761, - "total": 1531 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "assis", - "posture_origine": "calculee", - "hauteur_assise_cm": 45 - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 100, - "droite": 100 - }, - "mains": { - "gauche": "mouvement", - "droite": "repos" - } - }, - "accessoire": "", - "categorie": "assis", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "2329B5AE17064A1FC996C016256A1571D4B3370E5697A8600170D972A8850EFD" - }, - { - "cle": "qua_spell_simple_enter", - "appel": { - "script": "Actor.animate(qua_spell_simple_enter)", - "action": "ANIMATION: qua_spell_simple_enter" - }, - "nom": { - "fr": "Début d'incantation", - "en": "Spell start" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_spell_simple_enter_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_spell_simple_enter_M" - }, - "duree_s": 0.53, - "images": 17, - "planche": { - "fichier": "planches/qua_spell_simple_enter.jpg", - "images": 6, - "taille": 256, - "ips": 11.3 - }, - "cadence": 30, - "taille_ko": { - "feminin": 478, - "masculin": 468, - "total": 946 - }, - "taille_cuite": false, - "boucle": { - "oui": false, - "ecart_pct": 52.78 - }, - "deplacement": { - "cm": 3, - "cm_s": 5, - "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": "magie", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.01, - "pire_torsion_deg": 0.02 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "00C7EDB5BAECF9E2A08864B6609BCCDFD2B5E2D32843EC2CBDA30CAA2740198B" - }, - { - "cle": "qua_spell_simple_exit", - "appel": { - "script": "Actor.animate(qua_spell_simple_exit)", - "action": "ANIMATION: qua_spell_simple_exit" - }, - "nom": { - "fr": "Fin d'incantation", - "en": "Spell end" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_spell_simple_exit_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_spell_simple_exit_M" - }, - "duree_s": 0.43, - "images": 14, - "planche": { - "fichier": "planches/qua_spell_simple_exit.jpg", - "images": 5, - "taille": 256, - "ips": 11.6 - }, - "cadence": 30, - "taille_ko": { - "feminin": 463, - "masculin": 453, - "total": 916 - }, - "taille_cuite": false, - "boucle": { - "oui": false, - "ecart_pct": 52.78 - }, - "deplacement": { - "cm": 3, - "cm_s": 7, - "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": "magie", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.02, - "pire_torsion_deg": 0.04 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "C0A8FB24837735C6C9E1886115694B9FDB9F5D835FA7A9F740739AB2FCD9A9CE" - }, - { - "cle": "qua_spell_simple_idle_loop", - "appel": { - "script": "Actor.animate(qua_spell_simple_idle_loop)", - "action": "ANIMATION: qua_spell_simple_idle_loop" - }, - "nom": { - "fr": "Incantation", - "en": "Spell idle" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_spell_simple_idle_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_spell_simple_idle_loop_M" - }, - "duree_s": 2.1, - "images": 64, - "planche": { - "fichier": "planches/qua_spell_simple_idle_loop.jpg", - "images": 24, - "taille": 256, - "ips": 11.4 - }, - "cadence": 30, - "taille_ko": { - "feminin": 574, - "masculin": 566, - "total": 1140 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "repos", - "droite": "repos" - } - }, - "accessoire": "", - "categorie": "magie", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "F755790056282315856B4EDEF1EF497F12C8858D79FBCEB48C966722980DC4C9" - }, - { - "cle": "qua_spell_simple_shoot", - "appel": { - "script": "Actor.animate(qua_spell_simple_shoot)", - "action": "ANIMATION: qua_spell_simple_shoot" - }, - "nom": { - "fr": "Lancer un sort", - "en": "Cast spell" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_spell_simple_shoot_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_spell_simple_shoot_M" - }, - "duree_s": 0.5, - "images": 16, - "planche": { - "fichier": "planches/qua_spell_simple_shoot.jpg", - "images": 6, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 462, - "masculin": 452, - "total": 914 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "repos", - "droite": "repos" - } - }, - "accessoire": "", - "categorie": "magie", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "08C89F2D6EEA08A0CE215152F81520C1A2629E4C39829A0BAE6A92D30962569B" - }, - { - "cle": "qua_sprint_loop", - "appel": { - "script": "Actor.animate(qua_sprint_loop)", - "action": "ANIMATION: qua_sprint_loop" - }, - "nom": { - "fr": "Sprint", - "en": "Sprint" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_sprint_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_sprint_loop_M" - }, - "duree_s": 0.67, - "images": 21, - "planche": { - "fichier": "planches/qua_sprint_loop.jpg", - "images": 8, - "taille": 256, - "ips": 11.9 - }, - "cadence": 30, - "taille_ko": { - "feminin": 452, - "masculin": 443, - "total": 895 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.26 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "debout", - "posture_origine": "calculee" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 29, - "droite": 14 - }, - "mains": { - "gauche": "mouvement", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "locomotion", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.08, - "pire_torsion_deg": 0.14 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "02B94FD1A309E25A9A81FD558B24B9FB503796BAB7C6125351516FE55E7F2C76" - }, - { - "cle": "qua_swim_fwd_loop", - "appel": { - "script": "Actor.animate(qua_swim_fwd_loop)", - "action": "ANIMATION: qua_swim_fwd_loop" - }, - "nom": { - "fr": "Nage", - "en": "Swim" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_swim_fwd_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_swim_fwd_loop_M" - }, - "duree_s": 1.33, - "images": 41, - "planche": { - "fichier": "planches/qua_swim_fwd_loop.jpg", - "images": 16, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 498, - "masculin": 489, - "total": 987 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "dans l'eau", - "posture_origine": "declaree", - "posture_calculee": "allonge" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 100, - "droite": 100 - }, - "mains": { - "gauche": "mouvement", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "nage", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.02, - "pire_torsion_deg": 0.04 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "C77C54F53AA648222D8B86BD172A1AD1B5A1F42530F7298B389191B0AA94CD30" - }, - { - "cle": "qua_swim_idle_loop", - "appel": { - "script": "Actor.animate(qua_swim_idle_loop)", - "action": "ANIMATION: qua_swim_idle_loop" - }, - "nom": { - "fr": "Surplace dans l'eau", - "en": "Treading water" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_swim_idle_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_swim_idle_loop_M" - }, - "duree_s": 3.33, - "images": 101, - "planche": { - "fichier": "planches/qua_swim_idle_loop.jpg", - "images": 24, - "taille": 256, - "ips": 7.2 - }, - "cadence": 30, - "taille_ko": { - "feminin": 865, - "masculin": 856, - "total": 1721 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "dans l'eau", - "posture_origine": "declaree", - "posture_calculee": "a genoux" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 100, - "droite": 100 - }, - "mains": { - "gauche": "repos", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "nage", - "style": "neutre", - "usage": { - "fr": "Boucle de flottaison sur place dans l'eau.", - "en": "Looping treading water in place." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "B8D553ABA48541776648BBEB8785C655ADBA135D5C41F4A7B0E3F273386590A4" - }, - { - "cle": "qua_sword_attack", - "appel": { - "script": "Actor.animate(qua_sword_attack)", - "action": "ANIMATION: qua_sword_attack" - }, - "nom": { - "fr": "Attaque à l'épée", - "en": "Sword attack" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_sword_attack_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_sword_attack_M" - }, - "duree_s": 1.53, - "images": 47, - "planche": { - "fichier": "planches/qua_sword_attack.jpg", - "images": 18, - "taille": 256, - "ips": 11.8 - }, - "cadence": 30, - "taille_ko": { - "feminin": 549, - "masculin": 539, - "total": 1089 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "debout", - "posture_origine": "calculee" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 89, - "droite": 100 - }, - "mains": { - "gauche": "mouvement", - "droite": "mouvement" - } - }, - "accessoire": "épée", - "categorie": "combat", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.05, - "pire_torsion_deg": 0.1 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "5B3A15EACF396EF46B0BE10BCBE6658A1A8D18E7D762EB49D4376463362834C9" - }, - { - "cle": "qua_sword_idle", - "appel": { - "script": "Actor.animate(qua_sword_idle)", - "action": "ANIMATION: qua_sword_idle" - }, - "nom": { - "fr": "Attente à l'épée", - "en": "Sword idle" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_sword_idle_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_sword_idle_M" - }, - "duree_s": 1.67, - "images": 51, - "planche": { - "fichier": "planches/qua_sword_idle.jpg", - "images": 20, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 569, - "masculin": 558, - "total": 1126 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "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": "repos", - "droite": "repos" - } - }, - "accessoire": "épée", - "categorie": "combat", - "style": "agressif", - "usage": { - "fr": "Boucle d'attente debout, épée en main, en garde.", - "en": "Standing idle loop, sword in hand, on guard." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "3FFCEF2EF4332F5423035AB1008535E77F67DDD66ED82689582F19F52E74D525" - }, - { - "cle": "qua_walk_formal_loop", - "appel": { - "script": "Actor.animate(qua_walk_formal_loop)", - "action": "ANIMATION: qua_walk_formal_loop" - }, - "nom": { - "fr": "Marche formelle", - "en": "Formal walk" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_walk_formal_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_walk_formal_loop_M" - }, - "duree_s": 1.33, - "images": 41, - "planche": { - "fichier": "planches/qua_walk_formal_loop.jpg", - "images": 16, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 500, - "masculin": 490, - "total": 990 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.36 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "debout", - "posture_origine": "calculee" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 68, - "droite": 68 - }, - "mains": { - "gauche": "repos", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "locomotion", - "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)." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.0 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "8FADE11A0F54298B1F62464B329DEA56E75E931A91892C8D76D322D0003134D7" - }, - { - "cle": "qua_walk_loop", - "appel": { - "script": "Actor.animate(qua_walk_loop)", - "action": "ANIMATION: qua_walk_loop" - }, - "nom": { - "fr": "Marche", - "en": "Walk" - }, - "assets": { - "feminin": "/lpr_anim_quaternius/Anims/Female/qua_walk_loop_F", - "masculin": "/lpr_anim_quaternius/Anims/Male/qua_walk_loop_M" - }, - "duree_s": 1.33, - "images": 41, - "planche": { - "fichier": "planches/qua_walk_loop.jpg", - "images": 16, - "taille": 256, - "ips": 12.0 - }, - "cadence": 30, - "taille_ko": { - "feminin": 506, - "masculin": 496, - "total": 1002 - }, - "taille_cuite": false, - "boucle": { - "oui": true, - "ecart_pct": 0.0 - }, - "deplacement": { - "cm": 0, - "cm_s": 0, - "sens": "sur place" - }, - "posture": { - "principale": "debout", - "posture_origine": "calculee" - }, - "contacts": { - "pieds_au_sol_pct": { - "gauche": 68, - "droite": 68 - }, - "mains": { - "gauche": "mouvement", - "droite": "mouvement" - } - }, - "accessoire": "", - "categorie": "locomotion", - "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." - }, - "corps": [ - "feminin", - "masculin" - ], - "qualite": { - "verdict": "accepté", - "motifs": [], - "pire_brut_pct": 0.0, - "pire_torsion_deg": 0.01 - }, - "source": "Quaternius Universal Animation Library (Standard)", - "licence": "CC0 1.0", - "credit": "Animations : Universal Animation Library (Standard) par Quaternius, licence CC0 1.0, https://quaternius.com", - "version": "1.0", - "empreinte": "90F43B3F0D90DDE9387D90FB05770A118220A88BA0CB109850304211848CB24C" - } - ] -} diff --git a/ModSources/lpr_anim_quaternius/Module/CREDITS.txt b/ModSources/lpr_anim_quaternius/Module/CREDITS.txt deleted file mode 100644 index 7850c7e..0000000 --- a/ModSources/lpr_anim_quaternius/Module/CREDITS.txt +++ /dev/null @@ -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__F / qua__M. diff --git a/ModSources/lpr_anim_quaternius/Module/lpr_anim_quaternius.lpmod b/ModSources/lpr_anim_quaternius/Module/lpr_anim_quaternius.lpmod deleted file mode 100644 index 36d938a..0000000 --- a/ModSources/lpr_anim_quaternius/Module/lpr_anim_quaternius.lpmod +++ /dev/null @@ -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 diff --git a/ModSources/lpr_anim_quaternius/lpr_anim_quaternius.uplugin b/ModSources/lpr_anim_quaternius/lpr_anim_quaternius.uplugin deleted file mode 100644 index 703291b..0000000 --- a/ModSources/lpr_anim_quaternius/lpr_anim_quaternius.uplugin +++ /dev/null @@ -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" -} diff --git a/ModSources/lpr_hair_pocolov/Module/CREDITS.txt b/ModSources/lpr_hair_pocolov/Module/CREDITS.txt deleted file mode 100644 index 70ac928..0000000 --- a/ModSources/lpr_hair_pocolov/Module/CREDITS.txt +++ /dev/null @@ -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*). diff --git a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov03.lphair b/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov03.lphair deleted file mode 100644 index e88195a..0000000 --- a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov03.lphair +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov05.lphair b/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov05.lphair deleted file mode 100644 index b09af4a..0000000 --- a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov05.lphair +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov06.lphair b/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov06.lphair deleted file mode 100644 index 0cd4faa..0000000 --- a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov06.lphair +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov07.lphair b/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov07.lphair deleted file mode 100644 index ae55be6..0000000 --- a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov07.lphair +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov09.lphair b/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov09.lphair deleted file mode 100644 index d19a8f2..0000000 --- a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov09.lphair +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov10.lphair b/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov10.lphair deleted file mode 100644 index 838d8c8..0000000 --- a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov10.lphair +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov11.lphair b/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov11.lphair deleted file mode 100644 index b44e24d..0000000 --- a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov11.lphair +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov13.lphair b/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov13.lphair deleted file mode 100644 index b7cc1b5..0000000 --- a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov13.lphair +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov14.lphair b/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov14.lphair deleted file mode 100644 index 33de7b5..0000000 --- a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov14.lphair +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov15.lphair b/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov15.lphair deleted file mode 100644 index 16b1e20..0000000 --- a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov15.lphair +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov16.lphair b/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov16.lphair deleted file mode 100644 index 6faf91a..0000000 --- a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov16.lphair +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov17.lphair b/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov17.lphair deleted file mode 100644 index eba9f0a..0000000 --- a/ModSources/lpr_hair_pocolov/Module/Hairstyles/pocolov17.lphair +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/Module/lpr_hair_pocolov.lpmod b/ModSources/lpr_hair_pocolov/Module/lpr_hair_pocolov.lpmod deleted file mode 100644 index 970b81f..0000000 --- a/ModSources/lpr_hair_pocolov/Module/lpr_hair_pocolov.lpmod +++ /dev/null @@ -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 diff --git a/ModSources/lpr_hair_pocolov/lpr_hair_pocolov.uplugin b/ModSources/lpr_hair_pocolov/lpr_hair_pocolov.uplugin deleted file mode 100644 index 742e565..0000000 --- a/ModSources/lpr_hair_pocolov/lpr_hair_pocolov.uplugin +++ /dev/null @@ -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" -} diff --git a/README.md b/README.md index 6f55759..e23e390 100644 --- a/README.md +++ b/README.md @@ -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//` next to its text module in `LifePlay/Content/Modules//`, 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; diff --git a/Source/LifePlay/LPModInfoSubsystem.h b/Source/LifePlay/LPModInfoSubsystem.h index 204aaf2..551a9f9 100644 --- a/Source/LifePlay/LPModInfoSubsystem.h +++ b/Source/LifePlay/LPModInfoSubsystem.h @@ -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. diff --git a/Tools/BuildDebug.ps1 b/Tools/BuildDebug.ps1 index e9e91e8..4fd77b8 100644 --- a/Tools/BuildDebug.ps1 +++ b/Tools/BuildDebug.ps1 @@ -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)) diff --git a/Tools/BuildMod.ps1 b/Tools/BuildMod.ps1 deleted file mode 100644 index d8cd19d..0000000 --- a/Tools/BuildMod.ps1 +++ /dev/null @@ -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\\ (.uplugin, Content\, Module\ ; voir Tools/ModSource.ps1). - 1. controles : plugin et module (MODULE_UNIQUEID = ), version de reference Releases\\Windows, aucun - autre plugin dans Mods\ ; - 0. -Anim : chaine complete d'animations du pack (Tools\anim\AnimPipeline.ps1 -Pack , lot AP7) ; - -From 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 : 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\, 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\ dans tous les cas ; - 3. controle du conteneur : chaque paquet est sous Mods// (aucun asset du jeu embarque) ; - 4. assemblage \-for-LifePlay-\LifePlay\{Mods\, Content\Modules\} + notice bilingue, - archive .zip testee (7-Zip) ; un assemblage precedent du meme nom est deplace (precedent_), jamais supprime ; - 5. -Install : 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-.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//. - $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 diff --git a/Tools/DiagnosePose.py b/Tools/DiagnosePose.py deleted file mode 100644 index d53296c..0000000 --- a/Tools/DiagnosePose.py +++ /dev/null @@ -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() diff --git a/Tools/EditMod.ps1 b/Tools/EditMod.ps1 deleted file mode 100644 index 867ebf7..0000000 --- a/Tools/EditMod.ps1 +++ /dev/null @@ -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\ dans Mods\ (Tools/ModSource.ps1), lance l'editeur et attend sa fermeture, puis remet le - mod dans ModSources\ (meme si l'editeur plante). Les assets du mod apparaissent dans le navigateur de contenu - sous " Content" (chemin //) ; 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 -} diff --git a/Tools/FbxMaterialMap.py b/Tools/FbxMaterialMap.py deleted file mode 100644 index 3d0daba..0000000 --- a/Tools/FbxMaterialMap.py +++ /dev/null @@ -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 [] -# Sortie : {"": {"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(" "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("= 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) diff --git a/Tools/HairImportFbx.py b/Tools/HairImportFbx.py deleted file mode 100644 index 84524c3..0000000 --- a/Tools/HairImportFbx.py +++ /dev/null @@ -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 -run=pythonscript "-script=/Tools/HairImportFbx.py 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= [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=). - 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) diff --git a/Tools/HairMaterials.py b/Tools/HairMaterials.py deleted file mode 100644 index a4eb0fc..0000000 --- a/Tools/HairMaterials.py +++ /dev/null @@ -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 : -# _Cap : emplacements listes dans cap= (calotte) ; opacite cap_opacity= ou "white" (calotte opaque) -# _Strands : tous les autres emplacements (meches) ; opacite strands_opacity= -# Controle final : dependances du SkeletalMesh (squelette et ces deux materiaux seulement). -# -# Usage : -# UnrealEditor-Cmd.exe -run=pythonscript "-script=/Tools/HairMaterials.py mesh=/Game/Heavy/New5/Hairs/Nom -# diffuse= strands_opacity= 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= strands_opacity= [cap_opacity=|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 (//..., 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")) diff --git a/Tools/HairPipeline.ps1 b/Tools/HairPipeline.ps1 deleted file mode 100644 index d40bf13..0000000 --- a/Tools/HairPipeline.ps1 +++ /dev/null @@ -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 \\src ; copie le FBX et les textures sous des - noms sans espaces ; - 2. importe le FBX en maillages statiques (Tools/HairImportFbx.py) dans /_Work_ ; - 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_ deplace vers \\work (jamais supprime) ; - 4. Modules//Hairstyles/.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 \\precedente_ 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": "" et "folder": "//Hairs", les sources ModSources\ sont montees dans - Mods\ pendant le travail (Tools/ModSource.ps1) puis remises en place ; les .lphair et les credits vont dans - ModSources\\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 ; //X -> Mods//Content/X (mod, H3) -# ou Plugins//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/.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\ 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 diff --git a/Tools/ImportQuaternius.py b/Tools/ImportQuaternius.py deleted file mode 100644 index 6f05e40..0000000 --- a/Tools/ImportQuaternius.py +++ /dev/null @@ -1,40 +0,0 @@ -# Importe le mannequin et les animations CC0 de Quaternius (Universal Animation Library, sans root motion). -# -# Usage : -# UnrealEditor-Cmd.exe -run=pythonscript "-script=/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)) diff --git a/Tools/ModSource.ps1 b/Tools/ModSource.ps1 deleted file mode 100644 index 1f27fe6..0000000 --- a/Tools/ModSource.ps1 +++ /dev/null @@ -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\\ : - .uplugin plugin de contenu (CanContainContent) - Content\ assets du mod (non versionnes, comme Content/Heavy) - Module\ module LifePlay en texte (.lpmod, Hairstyles\*.lphair, CREDITS.txt) - L'editeur ne monte // que si le plugin est dans \Mods\ ; mais un plugin laisse dans Mods\ serait cuit - avec le jeu de base. Mount-ModSource deplace donc ModSources\ vers Mods\ 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) -} diff --git a/Tools/NewMod.ps1 b/Tools/NewMod.ps1 deleted file mode 100644 index 69afd20..0000000 --- a/Tools/NewMod.ps1 +++ /dev/null @@ -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\\ : - .uplugin plugin de contenu (assets cuits a part du jeu) - Content\ assets du mod (a remplir dans l'editeur : Tools/EditMod.ps1) - Module\.lpmod module LifePlay (MODULE_UNIQUEID = , active depuis le gestionnaire de mods) - Module\CREDITS.txt credits et licences des assets - et, avec -Hairs, Tools\mods\.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" diff --git a/Tools/SplitModules.ps1 b/Tools/SplitModules.ps1 index 1a381b8..5c4bdba 100644 --- a/Tools/SplitModules.ps1 +++ b/Tools/SplitModules.ps1 @@ -18,7 +18,7 @@ .PARAMETER (configuration "per_module": true, depuis la publication qui suit la V1.4) Une archive par module optionnel, -for-LifePlay-.zip (LifePlay\Content\Modules\ + notice bilingue tiree du .lpmod), dans \ ; un index INDEX.txt (nom, archive, taille, SHA-256) sert au message - de diffusion (Discord). Les mods d'assets (Tools/BuildMod.ps1) se deposent dans le meme dossier. + 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" } diff --git a/Tools/anim/AnimPipeline.ps1 b/Tools/anim/AnimPipeline.ps1 deleted file mode 100644 index ee15ef6..0000000 --- a/Tools/anim/AnimPipeline.ps1 +++ /dev/null @@ -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 : dossier AnimSources\ ; la chaine est decrite par le bloc "chaine" de AnimSources\\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 ) ; sans version de reference Releases\\Windows : - avertissement, sans echec ; -SansCuisson la saute - Rapports : \etapes\_.json (debut, fin, duree, commandes, code, controles, verdict) et - \etat.json (avancement, methode retenue, empreintes). -From reprend a l'etape n (etat.json lu ; - etapes precedentes non rejouees) ; -To s'arrete apres l'etape n. Un passage complet (sans -From) deplace - l'ancien contenu de dans \precedent_ (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 : , _Skeleton, _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 { '' } } -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 : .png dans $(Show-Rel (Join-Path $outDir 'vignettes')) ; .jpg et .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 .png par clip' ($missV.Count -eq 0) $(if ($missV) { 'manquantes : ' + (($missV | Select-Object -First 8) -join ', ') }) - Add-Check 'une planche .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_ 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 -} diff --git a/Tools/anim/Catalogue.py b/Tools/anim/Catalogue.py deleted file mode 100644 index 03e0c4d..0000000 --- a/Tools/anim/Catalogue.py +++ /dev/null @@ -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 .png (--vignettes, carres 512 px, defaut Saved/Measurement/vignettes), planches .jpg + .json -(--planches, defaut Saved/Measurement/planches ; bande de N images carrees de 256 px, animee au survol), -clips du mod ModSources//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: et animate().""" - 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=''+esc(a.nom.fr)+'' - +'

'+esc(a.nom.fr)+' / '+esc(a.nom.en)+'

' - +'' - +'
'+esc(a.appel.script)+'
'+esc(a.appel.action)+'
' - +'
'+esc(a.categorie)+''+esc(pos)+''+a.duree_s.toString().replace('.',',')+' s ('+dur(a)+')' - +''+(a.boucle.oui?'boucle':'une fois')+''+esc(mv)+''+(a.accessoire?''+esc(a.accessoire)+'':'')+'
' - +'
'+esc(a.usage.fr)+'
' - +'
Qualité : '+esc(q.verdict)+(q.motifs.length?' ('+esc(q.motifs.join(' ; '))+')':'')+'
'; - 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)s - - - -
-

%(title)s

-
Version %(version)s — %(n)d animations — généré le %(date)s. Copiez la clé puis écrivez Actor.animate(clé) dans une scène ou ANIMATION: clé dans une action. Survolez une vignette pour voir le mouvement.
-
- - - - -
-
-
-
Aucune animation ne correspond aux filtres.
-
-
%(credit)s
- - - - -""" % {"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 : /ModSources/)") - ap.add_argument("--planches", default="", help="dossier des planches .jpg/.json (defaut : /Saved/Measurement/planches)") - ap.add_argument("--vignettes", default="", help="dossier des vignettes .png (defaut : /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()) diff --git a/Tools/anim/DeliverAnims.ps1 b/Tools/anim/DeliverAnims.ps1 deleted file mode 100644 index 8f49bbf..0000000 --- a/Tools/anim/DeliverAnims.ps1 +++ /dev/null @@ -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_ -> //Anims/Female/qua__F ; M_ -> //Anims/Male/qua__M. - Le jeu appelle Actor.animate() et cherche _F et _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\ s'il manque (jamais d'ecrasement), monte le mod, lance Tools\anim\DeliverAnims.py - dans UnrealEditor-Cmd (journal Saved\Logs\portage\AP5_DeliverAnims_.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__F / qua__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)__F / $($pk.prefixe)__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 -} diff --git a/Tools/anim/DeliverAnims.py b/Tools/anim/DeliverAnims.py deleted file mode 100644 index ae4a159..0000000 --- a/Tools/anim/DeliverAnims.py +++ /dev/null @@ -1,136 +0,0 @@ -# Livraison des clips d'animation convertis dans un mod d'assets (lot AP5). -# Lancement : -# UnrealEditor-Cmd.exe -run=pythonscript "-script=/Tools/anim/DeliverAnims.py mod= src=/Game/Quaternius/UAL prefix=qua exclude=A_TPose pipeline=AnimPipeline/quaternius" -# Copie /B_ -> //Anims/Female/__F -# /M_ -> //Anims/Male/__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))) diff --git a/Tools/anim/RapportControles.py b/Tools/anim/RapportControles.py deleted file mode 100644 index d2e4a87..0000000 --- a/Tools/anim/RapportControles.py +++ /dev/null @@ -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 : /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 ( - '
\n' - '

Rapport des contr\u00f4les d\'animations

\n' - '
%s
\n' - '
\n' - '
G\u00e9n\u00e9r\u00e9 le
%s
\n' - '
Dossier des clips
%s
\n' - '
Clips
%s
\n' - '
Instants par clip
%s
\n' - '
\n
\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 = ['
\n

\u00c9checs

\n'] - if echecs: - sortie.append('
    \n') - sortie += ["
  • %s
  • \n" % esc(e) for e in echecs] - sortie.append("
\n") - else: - sortie.append('

Aucun \u00e9chec.

\n') - sortie.append("
\n
\n

Clips \u00e0 revoir

\n") - if revoir: - sortie.append('
    \n') - sortie += ["
  • %s
  • \n" % esc(e) for e in revoir] - sortie.append("
\n") - else: - sortie.append('

Aucun clip \u00e0 revoir.

\n') - sortie.append("
\n") - return "".join(sortie) - - -def html_legende(): - items = [] - for _, _, titre, unite, seuil in MESURES: - items.append("
  • %s %s %s
  • " % ( - esc(titre), esc(fmt(seuil, 0)), esc(unite))) - return ( - '
    \n

    L\u00e9gende des seuils

    \n' - '
      %s
    \n' - '

    Une mesure au-del\u00e0 du seuil donne ' - '\u00e0 revoir ; au-del\u00e0 de %s fois le seuil, ' - 'rejet\u00e9. ' - 'Dans chaque cellule : la valeur, puis l\'os concern\u00e9 en petit.

    \n' - '
    \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 '
    %s
    \n' % "".join( - '
    %s%s
    ' - % (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 = "ClipImages" + "".join( - "%s (%s)" % (esc(t), esc(u)) - for _, _, t, u, _ in MESURES) + "Verdict" - corps = [] - for r in lignes: - verdict = r.get("verdict", "") - cls = {"rejete": "v-rejete", "a revoir": "v-revoir"}.get(verdict, "v-accepte") - cases = ['%s' % esc(r.get("clip", "")), - '%s' % 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( - '%s%s' % ( - classe_mesure(v, seuil), esc(fmt(v)), - ('%s' % esc(os_)) if os_ else "")) - cases.append('%s' % ( - esc(verdict.replace(" ", "")), esc(verdict))) - corps.append('%s' % (cls, "".join(cases))) - return ('
    \n%s\n' - '\n%s\n
    \n') % (tete, "\n".join(corps)) - - -def html_pose(pose): - if not pose: - return '

    Pas de rapport de pose T pour ce corps.

    \n' - seg = pose.get("segments") or [] - lignes = [] - for s in seg: - cls = "ajuste" if s.get("ajuste") else "" - lignes.append( - '%s%s%s' - '%s' % ( - 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 = ['%s%s' % ( - esc(r.get("os", "")), esc(fmt(r.get("degres")))) for r in reglages] - valide = pose.get("valide") - return ( - '

    Profil : %s \u00b7 sym\u00e9trie max : %s \u00b7 pose T : ' - '%s

    \n' - '
    \n' - '

    Segments (\u00e9cart en %% de 180\u00b0, avant / apr\u00e8s ajustement)

    ' - '
    ' - '%s
    SegmentAvantApr\u00e8s
    \n' - '

    R\u00e9glages appliqu\u00e9s (degr\u00e9s)

    ' - '
    ' - '%s
    OsDegr\u00e9s
    \n
    \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 - 'Aucun r\u00e9glage.') - - -def html_ecarts(lignes): - tete = ("ClipInstant (s)Formule (%)Clip brut (%)" - "Torsion (\u00b0)") - corps = [] - for r in lignes: - corps.append( - '%s%s' - '%s%s' - '%s%s' - '%s%s' % ( - 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 ( - '
    \u00c9carts formule / clip / torsion par clip et par ' - 'instant (%d lignes)\n
    ' - '%s\n%s\n
    \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 = ["\n\n\n\n" - "\n" - "\n" - "Rapport des contr\u00f4les d'animations\n" - "\n\n\n
    \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('
    \n

    Corps : %s

    \n' - % (esc(nom), esc(nom))) - parts.append(html_resume_corps(c)) - parts.append("

    Contr\u00f4les par clip

    \n") - if ctrl: - parts.append(html_controles(ctrl)) - else: - parts.append('

    Fichier controles_%s.csv absent ou vide.

    \n' - % esc(nom)) - parts.append("

    Pose T

    \n") - parts.append(html_pose(pose)) - parts.append("

    \u00c9carts d\u00e9taill\u00e9s

    \n") - if evals: - parts.append(html_ecarts(evals)) - else: - parts.append('

    Fichier eval_%s.csv absent ou vide.

    \n' - % esc(nom)) - parts.append("
    \n") - - parts.append('
    Rapport g\u00e9n\u00e9r\u00e9 par RapportControles.py \u00b7 ' - 'source : %s
    \n
    \n\n\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)) diff --git a/Tools/anim/RetargetPack.py b/Tools/anim/RetargetPack.py deleted file mode 100644 index a805fc1..0000000 --- a/Tools/anim/RetargetPack.py +++ /dev/null @@ -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/.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_, -# 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 "). -# Methode factice ou copie, ou profil du maillage source different de profil_source : ligne -# "RETARGET echec : " 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_.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() diff --git a/Tools/anim/methodes/genesis_direct.json b/Tools/anim/methodes/genesis_direct.json deleted file mode 100644 index 73b69aa..0000000 --- a/Tools/anim/methodes/genesis_direct.json +++ /dev/null @@ -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)" - ] -} diff --git a/Tools/anim/methodes/ue4.json b/Tools/anim/methodes/ue4.json deleted file mode 100644 index 8837fdc..0000000 --- a/Tools/anim/methodes/ue4.json +++ /dev/null @@ -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" - ] -} diff --git a/Tools/anim/methodes/ue5.json b/Tools/anim/methodes/ue5.json deleted file mode 100644 index 08d50c9..0000000 --- a/Tools/anim/methodes/ue5.json +++ /dev/null @@ -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)" - ] -} diff --git a/Tools/anim/profiles/genesis8.json b/Tools/anim/profiles/genesis8.json deleted file mode 100644 index 6d45a6a..0000000 --- a/Tools/anim/profiles/genesis8.json +++ /dev/null @@ -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" - } -} diff --git a/Tools/anim/profiles/ue4_mannequin.json b/Tools/anim/profiles/ue4_mannequin.json deleted file mode 100644 index 13f5077..0000000 --- a/Tools/anim/profiles/ue4_mannequin.json +++ /dev/null @@ -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" - } -} diff --git a/Tools/anim/profiles/ue5_mannequin.json b/Tools/anim/profiles/ue5_mannequin.json deleted file mode 100644 index fcfcc80..0000000 --- a/Tools/anim/profiles/ue5_mannequin.json +++ /dev/null @@ -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" - } -} diff --git a/Tools/debug/LifePlay_interaction_sandbox.bat b/Tools/debug/LifePlay_interaction_sandbox.bat deleted file mode 100644 index fd1db6a..0000000 --- a/Tools/debug/LifePlay_interaction_sandbox.bat +++ /dev/null @@ -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" diff --git a/Tools/debug/LifePlay_measure_sandbox.bat b/Tools/debug/LifePlay_measure_sandbox.bat deleted file mode 100644 index a5d8f38..0000000 --- a/Tools/debug/LifePlay_measure_sandbox.bat +++ /dev/null @@ -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" diff --git a/Tools/debug/LifePlay_pair_sandbox.bat b/Tools/debug/LifePlay_pair_sandbox.bat deleted file mode 100644 index bb9cb1f..0000000 --- a/Tools/debug/LifePlay_pair_sandbox.bat +++ /dev/null @@ -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" diff --git a/Tools/hairs/HAIRS1.json b/Tools/hairs/HAIRS1.json deleted file mode 100644 index 5521f40..0000000 --- a/Tools/hairs/HAIRS1.json +++ /dev/null @@ -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 - } - ] -} diff --git a/Tools/mods/lpr_hair_pocolov.json b/Tools/mods/lpr_hair_pocolov.json deleted file mode 100644 index 9d0e2ca..0000000 --- a/Tools/mods/lpr_hair_pocolov.json +++ /dev/null @@ -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 - } - ] -}