示例#1
0
 public void Update()
 {
     // Keypress handlers
     if (Input.GetKeyDown(KeyCode.F10))
     {
         try
         {
             TextTranslator.Reload();
             Logger.Log(LogLevel.Info, "Reloaded translations");
         }
         catch (TranslatorException)
         {
             Logger.Log(LogLevel.Error, "Failed to reload translations");
         }
     }
     else if (Input.GetKeyDown(KeyCode.F9))
     {
         TextTranslator.DumpMissingTranslations();
         Logger.Log(LogLevel.Info, "Dumped missing translations");
     }
 }
示例#2
0
        public static bool LoadAssetAsyncPreHook(ref AssetBundleLoadAssetOperation __result, string assetBundleName, string assetName, Type type, string manifestAssetBundleName)
        {
            __result = ResourceRedirector.HandleAsset(assetBundleName, assetName, type, manifestAssetBundleName, ref __result);

            if (__result == null)
            {
                if (!File.Exists($"{Application.dataPath}/../abdata/{assetBundleName}"))
                {
                    //An asset that does not exist is being requested from from an asset bundle that does not exist
                    //Redirect to an asset bundle the does exist so that the game does not attempt to open a non-existant file and cause errors
                    Logger.Log(LogLevel.Warning, $"Asset {assetName} does not exist in asset bundle {assetBundleName}.");
                    assetBundleName = "chara/mt_ramp_00.unity3d";
                    assetName       = "dummy";
                }
                return(true);
            }
            else
            {
                return(false);
            }
        }
        /// <summary>
        /// Called by GUI button click. Unlinks the selected node.
        /// </summary>
        private void UnlinkCharacterToObject()
        {
            TreeNodeObject[] selectNodes = Singleton <Studio.Studio> .Instance.treeNodeCtrl.selectNodes;
            bool             DidUnlink   = false;

            for (int i = 0; i < selectNodes.Length; i++)
            {
                if (Singleton <Studio.Studio> .Instance.dicInfo.TryGetValue(selectNodes[i], out ObjectCtrlInfo objectCtrlInfo))
                {
                    if (objectCtrlInfo is OCIChar ociChar)
                    {
                        GetController(ociChar).RemoveLink(IKGuideObjects[SelectedGuideObject]);
                        DidUnlink = true;
                    }
                }
            }
            if (!DidUnlink)
            {
                Logger.Log(LogLevel.Info | LogLevel.Message, "Select a character.");
            }
        }
示例#4
0
        public static bool HandleAsset(string assetBundleName, string assetName, Type type, string manifestAssetBundleName, out AssetBundleLoadAssetOperation result)
        {
            if (assetName.StartsWith("bgm") && assetName.Length > 4)
            {
                int bgmTrack = int.Parse(assetName.Remove(0, 4));

                var path = Utility.CombinePaths(Utility.PluginsDirectory, "bgm", $"BGM{bgmTrack:00}.ogg");

                if (File.Exists(path))
                {
                    Logger.Log(LogLevel.Info, $"Loading BGM track \"{(BGM)bgmTrack}\" from {path}");

                    result = new AssetBundleLoadAssetOperationSimulation(AudioLoader.LoadVorbis(path));

                    return(true);
                }
            }

            result = null;
            return(false);
        }
示例#5
0
        private void OnEarlyMakerFinishedLoading(object sender, RegisterCustomControlsEvent e)
        {
            _boneController = FindObjectOfType <BoneController>();
            if (_boneController == null)
            {
                Logger.Log(LogLevel.Error, "[KKABMX_GUI] Failed to find a BoneController or there are no bone modifiers");
                return;
            }

            _boneController.NewDataLoaded += (s, args) =>
            {
                foreach (var action in _updateActionList)
                {
                    action();
                }
            };

            gameObject.AddComponent <KKABMX_AdvancedGUI>().enabled = false;

            RegisterCustomControls(e);
        }
示例#6
0
        protected void LoadTranslations()
        {
            string translationDir = Path.Combine(Paths.PluginPath, "translation");

            if (!Directory.Exists(translationDir))
            {
                Logger.Log(LogLevel.Debug, "Creating translation directory");
                Directory.CreateDirectory(translationDir);
            }
            else
            {
                try
                {
                    TextTranslator.Initialize(translationDir);
                }
                catch (TranslatorException)
                {
                    Logger.Log(LogLevel.Error, "Unable to initialize translator");
                }
            }
        }
示例#7
0
        private IEnumerator StudioLoadCoroutine(List <KeyValuePair <string, PngType> > scenes, List <KeyValuePair <string, PngType> > cards)
        {
            if (scenes.Count > 0)
            {
                if (scenes.Count > 1)
                {
                    Logger.Log(LogLevel.Message, "Warning: Only the first scene will be loaded.");
                }

                var scene = scenes[0];

                try
                {
                    LoadScene(scene.Key);
                }
                catch (Exception ex)
                {
                    PrintError(ex);
                }

                yield return(new WaitForEndOfFrame());
            }

            foreach (var card in cards)
            {
                try
                {
                    LoadSceneCharacter(card.Key);
                }
                catch (Exception ex)
                {
                    PrintError(ex);
                }

                yield return(new WaitForEndOfFrame());
            }

            PlaySound(SystemSE.ok_s);
        }
示例#8
0
        void ReloadPlugins()
        {
            Destroy(scriptManager);
            scriptManager = new GameObject($"ScriptEngine_{DateTime.Now.Ticks}");
            DontDestroyOnLoad(scriptManager);

            var files = Directory.GetFiles(ScriptDirectory, "*.dll");

            if (files.Length > 0)
            {
                foreach (string path in Directory.GetFiles(ScriptDirectory, "*.dll"))
                {
                    LoadDLL(path, scriptManager);
                }

                Logger.Log(LogLevel.Message, "Reloaded script plugins!");
            }
            else
            {
                Logger.Log(LogLevel.Message, "No plugins to reload");
            }
        }
示例#9
0
        private static void UntranslateTextAll()
        {
            TextHooks.TranslationHooksEnabled = false;

            var aliveCount = 0;

            foreach (var kv in OriginalTranslations)
            {
                if (!kv.Key.IsAlive)
                {
                    continue;
                }

                aliveCount++;

                try
                {
                    switch (kv.Key.Target)
                    {
                    case TMP_Text tmtext:
                        tmtext.text = kv.Value;
                        break;

                    case UnityEngine.UI.Text tmtext:
                        tmtext.text = kv.Value;
                        break;
                    }
                }
                catch
                {
                    // Bloody APIs crashing up my house
                }
            }

            Logger.Log(LogLevel.Message, $"{aliveCount} translations reloaded.");

            TextHooks.TranslationHooksEnabled = true;
        }
示例#10
0
        private void Awake()
        {
            ReplacingEnabled = new ConfigWrapper <bool>("ReplacingEnabled", this, true);
            DumpingEnabled   = new ConfigWrapper <bool>("DumpingEnabled", this, true);

            _assetsPath = Path.Combine(Paths.PluginPath, "Assets");

            if (!Directory.Exists(_assetsPath))
            {
                try
                {
                    Logger.Log(LogLevel.Info, "No Assets directory, creating...");
                    Directory.CreateDirectory(_assetsPath);
                }
                catch (Exception)
                {
                    Logger.Log(LogLevel.Error, "Error creating Assets directory " + _assetsPath);
                    throw;
                }
            }

            Load();
        }
        void UntranslateAll()
        {
            Hooks.TranslationHooksEnabled = false;

            int i = 0;

            foreach (var kv in originalTranslations)
            {
                if (kv.Key.IsAlive)
                {
                    i++;

                    if (kv.Key.Target is TMP_Text)
                    {
                        TMP_Text tmtext = (TMP_Text)kv.Key.Target;

                        tmtext.text = kv.Value;
                    }
                    else if (kv.Key.Target is TextMeshProUGUI)
                    {
                        TextMeshProUGUI tmtext = (TextMeshProUGUI)kv.Key.Target;

                        tmtext.text = kv.Value;
                    }
                    else if (kv.Key.Target is UnityEngine.UI.Text)
                    {
                        UnityEngine.UI.Text tmtext = (UnityEngine.UI.Text)kv.Key.Target;

                        tmtext.text = kv.Value;
                    }
                }
            }

            Logger.Log(LogLevel.Message, $"{i} translations reloaded.");

            Hooks.TranslationHooksEnabled = true;
        }
        private static IEnumerable <ReadonlyCacheEntry> GetInstanceClassScanner()
        {
            Logger.Log(LogLevel.Debug, "[CheatTools] Looking for class instances...");

            var query = AppDomain.CurrentDomain.GetAssemblies()
                        .SelectMany(x =>
            {
                try
                {
                    return(x.GetTypes());
                }
                catch (SystemException)
                {
                    return(Enumerable.Empty <Type>());
                }
            })
                        .Where(t => t.IsClass && !t.IsAbstract && !t.ContainsGenericParameters);

            foreach (var type in query)
            {
                object obj = null;
                try
                {
                    obj = type.GetProperty("Instance",
                                           BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
                          ?.GetValue(null, null);
                }
                catch (Exception ex)
                {
                    Logger.Log(LogLevel.Debug, ex.ToString());
                }
                if (obj != null)
                {
                    yield return(new ReadonlyCacheEntry(type.Name + ".Instance", obj));
                }
            }
        }
示例#13
0
        protected void LoadAllLists(ZipFile arc, Manifest manifest)
        {
            foreach (ZipEntry entry in arc)
            {
                if (entry.Name.StartsWith("abdata/list/characustom", StringComparison.OrdinalIgnoreCase) && entry.Name.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
                {
                    try
                    {
                        var stream = arc.GetInputStream(entry);

                        var chaListData = ListLoader.LoadCSV(stream);

                        SetPossessNew(chaListData);
                        UniversalAutoResolver.GenerateResolutionInfo(manifest, chaListData);
                        IndexList(manifest, chaListData);

                        ListLoader.ExternalDataList.Add(chaListData);

                        if (LoadedData.TryGetValue(manifest, out lists))
                        {
                            lists.Add(chaListData);
                        }
                        else
                        {
                            LoadedData[manifest] = new List <ChaListData> {
                                chaListData
                            };
                        }
                    }
                    catch (SystemException ex)
                    {
                        Logger.Log(LogLevel.Error, $"[SIDELOADER] Failed to load list file \"{entry.Name}\" from archive \"{arc.Name}\" with error: {ex.Message}");
                        Logger.Log(LogLevel.Debug, $"[SIDELOADER] Error details: {ex}");
                    }
                }
            }
        }
        private void Update()
        {
            if (_bytesToLoad != null)
            {
                try
                {
                    var tex = Util.TextureFromBytes(_bytesToLoad);

                    var origTex = GetOverlayController().GetApplicableRenderers(_typeToLoad).First().material.mainTexture;

                    if (tex.width != origTex.width || tex.height != origTex.height)
                    {
                        Logger.Log(LogLevel.Message | LogLevel.Warning, $"[KCOX] WARNING - Wrong texture resolution! It's recommended to use {origTex.width}x{origTex.height} instead.");
                    }
                    else
                    {
                        Logger.Log(LogLevel.Message, "[KCOX] Texture imported successfully");
                    }

                    SetTexAndUpdate(new ClothesTexData {
                        Override = _hideMainToLoad, Texture = tex
                    }, _typeToLoad);
                }
                catch (Exception ex)
                {
                    _lastError = ex;
                }
                _bytesToLoad = null;
            }

            if (_lastError != null)
            {
                Logger.Log(LogLevel.Error | LogLevel.Message, "[KCOX] Failed to load texture from file - " + _lastError.Message);
                Logger.Log(LogLevel.Debug, _lastError);
                _lastError = null;
            }
        }
示例#15
0
        private void ReplaceImages()
        {
            UnityEngine.Object[] textures = Resources.FindObjectsOfTypeAll(typeof(Texture2D));

            foreach (UnityEngine.Object t in textures)
            {
                try
                {
                    Texture2D tex = (Texture2D)t;
                    if (images.ContainsKey(t.name))
                    {
                        Logger.Log(LogLevel.Debug, "Replacing Image: " + tex.name);
                        byte[] fileData = File.ReadAllBytes(images[t.name]);
                        tex.LoadImage(fileData);
                    }
                }
                catch (Exception ex)
                {
                    Logger.Log(LogLevel.Error, ex.ToString());
                }
            }

            Resources.UnloadUnusedAssets();
        }
示例#16
0
 private void Update()
 {
     if ((Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt)) && Input.GetKey(TranslationSyncHotkey.Value.MainKey))
     {
         SyncTLs(TLType.Scenario, true);
         SyncTLs(TLType.Communication, true);
         SyncTLs(TLType.H, true);
         Logger.Log(LogLevel.Info, "Sync complete.");
     }
     else if (TranslationSyncHotkey.IsDown())
     {
         //CountText();
         SyncTLs(TLType.Scenario);
         SyncTLs(TLType.Communication);
         SyncTLs(TLType.H);
         Logger.Log(LogLevel.Info, "Sync complete.");
     }
     if ((Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) && Input.GetKey(TranslationSyncHotkey.Value.MainKey))
     {
         for (int i = 0; i <= 37; i++)
         {
             Personality.Value = "c" + i.ToString("00");
             SyncTLs(TLType.Scenario);
             SyncTLs(TLType.Communication);
             SyncTLs(TLType.H);
         }
         for (int i = 0; i <= 10; i++)
         {
             Personality.Value = "c-" + i.ToString("00");
             SyncTLs(TLType.Scenario);
             SyncTLs(TLType.Communication);
             SyncTLs(TLType.H);
         }
         Logger.Log(LogLevel.Info, "Sync complete.");
     }
 }
示例#17
0
        public static void LoadTextTranslations(string dirTranslation)
        {
            Logger.Log(LogLevel.Debug, "Loading all translations");
            //TODO: load .bin files here

            Logger.Log(LogLevel.Debug, $"Loaded {TLArchives.Count} archives");
            TLArchives.Clear();
            var dirTranslationText = Path.Combine(dirTranslation, "Text");

            if (!Directory.Exists(dirTranslationText))
            {
                Directory.CreateDirectory(dirTranslationText);
            }
            try
            {
                TLArchives.Add(new MarkupCompiler().CompileArchive(dirTranslationText));
                Logger.Log(LogLevel.Debug, $"Loaded {TLArchives.Last().Sections.Sum(x => x.Lines.Count)} lines from text");
            }
            catch (Exception ex)
            {
                Logger.Log(LogLevel.Error | LogLevel.Message, "Unable to load translations from text!");
                Logger.Log(LogLevel.Error, ex);
            }
        }
示例#18
0
        private static void LoadTranslationsFromFolder(string FolderPath)
        {
            Logger.Log(LogLevel.Debug, $"Loading translations from {FolderPath}");
            FolderTranslations.Clear();
            regexFolderTranslations.Clear();

            if (Directory.Exists(FolderPath))
            {
                try
                {
                    var arc = new MarkupCompiler().CompileArchive(FolderPath);

                    foreach (Section section in arc.Sections)
                    {
                        foreach (var line in section.Lines)
                        {
                            if (line.Flags.IsOriginalRegex)
                            {
                                regexFolderTranslations[new Regex(line.OriginalLine)] = line;
                            }
                            else
                            {
                                FolderTranslations[line.OriginalLine] = line;
                            }
                        }
                    }

                    Logger.Log(LogLevel.Debug, $"Loaded {FolderTranslations.Count} lines from text");
                }
                catch (Exception ex)
                {
                    Logger.Log(LogLevel.Error | LogLevel.Message, "Unable to load translations from text!");
                    Logger.Log(LogLevel.Error, ex);
                }
            }
        }
示例#19
0
        public Sideloader()
        {
            //ilmerge
            AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
            {
                if (args.Name == "I18N, Version=2.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756" ||
                    args.Name == "I18N.West, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")
                {
                    return(Assembly.GetExecutingAssembly());
                }

                return(null);
            };

            //install hooks
            Hooks.InstallHooks();
            AutoResolver.Hooks.InstallHooks();
            ResourceRedirector.ResourceRedirector.AssetResolvers.Add(RedirectHook);
            ResourceRedirector.ResourceRedirector.AssetBundleResolvers.Add(AssetBundleRedirectHook);

            MissingModWarning       = new ConfigWrapper <bool>("MissingModWarning", this, true);
            DebugLogging            = new ConfigWrapper <bool>("DebugLogging", this, false);
            DebugResolveInfoLogging = new ConfigWrapper <bool>("DebugResolveInfoLogging", this, false);
            KeepMissingAccessories  = new ConfigWrapper <bool>("KeepMissingAccessories", this, false);

            //check mods directory
            var modDirectory = Path.Combine(Paths.GameRootPath, "mods");

            if (!Directory.Exists(modDirectory))
            {
                Logger.Log(LogLevel.Warning, "[SIDELOADER] Could not find the \"mods\" directory");
                return;
            }

            LoadModsFromDirectory(modDirectory);
        }
示例#20
0
        private void GetImages()
        {
            if (!Directory.Exists(ImagesPath))
            {
                return;
            }

            try
            {
                Logger.Log(LogLevel.Debug, "Fetching Images...");

                foreach (string file in Directory.GetFiles(ImagesPath))
                {
                    if (Path.GetExtension(file).Equals(".png", StringComparison.OrdinalIgnoreCase))
                    {
                        images.Add(Path.GetFileNameWithoutExtension(file), file);
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Log(LogLevel.Error, ex.ToString());
            }
        }
        private void Start()
        {
            if (KoikatuAPI.CheckIncompatiblePlugin(this, "koikatsu.cartoonuncensor", LogLevel.Error))
            {
                Logger.Log(LogLevel.Error | LogLevel.Message, "CartoonUncensor.dll is incompatible with KK_UncensorSelector! Please remove it and restart the game.");
                return;
            }
            if (KoikatuAPI.CheckIncompatiblePlugin(this, "koikatsu.alexaebubblegum", LogLevel.Error))
            {
                Logger.Log(LogLevel.Error | LogLevel.Message, "AlexaeBubbleGum.dll is incompatible with KK_UncensorSelector! Please remove it and restart the game.");
                return;
            }

            var harmony = HarmonyInstance.Create(GUID);

            harmony.PatchAll(typeof(Hooks));

            Type       loadAsyncIterator         = typeof(ChaControl).GetNestedTypes(AccessTools.all).First(x => x.Name.StartsWith("<LoadAsync>c__Iterator"));
            MethodInfo loadAsyncIteratorMoveNext = loadAsyncIterator.GetMethod("MoveNext");

            harmony.Patch(loadAsyncIteratorMoveNext, null, null, new HarmonyMethod(typeof(Hooks).GetMethod(nameof(Hooks.LoadAsyncTranspiler), BindingFlags.Static | BindingFlags.Public)));

            PopulateUncensorLists();

            MakerAPI.RegisterCustomSubCategories += MakerAPI_RegisterCustomSubCategories;
            MakerAPI.MakerFinishedLoading        += MakerAPI_MakerFinishedLoading;
            CharacterApi.RegisterExtraBehaviour <UncensorSelectorController>(GUID);

            GenderBender       = new ConfigWrapper <bool>(nameof(GenderBender), PluginNameInternal, true);
            DefaultMaleBody    = new ConfigWrapper <string>(nameof(DefaultMaleBody), PluginNameInternal, BodyGuidToDisplayName, DisplayNameToBodyGuid, MaleBodyDefaultValue);
            DefaultMalePenis   = new ConfigWrapper <string>(nameof(DefaultMalePenis), PluginNameInternal, MalePenisGuidToDisplayName, DisplayNameToPenisGuid, MalePenisDefaultValue);
            DefaultMaleBalls   = new ConfigWrapper <string>(nameof(DefaultMaleBalls), PluginNameInternal, MaleBallsGuidToDisplayName, DisplayNameToBallsGuid, MaleBallsDefaultValue);
            DefaultFemaleBody  = new ConfigWrapper <string>(nameof(DefaultFemaleBody), PluginNameInternal, BodyGuidToDisplayName, DisplayNameToBodyGuid, FemaleBodyDefaultValue);
            DefaultFemalePenis = new ConfigWrapper <string>(nameof(DefaultFemalePenis), PluginNameInternal, FemalePenisGuidToDisplayName, DisplayNameToPenisGuid, FemalePenisDefaultValue);
            DefaultFemaleBalls = new ConfigWrapper <string>(nameof(DefaultFemaleBalls), PluginNameInternal, FemaleBallsGuidToDisplayName, DisplayNameToBallsGuid, FemaleBallsDefaultValue);
        }
示例#22
0
        public IEnumerator DownloadSubs()
        {
            string cache = Path.Combine(Paths.PluginPath, "hsubs.msgpack");

            if (File.Exists(cache))
            {
                subtitlesDict = LZ4MessagePackSerializer.Deserialize <Dictionary <string, string> >(File.ReadAllBytes(cache));
                Logger.Log(LogLevel.Info, "Found cached hsubs");
            }

            Logger.Log(LogLevel.Info, "Downloading subs from " + SHEET_KEY);
            var dl = new WWW($"https://docs.google.com/spreadsheets/d/{SHEET_KEY}/export?format=csv");

            while (!dl.isDone)
            {
                yield return(dl);
            }

            if (dl.error != null)
            {
                Logger.Log(LogLevel.Warning, "Failed to fetch latest subtitles. Going to use cached ones.");
                yield break;
            }

            Logger.Log(LogLevel.Info, $"Downloaded {dl.bytesDownloaded} bytes. Parsing...");
            int cnt = 0;

            foreach (IEnumerable <string> row in ParseCSV(dl.text))
            {
                int    idx   = 0;
                string sound = null;
                string tl    = null;
                foreach (string cell in row)
                {
                    if (idx == 0)
                    {
                        sound = cell.ToLower();
                    }
                    if (idx == 2)
                    {
                        tl = cell;
                    }
                    idx++;
                }

                if (sound != null && tl != null && sound.Length < 64)
                {
                    cnt++;
                    subtitlesDict[sound] = tl;
                }
            }

            Logger.Log(LogLevel.Info, $"Done parsing subtitles: {cnt} lines found.");
            if (cnt > 60000)
            {
                File.WriteAllBytes(cache, LZ4MessagePackSerializer.Serialize(subtitlesDict));
            }
            else
            {
                Logger.Log(LogLevel.Warning, "The amount of lines is suspiciously low (defaced sheet?); not caching.");
            }
        }
示例#23
0
            /// <summary>
            /// Called by the scene controller, loads animations from the loaded or imported scene
            /// </summary>
            internal void LoadAnimations(int characterDicKey, ReadOnlyDictionary <int, ObjectCtrlInfo> loadedItems)
            {
                try
                {
                    PluginData ExtendedData = ExtendedSave.GetSceneExtendedDataById(PluginNameInternal);

                    //Version 1 save data
                    if (ExtendedData?.data != null && ExtendedData.data.ContainsKey("AnimationInfo"))
                    {
                        List <AnimationControllerInfo> AnimationControllerInfoList = ((object[])ExtendedData.data["AnimationInfo"]).Select(x => AnimationControllerInfo.Unserialize((byte[])x)).ToList();

                        foreach (var AnimInfo in AnimationControllerInfoList)
                        {
                            //See if this is the right character
                            if (AnimInfo.CharDicKey != characterDicKey)
                            {
                                continue;
                            }

                            ObjectCtrlInfo linkedItem = loadedItems[AnimInfo.ItemDicKey];

                            if (AnimInfo.Version.IsNullOrEmpty())
                            {
                                AddLinkV1(AnimInfo.IKPart, linkedItem);
                            }
                            else
                            {
                                AddLink(AnimInfo.IKPart, linkedItem);
                            }
                        }
                    }
                    //Version 2 save data
                    else
                    {
                        var data = GetExtendedData();
                        if (data?.data != null)
                        {
                            if (data.data.TryGetValue("LinksV1", out var loadedLinksV1) && loadedLinksV1 != null)
                            {
                                foreach (var link in (Dictionary <object, object>)loadedLinksV1)
                                {
                                    AddLinkV1((string)link.Key, loadedItems[(int)link.Value]);
                                }
                            }

                            if (data.data.TryGetValue("Links", out var loadedLinks) && loadedLinks != null)
                            {
                                foreach (var link in (Dictionary <object, object>)loadedLinks)
                                {
                                    AddLink((string)link.Key, loadedItems[(int)link.Value]);
                                }
                            }

                            if (data.data.TryGetValue("Eyes", out var loadedEyeLink) && loadedEyeLink != null)
                            {
                                AddEyeLink(loadedItems[(int)loadedEyeLink]);
                            }

                            if (data.data.TryGetValue("Neck", out var loadedNeckLink) && loadedNeckLink != null)
                            {
                                AddNeckLink(loadedItems[(int)loadedNeckLink]);
                            }
                        }
                    }
                    Logger.Log(LogLevel.Debug, $"Loaded KK_AnimationController animations for character {ChaControl.chaFile.parameter.fullname.Trim()}");
                }
                catch (Exception ex)
                {
                    Logger.Log(LogLevel.Error | LogLevel.Message, "Could not load KK_AnimationController animations.");
                    Logger.Log(LogLevel.Error, ex.ToString());
                }
            }
示例#24
0
        void Start()
        {
            string managedDir = Path.Combine(Utility.ExecutingDirectory,
                                             $@"{System.Diagnostics.Process.GetCurrentProcess().ProcessName}_Data\Managed");

            if (File.Exists(Path.Combine(managedDir, "IllusionPlugin.dll")))
            {
                Logger.Log(LogLevel.Error | LogLevel.Message, "IPA has been detected to be installed! IPALoader may not function correctly!");
            }


            if (!Directory.Exists(IPAPluginDir))
            {
                Logger.Log(LogLevel.Message, "No IPA plugin directory, skipping load");
                return;
            }

            if (IPAManagerObject != null)
            {
                Destroy(IPAManagerObject);
            }

            IPAManagerObject = new GameObject("IPA_Manager");

            DontDestroyOnLoad(IPAManagerObject);
            IPAManagerObject.SetActive(false);

            Logger.Log(LogLevel.Info, "Loading IPA plugins");

            foreach (string path in Directory.GetFiles(IPAPluginDir, "*.dll"))
            {
                try
                {
                    var assembly = Assembly.LoadFile(path);

                    foreach (Type t in assembly.GetTypes())
                    {
                        if (t.IsAbstract || t.IsInterface)
                        {
                            continue;
                        }

                        if (t.Name == "CompositePlugin")
                        {
                            continue;
                        }

                        if (typeof(IPlugin).IsAssignableFrom(t))
                        {
                            pluginToLoad = (IPlugin)Activator.CreateInstance(t);

                            Component c = Chainloader.ManagerObject.AddComponent <IPAPlugin>();

                            Logger.Log(LogLevel.Info, $"Loaded IPA plugin [{pluginToLoad.Name}]");

                            pluginToLoad = null;
                        }
                    }
                }
                catch (Exception ex)
                {
                    Logger.Log(LogLevel.Error | LogLevel.Message, $"Error loading IPA plugin {Path.GetFileName(path)}");
                    Logger.Log(LogLevel.Error, ex.ToString());
                }
            }

            IPAManagerObject.SetActive(true);
        }
示例#25
0
        /// <summary>
        /// Look through all the GUIDs, compare it to the MigrationInfoList, and do migration when necessary
        /// </summary>
        private static IEnumerable <ResolveInfo> MigrateGUID(IEnumerable <ResolveInfo> extInfo, string characterName)
        {
            List <ResolveInfo> extInfoNew = new List <ResolveInfo>();
            bool DidBlankGUIDMessage      = false;

            try
            {
                if (extInfo == null)
                {
                    return(extInfo);
                }

                foreach (ResolveInfo resolveInfo in extInfo)
                {
                    if (resolveInfo.GUID.IsNullOrEmpty())
                    {
                        //Don't add empty GUID to the new list, this way CompatibilityResolve will treat it as a hard mod and attempt to find a match
                        if (!DidBlankGUIDMessage) //No need to spam it for every single thing
                        {
                            Logger.Log(LogLevel.Warning | LogLevel.Message, $"[{characterName}] Blank GUID detected, attempting Compatibility Resolve");
                            DidBlankGUIDMessage = true;
                        }
                    }
                    else
                    {
                        string propertyWithoutPrefix = resolveInfo.Property;

                        //Remove outfit and accessory prefixes for searching purposes
                        if (propertyWithoutPrefix.StartsWith("outfit"))
                        {
                            propertyWithoutPrefix = propertyWithoutPrefix.Remove(0, propertyWithoutPrefix.IndexOf('.') + 1);
                        }
                        if (propertyWithoutPrefix.StartsWith("accessory"))
                        {
                            propertyWithoutPrefix = propertyWithoutPrefix.Remove(0, propertyWithoutPrefix.IndexOf('.') + 1);
                        }

                        MigrationInfo info = MigrationInfoList.Where(x => (x.Property == propertyWithoutPrefix && x.OldID == resolveInfo.Slot && x.OldGUID == resolveInfo.GUID) ||
                                                                     (x.Property == "*" && x.OldGUID == resolveInfo.GUID) ||
                                                                     (x.Property == "-" && x.OldGUID == resolveInfo.GUID)).FirstOrDefault();
                        if (info == null)
                        {
                            //This item does not need to be migrated
                            extInfoNew.Add(resolveInfo);
                        }
                        else if (info.Property == "*") //* assumes only the GUID changed while the IDs stayed the same
                        {
                            ResolveInfo GUIDCheckOld = UniversalAutoResolver.LoadedResolutionInfo.FirstOrDefault(x => x.GUID == resolveInfo.GUID);

                            if (GUIDCheckOld == null)
                            {
                                //We do not have the old mod installed, do migration. Whether we have the new mod is irrelevant.
                                //If we don't have the new mod the user will get a missing mod warning for the new mod since they should be using that instead.
                                //If we do it will load correctly.
                                Logger.Log(LogLevel.Info, $"Migrating GUID {info.OldGUID} -> {info.NewGUID}");
                                ResolveInfo resolveInfoNew = new ResolveInfo();
                                resolveInfoNew      = resolveInfo;
                                resolveInfoNew.GUID = info.NewGUID;
                                extInfoNew.Add(resolveInfoNew);
                            }
                            else
                            {
                                ResolveInfo GUIDCheckNew = UniversalAutoResolver.LoadedResolutionInfo.FirstOrDefault(x => x.GUID == info.NewGUID);

                                if (GUIDCheckNew == null)
                                {
                                    //We have the old mod but not the new, do not do migration
                                    extInfoNew.Add(resolveInfo);
                                }
                                else
                                {
                                    //We have the old mod and the new, do migration so characters save with the new stuff
                                    Logger.Log(LogLevel.Info, $"Migrating GUID {info.OldGUID} -> {info.NewGUID}");
                                    ResolveInfo resolveInfoNew = new ResolveInfo();
                                    resolveInfoNew      = resolveInfo;
                                    resolveInfoNew.GUID = info.NewGUID;
                                    extInfoNew.Add(resolveInfoNew);
                                }
                            }
                        }
                        else if (info.Property == "-") //- indicates the entry needs to be stripped of its extended data and loaded as a hard mod
                        {
                            continue;
                        }
                        else
                        {
                            ResolveInfo intResolveOld = UniversalAutoResolver.LoadedResolutionInfo.FirstOrDefault(x => x.Property == propertyWithoutPrefix && x.Slot == resolveInfo.Slot && x.GUID == resolveInfo.GUID);

                            if (intResolveOld == null)
                            {
                                //We do not have the old mod installed, do migration. Whether we have the new mod is irrelevant.
                                //If we don't have the new mod the user will get a missing mod warning for the new mod since they should be using that instead.
                                //If we do it will load correctly.
                                Logger.Log(LogLevel.Info, $"Migrating {info.OldGUID}:{info.OldID} -> {info.NewGUID}:{info.NewID}");
                                ResolveInfo resolveInfoNew = new ResolveInfo();
                                resolveInfoNew      = resolveInfo;
                                resolveInfoNew.GUID = info.NewGUID;
                                resolveInfoNew.Slot = info.NewID;
                                extInfoNew.Add(resolveInfoNew);
                            }
                            else
                            {
                                ResolveInfo intResolveNew = UniversalAutoResolver.LoadedResolutionInfo.FirstOrDefault(x => x.Property == propertyWithoutPrefix && x.Slot == info.NewID && x.GUID == info.NewGUID);

                                if (intResolveNew == null)
                                {
                                    //We have the old mod but not the new, do not do migration
                                    extInfoNew.Add(resolveInfo);
                                }
                                else
                                {
                                    //We have the old mod and the new, do migration so characters save with the new stuff
                                    Logger.Log(LogLevel.Warning, $"Migrating {info.OldGUID}:{info.OldID} -> {info.NewGUID}:{info.NewID}");
                                    ResolveInfo b = new ResolveInfo();
                                    b      = resolveInfo;
                                    b.GUID = info.NewGUID;
                                    b.Slot = info.NewID;
                                    extInfoNew.Add(b);
                                }
                            }
                        }
                    }
                }
                extInfo = extInfoNew;
            }
            catch (Exception ex)
            {
                //If something goes horribly wrong, return the original extInfo
                Logger.Log(LogLevel.Error, $"GUID migration cancelled due to error: {ex}");
                return(extInfo);
            }

            return(extInfoNew);
        }
        /// <summary>
        /// Read all the manifest.xml files and generate a dictionary of uncensors to be used in config manager dropdown
        /// </summary>
        private static void PopulateUncensorLists()
        {
            BodyDictionary.Clear();
            BodyConfigListFull.Clear();
            PenisDictionary.Clear();
            PenisConfigListFull.Clear();
            BallsDictionary.Clear();
            BallsConfigListFull.Clear();

            //Add the default body options
            BodyConfigListFull.Add("None (censored)", UncensorKeyNone);
            BodyConfigListFull.Add("Random", UncensorKeyRandom);

            BodyData DefaultMale = new BodyData(0, "Default.Body.Male", "Default Body M");

            BodyDictionary.Add(DefaultMale.BodyGUID, DefaultMale);
            BodyConfigListFull.Add($"[{(DefaultMale.Sex == 0 ? "Male" : "Female")}] {DefaultMale.DisplayName}", DefaultMale.BodyGUID);

            BodyData DefaultFemale = new BodyData(1, "Default.Body.Female", "Default Body F");

            BodyDictionary.Add(DefaultFemale.BodyGUID, DefaultFemale);
            BodyConfigListFull.Add($"[{(DefaultFemale.Sex == 0 ? "Male" : "Female")}] {DefaultFemale.DisplayName}", DefaultFemale.BodyGUID);

            //Add the default penis options
            PenisConfigListFull.Add("None", UncensorKeyNone);
            PenisConfigListFull.Add("Random", UncensorKeyRandom);

            PenisData DefaultPenis = new PenisData("Default.Penis", "Mosaic Penis");

            PenisDictionary.Add(DefaultPenis.PenisGUID, DefaultPenis);
            PenisConfigListFull.Add(DefaultPenis.DisplayName, DefaultPenis.PenisGUID);

            //Add the default balls options
            BallsConfigListFull.Add("None", UncensorKeyNone);
            BallsConfigListFull.Add("Random", UncensorKeyRandom);

            BallsData DefaultBalls = new BallsData("Default.Balls", "Mosaic Balls");

            BallsDictionary.Add(DefaultBalls.BallsGUID, DefaultBalls);
            BallsConfigListFull.Add(DefaultBalls.DisplayName, DefaultBalls.BallsGUID);

            foreach (var manifest in Sideloader.Sideloader.LoadedManifests)
            {
                XDocument manifestDocument        = manifest.manifestDocument;
                XElement  uncensorSelectorElement = manifestDocument?.Root?.Element(PluginNameInternal);
                if (uncensorSelectorElement != null && uncensorSelectorElement.HasElements)
                {
                    foreach (XElement uncensorElement in uncensorSelectorElement.Elements("body"))
                    {
                        BodyData bodyData = new BodyData(uncensorElement);
                        if (bodyData.BodyGUID == null)
                        {
                            Logger.Log(LogLevel.Warning, "Body failed to load due to missing GUID.");
                            continue;
                        }
                        if (bodyData.DisplayName == null)
                        {
                            Logger.Log(LogLevel.Warning, "Body failed to load due to missing display name.");
                            continue;
                        }
                        if (bodyData.OOBase == Defaults.OOBase)
                        {
                            Logger.Log(LogLevel.Warning, "Body was not loaded because oo_base is the default.");
                            continue;
                        }
                        BodyDictionary.Add(bodyData.BodyGUID, bodyData);
                        BodyConfigListFull.Add($"[{(bodyData.Sex == 0 ? "Male" : "Female")}] {bodyData.DisplayName}", bodyData.BodyGUID);
                        foreach (var part in bodyData.AdditionalParts)
                        {
                            AllAdditionalParts.Add(part);
                        }
                    }
                    foreach (XElement uncensorElement in uncensorSelectorElement.Elements("penis"))
                    {
                        PenisData penisData = new PenisData(uncensorElement);
                        if (penisData.PenisGUID == null)
                        {
                            Logger.Log(LogLevel.Warning, "Penis failed to load due to missing GUID.");
                            continue;
                        }
                        if (penisData.DisplayName == null)
                        {
                            Logger.Log(LogLevel.Warning, "Penis failed to load due to missing display name.");
                            continue;
                        }
                        if (penisData.File == null)
                        {
                            Logger.Log(LogLevel.Warning, "Penis failed to load due to missing file.");
                            continue;
                        }
                        if (penisData.Asset == null)
                        {
                            Logger.Log(LogLevel.Warning, "Penis failed to load due to missing asset.");
                            continue;
                        }
                        PenisDictionary.Add(penisData.PenisGUID, penisData);
                        PenisConfigListFull.Add(penisData.DisplayName, penisData.PenisGUID);
                    }
                    foreach (XElement uncensorElement in uncensorSelectorElement.Elements("balls"))
                    {
                        BallsData ballsData = new BallsData(uncensorElement);
                        if (ballsData.BallsGUID == null)
                        {
                            Logger.Log(LogLevel.Warning, "Balls failed to load due to missing GUID.");
                            continue;
                        }
                        if (ballsData.DisplayName == null)
                        {
                            Logger.Log(LogLevel.Warning, "Balls failed to load due to missing display name.");
                            continue;
                        }
                        if (ballsData.File == null)
                        {
                            Logger.Log(LogLevel.Warning, "Balls failed to load due to missing file.");
                            continue;
                        }
                        if (ballsData.Asset == null)
                        {
                            Logger.Log(LogLevel.Warning, "Balls failed to load due to missing asset.");
                            continue;
                        }
                        BallsDictionary.Add(ballsData.BallsGUID, ballsData);
                        BallsConfigListFull.Add(ballsData.DisplayName, ballsData.BallsGUID);
                    }
                    foreach (XElement uncensorElement in uncensorSelectorElement.Elements("migration"))
                    {
                        MigrationData migrationData = new MigrationData(uncensorElement);
                        if (migrationData.UncensorGUID == null)
                        {
                            Logger.Log(LogLevel.Warning, "Migration data failed to load due to missing Uncensor GUID.");
                            continue;
                        }
                        if (migrationData.BodyGUID == null)
                        {
                            Logger.Log(LogLevel.Warning, "Migration data failed to load due to missing Body GUID.");
                            continue;
                        }
                        MigrationDictionary.Add(migrationData.UncensorGUID, migrationData);
                    }
                }
            }
        }
示例#27
0
        private static object DoReplace(TextAsset asset, ReplaceType replaceType)
        {
            object obj  = null;
            var    text = "";

            if (replaceType != ReplaceType.Text)
            {
                if (replaceType == ReplaceType.Bytes)
                {
                    try
                    {
                        RestoreMethodStart(_originalBytesLocation, _originalBytesCode);
                        obj = asset.bytes;
                        Memory.WriteJump(_originalBytesLocation, _replacementBytesLocation);
                        text = "bytes";
                    }
                    catch (Exception ex)
                    {
                        Logger.Log(LogLevel.Error, "Error getting bytes");
                        LogException(ex);
                        throw;
                    }
                }
            }
            else
            {
                try
                {
                    RestoreMethodStart(_originalTextLocation, _originalTextCode);
                    obj = asset.text;
                    Memory.WriteJump(_originalTextLocation, _replacementTextLocation);
                    text = "text";
                }
                catch (Exception ex2)
                {
                    Logger.Log(LogLevel.Error, "Error getting text");
                    LogException(ex2);
                    throw;
                }
            }

            var name = Path.Combine(_assetsPath, asset.name);
            var replacementFilePath = name + ReplacementFileExtension;

            if (File.Exists(replacementFilePath))
            {
                if (ReplacingEnabled.Value)
                {
                    Logger.Log(LogLevel.Info, "Replacing " + text + " in " + name);
                    try
                    {
                        if (replaceType != ReplaceType.Text)
                        {
                            if (replaceType == ReplaceType.Bytes)
                            {
                                obj = File.ReadAllBytes(replacementFilePath);
                            }
                        }
                        else
                        {
                            obj = File.ReadAllText(replacementFilePath, Encoding.UTF8);
                        }
                    }
                    catch (Exception e)
                    {
                        Logger.Log(LogLevel.Error, string.Concat("Error replacing ", text, " in ", name, ", using original"));
                        LogException(e);
                    }
                }
            }
            else
            {
                if (DumpingEnabled.Value)
                {
                    var dumpFilePath = name + DumpFileExtension;
                    if (!File.Exists(dumpFilePath))
                    {
                        Logger.Log(LogLevel.Info, "File " + replacementFilePath + " not found, creating 'found' file");
                        try
                        {
                            if (replaceType != ReplaceType.Text)
                            {
                                if (replaceType == ReplaceType.Bytes)
                                {
                                    File.WriteAllBytes(dumpFilePath, obj as byte[] ?? throw new InvalidOperationException());
                                }
                            }
                            else
                            {
                                File.WriteAllText(dumpFilePath, obj as string, Encoding.UTF8);
                            }
                        }
                        catch (Exception e2)
                        {
                            Logger.Log(LogLevel.Error, "Error creating file: " + dumpFilePath);
                            LogException(e2);
                        }
                    }

                    if (File.Exists(replacementFilePath))
                    {
                        obj = DoReplace(asset, replaceType);
                    }
                }
            }
            return(obj);
        }
示例#28
0
        private void LoadModsFromDirectory(string modDirectory)
        {
            string GetRelativeArchiveDir(string archiveDir)
            {
                return(archiveDir.Length < modDirectory.Length ? archiveDir : archiveDir.Substring(modDirectory.Length).Trim(' ', '/', '\\'));
            }

            Logger.Log(LogLevel.Info, "[SIDELOADER] Scanning the \"mods\" directory...");

            // Look for mods, load their manifests
            var allMods = Directory.GetFiles(modDirectory, "*", SearchOption.AllDirectories)
                          .Where(x => x.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) ||
                                 x.EndsWith(".zipmod", StringComparison.OrdinalIgnoreCase));

            var archives = new Dictionary <ZipFile, Manifest>();

            foreach (var archivePath in allMods)
            {
                ZipFile archive = null;
                try
                {
                    archive = new ZipFile(archivePath);

                    if (Manifest.TryLoadFromZip(archive, out Manifest manifest))
                    {
                        archives.Add(archive, manifest);
                    }
                }
                catch (Exception ex)
                {
                    Logger.Log(LogLevel.Error, $"[SIDELOADER] Failed to load archive \"{GetRelativeArchiveDir(archivePath)}\" with error: {ex.Message}");
                    Logger.Log(LogLevel.Debug, $"[SIDELOADER] Error details: {ex}");
                    archive?.Close();
                }
            }

            // Handlie duplicate GUIDs and load unique mods
            foreach (var modGroup in archives.GroupBy(x => x.Value.GUID))
            {
                // Order by version if available, else use modified dates (less reliable)
                // If versions match, prefer mods inside folders or with more descriptive names so modpacks are preferred
                var orderedModsQuery = modGroup.All(x => !string.IsNullOrEmpty(x.Value.Version))
                    ? modGroup.OrderByDescending(x => x.Value.Version, new ManifestVersionComparer()).ThenByDescending(x => x.Key.Name.Length)
                    : modGroup.OrderByDescending(x => File.GetLastWriteTime(x.Key.Name));

                var orderedMods = orderedModsQuery.ToList();

                if (orderedMods.Count > 1)
                {
                    var modList = string.Join(", ", orderedMods.Select(x => '"' + GetRelativeArchiveDir(x.Key.Name) + '"').ToArray());
                    Logger.Log(LogLevel.Warning, $"[SIDELOADER] Archives with identical GUIDs detected! Archives: {modList}");
                    Logger.Log(LogLevel.Warning, $"[SIDELOADER] Only \"{GetRelativeArchiveDir(orderedMods[0].Key.Name)}\" will be loaded because it's the newest");

                    // Don't keep the duplicate archives in memory
                    foreach (var dupeMod in orderedMods.Skip(1))
                    {
                        dupeMod.Key.Close();
                    }
                }

                // Actually load the mods (only one per GUID, the newest one)
                var archive  = orderedMods[0].Key;
                var manifest = orderedMods[0].Value;
                try
                {
                    Archives.Add(archive);
                    LoadedManifests.Add(manifest);

                    LoadAllUnityArchives(archive, archive.Name);
                    LoadAllLists(archive, manifest);
                    BuildPngFolderList(archive);

                    var trimmedName = manifest.Name?.Trim();
                    var displayName = !string.IsNullOrEmpty(trimmedName) ? trimmedName : Path.GetFileName(archive.Name);

                    Logger.Log(LogLevel.Info, $"[SIDELOADER] Loaded {displayName} {manifest.Version ?? ""}");
                }
                catch (Exception ex)
                {
                    Logger.Log(LogLevel.Error, $"[SIDELOADER] Failed to load archive \"{GetRelativeArchiveDir(archive.Name)}\" with error: {ex.Message}");
                    Logger.Log(LogLevel.Debug, $"[SIDELOADER] Error details: {ex}");
                }
            }
            BuildPngOnlyFolderList();
        }
示例#29
0
        private void LoadTranslations()
        {
            translations.Clear();
            var dirTranslation     = Path.Combine(Utility.PluginsDirectory, "translation");
            var dirTranslationText = Path.Combine(dirTranslation, "Text");

            if (!Directory.Exists(dirTranslationText))
            {
                Directory.CreateDirectory(dirTranslationText);
            }

            var translation = Directory.GetFiles(dirTranslationText, "*.txt", SearchOption.AllDirectories)
                              .SelectMany(File.ReadAllLines)
                              .ToArray();

            foreach (var line in translation)
            {
                if (!line.Contains('='))
                {
                    continue;
                }

                var split = line.Split('=');
                if (split.Length != 2)
                {
                    Logger.Log(LogLevel.Warning, "Invalid text translation entry: " + line);
                    continue;
                }

                translations[split[0].Trim()] = split[1];
            }

            //ITL
            var di_tl = new DirectoryInfo(Path.Combine(dirTranslation, "Images"));

            TL_DIR_ROOT  = $"{di_tl.FullName}/{Application.productName}";
            TL_DIR_SCENE = $"{TL_DIR_ROOT}/Scenes";

            var di = new DirectoryInfo(TL_DIR_SCENE);

            if (!di.Exists)
            {
                di.Create();
            }

            foreach (var t in new DirectoryInfo(TL_DIR_ROOT).GetFiles("*.txt"))
            {
                var sceneName = Path.GetFileNameWithoutExtension(t.Name);
                textureLoadTargets[sceneName] = new Dictionary <string, byte[]>();
                foreach (var tl in File.ReadAllLines(t.FullName))
                {
                    var tp = tl.Split('=');
                    if (tp.Length != 2)
                    {
                        Logger.Log(LogLevel.Warning, "Invalid entry in " + t.Name + " - " + tl);
                        continue;
                    }
                    var path = $"{TL_DIR_SCENE}/{tp[1]}";
                    if (!File.Exists(path))
                    {
                        Logger.Log(LogLevel.Warning, "Missing TL image: " + path);
                        continue;
                    }
                    textureLoadTargets[sceneName][tp[0]] = File.ReadAllBytes(path);
                }
            }

            GlobalTextureTargetExists = textureLoadTargets.ContainsKey(GlobalTextureTargetName);

            SceneManager.sceneUnloaded += s =>
            {
                if (IsDumpingEnabled.Value)
                {
                    var sn = DumpingAllToGlobal.Value ? GlobalTextureTargetName : s.name;
                    if (sw_textureNameDump.TryGetValue(sn, out var sw))
                    {
                        sw.Flush();
                    }
                }
            };
        }
示例#30
0
        /// <summary>
        /// Sets the visibility state of a character. If no optional parameters are set the character's visiblity state will be read from the character file and set from that.
        /// </summary>
        /// <param name="chaControl">Character for which to set visible state.</param>
        /// <param name="toggleVisible">Toggles the character from visible to invisible and vice versa. Not used if forceVisible is set.</param>
        /// <param name="forceVisible">Forces the character to the state set in forceVisibleState. Overrides default visibility state and toggleVisible.</param>
        /// <param name="forceVisibleState">The visibility state to set a character. Only used if forceVisible is set.</param>
        /// <param name="saveVisibleState">Whether or not the visible state should be saved to the card.</param>
        private static void SetVisibleState(ChaControl chaControl, bool toggleVisible = false, bool forceVisible = false, bool forceVisibleState = false, bool saveVisibleState = true)
        {
            bool       Visible;
            PluginData ExtendedData    = ExtendedSave.GetExtendedDataById(chaControl.chaFile, "KK_InvisibleBody");
            GameObject CharacterObject = GameObject.Find(chaControl.name);

            if (ExtendedData == null)
            {
                Logger.Log(LogLevel.Debug, "No KK_InvisibleBody marker found");
                Visible = true;
                //character has no extended data, create some so it will save and load with the scene
                ExtendedData = new PluginData();
                Dictionary <string, object> dic = new Dictionary <string, object> {
                    { "Visible", Visible }
                };
                ExtendedData.data = dic;
            }
            else
            {
                Logger.Log(LogLevel.Debug, $"KK_InvisibleBody marker found, Visible was {ExtendedData.data["Visible"]}");
                Visible = (bool)ExtendedData.data["Visible"];
            }

            if (forceVisible)
            {
                Visible = forceVisibleState;
            }
            else if (toggleVisible)
            {
                Visible = !Visible;
            }

            if (saveVisibleState)
            {
                ExtendedData.data["Visible"] = Visible;
                ExtendedSave.SetExtendedDataById(chaControl.chaFile, "KK_InvisibleBody", ExtendedData);
            }

            //No need to IterateVisible for visible characters that haven't changed
            if (!(Visible == true && toggleVisible == false && forceVisible == false))
            {
                Transform cf_j_root = CharacterObject.transform.Find("BodyTop/p_cf_body_bone/cf_j_root");
                if (cf_j_root != null)
                {
                    IterateVisible(cf_j_root.gameObject, Visible);
                }

                //female
                Transform cf_o_rootf = CharacterObject.transform.Find("BodyTop/p_cf_body_00/cf_o_root/");
                if (cf_o_rootf != null)
                {
                    IterateVisible(cf_o_rootf.gameObject, Visible);
                }

                //male
                Transform cf_o_rootm = CharacterObject.transform.Find("BodyTop/p_cm_body_00/cf_o_root/");
                if (cf_o_rootm != null)
                {
                    IterateVisible(cf_o_rootm.gameObject, Visible);
                }
            }
        }