From 66a5ece48d7ac3d2e18024798460dd7efd88b8ff Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Tue, 1 Oct 2024 00:12:59 +0200 Subject: [PATCH] Handle character copies; Refactoring --- SVS_CheatTools/CheatToolsWindowInit.SVS.cs | 118 ++++++------- SVS_CheatTools/GameUtilities.cs | 190 +++++++++++++++++++++ SVS_CheatTools/Il2CppExtensions.cs | 2 +- SVS_CheatTools/TranslationHelper.cs | 3 +- 4 files changed, 242 insertions(+), 71 deletions(-) create mode 100644 SVS_CheatTools/GameUtilities.cs diff --git a/SVS_CheatTools/CheatToolsWindowInit.SVS.cs b/SVS_CheatTools/CheatToolsWindowInit.SVS.cs index 8e8503a..f8abf05 100644 --- a/SVS_CheatTools/CheatToolsWindowInit.SVS.cs +++ b/SVS_CheatTools/CheatToolsWindowInit.SVS.cs @@ -5,6 +5,7 @@ using Character; using HarmonyLib; using Il2CppInterop.Runtime.InteropTypes.Arrays; +using IllusionMods; using RuntimeUnityEditor.Core.Inspector; using RuntimeUnityEditor.Core.Inspector.Entries; using RuntimeUnityEditor.Core.ObjectTree; @@ -24,7 +25,7 @@ public static class CheatToolsWindowInit private static int _otherCharaListIndex; private static ImguiComboBox _otherCharaDropdown = new(); private static KeyValuePair[] _openInInspectorButtons; - private static Actor _currentVisibleChara; + private static Actor _currentVisibleChara, _currentVisibleCharaMain; private static bool InsideADV => ADV.ADVManager._instance?.IsADV == true; private static bool InsideH => SV.H.HScene.Active(); @@ -94,55 +95,6 @@ public static void Initialize(CheatToolsPlugin instance) Harmony.CreateAndPatchAll(typeof(Hooks)); } - private static string GetCharaName(this Actor chara, bool translated) - { - var fullname = chara?.charFile?.Parameter?.fullname; - if (!string.IsNullOrEmpty(fullname)) - { - if (translated) - { - TranslationHelper.TryTranslate(fullname, out var translatedName); - if (!string.IsNullOrEmpty(translatedName)) - return translatedName; - } - return fullname; - } - return chara?.chaCtrl?.name ?? chara?.ToString(); - } - - private static int GetActorId(this Actor currentAdvChara) - { - return Manager.Game.Charas.AsManagedEnumerable().Single(x => x.Value.Equals(currentAdvChara)).Key; - } - - private static IEnumerable> GetVisibleCharas() - { - if (InsideH) - { - // HScene.Actors contains copies of the actors. Couldn't find a better way to get the originals - return SV.H.HScene._instance.Actors.Select(GetMainActorInstance).Where(x => x.Value != null); - } - - var talkManager = Manager.TalkManager._instance; - if (talkManager != null && InsideADV) - { - return new List> - { - // PlayerHi and Npc1-4 contain copies of the Actors - GetMainActorInstance(talkManager.PlayerHi), - GetMainActorInstance(talkManager.Npc1), - GetMainActorInstance(talkManager.Npc2), - GetMainActorInstance(talkManager.Npc3), - GetMainActorInstance(talkManager.Npc4), - }.Where(x => x.Value != null); - } - - return Manager.Game.saveData.Charas.AsManagedEnumerable().Where(x => x.Value != null); - } - - private static KeyValuePair GetMainActorInstance(this SV.H.HActor x) => x?.Actor.GetMainActorInstance() ?? default; - private static KeyValuePair GetMainActorInstance(this Actor x) => x == null ? default : Manager.Game.Charas.AsManagedEnumerable().FirstOrDefault(y => x.charFile.About.dataID == y.Value.charFile.About.dataID); - private static void DrawHSceneCheats(CheatToolsWindow cheatToolsWindow) { var hScene = SV.H.HScene._instance; @@ -285,10 +237,15 @@ private static void DrawGirlCheatMenu(CheatToolsWindow cheatToolsWindow) { GUILayout.Label("Character status editor"); - foreach (var chara in GetVisibleCharas()) + foreach (var chara in GameUtilities.GetCurrentActors(false)) { - if (GUILayout.Button($"Select #{chara.Key} - {GetCharaName(chara.Value, true)}")) + var main = chara.Value.FindMainActorInstance(); + var isCopy = !ReferenceEquals(main.Value, chara.Value); + if (GUILayout.Button($"Select #{chara.Key} - {chara.Value.GetCharaName(true)}{(isCopy ? " (Copy)" : "")}")) + { _currentVisibleChara = chara.Value; + _currentVisibleCharaMain = isCopy ? main.Value : null; + } } GUILayout.Space(6); @@ -296,7 +253,7 @@ private static void DrawGirlCheatMenu(CheatToolsWindow cheatToolsWindow) try { if (_currentVisibleChara != null) - DrawSingleCharaCheats(_currentVisibleChara, cheatToolsWindow); + DrawSingleCharaCheats(_currentVisibleChara, _currentVisibleCharaMain, cheatToolsWindow); else GUILayout.Label("Select a character to edit their stats"); } @@ -307,9 +264,10 @@ private static void DrawGirlCheatMenu(CheatToolsWindow cheatToolsWindow) } } - private static void DrawSingleCharaCheats(Actor currentAdvChara, CheatToolsWindow cheatToolsWindow) + private static void DrawSingleCharaCheats(Actor currentAdvChara, Actor mainChara, CheatToolsWindow cheatToolsWindow) { var comboboxMaxY = (int)cheatToolsWindow.WindowRect.bottom - 30; + var isCopy = mainChara != null; GUILayout.BeginVertical(GUI.skin.box); { @@ -317,12 +275,29 @@ private static void DrawSingleCharaCheats(Actor currentAdvChara, CheatToolsWindo { GUILayout.Label("Selected:", IMGUIUtils.LayoutOptionsExpandWidthFalse); GUILayout.FlexibleSpace(); - GUILayout.Label(GetCharaName(currentAdvChara, true), IMGUIUtils.LayoutOptionsExpandWidthFalse); + GUILayout.Label(currentAdvChara.GetCharaName(true), IMGUIUtils.LayoutOptionsExpandWidthFalse); GUILayout.FlexibleSpace(); if (GUILayout.Button("Close", IMGUIUtils.LayoutOptionsExpandWidthFalse)) _currentVisibleChara = null; } GUILayout.EndHorizontal(); + if (isCopy) + { + GUILayout.BeginHorizontal(); + { + GUILayout.Label(new GUIContent("!! This character is a copy !!", null, "All changes made to this characters will be lost after the current scene finishes.\n\n" + + "If you want to make permanent changes, open the main instance of this character and do your changes there.\n" + + "You will have to exit and re-enter current scene to propagate the changes to the copied character)."), IMGUIUtils.LayoutOptionsExpandWidthFalse); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Open main")) + { + _currentVisibleChara = mainChara; + _currentVisibleCharaMain = null; + } + } + GUILayout.EndHorizontal(); + } + GUILayout.Space(6); var charasGameParam = currentAdvChara.charasGameParam; @@ -393,6 +368,11 @@ private static void DrawSingleCharaCheats(Actor currentAdvChara, CheatToolsWindo GUILayout.Space(6); GUILayout.BeginVertical(GUI.skin.box); + if (isCopy) + { + GUILayout.Label("Can't edit relationships of copied characters, open the main character first."); + } + else { // DarkSoldier27: Ok I figure it out: // 0:LOVE @@ -419,7 +399,7 @@ private static void DrawSingleCharaCheats(Actor currentAdvChara, CheatToolsWindo if (_otherCharaListIndex >= 0) { - _otherCharaListIndex = _otherCharaDropdown.Show(_otherCharaListIndex, targets.Select(x => new GUIContent(GetCharaName(x, true))).ToArray(), comboboxMaxY); + _otherCharaListIndex = _otherCharaDropdown.Show(_otherCharaListIndex, targets.Select(x => new GUIContent(x.GetCharaName(true))).ToArray(), comboboxMaxY); targets = new[] { targets[_otherCharaListIndex] }; } @@ -427,9 +407,9 @@ private static void DrawSingleCharaCheats(Actor currentAdvChara, CheatToolsWindo { // H Affinity controls var targetChara = targets[0]; - var targetCharaId = targetChara.GetActorId(); + var targetCharaId = targetChara.TryGetActorId(); var to = baseParameter.GetHAffinity(targetCharaId); - var currentCharaId = currentAdvChara.GetActorId(); + var currentCharaId = currentAdvChara.TryGetActorId(); var targetBaseParameter = targetChara.charasGameParam.baseParameter; var fro = targetBaseParameter.GetHAffinity(currentCharaId); @@ -455,18 +435,18 @@ private static void DrawSingleCharaCheats(Actor currentAdvChara, CheatToolsWindo GUILayout.Label("H Affinity with everyone: "); if (GUILayout.Button("Max lvl")) { - var targetIds = targets.Select(x => x.GetActorId()).ToArray(); + var targetIds = targets.Select(x => x.TryGetActorId()).ToArray(); foreach (var targetId in targetIds) baseParameter.AddHAffinity(targetId, 100); - var currentCharaId = currentAdvChara.GetActorId(); + var currentCharaId = currentAdvChara.TryGetActorId(); foreach (var target in targets) target.charasGameParam.baseParameter.AddHAffinity(currentCharaId, 100); } if (GUILayout.Button("Set to 0")) { - var targetIds = targets.Select(x => x.GetActorId()).ToArray(); + var targetIds = targets.Select(x => x.TryGetActorId()).ToArray(); foreach (var targetId in targetIds) baseParameter.RemoveHAffinity(targetId); - var currentCharaId = currentAdvChara.GetActorId(); + var currentCharaId = currentAdvChara.TryGetActorId(); foreach (var target in targets) target.charasGameParam.baseParameter.RemoveHAffinity(currentCharaId); } } @@ -519,10 +499,10 @@ private static void DrawSingleCharaCheats(Actor currentAdvChara, CheatToolsWindo } if (gameParam != null && GUILayout.Button("Inspect GameParameter")) - Inspector.Instance.Push(new InstanceStackEntry(gameParam, "GameParam " + GetCharaName(currentAdvChara, true)), true); + Inspector.Instance.Push(new InstanceStackEntry(gameParam, "GameParam " + currentAdvChara.GetCharaName(true)), true); if (charasGameParam != null && GUILayout.Button("Inspect CharactersGameParameter")) - Inspector.Instance.Push(new InstanceStackEntry(charasGameParam, "CharaGameParam " + GetCharaName(currentAdvChara, true)), true); + Inspector.Instance.Push(new InstanceStackEntry(charasGameParam, "CharaGameParam " + currentAdvChara.GetCharaName(true)), true); if (GUILayout.Button("Navigate to Character's GameObject")) { @@ -533,7 +513,7 @@ private static void DrawSingleCharaCheats(Actor currentAdvChara, CheatToolsWindo } if (GUILayout.Button("Open Character in inspector")) - Inspector.Instance.Push(new InstanceStackEntry(currentAdvChara, "Actor " + GetCharaName(currentAdvChara, true)), true); + Inspector.Instance.Push(new InstanceStackEntry(currentAdvChara, "Actor " + currentAdvChara.GetCharaName(true)), true); //if (GUILayout.Button("Inspect extended data")) //{ @@ -666,7 +646,7 @@ private static void DrawSingleRankEditor(SensitivityKind kind, Actor targetChara if (affectedCharas.Count == 1) { - var rank = targetCharaSensitivity.tableFavorabiliry[GetActorId(affectedCharas[0])].ranks[(int)kind]; + var rank = targetCharaSensitivity.tableFavorabiliry[affectedCharas[0].TryGetActorId()].ranks[(int)kind]; GUILayout.Label(((int)rank).ToString()); } @@ -677,7 +657,7 @@ private static void DrawSingleRankEditor(SensitivityKind kind, Actor targetChara if (affectedCharas.Count == 1) { - var rank = affectedCharas[0].charasGameParam.sensitivity.tableFavorabiliry[GetActorId(targetChara)].ranks[(int)kind]; + var rank = affectedCharas[0].charasGameParam.sensitivity.tableFavorabiliry[targetChara.TryGetActorId()].ranks[(int)kind]; GUILayout.Label(((int)rank).ToString()); } @@ -689,7 +669,7 @@ private static void DrawSingleRankEditor(SensitivityKind kind, Actor targetChara void OnOutgoing(int amount) { - var targetIds = affectedCharas.Select(GetActorId).ToArray(); + var targetIds = affectedCharas.Select(actor => actor.TryGetActorId()).ToArray(); foreach (var tabkvp in targetCharaSensitivity.tableFavorabiliry) { if (targetIds.Contains(tabkvp.Key)) @@ -706,7 +686,7 @@ void OnOutgoing(int amount) } void OnIncoming(int amount) { - var ourId = GetActorId(targetChara); + var ourId = targetChara.TryGetActorId(); foreach (var charaKvp in affectedCharas) { var otherSensitivity = charaKvp.charasGameParam.sensitivity; diff --git a/SVS_CheatTools/GameUtilities.cs b/SVS_CheatTools/GameUtilities.cs new file mode 100644 index 0000000..6e3a6de --- /dev/null +++ b/SVS_CheatTools/GameUtilities.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Character; +using SaveData; + +namespace IllusionMods; + +/// +/// TODO move to a separate API dll +/// +internal static class GameUtilities +{ + public const string GameProcessName = "SamabakeScramble"; + + /// + /// True in character maker, both in main menu and in-game maker. + /// + public static bool InsideMaker => CharacterCreation.HumanCustom.Initialized; + + /// + /// True if the game is running, e.g. a new game was started or a game was loaded. False in main menu, main menu character maker, etc. + /// + public static bool InsideGame => Manager.Game.saveData.WorldTime > 0; + + /// + /// True if an H Scene is currently playing. + /// + public static bool InsideHScene => SV.H.HScene.Active(); + + /// + /// True if an ADV Scene is currently playing (both when talking with a VN text box at the bottom, and when the right-side conversation menu is shown). + /// + public static bool InsideADVScene => ADV.ADVManager.Initialized && ADV.ADVManager.Instance.IsADV; + + /// + /// Get a display name of the character. Only use in interface, not for keeping track of the character. + /// If is true and AutoTranslator is active, try to get a translated version of the name in current language. Otherwise, return the original name. + /// + public static string GetCharaName(this Actor chara, bool translated) + { + var fullname = chara?.charFile?.Parameter.GetCharaName(translated); + if (!string.IsNullOrEmpty(fullname)) + { + if (translated) + { + TranslationHelper.TryTranslate(fullname, out var translatedName); + if (!string.IsNullOrEmpty(translatedName)) + return translatedName; + } + return fullname; + } + return chara?.chaCtrl?.name ?? chara?.ToString(); + } + + /// + /// Get a display name of the character. Only use in interface, not for keeping track of the character. + /// If is true and AutoTranslator is active, try to get a translated version of the name in current language. Otherwise, return the original name. + /// + public static string GetCharaName(this HumanDataParameter param, bool translated) + { + var fullname = param?.fullname; + if (!string.IsNullOrEmpty(fullname)) + { + if (translated) + { + TranslationHelper.TryTranslate(fullname, out var translatedName); + if (!string.IsNullOrEmpty(translatedName)) + return translatedName; + } + return fullname; + } + return ""; + } + + + /// + /// Get ID of this character in the main character list (in save data). Returns -1 if the character is a copy, or if it is not saved to the save data. + /// + public static int TryGetActorId(this Actor currentAdvChara) + { + if (currentAdvChara == null) throw new ArgumentNullException(nameof(currentAdvChara)); + + var found = Manager.Game.Charas.AsManagedEnumerable().FirstOrDefault(x => currentAdvChara.Equals(x.Value)); + return found.Value != null ? found.Key : -1; + } + + /// + /// Get ID of this character (or ID of the original instance of this character copy) in the main character list (in save data). Returns -1 if the character is not on the main game map and is not saved to the save data. + /// + public static int FindMainActorId(this Actor currentAdvChara) + { + if (currentAdvChara == null) throw new ArgumentNullException(nameof(currentAdvChara)); + + var mainActorInstance = currentAdvChara.FindMainActorInstance(); + return mainActorInstance.Value != null ? mainActorInstance.Key : -1; + } + + /// + /// Get Humans involved in the current scene. + /// If is true, the original overworld characters are returned (which are saved to the save file; if not found the character is not included in the result). + /// If is false, the actors in the current scene are returned (which are copies of the original characters in H and ADV scenes; in maker nothing is returned since there is no actor). + /// + public static IEnumerable GetCurrentHumans(bool mainInstances) + { + if (InsideMaker) + { + var maker = CharacterCreation.HumanCustom.Instance; + if (!mainInstances) + return new[] { maker.Human }; + + var result = maker.HumanData.About.FindMainActorInstance().Value?.chaCtrl; + return result != null ? new[] { result } : Enumerable.Empty(); + } + + return GetCurrentActors(mainInstances).Select(x => x.Value.chaCtrl).Where(x => x != null); + } + + /// + /// Get actors involved in the current scene and their IDs. + /// If is true, the original overworld characters are returned with their save data IDs (the characters that are saved to the save file; if not found the character is not included in the result). + /// If is false, the actors in the current scene are returned with their relative IDs (which are copies of the original characters in H and ADV scenes; in maker nothing is returned since there is no actor). + /// + public static IEnumerable> GetCurrentActors(bool mainInstances) + { + if (InsideMaker) + { + if (mainInstances) + { + var actor = CharacterCreation.HumanCustom.Instance.HumanData.About.FindMainActorInstance(); + if (actor.Value != null) + return new[] { actor }; + } + + return Enumerable.Empty>(); + } + + if (SV.H.HScene.Active()) + { + // HScene.Actors contains copies of the actors + if (mainInstances) + return SV.H.HScene._instance.Actors.Select(FindMainActorInstance).Where(x => x.Value != null); + else + return SV.H.HScene._instance.Actors.Select((ha, i) => new KeyValuePair(i, ha.Actor)).Where(x => x.Value != null); + } + + var talkManager = Manager.TalkManager._instance; + if (talkManager != null && ADV.ADVManager._instance?.IsADV == true) + { + var npcs = new List> + { + // PlayerHi and Npc1-4 contain copies of the Actors + new(0,talkManager.Npc1), + new(1,talkManager.Npc2), + new(2,talkManager.Npc3), + new(3,talkManager.Npc4), + new(4,talkManager.PlayerHi), + }.AsEnumerable(); + if (mainInstances) + npcs = npcs.Select(pair => pair.Value.FindMainActorInstance()); + return npcs.Where(x => x.Value != null); + } + + return GetMainActors(); + } + + /// + /// Get all overworld characters together with their save data IDs (the characters that are saved to the save file). + /// + public static IEnumerable> GetMainActors() + { + return Manager.Game.saveData.Charas.AsManagedEnumerable().Where(x => x.Value != null); + } + + /// + /// Get the main character instance of the actor (the one that is visible on the main map and saved to the save file). + /// + public static KeyValuePair FindMainActorInstance(this SV.H.HActor x) => x?.Actor.FindMainActorInstance() ?? default; + + /// + /// Get the main character instance of the actor (the one that is visible on the main map and saved to the save file). + /// + public static KeyValuePair FindMainActorInstance(this Actor x) => x?.charFile.About.FindMainActorInstance() ?? default; + + /// + /// Get the main character instance of the actor (the one that is visible on the main map and saved to the save file). + /// TODO: Find a better way to get the originals + /// + public static KeyValuePair FindMainActorInstance(this HumanDataAbout x) => x == null ? default : Manager.Game.Charas.AsManagedEnumerable().FirstOrDefault(y => x.dataID == y.Value.charFile.About.dataID); +} diff --git a/SVS_CheatTools/Il2CppExtensions.cs b/SVS_CheatTools/Il2CppExtensions.cs index 14389c1..0a331ed 100644 --- a/SVS_CheatTools/Il2CppExtensions.cs +++ b/SVS_CheatTools/Il2CppExtensions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace CheatTools +namespace IllusionMods { internal static class Il2CppExtensions { diff --git a/SVS_CheatTools/TranslationHelper.cs b/SVS_CheatTools/TranslationHelper.cs index 74a5d18..f4f73ee 100644 --- a/SVS_CheatTools/TranslationHelper.cs +++ b/SVS_CheatTools/TranslationHelper.cs @@ -1,8 +1,9 @@ using System; using System.Linq; +using CheatTools; using XUnity.AutoTranslator.Plugin.Core; -namespace CheatTools +namespace IllusionMods { /// /// Class that abstracts away AutoTranslator. It lets you translate text to current language.