diff --git a/doc/KKAPI.MainGame.md b/doc/KKAPI.MainGame.md index ec2ff52..e8d1a84 100644 --- a/doc/KKAPI.MainGame.md +++ b/doc/KKAPI.MainGame.md @@ -17,6 +17,7 @@ Static Methods | Type | Name | Summary | | --- | --- | --- | +| `void` | AddActionIcon(`Int32` mapNo, `Vector3` position, `Sprite` iconOn, `Sprite` iconOff, `Action` onOpen, `Action` onCreated = null) | Register a new action icon in roaming mode (like the icons for training/studying, club report screen, peeping). | | `IEnumerable` | GetBehaviours() | Get all registered behaviours for the game. | | `GameCustomFunctionController` | GetRegisteredBehaviour(`String` extendedDataId) | Get the first controller that was registered with the specified extendedDataId. | | `GameCustomFunctionController` | GetRegisteredBehaviour(`Type` controllerType) | Get the first controller that was registered with the specified extendedDataId. | @@ -93,6 +94,7 @@ Static Methods | `IEnumerable` | GetRelatedChaFiles(this `Heroine` heroine) | Get ChaFiles that are related to this heroine. Warning: It might not return some copies. | | `IEnumerable` | GetRelatedChaFiles(this `Player` player) | Get ChaFiles that are related to this heroine. Warning: It might not return some copies. | | `Boolean` | IsShowerPeeping(this `HFlag` hFlag) | Returns true if the H scene is peeping in the shower. Use `HFlag.mode` to get info on what mode the H scene is in. | +| `void` | SetIsCursorLock(this `ActionScene` actScene, `Boolean` value) | Set the value of isCursorLock (setter is private by default). Used to regain mouse cursor during roaming mode. Best used together with setting `UnityEngine.Time.timeScale` to 0 to pause the game. | ## `GameSaveLoadEventArgs` diff --git a/src/KKAPI/KKAPI.csproj b/src/KKAPI/KKAPI.csproj index e4b81ec..b2c4ae5 100644 --- a/src/KKAPI/KKAPI.csproj +++ b/src/KKAPI/KKAPI.csproj @@ -81,6 +81,7 @@ + @@ -114,14 +115,14 @@ IF EXIST $(SolutionDir)PostBuild.bat CALL "$(SolutionDir)PostBuild.bat" $(Target - + - + \ No newline at end of file diff --git a/src/KKAPI/MainGame/CustomActionIcon.cs b/src/KKAPI/MainGame/CustomActionIcon.cs new file mode 100644 index 0000000..ff700dc --- /dev/null +++ b/src/KKAPI/MainGame/CustomActionIcon.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using ActionGame; +using ActionGame.Chara; +using HarmonyLib; +using Illusion.Component; +using Manager; +using UniRx; +using UniRx.Triggers; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace KKAPI.MainGame +{ + internal static class CustomActionIcon + { + private sealed class ActionIconEntry + { + public readonly Sprite IconOff, IconOn; + public readonly int MapNo; + public readonly Vector3 Position; + public readonly Action OnOpen; + public readonly Action OnCreated; + + public ActionIconEntry(int mapNo, Vector3 position, Sprite iconOn, Sprite iconOff, Action onOpen, Action onCreated) + { + IconOff = iconOff; + OnOpen = onOpen; + OnCreated = onCreated; + IconOn = iconOn; + MapNo = mapNo; + Position = position; + } + } + + private static readonly List _entries = new List(); + + public static void AddActionIcon(int mapNo, Vector3 position, Sprite iconOn, Sprite iconOff, Action onOpen, Action onCreated = null) + { + if (iconOn == null) throw new ArgumentNullException(nameof(iconOn)); + if (iconOff == null) throw new ArgumentNullException(nameof(iconOff)); + if (onOpen == null) throw new ArgumentNullException(nameof(onOpen)); + + Object.DontDestroyOnLoad(iconOn); + Object.DontDestroyOnLoad(iconOff); + + var entry = new ActionIconEntry(mapNo, position, iconOn, iconOff, onOpen, onCreated); + _entries.Add(entry); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(ActionMap), "Reserve")] + private static void OnMapChangedHook(ActionMap __instance) + { + if (__instance.mapRoot == null || __instance.isMapLoading) return; + + var created = 0; + + foreach (var iconEntry in _entries) + { + if (iconEntry.MapNo == __instance.no) + { + try + { + SpawnActionPoint(iconEntry); + created++; + } + catch (Exception e) + { + KoikatuAPI.Logger.LogError($"Failed to created custom action point on map no {__instance.no} at {iconEntry.Position}\n{e}"); + } + } + } + + if (created > 0) + KoikatuAPI.Logger.LogDebug($"Created {created} custom action points on map no {__instance.no}"); + } + + private static void SpawnActionPoint(ActionIconEntry iconEntry) + { + var inst = CommonLib.LoadAsset("map/playeractionpoint/00.unity3d", "PlayerActionPoint_05", true); + var parent = GameObject.Find("Map/ActionPoints"); + inst.transform.SetParent(parent.transform, true); + + var pap = inst.GetComponentInChildren(); + var iconRootObject = pap.gameObject; + var iconRootTransform = pap.transform; + Object.DestroyImmediate(pap, false); + + iconRootTransform.position = iconEntry.Position; + + var evt = iconRootObject.AddComponent(); + var animator = iconRootObject.GetComponentInChildren(); + var rendererIcon = iconRootObject.GetComponentInChildren(); + rendererIcon.sprite = iconEntry.IconOff; + var playerInRange = false; + evt.onTriggerEnter += c => + { + if (!c.CompareTag("Player")) return; + playerInRange = true; + animator.Play("icon_action"); + rendererIcon.sprite = iconEntry.IconOn; + c.GetComponent().actionPointList.Add(evt); + }; + evt.onTriggerExit += c => + { + if (!c.CompareTag("Player")) return; + playerInRange = false; + animator.Play("icon_stop"); + rendererIcon.sprite = iconEntry.IconOff; + c.GetComponent().actionPointList.Remove(evt); + }; + + var player = Singleton.Instance.actScene.Player; + evt.UpdateAsObservable() + .Subscribe(_ => + { + // Hide in H scenes and other places + var isVisible = Singleton.IsInstance() && !Singleton.Instance.IsRegulate(true); + if (rendererIcon.enabled != isVisible) + rendererIcon.enabled = isVisible; + + // Check if player clicked this point + if (isVisible && playerInRange && ActionInput.isAction && !player.isActionNow) + iconEntry.OnOpen(); + }) + .AddTo(evt); + + iconEntry.OnCreated?.Invoke(evt); + } + } +} diff --git a/src/KKAPI/MainGame/GameAPI.Hooks.cs b/src/KKAPI/MainGame/GameAPI.Hooks.cs index 7904d39..ba91e50 100644 --- a/src/KKAPI/MainGame/GameAPI.Hooks.cs +++ b/src/KKAPI/MainGame/GameAPI.Hooks.cs @@ -9,9 +9,9 @@ public static partial class GameAPI { private class Hooks { - public static void SetupHooks() + public static void SetupHooks(Harmony hi) { - Harmony.CreateAndPatchAll(typeof(Hooks)); + hi.PatchAll(typeof(Hooks)); } [HarmonyPostfix] diff --git a/src/KKAPI/MainGame/GameApi.cs b/src/KKAPI/MainGame/GameApi.cs index b961310..b18eb30 100644 --- a/src/KKAPI/MainGame/GameApi.cs +++ b/src/KKAPI/MainGame/GameApi.cs @@ -4,7 +4,10 @@ using System.Linq; using ActionGame; using BepInEx.Bootstrap; +using HarmonyLib; +using Illusion.Component; using KKAPI.Studio; +using UniRx.Triggers; using UnityEngine; using UnityEngine.SceneManagement; @@ -22,7 +25,7 @@ public static partial class GameAPI // navigating these scenes in order triggers OnNewGame private static readonly IList _newGameDetectionScenes = - new List(new[] {"Title", "EntryPlayer", "ClassRoomSelect", "Action"}); + new List(new[] { "Title", "EntryPlayer", "ClassRoomSelect", "Action" }); private static int _newGameDetectionIndex = -1; @@ -111,11 +114,31 @@ public static void RegisterExtraBehaviour(string extendedDataId) where T : Ga _registeredHandlers.Add(newBehaviour, extendedDataId); } + /// + /// Register a new action icon in roaming mode (like the icons for training/studying, club report screen, peeping). + /// + /// Identification number of the map the icon should be spawned on + /// Position of the icon. All default icons are spawned at y=0, but different heights work fine to a degree. + /// You can figure out the position by walking to it and getting the player position with RUE. + /// Icon shown when player is in range to click it. + /// Icon shown when player is out of range. + /// Action triggered when player clicks the icon (If you want to open your own menu, use + /// to enable mouse cursor and hide the action icon to prevent it from being clicked again.). + /// Optional action to run after the icon is created. + /// Use to attach extra code to the icon, e.g. by using and similar methods. + public static void AddActionIcon(int mapNo, Vector3 position, Sprite iconOn, Sprite iconOff, Action onOpen, Action onCreated = null) + { + if (StudioAPI.InsideStudio) return; + CustomActionIcon.AddActionIcon(mapNo, position, iconOn, iconOff, onOpen, onCreated); + } + internal static void Init(bool insideStudio) { if (insideStudio) return; - Hooks.SetupHooks(); + var hi = new Harmony(typeof(GameAPI).FullName); + hi.PatchAll(typeof(Hooks)); + hi.PatchAll(typeof(CustomActionIcon)); _functionControllerContainer = new GameObject("GameCustomFunctionController Zoo"); _functionControllerContainer.transform.SetParent(Chainloader.ManagerObject.transform, false);