diff --git a/ml_bft/FingerSystem.cs b/ml_bft/FingerSystem.cs index 8946ef3..f7286d9 100644 --- a/ml_bft/FingerSystem.cs +++ b/ml_bft/FingerSystem.cs @@ -121,9 +121,11 @@ namespace ml_bft Animator l_animator = PlayerSetup.Instance.Animator; if(l_animator.isHuman) { - Utils.SetAvatarTPose(); - InputHandler.Instance.Rebind(PlayerSetup.Instance.transform.rotation); + IKSystem.Instance.SetAvatarPose(IKSystem.AvatarPose.TPose); + PlayerSetup.Instance.AvatarTransform.localPosition = Vector3.zero; + PlayerSetup.Instance.AvatarTransform.localRotation = Quaternion.identity; + InputHandler.Instance.Rebind(PlayerSetup.Instance.transform.rotation); foreach(var l_tuple in ms_fingersChains) { ReorientateTowards( @@ -221,14 +223,14 @@ namespace ml_bft { if(m_ready && MetaPort.Instance.isUsingVr && (p_handler != null) && Settings.SkeletalInput) { - if(CVRInputManager.Instance._leftController != ABI_RC.Systems.InputManagement.XR.eXRControllerType.None) + if(CVRInputManager.Instance.IsLeftControllerTracking()) { Quaternion l_turnBack = (m_leftHandOffset.m_source.rotation * m_leftHandOffset.m_offset) * Quaternion.Inverse(m_leftHandOffset.m_target.rotation); foreach(var l_offset in m_leftFingerOffsets) l_offset.m_target.rotation = l_turnBack * (l_offset.m_source.rotation * l_offset.m_offset); } - if(CVRInputManager.Instance._rightController != ABI_RC.Systems.InputManagement.XR.eXRControllerType.None) + if(CVRInputManager.Instance.IsRightControllerTracking()) { Quaternion l_turnBack = (m_rightHandOffset.m_source.rotation * m_rightHandOffset.m_offset) * Quaternion.Inverse(m_rightHandOffset.m_target.rotation); foreach(var l_offset in m_rightFingerOffsets) diff --git a/ml_bft/Utils.cs b/ml_bft/Utils.cs index adadeff..cfdbfd7 100644 --- a/ml_bft/Utils.cs +++ b/ml_bft/Utils.cs @@ -17,12 +17,5 @@ namespace ml_bft public static bool IsInVR() => ((MetaPort.Instance != null) && MetaPort.Instance.isUsingVr); public static bool AreKnucklesInUse() => ((CVRInputManager.Instance._leftController == ABI_RC.Systems.InputManagement.XR.eXRControllerType.Index) || (CVRInputManager.Instance._rightController == ABI_RC.Systems.InputManagement.XR.eXRControllerType.Index)); - - public static void SetAvatarTPose() - { - IKSystem.Instance.SetAvatarPose(IKSystem.AvatarPose.TPose); - PlayerSetup.Instance.AvatarTransform.localPosition = Vector3.zero; - PlayerSetup.Instance.AvatarTransform.localRotation = Quaternion.identity; - } } } diff --git a/ml_mods_cvr.sln b/ml_mods_cvr.sln index 1a8e1ef..df44b4a 100644 --- a/ml_mods_cvr.sln +++ b/ml_mods_cvr.sln @@ -32,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ml_ppu", "ml_ppu\ml_ppu.csp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ml_vet", "ml_vet\ml_vet.csproj", "{8DB32590-FC5B-46A8-9747-344E86B18ACF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ml_pah", "ml_pah\ml_pah.csproj", "{C4659F60-3FED-4F43-88E4-969907D4C7A6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -83,6 +85,9 @@ Global {8DB32590-FC5B-46A8-9747-344E86B18ACF}.Debug|x64.Build.0 = Debug|x64 {8DB32590-FC5B-46A8-9747-344E86B18ACF}.Release|x64.ActiveCfg = Release|x64 {8DB32590-FC5B-46A8-9747-344E86B18ACF}.Release|x64.Build.0 = Release|x64 + {C4659F60-3FED-4F43-88E4-969907D4C7A6}.Debug|x64.ActiveCfg = Debug|x64 + {C4659F60-3FED-4F43-88E4-969907D4C7A6}.Release|x64.ActiveCfg = Release|x64 + {C4659F60-3FED-4F43-88E4-969907D4C7A6}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ml_pah/AvatarEntry.cs b/ml_pah/AvatarEntry.cs new file mode 100644 index 0000000..ee940ff --- /dev/null +++ b/ml_pah/AvatarEntry.cs @@ -0,0 +1,14 @@ +using System; + +namespace ml_pah +{ + [Serializable] + class AvatarEntry + { + public string m_id; + public string m_name; + public string m_imageUrl; + public DateTime m_lastUsageDate; + public bool m_cached = false; + } +} diff --git a/ml_pah/HistoryManager.cs b/ml_pah/HistoryManager.cs new file mode 100644 index 0000000..88e9fd5 --- /dev/null +++ b/ml_pah/HistoryManager.cs @@ -0,0 +1,205 @@ +using ABI_RC.Core.Networking.API; +using ABI_RC.Core.Networking.API.Responses; +using Newtonsoft.Json; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace ml_pah +{ + static class HistoryManager + { + internal class EntriesUpdateEvent + { + event Action m_action; + public void AddListener(Action p_listener) => m_action += p_listener; + public void RemoveListener(Action p_listener) => m_action -= p_listener; + public void Invoke() => m_action?.Invoke(); + } + + public static readonly EntriesUpdateEvent OnEntriesUpdated = new EntriesUpdateEvent(); + + static bool ms_initialized = false; + static string ms_historyPath; + readonly static List ms_avatarEntries = new List(); + + static int ms_lastTick = 0; + + // Init + internal static void Initialize() + { + if(!ms_initialized) + { + ms_historyPath = Path.Combine(MelonLoader.Utils.MelonEnvironment.UserDataDirectory, "PlayerAvatarHistory.json"); + + try + { + if(File.Exists(ms_historyPath)) + { + string l_json = File.ReadAllText(ms_historyPath); + List l_entries = JsonConvert.DeserializeObject>(l_json); + if(l_entries != null) + { + ms_avatarEntries.AddRange(l_entries); + LimitEntries(); + ms_avatarEntries.Sort((a, b) => a.m_lastUsageDate.CompareTo(b.m_lastUsageDate)); + } + } + } + catch(Exception e) + { + MelonLoader.MelonLogger.Error(e); + } + + ms_lastTick = Environment.TickCount; + Settings.OnAutosaveTimeChanged.AddListener(OnAutosaveTimeChanged); + + ms_initialized = true; + } + } + + internal static void Shutdown() + { + if(ms_initialized) + { + SaveHistory(); + + Settings.OnAutosaveTimeChanged.RemoveListener(OnAutosaveTimeChanged); + + ms_initialized = false; + } + } + + // Update + public static void Update() + { + if(ms_initialized && (Settings.AutosaveTime > 0)) + { + int l_tick = Environment.TickCount; + if((l_tick - ms_lastTick) >= (Settings.AutosaveTime * 60000)) + { + MelonLoader.MelonCoroutines.Start(AutosaveCoroutine()); + ms_lastTick = l_tick; + } + } + } + + // Entries + internal static List GetAvatarEntries() => ms_avatarEntries; + + internal static void AddEntry(string p_id) + { + if(ms_initialized) + { + int l_index = ms_avatarEntries.FindIndex(l_entry => l_entry.m_id == p_id); + if(l_index != -1) + { + ms_avatarEntries[l_index].m_lastUsageDate = DateTime.Now; + + if(l_index != 0) + { + // Move in list + AvatarEntry l_entry = ms_avatarEntries[l_index]; + ms_avatarEntries.RemoveAt(l_index); + ms_avatarEntries.Insert(0, l_entry); + + OnEntriesUpdated?.Invoke(); + } + } + else + { + AvatarEntry l_entry = new AvatarEntry(); + l_entry.m_id = p_id; + l_entry.m_name = "Loading ..."; + l_entry.m_lastUsageDate = DateTime.Now; + + MelonLoader.MelonCoroutines.Start(RequestAvatarInfo(l_entry)); + } + } + } + + // History + internal static void ClearHistory() => ms_avatarEntries.Clear(); + + internal static void SaveHistory() + { + if(ms_initialized) + { + try + { + string l_json = JsonConvert.SerializeObject(ms_avatarEntries, Formatting.Indented); + File.WriteAllText(ms_historyPath, l_json); + } + catch(Exception e) + { + MelonLoader.MelonLogger.Error(e); + } + } + } + + static IEnumerator AutosaveCoroutine() + { + List l_listCopy = new List(); + l_listCopy.AddRange(ms_avatarEntries); + + Task l_task = Task.Run(() => AutosaveTask(l_listCopy)); + while(!l_task.IsCompleted) + yield return null; + } + + static async Task AutosaveTask(List p_entries) + { + try + { + string l_json = JsonConvert.SerializeObject(p_entries, Formatting.Indented); + File.WriteAllText(ms_historyPath, l_json); + } + catch(Exception e) + { + MelonLoader.MelonLogger.Error(e); + } + + await Task.Delay(1); + } + + // Network request + static IEnumerator RequestAvatarInfo(AvatarEntry p_entry) + { + Task l_task = Task.Run(() => RequestAvatarInfoTask(p_entry)); + while(!l_task.IsCompleted) + yield return null; + + ms_avatarEntries.Insert(0, p_entry); + LimitEntries(); + OnEntriesUpdated?.Invoke(); + } + + static async Task RequestAvatarInfoTask(AvatarEntry p_entry) + { + BaseResponse l_baseResponse = await ApiConnection.MakeRequest(ApiConnection.ApiOperation.AvatarDetail, new { avatarID = p_entry.m_id }); + if(l_baseResponse != null) + { + if(!l_baseResponse.IsSuccessStatusCode) return; + p_entry.m_name = l_baseResponse.Data.Name; + p_entry.m_imageUrl = l_baseResponse.Data.ImageUrl; + p_entry.m_cached = true; + } + } + + // Settings + static void OnAutosaveTimeChanged(int p_value) + { + ms_lastTick = Environment.TickCount; + } + + // Utility + static void LimitEntries() + { + int l_currentLimit = Settings.AvatarsLimit; + if(ms_avatarEntries.Count > l_currentLimit) + ms_avatarEntries.RemoveRange(l_currentLimit, ms_avatarEntries.Count - (l_currentLimit - 1)); + } + } +} diff --git a/ml_pah/Main.cs b/ml_pah/Main.cs new file mode 100644 index 0000000..528c1f8 --- /dev/null +++ b/ml_pah/Main.cs @@ -0,0 +1,59 @@ +using ABI.CCK.Components; +using ABI_RC.Core; +using System; +using System.Collections; +using ABI_RC.Systems.GameEventSystem; + +namespace ml_pah +{ + public class PlayerAvatarHistory : MelonLoader.MelonMod + { + public override void OnInitializeMelon() + { + Settings.Init(); + HistoryManager.Initialize(); + ModUi.Initialize(); + MelonLoader.MelonCoroutines.Start(WaitForRootLogic()); + } + + public override void OnDeinitializeMelon() + { + CVRGameEventSystem.Avatar.OnLocalAvatarLoad.RemoveListener(this.OnLocalAvatarLoad); + HistoryManager.OnEntriesUpdated.RemoveListener(this.OnHistoryEntriesUpdated); + + ModUi.Shutdown(); + HistoryManager.Shutdown(); + } + + IEnumerator WaitForRootLogic() + { + while(RootLogic.Instance == null) + yield return null; + + CVRGameEventSystem.Avatar.OnLocalAvatarLoad.AddListener(this.OnLocalAvatarLoad); + HistoryManager.OnEntriesUpdated.AddListener(this.OnHistoryEntriesUpdated); + } + + public override void OnUpdate() + { + HistoryManager.Update(); + } + + // Game events + void OnLocalAvatarLoad(CVRAvatar p_avatar) + { + try + { + if((p_avatar.AssetInfo != null) && (p_avatar.AssetInfo.objectId.Length > 0)) + HistoryManager.AddEntry(p_avatar.AssetInfo.objectId); + } + catch(Exception e) + { + MelonLoader.MelonLogger.Error(e); + } + } + + // Mod events + void OnHistoryEntriesUpdated() => ModUi.UpdateAvatarsList(); + } +} diff --git a/ml_pah/ModUi.cs b/ml_pah/ModUi.cs new file mode 100644 index 0000000..a7aa3b9 --- /dev/null +++ b/ml_pah/ModUi.cs @@ -0,0 +1,131 @@ +using ABI_RC.Core.EventSystem; +using ABI_RC.Core.InteractionSystem; +using BTKUILib.UIObjects; +using BTKUILib.UIObjects.Components; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace ml_pah +{ + static class ModUi + { + readonly static string ms_namespace = typeof(ModUi).Namespace; + static bool ms_initialized = false; + + static Page ms_page = null; + + static Category ms_settingsCategory = null; + static Button ms_settingsClearButton = null; + static Button ms_settingSaveButton = null; + static SliderFloat ms_settingsEntriesLimit = null; + static SliderFloat ms_settingsAutosaveTime = null; + + static Category ms_buttonsCategory = null; + static readonly List