Code commit

This commit is contained in:
RisaDev
2024-01-06 01:21:41 +03:00
parent a7d7297c59
commit a486dd2c96
90 changed files with 11576 additions and 0 deletions

View 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
}

View 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;
}
}

View 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";
}

View 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
}
}

View 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]}...";
}
}
}

View 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()
};
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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}");
}
}
}

View 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);
}
}

View 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();
}
}

View 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);
}

View 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();
}
}

View 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)
{ }
}