Code commit
This commit is contained in:
570
CustomizePlus/Core/Data/BoneData.cs
Normal file
570
CustomizePlus/Core/Data/BoneData.cs
Normal file
@@ -0,0 +1,570 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace CustomizePlus.Core.Data;
|
||||
|
||||
public static class BoneData //todo: DI, do not show IVCS unless IVCS is installed/user enabled it, do not show weapon bones
|
||||
{
|
||||
public enum BoneFamily
|
||||
{
|
||||
Root,
|
||||
Spine,
|
||||
Hair,
|
||||
Face,
|
||||
Ears,
|
||||
Chest,
|
||||
Arms,
|
||||
Hands,
|
||||
Tail,
|
||||
Groin,
|
||||
Legs,
|
||||
Feet,
|
||||
Earrings,
|
||||
Hat,
|
||||
Cape,
|
||||
Armor,
|
||||
Skirt,
|
||||
Equipment,
|
||||
Unknown
|
||||
}
|
||||
|
||||
//TODO move the csv data to an external (compressed?) file
|
||||
private static readonly string[] BoneRawTable =
|
||||
{
|
||||
//Codename, Display Name, Bone Family, Parent (if any), Mirrored Bone (if any)
|
||||
"n_root,Root,Root,TRUE,FALSE,,", "n_hara,Abdomen,Root,TRUE,FALSE,,", "j_kao,Head,Spine,TRUE,FALSE,j_kubi,",
|
||||
"j_kubi,Neck,Spine,TRUE,FALSE,j_sebo_c,", "j_sebo_c,Spine C,Spine,TRUE,FALSE,j_sebo_b,",
|
||||
"j_sebo_b,Spine B,Spine,TRUE,FALSE,j_sebo_a,", "j_sebo_a,Spine A,Spine,TRUE,FALSE,j_kosi,",
|
||||
"j_kosi,Waist,Spine,TRUE,FALSE,,", "j_kami_a,Hair A,Hair,TRUE,FALSE,j_kao,",
|
||||
"j_kami_b,Hair B,Hair,TRUE,FALSE,j_kami_a,", "j_kami_f_l,Hair Front Left,Hair,TRUE,FALSE,j_kao,j_kami_f_r",
|
||||
"j_kami_f_r,Hair Front Right,Hair,TRUE,FALSE,j_kao,j_kami_f_l",
|
||||
"j_f_mayu_l,Brow Outer Left,Face,TRUE,FALSE,j_kao,j_f_mayu_r",
|
||||
"j_f_mayu_r,Brow Outer Right,Face,TRUE,FALSE,j_kao,j_f_mayu_l",
|
||||
"j_f_miken_l,Brow Inner Left,Face,TRUE,FALSE,j_kao,j_f_miken_r",
|
||||
"j_f_miken_r,Brow Inner Right,Face,TRUE,FALSE,j_kao,j_f_miken_l",
|
||||
"j_f_memoto,Bridge,Face,TRUE,FALSE,j_kao,", "j_f_umab_l,Eyelid Upper Left,Face,TRUE,FALSE,j_kao,j_f_umab_r",
|
||||
"j_f_umab_r,Eyelid Upper Right,Face,TRUE,FALSE,j_kao,j_f_umab_l",
|
||||
"j_f_dmab_l,Eyelid Lower Left,Face,TRUE,FALSE,j_kao,j_f_dmab_r",
|
||||
"j_f_dmab_r,Eyelid Lower Right,Face,TRUE,FALSE,j_kao,j_f_dmab_l",
|
||||
"j_f_eye_l,Eye Left,Face,TRUE,FALSE,j_kao,j_f_eye_r", "j_f_eye_r,Eye Right,Face,TRUE,FALSE,j_kao,j_f_eye_l",
|
||||
"j_f_hoho_l,Cheek Left,Face,TRUE,FALSE,j_kao,j_f_hoho_r",
|
||||
"j_f_hoho_r,Cheek Right,Face,TRUE,FALSE,j_kao,j_f_hoho_l",
|
||||
"j_f_hige_l,Hrothgar Whiskers Left,Face,FALSE,FALSE,j_kao,j_f_hige_r",
|
||||
"j_f_hige_r,Hrothgar Whiskers Right,Face,FALSE,FALSE,j_kao,j_f_hige_l",
|
||||
"j_f_hana,Nose,Face,TRUE,FALSE,j_kao,", "j_f_lip_l,Lips Left,Face,TRUE,FALSE,j_kao,j_f_lip_r",
|
||||
"j_f_lip_r,Lips Right,Face,TRUE,FALSE,j_kao,j_f_lip_l", "j_f_ulip_a,Lip Upper A,Face,TRUE,FALSE,j_kao,",
|
||||
"j_f_ulip_b,Lip Upper B,Face,TRUE,FALSE,j_kao,", "j_f_dlip_a,Lip Lower A,Face,TRUE,FALSE,j_kao,",
|
||||
"j_f_dlip_b,Lip Lower B,Face,TRUE,FALSE,j_kao,",
|
||||
"n_f_lip_l,Hrothgar Cheek Left,Face,FALSE,FALSE,j_kao,n_f_lip_r",
|
||||
"n_f_lip_r,Hrothgar Cheek Right,Face,FALSE,FALSE,j_kao,n_f_lip_l",
|
||||
"n_f_ulip_l,Hrothgar Lip Upper Left,Face,FALSE,FALSE,j_kao,n_f_ulip_r",
|
||||
"n_f_ulip_r,Hrothgar Lip Upper Right,Face,FALSE,FALSE,j_kao,n_f_ulip_l",
|
||||
"j_f_dlip,Hrothgar Lip Lower,Face,FALSE,FALSE,j_kao,", "j_ago,Jaw,Face,TRUE,FALSE,j_kao,",
|
||||
"j_f_uago,Hrothgar Palate Upper,Face,FALSE,FALSE,j_kao,",
|
||||
"j_f_ulip,Hrothgar Palate Lower,Face,FALSE,FALSE,j_kao,",
|
||||
"j_mimi_l,Ear Left,Ears,TRUE,FALSE,j_kao,j_mimi_r", "j_mimi_r,Ear Right,Ears,TRUE,FALSE,j_kao,j_mimi_l",
|
||||
"j_zera_a_l,Viera Ear 01 A Left,Ears,FALSE,FALSE,j_kao,j_zera_a_r",
|
||||
"j_zera_a_r,Viera Ear 01 A Right,Ears,FALSE,FALSE,j_kao,j_zera_a_l",
|
||||
"j_zera_b_l,Viera Ear 01 B Left,Ears,FALSE,FALSE,j_kao,j_zera_b_r",
|
||||
"j_zera_b_r,Viera Ear 01 B Right,Ears,FALSE,FALSE,j_kao,j_zera_b_l",
|
||||
"j_zerb_a_l,Viera Ear 02 A Left,Ears,FALSE,FALSE,j_kao,j_zerb_a_r",
|
||||
"j_zerb_a_r,Viera Ear 02 A Right,Ears,FALSE,FALSE,j_kao,j_zerb_a_l",
|
||||
"j_zerb_b_l,Viera Ear 02 B Left,Ears,FALSE,FALSE,j_kao,j_zerb_b_r",
|
||||
"j_zerb_b_r,Viera Ear 02 B Right,Ears,FALSE,FALSE,j_kao,j_zerb_b_l",
|
||||
"j_zerc_a_l,Viera Ear 03 A Left,Ears,FALSE,FALSE,j_kao,j_zerc_a_r",
|
||||
"j_zerc_a_r,Viera Ear 03 A Right,Ears,FALSE,FALSE,j_kao,j_zerc_a_l",
|
||||
"j_zerc_b_l,Viera Ear 03 B Left,Ears,FALSE,FALSE,j_kao,j_zerc_b_r",
|
||||
"j_zerc_b_r,Viera Ear 03 B Right,Ears,FALSE,FALSE,j_kao,j_zerc_b_l",
|
||||
"j_zerd_a_l,Viera Ear 04 A Left,Ears,FALSE,FALSE,j_kao,j_zerd_a_r",
|
||||
"j_zerd_a_r,Viera Ear 04 A Right,Ears,FALSE,FALSE,j_kao,j_zerd_a_l",
|
||||
"j_zerd_b_l,Viera Ear 04 B Left,Ears,FALSE,FALSE,j_kao,j_zerd_b_r",
|
||||
"j_zerd_b_r,Viera Ear 04 B Right,Ears,FALSE,FALSE,j_kao,j_zerd_b_l",
|
||||
"j_sako_l,Clavicle Left,Chest,TRUE,FALSE,j_sebo_c,j_sako_r",
|
||||
"j_sako_r,Clavicle Right,Chest,TRUE,FALSE,j_sebo_c,j_sako_l",
|
||||
"j_mune_l,Breast Left,Chest,TRUE,FALSE,j_sebo_b,j_mune_r",
|
||||
"j_mune_r,Breast Right,Chest,TRUE,FALSE,j_sebo_b,j_mune_l",
|
||||
"iv_c_mune_l,Breast B Left,Chest,FALSE,TRUE,j_mune_l,iv_c_mune_r",
|
||||
"iv_c_mune_r,Breast B Right,Chest,FALSE,TRUE,j_mune_r,iv_c_mune_l",
|
||||
"n_hkata_l,Shoulder Left,Arms,TRUE,FALSE,j_ude_a_l,n_hkata_r",
|
||||
"n_hkata_r,Shoulder Right,Arms,TRUE,FALSE,j_ude_a_r,n_hkata_l",
|
||||
"j_ude_a_l,Arm Left,Arms,TRUE,FALSE,j_sako_l,j_ude_a_r",
|
||||
"j_ude_a_r,Arm Right,Arms,TRUE,FALSE,j_sako_r,j_ude_a_l",
|
||||
"iv_nitoukin_l,Bicep Left,Arms,FALSE,TRUE,j_ude_a_l,iv_nitoukin_r",
|
||||
"iv_nitoukin_r,Bicep Right,Arms,FALSE,TRUE,j_ude_a_r,iv_nitoukin_l",
|
||||
"n_hhiji_l,Elbow Left,Arms,TRUE,FALSE,j_ude_b_l,n_hhiji_r",
|
||||
"n_hhiji_r,Elbow Right,Arms,TRUE,FALSE,j_ude_b_r,n_hhiji_l",
|
||||
"j_ude_b_l,Forearm Left,Arms,TRUE,FALSE,j_ude_a_l,j_ude_b_r",
|
||||
"j_ude_b_r,Forearm Right,Arms,TRUE,FALSE,j_ude_a_r,j_ude_b_l",
|
||||
"n_hte_l,Wrist Left,Arms,TRUE,FALSE,j_ude_b_l,n_hte_r",
|
||||
"n_hte_r,Wrist Right,Arms,TRUE,FALSE,j_ude_b_r,n_hte_l", "j_te_l,Hand Left,Hands,TRUE,FALSE,n_hte_l,j_te_r",
|
||||
"j_te_r,Hand Right,Hands,TRUE,FALSE,n_hte_r,j_te_l",
|
||||
"j_oya_a_l,Thumb A Left,Hands,TRUE,FALSE,j_te_l,j_oya_a_r",
|
||||
"j_oya_a_r,Thumb A Right,Hands,TRUE,FALSE,j_te_r,j_oya_a_l",
|
||||
"j_oya_b_l,Thumb B Left,Hands,TRUE,FALSE,j_oya_a_l,j_oya_b_r",
|
||||
"j_oya_b_r,Thumb B Right,Hands,TRUE,FALSE,j_oya_a_r,j_oya_b_l",
|
||||
"j_hito_a_l,Index A Left,Hands,TRUE,FALSE,j_te_l,j_hito_a_r",
|
||||
"j_hito_a_r,Index A Right,Hands,TRUE,FALSE,j_te_r,j_hito_a_l",
|
||||
"j_hito_b_l,Index B Left,Hands,TRUE,FALSE,j_hito_a_l,j_hito_b_r",
|
||||
"j_hito_b_r,Index B Right,Hands,TRUE,FALSE,j_hito_a_r,j_hito_b_l",
|
||||
"j_naka_a_l,Middle A Left,Hands,TRUE,FALSE,j_te_l,j_naka_a_r",
|
||||
"j_naka_a_r,Middle A Right,Hands,TRUE,FALSE,j_te_r,j_naka_a_l",
|
||||
"j_naka_b_l,Middle B Left,Hands,TRUE,FALSE,j_naka_a_l,j_naka_b_r",
|
||||
"j_naka_b_r,Middle B Right,Hands,TRUE,FALSE,j_naka_a_r,j_naka_b_l",
|
||||
"j_kusu_a_l,Ring A Left,Hands,TRUE,FALSE,j_te_l,j_kusu_a_r",
|
||||
"j_kusu_a_r,Ring A Right,Hands,TRUE,FALSE,j_te_r,j_kusu_a_l",
|
||||
"j_kusu_b_l,Ring B Left,Hands,TRUE,FALSE,j_kusu_a_l,j_kusu_b_r",
|
||||
"j_kusu_b_r,Ring B Right,Hands,TRUE,FALSE,j_kusu_a_r,j_kusu_b_l",
|
||||
"j_ko_a_l,Pinky A Left,Hands,TRUE,FALSE,j_te_l,j_ko_a_r",
|
||||
"j_ko_a_r,Pinky A Right,Hands,TRUE,FALSE,j_te_r,j_ko_a_l",
|
||||
"j_ko_b_l,Pinky B Left,Hands,TRUE,FALSE,j_ko_a_l,j_ko_b_r",
|
||||
"j_ko_b_r,Pinky B Right,Hands,TRUE,FALSE,j_ko_a_r,j_ko_b_l",
|
||||
"iv_hito_c_l,Index C Left,Hands,FALSE,TRUE,j_hito_b_l,iv_hito_c_r",
|
||||
"iv_hito_c_r,Index C Right,Hands,FALSE,TRUE,j_hito_b_r,iv_hito_c_l",
|
||||
"iv_naka_c_l,Middle C Left,Hands,FALSE,TRUE,j_naka_b_l,iv_naka_c_r",
|
||||
"iv_naka_c_r,Middle C Right,Hands,FALSE,TRUE,j_naka_b_r,iv_naka_c_l",
|
||||
"iv_kusu_c_l,Ring C Left,Hands,FALSE,TRUE,j_kusu_b_l,iv_kusu_c_r",
|
||||
"iv_kusu_c_r,Ring C Right,Hands,FALSE,TRUE,j_kusu_b_r,iv_kusu_c_l",
|
||||
"iv_ko_c_l,Pinky C Left,Hands,FALSE,TRUE,j_ko_b_l,iv_ko_c_r",
|
||||
"iv_ko_c_r,Pinky C Right,Hands,FALSE,TRUE,j_ko_b_r,iv_ko_c_l", "n_sippo_a,Tail A,Tail,FALSE,FALSE,j_kosi,",
|
||||
"n_sippo_b,Tail B,Tail,FALSE,FALSE,n_sippo_a,", "n_sippo_c,Tail C,Tail,FALSE,FALSE,n_sippo_b,",
|
||||
"n_sippo_d,Tail D,Tail,FALSE,FALSE,n_sippo_c,", "n_sippo_e,Tail E,Tail,FALSE,FALSE,n_sippo_d,",
|
||||
"iv_shiri_l,Buttock Left,Groin,FALSE,TRUE,j_kosi,iv_shiri_r",
|
||||
"iv_shiri_r,Buttock Right,Groin,FALSE,TRUE,j_kosi,iv_shiri_l",
|
||||
"iv_kougan_l,Scrotum Left,Groin,FALSE,TRUE,iv_ochinko_a,iv_kougan_r",
|
||||
"iv_kougan_r,Scrotum Right,Groin,FALSE,TRUE,iv_ochinko_a,iv_kougan_l",
|
||||
"iv_ochinko_a,Penis A,Groin,FALSE,TRUE,j_kosi,", "iv_ochinko_b,Penis B,Groin,FALSE,TRUE,iv_ochinko_a,",
|
||||
"iv_ochinko_c,Penis C,Groin,FALSE,TRUE,iv_ochinko_b,",
|
||||
"iv_ochinko_d,Penis D,Groin,FALSE,TRUE,iv_ochinko_c,",
|
||||
"iv_ochinko_e,Penis E,Groin,FALSE,TRUE,iv_ochinko_d,",
|
||||
"iv_ochinko_f,Penis F,Groin,FALSE,TRUE,iv_ochinko_e,", "iv_omanko,Vagina,Groin,FALSE,TRUE,j_kosi,",
|
||||
"iv_kuritto,Clitoris,Groin,FALSE,TRUE,iv_omanko,",
|
||||
"iv_inshin_l,Labia Left,Groin,FALSE,TRUE,iv_omanko,iv_inshin_r",
|
||||
"iv_inshin_r,Labia Right,Groin,FALSE,TRUE,iv_omanko,iv_inshin_l", "iv_koumon,Anus,Groin,FALSE,TRUE,j_kosi,",
|
||||
"iv_koumon_l,Anus B Right,Groin,FALSE,TRUE,iv_koumon,iv_koumon_r",
|
||||
"iv_koumon_r,Anus B Left,Groin,FALSE,TRUE,iv_koumon,iv_koumon_l",
|
||||
"j_asi_a_l,Leg Left,Legs,TRUE,FALSE,j_kosi,j_asi_a_r",
|
||||
"j_asi_a_r,Leg Right,Legs,TRUE,FALSE,j_kosi,j_asi_a_l",
|
||||
"j_asi_b_l,Knee Left,Legs,TRUE,FALSE,j_asi_a_l,j_asi_b_r",
|
||||
"j_asi_b_r,Knee Right,Legs,TRUE,FALSE,j_asi_a_r,j_asi_b_l",
|
||||
"j_asi_c_l,Calf Left,Legs,TRUE,FALSE,j_asi_b_l,j_asi_c_r",
|
||||
"j_asi_c_r,Calf Right,Legs,TRUE,FALSE,j_asi_b_r,j_asi_c_l",
|
||||
"j_asi_d_l,Foot Left,Feet,TRUE,FALSE,j_asi_c_l,j_asi_d_r",
|
||||
"j_asi_d_r,Foot Right,Feet,TRUE,FALSE,j_asi_c_r,j_asi_d_l",
|
||||
"j_asi_e_l,Toes Left,Feet,TRUE,FALSE,j_asi_d_l,j_asi_e_r",
|
||||
"j_asi_e_r,Toes Right,Feet,TRUE,FALSE,j_asi_d_r,j_asi_e_l",
|
||||
"iv_asi_oya_a_l,Big Toe A Left,Feet,FALSE,TRUE,j_asi_e_l,iv_asi_oya_a_r",
|
||||
"iv_asi_oya_a_r,Big Toe A Right,Feet,FALSE,TRUE,j_asi_e_r,iv_asi_oya_a_l",
|
||||
"iv_asi_oya_b_l,Big Toe B Left,Feet,FALSE,TRUE,j_asi_oya_a_l,iv_asi_oya_b_r",
|
||||
"iv_asi_oya_b_r,Big Toe B Right,Feet,FALSE,TRUE,j_asi_oya_a_r,iv_asi_oya_b_l",
|
||||
"iv_asi_hito_a_l,Index Toe A Left,Feet,FALSE,TRUE,j_asi_e_l,iv_asi_hito_a_r",
|
||||
"iv_asi_hito_a_r,Index Toe A Right,Feet,FALSE,TRUE,j_asi_e_r,iv_asi_hito_a_l",
|
||||
"iv_asi_hito_b_l,Index Toe B Left,Feet,FALSE,TRUE,j_asi_hito_a_l,iv_asi_hito_b_r",
|
||||
"iv_asi_hito_b_r,Index Toe B Right,Feet,FALSE,TRUE,j_asi_hito_a_r,iv_asi_hito_b_l",
|
||||
"iv_asi_naka_a_l,Middle Toe A Left,Feet,FALSE,TRUE,j_asi_e_l,iv_asi_naka_a_r",
|
||||
"iv_asi_naka_a_r,Middle Toe A Right,Feet,FALSE,TRUE,j_asi_e_r,iv_asi_naka_a_l",
|
||||
"iv_asi_naka_b_l,Middle Toe B Left,Feet,FALSE,TRUE,j_asi_naka_b_l,iv_asi_naka_b_r",
|
||||
"iv_asi_naka_b_r,Middle Toe B Right,Feet,FALSE,TRUE,j_asi_naka_b_r,iv_asi_naka_b_l",
|
||||
"iv_asi_kusu_a_l,Fore Toe A Left,Feet,FALSE,TRUE,j_asi_e_l,iv_asi_kusu_a_r",
|
||||
"iv_asi_kusu_a_r,Fore Toe A Right,Feet,FALSE,TRUE,j_asi_e_r,iv_asi_kusu_a_l",
|
||||
"iv_asi_kusu_b_l,Fore Toe B Left,Feet,FALSE,TRUE,j_asi_kusu_a_l,iv_asi_kusu_b_r",
|
||||
"iv_asi_kusu_b_r,Fore Toe B Right,Feet,FALSE,TRUE,j_asi_kusu_a_r,iv_asi_kusu_b_l",
|
||||
"iv_asi_ko_a_l,Pinky Toe A Left,Feet,FALSE,TRUE,j_asi_e_l,iv_asi_ko_a_r",
|
||||
"iv_asi_ko_a_r,Pinky Toe A Right,Feet,FALSE,TRUE,j_asi_e_r,iv_asi_ko_a_l",
|
||||
"iv_asi_ko_b_l,Pinky Toe B Left,Feet,FALSE,TRUE,j_asi_ko_a_l,iv_asi_ko_b_r",
|
||||
"iv_asi_ko_b_r,Pinky Toe B Right,Feet,FALSE,TRUE,j_asi_ko_a_r,iv_asi_ko_b_l",
|
||||
"j_ex_met_va,Visor,Hat,FALSE,FALSE,j_kao,", "j_ex_met_a,Hat Accessory A,Hat,FALSE,FALSE,j_kao,",
|
||||
"j_ex_met_b,Hat Accessory B,Hat,FALSE,FALSE,j_kao,",
|
||||
"n_ear_b_l,Earring B Left,Earrings,FALSE,FALSE,n_ear_a_l,n_ear_b_r",
|
||||
"n_ear_b_r,Earring B Right,Earrings,FALSE,FALSE,n_ear_a_r,n_ear_b_l",
|
||||
"n_ear_a_l,Earring A Left,Earrings,FALSE,FALSE,j_kao,n_ear_a_r",
|
||||
"n_ear_a_r,Earring A Right,Earrings,FALSE,FALSE,j_kao,n_ear_a_l",
|
||||
"j_ex_top_a_l,Cape A Left,Cape,FALSE,FALSE,j_sebo_c,j_ex_top_a_r",
|
||||
"j_ex_top_a_r,Cape A Right,Cape,FALSE,FALSE,j_sebo_c,j_ex_top_a_l",
|
||||
"j_ex_top_b_l,Cape B Left,Cape,FALSE,FALSE,j_ex_top_a_l,j_ex_top_b_r",
|
||||
"j_ex_top_b_r,Cape B Right,Cape,FALSE,FALSE,j_ex_top_a_r,j_ex_top_b_l",
|
||||
"n_kataarmor_l,Pauldron Left,Armor,FALSE,FALSE,n_hkata_l,n_kataarmor_r",
|
||||
"n_kataarmor_r,Pauldron Right,Armor,FALSE,FALSE,n_hkata_r,n_kataarmor_l",
|
||||
"n_hijisoubi_l,Elbow Plate Left,Armor,FALSE,FALSE,n_hhiji_l,n_hijisoubi_r",
|
||||
"n_hijisoubi_r,Elbow Plate Right,Armor,FALSE,FALSE,n_hhiji_r,n_hijisoubi_l",
|
||||
"n_hizasoubi_l,Knee Plate Left,Armor,FALSE,FALSE,j_asi_b_l,n_hizasoubi_r",
|
||||
"n_hizasoubi_r,Knee Plate Right,Armor,FALSE,FALSE,j_asi_b_r,n_hizasoubi_l",
|
||||
"j_sk_b_a_l,Skirt Back A Left,Skirt,FALSE,FALSE,j_kosi,j_sk_b_a_r",
|
||||
"j_sk_b_a_r,Skirt Back A Right,Skirt,FALSE,FALSE,j_kosi,j_sk_b_a_l",
|
||||
"j_sk_b_b_l,Skirt Back B Left,Skirt,FALSE,FALSE,j_sk_b_a_l,j_sk_b_b_r",
|
||||
"j_sk_b_b_r,Skirt Back B Right,Skirt,FALSE,FALSE,j_sk_b_a_r,j_sk_b_b_l",
|
||||
"j_sk_b_c_l,Skirt Back C Left,Skirt,FALSE,FALSE,j_sk_b_b_l,j_sk_b_c_r",
|
||||
"j_sk_b_c_r,Skirt Back C Right,Skirt,FALSE,FALSE,j_sk_b_b_r,j_sk_b_c_l",
|
||||
"j_sk_f_a_l,Skirt Front A Left,Skirt,FALSE,FALSE,j_kosi,j_sk_f_a_r",
|
||||
"j_sk_f_a_r,Skirt Front A Right,Skirt,FALSE,FALSE,j_kosi,j_sk_f_a_l",
|
||||
"j_sk_f_b_l,Skirt Front B Left,Skirt,FALSE,FALSE,j_sk_f_a_l,j_sk_f_b_r",
|
||||
"j_sk_f_b_r,Skirt Front B Right,Skirt,FALSE,FALSE,j_sk_f_a_r,j_sk_f_b_l",
|
||||
"j_sk_f_c_l,Skirt Front C Left,Skirt,FALSE,FALSE,j_sk_f_b_l,j_sk_f_c_r",
|
||||
"j_sk_f_c_r,Skirt Front C Right,Skirt,FALSE,FALSE,j_sk_f_b_r,j_sk_f_c_l",
|
||||
"j_sk_s_a_l,Skirt Side A Left,Skirt,FALSE,FALSE,j_kosi,j_sk_s_a_r",
|
||||
"j_sk_s_a_r,Skirt Side A Right,Skirt,FALSE,FALSE,j_kosi,j_sk_s_a_l",
|
||||
"j_sk_s_b_l,Skirt Side B Left,Skirt,FALSE,FALSE,j_sk_s_a_l,j_sk_s_b_r",
|
||||
"j_sk_s_b_r,Skirt Side B Right,Skirt,FALSE,FALSE,j_sk_s_a_r,j_sk_s_b_l",
|
||||
"j_sk_s_c_l,Skirt Side C Left,Skirt,FALSE,FALSE,j_sk_s_b_l,j_sk_s_c_r",
|
||||
"j_sk_s_c_r,Skirt Side C Right,Skirt,FALSE,FALSE,j_sk_s_b_r,j_sk_s_c_l",
|
||||
"n_throw,Throw,Root,FALSE,FALSE,j_kosi,",
|
||||
"j_buki_sebo_l,Scabbard Left,Equipment,FALSE,FALSE,j_kosi,j_buki_sebo_r",
|
||||
"j_buki_sebo_r,Scabbard Right,Equipment,FALSE,FALSE,j_kosi,j_buki_sebo_l",
|
||||
"j_buki2_kosi_l,Holster Left,Equipment,FALSE,FALSE,j_kosi,j_buki2_kosi_r",
|
||||
"j_buki2_kosi_r,Holster Right,Equipment,FALSE,FALSE,j_kosi,j_buki2_kosi_l",
|
||||
"j_buki_kosi_l,Sheath Left,Equipment,FALSE,FALSE,j_kosi,j_buki_kosi_r",
|
||||
"j_buki_kosi_r,Sheath Right,Equipment,FALSE,FALSE,j_kosi,j_buki_kosi_l",
|
||||
"n_buki_tate_l,Shield Left,Equipment,FALSE,FALSE,n_hte_l,n_buki_tate_r",
|
||||
"n_buki_tate_r,Shield Right,Equipment,FALSE,FALSE,n_hte_r,n_buki_tate_l",
|
||||
"n_buki_l,Weapon Left,Equipment,FALSE,FALSE,j_te_l,n_buki_r",
|
||||
"n_buki_r,Weapon Right,Equipment,FALSE,FALSE,j_te_r,n_buki_l"
|
||||
};
|
||||
|
||||
public static readonly Dictionary<BoneFamily, string?> DisplayableFamilies = new()
|
||||
{
|
||||
{ BoneFamily.Spine, null },
|
||||
{ BoneFamily.Hair, null },
|
||||
{ BoneFamily.Face, null },
|
||||
{ BoneFamily.Ears, null },
|
||||
{ BoneFamily.Chest, null },
|
||||
{ BoneFamily.Arms, null },
|
||||
{ BoneFamily.Hands, null },
|
||||
{ BoneFamily.Tail, null },
|
||||
{ BoneFamily.Groin, "NSFW IVCS Bones" },
|
||||
{ BoneFamily.Legs, null },
|
||||
{ BoneFamily.Feet, null },
|
||||
{ BoneFamily.Earrings, "Some mods utilize these bones for their physics properties" },
|
||||
{ BoneFamily.Hat, null },
|
||||
{ BoneFamily.Cape, "Some mods utilize these bones for their physics properties" },
|
||||
{ BoneFamily.Armor, null },
|
||||
{ BoneFamily.Skirt, null },
|
||||
{ BoneFamily.Equipment, "These may behave oddly" },
|
||||
{
|
||||
BoneFamily.Unknown,
|
||||
"These bones weren't immediately identifiable.\nIf you can figure out what they're for, let us know and we'll add them to the table."
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, BoneDatum> BoneTable = new();
|
||||
|
||||
private static readonly Dictionary<string, string> BoneLookupByDispName = new();
|
||||
|
||||
static BoneData()
|
||||
{
|
||||
//apparently static constructors are only guaranteed to START before the class is called
|
||||
//which can apparently lead to race conditions, as I've found out
|
||||
//this lock is to make sure the table is fully initialized before anything else can try to look at it
|
||||
lock (BoneTable)
|
||||
{
|
||||
var rowIndex = 0;
|
||||
foreach (var entry in BoneRawTable)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cells = entry.Split(',');
|
||||
var codename = cells[0];
|
||||
var dispName = cells[1];
|
||||
|
||||
BoneTable[codename] = new BoneDatum(rowIndex, cells);
|
||||
BoneLookupByDispName[dispName] = codename;
|
||||
|
||||
if (BoneTable[codename].Family == BoneFamily.Unknown)
|
||||
{
|
||||
throw new Exception("what the fuck?");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new InvalidCastException($"Couldn't parse raw bone table @ row {rowIndex}");
|
||||
}
|
||||
|
||||
++rowIndex;
|
||||
}
|
||||
|
||||
//iterate through the complete collection and assign children to their parents
|
||||
foreach (var kvp in BoneTable)
|
||||
{
|
||||
var datum = BoneTable[kvp.Key];
|
||||
|
||||
datum.Children = BoneTable.Where(x => x.Value.Parent == kvp.Key).Select(x => x.Key).ToArray();
|
||||
|
||||
BoneTable[kvp.Key] = datum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogNewBones(params string[] boneNames)
|
||||
{
|
||||
var probablyHairstyleBones = boneNames.Where(IsProbablyHairstyle).ToArray();
|
||||
|
||||
foreach (var hairBone in ParseHairstyle(probablyHairstyleBones))
|
||||
{
|
||||
BoneTable[hairBone.Codename] = hairBone;
|
||||
}
|
||||
|
||||
foreach (var boneName in boneNames.Except(BoneTable.Keys))
|
||||
{
|
||||
var newBone = new BoneDatum
|
||||
{
|
||||
RowIndex = -1,
|
||||
Codename = boneName,
|
||||
DisplayName = $"Unknown ({boneName})",
|
||||
Family = BoneFamily.Unknown,
|
||||
Parent = "j_kosi",
|
||||
Children = Array.Empty<string>(),
|
||||
MirroredCodename = null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateParentage(string parentName, string childName)
|
||||
{
|
||||
var child = BoneTable[childName];
|
||||
var parent = BoneTable[parentName];
|
||||
|
||||
child.Parent = parentName;
|
||||
parent.Children = parent.Children.Append(childName).Distinct().ToArray();
|
||||
|
||||
BoneTable[childName] = child;
|
||||
BoneTable[parentName] = parent;
|
||||
}
|
||||
|
||||
public static string GetBoneDisplayName(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) ? row.DisplayName : codename;
|
||||
}
|
||||
|
||||
public static string? GetBoneCodename(string boneDisplayName)
|
||||
{
|
||||
return BoneLookupByDispName.TryGetValue(boneDisplayName, out var name) ? name : null;
|
||||
}
|
||||
|
||||
public static List<string> GetBoneCodenames()
|
||||
{
|
||||
return BoneTable.Keys.ToList();
|
||||
}
|
||||
|
||||
public static List<string> GetBoneCodenames(Func<BoneDatum, bool> predicate)
|
||||
{
|
||||
return BoneTable.Where(x => predicate(x.Value)).Select(x => x.Key).ToList();
|
||||
}
|
||||
|
||||
public static List<string> GetBoneDisplayNames()
|
||||
{
|
||||
return BoneLookupByDispName.Keys.ToList();
|
||||
}
|
||||
|
||||
public static BoneFamily GetBoneFamily(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) ? row.Family : BoneFamily.Unknown;
|
||||
}
|
||||
|
||||
public static bool IsDefaultBone(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) && row.IsDefault;
|
||||
}
|
||||
|
||||
public static int GetBoneRanking(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) ? row.RowIndex : 0;
|
||||
}
|
||||
|
||||
public static bool IsIVCSBone(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) && row.IsIVCS;
|
||||
}
|
||||
|
||||
public static string? GetBoneMirror(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) ? row.MirroredCodename : null;
|
||||
}
|
||||
|
||||
public static string[] GetChildren(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) ? row.Children : Array.Empty<string>();
|
||||
}
|
||||
|
||||
public static bool IsProbablyHairstyle(string codename)
|
||||
{
|
||||
return Regex.IsMatch(codename, @"j_ex_h\d\d\d\d_ke_[abcdeflrsu](_[abcdeflrsu])?");
|
||||
}
|
||||
|
||||
public static bool IsNewBone(string codename)
|
||||
{
|
||||
return !BoneTable.ContainsKey(codename);
|
||||
}
|
||||
|
||||
private static BoneFamily ParseFamilyName(string n)
|
||||
{
|
||||
var simplified = n.Split(' ').FirstOrDefault()?.ToLower() ?? string.Empty;
|
||||
|
||||
var fam = simplified switch
|
||||
{
|
||||
"root" => BoneFamily.Root,
|
||||
"spine" => BoneFamily.Spine,
|
||||
"hair" => BoneFamily.Hair,
|
||||
"face" => BoneFamily.Face,
|
||||
"ears" => BoneFamily.Ears,
|
||||
"chest" => BoneFamily.Chest,
|
||||
"arms" => BoneFamily.Arms,
|
||||
"hands" => BoneFamily.Hands,
|
||||
"tail" => BoneFamily.Tail,
|
||||
"groin" => BoneFamily.Groin,
|
||||
"legs" => BoneFamily.Legs,
|
||||
"feet" => BoneFamily.Feet,
|
||||
"earrings" => BoneFamily.Earrings,
|
||||
"hat" => BoneFamily.Hat,
|
||||
"cape" => BoneFamily.Cape,
|
||||
"armor" => BoneFamily.Armor,
|
||||
"skirt" => BoneFamily.Skirt,
|
||||
"equipment" => BoneFamily.Equipment,
|
||||
_ => BoneFamily.Unknown
|
||||
};
|
||||
|
||||
return fam;
|
||||
}
|
||||
|
||||
public struct BoneDatum : IComparable<BoneDatum>
|
||||
{
|
||||
public int RowIndex;
|
||||
|
||||
public string Codename;
|
||||
public string DisplayName;
|
||||
public BoneFamily Family;
|
||||
|
||||
public bool IsDefault;
|
||||
public bool IsIVCS;
|
||||
|
||||
public string? Parent;
|
||||
public string? MirroredCodename;
|
||||
|
||||
public string[] Children;
|
||||
|
||||
public BoneDatum(int row, string[] fields)
|
||||
{
|
||||
RowIndex = row;
|
||||
|
||||
var i = 0;
|
||||
|
||||
Codename = fields[i++];
|
||||
DisplayName = fields[i++];
|
||||
|
||||
Family = ParseFamilyName(fields[i++]);
|
||||
|
||||
IsDefault = bool.Parse(fields[i++]);
|
||||
IsIVCS = bool.Parse(fields[i++]);
|
||||
|
||||
Parent = fields[i].IsNullOrEmpty() ? null : fields[i];
|
||||
i++;
|
||||
MirroredCodename = fields[i].IsNullOrEmpty() ? null : fields[i];
|
||||
i++;
|
||||
|
||||
Children = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public int CompareTo(BoneDatum other)
|
||||
{
|
||||
return RowIndex != other.RowIndex
|
||||
? RowIndex.CompareTo(other.RowIndex)
|
||||
: string.Compare(DisplayName, other.DisplayName, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
#region hair stuff
|
||||
|
||||
private static IEnumerable<BoneDatum> ParseHairstyle(params string[] boneNames)
|
||||
{
|
||||
List<BoneDatum> output = new();
|
||||
|
||||
var index = 0;
|
||||
foreach (var style in boneNames.GroupBy(x => Regex.Match(x, @"\d\d\d\d").Value))
|
||||
{
|
||||
try
|
||||
{
|
||||
var parsedBones = style.Select(ParseHairBone).ToArray();
|
||||
|
||||
// if any of the first subs is nonstandard letter, we can presume that any bcd... are part of a rising sequence
|
||||
var firstAsc =
|
||||
parsedBones.Any(x => x.sub1 is "a" or "c" or "d" or "e");
|
||||
//and we can then presume that the second subs are directional
|
||||
//or vice versa. the naming conventions aren't really consistent about whether the sequence is first or second
|
||||
|
||||
foreach (var boneInfo in parsedBones)
|
||||
{
|
||||
StringBuilder dispName = new();
|
||||
dispName.Append($"Hair #{boneInfo.id}");
|
||||
|
||||
var sub1 = GetHairBoneSubLabel(boneInfo.sub1, firstAsc);
|
||||
var sub2 = boneInfo.sub2 == null ? null : GetHairBoneSubLabel(boneInfo.sub2, !firstAsc);
|
||||
|
||||
dispName.Append($" {sub1}");
|
||||
if (sub2 != null)
|
||||
{
|
||||
dispName.Append($" {sub2}");
|
||||
}
|
||||
|
||||
var result = new BoneDatum
|
||||
{
|
||||
RowIndex = -1,
|
||||
Codename = boneInfo.name,
|
||||
DisplayName = dispName.ToString(),
|
||||
Family = BoneFamily.Hair,
|
||||
IsDefault = false,
|
||||
IsIVCS = false,
|
||||
Parent = "j_kao",
|
||||
Children = Array.Empty<string>(),
|
||||
MirroredCodename = null
|
||||
};
|
||||
|
||||
output.Add(result);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Logger.Error($"Failed to dynamically parse bones for hairstyle of '{boneNames[index]}'");
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static (string name, int id, string sub1, string? sub2) ParseHairBone(string boneName)
|
||||
{
|
||||
var groups = Regex.Match(boneName.ToLower(), @"j_ex_h(\d\d\d\d)_ke_([abcdeflrsu])(?:_([abcdeflrsu]))?")
|
||||
.Groups;
|
||||
|
||||
var idNo = int.Parse(groups[1].Value);
|
||||
var subFirst = groups[2].Value;
|
||||
var subSecond = groups[3].Value.IsNullOrWhitespace() ? null : groups[3].Value;
|
||||
|
||||
return (boneName, idNo, subFirst, subSecond);
|
||||
}
|
||||
|
||||
private static string GetHairBoneSubLabel(string sub, bool ascending)
|
||||
{
|
||||
return (sub.ToLower(), ascending) switch
|
||||
{
|
||||
("a", _) => "A",
|
||||
("b", true) => "B",
|
||||
("b", false) => "Back",
|
||||
("c", _) => "C",
|
||||
("d", _) => "D",
|
||||
("e", _) => "E",
|
||||
("f", true) => "F",
|
||||
("f", false) => "Front",
|
||||
("l", _) => "Left",
|
||||
("r", _) => "Right",
|
||||
("u", _) => "Upper",
|
||||
("s", _) => "Side",
|
||||
(_, true) => "Next",
|
||||
(_, false) => "Bone"
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
243
CustomizePlus/Core/Data/BoneTransform.cs
Normal file
243
CustomizePlus/Core/Data/BoneTransform.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.Serialization;
|
||||
using CustomizePlus.Core.Extensions;
|
||||
using CustomizePlus.Game.Services.GPose.ExternalTools;
|
||||
using FFXIVClientStructs.Havok;
|
||||
|
||||
namespace CustomizePlus.Core.Data;
|
||||
|
||||
//not the correct terms but they double as user-visible labels so ¯\_(ツ)_/¯
|
||||
public enum BoneAttribute
|
||||
{
|
||||
//hard-coding the backing values for legacy purposes
|
||||
Position = 0,
|
||||
Rotation = 1,
|
||||
Scale = 2
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class BoneTransform
|
||||
{
|
||||
//TODO if if ever becomes a point of concern, I might be able to marginally speed things up
|
||||
//by natively storing translation and scaling values as their own vector4s
|
||||
//that way the cost of translating back and forth to vector3s would be frontloaded
|
||||
//to when the user is updating things instead of during the render loop
|
||||
|
||||
public BoneTransform()
|
||||
{
|
||||
Translation = Vector3.Zero;
|
||||
Rotation = Vector3.Zero;
|
||||
Scaling = Vector3.One;
|
||||
}
|
||||
|
||||
public BoneTransform(BoneTransform original)
|
||||
{
|
||||
UpdateToMatch(original);
|
||||
}
|
||||
|
||||
private Vector3 _translation;
|
||||
public Vector3 Translation
|
||||
{
|
||||
get => _translation;
|
||||
set => _translation = ClampVector(value);
|
||||
}
|
||||
|
||||
private Vector3 _rotation;
|
||||
public Vector3 Rotation
|
||||
{
|
||||
get => _rotation;
|
||||
set => _rotation = ClampAngles(value);
|
||||
}
|
||||
|
||||
private Vector3 _scaling;
|
||||
public Vector3 Scaling
|
||||
{
|
||||
get => _scaling;
|
||||
set => _scaling = ClampVector(value);
|
||||
}
|
||||
|
||||
[OnDeserialized]
|
||||
internal void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
//Sanitize all values on deserialization
|
||||
_translation = ClampToDefaultLimits(_translation);
|
||||
_rotation = ClampAngles(_rotation);
|
||||
_scaling = ClampToDefaultLimits(_scaling);
|
||||
}
|
||||
|
||||
public bool IsEdited()
|
||||
{
|
||||
return !Translation.IsApproximately(Vector3.Zero, 0.00001f)
|
||||
|| !Rotation.IsApproximately(Vector3.Zero, 0.1f)
|
||||
|| !Scaling.IsApproximately(Vector3.One, 0.00001f);
|
||||
}
|
||||
|
||||
public BoneTransform DeepCopy()
|
||||
{
|
||||
return new BoneTransform
|
||||
{
|
||||
Translation = Translation,
|
||||
Rotation = Rotation,
|
||||
Scaling = Scaling
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateAttribute(BoneAttribute which, Vector3 newValue)
|
||||
{
|
||||
if (which == BoneAttribute.Position)
|
||||
{
|
||||
Translation = newValue;
|
||||
}
|
||||
else if (which == BoneAttribute.Rotation)
|
||||
{
|
||||
Rotation = newValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
Scaling = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateToMatch(BoneTransform newValues)
|
||||
{
|
||||
Translation = newValues.Translation;
|
||||
Rotation = newValues.Rotation;
|
||||
Scaling = newValues.Scaling;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flip a bone's transforms from left to right, so you can use it to update its sibling.
|
||||
/// IVCS bones need to use the special reflection instead.
|
||||
/// </summary>
|
||||
public BoneTransform GetStandardReflection()
|
||||
{
|
||||
return new BoneTransform
|
||||
{
|
||||
Translation = new Vector3(Translation.X, Translation.Y, -1 * Translation.Z),
|
||||
Rotation = new Vector3(-1 * Rotation.X, -1 * Rotation.Y, Rotation.Z),
|
||||
Scaling = Scaling
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flip a bone's transforms from left to right, so you can use it to update its sibling.
|
||||
/// IVCS bones are oriented in a system with different symmetries, so they're handled specially.
|
||||
/// </summary>
|
||||
public BoneTransform GetSpecialReflection()
|
||||
{
|
||||
return new BoneTransform
|
||||
{
|
||||
Translation = new Vector3(Translation.X, -1 * Translation.Y, Translation.Z),
|
||||
Rotation = new Vector3(Rotation.X, -1 * Rotation.Y, -1 * Rotation.Z),
|
||||
Scaling = Scaling
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitize all vectors inside of this container.
|
||||
/// </summary>
|
||||
private void Sanitize()
|
||||
{
|
||||
_translation = ClampVector(_translation);
|
||||
_rotation = ClampAngles(_rotation);
|
||||
_scaling = ClampVector(_scaling);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp all vector values to be within allowed limits.
|
||||
/// </summary>
|
||||
private Vector3 ClampVector(Vector3 vector)
|
||||
{
|
||||
return new Vector3
|
||||
{
|
||||
X = Math.Clamp(vector.X, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit),
|
||||
Y = Math.Clamp(vector.Y, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit),
|
||||
Z = Math.Clamp(vector.Z, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit)
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector3 ClampAngles(Vector3 rotVec)
|
||||
{
|
||||
static float Clamp(float angle)
|
||||
{
|
||||
if (angle > 180)
|
||||
angle -= 360;
|
||||
else if (angle < -180)
|
||||
angle += 360;
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
rotVec.X = Clamp(rotVec.X);
|
||||
rotVec.Y = Clamp(rotVec.Y);
|
||||
rotVec.Z = Clamp(rotVec.Z);
|
||||
|
||||
return rotVec;
|
||||
}
|
||||
|
||||
public hkQsTransformf ModifyExistingTransform(hkQsTransformf tr)
|
||||
{
|
||||
return ModifyExistingTranslationWithRotation(ModifyExistingRotation(ModifyExistingScale(tr)));
|
||||
}
|
||||
|
||||
public hkQsTransformf ModifyExistingScale(hkQsTransformf tr)
|
||||
{
|
||||
if (PosingModeDetectService.IsAnamnesisScalingFrozen) return tr;
|
||||
|
||||
tr.Scale.X *= Scaling.X;
|
||||
tr.Scale.Y *= Scaling.Y;
|
||||
tr.Scale.Z *= Scaling.Z;
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
public hkQsTransformf ModifyExistingRotation(hkQsTransformf tr)
|
||||
{
|
||||
if (PosingModeDetectService.IsAnamnesisRotationFrozen) return tr;
|
||||
|
||||
var newRotation = Quaternion.Multiply(tr.Rotation.ToQuaternion(), Rotation.ToQuaternion());
|
||||
tr.Rotation.X = newRotation.X;
|
||||
tr.Rotation.Y = newRotation.Y;
|
||||
tr.Rotation.Z = newRotation.Z;
|
||||
tr.Rotation.W = newRotation.W;
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
public hkQsTransformf ModifyExistingTranslationWithRotation(hkQsTransformf tr)
|
||||
{
|
||||
if (PosingModeDetectService.IsAnamnesisPositionFrozen) return tr;
|
||||
|
||||
var adjustedTranslation = Vector4.Transform(Translation, tr.Rotation.ToQuaternion());
|
||||
tr.Translation.X += adjustedTranslation.X;
|
||||
tr.Translation.Y += adjustedTranslation.Y;
|
||||
tr.Translation.Z += adjustedTranslation.Z;
|
||||
tr.Translation.W += adjustedTranslation.W;
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
public hkQsTransformf ModifyExistingTranslation(hkQsTransformf tr)
|
||||
{
|
||||
if (PosingModeDetectService.IsAnamnesisPositionFrozen) return tr;
|
||||
|
||||
tr.Translation.X += Translation.X;
|
||||
tr.Translation.Y += Translation.Y;
|
||||
tr.Translation.Z += Translation.Z;
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp all vector values to be within allowed limits.
|
||||
/// </summary>
|
||||
private static Vector3 ClampToDefaultLimits(Vector3 vector)
|
||||
{
|
||||
vector.X = Math.Clamp(vector.X, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit);
|
||||
vector.Y = Math.Clamp(vector.Y, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit);
|
||||
vector.Z = Math.Clamp(vector.Z, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit);
|
||||
|
||||
return vector;
|
||||
}
|
||||
}
|
||||
82
CustomizePlus/Core/Data/Constants.cs
Normal file
82
CustomizePlus/Core/Data/Constants.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using FFXIVClientStructs.Havok;
|
||||
|
||||
namespace CustomizePlus.Core.Data;
|
||||
|
||||
internal static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// Version of the configuration file, when increased a converter should be implemented if necessary.
|
||||
/// </summary>
|
||||
public const int ConfigurationVersion = 4;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the root bone.
|
||||
/// </summary>
|
||||
public const string RootBoneName = "n_root";
|
||||
|
||||
/// <summary>
|
||||
/// Minimum allowed value for any of the vector values.
|
||||
/// </summary>
|
||||
public const int MinVectorValueLimit = -512;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed value for any of the vector values.
|
||||
/// </summary>
|
||||
public const int MaxVectorValueLimit = 512;
|
||||
|
||||
/// <summary>
|
||||
/// Predicate function for determining if the given object table index represents an
|
||||
/// NPC in a busy area (i.e. there are ~245 other objects already).
|
||||
/// </summary>
|
||||
public static bool IsInObjectTableBusyNPCRange(int index) => index > 245;
|
||||
|
||||
/// <summary>
|
||||
/// A "null" havok vector. Since the type isn't inherently nullable, and the default value (0, 0, 0, 0)
|
||||
/// is valid input in a lot of cases, we can use this instead.
|
||||
/// </summary>
|
||||
public static readonly hkVector4f NullVector = new()
|
||||
{
|
||||
X = float.NaN,
|
||||
Y = float.NaN,
|
||||
Z = float.NaN,
|
||||
W = float.NaN
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A "null" havok quaternion. Since the type isn't inherently nullable, and the default value (0, 0, 0, 0)
|
||||
/// is valid input in a lot of cases, we can use this instead.
|
||||
/// </summary>
|
||||
public static readonly hkQuaternionf NullQuaternion = new()
|
||||
{
|
||||
X = float.NaN,
|
||||
Y = float.NaN,
|
||||
Z = float.NaN,
|
||||
W = float.NaN
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A "null" havok transform. Since the type isn't inherently nullable, and the default values
|
||||
/// aren't immediately obviously wrong, we can use this instead.
|
||||
/// </summary>
|
||||
public static readonly hkQsTransformf NullTransform = new()
|
||||
{
|
||||
Translation = NullVector,
|
||||
Rotation = NullQuaternion,
|
||||
Scale = NullVector
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The pose at index 0 is the only one we apparently need to care about.
|
||||
/// </summary>
|
||||
public const int TruePoseIndex = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Main render hook address
|
||||
/// </summary>
|
||||
public const string RenderHookAddress = "E8 ?? ?? ?? ?? 48 81 C3 ?? ?? ?? ?? BF ?? ?? ?? ?? 33 ED";
|
||||
|
||||
/// <summary>
|
||||
/// Movement hook address, used for position offset and other changes which cannot be done in main hook
|
||||
/// </summary>
|
||||
public const string MovementHookAddress = "E8 ?? ?? ?? ?? EB 29 48 8B 5F 08";
|
||||
}
|
||||
23
CustomizePlus/Core/Events/ReloadEvent.cs
Normal file
23
CustomizePlus/Core/Events/ReloadEvent.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using OtterGui.Classes;
|
||||
using System;
|
||||
|
||||
namespace CustomizePlus.Core.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when complete plugin reload is requested
|
||||
/// </summary>
|
||||
public sealed class ReloadEvent() : EventWrapper<ReloadEvent.Type, ReloadEvent.Priority>(nameof(ReloadEvent))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
ReloadAll,
|
||||
ReloadProfiles,
|
||||
ReloadTemplates
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
TemplateManager = -2,
|
||||
ProfileManager = -1
|
||||
}
|
||||
}
|
||||
32
CustomizePlus/Core/Extensions/StringExtensions.cs
Normal file
32
CustomizePlus/Core/Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace CustomizePlus.Core.Extensions
|
||||
{
|
||||
internal static class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Incognify string. Usually used for logging character names and stuff. Does nothing in debug build.
|
||||
/// </summary>
|
||||
public static string Incognify(this string str)
|
||||
{
|
||||
if (str.IsNullOrWhitespace())
|
||||
return str;
|
||||
|
||||
#if DEBUG
|
||||
return str;
|
||||
#endif
|
||||
|
||||
if (str.Contains(" "))
|
||||
{
|
||||
var split = str.Split(' ');
|
||||
|
||||
if (split.Length > 2)
|
||||
return $"{str[..2]}...";
|
||||
|
||||
return $"{split[0][0]}.{split[1][0]}";
|
||||
}
|
||||
|
||||
return $"{str[..2]}...";
|
||||
}
|
||||
}
|
||||
}
|
||||
56
CustomizePlus/Core/Extensions/TransformExtensions.cs
Normal file
56
CustomizePlus/Core/Extensions/TransformExtensions.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using CustomizePlus.Core.Data;
|
||||
using FFXIVClientStructs.Havok;
|
||||
|
||||
//using FFXIVClientStructs.FFXIV.Client.Graphics;
|
||||
|
||||
namespace CustomizePlus.Core.Extensions;
|
||||
|
||||
internal static class TransformExtensions
|
||||
{
|
||||
public static bool Equals(this hkQsTransformf first, hkQsTransformf second)
|
||||
{
|
||||
return first.Translation.Equals(second.Translation)
|
||||
&& first.Rotation.Equals(second.Rotation)
|
||||
&& first.Scale.Equals(second.Scale);
|
||||
}
|
||||
|
||||
public static bool IsNull(this hkQsTransformf t)
|
||||
{
|
||||
return t.Equals(Constants.NullTransform);
|
||||
}
|
||||
|
||||
public static hkQsTransformf ToHavokTransform(this BoneTransform bt)
|
||||
{
|
||||
return new hkQsTransformf
|
||||
{
|
||||
Translation = bt.Translation.ToHavokTranslation(),
|
||||
Rotation = bt.Rotation.ToQuaternion().ToHavokRotation(),
|
||||
Scale = bt.Scaling.ToHavokScaling()
|
||||
};
|
||||
}
|
||||
|
||||
public static BoneTransform ToBoneTransform(this hkQsTransformf t)
|
||||
{
|
||||
var rotVec = Quaternion.Divide(t.Translation.ToQuaternion(), t.Rotation.ToQuaternion());
|
||||
|
||||
return new BoneTransform
|
||||
{
|
||||
Translation = new Vector3(rotVec.X / rotVec.W, rotVec.Y / rotVec.W, rotVec.Z / rotVec.W),
|
||||
Rotation = t.Rotation.ToQuaternion().ToEulerAngles(),
|
||||
Scaling = new Vector3(t.Scale.X, t.Scale.Y, t.Scale.Z)
|
||||
};
|
||||
}
|
||||
|
||||
public static hkVector4f GetAttribute(this hkQsTransformf t, BoneAttribute att)
|
||||
{
|
||||
return att switch
|
||||
{
|
||||
BoneAttribute.Position => t.Translation,
|
||||
BoneAttribute.Rotation => t.Rotation.ToQuaternion().GetAsNumericsVector().ToHavokVector(),
|
||||
BoneAttribute.Scale => t.Scale,
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
}
|
||||
144
CustomizePlus/Core/Extensions/VectorExtensions.cs
Normal file
144
CustomizePlus/Core/Extensions/VectorExtensions.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using CustomizePlus.Anamnesis.Data;
|
||||
using FFXIVClientStructs.Havok;
|
||||
|
||||
namespace CustomizePlus.Core.Extensions;
|
||||
|
||||
internal static class VectorExtensions
|
||||
{
|
||||
public static bool IsApproximately(this hkVector4f vector, Vector3 other, float errorMargin = 0.001f)
|
||||
{
|
||||
return IsApproximately(vector.X, other.X, errorMargin)
|
||||
&& IsApproximately(vector.Y, other.Y, errorMargin)
|
||||
&& IsApproximately(vector.Z, other.Z, errorMargin);
|
||||
}
|
||||
|
||||
public static bool IsApproximately(this Vector3 vector, Vector3 other, float errorMargin = 0.001f)
|
||||
{
|
||||
return IsApproximately(vector.X, other.X, errorMargin)
|
||||
&& IsApproximately(vector.Y, other.Y, errorMargin)
|
||||
&& IsApproximately(vector.Z, other.Z, errorMargin);
|
||||
}
|
||||
|
||||
private static bool IsApproximately(float a, float b, float errorMargin)
|
||||
{
|
||||
var d = MathF.Abs(a - b);
|
||||
return d < errorMargin;
|
||||
}
|
||||
|
||||
public static Quaternion ToQuaternion(this Vector3 rotation)
|
||||
{
|
||||
return Quaternion.CreateFromYawPitchRoll(
|
||||
rotation.X * MathF.PI / 180,
|
||||
rotation.Y * MathF.PI / 180,
|
||||
rotation.Z * MathF.PI / 180);
|
||||
}
|
||||
|
||||
public static Vector3 ToEulerAngles(this Quaternion q)
|
||||
{
|
||||
var nq = Vector4.Normalize(q.GetAsNumericsVector());
|
||||
|
||||
var rollX = MathF.Atan2(
|
||||
2 * (nq.W * nq.X + nq.Y * nq.Z),
|
||||
1 - 2 * (nq.X * nq.X + nq.Y * nq.Y));
|
||||
|
||||
var pitchY = 2 * MathF.Atan2(
|
||||
MathF.Sqrt(1 + 2 * (nq.W * nq.Y - nq.X * nq.Z)),
|
||||
MathF.Sqrt(1 - 2 * (nq.W * nq.Y - nq.X * nq.Z)));
|
||||
|
||||
var yawZ = MathF.Atan2(
|
||||
2 * (nq.W * nq.Z + nq.X * nq.Y),
|
||||
1 - 2 * (nq.Y * nq.Y + nq.Z * nq.Z));
|
||||
|
||||
return new Vector3(rollX, pitchY, yawZ);
|
||||
}
|
||||
|
||||
public static Quaternion ToQuaternion(this Vector4 rotation)
|
||||
{
|
||||
return new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W);
|
||||
}
|
||||
|
||||
public static Quaternion ToQuaternion(this hkQuaternionf rotation)
|
||||
{
|
||||
return new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W);
|
||||
}
|
||||
|
||||
public static Quaternion ToQuaternion(this hkVector4f rotation)
|
||||
{
|
||||
return new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W);
|
||||
}
|
||||
|
||||
|
||||
public static hkQuaternionf ToHavokRotation(this Quaternion rotation)
|
||||
{
|
||||
return new hkQuaternionf
|
||||
{
|
||||
X = rotation.X,
|
||||
Y = rotation.Y,
|
||||
Z = rotation.Z,
|
||||
W = rotation.W
|
||||
};
|
||||
}
|
||||
|
||||
public static hkVector4f ToHavokTranslation(this Vector3 translation)
|
||||
{
|
||||
return new hkVector4f
|
||||
{
|
||||
X = translation.X,
|
||||
Y = translation.Y,
|
||||
Z = translation.Z,
|
||||
W = 0.0f
|
||||
};
|
||||
}
|
||||
|
||||
public static hkVector4f ToHavokScaling(this Vector3 scaling)
|
||||
{
|
||||
return new hkVector4f
|
||||
{
|
||||
X = scaling.X,
|
||||
Y = scaling.Y,
|
||||
Z = scaling.Z,
|
||||
W = 1.0f
|
||||
};
|
||||
}
|
||||
|
||||
public static hkVector4f ToHavokVector(this Vector4 vec)
|
||||
{
|
||||
return new hkVector4f
|
||||
{
|
||||
X = vec.X,
|
||||
Y = vec.Y,
|
||||
Z = vec.Z,
|
||||
W = vec.W
|
||||
};
|
||||
}
|
||||
|
||||
public static Vector3 GetAsNumericsVector(this PoseFile.Vector vec)
|
||||
{
|
||||
return new Vector3(vec.X, vec.Y, vec.Z);
|
||||
}
|
||||
|
||||
public static Vector4 GetAsNumericsVector(this hkVector4f vec)
|
||||
{
|
||||
return new Vector4(vec.X, vec.Y, vec.Z, vec.W);
|
||||
}
|
||||
|
||||
public static Vector4 GetAsNumericsVector(this Quaternion q)
|
||||
{
|
||||
return new Vector4(q.X, q.Y, q.Z, q.W);
|
||||
}
|
||||
|
||||
public static Vector3 RemoveWTerm(this Vector4 vec)
|
||||
{
|
||||
return new Vector3(vec.X, vec.Y, vec.Z);
|
||||
}
|
||||
|
||||
public static bool Equals(this hkVector4f first, hkVector4f second)
|
||||
{
|
||||
return first.X == second.X
|
||||
&& first.Y == second.Y
|
||||
&& first.Z == second.Z
|
||||
&& first.W == second.W;
|
||||
}
|
||||
}
|
||||
60
CustomizePlus/Core/Helpers/Base64Helper.cs
Normal file
60
CustomizePlus/Core/Helpers/Base64Helper.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CustomizePlus.Core.Helpers;
|
||||
|
||||
public static class Base64Helper
|
||||
{
|
||||
// Compress any type to a base64 encoding of its compressed json representation, prepended with a version byte.
|
||||
// Returns an empty string on failure.
|
||||
// Original by Ottermandias: OtterGui <3
|
||||
public static unsafe string ExportToBase64<T>(T obj, byte version)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(obj, Formatting.None);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
using var compressedStream = new MemoryStream();
|
||||
using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress))
|
||||
{
|
||||
zipStream.Write(new ReadOnlySpan<byte>(&version, 1));
|
||||
zipStream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
return Convert.ToBase64String(compressedStream.ToArray());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
// Decompress a base64 encoded string to the given type and a prepended version byte if possible.
|
||||
// On failure, data will be String error and version will be byte.MaxValue.
|
||||
// Original by Ottermandias: OtterGui <3
|
||||
public static byte ImportFromBase64(string base64, out string data)
|
||||
{
|
||||
var version = byte.MaxValue;
|
||||
try
|
||||
{
|
||||
var bytes = Convert.FromBase64String(base64);
|
||||
using var compressedStream = new MemoryStream(bytes);
|
||||
using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
|
||||
using var resultStream = new MemoryStream();
|
||||
zipStream.CopyTo(resultStream);
|
||||
bytes = resultStream.ToArray();
|
||||
version = bytes[0];
|
||||
var json = Encoding.UTF8.GetString(bytes, 1, bytes.Length - 1);
|
||||
data = json;
|
||||
}
|
||||
catch
|
||||
{
|
||||
data = "error";
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
}
|
||||
122
CustomizePlus/Core/Helpers/CtrlHelper.cs
Normal file
122
CustomizePlus/Core/Helpers/CtrlHelper.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Utility;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace CustomizePlus.Core.Helpers;
|
||||
|
||||
public static class CtrlHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the width of an icon button, checkbox, etc...
|
||||
/// </summary>
|
||||
/// per https://github.com/ocornut/imgui/issues/3714#issuecomment-759319268
|
||||
public static float IconButtonWidth => ImGui.GetFrameHeight() + 2 * ImGui.GetStyle().ItemInnerSpacing.X;
|
||||
|
||||
public static bool TextBox(string label, ref string value)
|
||||
{
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
|
||||
return ImGui.InputText(label, ref value, 1024);
|
||||
}
|
||||
|
||||
public static bool TextPropertyBox(string label, Func<string> get, Action<string> set)
|
||||
{
|
||||
var temp = get();
|
||||
var result = TextBox(label, ref temp);
|
||||
if (result)
|
||||
{
|
||||
set(temp);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool Checkbox(string label, ref bool value)
|
||||
{
|
||||
return ImGui.Checkbox(label, ref value);
|
||||
}
|
||||
|
||||
public static bool CheckboxWithTextAndHelp(string label, string text, string helpText, ref bool value)
|
||||
{
|
||||
var checkBoxState = ImGui.Checkbox(label, ref value);
|
||||
ImGui.SameLine();
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
ImGui.Text(FontAwesomeIcon.InfoCircle.ToIconString());
|
||||
ImGui.PopFont();
|
||||
AddHoverText(helpText);
|
||||
ImGui.SameLine();
|
||||
ImGui.Text(text);
|
||||
|
||||
AddHoverText(helpText);
|
||||
|
||||
return checkBoxState;
|
||||
}
|
||||
|
||||
public static bool CheckboxToggle(string label, in bool shown, Action<bool> toggle)
|
||||
{
|
||||
var temp = shown;
|
||||
var toggled = ImGui.Checkbox(label, ref temp);
|
||||
|
||||
if (toggled)
|
||||
{
|
||||
toggle(temp);
|
||||
}
|
||||
|
||||
return toggled;
|
||||
}
|
||||
|
||||
public static bool ArrowToggle(string label, ref bool value)
|
||||
{
|
||||
var toggled = ImGui.ArrowButton(label, value ? ImGuiDir.Down : ImGuiDir.Right);
|
||||
|
||||
if (toggled)
|
||||
{
|
||||
value = !value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static void AddHoverText(string text)
|
||||
{
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.SetTooltip(text);
|
||||
}
|
||||
}
|
||||
|
||||
public enum TextAlignment { Left, Center, Right };
|
||||
public static void StaticLabel(string? text, TextAlignment align = TextAlignment.Left, string tooltip = "")
|
||||
{
|
||||
if (text != null)
|
||||
{
|
||||
if (align == TextAlignment.Center)
|
||||
{
|
||||
ImGui.Dummy(new System.Numerics.Vector2((ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize(text).X) / 2, 0));
|
||||
ImGui.SameLine();
|
||||
}
|
||||
else if (align == TextAlignment.Right)
|
||||
{
|
||||
ImGui.Dummy(new System.Numerics.Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize(text).X, 0));
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
ImGui.Text(text);
|
||||
if (!tooltip.IsNullOrWhitespace())
|
||||
{
|
||||
AddHoverText(tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void LabelWithIcon(FontAwesomeIcon icon, string text, bool isSameLine = true)
|
||||
{
|
||||
if (isSameLine)
|
||||
ImGui.SameLine();
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
ImGui.Text(icon.ToIconString());
|
||||
ImGui.PopFont();
|
||||
ImGui.SameLine();
|
||||
ImGui.TextWrapped(text);
|
||||
}
|
||||
}
|
||||
22
CustomizePlus/Core/Helpers/NameParsingHelper.cs
Normal file
22
CustomizePlus/Core/Helpers/NameParsingHelper.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace CustomizePlus.Core.Helpers;
|
||||
|
||||
//todo: better name
|
||||
internal static class NameParsingHelper
|
||||
{
|
||||
internal static (string Name, string? Path) ParseName(string name, bool handlePath)
|
||||
{
|
||||
var actualName = name;
|
||||
string? path = null;
|
||||
if (handlePath)
|
||||
{
|
||||
var slashPos = name.LastIndexOf('/');
|
||||
if (slashPos >= 0)
|
||||
{
|
||||
path = name[..slashPos];
|
||||
actualName = slashPos >= name.Length - 1 ? "<Unnamed>" : name[(slashPos + 1)..];
|
||||
}
|
||||
}
|
||||
|
||||
return (actualName, path);
|
||||
}
|
||||
}
|
||||
194
CustomizePlus/Core/ServiceManager.cs
Normal file
194
CustomizePlus/Core/ServiceManager.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using Dalamud.Plugin;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Debug;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Configuration.Services;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
|
||||
using CustomizePlus.Armatures.Events;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Core.Events;
|
||||
using CustomizePlus.UI;
|
||||
using CustomizePlus.UI.Windows.Controls;
|
||||
using CustomizePlus.Anamnesis;
|
||||
using CustomizePlus.Armatures.Services;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
|
||||
using CustomizePlus.UI.Windows.MainWindow;
|
||||
using CustomizePlus.Game.Events;
|
||||
using CustomizePlus.UI.Windows;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using CustomizePlus.Profiles.Events;
|
||||
using CustomizePlus.Api.Compatibility;
|
||||
using CustomizePlus.Game.Services.GPose;
|
||||
using CustomizePlus.Game.Services.GPose.ExternalTools;
|
||||
using CustomizePlus.GameData.Services;
|
||||
using CustomizePlus.Configuration.Services.Temporary;
|
||||
|
||||
namespace CustomizePlus.Core;
|
||||
|
||||
public static class ServiceManager
|
||||
{
|
||||
public static ServiceProvider CreateProvider(DalamudPluginInterface pi, Logger logger)
|
||||
{
|
||||
var services = new ServiceCollection()
|
||||
.AddSingleton(logger)
|
||||
.AddDalamud(pi)
|
||||
.AddCore()
|
||||
.AddEvents()
|
||||
.AddGPoseServices()
|
||||
.AddArmatureServices()
|
||||
.AddUI()
|
||||
.AddGameDataServices()
|
||||
.AddTemplateServices()
|
||||
.AddProfileServices()
|
||||
.AddGameServices()
|
||||
.AddConfigServices()
|
||||
.AddRestOfServices();
|
||||
return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });
|
||||
}
|
||||
|
||||
private static IServiceCollection AddDalamud(this IServiceCollection services, DalamudPluginInterface pluginInterface)
|
||||
{
|
||||
new DalamudServices(pluginInterface).AddServices(services);
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddGPoseServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<PosingModeDetectService>()
|
||||
.AddSingleton<GPoseService>()
|
||||
.AddSingleton<GPoseStateChanged>();
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddArmatureServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<ArmatureManager>();
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddUI(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<TemplateCombo>()
|
||||
.AddSingleton<PluginStateBlock>()
|
||||
.AddSingleton<SettingsTab>()
|
||||
// template
|
||||
.AddSingleton<TemplatesTab>()
|
||||
.AddSingleton<TemplateFileSystemSelector>()
|
||||
.AddSingleton<TemplatePanel>()
|
||||
.AddSingleton<BoneEditorPanel>()
|
||||
// /template
|
||||
// profile
|
||||
.AddSingleton<ProfilesTab>()
|
||||
.AddSingleton<ProfileFileSystemSelector>()
|
||||
.AddSingleton<ProfilePanel>()
|
||||
// /profile
|
||||
// messages
|
||||
.AddSingleton<MessageService>()
|
||||
.AddSingleton<MessagesTab>()
|
||||
// /messages
|
||||
//
|
||||
.AddSingleton<IPCTestTab>()
|
||||
.AddSingleton<StateMonitoringTab>()
|
||||
//
|
||||
.AddSingleton<PopupSystem>()
|
||||
.AddSingleton<CPlusChangeLog>()
|
||||
.AddSingleton<CPlusWindowSystem>()
|
||||
.AddSingleton<MainWindow>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddEvents(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<ProfileChanged>()
|
||||
.AddSingleton<TemplateChanged>()
|
||||
.AddSingleton<ReloadEvent>()
|
||||
.AddSingleton<ArmatureChanged>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddCore(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<HookingService>()
|
||||
.AddSingleton<ChatService>()
|
||||
.AddSingleton<CommandService>()
|
||||
.AddSingleton<SaveService>()
|
||||
.AddSingleton<FilenameService>()
|
||||
.AddSingleton<BackupService>()
|
||||
.AddSingleton<FantasiaPlusDetectService>()
|
||||
.AddSingleton<FrameworkManager>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddRestOfServices(this IServiceCollection services) //temp
|
||||
{
|
||||
services
|
||||
.AddSingleton<PoseFileBoneLoader>()
|
||||
.AddSingleton<CustomizePlusIpc>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddConfigServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<PluginConfiguration>()
|
||||
.AddSingleton<ConfigurationMigrator>()
|
||||
.AddSingleton<FantasiaPlusConfigMover>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddGameServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<GameObjectService>()
|
||||
.AddSingleton<GameStateService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddProfileServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<ProfileManager>()
|
||||
.AddSingleton<ProfileFileSystem>()
|
||||
.AddSingleton<TemplateEditorManager>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddTemplateServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<TemplateManager>()
|
||||
.AddSingleton<TemplateFileSystem>()
|
||||
.AddSingleton<TemplateEditorManager>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddGameDataServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<CutsceneService>()
|
||||
.AddSingleton<GameEventManager>()
|
||||
.AddSingleton<ActorService>()
|
||||
.AddSingleton<ObjectManager>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
59
CustomizePlus/Core/Services/BackupService.cs
Normal file
59
CustomizePlus/Core/Services/BackupService.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
public class BackupService
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly FilenameService _filenameService;
|
||||
private readonly DirectoryInfo _configDirectory;
|
||||
private readonly IReadOnlyList<FileInfo> _fileNames;
|
||||
|
||||
public BackupService(Logger logger, FilenameService filenameService)
|
||||
{
|
||||
_logger = logger;
|
||||
_filenameService = filenameService;
|
||||
_fileNames = PluginFiles(_filenameService);
|
||||
_configDirectory = new DirectoryInfo(_filenameService.ConfigDirectory);
|
||||
Backup.CreateAutomaticBackup(logger, _configDirectory, _fileNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a permanent backup with a given name for migrations.
|
||||
/// </summary>
|
||||
public void CreateMigrationBackup(string name)
|
||||
=> Backup.CreatePermanentBackup(_logger, _configDirectory, _fileNames, name);
|
||||
|
||||
/// <summary>
|
||||
/// Create backup for all version 3 configuration files
|
||||
/// </summary>
|
||||
public void CreateV3Backup(string name = "v3_to_v4_migration")
|
||||
{
|
||||
var list = new List<FileInfo>(16) { new(_filenameService.ConfigFile) };
|
||||
list.AddRange(Directory.EnumerateFiles(_filenameService.ConfigDirectory, "*.profile", SearchOption.TopDirectoryOnly).Select(x => new FileInfo(x)));
|
||||
|
||||
Backup.CreatePermanentBackup(_logger, _configDirectory, list, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collect all relevant files for plugin configuration.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<FileInfo> PluginFiles(FilenameService fileNames)
|
||||
{
|
||||
var list = new List<FileInfo>(16)
|
||||
{
|
||||
new(fileNames.ConfigFile),
|
||||
new(fileNames.ProfileFileSystem),
|
||||
new(fileNames.TemplateFileSystem)
|
||||
};
|
||||
|
||||
list.AddRange(fileNames.Profiles());
|
||||
list.AddRange(fileNames.Templates());
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
182
CustomizePlus/Core/Services/CommandService.cs
Normal file
182
CustomizePlus/Core/Services/CommandService.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Classes;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using OtterGui.Log;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
|
||||
using CustomizePlus.UI.Windows.MainWindow;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
public class CommandService : IDisposable
|
||||
{
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly GameObjectService _gameObjectService;
|
||||
private readonly ICommandManager _commandManager;
|
||||
private readonly Logger _logger;
|
||||
private readonly ChatService _chatService;
|
||||
private readonly MainWindow _mainWindow;
|
||||
private readonly BoneEditorPanel _boneEditorPanel;
|
||||
private readonly MessageService _messageService;
|
||||
|
||||
private static readonly string[] Commands = new[] { "/customize", "/c+" };
|
||||
|
||||
public CommandService(
|
||||
ProfileManager profileManager,
|
||||
GameObjectService gameObjectService,
|
||||
ICommandManager commandManager,
|
||||
MainWindow mainWindow,
|
||||
ChatService chatService,
|
||||
BoneEditorPanel boneEditorPanel,
|
||||
Logger logger,
|
||||
MessageService messageService)
|
||||
{
|
||||
_profileManager = profileManager;
|
||||
_gameObjectService = gameObjectService;
|
||||
_commandManager = commandManager;
|
||||
_logger = logger;
|
||||
_chatService = chatService;
|
||||
_mainWindow = mainWindow;
|
||||
_boneEditorPanel = boneEditorPanel;
|
||||
_messageService = messageService;
|
||||
|
||||
foreach (var command in Commands)
|
||||
{
|
||||
_commandManager.AddHandler(command, new CommandInfo(OnMainCommand) { HelpMessage = "Toggles main plugin window if no commands passed. Use \"/customize help\" for list of available commands." });
|
||||
}
|
||||
|
||||
chatService.PrintInChat($"Started!"); //safe to assume at this point we have successfully initialized
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var command in Commands)
|
||||
{
|
||||
_commandManager.RemoveHandler(command);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMainCommand(string command, string arguments)
|
||||
{
|
||||
if (_boneEditorPanel.IsEditorActive)
|
||||
{
|
||||
_messageService.NotificationMessage("Customize+ commands cannot be used when editor is active", NotificationType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var argumentList = arguments.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var argument = argumentList.Length == 2 ? argumentList[1] : string.Empty;
|
||||
|
||||
if (arguments.Length > 0)
|
||||
{
|
||||
switch (argumentList[0].ToLowerInvariant())
|
||||
{
|
||||
case "apply":
|
||||
Apply(argument);
|
||||
return;
|
||||
case "toggle":
|
||||
Apply(argument, true);
|
||||
return;
|
||||
default:
|
||||
case "help":
|
||||
PrintHelp(argumentList[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_mainWindow.Toggle();
|
||||
}
|
||||
|
||||
private bool PrintHelp(string argument)
|
||||
{
|
||||
if (!string.Equals(argument, "help", StringComparison.OrdinalIgnoreCase) && argument != "?")
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddText("The given argument ").AddRed(argument, true)
|
||||
.AddText(" is not valid. Valid arguments are:").BuiltString);
|
||||
else
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddText("Valid arguments for /customize are:").BuiltString);
|
||||
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddCommand("apply", "Applies a given profile for a given character. Use without arguments for help.")
|
||||
.BuiltString);
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddCommand("toggle", "Toggles a given profile for a given character. Use without arguments for help.")
|
||||
.BuiltString);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Apply(string argument, bool toggle = false)
|
||||
{
|
||||
var argumentList = argument.Split(',', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (argumentList.Length != 2)
|
||||
{
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddText($"Usage: /customize {(toggle ? "toggle" : "apply")} ").AddBlue("Character Name", true)
|
||||
.AddText(",")
|
||||
.AddRed("Profile Name", true)
|
||||
.BuiltString);
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddText(" 》 ")
|
||||
.AddBlue("Character Name", true).AddText("can be either full character name or one of the following:").BuiltString);
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddText(" 》 To apply to yourself: ").AddBlue("<me>").AddText(", ").AddBlue("self").BuiltString);
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddText(" 》 To apply to target: ").AddBlue("<t>").AddText(", ").AddBlue("target").BuiltString);
|
||||
return;
|
||||
}
|
||||
|
||||
string charaName = "", profName = "";
|
||||
|
||||
try
|
||||
{
|
||||
(charaName, profName) = argumentList switch { var a => (a[0].Trim(), a[1].Trim()) };
|
||||
|
||||
charaName = charaName switch
|
||||
{
|
||||
"<me>" => _gameObjectService.GetCurrentPlayerName() ?? string.Empty,
|
||||
"self" => _gameObjectService.GetCurrentPlayerName() ?? string.Empty,
|
||||
"<t>" => _gameObjectService.GetCurrentPlayerTargetName() ?? string.Empty,
|
||||
"target" => _gameObjectService.GetCurrentPlayerTargetName() ?? string.Empty,
|
||||
_ => charaName,
|
||||
};
|
||||
|
||||
if (!_profileManager.Profiles.Any())
|
||||
{
|
||||
_chatService.PrintInChat(
|
||||
$"Can't {(toggle ? "toggle" : "apply")} profile \"{profName}\" for character \"{charaName}\" because no profiles exist", ChatService.ChatMessageColor.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_profileManager.Profiles.Count(x => x.Name == profName && x.CharacterName == charaName) > 1)
|
||||
{
|
||||
_logger.Warning(
|
||||
$"Found more than one profile matching profile \"{profName}\" and character \"{charaName}\". Using first match.");
|
||||
}
|
||||
|
||||
var outProf = _profileManager.Profiles.FirstOrDefault(x => x.Name == profName && x.CharacterName == charaName);
|
||||
|
||||
if (outProf == null)
|
||||
{
|
||||
_chatService.PrintInChat(
|
||||
$"Can't {(toggle ? "toggle" : "apply")} profile \"{(string.IsNullOrWhiteSpace(profName) ? "empty (none provided)" : profName)}\" " +
|
||||
$"for Character \"{(string.IsNullOrWhiteSpace(charaName) ? "empty (none provided)" : charaName)}\"\n" +
|
||||
"Check if the profile and character names were provided correctly and said profile exists for chosen character", ChatService.ChatMessageColor.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!toggle)
|
||||
_profileManager.SetEnabled(outProf, true);
|
||||
else
|
||||
_profileManager.SetEnabled(outProf, !outProf.Enabled);
|
||||
|
||||
_chatService.PrintInChat(
|
||||
$"{outProf.Name} was successfully {(toggle ? "toggled" : "applied")} for {outProf.CharacterName}", ChatService.ChatMessageColor.Info);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_chatService.PrintInChat($"Error while {(toggle ? "toggling" : "applying")} profile, details are available in dalamud log", ChatService.ChatMessageColor.Error);
|
||||
_logger.Error($"Error {(toggle ? "toggling" : "applying")} profile by command: \n" +
|
||||
$"Profile name \"{(string.IsNullOrWhiteSpace(profName) ? "empty (none provided)" : profName)}\"\n" +
|
||||
$"Character name \"{(string.IsNullOrWhiteSpace(charaName) ? "empty (none provided)" : charaName)}\"\n" +
|
||||
$"Error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
93
CustomizePlus/Core/Services/DalamudServices.cs
Normal file
93
CustomizePlus/Core/Services/DalamudServices.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
public class DalamudServices
|
||||
{
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public DalamudPluginInterface PluginInterface { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public ISigScanner SigScanner { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public IFramework Framework { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IObjectTable ObjectTable { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public ICommandManager CommandManager { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IChatGui ChatGui { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IClientState ClientState { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IGameGui GameGui { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
internal IGameInteropProvider Hooker { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IKeyState KeyState { get; private set; } = null!;
|
||||
|
||||
//GameData
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IDataManager DataManager { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IPluginLog PluginLog { get; private set; } = null!;
|
||||
|
||||
/*[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public ICondition Condition { get; private set; } = null!;*/
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public ITargetManager TargetManager { get; private set; } = null!;
|
||||
|
||||
public DalamudServices(DalamudPluginInterface pluginInterface)
|
||||
{
|
||||
pluginInterface.Inject(this);
|
||||
}
|
||||
|
||||
public void AddServices(IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton(PluginInterface)
|
||||
.AddSingleton(SigScanner)
|
||||
.AddSingleton(Framework)
|
||||
.AddSingleton(ObjectTable)
|
||||
.AddSingleton(CommandManager)
|
||||
.AddSingleton(ChatGui)
|
||||
.AddSingleton(ClientState)
|
||||
.AddSingleton(GameGui)
|
||||
.AddSingleton(Hooker)
|
||||
.AddSingleton(KeyState)
|
||||
.AddSingleton(this)
|
||||
.AddSingleton(PluginInterface.UiBuilder)
|
||||
.AddSingleton(DataManager)
|
||||
.AddSingleton(PluginLog)
|
||||
//.AddSingleton(Condition)
|
||||
.AddSingleton(TargetManager);
|
||||
}
|
||||
}
|
||||
77
CustomizePlus/Core/Services/FantasiaPlusDetectService.cs
Normal file
77
CustomizePlus/Core/Services/FantasiaPlusDetectService.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using CustomizePlus.UI.Windows;
|
||||
using Dalamud.Plugin;
|
||||
using OtterGui.Log;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Timers;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Detects is Fantasia+ is installed and shows a message if it is. The check is performed every 15 seconds.
|
||||
/// </summary>
|
||||
public class FantasiaPlusDetectService : IDisposable
|
||||
{
|
||||
private readonly DalamudPluginInterface _pluginInterface;
|
||||
private readonly PopupSystem _popupSystem;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private Timer? _checkTimer = null;
|
||||
|
||||
/// <summary>
|
||||
/// Note: if this is set to true then this is locked until the plugin or game is restarted
|
||||
/// </summary>
|
||||
public bool IsFantasiaPlusInstalled { get; private set; }
|
||||
|
||||
public FantasiaPlusDetectService(DalamudPluginInterface pluginInterface, PopupSystem popupSystem, Logger logger)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
_popupSystem = popupSystem;
|
||||
_logger = logger;
|
||||
|
||||
_popupSystem.RegisterPopup("fantasia_detected_warn", "Customize+ detected that you have Fantasia+ installed.\nPlease delete or turn it off and restart your game to use Customize+.");
|
||||
|
||||
if (CheckFantasiaPlusPresence())
|
||||
{
|
||||
_popupSystem.ShowPopup("fantasia_detected_warn");
|
||||
_logger.Error("Fantasia+ detected during startup, plugin will be locked");
|
||||
}
|
||||
else
|
||||
{
|
||||
_checkTimer = new Timer(15 * 1000);
|
||||
_checkTimer.Elapsed += CheckTimerOnElapsed;
|
||||
_checkTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if Fantasia+ is installed and loaded
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool CheckFantasiaPlusPresence()
|
||||
{
|
||||
if (IsFantasiaPlusInstalled)
|
||||
return true;
|
||||
|
||||
IsFantasiaPlusInstalled = _pluginInterface.InstalledPlugins.Any(pluginInfo => pluginInfo is { InternalName: "FantasiaPlus", IsLoaded: true });
|
||||
|
||||
return IsFantasiaPlusInstalled;
|
||||
}
|
||||
|
||||
private void CheckTimerOnElapsed(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
if (CheckFantasiaPlusPresence())
|
||||
{
|
||||
_popupSystem.ShowPopup("fantasia_detected_warn");
|
||||
_checkTimer!.Stop();
|
||||
_checkTimer?.Dispose();
|
||||
_logger.Error("Fantasia+ detected by timer, plugin will be locked");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_checkTimer != null)
|
||||
_checkTimer.Dispose();
|
||||
}
|
||||
}
|
||||
57
CustomizePlus/Core/Services/FilenameService.cs
Normal file
57
CustomizePlus/Core/Services/FilenameService.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Dalamud.Plugin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
public class FilenameService
|
||||
{
|
||||
public readonly string ConfigDirectory;
|
||||
public readonly string ConfigFile;
|
||||
public readonly string ProfileDirectory;
|
||||
public readonly string ProfileFileSystem;
|
||||
public readonly string TemplateDirectory;
|
||||
public readonly string TemplateFileSystem;
|
||||
|
||||
public FilenameService(DalamudPluginInterface pi)
|
||||
{
|
||||
ConfigDirectory = pi.ConfigDirectory.FullName;
|
||||
ConfigFile = pi.ConfigFile.FullName;
|
||||
ProfileDirectory = Path.Combine(ConfigDirectory, "profiles");
|
||||
ProfileFileSystem = Path.Combine(ConfigDirectory, "profile_sort_order.json");
|
||||
TemplateDirectory = Path.Combine(ConfigDirectory, "templates");
|
||||
TemplateFileSystem = Path.Combine(ConfigDirectory, "template_sort_order.json");
|
||||
}
|
||||
|
||||
public IEnumerable<FileInfo> Templates()
|
||||
{
|
||||
if (!Directory.Exists(TemplateDirectory))
|
||||
yield break;
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(TemplateDirectory, "*.json", SearchOption.TopDirectoryOnly))
|
||||
yield return new FileInfo(file);
|
||||
}
|
||||
|
||||
public string TemplateFile(Guid id)
|
||||
=> Path.Combine(TemplateDirectory, $"{id}.json");
|
||||
|
||||
public string TemplateFile(Templates.Data.Template template)
|
||||
=> TemplateFile(template.UniqueId);
|
||||
|
||||
public IEnumerable<FileInfo> Profiles()
|
||||
{
|
||||
if (!Directory.Exists(ProfileDirectory))
|
||||
yield break;
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(ProfileDirectory, "*.json", SearchOption.TopDirectoryOnly))
|
||||
yield return new FileInfo(file);
|
||||
}
|
||||
|
||||
public string ProfileFile(Guid id)
|
||||
=> Path.Combine(ProfileDirectory, $"{id}.json");
|
||||
|
||||
public string ProfileFile(Profile profile)
|
||||
=> ProfileFile(profile.UniqueId);
|
||||
}
|
||||
156
CustomizePlus/Core/Services/HookingService.cs
Normal file
156
CustomizePlus/Core/Services/HookingService.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using OtterGui.Log;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Armatures.Services;
|
||||
using CustomizePlus.GameData.Data;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
public class HookingService : IDisposable
|
||||
{
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly ISigScanner _sigScanner;
|
||||
private readonly IGameInteropProvider _hooker;
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly ArmatureManager _armatureManager;
|
||||
private readonly GameStateService _gameStateService;
|
||||
private readonly FantasiaPlusDetectService _fantasiaPlusDetectService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private Hook<RenderDelegate>? _renderManagerHook;
|
||||
private Hook<GameObjectMovementDelegate>? _gameObjectMovementHook;
|
||||
|
||||
private delegate nint RenderDelegate(nint a1, nint a2, int a3, int a4);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate void GameObjectMovementDelegate(nint gameObject);
|
||||
|
||||
public HookingService(
|
||||
PluginConfiguration configuration,
|
||||
ISigScanner sigScanner,
|
||||
IGameInteropProvider hooker,
|
||||
ProfileManager profileManager,
|
||||
ArmatureManager armatureManager,
|
||||
GameStateService gameStateService,
|
||||
FantasiaPlusDetectService fantasiaPlusDetectService,
|
||||
Logger logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_sigScanner = sigScanner;
|
||||
_hooker = hooker;
|
||||
_profileManager = profileManager;
|
||||
_armatureManager = armatureManager;
|
||||
_gameStateService = gameStateService;
|
||||
_fantasiaPlusDetectService = fantasiaPlusDetectService;
|
||||
_logger = logger;
|
||||
|
||||
ReloadHooks();
|
||||
}
|
||||
|
||||
public void ReloadHooks()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_configuration.PluginEnabled)
|
||||
{
|
||||
if (_renderManagerHook == null)
|
||||
{
|
||||
var renderAddress = _sigScanner.ScanText(Constants.RenderHookAddress);
|
||||
_renderManagerHook = _hooker.HookFromAddress<RenderDelegate>(renderAddress, OnRender);
|
||||
_logger.Debug("Render hook established");
|
||||
}
|
||||
|
||||
if (_gameObjectMovementHook == null)
|
||||
{
|
||||
var movementAddress = _sigScanner.ScanText(Constants.MovementHookAddress);
|
||||
_gameObjectMovementHook = _hooker.HookFromAddress<GameObjectMovementDelegate>(movementAddress, OnGameObjectMove);
|
||||
_logger.Debug("Movement hook established");
|
||||
}
|
||||
|
||||
_logger.Debug("Hooking render & movement functions");
|
||||
_renderManagerHook.Enable();
|
||||
_gameObjectMovementHook.Enable();
|
||||
|
||||
_logger.Debug("Hooking render manager");
|
||||
_renderManagerHook.Enable();
|
||||
|
||||
//Send current player's profile update message to IPC
|
||||
//IPCManager.OnProfileUpdate(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Unhooking...");
|
||||
_renderManagerHook?.Disable();
|
||||
_gameObjectMovementHook?.Disable();
|
||||
_renderManagerHook?.Disable();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error($"Failed to hook Render::Manager::Render {e}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private nint OnRender(nint a1, nint a2, int a3, int a4)
|
||||
{
|
||||
if (_renderManagerHook == null)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
if (_fantasiaPlusDetectService.IsFantasiaPlusInstalled)
|
||||
{
|
||||
_logger.Error($"Fantasia+ detected, disabling all hooks");
|
||||
_renderManagerHook?.Disable();
|
||||
_gameObjectMovementHook.Disable();
|
||||
|
||||
return _renderManagerHook.Original(a1, a2, a3, a4);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_armatureManager.OnRender();
|
||||
_profileManager.OnRender();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error($"Error in Customize+ render hook {e}");
|
||||
_renderManagerHook?.Disable();
|
||||
}
|
||||
|
||||
return _renderManagerHook.Original(a1, a2, a3, a4);
|
||||
}
|
||||
|
||||
private unsafe void OnGameObjectMove(nint gameObjectPtr)
|
||||
{
|
||||
// Call the original function.
|
||||
_gameObjectMovementHook.Original(gameObjectPtr);
|
||||
|
||||
// If GPose and a 3rd-party posing service are active simultneously, abort
|
||||
if (_gameStateService.GameInPosingMode())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var actor = (Actor)gameObjectPtr;
|
||||
if (actor.Valid)
|
||||
_armatureManager.OnGameObjectMove((Actor)gameObjectPtr);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gameObjectMovementHook?.Disable();
|
||||
_gameObjectMovementHook?.Dispose();
|
||||
|
||||
_renderManagerHook?.Disable();
|
||||
_renderManagerHook?.Dispose();
|
||||
}
|
||||
}
|
||||
17
CustomizePlus/Core/Services/SaveService.cs
Normal file
17
CustomizePlus/Core/Services/SaveService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Any file type that we want to save via SaveService.
|
||||
/// </summary>
|
||||
public interface ISavable : ISavable<FilenameService>
|
||||
{ }
|
||||
|
||||
public sealed class SaveService : SaveServiceBase<FilenameService>
|
||||
{
|
||||
public SaveService(Logger logger, FrameworkManager framework, FilenameService fileNames)
|
||||
: base(logger, framework, fileNames)
|
||||
{ }
|
||||
}
|
||||
Reference in New Issue
Block a user