/// <summary>
 /// Get extended data for character's face (face sliders, eye settings).
 /// Do not store this data because it might change without notice, for example when clothing is copied. Always call Get at the point where you need the data, not earlier.
 /// If you change any of the data, remember to call the corresponding Set method or the change might not be saved.
 /// This data is saved alongside game data, which means it is automatically copied and moved as necessary.
 /// If no extended data of this plugin was set yet, this method will return null.
 /// In maker, you can update controls that use this data in the <see cref="MakerAPI.ReloadCustomInterface"/> event.
 /// </summary>
 public PluginData GetFaceExtData()
 {
     KoikatuAPI.Assert(ChaFileControl.custom != null, "ChaFileControl.custom != null");
     KoikatuAPI.Assert(ChaFileControl.custom.face != null, "ChaFileControl.custom.face != null");
     ChaFileControl.custom.face.TryGetExtendedDataById(ExtendedDataId, out var data);
     return(data);
 }
        /// <summary>
        /// Get extended data for character's parameters (personality, preferences, traits).
        /// Do not store this data because it might change without notice, for example when clothing is copied. Always call Get at the point where you need the data, not earlier.
        /// If you change any of the data, remember to call the corresponding Set method or the change might not be saved.
        /// This data is saved alongside game data, which means it is automatically copied and moved as necessary.
        /// If no extended data of this plugin was set yet, this method will return null.
        /// In maker, you can update controls that use this data in the <see cref="MakerAPI.ReloadCustomInterface"/> event.
        /// </summary>
        public PluginData GetParameterExtData()
        {
            var chafile = GetExtDataTargetChaFile(false);

            KoikatuAPI.Assert(chafile.parameter != null, "chafile.parameter != null");
            chafile.parameter.TryGetExtendedDataById(ExtendedDataId, out var data);
            return(data);
        }
        /// <summary>
        /// Get extended data for character's face (face sliders, eye settings).
        /// Do not store this data because it might change without notice, for example when clothing is copied. Always call Get at the point where you need the data, not earlier.
        /// If you change any of the data, remember to call the corresponding Set method or the change might not be saved.
        /// This data is saved alongside game data, which means it is automatically copied and moved as necessary.
        /// If no extended data of this plugin was set yet, this method will return null.
        /// In maker, you can update controls that use this data in the <see cref="MakerAPI.ReloadCustomInterface"/> event.
        /// </summary>
        public PluginData GetFaceExtData()
        {
            var chafile = GetExtDataTargetChaFile(false);

            KoikatuAPI.Assert(chafile.custom != null, "chafile.custom != null");
            KoikatuAPI.Assert(chafile.custom.face != null, "chafile.custom.face != null");
            chafile.custom.face.TryGetExtendedDataById(ExtendedDataId, out var data);
            return(data);
        }
        /// <summary>
        /// Set extended data for a specific accessory.
        /// Always call Set right after changing any of the data, or the change might not be saved if the data is changed for whatever reason (clothing change, reload, etc.)
        /// This data is saved alongside game data, which means it is automatically copied and moved as necessary.
        /// </summary>
        /// <param name="data">Extended data to save.</param>
        /// <param name="accessoryPartId">The accessory part number to open extened data for (0-indexed).</param>
        /// <param name="coordinateId">The coordinate number to open extened data for if the game supports multiple coords (0-indexed). -1 will use the current coordinate.</param>
        public void SetAccessoryExtData(PluginData data, int accessoryPartId, int coordinateId = -1)
        {
            var coord = GetCoordinate(coordinateId);

            KoikatuAPI.Assert(coord != null, nameof(coord) + " != null");
            KoikatuAPI.Assert(coord.accessory != null, "coord.accessory != null");
            var accessoryPart = coord.accessory.parts[accessoryPartId];

            accessoryPart.SetExtendedDataById(ExtendedDataId, data);
        }
        /// <summary>
        /// Set extended data for specific clothes.
        /// Always call Set right after changing any of the data, or the change might not be saved if the data is changed for whatever reason (clothing change, reload, etc.)
        /// This data is saved alongside game data, which means it is automatically copied and moved as necessary.
        /// </summary>
        /// <param name="data">Extended data to save.</param>
        /// <param name="coordinateId">The coordinate number to open extened data for if the game supports multiple coords (0-indexed). -1 will use the current coordinate.</param>
        public void SetClothesExtData(PluginData data, int coordinateId = -1)
        {
            var coord = GetCoordinate(coordinateId);

            KoikatuAPI.Assert(coord != null, nameof(coord) + " != null");
            KoikatuAPI.Assert(coord.clothes != null, "coord.clothes != null");
            var clothes = coord.clothes;

            clothes.SetExtendedDataById(ExtendedDataId, data);
        }
        /// <summary>
        /// Get extended data for a specific accessory.
        /// Do not store this data because it might change without notice, for example when clothing is copied. Always call Get at the point where you need the data, not earlier.
        /// If you change any of the data, remember to call the corresponding Set method or the change might not be saved.
        /// This data is saved alongside game data, which means it is automatically copied and moved as necessary.
        /// If no extended data of this plugin was set yet, this method will return null.
        /// In maker, you can update controls that use this data in the <see cref="MakerAPI.ReloadCustomInterface"/> event.
        /// </summary>
        /// <param name="accessoryPartId">The accessory part number to open extened data for (0-indexed).</param>
        /// <param name="coordinateId">The coordinate number to open extened data for if the game supports multiple coords (0-indexed). -1 will use the current coordinate.</param>
        public PluginData GetAccessoryExtData(int accessoryPartId, int coordinateId = -1)
        {
            var coord = GetCoordinate(coordinateId);

            KoikatuAPI.Assert(coord != null, nameof(coord) + " != null");
            KoikatuAPI.Assert(coord.accessory != null, "coord.accessory != null");
            var accessoryPart = coord.accessory.parts[accessoryPartId];

            accessoryPart.TryGetExtendedDataById(ExtendedDataId, out var data);
            return(data);
        }
        /// <summary>
        /// Get extended data for specific clothes.
        /// Do not store this data because it might change without notice, for example when clothing is copied. Always call Get at the point where you need the data, not earlier.
        /// If you change any of the data, remember to call the corresponding Set method or the change might not be saved.
        /// This data is saved alongside game data, which means it is automatically copied and moved as necessary.
        /// If no extended data of this plugin was set yet, this method will return null.
        /// In maker, you can update controls that use this data in the <see cref="MakerAPI.ReloadCustomInterface"/> event.
        /// </summary>
        /// <param name="coordinateId">The coordinate number to open extened data for if the game supports multiple coords (0-indexed). -1 will use the current coordinate.</param>
        public PluginData GetClothesExtData(int coordinateId = -1)
        {
            var coord = GetCoordinate(coordinateId);

            KoikatuAPI.Assert(coord != null, nameof(coord) + " != null");
            KoikatuAPI.Assert(coord.clothes != null, "coord.clothes != null");
            var clothes = coord.clothes;

            clothes.TryGetExtendedDataById(ExtendedDataId, out var data);
            return(data);
        }
        /// <summary>
        /// Set extended data for character's parameters (personality, preferences, traits).
        /// Always call Set right after changing any of the data, or the change might not be saved if the data is changed for whatever reason (clothing change, reload, etc.)
        /// This data is saved alongside game data, which means it is automatically copied and moved as necessary.
        /// </summary>
        /// <param name="data">Extended data to save.</param>
        public void SetParameterExtData(PluginData data)
        {
            var chafile = GetExtDataTargetChaFile(true);

            KoikatuAPI.Assert(chafile.parameter != null, "chafile.parameter != null");
            chafile.parameter.SetExtendedDataById(ExtendedDataId, data);
            // Save both to the main chafile and to the current instance in case it gets saved by something
            if (chafile != ChaFileControl)
            {
                ChaFileControl.parameter.SetExtendedDataById(ExtendedDataId, data);
            }
        }
        protected override void OnReload(GameMode currentGameMode, bool maintainState)
        {
            var a = GetExtendedData();
            var b = GetBodyExtData();
            var c = GetParameterExtData();
            var d = GetFaceExtData();

            KoikatuAPI.Assert((a == null && b == null && c == null && d == null) || (a != null && b != null && c != null && d != null), "ext data not lining up");
            if (a != null)
            {
                KoikatuAPI.Assert(b != null, "b != null");
                KoikatuAPI.Assert(c != null, "c != null");
                KoikatuAPI.Assert(d != null, "d != null");
                var newId = a.data["id"] as string;
                KoikatuAPI.Assert(newId == b?.data["id"] as string, "a.data[\"id\"] == b.data[\"id\"]");
                KoikatuAPI.Assert(newId == c?.data["id"] as string, "a.data[\"id\"] == c.data[\"id\"]");
                KoikatuAPI.Assert(newId == d?.data["id"] as string, "a.data[\"id\"] == d.data[\"id\"]");
                Console.WriteLine($"ID get from ext data - {newId}  |  Old ID - {id}  |  Chara - {ChaControl.name}");
                id = newId;
            }
            if (id == null)
            {
                id = $"{No++} - {SceneApi.GetLoadSceneName()} - {SceneApi.GetAddSceneName()}";
                Console.WriteLine($"New ID assigned - {id}  |  Chara - {ChaControl.name} | {ChaFileControl.parameter.fullname}");
                SetParameterExtData(new PluginData()
                {
                    data = new Dictionary <string, object> {
                        { "id", id }
                    }
                });
                SetBodyExtData(new PluginData()
                {
                    data = new Dictionary <string, object> {
                        { "id", id }
                    }
                });
                SetFaceExtData(new PluginData()
                {
                    data = new Dictionary <string, object> {
                        { "id", id }
                    }
                });
                SetExtendedData(new PluginData()
                {
                    data = new Dictionary <string, object> {
                        { "id", id }
                    }
                });
            }

            KoikatuAPI.Logger.LogWarning($"event:OnReload  chara:{ChaControl.name}  currentGameMode:{currentGameMode}  maintainState:{maintainState}");
        }
        private ChaFileCoordinate GetCoordinate(int coordinateId)
        {
            KoikatuAPI.Assert(ChaControl.nowCoordinate != null, "ChaControl.nowCoordinate != null");
#if KK || KKS
            return(coordinateId < 0 ? ChaControl.nowCoordinate : ChaFileControl.coordinate[coordinateId]);
#elif EC || AI || HS2
            if (coordinateId > 0)
            {
                KoikatuAPI.Logger.LogWarning("This game doesn't support multiple coordinates, nowCoordinate will be used!\n" + new System.Diagnostics.StackTrace());
            }
            return(ChaControl.nowCoordinate);
#endif
        }
        private ChaFileCoordinate GetCoordinate(int coordinateId)
        {
            // Get coord from the current ChaControl since it can be temporarily changed and would mess up ext data of clothes inside heroine.chaCtrl if we saved there
            KoikatuAPI.Assert(ChaControl.nowCoordinate != null, "ChaControl.nowCoordinate != null");
#if KK || KKS
            return(coordinateId < 0 ? ChaControl.nowCoordinate : ChaFileControl.coordinate[coordinateId]);
#elif EC || AI || HS2
            if (coordinateId > 0)
            {
                KoikatuAPI.Logger.LogWarning("This game doesn't support multiple coordinates, nowCoordinate will be used!\n" + new System.Diagnostics.StackTrace());
            }
            return(ChaControl.nowCoordinate);
#endif
        }
        /// <inheritdoc />
        protected override GameObject OnCreateControl(Transform subCategoryList)
        {
            var tr = Object.Instantiate(RadioCopy, subCategoryList, false);

            tr.name = "rb";

            var settingName = tr.Find("textTglTitle").GetComponent <TextMeshProUGUI>();

            settingName.text  = _settingName;
            settingName.color = TextColor;

            var sourceToggle = tr.Find("rb00");

            Buttons = _buttons.Select(
                (x, i) =>
            {
                if (i == 0)
                {
                    return(sourceToggle);
                }

                var newButton  = Object.Instantiate(sourceToggle, tr, false);
                newButton.name = "rb0" + i;

                return(newButton);
            })
                      .Select(x => x.GetComponent <Toggle>())
                      .ToList()
                      .AsReadOnly();

            // If there's less toggles than the number of toggles in a row, expand them to fill all space
            var singleToggleWidth = 280 / Mathf.Min(Buttons.Count, ColumnCount);
            var rowCount          = Mathf.CeilToInt((float)Buttons.Count / ColumnCount);

            tr.GetComponent <LayoutElement>().minHeight = 40 * rowCount;
            var buttonIndex = 0;

            for (var rowIndex = 0; rowIndex < rowCount; rowIndex++)
            {
                for (var columnIndex = 0; columnIndex < ColumnCount; columnIndex++)
                {
                    if (buttonIndex >= Buttons.Count)
                    {
                        break;
                    }
                    var toggle = Buttons[buttonIndex];

                    var rt = toggle.GetComponent <RectTransform>();
                    rt.offsetMin = new Vector2(singleToggleWidth * columnIndex - 280, 8);
                    rt.offsetMax = new Vector2(singleToggleWidth * (columnIndex + 1) - 280, -8);

                    rt.anchorMax = new Vector2(1, 1 - rowIndex / (float)rowCount);
                    rt.anchorMin = new Vector2(1, 1 - (rowIndex + 1) / (float)rowCount);

                    toggle.GetComponentInChildren <TextMeshProUGUI>().text = _buttons[buttonIndex];

                    var indexCopy = buttonIndex;
                    toggle.onValueChanged.AddListener(
                        a =>
                    {
                        if (a || indexCopy == Value)
                        {
                            SetValue(indexCopy);
                        }
                    });

                    buttonIndex++;
                }
            }

            KoikatuAPI.Assert(buttonIndex >= Buttons.Count, "Didn't loop over all radio buttons, only " + buttonIndex);

            BufferedValueChanged.Subscribe(
                i =>
            {
                for (var index = 0; index < Buttons.Count; index++)
                {
                    var tgl  = Buttons[index];
                    tgl.isOn = index == i;
                }
            });

            return(tr.gameObject);
        }
        private static void SpawnActionPoint(ActionIconEntry iconEntry, int created)
        {
            var inst = CommonLib.LoadAsset <GameObject>("map/playeractionpoint/00.unity3d", "PlayerActionPoint_05", true);

            inst.gameObject.name = "CustomActionPoint_" + created;
            var parent = GameObject.Find("Map/ActionPoints");

            inst.transform.SetParent(parent.transform, true);

            var pap            = inst.GetComponentInChildren <PlayerActionPoint>();
            var iconRootObject = pap.gameObject;

            pap.gameObject.name = inst.name;
            var iconRootTransform = pap.transform;

            var rendererIcon = pap.renderers.Reverse().First(x =>
            {
                var tex = x.material.mainTexture;
                return(tex.width == 256 && tex.height == 256);
            });
            var animator = pap.animator;

            var renderers = pap.renderers;       //iconRootTransform.GetComponentsInChildren<MeshRenderer>();
            var particles = pap.particleSystems; //iconRootTransform.GetComponentsInChildren<ParticleSystem>();

            KoikatuAPI.Assert(particles.Length == 3, "SpawnActionPoint particles.Length == 3 failed");

            pap.gameObject.layer = LayerMask.NameToLayer("Action/ActionPoint");

            foreach (Transform child in pap.transform.parent)
            {
                if (child != pap.transform)
                {
                    Object.Destroy(child.gameObject);
                }
            }
            Object.DestroyImmediate(pap, false);

            iconRootTransform.position = iconEntry.Position;

            // Set color to pink
            var pointColor = iconEntry.Color;

            foreach (var rend in renderers)
            {
                rend.material.color = pointColor;
            }
#pragma warning disable 618
            foreach (var rend in particles)
            {
                rend.startColor = pointColor;
            }
#pragma warning restore 618

            // Initial animation state
            particles[0].Play();
            particles[1].Stop();
            particles[2].Stop();

            // Hook up event/anim logic
            var evt = iconRootObject.AddComponent <TriggerEnterExitEvent>();
            rendererIcon.material.mainTexture = iconEntry.Icon;
            var playerInRange = false;
            evt.onTriggerEnter += c =>
            {
                if (!c.CompareTag("Player"))
                {
                    return;
                }
                playerInRange = true;
                animator.Play(PAP.Assist.Animation.SpinState);
                Utils.Sound.Play(Manager.Sound.Type.GameSE2D, Utils.Sound.SEClipTable[0x38], 0f);
                c.GetComponent <Player>().actionPointList.Add(evt);
                ActionScene.instance.actionChangeUI.Set(ActionChangeUI.ActionType.Shop);
                ActionScene.instance.actionChangeUI._text.text = iconEntry.PopupText;
                particles[0].Stop();
                particles[1].Play();
                particles[2].Play();
            };
            evt.onTriggerExit += c =>
            {
                if (!c.CompareTag("Player"))
                {
                    return;
                }
                playerInRange = false;
                animator.Play(PAP.Assist.Animation.IdleState);
                c.GetComponent <Player>().actionPointList.Remove(evt);
                ActionScene.instance.actionChangeUI.Remove(ActionChangeUI.ActionType.Shop);
                particles[0].Play();
                particles[1].Stop();
                particles[2].Stop();
            };

            evt.UpdateAsObservable()
            .Subscribe(_ =>
            {
                // Hide in H scenes and other places
                var isVisible = !Game.IsRegulate(true) && ActionScene.initialized;

                if (isVisible)
                {
                    var asi   = ActionScene.instance;
                    isVisible = asi.regulate == 0 && (asi.AdvScene == null || !asi.AdvScene.gameObject.activeSelf) && !TalkScene.isPaly;
                }

                if (renderers[0].enabled != isVisible)
                {
                    foreach (var renderer in renderers)
                    {
                        renderer.enabled = isVisible;
                    }

                    if (!isVisible)
                    {
                        particles[0].Stop();
                        particles[1].Stop();
                        particles[2].Stop();
                    }
                    else
                    {
                        particles[0].Play();
                        particles[1].Stop();
                        particles[2].Stop();
                    }
                }

                // Check if player clicked this point
                if (isVisible && playerInRange && ActionInput.isAction && !ActionScene.instance.Player.isActionNow)
                {
                    iconEntry.OnOpen();
                }
            })
            .AddTo(evt);

            iconEntry.Instance = inst;

            iconEntry.OnCreated?.Invoke(evt);
        }
 /// <summary>
 /// Set extended data for character's parameters (personality, preferences, traits).
 /// Always call Set right after changing any of the data, or the change might not be saved if the data is changed for whatever reason (clothing change, reload, etc.)
 /// This data is saved alongside game data, which means it is automatically copied and moved as necessary.
 /// </summary>
 /// <param name="data">Extended data to save.</param>
 public void SetParameterExtData(PluginData data)
 {
     KoikatuAPI.Assert(ChaFileControl.parameter != null, "ChaFileControl.parameter != null");
     ChaFileControl.parameter.SetExtendedDataById(ExtendedDataId, data);
 }
 /// <summary>
 /// Set extended data for character's face (face sliders, eye settings).
 /// Always call Set right after changing any of the data, or the change might not be saved if the data is changed for whatever reason (clothing change, reload, etc.)
 /// This data is saved alongside game data, which means it is automatically copied and moved as necessary.
 /// </summary>
 /// <param name="data">Extended data to save.</param>
 public void SetFaceExtData(PluginData data)
 {
     KoikatuAPI.Assert(ChaFileControl.custom != null, "ChaFileControl.custom != null");
     KoikatuAPI.Assert(ChaFileControl.custom.face != null, "ChaFileControl.custom.face != null");
     ChaFileControl.custom.face.SetExtendedDataById(ExtendedDataId, data);
 }
 /// <summary>
 /// Get extended data for character's parameters (personality, preferences, traits).
 /// Do not store this data because it might change without notice, for example when clothing is copied. Always call Get at the point where you need the data, not earlier.
 /// If you change any of the data, remember to call the corresponding Set method or the change might not be saved.
 /// This data is saved alongside game data, which means it is automatically copied and moved as necessary.
 /// If no extended data of this plugin was set yet, this method will return null.
 /// In maker, you can update controls that use this data in the <see cref="MakerAPI.ReloadCustomInterface"/> event.
 /// </summary>
 public PluginData GetParameterExtData()
 {
     KoikatuAPI.Assert(ChaFileControl.parameter != null, "ChaFileControl.parameter != null");
     ChaFileControl.parameter.TryGetExtendedDataById(ExtendedDataId, out var data);
     return(data);
 }