├── linker.xml ├── src ├── SkipAllRefs.targets ├── AnimationLoader.KoikatsuSunshine │ ├── Plugin.cs │ ├── AnimationLoaderGameController.cs │ ├── AnimationLoader.KoikatsuSunshine.csproj │ ├── GlobalSuppressions.cs │ ├── UsedAnimations.cs │ ├── LoadXML.Overrides.cs │ └── Hooks.LoadMotionList.cs ├── AnimationLoader.Koikatu │ ├── Plugin.cs │ ├── AnimationLoaderGameController.cs │ ├── GlobalSuppressions.cs │ ├── AnimationLoader.Koikatu.csproj │ └── Hooks.LoadMotionList.cs ├── BuildSettings.Common.props └── AnimationLoader.Core │ ├── Properties │ └── AssemblyInfo.cs │ ├── AnimatorOverrideController.cs │ ├── AnimationLoader.Core.shproj │ ├── Plugin.Info.cs │ ├── Utils │ ├── TextScroll.cs │ ├── Log.cs │ ├── AnimationUsage.cs │ ├── Extensions.cs │ └── Utilities.cs │ ├── AnimationLoader.Core.projitems │ ├── FootJobAnimations.cs │ ├── LoadXML.VersionChecks.cs │ ├── Fixes │ ├── Move.Controller.cs │ └── Move.AnimationInfo.cs │ ├── Hooks.cs │ ├── LoadXML.cs │ ├── Plugin.cs │ ├── SwapAnimationInfo.cs │ ├── LoadXML.ProcessArray.cs │ ├── AnimationsNames.cs │ ├── Plugin.ConfigEntries.cs │ ├── Hooks.Animators.cs │ ├── AnimationClipsCache.cs │ └── Hooks.AnimationLists.cs ├── nuget.config ├── AnimationLoader.sln ├── README.md ├── CHANGELOG.md ├── .gitignore └── Manifest.md /linker.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/SkipAllRefs.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/AnimationLoader.KoikatsuSunshine/Plugin.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Entry point for AnimationLoader.KoikatsuSunshine 3 | // 4 | using BepInEx; 5 | 6 | using KKAPI; 7 | 8 | 9 | namespace AnimationLoader 10 | { 11 | [BepInDependency(Sideloader.Sideloader.GUID, Sideloader.Sideloader.Version)] 12 | [BepInDependency(KoikatuAPI.GUID, KoikatuAPI.VersionConst)] 13 | [BepInPlugin(GUID, PluginDisplayName, Version)] 14 | [BepInProcess(KoikatuAPI.GameProcessName)] 15 | [BepInProcess(KoikatuAPI.StudioProcessName)] 16 | [BepInProcess(KoikatuAPI.VRProcessName)] 17 | public partial class SwapAnim : BaseUnityPlugin 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/AnimationLoader.Koikatu/Plugin.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Entry point for AnimationLoader.Koikatu 3 | // 4 | using BepInEx; 5 | 6 | using KKAPI; 7 | 8 | 9 | namespace AnimationLoader 10 | { 11 | [BepInDependency(KoikatuAPI.GUID, KoikatuAPI.VersionConst)] 12 | [BepInDependency(Sideloader.Sideloader.GUID, Sideloader.Sideloader.Version)] 13 | [BepInProcess(KoikatuAPI.GameProcessName)] 14 | [BepInProcess(KoikatuAPI.GameProcessNameSteam)] 15 | [BepInProcess(KoikatuAPI.StudioProcessName)] 16 | [BepInProcess(KoikatuAPI.VRProcessName)] 17 | [BepInProcess(KoikatuAPI.VRProcessNameSteam)] 18 | [BepInPlugin(GUID, PluginDisplayName, Version)] 19 | public partial class SwapAnim : BaseUnityPlugin 20 | { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/BuildSettings.Common.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | embedded 5 | false 6 | false 7 | $(SolutionDir)bin\$(Configuration) 8 | true 9 | latest 10 | latest 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("https://gitgoon.dev/IllusionMods/AnimationLoader")] 9 | [assembly: AssemblyCopyright("Copyright ©IllusionMods 2021-2022")] 10 | [assembly: AssemblyTrademark("")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | -------------------------------------------------------------------------------- /src/AnimationLoader.Koikatu/AnimationLoaderGameController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using UnityEngine; 4 | 5 | using KKAPI.MainGame; 6 | 7 | using static AnimationLoader.SwapAnim; 8 | 9 | namespace AnimationLoader 10 | { 11 | /// 12 | /// Save use animations. 13 | /// 14 | internal class AnimationLoaderGameController : GameCustomFunctionController 15 | { 16 | protected override void OnEndH(MonoBehaviour proc, HFlag flags, bool vr) 17 | { 18 | if (flags.isFreeH) 19 | { 20 | return; 21 | } 22 | 23 | try 24 | { 25 | _animationsUseStats.Save(); 26 | } 27 | catch (Exception ex) 28 | { 29 | Log.Error($"0033: Error saving used animations - {ex}"); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/AnimationLoader.KoikatsuSunshine/AnimationLoaderGameController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using UnityEngine; 4 | 5 | using KKAPI.MainGame; 6 | 7 | using static AnimationLoader.SwapAnim; 8 | 9 | namespace AnimationLoader 10 | { 11 | /// 12 | /// Save use animations. 13 | /// 14 | internal class AnimationLoaderGameController : GameCustomFunctionController 15 | { 16 | protected override void OnEndH(MonoBehaviour proc, HFlag flags, bool vr) 17 | { 18 | if (flags.isFreeH) 19 | { 20 | return; 21 | } 22 | 23 | try 24 | { 25 | _usedAnimations.Save(); 26 | _animationsUseStats.Save(); 27 | } 28 | catch (Exception ex) 29 | { 30 | Log.Error($"0033: Error saving used animations - {ex}"); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/AnimatorOverrideController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | using UnityEngine; 4 | 5 | namespace AnimationLoader 6 | { 7 | public partial class SwapAnim 8 | { 9 | private static AnimatorOverrideController SetupAnimatorOverrideController( 10 | RuntimeAnimatorController src, 11 | RuntimeAnimatorController over) 12 | { 13 | if (src == null || over == null) 14 | { 15 | return null; 16 | } 17 | 18 | var aoc = new AnimatorOverrideController(src); 19 | var target = new AnimatorOverrideController(over); 20 | foreach (var ac in src.animationClips.Where(x => x != null)) //thanks omega/katarsys 21 | { 22 | aoc[ac.name] = ac; 23 | } 24 | 25 | foreach (var ac in target.animationClips.Where(x => x != null)) //thanks omega/katarsys 26 | { 27 | aoc[ac.name] = ac; 28 | } 29 | 30 | aoc.name = over.name; 31 | return aoc; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/AnimationLoader.Core.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | e48d1a5f-a706-4293-88bb-d82bd68cdfed 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Plugin.Info.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | using AnimationLoader; 4 | 5 | #region Assembly attributes 6 | 7 | /* 8 | * These attributes define various meta-information of the generated DLL. 9 | * In general, you don't need to touch these. Instead, edit the values in Info. 10 | */ 11 | [assembly: AssemblyTitle(SwapAnim.PluginName + " (" + SwapAnim.GUID + ")")] 12 | [assembly: AssemblyProduct(SwapAnim.PluginName)] 13 | [assembly: AssemblyVersion(SwapAnim.Version)] 14 | [assembly: AssemblyFileVersion(SwapAnim.Version)] 15 | 16 | #endregion Assembly attributes 17 | 18 | 19 | namespace AnimationLoader 20 | { 21 | public partial class SwapAnim 22 | { 23 | public const string GUID = "essuhauled.animationloader"; 24 | #if DEBUG 25 | public const string PluginDisplayName = "Animation Loader (Debug)"; 26 | #else 27 | public const string PluginDisplayName = "Animation Loader"; 28 | #endif 29 | public const string Version = "1.1.3.4"; 30 | #if KK 31 | public const string PluginName = "AnimationLoader.Koikatu"; 32 | #else 33 | public const string PluginName = "AnimationLoader.KoikatsuSunshine"; 34 | #endif 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Utils/TextScroll.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using TMPro; 4 | using UnityEngine; 5 | using UnityEngine.EventSystems; 6 | 7 | 8 | namespace AnimationLoader 9 | { 10 | public class TextScroll : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler 11 | { 12 | public RectTransform transBase; 13 | public TextMeshProUGUI textMesh; 14 | public float speed = 70f; 15 | private bool move; 16 | 17 | public void OnPointerEnter(PointerEventData eventData) 18 | { 19 | move = true; 20 | StartCoroutine(MoveText()); 21 | } 22 | 23 | public void OnPointerExit(PointerEventData eventData) 24 | { 25 | move = false; 26 | MarginSet(0f); 27 | } 28 | 29 | private IEnumerator MoveText() 30 | { 31 | while(move) 32 | { 33 | if (textMesh.preferredWidth - Math.Abs(textMesh.margin.x) 34 | < transBase.sizeDelta.x * 0.5f) 35 | { 36 | MarginSet(transBase.sizeDelta.x * 0.5f); 37 | } 38 | else 39 | { 40 | MarginAdd(-speed * Time.deltaTime); 41 | } 42 | yield return null; 43 | } 44 | } 45 | 46 | private void MarginAdd(float value) 47 | { 48 | var margin = textMesh.margin; 49 | margin.x += value; 50 | textMesh.margin = margin; 51 | } 52 | 53 | private void MarginSet(float value) 54 | { 55 | var margin = textMesh.margin; 56 | margin.x = value; 57 | textMesh.margin = margin; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/AnimationLoader.Koikatu/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Style", "IDE0022:Use block body for methods", Justification = "Style use by community", Scope = "member", Target = "~M:AnimationLoader.SwapAnim.GetMoveController(ChaControl)~AnimationLoader.SwapAnim.MoveController")] 9 | //[assembly: SuppressMessage("Style", "IDE0022:Use block body for methods", Justification = "Style use by community", Scope = "member", Target = "~M:AnimationLoader.SwapAnim.MoveController.SetOriginalPosition")] 10 | [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~M:AnimationLoader.SwapAnim.ReadNames(AnimationLoader.SwapAnim.Names@,System.String)")] 11 | [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~M:AnimationLoader.SwapAnim.SaveNamesXmls")] 12 | [assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Variable modified in code", Scope = "member", Target = "~F:AnimationLoader.SwapAnim.Hooks._animationClipsCache")] 13 | [assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Variable modified in code", Scope = "member", Target = "~F:AnimationLoader.SwapAnim.Hooks._animationClipsByType")] 14 | [assembly: SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "It is used", Scope = "member", Target = "~F:AnimationLoader.SwapAnim._specialAnimation")] 15 | -------------------------------------------------------------------------------- /src/AnimationLoader.Koikatu/AnimationLoader.Koikatu.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | net35 9 | AnimationLoader 10 | $(MSBuildProjectName) 11 | 12 | 13 | 14 | TRACE;DEBUG;KK 15 | 16 | 17 | 18 | TRACE;KK 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | all 32 | runtime; build; native; contentfiles; analyzers; buildtransitive 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/AnimationLoader.KoikatsuSunshine/AnimationLoader.KoikatsuSunshine.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | net46 9 | AnimationLoader 10 | $(MSBuildProjectName) 11 | warnings 12 | 13 | 14 | 15 | TRACE;DEBUG;KKS 16 | 17 | 18 | 19 | TRACE;KKS 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | all 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/AnimationLoader.KoikatsuSunshine/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | //[assembly: SuppressMessage("Style", "IDE0022:Use block body for methods", Justification = "Community usage", Scope = "member", Target = "~M:AnimationLoader.SwapAnim.MoveController.SetOriginalPosition")] 9 | [assembly: SuppressMessage("Style", "IDE0022:Use block body for methods", Justification = "Community usage", Scope = "member", Target = "~M:AnimationLoader.SwapAnim.GetMoveController(ChaControl)~AnimationLoader.SwapAnim.MoveController")] 10 | [assembly: SuppressMessage("Performance", "CA1825:Avoid zero-length array allocations", Justification = "KK does not have Array.Empty", Scope = "member", Target = "~F:AnimationLoader.OverrideInfo.categories")] 11 | [assembly: SuppressMessage("Performance", "CA1825:Avoid zero-length array allocations", Justification = "KK does not have Array.Empty", Scope = "member", Target = "~F:AnimationLoader.SwapAnimationInfo.categories")] 12 | [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Leave for reference", Scope = "member", Target = "~M:AnimationLoader.SwapAnim.SaveNamesXmls")] 13 | [assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Leave for reference", Scope = "member", Target = "~M:AnimationLoader.SwapAnim.ReadNames(AnimationLoader.SwapAnim.Names@,System.String)")] 14 | [assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "", Scope = "member", Target = "~F:AnimationLoader.SwapAnim.Hooks._animationClipsCache")] 15 | [assembly: SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "", Scope = "member", Target = "~F:AnimationLoader.SwapAnim.Hooks._animationClipsByType")] 16 | [assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "", Scope = "member", Target = "~M:AnimationLoader.SwapAnim.Hooks.LoadAddTaiiPostfix(System.Object,System.Collections.Generic.List{AddTaiiData.Param})")] 17 | -------------------------------------------------------------------------------- /src/AnimationLoader.KoikatsuSunshine/UsedAnimations.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Save key of used animations 3 | // 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Xml.Serialization; 10 | 11 | 12 | namespace AnimationLoader 13 | { 14 | [XmlRoot("Animations")] 15 | [Serializable] 16 | public class UsedAnimations 17 | { 18 | [XmlElement] 19 | public HashSet Keys = []; 20 | 21 | private static readonly string _path = Path.Combine(UserData.Path, "save"); 22 | private static readonly string _fileName = $"{_path}/animations.xml"; 23 | private static readonly XmlSerializer _xmlSerializer = new(typeof(UsedAnimations)); 24 | private static readonly FileInfo _fileInfo = new(_fileName); 25 | 26 | public override string ToString() 27 | { 28 | var sb = new StringBuilder(); 29 | var total = Keys.Count; 30 | var count = 0; 31 | 32 | foreach (var animation in Keys.OrderBy(x => x)) 33 | { 34 | count++; 35 | if (count == total) 36 | { 37 | sb.Append(animation); 38 | } 39 | else 40 | { 41 | sb.Append($"{animation}, "); 42 | } 43 | } 44 | return "{ " + sb.ToString() + " }"; 45 | } 46 | 47 | public void Save() 48 | { 49 | _fileInfo.Directory.Create(); 50 | StreamWriter writer = new(_fileName); 51 | _xmlSerializer.Serialize(writer.BaseStream, this); 52 | writer.Close(); 53 | } 54 | 55 | public void Read() 56 | { 57 | if (_fileInfo.Exists) 58 | { 59 | StreamReader reader = new(_fileName); 60 | var tmp = (UsedAnimations)_xmlSerializer.Deserialize(reader.BaseStream); 61 | reader.Close(); 62 | // This can be removed later for some reason was using a List instead of 63 | // a HashSet Removing duplicates. 64 | foreach (var e in tmp.Keys) 65 | { 66 | Keys.Add(e); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/AnimationLoader.Core.projitems: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | e48d1a5f-a706-4293-88bb-d82bd68cdfed 7 | 8 | 9 | AnimationLoader.Core 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /AnimationLoader.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31919.166 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6B1C4EAA-7ADD-47A9-AC1E-004D83C9BC6C}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | .gitignore = .gitignore 10 | src\BuildSettings.Common.props = src\BuildSettings.Common.props 11 | CHANGELOG.md = CHANGELOG.md 12 | Manifest.md = Manifest.md 13 | nuget.config = nuget.config 14 | README.md = README.md 15 | src\SkipAllRefs.targets = src\SkipAllRefs.targets 16 | template.xml = template.xml 17 | EndProjectSection 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnimationLoader.KoikatsuSunshine", "src\AnimationLoader.KoikatsuSunshine\AnimationLoader.KoikatsuSunshine.csproj", "{269D192E-9499-429F-A07F-C8B7E13DC861}" 20 | EndProject 21 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "AnimationLoader.Core", "src\AnimationLoader.Core\AnimationLoader.Core.shproj", "{E48D1A5F-A706-4293-88BB-D82BD68CDFED}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnimationLoader.Koikatu", "src\AnimationLoader.Koikatu\AnimationLoader.Koikatu.csproj", "{3EF70928-3925-4F6C-96E9-0EF3B9228E0D}" 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Release|Any CPU = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {269D192E-9499-429F-A07F-C8B7E13DC861}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {269D192E-9499-429F-A07F-C8B7E13DC861}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {269D192E-9499-429F-A07F-C8B7E13DC861}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {269D192E-9499-429F-A07F-C8B7E13DC861}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {3EF70928-3925-4F6C-96E9-0EF3B9228E0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {3EF70928-3925-4F6C-96E9-0EF3B9228E0D}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {3EF70928-3925-4F6C-96E9-0EF3B9228E0D}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {3EF70928-3925-4F6C-96E9-0EF3B9228E0D}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(SolutionProperties) = preSolution 41 | HideSolutionNode = FALSE 42 | EndGlobalSection 43 | GlobalSection(ExtensibilityGlobals) = postSolution 44 | SolutionGuid = {F34D347D-7842-4922-A372-47ECD316CD6E} 45 | EndGlobalSection 46 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 47 | src\AnimationLoader.Core\AnimationLoader.Core.projitems*{269d192e-9499-429f-a07f-c8b7e13dc861}*SharedItemsImports = 5 48 | src\AnimationLoader.Core\AnimationLoader.Core.projitems*{3ef70928-3925-4f6c-96e9-0ef3b9228e0d}*SharedItemsImports = 5 49 | src\AnimationLoader.Core\AnimationLoader.Core.projitems*{e48d1a5f-a706-4293-88bb-d82bd68cdfed}*SharedItemsImports = 13 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Utils/Log.cs: -------------------------------------------------------------------------------- 1 | // 2 | // class to control the display of logs 3 | // Missing rotating logs :( have to do something about that 4 | // 5 | using BepInEx.Logging; 6 | 7 | 8 | /// 9 | /// Show logs when enabled 10 | /// 11 | internal static class Log 12 | { 13 | private static ManualLogSource _logSource; 14 | private static bool _enabled = false; 15 | private static bool _debugToConsole = false; 16 | 17 | public static bool Enabled { 18 | get 19 | { 20 | return _enabled; 21 | } 22 | 23 | set 24 | { 25 | _enabled = value; 26 | } 27 | } 28 | 29 | public static bool DebugToConsole { 30 | get 31 | { 32 | return _debugToConsole; 33 | } 34 | set 35 | { 36 | _debugToConsole = value; 37 | } 38 | } 39 | 40 | public static ManualLogSource LogSource { 41 | get 42 | { 43 | return _logSource; 44 | } 45 | set 46 | { 47 | _logSource = value; 48 | } 49 | } 50 | 51 | public static void SetLogSource(ManualLogSource logSource) 52 | { 53 | _logSource = logSource; 54 | } 55 | 56 | public static void Info(object data) 57 | { 58 | if (_enabled) 59 | { 60 | _logSource.LogInfo(data); 61 | } 62 | } 63 | 64 | public static void Debug(object data) 65 | { 66 | if (_enabled) 67 | { 68 | if (_debugToConsole) 69 | { 70 | _logSource.LogInfo(data); 71 | } 72 | else 73 | { 74 | _logSource.LogDebug(data); 75 | } 76 | } 77 | } 78 | 79 | public static void Error(object data) 80 | { 81 | if (_enabled) 82 | { 83 | if (_debugToConsole) 84 | { 85 | _logSource.LogError(data); 86 | } 87 | else 88 | { 89 | _logSource.LogError(data); 90 | } 91 | } 92 | } 93 | 94 | public static void Fatal(object data) 95 | { 96 | if (_enabled) 97 | { 98 | _logSource.LogFatal(data); 99 | } 100 | } 101 | 102 | public static void Message(object data) 103 | { 104 | if (_enabled) 105 | { 106 | _logSource.LogMessage(data); 107 | } 108 | } 109 | 110 | public static void Warning(object data) 111 | { 112 | if (_enabled) 113 | { 114 | _logSource.LogWarning(data); 115 | } 116 | } 117 | 118 | /// 119 | /// For logs that should not depend on the logs been enabled 120 | /// 121 | /// 122 | /// 123 | public static void Level(LogLevel level, object data) 124 | { 125 | _logSource.Log(level, data); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/FootJobAnimations.cs: -------------------------------------------------------------------------------- 1 | // 2 | // FootJobAnimations.cs 3 | // 4 | 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Runtime.Serialization; 9 | using System.Text; 10 | 11 | using BepInEx; 12 | 13 | namespace AnimationLoader 14 | { 15 | public partial class SwapAnim 16 | { 17 | internal static FootJobAnimations _footJobAnimations = []; 18 | 19 | [DataContract(Name = "FootJobAnimations", Namespace = "https://gitgoon.dev/IllusionMods/AnimationLoader")] 20 | public class FootJobAnimations : IEnumerable 21 | { 22 | [DataMember] 23 | private HashSet Animations; 24 | 25 | private static readonly string _path = Path.Combine(Paths.ConfigPath, "AnimationLoader/FootJob"); 26 | private static readonly string _fileName = $"{_path}/FootJobAnimations.xml"; 27 | private static readonly DataContractSerializer _serializer = new(typeof(FootJobAnimations)); 28 | private static readonly FileInfo _fileInfo = new(_fileName); 29 | 30 | public int Count => Animations.Count; 31 | 32 | public FootJobAnimations() 33 | { 34 | Animations = []; 35 | Animations.Clear(); 36 | } 37 | 38 | public IEnumerator GetEnumerator() 39 | { 40 | return Animations.GetEnumerator(); 41 | } 42 | 43 | public void Add(string item) 44 | { 45 | Animations.Add(item); 46 | } 47 | 48 | public void Clear() 49 | { 50 | Animations.Clear(); 51 | } 52 | 53 | public bool Contains(string item) 54 | { 55 | return Animations.Contains(item); 56 | } 57 | 58 | public void Save() 59 | { 60 | _fileInfo.Directory.Create(); 61 | var writer = new FileStream(_fileName, FileMode.Create, FileAccess.Write); 62 | _serializer.WriteObject(writer, this); 63 | writer.Close(); 64 | } 65 | 66 | public void Read() 67 | { 68 | if (_fileInfo.Exists) 69 | { 70 | using var fileStream = System.IO.File.Open(_fileName, FileMode.Open, FileAccess.Read); 71 | var tmp = _serializer.ReadObject(fileStream) as FootJobAnimations; 72 | fileStream.Close(); 73 | 74 | Animations = tmp?.Animations; 75 | } 76 | else 77 | { 78 | Log.Error("[FootJobAnimations.Read] File not found."); 79 | } 80 | #if DEBUG 81 | var logLines = new StringBuilder(); 82 | 83 | foreach (var fj in _footJobAnimations) 84 | { 85 | logLines.AppendLine(fj.ToString()); 86 | } 87 | Log.Debug($"0012: Foot jobs.\n{logLines}"); 88 | #endif 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Utils/AnimationUsage.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Save key of used animations 3 | // 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.IO; 8 | using System.Runtime.Serialization; 9 | using System.Text; 10 | 11 | using static AnimationLoader.SwapAnim; 12 | 13 | namespace AnimationLoader 14 | { 15 | [DataContract(Name = "AnimationsUsage", Namespace = "https://gitgoon.dev/IllusionMods/AnimationLoader")] 16 | public class AnimationsUseStats 17 | { 18 | [DataMember] 19 | public Dictionary Stats { set; get; } 20 | 21 | private static readonly string _path = Path.Combine(UserData.Path, "AnimationLoader/Usage"); 22 | private static readonly string _fileName = $"{_path}/AnimationsUsage.xml"; 23 | private static readonly DataContractSerializer _serializer = new(typeof(AnimationsUseStats)); 24 | private static readonly FileInfo _fileInfo = new(_fileName); 25 | 26 | public int this[string key] 27 | { 28 | get { return Stats[key]; } 29 | set { Stats[key] = value; } 30 | } 31 | 32 | public int Count => Stats.Count; 33 | 34 | public AnimationsUseStats() 35 | { 36 | Stats = []; 37 | Stats.Clear(); 38 | } 39 | 40 | public bool TryGetValue( 41 | string key, 42 | out int value) 43 | { 44 | return Stats.TryGetValue(key, out value); 45 | } 46 | 47 | public void Init(List[] lstAnimInfo) 48 | { 49 | foreach (var c in lstAnimInfo) 50 | { 51 | foreach (var a in c) 52 | { 53 | var key = GetAnimationKey(a); 54 | if (!Stats.ContainsKey(key)) 55 | { 56 | Stats.Add(key, 0); 57 | } 58 | } 59 | } 60 | } 61 | 62 | public override string ToString() 63 | { 64 | var animations = new StringBuilder(); 65 | 66 | foreach (var e in Stats) 67 | { 68 | animations.AppendLine($"Key={e.Key} Value={e.Value}"); 69 | } 70 | 71 | return animations.ToString(); 72 | } 73 | 74 | public void Save() 75 | { 76 | _fileInfo.Directory.Create(); 77 | var writer = new FileStream(_fileName, FileMode.Create, FileAccess.Write); 78 | _serializer.WriteObject(writer, this); 79 | writer.Close(); 80 | } 81 | 82 | public void Read() 83 | { 84 | if (_fileInfo.Exists) 85 | { 86 | using var fileStream = File.Open(_fileName, FileMode.Open, FileAccess.Read); 87 | var tmp = _serializer.ReadObject(fileStream) as AnimationsUseStats; 88 | fileStream.Close(); 89 | 90 | Stats = tmp?.Stats; 91 | } 92 | } 93 | 94 | public IOrderedEnumerable> Sorted() 95 | { 96 | var sortedByUsage = Stats 97 | .OrderByDescending(u => u.Value) 98 | .ThenBy(n => n.Key); 99 | 100 | return sortedByUsage; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnimationLoader 2 | A plugin for loading animations from Sideloader zipmods. 3 | Thank you Essu for the [main bulk of the code](https://gitgoon.dev/IllusionMods/AnimationLoader/commit/402c02af3bbb5a6e1b3015bd0caa3f0a7db618fc) 4 | See the [template](template.xml) for how to configure animations in your own mod. 5 | 6 | ## How to install 7 | 1. Install the latest build of [BepInEx](https://github.com/BepInEx/BepInEx/releases) 8 | 2. Install IllusionModdingAPI 1.39 and BepisPlugins 20.0 (or newer) 9 | 4. Download the latest release from [the releases page](../../releases) 10 | 4. Drop the dll to `bepinex/plugins` 11 | 5. Add animation packages to the mods folder. (These can usually be downloaded with KKManager) 12 | 13 | ## 14 | The manifest.xml was extended from version **1.0.8**. 15 | 16 | 1. **PositionHeroine** and **PositionPlayer** vectors (x, y, z) that represent: 17 | ```xml 18 | 19 | 0 20 | 0 21 | 0 22 | 23 | 24 | 0 25 | 0 26 | 0 27 | 28 | ``` 29 | x is left and right movement (red axis) 30 | y is up and down (green axis) 31 | z is forward and backwards (blue axis) 32 | The values represent a factor by which a normalized vector in the direction of the axes is 33 | multiplied and the result added to the axis. The magnitude of normalized vector is one. The scale 34 | a good guess could be a meter. If there is a need to move half a unit the factor for that axis 35 | would be 0.5 for example. 36 | 37 | 2. **GameSpecificOverrides** - the manifest.xml is one for both KK and KKS by using this section 38 | it can be fine tuned for both games. 39 | ```xml 40 | 41 | 1 42 | Animation 1 43 | 0 44 | 45 | 46 | 55 47 | 48 | 49 | 50 | ``` 51 | Here the NeckDonorId when KK reads the configuration will be 0 but when KKS reads the configuration 52 | will be 55. 53 | 54 | 3. **Game specific animations** 55 | ```xml 56 | 57 | 58 | 1 59 | Animation 1 60 | 0 61 | 62 | 63 | 64 | 2 65 | Animation 1 66 | 0 67 | 68 | 69 | ``` 70 | When KKS reads this manifest the animation with StudioId 1 will be ignored. Koikatu will read both 71 | StudioId 1 and 2. 72 | 73 | 4. **Game experience levels and animations** 74 | ```xml 75 | 76 | 1 77 | Animation 1 78 | 0 79 | 80 | 81 | 55 82 | 50 83 | 84 | 85 | 86 | ``` 87 | For KKS the animations are available gradually one of the factors is the heroine experience. For the 88 | game there are 3 levels None, 50%, 100%. 89 | - **None** the animation does not require experience 90 | - **50%** the animation requires at least 50% experience 91 | - **100%** the animation requires 100% experience 92 | 93 | Setting this field will apply levels to the animations. In the example above the animation will be 94 | available after the heroine reaches 50% experience. 95 | 96 | More detailed information in the 97 | [wiki](https://gitgoon.dev/IllusionMods/AnimationLoader/wiki/manifest.xml). 98 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Utils/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Diagnostics; 4 | using UnityEngine; 5 | 6 | namespace AnimationLoader 7 | { 8 | public static class Extensions 9 | { 10 | /// 11 | /// Create a coroutine that calls each of the actions in order after base coroutine finishes 12 | /// 13 | public static IEnumerator AppendCo(this IEnumerator baseCoroutine, params Action[] actions) 14 | { 15 | return new object[] { baseCoroutine, CreateCoroutine(actions) }.GetEnumerator(); 16 | } 17 | 18 | /// 19 | /// Create a coroutine that calls each of the action delegates on consecutive frames 20 | /// (yield return null is returned after each one of the actions) 21 | /// 22 | public static IEnumerator CreateCoroutine(params Action[] actions) 23 | { 24 | #if DEBUG 25 | var stopWatch = new Stopwatch(); 26 | 27 | stopWatch.Start(); 28 | #endif 29 | foreach (var action in actions) 30 | { 31 | action(); 32 | yield return null; 33 | } 34 | #if DEBUG 35 | stopWatch.Stop(); 36 | var ts = stopWatch.Elapsed; 37 | 38 | var elapsedTime = string.Format("{0:00}:{1:00}:{2:00}.{3:0000}", 39 | ts.Hours, ts.Minutes, ts.Seconds, 40 | ts.Milliseconds); 41 | Log.Warning($"Load time for LoadStudioAnims {elapsedTime}"); 42 | #endif 43 | } 44 | 45 | public static void SetRect( 46 | this Transform self, 47 | float anchorLeft = 0f, 48 | float anchorBottom = 0f, 49 | float anchorRight = 1f, 50 | float anchorTop = 1f, 51 | float offsetLeft = 0f, 52 | float offsetBottom = 0f, 53 | float offsetRight = 0f, 54 | float offsetTop = 0f) 55 | { 56 | var rt = (RectTransform)self; 57 | rt.anchorMin = new Vector2(anchorLeft, anchorBottom); 58 | rt.anchorMax = new Vector2(anchorRight, anchorTop); 59 | rt.offsetMin = new Vector2(offsetLeft, offsetBottom); 60 | rt.offsetMax = new Vector2(offsetRight, offsetTop); 61 | } 62 | 63 | /// 64 | /// String format Vector3 variables for easier logging 65 | /// 66 | /// Vector3 object 67 | /// decimal places in string format "F3" for example 68 | /// number of spaces left justified 69 | /// 70 | public static string Format( 71 | this Vector3 self, 72 | string decimals = default, 73 | int spaces = default) 74 | { 75 | string formatString; 76 | 77 | formatString = $"( " + 78 | $"{string.Format($"{{0,{spaces}:{decimals}}}", self.x)}, " + 79 | $"{string.Format($"{{0,{spaces}:{decimals}}}", self.y)}, " + 80 | $"{string.Format($"{{0,{spaces}:{decimals}}}", self.z)} )"; 81 | 82 | return formatString; 83 | } 84 | 85 | /// 86 | /// Adjust move vector to transform vector in Unity when using transform it looks 87 | /// that moving forward sometimes is in the X axis (Why?) 88 | /// 89 | /// object self reference 90 | /// character transform 91 | /// 92 | public static Vector3 MovementTransform(this Vector3 self, Transform transform) 93 | { 94 | var result = new Vector3(0, 0, 0); 95 | 96 | result += transform.right * self.x; 97 | result += transform.up * self.y; 98 | result += transform.forward * self.z; 99 | 100 | return result; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/LoadXML.VersionChecks.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Load XML animation information 3 | // 4 | using System; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Xml.Linq; 8 | 9 | using BepInEx.Logging; 10 | 11 | 12 | namespace AnimationLoader 13 | { 14 | public partial class SwapAnim 15 | { 16 | private static void VersionChecks(XElement manifest) 17 | { 18 | var guid = manifest?.Element("guid")?.Value; 19 | var version = manifest?.Element("version")?.Value; 20 | var author = manifest?.Element("author")?.Value; 21 | var kplugBundleVersion = manifest?.Element("KPluganimBundle")?.Value; 22 | 23 | _animationLoaderVersion = manifest?.Element("AnimationLoaderVersion"); 24 | 25 | if (_animationLoaderVersion is not null) 26 | { 27 | var lines = new StringBuilder(); 28 | var bundle = "."; 29 | var warning = false; 30 | var alVersion = new Version(_animationLoaderVersion.Value); 31 | var pVersion = new Version(Version); 32 | 33 | if (kplugBundleVersion != null) 34 | { 35 | var bundleVersion = BundleVersion(); 36 | var minVersion = new Version(kplugBundleVersion); 37 | if (bundleVersion != null) 38 | { 39 | if (bundleVersion.CompareTo(minVersion) < 0) 40 | { 41 | bundle = $" KPlug Animation Bundle " + 42 | $"version={bundleVersion} minimum={minVersion} some " + 43 | $"features may not work upgrade to latest version."; 44 | warning = true; 45 | } 46 | else 47 | { 48 | bundle = $" KPlug Animation Bundle version={bundleVersion} " + 49 | $"minimum={minVersion}."; 50 | } 51 | } 52 | } 53 | if (pVersion != null) 54 | { 55 | if (pVersion.CompareTo(alVersion) < 0) 56 | { 57 | var tmp = author is not null ? author : "N/A"; 58 | lines.AppendLine($"0011: Manifest " + 59 | $"guid={guid} version={version} author=[{tmp}] " + 60 | $"AnimationLoader version={pVersion} minimum={alVersion} " + 61 | $"some features may not work upgrade to latest version,{bundle}"); 62 | warning = true; 63 | } 64 | else 65 | { 66 | var tmp = author is not null ? author : "N/A"; 67 | lines.AppendLine($"0011: Manifest " + 68 | $"guid={guid} version={version} author=[{tmp}] " + 69 | $"AnimationLoader version={pVersion} minimum={alVersion}" + 70 | $"{bundle}"); 71 | } 72 | } 73 | if (warning) 74 | { 75 | Log.Level(LogLevel.Warning | LogLevel.Debug, lines.ToString()); 76 | } 77 | else 78 | { 79 | Log.Level(LogLevel.Info | LogLevel.Debug, lines.ToString()); 80 | } 81 | } 82 | } 83 | 84 | private static Version BundleVersion() 85 | { 86 | var manifests = Sideloader.Sideloader.Manifests.Values 87 | .Select(x => x.ManifestDocument); 88 | var manifest = manifests 89 | .Select(x => x.Root) 90 | .Where(x => x?.Element("guid").Value == "kpluganim.bundle") 91 | .FirstOrDefault(); 92 | 93 | if (manifest != null) 94 | { 95 | return new Version(manifest.Element("version").Value); 96 | } 97 | return null; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Fixes/Move.Controller.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using UnityEngine; 4 | 5 | using BepInEx.Logging; 6 | 7 | using KKAPI; 8 | using KKAPI.Chara; 9 | 10 | 11 | namespace AnimationLoader 12 | { 13 | public partial class SwapAnim 14 | { 15 | /// 16 | /// Move characters to a different positions 17 | /// 18 | public partial class MoveController : CharaCustomFunctionController 19 | { 20 | internal Vector3 _originalPosition = new(0, 0, 0); 21 | internal Vector3 _lastMovePosition = new(0, 0, 0); 22 | internal CharacterType _chaType = CharacterType.Unknown; 23 | 24 | public enum CharacterType { Heroine, Heroine3P, Player, Janitor, Group, Unknown } 25 | 26 | /// 27 | /// Required definition. 28 | /// 29 | /// 30 | protected override void OnCardBeingSaved(GameMode currentGameMode) 31 | { 32 | } 33 | 34 | internal void Init(CharacterType chaType) 35 | { 36 | _chaType = chaType; 37 | } 38 | 39 | /// 40 | /// Save original position 41 | /// 42 | internal void SetOriginalPosition(Vector3 position) 43 | { 44 | _originalPosition = position; 45 | } 46 | 47 | /// 48 | /// Restore original position 49 | /// 50 | public void ResetPosition() 51 | { 52 | ChaControl.transform.position = _originalPosition; 53 | #if DEBUG 54 | Log.Warning($"0032: Resetting character position for {_chaType}" + 55 | $"to position={ChaControl.transform.position.Format()}."); 56 | #endif 57 | } 58 | 59 | public void Move(Vector3 move) 60 | { 61 | try 62 | { 63 | //if (_originalPosition == Vector3.zero) 64 | //{ 65 | // originalPosition = character.transform.position; 66 | //} 67 | var original = ChaControl.transform.position; 68 | var rightZAxis = ChaControl.transform.right * move.x; 69 | var upYAxis = ChaControl.transform.up * move.y; 70 | var upXAxis = ChaControl.transform.forward * move.z; 71 | 72 | // In manifest x is right z is forward 73 | // in game transform x is forward z is right 74 | Vector3 gameMove = new(move.z, move.y, move.x); 75 | var newPosition = original + gameMove; 76 | var gameMove2 = move.MovementTransform(ChaControl.transform); 77 | 78 | ChaControl.transform.position += upXAxis; 79 | ChaControl.transform.position += upYAxis; 80 | ChaControl.transform.position += rightZAxis; 81 | _lastMovePosition = ChaControl.transform.position; 82 | #if DEBUG 83 | Log.Level(LogLevel.Warning, 84 | $"[Move] Adjusting character position for {_chaType}\n" + 85 | $" move={gameMove.Format()}\n" + 86 | $" trans move={gameMove2.Format()}\n" + 87 | $" up={ChaControl.transform.up.Format()}\n" + 88 | $" right={ChaControl.transform.right.Format()}\n" + 89 | $" forward={ChaControl.transform.forward.Format()}\n" + 90 | $" y(up)={upYAxis.Format()}\n" + 91 | $" z(right)={rightZAxis.Format()}\n" + 92 | $" x(forward)={upXAxis.Format()}\n" + 93 | $" From={original.Format()}\n" + 94 | $" to={_lastMovePosition.Format()}\n" + 95 | $" vector+={newPosition.Format()}."); 96 | #else 97 | Log.Debug($"0031: Adjusting character position for {_chaType} to " + 98 | $"position={_lastMovePosition.Format()}."); 99 | #endif 100 | } 101 | catch (Exception e) 102 | { 103 | Log.Level(LogLevel.Error, $"0010: Cannot adjust {_chaType} - " + 104 | $"{ChaControl.name} - {e}."); 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Hooks.cs: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationLoader Hooks 3 | // 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Reflection.Emit; 7 | 8 | using H; 9 | 10 | using HarmonyLib; 11 | 12 | using static AnimationLoader.SwapAnim.MoveController; 13 | 14 | 15 | namespace AnimationLoader 16 | { 17 | public partial class SwapAnim 18 | { 19 | private static ChaControl _heroine; 20 | private static ChaControl _heroine3P; 21 | private static List _lstHeroines; 22 | private static ChaControl _player; 23 | private static HFlag _flags; 24 | private static Harmony _hookInstance; 25 | private static bool _specialAnimation = false; 26 | 27 | internal partial class Hooks 28 | { 29 | /// 30 | /// Initialize the Hooks patch instance 31 | /// 32 | internal static void Init() 33 | { 34 | _hookInstance = Harmony.CreateAndPatchAll(typeof(Hooks)); 35 | 36 | if (VRHSceneType != null) 37 | { 38 | _hookInstance.Patch( 39 | AccessTools.Method( 40 | VRHSceneType, 41 | nameof(HSceneProc.ChangeAnimator)), 42 | postfix: new HarmonyMethod(typeof(Hooks), 43 | nameof(SwapAnimation))); 44 | _hookInstance.Patch( 45 | AccessTools.Method( 46 | VRHSceneType, 47 | nameof(HSceneProc.CreateAllAnimationList)), 48 | postfix: new HarmonyMethod(typeof(Hooks), 49 | nameof(ExtendList))); 50 | } 51 | } 52 | 53 | /// 54 | /// Initialize MoveController for characters 55 | /// 56 | /// 57 | /// 58 | /// 59 | [HarmonyPrefix] 60 | [HarmonyPatch(typeof(HSceneProc), nameof(HSceneProc.SetShortcutKey))] 61 | private static void SetShortcutKeyPrefix( 62 | object __instance, 63 | List ___lstFemale, 64 | ChaControl ___male) 65 | { 66 | _lstHeroines = ___lstFemale; 67 | _heroine = _lstHeroines[0]; 68 | GetMoveController(_heroine).Init(CharacterType.Heroine); 69 | 70 | if (___lstFemale.Count > 1) 71 | { 72 | _heroine3P = _lstHeroines[1]; 73 | GetMoveController(_heroine3P).Init(CharacterType.Heroine3P); 74 | } 75 | 76 | _player = ___male; 77 | GetMoveController(_player).Init(CharacterType.Player); 78 | 79 | var hsceneTraverse = Traverse.Create(__instance); 80 | var categorys = hsceneTraverse.Field>("categorys").Value; 81 | _specialAnimation = (categorys[0] is 12 or > 1000); 82 | 83 | _flags = hsceneTraverse 84 | .Field("flags").Value; 85 | } 86 | 87 | [HarmonyTranspiler] 88 | [HarmonyPatch(typeof(HSprite), nameof(HSprite.OnChangePlaySelect))] 89 | private static IEnumerable OnChangePlaySelect( 90 | IEnumerable instructions) 91 | { 92 | // Force position change even if position appears to match. 93 | // Prevents clicks from being eaten. 94 | return new CodeMatcher(instructions) 95 | .MatchForward(false, 96 | new CodeMatch( 97 | OpCodes.Ldfld, 98 | AccessTools.Field(typeof(HSprite), 99 | "flags")), 100 | new CodeMatch( 101 | OpCodes.Ldfld, 102 | AccessTools.Field(typeof(HFlag), 103 | "nowAnimationInfo")), 104 | new CodeMatch( 105 | OpCodes.Ldfld, 106 | AccessTools.Field(typeof(HSceneProc.AnimationListInfo), 107 | "id")) 108 | ) 109 | .MatchForward(false, 110 | new CodeMatch(OpCodes.Ret) 111 | ) 112 | .SetAndAdvance(OpCodes.Nop, null) 113 | .InstructionEnumeration(); 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/AnimationLoader.KoikatsuSunshine/LoadXML.Overrides.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Load XML animation information 3 | // 4 | using UnityEngine; 5 | 6 | 7 | namespace AnimationLoader 8 | { 9 | public partial class SwapAnim 10 | { 11 | private static void Override(ref string lhs, string rhs) 12 | { 13 | if (rhs != null) 14 | { 15 | lhs = rhs; 16 | } 17 | } 18 | 19 | private static void Override(ref int lhs, int rhs) 20 | { 21 | if (rhs >= 0) 22 | { 23 | lhs = rhs; 24 | } 25 | } 26 | 27 | private static void DoOverrides( 28 | ref SwapAnimationInfo data, 29 | OverrideInfo overrides, 30 | ref Animation animation, 31 | bool overrideName) 32 | { 33 | if (overrides.PathFemale != null) 34 | { 35 | data.PathFemale = string.Copy(overrides.PathFemale); 36 | } 37 | if (overrides.ControllerFemale != null) 38 | { 39 | data.ControllerFemale = string.Copy(overrides.ControllerFemale); 40 | } 41 | 42 | Override(ref data.PathFemale1, overrides.PathFemale1); 43 | Override(ref data.ControllerFemale1, overrides.ControllerFemale1); 44 | 45 | if (overrides.PathMale != null) 46 | { 47 | data.PathMale = string.Copy(overrides.PathMale); 48 | } 49 | if (overrides.ControllerMale != null) 50 | { 51 | data.ControllerMale = string.Copy(overrides.ControllerMale); 52 | } 53 | if ((overrides.AnimationName != null) && !overrideName) 54 | { 55 | data.AnimationName = string.Copy(overrides.AnimationName); 56 | if (UserOverrides.Value) 57 | { 58 | animation.KoikatsuSunshine = string.Copy(overrides.AnimationName); 59 | animation.KoikatsuSunshineReference = string 60 | .Copy(overrides.AnimationName); 61 | } 62 | } 63 | if (overrides.Mode >= 0) 64 | { 65 | data.Mode = overrides.Mode; 66 | } 67 | if (overrides.kindHoushi >= 0) 68 | { 69 | data.kindHoushi = overrides.kindHoushi; 70 | } 71 | 72 | overrides.categories?.CopyTo(data.categories, 0); 73 | 74 | if (overrides.DonorPoseId >= 0) 75 | { 76 | data.DonorPoseId = overrides.DonorPoseId; 77 | } 78 | 79 | if (overrides.NeckDonorId >= -1) 80 | { 81 | data.NeckDonorId = overrides.NeckDonorId; 82 | } 83 | 84 | Override(ref data.NeckDonorIdFemale, overrides.NeckDonorIdFemale); 85 | Override(ref data.NeckDonorIdFemale1, overrides.NeckDonorIdFemale1); 86 | 87 | if (overrides.NeckDonorIdMale >= 0) 88 | { 89 | data.NeckDonorIdMale = overrides.NeckDonorIdMale; 90 | } 91 | 92 | if (overrides.FileMotionNeck != null) 93 | { 94 | data.FileMotionNeck = string.Copy(overrides.FileMotionNeck); 95 | } 96 | if (overrides.FileMotionNeckMale != null) 97 | { 98 | data.FileMotionNeckMale = string.Copy(overrides.FileMotionNeckMale); 99 | } 100 | if (overrides.IsFemaleInitiative != null) 101 | { 102 | data.IsFemaleInitiative = overrides.IsFemaleInitiative; 103 | } 104 | if (overrides.FileSiruPaste != null) 105 | { 106 | data.FileSiruPaste = string.Copy(overrides.FileSiruPaste); 107 | } 108 | if (overrides.MotionIKDonor != null) 109 | { 110 | data.MotionIKDonor = overrides.MotionIKDonor; 111 | } 112 | if (overrides.MotionIKDonorFemale != null) 113 | { 114 | data.MotionIKDonorFemale = overrides.MotionIKDonorFemale; 115 | } 116 | if (overrides.MotionIKDonorFemale1 != null) 117 | { 118 | data.MotionIKDonorFemale1 = overrides.MotionIKDonorFemale1; 119 | } 120 | if (overrides.MotionIKDonorMale != null) 121 | { 122 | data.MotionIKDonorMale = overrides.MotionIKDonorMale; 123 | } 124 | if (overrides.ExpTaii >= 0) 125 | { 126 | data.ExpTaii = overrides.ExpTaii; 127 | } 128 | if (overrides.PositionHeroine != Vector3.zero) 129 | { 130 | data.PositionHeroine = overrides.PositionHeroine; 131 | } 132 | if (overrides.PositionPlayer != Vector3.zero) 133 | { 134 | data.PositionPlayer = overrides.PositionPlayer; 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/LoadXML.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Load XML animation information 3 | // 4 | //using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Xml.Linq; 10 | using System.Xml.Serialization; 11 | 12 | using BepInEx; 13 | using BepInEx.Logging; 14 | using KKAPI; 15 | 16 | //using UnityEngine; 17 | using static HFlag; 18 | 19 | 20 | namespace AnimationLoader 21 | { 22 | public partial class SwapAnim 23 | { 24 | private const string ManifestRootElement = "AnimationLoader"; 25 | private const string ManifestArrayItem = "Animation"; 26 | private const string ManifestGSArrayItem = KoikatuAPI.GameProcessName; 27 | 28 | private static readonly XmlSerializer xmlSerializer = 29 | new(typeof(SwapAnimationInfo)); 30 | #if KKS 31 | private const string ManifestOverride = "GameSpecificOverrides"; 32 | private static readonly XmlSerializer xmlOverrideSerializer = 33 | new(typeof(OverrideInfo)); 34 | #endif 35 | private static XElement _animRoot; 36 | private static XElement _animRootGS; 37 | private static XElement _animationLoaderVersion; 38 | 39 | private static bool _saveNames = false; 40 | 41 | private static void LoadTestXml() 42 | { 43 | var path = Path.Combine(Paths.ConfigPath, "AnimationLoader"); 44 | if(Directory.Exists(path)) 45 | { 46 | var docs = Directory.GetFiles(path, "*.xml") 47 | .Select(XDocument.Load).ToList(); 48 | if(docs.Count > 0) 49 | { 50 | Log.Level(LogLevel.Message, $"0014: [{PluginName}] Loading test " + 51 | $"animations"); 52 | LoadXml(docs); 53 | return; 54 | } 55 | } 56 | Log.Level(LogLevel.Message, "0015: Make a manifest format .xml in the " + 57 | "config/AnimationLoader folder to test animations"); 58 | } 59 | 60 | private static void LoadXml(IEnumerable manifests) 61 | { 62 | animationDict = []; 63 | var count = 0; 64 | var overrideNames = UserOverrides.Value; 65 | var logLines = new StringBuilder(); 66 | 67 | // Select the only manifests that AnimationLoader will process 68 | foreach (var manifest in manifests 69 | .Select(x => x.Root) 70 | .Where(x => x?.Element(ManifestRootElement) != null)) 71 | { 72 | _animRoot = manifest?.Element(ManifestRootElement); 73 | 74 | _animRootGS = manifest? 75 | .Element(ManifestRootElement)? 76 | .Element(KoikatuAPI.GameProcessName); 77 | 78 | if ((_animRoot is null) && (_animRootGS is null)) 79 | { 80 | continue; 81 | } 82 | var guid = manifest?.Element("guid").Value; 83 | var version = manifest?.Element("version").Value; 84 | 85 | VersionChecks(manifest); 86 | 87 | // Process elements valid for any game 88 | count += ProcessArray( 89 | _animRoot, guid, version, overrideNames, ref logLines); 90 | if (_animRootGS is null) 91 | { 92 | if (_saveNames) 93 | { 94 | SaveNames(animationNamesDict[guid], guid, true); 95 | } 96 | continue; 97 | } 98 | 99 | // Process game specific animations 100 | // Not needed from 1.5.3 on. 101 | var _animSpecific = _animRoot?.Elements(ManifestGSArrayItem); 102 | 103 | if (_animSpecific is not null) 104 | { 105 | foreach (var gameSpecificElement in _animSpecific) 106 | { 107 | if (gameSpecificElement is null) 108 | { 109 | continue; 110 | } 111 | count += ProcessArray( 112 | gameSpecificElement, 113 | guid, 114 | version, 115 | overrideNames, 116 | ref logLines); 117 | } 118 | } 119 | 120 | if (_saveNames) 121 | { 122 | SaveNames(animationNamesDict[guid], guid, true); 123 | } 124 | } 125 | if (count > 0) 126 | { 127 | #if KKS 128 | Utilities.AlDicExpAddTaii(); 129 | #endif 130 | logLines.Append($"A total of {count} animations processed from " + 131 | "manifests."); 132 | Log.Debug($"0016: Animations loaded:\n\n{logLines}\n"); 133 | } 134 | else 135 | { 136 | Log.Level( 137 | LogLevel.Message | LogLevel.Debug, 138 | "0017: No animation manifests found."); 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | #### v1.1.3.4 - 2024-03-16 4 | 5 | ##### Added 6 | 7 | - If the animation is a foot job the heroine shoes will be removed automatically 8 | 9 | #### v1.1.2.0 - 2022-06-16 10 | 11 | ##### Added 12 | 13 | - 3P support 14 | - Manifest will have the minimum version needed of AnimationLoader 15 | - If the version needed is higher a warning massage will be logged 16 | 17 | ##### Changed 18 | 19 | - NeckDonorId option in manifest can be configured per character in the animation 20 | - The NeckDonorId does not have to be the same for every character. For certain animations different NeckDonorId's works better 21 | 22 | #### v1.1.1.3 - 2022-04-19 23 | 24 | Update libraries used. 25 | 26 | ##### Added 27 | 28 | - [KKS] Really basic MotionIK support for a few cases that the donor animations more or less align 29 | 30 | #### v1.1.1.1 - 2022-02-12 31 | 32 | ##### Fixed 33 | 34 | - [KKS] Fix problem in KKS when disabling the plug-in in Character Studio. 35 | 36 | #### v1.1.1 - 2022-02-12 37 | 38 | ##### Added 39 | 40 | - [KKS] VR support 41 | - [KKS] New labels for added animations work as expected 42 | - [KKS] In Free-H if added animations are New (unused) they are not available like the standard 43 | ones. Optional disable in configuration. 44 | - [KKS] Animations can be made dependent of experience like the standard ones. 45 | Optional disable in configuration. 46 | - Animation names can be maintained by the user. Names are saved in UserData/AnimationLoader/Names. 47 | There will be one file per animation bundle. Off by default. 48 | 49 | ##### Changed 50 | 51 | - Optimizations to reduce the load time in Character Studio 52 | 53 | ##### Fixed 54 | 55 | - [KK] Fixed button duplication in Grid 56 | - [@Kokaiinum] fix exception when setting NeckDonorId 57 | 58 | When posting issues one thing that may help to find a solution faster is turning on Debug Information (Advance Settings) and post the output_log.txt in [GitHub](https://gitgoon.dev/IllusionMods/AnimationLoader/issues). If you post it only on Discord and don't address IDontHaveIdea I may not see it. 59 | 60 | 61 | 62 | #### v1.1.0.4b1 - 2022-02-08 63 | 64 | ##### Added 65 | 66 | ##### Changed 67 | 68 | - Optimizations to reduce the load time in Character Studio 69 | 70 | ##### Fixed 71 | 72 | 73 | #### v1.1.0.3b1 - 2022-02-05 74 | 75 | Only one dll for standard and VR in KoikatsuSunshine. It needs KKAPI 1.31.2 minimum. 76 | 77 | ##### Added 78 | 79 | ##### Changed 80 | 81 | - [KKS] One dll for standard and VR modes 82 | 83 | ##### Fixed 84 | 85 | - [KKS] Configuration for FreeH animations was missing. 86 | - [KKS] Change List to HashSet for used animations tracking any duplicate entries will be safely 87 | removed 88 | 89 | ### v1.1.0.1b1 - 2022-02-03 90 | 91 | ##### Added 92 | 93 | - The majority of logs are enabled in config 94 | - Log warning when moving characters 95 | - [KKS] New labels for added animations work as expected 96 | - [KKS] In Free-H if added animations are New they are not available like the standard ones 97 | - [KKS] Animations can be made dependent of experience. 98 | - Animation names can be maintained by the user. (Off by default) 99 | 100 | ##### Changed 101 | 102 | - Log of lists are done in one call 103 | - Reorganize the project renaming and major refactoring 104 | - [KKS] LoadMotionList re-done complete code for buttons in order to ease extensions to plug-in 105 | 106 | ##### Fixed 107 | 108 | - [KK] Fixed button duplication in Grid 109 | - [@Kokaiinum] fix exception when setting NeckDonorId 110 | 111 | ### v1.1.0.0 - 2021-12-25 112 | 113 | First official release with support for Koikatsu Sunshine. 114 | 115 | ##### Added 116 | 117 | - Option to adjust character positions 118 | - Support manifest extension in KK 119 | - Specified processes permitted to load in KK 120 | - Enable two more animations that needed position adjustment 121 | - For developers if there no zipmod with a manifest for animations it will load any test 122 | manifests found in the directory config\AnimationLoader 123 | - Animations can be mark individually as game specific no need to group them all in one section 124 | - Mod can be disabled for Studio if not needed 125 | 126 | ##### Changed 127 | 128 | - Modified the manifest.xml for katarsys animations using new extensions 129 | - SiruPasteFiles can be specified by the name no need to be in dictionary. The dictionary is still 130 | used it has precedence. 131 | - Updates to manifest.xml for KKS (changes in some donors id's) 132 | - Update to latest libraries in KK 133 | 134 | ##### Fixed 135 | 136 | - kind of hoshi of the donor was used instead of the one defined in the manifest 137 | 138 | ### v1.0.9b1 - 2021-12-10 139 | 140 | This is the first release with Koikatsu Sunshine support. In KKS the animations are available 141 | depending on the Heroine experience.a 142 | 143 | ##### Added 144 | 145 | - Koikatsu Sunshine Support 146 | - Extensions to the manifest.xml format 147 | 148 | ##### Changed 149 | 150 | - Solution use shared code now 151 | 152 | 153 | ###### Known Issues 154 | 155 | - For the game the experience has three levels 0-49, 50-99 and a 100. Dependency in experience can 156 | be increased that is if a donor has 100% experience requirement it can not be lowered. If the 157 | experience is level 2 (50-99) it can go level 3 for example. For added animations is dot not have to 158 | be 3 levels. 159 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Plugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | #if DEBUG 5 | using System.Diagnostics; 6 | #endif 7 | 8 | using BepInEx.Logging; 9 | 10 | using KKAPI; 11 | using KKAPI.Chara; 12 | using KKAPI.MainGame; 13 | 14 | #if KK 15 | using UnityEngine; 16 | #endif 17 | 18 | using static HFlag; 19 | 20 | 21 | namespace AnimationLoader 22 | { 23 | public partial class SwapAnim 24 | { 25 | #if KK 26 | private static readonly Color buttonColor = new(0.96f, 1f, 0.9f); 27 | #endif 28 | private static Dictionary> animationDict; 29 | private static Dictionary 30 | swapAnimationMapping; 31 | private static readonly Type VRHSceneType = Type.GetType("VRHScene, Assembly-CSharp"); 32 | 33 | private static readonly Dictionary SiruPasteFiles = new() { 34 | { "", "" }, 35 | { "butt", "siru_t_khs_n06" }, 36 | { "facetits", "siru_t_khh_32" }, 37 | { "facetitspussy", "siru_t_khh_32" }, // have to make this manually, for now copy FaceTits 38 | { "titspussy", "siru_t_khs_n07" }, 39 | { "tits", "siru_t_khh_11" }, 40 | { "pussy", "siru_t_khs_n07" }, // have to make this manually, for now copy TitsPussy 41 | { "kksbutt", "siru_t_khs_14" }, 42 | { "kksfacetits", "siru_t_khh_33" }, 43 | }; 44 | 45 | private static readonly Dictionary EModeGroups = new() { 46 | { "aibu1", 998 }, 47 | { "houshi0", 999 }, 48 | { "houshi1", 1000 }, 49 | { "sonyu0", 1001 }, 50 | { "sonyu1", 1002 }, 51 | { "masturbation1", 1003 }, 52 | { "peeping0", 1004 }, 53 | { "peeping1", 1005 }, 54 | { "lesbian1", 1006 }, 55 | }; 56 | 57 | /// 58 | /// Make information available for other plugins 59 | /// 60 | public static Dictionary> AnimationsDict { 61 | get { return animationDict; } 62 | private set { animationDict = value; } 63 | } 64 | 65 | /// 66 | /// Make information available for other plugins 67 | /// 68 | public static Dictionary 69 | SwapAnimationMapping { 70 | get { return swapAnimationMapping; } 71 | private set { swapAnimationMapping = value; } 72 | } 73 | 74 | private void Awake() 75 | { 76 | Log.LogSource = Logger; ; 77 | 78 | ConfigEntries(); 79 | 80 | Log.Enabled = DebugInfo.Value; 81 | Log.DebugToConsole = DebugToConsole.Value; 82 | #if KKS 83 | if (KoikatuAPI.GetCurrentGameMode() == GameMode.Studio) 84 | { 85 | if (!LoadInCharStudio.Value) 86 | { 87 | Log.Level(LogLevel.Message, "0013: Animation Loader disabled in configuration."); 88 | enabled = false; 89 | return; 90 | } 91 | } 92 | #endif 93 | Hooks.Init(); 94 | // Register move characters controller 95 | CharacterApi.RegisterExtraBehaviour(GUID); 96 | #if KKS 97 | // To save used animations on H exit 98 | GameAPI.RegisterExtraBehaviour(GUID); 99 | #endif 100 | } 101 | 102 | private void Start() 103 | { 104 | #if DEBUG 105 | var stopWatch = new Stopwatch(); 106 | 107 | stopWatch.Start(); 108 | #endif 109 | #if KKS 110 | // Read used animations 111 | _usedAnimations.Read(); 112 | _animationsUseStats.Read(); 113 | #endif 114 | // 115 | // Save names for animations for users who update them and not overwritten 116 | // with updates 117 | // 118 | LoadNamesXml(); 119 | 120 | // 121 | // Get loaded manifests 122 | // 123 | LoadXml(Sideloader.Sideloader.Manifests.Values.Select(x => x.ManifestDocument)); 124 | 125 | // 126 | // Read foot job animations 127 | // 128 | _footJobAnimations.Read(); 129 | #if DEBUG 130 | stopWatch.Stop(); 131 | var ts = stopWatch.Elapsed; 132 | 133 | var elapsedTime = string.Format("{0:00}:{1:00}:{2:00}.{3:00000}", 134 | ts.Hours, ts.Minutes, ts.Seconds, 135 | ts.Milliseconds); 136 | Log.Level(LogLevel.Warning, $"Load time for LoadXmls {elapsedTime}"); 137 | 138 | // 139 | // For test environment animations manifest are kept in config/AnimationLoader 140 | // when the plug-in starts it will load them if no zipmod with manifests found 141 | // May be a feature config flag for everybody or load from here not from there?? 142 | // I like the last one. 143 | // 144 | if (animationDict.Count < 1) 145 | { 146 | stopWatch.Reset(); 147 | stopWatch.Start(); 148 | 149 | LoadTestXml(); 150 | 151 | stopWatch.Stop(); 152 | ts = stopWatch.Elapsed; 153 | elapsedTime = string.Format("{0:00}:{1:00}:{2:00}.{3:00000}", 154 | ts.Hours, ts.Minutes, ts.Seconds, 155 | ts.Milliseconds); 156 | Log.Level(LogLevel.Warning, $"Load time for LoadXmls {elapsedTime}"); 157 | } 158 | #endif 159 | } 160 | 161 | private void Update() 162 | { 163 | if (ReloadManifests.Value.IsDown()) 164 | { 165 | LoadTestXml(); 166 | _footJobAnimations.Read(); 167 | } 168 | } 169 | 170 | /// 171 | /// Get move controller for characters 172 | /// 173 | /// 174 | /// 175 | public static MoveController GetMoveController(ChaControl chaControl) => 176 | (chaControl == null) || (chaControl.gameObject == null) 177 | ? null : chaControl.GetComponent(); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/SwapAnimationInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Xml.Linq; 3 | using System.Xml.Serialization; 4 | 5 | using UnityEngine; 6 | 7 | using KKAPI; 8 | 9 | using static HFlag; 10 | 11 | 12 | namespace AnimationLoader 13 | { 14 | [XmlRoot("Animation")] 15 | [Serializable] 16 | public class SwapAnimationInfo 17 | { 18 | private bool _isRelease = false; 19 | 20 | [XmlIgnore] 21 | public string Guid; 22 | 23 | [XmlElement] 24 | public int StudioId = -1; 25 | 26 | [XmlElement] 27 | public string PathFemale; 28 | 29 | [XmlElement] 30 | public string ControllerFemale; 31 | 32 | [XmlElement] 33 | public string PathFemale1; 34 | 35 | [XmlElement] 36 | public string ControllerFemale1; 37 | 38 | [XmlElement] 39 | public string PathMale; 40 | 41 | [XmlElement] 42 | public string ControllerMale; 43 | 44 | [XmlElement] 45 | public string AnimationName; 46 | 47 | [XmlElement] 48 | public HFlag.EMode Mode; 49 | 50 | [XmlElement] 51 | public KindHoushi kindHoushi; 52 | 53 | [XmlArray] 54 | [XmlArrayItem("category", Type = typeof(PositionCategory))] 55 | public PositionCategory[] categories = []; 56 | //public PositionCategory[] categories = new PositionCategory[0]; 57 | 58 | [XmlElement] 59 | public int DonorPoseId = -1; 60 | 61 | // NeckDonorId set to -2 so it can be overwritten in manifest when needed 62 | [XmlElement] 63 | public int NeckDonorId = -2; 64 | 65 | [XmlElement] 66 | public int NeckDonorIdFemale = -1; 67 | 68 | [XmlElement] 69 | public int NeckDonorIdFemale1 = -1; 70 | 71 | [XmlElement] 72 | public int NeckDonorIdMale = -1; 73 | 74 | [XmlElement] 75 | public string FileMotionNeck; 76 | 77 | [XmlElement] 78 | public string FileMotionNeckMale; 79 | 80 | [XmlElement] 81 | public bool? IsFemaleInitiative; 82 | 83 | [XmlElement] 84 | public string FileSiruPaste; 85 | 86 | [XmlElement] 87 | public string MotionIKDonor; 88 | 89 | [XmlElement] 90 | public string MotionIKDonorFemale; 91 | 92 | [XmlElement] 93 | public string MotionIKDonorFemale1; 94 | 95 | [XmlElement] 96 | public string MotionIKDonorMale; 97 | 98 | [XmlElement] 99 | public int ExpTaii = -1; 100 | 101 | [XmlElement] 102 | public Vector3 PositionHeroine = Vector3.zero; 103 | 104 | [XmlElement] 105 | public Vector3 PositionPlayer = Vector3.zero; 106 | 107 | [XmlElement] 108 | public string SpecificFor = string.Empty; 109 | 110 | public bool IsRelease { 111 | get 112 | { 113 | return _isRelease; 114 | } 115 | set 116 | { 117 | _isRelease = value; 118 | } 119 | } 120 | } 121 | 122 | [XmlRoot(KoikatuAPI.GameProcessName)] 123 | [Serializable] 124 | public class OverrideInfo 125 | { 126 | [XmlIgnore] 127 | public string Guid; 128 | 129 | [XmlElement] 130 | public int StudioId = -1; 131 | 132 | [XmlElement] 133 | public string PathFemale; 134 | 135 | [XmlElement] 136 | public string ControllerFemale; 137 | 138 | [XmlElement] 139 | public string PathFemale1; 140 | 141 | [XmlElement] 142 | public string ControllerFemale1; 143 | 144 | [XmlElement] 145 | public string PathMale; 146 | 147 | [XmlElement] 148 | public string ControllerMale; 149 | 150 | [XmlElement] 151 | public string AnimationName; 152 | 153 | [XmlElement] 154 | public EMode Mode = EMode.none; 155 | 156 | [XmlElement] 157 | public KindHoushi kindHoushi = KindHoushi.none; 158 | 159 | [XmlArray] 160 | [XmlArrayItem("category", Type = typeof(PositionCategory))] 161 | //public PositionCategory[] categories = new PositionCategory[0]; 162 | public PositionCategory[] categories = []; 163 | 164 | [XmlElement] 165 | public int DonorPoseId = -1; 166 | 167 | // NeckDonorId set to -2 so it can be overwritten in manifest when needed 168 | [XmlElement] 169 | public int NeckDonorId = -2; 170 | 171 | [XmlElement] 172 | public int NeckDonorIdFemale = -1; 173 | 174 | [XmlElement] 175 | public int NeckDonorIdFemale1 = -1; 176 | 177 | [XmlElement] 178 | public int NeckDonorIdMale = -1; 179 | 180 | [XmlElement] 181 | public string FileMotionNeck; 182 | 183 | [XmlElement] 184 | public string FileMotionNeckMale; 185 | 186 | [XmlElement] 187 | public bool? IsFemaleInitiative; 188 | 189 | [XmlElement] 190 | public string FileSiruPaste; 191 | 192 | [XmlElement] 193 | public string MotionIKDonor; 194 | 195 | [XmlElement] 196 | public string MotionIKDonorFemale; 197 | 198 | [XmlElement] 199 | public string MotionIKDonorFemale1; 200 | 201 | [XmlElement] 202 | public string MotionIKDonorMale; 203 | 204 | [XmlElement] 205 | public int ExpTaii = -1; 206 | 207 | [XmlElement] 208 | public Vector3 PositionHeroine = Vector3.zero; 209 | 210 | [XmlElement] 211 | public Vector3 PositionPlayer = Vector3.zero; 212 | } 213 | 214 | public enum KindHoushi 215 | { 216 | Hand = 0, 217 | Mouth = 1, 218 | Breasts = 2, 219 | none = -1 220 | } 221 | 222 | public enum PositionCategory 223 | { 224 | LieDown = 0, 225 | Stand = 1, 226 | SitChair = 2, 227 | Stool = 3, 228 | SofaBench = 4, 229 | BacklessBench = 5, 230 | SchoolDesk = 6, 231 | Desk = 7, 232 | Wall = 8, 233 | StandPool = 9, 234 | SitDesk = 10, 235 | SquadDesk = 11, 236 | Pool = 1004, 237 | MischievousCaress = 1003, 238 | LieDownLesbian = 1100, 239 | SitChairLesbian = 1101, 240 | StandLesbian = 1102, 241 | ALLesbian50 = 1150, 242 | AquariumCrowded = 1304, 243 | LieDown3P = 3000, 244 | SitChair3P = 3001 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/LoadXML.ProcessArray.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Load XML animation information 3 | // 4 | using System; 5 | using System.Collections.Generic; 6 | //using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Xml.Linq; 10 | //using System.Xml.Serialization; 11 | 12 | //using BepInEx; 13 | //using BepInEx.Logging; 14 | 15 | using KKAPI; 16 | 17 | //using UnityEngine; 18 | 19 | //using static HFlag; 20 | 21 | 22 | namespace AnimationLoader 23 | { 24 | public partial class SwapAnim 25 | { 26 | private static int ProcessArray( 27 | XElement root, 28 | string guid, 29 | string version, 30 | bool overrideNames, 31 | ref StringBuilder logLines) 32 | { 33 | var count = 0; 34 | var specificLog = new StringBuilder(); 35 | var specificFor = false; 36 | 37 | if (Sideloader.Sideloader.ZipArchives.TryGetValue(guid, out var zipFileName)) 38 | { 39 | logLines.Append($"From {zipFileName} {guid}-{version}: " + 40 | $"{RootText(root.Name)}\n"); 41 | } 42 | else 43 | { 44 | logLines.Append($"From {guid}-{version}: {RootText(root.Name)}\n"); 45 | } 46 | foreach (var animElem in root.Elements(ManifestArrayItem)) 47 | { 48 | if (animElem == null) 49 | { 50 | continue; 51 | } 52 | var animation = new Animation(); 53 | var reader = animElem.CreateReader(); 54 | var overrideName = overrideNames; 55 | var data = (SwapAnimationInfo)xmlSerializer.Deserialize(reader); 56 | data.Guid = guid; 57 | reader.Close(); 58 | 59 | if (!data.SpecificFor.IsNullOrWhiteSpace()) 60 | { 61 | if (data.SpecificFor.Equals(KoikatuAPI.GameProcessName)) 62 | { 63 | specificFor = true; 64 | } 65 | else 66 | { 67 | // Not for current game 68 | continue; 69 | } 70 | } 71 | 72 | if (UserOverrides.Value) 73 | { 74 | // user override name 75 | animation.StudioId = data.StudioId; 76 | animation.Controller = string.Copy(data.ControllerFemale); 77 | animation.Koikatu = string.Copy(data.AnimationName); 78 | animation.KoikatuReference = string.Copy(data.AnimationName); 79 | animation.KoikatsuSunshine = string.Copy(data.AnimationName); 80 | animation.KoikatsuSunshineReference = string 81 | .Copy(data.AnimationName); 82 | 83 | overrideName = false; 84 | var animationOverride = new Animation(); 85 | 86 | if (animationNamesDict.TryGetValue(guid, out var names)) 87 | { 88 | if (names.Anim.Count > 0) 89 | { 90 | animationOverride = names.Anim 91 | .Where(x => (x.StudioId == data.StudioId) && 92 | (x.Controller == data.ControllerFemale)) 93 | .FirstOrDefault(); 94 | if (animationOverride != null) 95 | { 96 | overrideName = true; 97 | } 98 | } 99 | } 100 | 101 | if (overrideName) 102 | { 103 | var name = data.AnimationName; 104 | #if KKS 105 | data.AnimationName = animationOverride?.KoikatsuSunshine; 106 | #endif 107 | #if KK 108 | data.AnimationName = animationOverride?.Koikatu; 109 | #endif 110 | #if DEBUG 111 | Log.Debug($"ProcessArray: Replacing name={name} with " + 112 | $"replace={data.AnimationName}."); 113 | #endif 114 | } 115 | else if (animationNamesDict.TryGetValue(guid, out var name)) 116 | { 117 | if (name != null) 118 | { 119 | name.Anim.Add(animation); 120 | //animationNamesDict[guid].Anim.Add(animation); 121 | if (!_saveNames) 122 | { 123 | _saveNames = true; 124 | } 125 | } 126 | } 127 | 128 | } 129 | #if KKS 130 | // Assuming configuration is for KK like originally is and the 131 | // overrides are for KKS only no the other way around. 132 | // TODO: Changing it so it can be the other way around also. 133 | var overrideRoot = animElem? 134 | .Element(ManifestOverride)? 135 | .Element(KoikatuAPI.GameProcessName); 136 | if (overrideRoot != null) 137 | { 138 | var overrideReader = overrideRoot.CreateReader(); 139 | var overrideData = (OverrideInfo)xmlOverrideSerializer 140 | .Deserialize(overrideReader); 141 | overrideReader.Close(); 142 | DoOverrides(ref data, overrideData, ref animation, overrideName); 143 | } 144 | #endif 145 | if (!animationDict.TryGetValue(data.Mode, out var list)) 146 | { 147 | animationDict[data.Mode] = list = []; 148 | } 149 | list.Add(data); 150 | if (specificFor) 151 | { 152 | specificLog.Append($"{GetAnimationKey(data),-37} - " + 153 | $"{Utilities.Translate(data.AnimationName)}\n"); 154 | specificFor = false; 155 | } 156 | else 157 | { 158 | logLines.Append($"{GetAnimationKey(data),-37} - " + 159 | $"{Utilities.Translate(data.AnimationName)}\n"); 160 | } 161 | count++; 162 | } 163 | if (specificLog.Length > 0) 164 | { 165 | logLines.AppendLine($"\nAnimations specific for " + 166 | $"{KoikatuAPI.GameProcessName}:\n\n{specificLog}"); 167 | } 168 | logLines.Append('\n'); 169 | return count; 170 | } 171 | 172 | internal static Func 173 | RootText = x => x == KoikatuAPI.GameProcessName ? 174 | $"Game specific elements of {x}" : $"Root elements of {x}"; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015/2017 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # Visual Studio 2017 auto generated files 34 | Generated\ Files/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # Benchmark Results 50 | BenchmarkDotNet.Artifacts/ 51 | 52 | # .NET Core 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_h.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *_wpftmp.csproj 81 | *.log 82 | *.vspscc 83 | *.vssscc 84 | .builds 85 | *.pidb 86 | *.svclog 87 | *.scc 88 | 89 | # Chutzpah Test files 90 | _Chutzpah* 91 | 92 | # Visual C++ cache files 93 | ipch/ 94 | *.aps 95 | *.ncb 96 | *.opendb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | *.VC.db 101 | *.VC.VC.opendb 102 | 103 | # Visual Studio profiler 104 | *.psess 105 | *.vsp 106 | *.vspx 107 | *.sap 108 | 109 | # Visual Studio Trace Files 110 | *.e2e 111 | 112 | # TFS 2012 Local Workspace 113 | $tf/ 114 | 115 | # Guidance Automation Toolkit 116 | *.gpState 117 | 118 | # ReSharper is a .NET coding add-in 119 | _ReSharper*/ 120 | *.[Rr]e[Ss]harper 121 | *.DotSettings.user 122 | 123 | # JustCode is a .NET coding add-in 124 | .JustCode 125 | 126 | # TeamCity is a build add-in 127 | _TeamCity* 128 | 129 | # DotCover is a Code Coverage Tool 130 | *.dotCover 131 | 132 | # AxoCover is a Code Coverage Tool 133 | .axoCover/* 134 | !.axoCover/settings.json 135 | 136 | # Visual Studio code coverage results 137 | *.coverage 138 | *.coveragexml 139 | 140 | # NCrunch 141 | _NCrunch_* 142 | .*crunch*.local.xml 143 | nCrunchTemp_* 144 | 145 | # MightyMoose 146 | *.mm.* 147 | AutoTest.Net/ 148 | 149 | # Web workbench (sass) 150 | .sass-cache/ 151 | 152 | # Installshield output folder 153 | [Ee]xpress/ 154 | 155 | # DocProject is a documentation generator add-in 156 | DocProject/buildhelp/ 157 | DocProject/Help/*.HxT 158 | DocProject/Help/*.HxC 159 | DocProject/Help/*.hhc 160 | DocProject/Help/*.hhk 161 | DocProject/Help/*.hhp 162 | DocProject/Help/Html2 163 | DocProject/Help/html 164 | 165 | # Click-Once directory 166 | publish/ 167 | 168 | # Publish Web Output 169 | *.[Pp]ublish.xml 170 | *.azurePubxml 171 | # Note: Comment the next line if you want to checkin your web deploy settings, 172 | # but database connection strings (with potential passwords) will be unencrypted 173 | *.pubxml 174 | *.publishproj 175 | 176 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 177 | # checkin your Azure Web App publish settings, but sensitive information contained 178 | # in these scripts will be unencrypted 179 | PublishScripts/ 180 | 181 | # NuGet Packages 182 | *.nupkg 183 | # The packages folder can be ignored because of Package Restore 184 | **/[Pp]ackages/* 185 | # except build/, which is used as an MSBuild target. 186 | !**/[Pp]ackages/build/ 187 | # Uncomment if necessary however generally it will be regenerated when needed 188 | #!**/[Pp]ackages/repositories.config 189 | # NuGet v3's project.json files produces more ignorable files 190 | *.nuget.props 191 | *.nuget.targets 192 | 193 | # Microsoft Azure Build Output 194 | csx/ 195 | *.build.csdef 196 | 197 | # Microsoft Azure Emulator 198 | ecf/ 199 | rcf/ 200 | 201 | # Windows Store app package directories and files 202 | AppPackages/ 203 | BundleArtifacts/ 204 | Package.StoreAssociation.xml 205 | _pkginfo.txt 206 | *.appx 207 | 208 | # Visual Studio cache files 209 | # files ending in .cache can be ignored 210 | *.[Cc]ache 211 | # but keep track of directories ending in .cache 212 | !*.[Cc]ache/ 213 | 214 | # Others 215 | ClientBin/ 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush personal settings 296 | .cr/personal 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | 333 | # Local History for Visual Studio 334 | .localhistory/ 335 | 336 | # Local configuraton 337 | /PostBuild.bat 338 | /.editorconfig 339 | /release.ps1 340 | /test.ps1 341 | Hooks.Test.cs 342 | AnimationLoader.KoikatsuSunshine.Test/ 343 | rel/ 344 | -------------------------------------------------------------------------------- /Manifest.md: -------------------------------------------------------------------------------- 1 | # Manifest.xml for animations 2 | 3 | The current manifest.xml will continue to work. Both Koikatu (**KK**) and Koikatsu Sunshine 4 | (**KKS**) will try to load the two animations. The extensions described here are optional and made 5 | to have more control on how AnimationLoader will load the animations. 6 | 7 | ```xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ``` 64 | 65 | ## Changes 66 | 67 | 1- The NeckDonorId was been applied was been only applied to the female character. From version 1.1.2 it will be applied to both male and female and there are definitions for the character individually. This permits to assign different values to each character 68 | 69 | - NeckDonorIdFemale 70 | - NeckDonorIdFemale1 71 | - NeckDonorIdMale 72 | ## Extensions: 73 | 74 | 1- **PositionHeroine** and **PositionPlayer** - vectors that represent: 75 | - x axis if left and right movement (red axis) 76 | - y axis up and down (green axis) 77 | - z axis forward and backwards (blue axis) 78 | 79 | The values represent a factor or fraction of one unit of movement. For example: 80 | - to move one unit use 1 81 | - to move one and half units 1.5. 82 | - to move one fifth of a unit use 0.2 83 | 84 | The scale may be around a meter. 85 | 86 | 2- **GameSpecificOverrides** - Since the manifest.xml is the same for KK and KKS taking as a base that 87 | the definitions are for KK to make any adjustment for KKS a node can be added for KKS 88 | with value overrides for the animations that KKS will read. 89 | 90 | 3- **Nodes and ** - any animation inside these will be read by the 91 | corresponding game. I there are more than one animation that is exclusive or a game all can be inside 92 | the same node. The can also be mark individually if desired. 93 | 94 | ## Examples 95 | 96 | ### Example 1 97 | 98 | ```xml 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 0 111 | anim_imports/kplug/female/02_40.unity3d 112 | anim_imports/kplug/male/02_40.unity3d 113 | khh_f_60 114 | khh_m_60 115 | Animation 0 116 | houshi 117 | Hand 118 | 119 | LieDown 120 | Stand 121 | 122 | 0 123 | 0 124 | false 125 | TitsPussy 126 | 127 | 128 | 129 | 1 130 | anim_imports/kplug/female/02_41.unity3d 131 | anim_imports/kplug/male/02_41.unity3d 132 | khh_f_61 133 | khh_m_61 134 | Animation 1 135 | houshi 136 | Hand 137 | 138 | LieDown 139 | Stand 140 | 141 | 0 142 | 55 143 | false 144 | TitsPussy 145 | 146 | 147 | 148 | 149 | 150 | ``` 151 | 152 | This is a example of the current format. There is a problem beecause KK does not have NeckDonorId 55. To 153 | account for this there are two options. From here on I will just use the minimum number of fields in 154 | the examples. 155 | 156 | 1- The animation **Animation 1** only works for KKS. In this case inside the node AnimationLoader you 157 | can add a section `````` and move **Animation 1** there. 158 | 159 | ```xml 160 | 161 | 162 | 163 | 0 164 | Animation 0 165 | 166 | 167 | 168 | 169 | 1 170 | Animation 1 171 | 55 172 | 173 | 174 | 175 | 176 | ``` 177 | This way only KKS will try to load **Animation 1**. KK will ignore the **KoikatsuSunshine** node. 178 | Exclusive animations can be all in one section or marked individually. 179 | 180 | 2- **Animation 1** works for both games but for KKS works better with NeckDonorId 55. 181 | ```xml 182 | 183 | 184 | 185 | 0 186 | Animation 0 187 | 188 | 189 | 190 | 1 191 | Animation 1 192 | 0 193 | 194 | 195 | 55 196 | 197 | 198 | 199 | 200 | 201 | ``` 202 | GameSpecificOverrides only work for KKS do to Unity manage code stripping having an element with xml 203 | will trigger errors. But since the current definitions are for KK anyway this should not be a 204 | limitation. Animations targeting KK and KKS should always start with the definition for KK then do 205 | any fine tune for KKS with the overrides. 206 | 207 | ### Example 2 208 | 209 | The position of the characters can be adjusted. 210 | 211 | ```xml 212 | 213 | 0 214 | 0 215 | 1 216 | 217 | 218 | 0 219 | 0 220 | 1 221 | 222 | ``` 223 | 224 | Move the character forward 1 unit. 225 | 226 | The characters are move individually only one position adjustment can be made no need to have a move 227 | configuration for both characters. 228 | 229 | ```xml 230 | 231 | 0 232 | 0 233 | -0.04 234 | 235 | ``` 236 | 237 | Here it shows move the Heroine 0.04 fraction of a unit backwards. 238 | 239 | ### Example 3 240 | 241 | ```xml 242 | 12 243 | 2 244 | ``` 245 | Applay NeckDonor 12 for the female character and 2 for the male. 246 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Fixes/Move.AnimationInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Class to help identify the animations 3 | // 4 | using System.Collections.Generic; 5 | 6 | using UnityEngine; 7 | 8 | using KKAPI; 9 | 10 | using static HFlag; 11 | 12 | 13 | namespace AnimationLoader 14 | { 15 | public partial class SwapAnim 16 | { 17 | /// 18 | /// Class to help uniquely identified the animations 19 | /// 20 | public class AnimationInfo 21 | { 22 | internal SwapAnimationInfo _anim; 23 | internal HSceneProc.AnimationListInfo _animGA; 24 | internal bool _isAnimationLoader; 25 | 26 | internal string _Guid; 27 | internal EMode _mode; 28 | internal int _id; 29 | internal string _controller; 30 | internal string _name; 31 | internal int _donor; 32 | 33 | internal string Controller => _controller; 34 | public int Donor => _donor; 35 | public string Guid => _Guid; 36 | public int Id => _id; 37 | public string Key => $"{_Guid}-{_mode}-{_controller}-{_id:D3}"; 38 | public EMode Mode => _mode; 39 | public string Name => _name; 40 | public bool IsAnimationLoader => _isAnimationLoader; 41 | public SwapAnimationInfo SwapAnim => _anim; 42 | public HSceneProc.AnimationListInfo Animation => _animGA; 43 | 44 | /// 45 | /// These are the fields for the key 46 | /// _Guid - The guid of the zipmod 47 | /// _mode - Animator mode (aibu, hoshi, ..) 48 | /// _controller - Controller for the animation some zipmod don't define StudioId 49 | /// this is added to make key unique 50 | /// _id = Studio ID 51 | /// 52 | public AnimationInfo() 53 | { 54 | _Guid = string.Empty; 55 | _mode = EMode.none; 56 | _controller = string.Empty; 57 | _id = -1; 58 | _name = string.Empty; 59 | _donor = -1; 60 | } 61 | 62 | public AnimationInfo(HSceneProc.AnimationListInfo animation) 63 | { 64 | AnimationInfoHelper(animation); 65 | } 66 | 67 | internal void AnimationInfoHelper(HSceneProc.AnimationListInfo animation) 68 | { 69 | _mode = animation.mode; 70 | swapAnimationMapping.TryGetValue(animation, out var anim); 71 | if (anim != null) 72 | { 73 | // AnimationLoader animation 74 | _anim = anim; 75 | _animGA = null; 76 | _controller = anim.ControllerFemale; 77 | _Guid = anim.Guid; 78 | _id = anim.StudioId; 79 | _isAnimationLoader = true; 80 | _name = anim.AnimationName; 81 | _donor = anim.DonorPoseId; 82 | } 83 | else 84 | { 85 | // Game animation 86 | _anim = null; 87 | _animGA = animation; 88 | _controller = animation.paramFemale.path.file; 89 | _Guid = KoikatuAPI.GameProcessName; 90 | _id = animation.id; 91 | _isAnimationLoader = false; 92 | _name = animation.nameAnimation; 93 | _donor = -1; 94 | } 95 | } 96 | 97 | public void SetAnimation(object animation) 98 | { 99 | AnimationInfoHelper((HSceneProc.AnimationListInfo)animation); 100 | } 101 | 102 | public string TranslateName() 103 | { 104 | return Utilities.TranslateName(_name); 105 | } 106 | 107 | public object Anim() 108 | { 109 | if (_anim == null) 110 | { 111 | return _animGA; 112 | } 113 | return _anim; 114 | } 115 | 116 | } 117 | 118 | /// 119 | /// Static function to get the key for any animation 120 | /// 121 | /// 122 | /// 123 | /// 124 | public static string GetAnimationKey(HSceneProc.AnimationListInfo animation) 125 | { 126 | string Guid; 127 | EMode mode; 128 | int id; 129 | string controller; 130 | 131 | Guid = "com.illusion"; 132 | mode = animation.mode; 133 | id = animation.id; 134 | controller = animation.paramFemale.path.file; 135 | 136 | if (swapAnimationMapping != null) 137 | { 138 | swapAnimationMapping.TryGetValue(animation, out var anim); 139 | if (anim != null) 140 | { 141 | Guid = anim.Guid; 142 | id = anim.StudioId; 143 | controller = anim.ControllerFemale; 144 | } 145 | } 146 | 147 | return $"{Guid}-{mode}-{controller}-{id:D3}"; 148 | } 149 | 150 | /// 151 | /// GetKey overload 152 | /// 153 | /// 154 | /// 155 | public static string GetAnimationKey(SwapAnimationInfo animation) 156 | { 157 | return $"{animation.Guid}-{animation.Mode}-{animation.ControllerFemale}" + 158 | $"-{animation.StudioId:D3}"; 159 | } 160 | 161 | public static List Controllers(HSceneProc.AnimationListInfo animation) 162 | { 163 | var result = new List(); 164 | 165 | result.Clear(); 166 | 167 | 168 | if (swapAnimationMapping != null) 169 | { 170 | swapAnimationMapping.TryGetValue(animation, out var anim); 171 | if (anim != null) 172 | { 173 | return Controllers(anim); 174 | } 175 | } 176 | 177 | if (animation.paramFemale.path.file != null) 178 | { 179 | result.Add(animation.paramFemale.path.file); 180 | } 181 | if (animation.paramMale.path.file != null) 182 | { 183 | result.Add(animation.paramMale.path.file); 184 | } 185 | if (animation.paramFemale1.path.file != null) 186 | { 187 | result.Add(animation.paramFemale1.path.file); 188 | } 189 | 190 | return result; 191 | } 192 | 193 | 194 | public static List Controllers(SwapAnimationInfo animation) 195 | { 196 | var result = new List(); 197 | 198 | result.Clear(); 199 | 200 | if (animation.ControllerFemale != null) 201 | { 202 | result.Add(animation.ControllerFemale); 203 | } 204 | if (animation.ControllerMale != null) 205 | { 206 | result.Add(animation.ControllerMale); 207 | } 208 | if (animation.ControllerFemale1 != null) 209 | { 210 | result.Add(animation.ControllerFemale1); 211 | } 212 | 213 | return result; 214 | } 215 | 216 | /// 217 | /// Get a list with the positions adjustment in the manifest. If it is a regular 218 | /// animation it will return zero vectors 219 | /// 220 | /// animation to search 221 | /// 222 | public static List GetAnimationMovement(HSceneProc.AnimationListInfo animation) 223 | { 224 | List result = [new Vector3(0, 0, 0), new Vector3(0, 0, 0)]; 225 | 226 | if (swapAnimationMapping != null) 227 | { 228 | swapAnimationMapping.TryGetValue(animation, out var anim); 229 | if (anim != null) 230 | { 231 | result[0] = anim.PositionPlayer; 232 | result[1] = anim.PositionHeroine; 233 | } 234 | } 235 | return result; 236 | } 237 | 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/AnimationLoader.Koikatu/Hooks.LoadMotionList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using TMPro; 6 | using UnityEngine; 7 | using UnityEngine.EventSystems; 8 | using UnityEngine.UI; 9 | 10 | using IllusionUtility.GetUtility; 11 | 12 | using HarmonyLib; 13 | using static ADV.Info; 14 | 15 | 16 | namespace AnimationLoader 17 | { 18 | public partial class SwapAnim 19 | { 20 | internal partial class Hooks 21 | { 22 | [HarmonyPostfix] 23 | [HarmonyPatch(typeof(HSprite), nameof(HSprite.LoadMotionList))] 24 | private static void LoadMotionList( 25 | HSprite __instance, 26 | List _lstAnimInfo, 27 | GameObject _objParent) 28 | { 29 | if (_lstAnimInfo == null || _lstAnimInfo.Count == 0) 30 | { 31 | return; 32 | } 33 | var buttonParent = _objParent.transform; 34 | var buttons = _objParent.transform.Cast().ToList(); 35 | Transform scrollT = null; 36 | if (VRHSceneType != null || UseGrid.Value) 37 | { 38 | DestroyImmediate(_objParent.GetComponent()); 39 | DestroyImmediate(_objParent.GetComponent()); 40 | DestroyImmediate(_objParent.GetComponent()); 41 | var glg = _objParent.AddComponent(); 42 | glg.cellSize = new Vector2(200, 35); 43 | glg.startAxis = GridLayoutGroup.Axis.Vertical; 44 | glg.startCorner = GridLayoutGroup.Corner.UpperRight; 45 | glg.constraint = GridLayoutGroup.Constraint.FixedRowCount; 46 | glg.constraintCount = 15; 47 | glg.childAlignment = TextAnchor.UpperRight; 48 | } 49 | else 50 | { 51 | var go = DefaultControls.CreateScrollView(new DefaultControls.Resources()); 52 | go.transform.SetParent(_objParent.transform, false); 53 | var scroll = go.GetComponent(); 54 | scroll.horizontal = false; 55 | scroll.scrollSensitivity = 32f; 56 | scroll.movementType = ScrollRect.MovementType.Clamped; 57 | scroll.horizontalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHide; 58 | scroll.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHide; 59 | DestroyImmediate(scroll.horizontalScrollbar.gameObject); 60 | DestroyImmediate(scroll.verticalScrollbar.gameObject); 61 | DestroyImmediate(scroll.GetComponent()); 62 | 63 | var copyTarget = GameObject 64 | .Find("Canvas").transform 65 | .Find("clothesFileWindow/Window/WinRect/ListArea/Scroll " + 66 | "View/Scrollbar Vertical").gameObject; 67 | var newScrollbar = Instantiate(copyTarget, go.transform); 68 | scroll.verticalScrollbar = newScrollbar.GetComponent(); 69 | newScrollbar.transform.SetRect(1f, 0f, 1f, 1f, 0f, 0f, 18f); 70 | 71 | var triggerEvent = new EventTrigger.TriggerEvent(); 72 | triggerEvent.AddListener(x => GlobalMethod.SetCameraMoveFlag( 73 | __instance.flags.ctrlCamera, 74 | false)); 75 | var eventTrigger = newScrollbar.AddComponent(); 76 | eventTrigger.triggers.Add(new EventTrigger.Entry { 77 | eventID = EventTriggerType.PointerDown, callback = triggerEvent }); 78 | 79 | var vlg = _objParent.GetComponent(); 80 | var csf = _objParent.GetComponent(); 81 | vlg.enabled = false; 82 | csf.enabled = false; 83 | CopyComponent(vlg, scroll.content.gameObject).enabled = true; 84 | CopyComponent(csf, scroll.content.gameObject).enabled = true; 85 | 86 | buttonParent = scroll.content; 87 | scrollT = scroll.gameObject.transform; 88 | } 89 | 90 | // remove the buttons as we're going to rebuild the entire list 91 | buttons.ForEach(x => Destroy(x.gameObject)); 92 | 93 | foreach (var anim in _lstAnimInfo) 94 | { 95 | var btn = Instantiate(__instance.objMotionListNode, buttonParent, false); 96 | btn.AddComponent().info = anim; 97 | var label = btn.GetComponentInChildren(); 98 | label.text = anim.nameAnimation; 99 | label.color = Color.black; 100 | 101 | //TODO: what 102 | var tgl = btn.GetComponent(); 103 | tgl.group = _objParent.GetComponent(); 104 | tgl.enabled = false; 105 | tgl.enabled = true; 106 | 107 | btn.GetComponent().listClickAction.Add(() => 108 | { 109 | __instance.OnChangePlaySelect(btn); 110 | }); 111 | 112 | btn.SetActive(true); 113 | if (__instance.flags.nowAnimationInfo == anim) 114 | { 115 | btn.GetComponent().isOn = true; 116 | } 117 | 118 | swapAnimationMapping.TryGetValue(anim, out var swap); 119 | if (swap != null) 120 | { 121 | btn.transform 122 | .FindLoop("Background").GetComponent().color = buttonColor; 123 | label.text = swap.AnimationName; 124 | } 125 | } 126 | 127 | // order all buttons by name 128 | var allButtons = buttonParent.Cast() 129 | .OrderBy(x => x.GetComponentInChildren().text).ToList(); 130 | foreach (var t in allButtons) 131 | { 132 | // disable New text 133 | var newT = t.FindLoop("New"); 134 | if (newT) 135 | { 136 | //newT.gameObject.SetActive(false); 137 | newT.SetActive(false); 138 | } 139 | 140 | if (SortPositions.Value) 141 | { 142 | t.SetAsLastSibling(); 143 | } 144 | 145 | //var textMeshGo = t.FindLoop("TextMeshPro Text").gameObject; 146 | var textMeshGo = t.FindLoop("TextMeshPro Text"); 147 | var textMesh = textMeshGo.GetComponent(); 148 | textMesh.enableWordWrapping = false; 149 | textMesh.overflowMode = TextOverflowModes.Overflow; // disable ... after text 150 | 151 | // add scrolling text if text is long enough 152 | var rectT = (RectTransform)t; 153 | if (rectT.sizeDelta.x < textMesh.preferredWidth) 154 | { 155 | textMesh.alignment = TextAlignmentOptions.CaplineLeft; 156 | 157 | var txtScroll = textMeshGo.AddComponent(); 158 | txtScroll.textMesh = textMesh; 159 | txtScroll.transBase = rectT; 160 | } 161 | } 162 | 163 | if (scrollT != null && allButtons.Count > 8) 164 | { 165 | scrollT.SetRect(0f, 0f, 1f, 1f, -5f, -100f, -5f, 100f); 166 | } 167 | } 168 | } 169 | 170 | private static T CopyComponent(T original, GameObject destination) where T : Component 171 | { 172 | var type = original.GetType(); 173 | var dst = destination.GetComponent(type) as T; 174 | if (!dst) 175 | { 176 | dst = destination.AddComponent(type) as T; 177 | } 178 | 179 | var fields = type.GetFields(); 180 | foreach (var field in fields) 181 | { 182 | if (field.IsStatic) 183 | { 184 | continue; 185 | } 186 | 187 | field.SetValue(dst, field.GetValue(original)); 188 | } 189 | var props = type.GetProperties(); 190 | foreach (var prop in props) 191 | { 192 | if (!prop.CanWrite || !prop.CanWrite || prop.Name == "name") 193 | { 194 | continue; 195 | } 196 | 197 | prop.SetValue(dst, prop.GetValue(original, null), null); 198 | } 199 | return dst; 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/AnimationsNames.cs: -------------------------------------------------------------------------------- 1 | // 2 | // For people who keep their own names for the animations or translate them to other 3 | // language. 4 | // 5 | // Save animations names to UserData/AnimationLoader/Names so they are not overwritten by 6 | // updates. They can be maintained and updated there. 7 | // 8 | // Only animations not in the file are added. No animation in the file will be 9 | // overwritten 10 | // 11 | using System; 12 | using System.Collections.Generic; 13 | using System.IO; 14 | using System.Linq; 15 | using System.Xml.Linq; 16 | using System.Xml.Serialization; 17 | 18 | using Sideloader; 19 | 20 | 21 | namespace AnimationLoader 22 | { 23 | public partial class SwapAnim 24 | { 25 | private static readonly XmlSerializer xmlNamesSerializer = new(typeof(Names)); 26 | private static Dictionary animationNamesDict = []; 27 | 28 | #region Serializable classes 29 | [XmlRoot("Names")] 30 | [Serializable] 31 | public class Names 32 | { 33 | [XmlElement] 34 | public string guid = string.Empty; 35 | 36 | [XmlElement] 37 | public string name = string.Empty; 38 | 39 | [XmlElement] 40 | public string version = string.Empty; 41 | 42 | [XmlElement] 43 | public string author = string.Empty; 44 | 45 | [XmlElement] 46 | public string description = string.Empty; 47 | 48 | [XmlElement] 49 | public string website = string.Empty; 50 | 51 | [XmlArray("AnimationLoader")] 52 | public List Anim = []; 53 | } 54 | 55 | // 56 | // A real dictionary was better for programming but less friendly to user interaction 57 | // 58 | [XmlRoot("Animation")] 59 | [Serializable] 60 | public class Animation 61 | { 62 | [XmlElement] 63 | public int StudioId; 64 | [XmlElement] 65 | public string Controller; 66 | [XmlElement] 67 | public string Koikatu; 68 | [XmlElement] 69 | public string KoikatuReference; 70 | [XmlElement] 71 | public string KoikatsuSunshine; 72 | [XmlElement] 73 | public string KoikatsuSunshineReference; 74 | } 75 | #endregion 76 | 77 | /// 78 | /// Read animations names xml files 79 | /// 80 | private static void LoadNamesXml() 81 | { 82 | if (!UserOverrides.Value) 83 | { 84 | Log.Debug($"[LoadNamesXml]: Animations names disabled by user."); 85 | return; 86 | } 87 | 88 | var path = Path.Combine(UserData.Path, "AnimationLoader/Names"); 89 | if (Directory.Exists(path)) 90 | { 91 | var docs = Directory.GetFiles(path, "*.xml").Select(XDocument.Load).ToList(); 92 | if (docs.Count > 0) 93 | { 94 | Log.Debug($"0001: [{PluginName}] Loading animations names."); 95 | LoadNamesXmls(docs); 96 | return; 97 | } 98 | } 99 | // Create empty dictionary 100 | InitAnimationNamesDict(); 101 | Log.Debug("0002: No names found."); 102 | } 103 | 104 | /// 105 | /// Create empty dictionary for any animation bundle found 106 | /// 107 | private static void InitAnimationNamesDict() 108 | { 109 | var manifests = Sideloader.Sideloader.Manifests.Values.Select(x => x.ManifestDocument); 110 | 111 | foreach (var manifest in manifests 112 | .Select(x => x.Root) 113 | .Where(x => x?.Element(ManifestRootElement) != null)) 114 | { 115 | var guid = manifest.Element("guid")?.Value; 116 | Log.Debug($"InitAnimationNamesDict: Add GUID={guid}"); 117 | NamesAddGuidHelper(manifest); 118 | } 119 | } 120 | 121 | /// 122 | /// Add the names on the xml files to names dictionary 123 | /// 124 | /// 125 | private static void LoadNamesXmls(IEnumerable namesDocs) 126 | { 127 | if (!UserOverrides.Value) 128 | { 129 | return; 130 | } 131 | 132 | foreach (var animElem in namesDocs.Select(x => x.Root)) 133 | { 134 | try 135 | { 136 | var reader = animElem.CreateReader(); 137 | var names = (Names)xmlNamesSerializer.Deserialize(reader); 138 | reader.Close(); 139 | animationNamesDict.Add(names.guid, names); 140 | #if DEBUG 141 | if (names?.Anim.Count > 0) 142 | { 143 | foreach (var a in names.Anim) 144 | { 145 | Log.Info($"Animation {a.StudioId} ref={a.KoikatuReference} " + 146 | $"trans={a.KoikatsuSunshine}"); 147 | } 148 | } 149 | #endif 150 | } 151 | catch (Exception ex) 152 | { 153 | Log.Error($"0003: Error trying to read names file. " + 154 | $"GUID={animElem.Name} " + 155 | $"{ex.Message}"); 156 | } 157 | } 158 | } 159 | 160 | /// 161 | /// Setup new names for guid 162 | /// 163 | /// 164 | private static void NamesAddGuid(XElement manifest) 165 | { 166 | if (!UserOverrides.Value) 167 | { 168 | return; 169 | } 170 | 171 | NamesAddGuidHelper(manifest); 172 | } 173 | 174 | private static void NamesAddGuidHelper(XElement manifest) 175 | { 176 | // new names 177 | var names = new Names(); 178 | 179 | // initialize fields with manifest date 180 | names.guid = manifest.Element(nameof(names.guid)).Value; 181 | names.name = manifest.Element(nameof(names.name)).Value; 182 | names.version = manifest.Element(nameof(names.version)).Value; 183 | names.author = manifest.Element(nameof(names.author)).Value; 184 | names.description = "Modify the items Koikatu and KoikatsuSunshine only."; 185 | names.website = manifest.Element(nameof(names.website)).Value; 186 | 187 | // add new names to dictionary 188 | animationNamesDict.Add(names.guid, names); 189 | } 190 | 191 | /// 192 | /// Save all the names in the names dictionary to xml files 193 | /// 194 | private static void SaveNamesXmls() 195 | { 196 | if (!UserOverrides.Value) 197 | { 198 | return; 199 | } 200 | 201 | foreach (var guid in animationNamesDict.Keys) 202 | { 203 | var animNames = animationNamesDict[guid]; 204 | SaveNames(animNames, guid); 205 | } 206 | } 207 | 208 | /// 209 | /// Save names to xml files. One xml file is saved per guid in dictionary. 210 | /// 211 | /// 212 | /// 213 | private static void SaveNames(Names names, string guid, bool overwrite = false) 214 | { 215 | if (!UserOverrides.Value) 216 | { 217 | return; 218 | } 219 | 220 | var path = Path.Combine(UserData.Path, "AnimationLoader/Names"); 221 | var fileName = $"{path}/names-{guid}.xml"; 222 | FileInfo file = new(fileName); 223 | 224 | Log.Debug($"0004: Saving names file {fileName}."); 225 | 226 | // Create directory where to save the file. 227 | // Safe to do even if directory exists. 228 | file.Directory.Create(); 229 | if (file.Exists && !overwrite) 230 | { 231 | Log.Debug($"0005: File {fileName} already exits not overwritten."); 232 | return; 233 | } 234 | 235 | XmlSerializer xmlSerializer = new(typeof(Names)); 236 | var writer = new StreamWriter($"{path}/names-{guid}.xml"); 237 | xmlSerializer.Serialize(writer.BaseStream, names); 238 | writer.Close(); 239 | } 240 | 241 | /// 242 | /// Read the names from an xml file 243 | /// 244 | /// 245 | /// 246 | private static void ReadNames(ref Names names, string guid) 247 | { 248 | if (!UserOverrides.Value) 249 | { 250 | return; 251 | } 252 | 253 | var path = Path.Combine(UserData.Path, "AnimationLoader/Names"); 254 | var fileName = $"{path}/names-{guid}.xml"; 255 | FileInfo file = new(fileName); 256 | 257 | if (!file.Exists) 258 | { 259 | Log.Debug($"0006: Names file {fileName} not found. Safe to ignore."); 260 | return; 261 | } 262 | 263 | XmlSerializer xmlSerializer = new(typeof(Names)); 264 | StreamReader reader = new($"{path}/names-{guid}.xml"); 265 | names = (Names)xmlSerializer.Deserialize(reader); 266 | reader.Close(); 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Plugin.ConfigEntries.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration entries 3 | // 4 | using ADV.Commands.Base; 5 | 6 | using BepInEx.Configuration; 7 | using BepInEx.Logging; 8 | 9 | using KKAPI; 10 | using KKAPI.Utilities; 11 | 12 | using UnityEngine; 13 | 14 | 15 | namespace AnimationLoader 16 | { 17 | public partial class SwapAnim 18 | { 19 | #if KKS 20 | internal static ConfigEntry LoadInCharStudio { get; set; } 21 | internal static ConfigEntry UseAnimationLevels { get; set; } 22 | internal static ConfigEntry EnableAllFreeH { get; set; } 23 | #endif 24 | #if KK 25 | private static ConfigEntry UseGrid { get; set; } 26 | #endif 27 | #if DEBUG 28 | internal static ConfigEntry TestMode { get; set; } 29 | #endif 30 | internal static ConfigEntry DebugInfo { get; set; } 31 | internal static ConfigEntry DebugToConsole { get; set; } 32 | internal static ConfigEntry UserOverrides { get; set; } 33 | internal static ConfigEntry ReloadManifests { get; set; } 34 | 35 | internal static ConfigEntry Reposition { get; set; } 36 | internal static ConfigEntry SortPositions { get; set; } 37 | internal static ConfigEntry HighLight { get; set; } 38 | internal static ConfigEntry MotionIK { get; set; } 39 | 40 | internal const string GeneralSection = "General"; 41 | internal const string DebugSection = "Debug"; 42 | internal const string AdvanceSection = "Advance"; 43 | 44 | internal void ConfigEntries() 45 | { 46 | #if KKS 47 | LoadInCharStudio = Config.Bind( 48 | section: GeneralSection, 49 | key: "Character Studio", 50 | defaultValue: true, 51 | configDescription: new ConfigDescription( 52 | description: "Enable/Disabled module for Studio", 53 | tags: new ConfigurationManagerAttributes { Order = 35 })); 54 | 55 | if (KoikatuAPI.GetCurrentGameMode() == GameMode.Studio) 56 | { 57 | if (!LoadInCharStudio.Value) 58 | { 59 | Log.Message("0013: MOD disabled in configuration."); 60 | enabled = false; 61 | return; 62 | } 63 | } 64 | 65 | UseAnimationLevels = Config.Bind( 66 | section: GeneralSection, 67 | key: "Use Experience Levels", 68 | defaultValue: true, 69 | configDescription: new ConfigDescription( 70 | description: "Apply experience levels to animations", 71 | tags: new ConfigurationManagerAttributes { Order = 16})); 72 | 73 | EnableAllFreeH = Config.Bind( 74 | section: GeneralSection, 75 | key: "All Animations in FreeH", 76 | defaultValue: false, 77 | configDescription: new ConfigDescription( 78 | description: "Enable all loaded animations in FreeH without needing to use them " + 79 | "in story mode", 80 | tags: new ConfigurationManagerAttributes { Order = 15})); 81 | #endif 82 | #if KK 83 | // TODO: Grid UI for KKS 84 | // How many animations will require a scrollable grid 85 | UseGrid = Config.Bind( 86 | section: GeneralSection, 87 | key: nameof(UseGrid), 88 | defaultValue: false, 89 | configDescription: new ConfigDescription( 90 | description: "If you don't want to use the scrollable" + 91 | " list for some reason", 92 | tags: new ConfigurationManagerAttributes { Order = 6 })); 93 | #endif 94 | 95 | // Load manifests from configuration directory 96 | ReloadManifests = Config.Bind( 97 | section: GeneralSection, 98 | key: nameof(ReloadManifests), 99 | defaultValue: new KeyboardShortcut(KeyCode.None), 100 | configDescription: new ConfigDescription( 101 | description: "Load positions from all manifest format xml files inside " + 102 | "config/AnimationLoader folder", 103 | tags: new ConfigurationManagerAttributes { Order = 39 })); 104 | 105 | SortPositions = Config.Bind( 106 | section: GeneralSection, 107 | key: nameof(SortPositions), 108 | defaultValue: true, 109 | configDescription: new ConfigDescription( 110 | description: "Sort positions alphabetically", 111 | tags: new ConfigurationManagerAttributes { Order = 37 })); 112 | 113 | // Reposition characters in the animations it can help with clipping 114 | Reposition = Config.Bind( 115 | section: AdvanceSection, 116 | key: "Reposition Characters", 117 | defaultValue: true, 118 | configDescription: new ConfigDescription( 119 | description: "Some animations have information in the manifest to move " 120 | + "the characters", 121 | acceptableValues: null, 122 | tags: new ConfigurationManagerAttributes { Order = 19, IsAdvanced = true })); 123 | 124 | // Highlight text in buttons helps identify store effects when used 125 | HighLight = Config.Bind( 126 | section: AdvanceSection, 127 | key: "Highlight Buttons Text", 128 | defaultValue: true, 129 | configDescription: new ConfigDescription( 130 | description: "Uses more color to highlight the effects of the " 131 | + "store plug-in and Free-H", 132 | acceptableValues: null, 133 | tags: new ConfigurationManagerAttributes { Order = 18, IsAdvanced = true })); 134 | 135 | // Reposition characters in the animations it can help with clipping 136 | MotionIK = Config.Bind( 137 | section: AdvanceSection, 138 | key: "Setup Motion IK", 139 | defaultValue: true, 140 | configDescription: new ConfigDescription( 141 | description: "Some animations have motion IK configurations if color" + 142 | " is used they can be identified by a darker yellow color", 143 | acceptableValues: null, 144 | tags: new ConfigurationManagerAttributes { Order = 17, IsAdvanced = true })); 145 | 146 | 147 | // Save names in UserData. These can be expanded to other fields if need be. 148 | UserOverrides = Config.Bind( 149 | section: GeneralSection, 150 | key: "Save Names", 151 | defaultValue: false, 152 | configDescription: new ConfigDescription( 153 | description: "Save and use names stored in UserData/AnimationLoader/Names. " + 154 | "Names then can be customized and won't be overwritten by updates.", 155 | tags: new ConfigurationManagerAttributes { Order = 33 })); 156 | 157 | // To generate debug information this has to be enabled 158 | // The majority of the Logs are in conditional compilation 159 | DebugInfo = Config.Bind( 160 | section: DebugSection, 161 | key: "Debug Information", 162 | defaultValue: false, 163 | configDescription: new ConfigDescription( 164 | description: "Show debug information", 165 | acceptableValues: null, 166 | tags: new ConfigurationManagerAttributes { Order = 29, IsAdvanced = true })); 167 | DebugInfo.SettingChanged += (_sender, _args) => 168 | { 169 | Log.Enabled = DebugInfo.Value; 170 | #if DEBUG 171 | Log.Level(LogLevel.Info, $"0028: Log.Enabled set to {Log.Enabled}"); 172 | #endif 173 | }; 174 | 175 | DebugToConsole = Config.Bind( 176 | section: DebugSection, 177 | key: "Debug information to Console", 178 | defaultValue: false, 179 | configDescription: new ConfigDescription( 180 | description: "Show debug information in Console", 181 | acceptableValues: null, 182 | tags: new ConfigurationManagerAttributes { 183 | Order = 27, 184 | IsAdvanced = true 185 | })); 186 | DebugToConsole.SettingChanged += (_sender, _args) => 187 | { 188 | Log.DebugToConsole = DebugToConsole.Value; 189 | #if DEBUG 190 | Log.Level(LogLevel.Info, $"[ConfigEntries] Log.DebugToConsole set to " + 191 | $"{Log.DebugToConsole}"); 192 | #endif 193 | }; 194 | #if DEBUG 195 | Log.Level(LogLevel.Info, $"0028: Log.Enabled set to {Log.Enabled}"); 196 | 197 | 198 | TestMode = Config.Bind( 199 | section: DebugSection, 200 | key: "Enable Test Mode", 201 | defaultValue: false, 202 | configDescription: new ConfigDescription( 203 | description: "Allow unused animations in Free-H", 204 | acceptableValues: null, 205 | tags: new ConfigurationManagerAttributes { Order = 26, IsAdvanced = true })); 206 | #endif 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Hooks.Animators.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using UnityEngine; 5 | 6 | using KKAPI.MainGame; 7 | 8 | #if DEBUG 9 | using BepInEx.Logging; 10 | #endif 11 | using HarmonyLib; 12 | 13 | 14 | namespace AnimationLoader 15 | { 16 | public partial class SwapAnim 17 | { 18 | internal enum State { On = 0, Shift = 1, Hang = 2, Off = 3 } 19 | 20 | internal partial class Hooks 21 | { 22 | /// 23 | /// Set the new original position when changing positions 24 | /// 25 | /// 26 | [HarmonyPrefix] 27 | [HarmonyPatch(typeof(HSceneProc), nameof(HSceneProc.ChangeAnimator))] 28 | private static void ChangeAnimatorPrefix( 29 | object __instance, 30 | HSceneProc.AnimationListInfo _nextAinmInfo) 31 | { 32 | // Temporarily disable movement in KK TODO: Get start position without 33 | // using the characters current position to get the value. 34 | if (_nextAinmInfo == null) 35 | { 36 | return; 37 | } 38 | 39 | if (_heroine == null) 40 | { 41 | return; 42 | } 43 | 44 | var heroine = GameAPI.GetCurrentHeroine(); 45 | var animationKey = GetAnimationKey(_nextAinmInfo); 46 | 47 | // Get shoes off for foot job animations in 48 | // /BepInEx/config/AnimationLoader/FootJob/FootJobAnimations.xml 49 | // Check in KK somehow works not that intuitive 50 | if ((heroine != null) && _footJobAnimations.Contains(animationKey)) 51 | { 52 | heroine.chaCtrl.SetClothesState( 53 | (int)ChaFileDefine.ClothesKind.shoes_inner, 54 | (byte)State.Off); 55 | Log.Debug("0019: [ChangeAnimatorPrefix] Taking shoes off."); 56 | } 57 | #if DEBUG 58 | if (heroine == null) 59 | { 60 | Log.Debug("0019: [ChangeAnimatorPrefix] Heroine is null."); 61 | } 62 | #endif 63 | 64 | #if KKS 65 | try 66 | { 67 | var hSceneTraverse = Traverse.Create(__instance); 68 | var flags = hSceneTraverse.Field("flags").Value; 69 | var position = hSceneTraverse 70 | .Field("nowHpointDataPos").Value; 71 | var lstFemales = hSceneTraverse 72 | .Field>("lstFemale").Value; 73 | var key = GetAnimationKey(_nextAinmInfo); 74 | if (_animationsUseStats.Stats.ContainsKey(key)) 75 | { 76 | _animationsUseStats.Stats[key] += 1; 77 | } 78 | #if DEBUG 79 | var nowAnimName = "None"; 80 | if (flags.nowAnimationInfo != null) 81 | { 82 | nowAnimName = Utilities 83 | .TranslateName(flags.nowAnimationInfo.nameAnimation); 84 | } 85 | 86 | Log.Warning($"0007: [ChangeAnimatorPrefix] Animator changing - " + 87 | $"[{Manager.Scene.ActiveScene.name}]\n" + 88 | $"Now Animation {nowAnimName} " + 89 | $"Animation {Utilities.TranslateName(_nextAinmInfo.nameAnimation)} " + 90 | $"Key={GetAnimationKey(_nextAinmInfo)} " + 91 | $"SiruPaste={SiruPaste(_nextAinmInfo.paramFemale.fileSiruPaste)}\n" + 92 | $"nowHpointDataPos={position.Format()}"); 93 | #endif 94 | // Reposition characters before animation starts 95 | if (Reposition.Value) 96 | { 97 | var nowAnimationInfo = flags.nowAnimationInfo; 98 | var nowAnim = new AnimationInfo(flags.nowAnimationInfo); 99 | // var heroinePos = 100 | // GetMoveController(_heroine).ChaControl.transform.position; 101 | // Cannot use _heroine this call is prior to HSceneProc.SetShortcut 102 | // _heroine is still null 103 | var heroinePos = lstFemales[0].transform.position; 104 | 105 | if (nowAnim != null) 106 | { 107 | // If there is a position adjustment 108 | // Reset position for new animation in same HPoint 109 | if (Utilities.HasMovement(nowAnim)) 110 | { 111 | if (nowAnim.SwapAnim.PositionHeroine != Vector3.zero) 112 | { 113 | if (!Utilities.IsNewPosition(_heroine)) 114 | { 115 | GetMoveController(_heroine).ResetPosition(); 116 | } 117 | } 118 | if (nowAnim.SwapAnim.PositionPlayer != Vector3.zero) 119 | { 120 | if (!Utilities.IsNewPosition(_player)) 121 | { 122 | GetMoveController(_player).ResetPosition(); 123 | } 124 | } 125 | } 126 | } 127 | 128 | var nextAnim = new AnimationInfo(_nextAinmInfo); 129 | if (nextAnim != null) 130 | { 131 | // Move characters 132 | if (Utilities.HasMovement(nextAnim)) 133 | { 134 | // The position of characters as set by the current 135 | // animation pose 136 | position = hSceneTraverse 137 | .Field("nowHpointDataPos").Value; 138 | 139 | Utilities.SetOriginalPositionAll(position); 140 | if (nextAnim.SwapAnim.PositionHeroine != Vector3.zero) 141 | { 142 | GetMoveController(_heroine) 143 | .Move(nextAnim.SwapAnim.PositionHeroine); 144 | } 145 | if (nextAnim.SwapAnim.PositionPlayer != Vector3.zero) 146 | { 147 | GetMoveController(_player) 148 | .Move(nextAnim.SwapAnim.PositionPlayer); 149 | } 150 | } 151 | // Save used animation 152 | if (nextAnim.IsAnimationLoader) 153 | { 154 | _usedAnimations.Keys.Add(nextAnim.Key); 155 | } 156 | } 157 | } 158 | } 159 | catch (Exception e) 160 | { 161 | Log.Error($"0008: Error={e}"); 162 | } 163 | #endif 164 | } 165 | 166 | internal static Func SiruPaste = x => x == string.Empty ? 167 | $"None" : $"{x}"; 168 | 169 | /// 170 | /// Swap animation if found in mapping dictionary 171 | /// 172 | /// 173 | /// 174 | [HarmonyPostfix] 175 | [HarmonyPatch(typeof(HSceneProc), nameof(HSceneProc.ChangeAnimator))] 176 | private static void SwapAnimation( 177 | object __instance, 178 | HSceneProc.AnimationListInfo _nextAinmInfo) 179 | { 180 | if (!swapAnimationMapping.TryGetValue( 181 | _nextAinmInfo, out var swapAnimationInfo)) 182 | { 183 | return; 184 | } 185 | 186 | RuntimeAnimatorController femaleCtrl = null; 187 | RuntimeAnimatorController female1Ctrl = null; 188 | RuntimeAnimatorController maleCtrl = null; 189 | 190 | if (!string.IsNullOrEmpty(swapAnimationInfo.PathFemale) 191 | || !string.IsNullOrEmpty(swapAnimationInfo.ControllerFemale)) 192 | { 193 | femaleCtrl = AssetBundleManager.LoadAsset( 194 | swapAnimationInfo.PathFemale, 195 | swapAnimationInfo.ControllerFemale, 196 | typeof(RuntimeAnimatorController)).GetAsset(); 197 | } 198 | 199 | // Third wheel 200 | if (!string.IsNullOrEmpty(swapAnimationInfo.PathFemale1) 201 | || !string.IsNullOrEmpty(swapAnimationInfo.ControllerFemale1)) 202 | { 203 | female1Ctrl = AssetBundleManager.LoadAsset( 204 | swapAnimationInfo.PathFemale1, 205 | swapAnimationInfo.ControllerFemale1, 206 | typeof(RuntimeAnimatorController)).GetAsset(); 207 | } 208 | 209 | if (!string.IsNullOrEmpty(swapAnimationInfo.PathMale) 210 | || !string.IsNullOrEmpty(swapAnimationInfo.ControllerMale)) 211 | { 212 | maleCtrl = AssetBundleManager.LoadAsset( 213 | swapAnimationInfo.PathMale, 214 | swapAnimationInfo.ControllerMale, 215 | typeof(RuntimeAnimatorController)).GetAsset(); 216 | } 217 | var t_hsp = Traverse.Create(__instance); 218 | var lstFemale = t_hsp.Field>("lstFemale").Value; 219 | var female = lstFemale[0]; 220 | var female1 = ((lstFemale.Count > 1) ? lstFemale[1] : null); 221 | var male = t_hsp.Field("male").Value; 222 | ////TODO: male1 223 | 224 | if (femaleCtrl != null) 225 | { 226 | female.animBody.runtimeAnimatorController = SetupAnimatorOverrideController( 227 | female.animBody.runtimeAnimatorController, 228 | femaleCtrl); 229 | } 230 | if ((female1Ctrl != null) 231 | && female1 != null) 232 | { 233 | female1.animBody.runtimeAnimatorController = SetupAnimatorOverrideController( 234 | female1.animBody.runtimeAnimatorController, 235 | female1Ctrl); 236 | } 237 | if (maleCtrl != null) 238 | { 239 | male.animBody.runtimeAnimatorController = SetupAnimatorOverrideController( 240 | male.animBody.runtimeAnimatorController, maleCtrl); 241 | } 242 | // Temporarily disable in KK giving problem for some users TODO: More testing 243 | SetupMotionIK(__instance, swapAnimationInfo, _nextAinmInfo); 244 | } 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/AnimationClipsCache.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Save key of used animations 3 | // 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Runtime.Serialization; 8 | 9 | using BepInEx; 10 | 11 | namespace AnimationLoader 12 | { 13 | [DataContract(Name = "AnimationLoader", Namespace = "https://gitgoon.dev/IllusionMods/AnimationLoader")] 14 | public class AnimationClipsCache 15 | { 16 | [DataMember] 17 | public Dictionary> Clips { set; get; } 18 | 19 | private static readonly string _path = Path.Combine(Paths.ConfigPath, "AnimationLoader"); 20 | private static readonly string _fileName = $"{_path}/animationClips.cache"; 21 | private static readonly DataContractSerializer _serializer = new(typeof(AnimationClipsCache)); 22 | private static readonly FileInfo _fileInfo = new(_fileName); 23 | 24 | public AnimationClipsCache() 25 | { 26 | Clips = []; 27 | Clips.Clear(); 28 | } 29 | 30 | public void Save() 31 | { 32 | _fileInfo.Directory.Create(); 33 | var writer = new FileStream(_fileName, FileMode.Create, FileAccess.Write); 34 | _serializer.WriteObject(writer, this); 35 | writer.Close(); 36 | } 37 | 38 | public void Read() 39 | { 40 | if (_fileInfo.Exists) 41 | { 42 | using var fileStream = File.Open(_fileName, FileMode.Open, FileAccess.Read); 43 | var tmp = _serializer.ReadObject(fileStream) as AnimationClipsCache; 44 | fileStream.Close(); 45 | 46 | Clips = tmp?.Clips; 47 | 48 | } 49 | } 50 | } 51 | 52 | [DataContract] 53 | public class AnimationClipsByType 54 | { 55 | [DataMember] 56 | public Dictionary> Clips { set; get; } 57 | 58 | private static readonly string _path = Path.Combine(Paths.ConfigPath, "AnimationLoader"); 59 | private static readonly string _fileName = $"{_path}/animationClipsByType.xml"; 60 | private static readonly DataContractSerializer _serializer = new(typeof(AnimationClipsByType)); 61 | private static readonly FileInfo _fileInfo = new(_fileName); 62 | 63 | public AnimationClipsByType() 64 | { 65 | Clips = []; 66 | Clips.Clear(); 67 | } 68 | 69 | public void Save() 70 | { 71 | _fileInfo.Directory.Create(); 72 | var writer = new FileStream(_fileName, FileMode.Create, FileAccess.Write); 73 | _serializer.WriteObject(writer, this); 74 | writer.Close(); 75 | } 76 | 77 | public void Read() 78 | { 79 | if (_fileInfo.Exists) 80 | { 81 | try 82 | { 83 | var reader = new FileStream(_fileName, FileMode.Open, FileAccess.Read); 84 | var tmp = _serializer.ReadObject(reader) as AnimationClipsByType; 85 | reader.Close(); 86 | 87 | Clips = tmp?.Clips; 88 | } 89 | catch (Exception ex) 90 | { 91 | Log.Error($"[AnimationClipsByType] Reading Error: {ex}"); 92 | } 93 | } 94 | } 95 | } 96 | 97 | /// 98 | /// The clip names are the same in each animation for female and male. 99 | /// Every List of clip names are the same by type only three 100 | /// are needed. 101 | /// 102 | /// 1- houshi kind Hand 103 | /// 2- houshi kind Mouth 104 | /// 3- houshi kind Breasts 105 | /// 4- sonyu 106 | /// 107 | /// In the case of houshi for Hand and Breasts the list is the same. I leave 108 | /// them separate for clarity. 109 | /// 110 | public static class AnimationClips 111 | { 112 | public static readonly Dictionary> Clips = new() { 113 | { "houshi-Hand", new List { 114 | "L_Idle", 115 | "L_OLoop", 116 | "L_OUT_A", 117 | "L_OUT_Loop", 118 | "L_OUT_Start", 119 | "L_SLoop1", 120 | "L_SLoop2", 121 | "L_Stop_Idle", 122 | "L_WLoop1", 123 | "L_WLoop2", 124 | "M_Idle", 125 | "M_OLoop", 126 | "M_OUT_A", 127 | "M_OUT_Loop", 128 | "M_OUT_Start", 129 | "M_SLoop1", 130 | "M_SLoop2", 131 | "M_Stop_Idle", 132 | "M_WLoop1", 133 | "M_WLoop2", 134 | "S_Idle", 135 | "S_OLoop", 136 | "S_OUT_A", 137 | "S_OUT_Loop", 138 | "S_OUT_Start", 139 | "S_SLoop1", 140 | "S_SLoop2", 141 | "S_Stop_Idle", 142 | "S_WLoop1", 143 | "S_WLoop2" } 144 | }, 145 | { "houshi-Mouth", new List { 146 | "L_Drink", 147 | "L_Drink_A", 148 | "L_Drink_IN", 149 | "L_Idle", 150 | "L_IN_Loop", 151 | "L_IN_Start", 152 | "L_OLoop", 153 | "L_Oral_Idle", 154 | "L_Oral_Idle_IN", 155 | "L_OUT_A", 156 | "L_OUT_Loop", 157 | "L_OUT_Start", 158 | "L_SLoop1", 159 | "L_SLoop2", 160 | "L_Stop_Idle", 161 | "L_Vomit", 162 | "L_Vomit_A", 163 | "L_Vomit_IN", 164 | "L_WLoop1", 165 | "L_WLoop2", 166 | "M_Drink", 167 | "M_Drink_A", 168 | "M_Drink_IN", 169 | "M_Idle", 170 | "M_IN_Loop", 171 | "M_IN_Start", 172 | "M_OLoop", 173 | "M_Oral_Idle", 174 | "M_Oral_Idle_IN", 175 | "M_OUT_A", 176 | "M_OUT_Loop", 177 | "M_OUT_Start", 178 | "M_SLoop1", 179 | "M_SLoop2", 180 | "M_Stop_Idle", 181 | "M_Vomit", 182 | "M_Vomit_A", 183 | "M_Vomit_IN", 184 | "M_WLoop1", 185 | "M_WLoop2", 186 | "S_Drink", 187 | "S_Drink_A", 188 | "S_Drink_IN", 189 | "S_Idle", 190 | "S_IN_Loop", 191 | "S_IN_Start", 192 | "S_OLoop", 193 | "S_Oral_Idle", 194 | "S_Oral_Idle_IN", 195 | "S_OUT_A", 196 | "S_OUT_Loop", 197 | "S_OUT_Start", 198 | "S_SLoop1", 199 | "S_SLoop2", 200 | "S_Stop_Idle", 201 | "S_Vomit", 202 | "S_Vomit_A", 203 | "S_Vomit_IN", 204 | "S_WLoop1", 205 | "S_WLoop2" } 206 | }, 207 | { "houshi-Breasts", new List { 208 | "L_Idle", 209 | "L_OLoop", 210 | "L_OUT_A", 211 | "L_OUT_Loop", 212 | "L_OUT_Start", 213 | "L_SLoop1", 214 | "L_SLoop2", 215 | "L_Stop_Idle", 216 | "L_WLoop1", 217 | "L_WLoop2", 218 | "M_Idle", 219 | "M_OLoop", 220 | "M_OUT_A", 221 | "M_OUT_Loop", 222 | "M_OUT_Start", 223 | "M_SLoop1", 224 | "M_SLoop2", 225 | "M_Stop_Idle", 226 | "M_WLoop1", 227 | "M_WLoop2", 228 | "S_Idle", 229 | "S_OLoop", 230 | "S_OUT_A", 231 | "S_OUT_Loop", 232 | "S_OUT_Start", 233 | "S_SLoop1", 234 | "S_SLoop2", 235 | "S_Stop_Idle", 236 | "S_WLoop1", 237 | "S_WLoop2" } 238 | }, 239 | { "sonyu", new List { 240 | "L_Drop", 241 | "L_Idle", 242 | "L_IN_A", 243 | "L_Insert", 244 | "L_InsertIdle", 245 | "L_M_IN_Loop", 246 | "L_M_IN_Start", 247 | "L_M_OUT_Loop", 248 | "L_M_OUT_Start", 249 | "L_OLoop", 250 | "L_OUT_A", 251 | "L_Pull", 252 | "L_SF_IN_Loop", 253 | "L_SF_IN_Start", 254 | "L_SLoop1", 255 | "L_SLoop2", 256 | "L_SS_IN_A", 257 | "L_SS_IN_Loop", 258 | "L_SS_IN_Start", 259 | "L_WF_IN_Loop", 260 | "L_WF_IN_Start", 261 | "L_WLoop1", 262 | "L_WLoop2", 263 | "L_WS_IN_A", 264 | "L_WS_IN_Loop", 265 | "L_WS_IN_Start", 266 | "M_Drop", 267 | "M_Idle", 268 | "M_IN_A", 269 | "M_Insert", 270 | "M_InsertIdle", 271 | "M_M_IN_Loop", 272 | "M_M_IN_Start", 273 | "M_M_OUT_Loop", 274 | "M_M_OUT_Start", 275 | "M_OLoop", 276 | "M_OUT_A", 277 | "M_Pull", 278 | "M_SF_IN_Loop", 279 | "M_SF_IN_Start", 280 | "M_SLoop1", 281 | "M_SLoop2", 282 | "M_SS_IN_A", 283 | "M_SS_IN_Loop", 284 | "M_SS_IN_Start", 285 | "M_WF_IN_Loop", 286 | "M_WF_IN_Start", 287 | "M_WLoop1", 288 | "M_WLoop2", 289 | "M_WS_IN_A", 290 | "M_WS_IN_Loop", 291 | "M_WS_IN_Start", 292 | "S_Drop", 293 | "S_Idle", 294 | "S_IN_A", 295 | "S_Insert", 296 | "S_InsertIdle", 297 | "S_M_IN_Loop", 298 | "S_M_IN_Start", 299 | "S_M_OUT_Loop", 300 | "S_M_OUT_Start", 301 | "S_OLoop", 302 | "S_OUT_A", 303 | "S_Pull", 304 | "S_SF_IN_Loop", 305 | "S_SF_IN_Start", 306 | "S_SLoop1", 307 | "S_SLoop2", 308 | "S_SS_IN_A", 309 | "S_SS_IN_Loop", 310 | "S_SS_IN_Start", 311 | "S_WF_IN_Loop", 312 | "S_WF_IN_Start", 313 | "S_WLoop1", 314 | "S_WLoop2", 315 | "S_WS_IN_A", 316 | "S_WS_IN_Loop", 317 | "S_WS_IN_Start", 318 | "L_A_Drop", 319 | "L_A_Idle", 320 | "L_A_IN_A", 321 | "L_A_Insert", 322 | "L_A_InsertIdle", 323 | "L_A_M_IN_Loop", 324 | "L_A_M_IN_Start", 325 | "L_A_M_OUT_A", 326 | "L_A_M_OUT_Loop", 327 | "L_A_M_OUT_Start", 328 | "L_A_OLoop", 329 | "L_A_Pull", 330 | "L_A_SF_IN_Loop", 331 | "L_A_SF_IN_Start", 332 | "L_A_SLoop1", 333 | "L_A_SLoop2", 334 | "L_A_SS_IN_A", 335 | "L_A_SS_IN_Loop", 336 | "L_A_SS_IN_Start", 337 | "L_A_WF_IN_Loop", 338 | "L_A_WF_IN_Start", 339 | "L_A_WLoop1", 340 | "L_A_WLoop2", 341 | "L_A_WS_IN_A", 342 | "L_A_WS_IN_Loop", 343 | "L_A_WS_IN_Start", 344 | "M_A_Drop", 345 | "M_A_Idle", 346 | "M_A_IN_A", 347 | "M_A_Insert", 348 | "M_A_InsertIdle", 349 | "M_A_M_IN_Loop", 350 | "M_A_M_IN_Start", 351 | "M_A_M_OUT_A", 352 | "M_A_M_OUT_Loop", 353 | "M_A_M_OUT_Start", 354 | "M_A_OLoop", 355 | "M_A_Pull", 356 | "M_A_SF_IN_Loop", 357 | "M_A_SF_IN_Start", 358 | "M_A_SLoop1", 359 | "M_A_SLoop2", 360 | "M_A_SS_IN_A", 361 | "M_A_SS_IN_Loop", 362 | "M_A_SS_IN_Start", 363 | "M_A_WF_IN_Loop", 364 | "M_A_WF_IN_Start", 365 | "M_A_WLoop1", 366 | "M_A_WLoop2", 367 | "M_A_WS_IN_A", 368 | "M_A_WS_IN_Loop", 369 | "M_A_WS_IN_Start", 370 | "S_A_Drop", 371 | "S_A_Idle", 372 | "S_A_IN_A", 373 | "S_A_Insert", 374 | "S_A_InsertIdle", 375 | "S_A_M_IN_Loop", 376 | "S_A_M_IN_Start", 377 | "S_A_M_OUT_A", 378 | "S_A_M_OUT_Loop", 379 | "S_A_M_OUT_Start", 380 | "S_A_OLoop", 381 | "S_A_Pull", 382 | "S_A_SF_IN_Loop", 383 | "S_A_SF_IN_Start", 384 | "S_A_SLoop1", 385 | "S_A_SLoop2", 386 | "S_A_SS_IN_A", 387 | "S_A_SS_IN_Loop", 388 | "S_A_SS_IN_Start", 389 | "S_A_WF_IN_Loop", 390 | "S_A_WF_IN_Start", 391 | "S_A_WLoop1", 392 | "S_A_WLoop2", 393 | "S_A_WS_IN_A", 394 | "S_A_WS_IN_Loop", 395 | "S_A_WS_IN_Start" } 396 | } 397 | }; 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Utils/Utilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | using UnityEngine; 7 | 8 | using Illusion.Extensions; 9 | 10 | using KKAPI; 11 | using KKAPI.Utilities; 12 | 13 | 14 | namespace AnimationLoader 15 | { 16 | public partial class SwapAnim 17 | { 18 | internal class Utilities 19 | { 20 | private static readonly byte _alpha = 255; 21 | private static readonly bool _isSunshine = (KoikatuAPI.GameProcessName == "KoikatsuSunshine") 22 | || (KoikatuAPI.VRProcessName == "KoikatsuSunshine_VR"); 23 | 24 | private static readonly bool _isSunshineEx = _isSunshine && 25 | (KoikatuAPI.GetGameVersion().CompareTo(new Version("1.0.8")) > 0); 26 | 27 | #region Color properties 28 | #pragma warning disable IDE1006 // Naming Styles 29 | 30 | public static Color red => new Color32(255, 0, 0, _alpha); 31 | public static Color darkred => new Color32(139, 0, 0, _alpha); 32 | 33 | public static Color magenta => new Color32(255, 0, 255, _alpha); 34 | public static Color darkmagenta => new Color32(139, 0, 139, _alpha); 35 | public static Color pink => new Color32(255, 192, 203, _alpha); 36 | 37 | public static Color yellow => new Color32(255, 255, 0, _alpha); 38 | public static Color lightyellow => new Color32(255, 255, 224, _alpha); 39 | 40 | public static Color blue => new Color32(0, 0, 255, _alpha); 41 | public static Color darkblue => new Color32(0, 0, 139, _alpha); 42 | 43 | public static Color cyan => new Color32(0, 255, 255, _alpha); 44 | public static Color darkcyan => new Color32(0, 139, 139, _alpha); 45 | 46 | public static Color orange => new Color32(255, 165, 0, _alpha); 47 | public static Color darkorange => new Color32(255, 140, 0, _alpha); 48 | public static Color gold => new Color32(255, 215, 0, _alpha); 49 | 50 | public static Color green => new Color32(0, 128, 0, _alpha); 51 | public static Color darkgreen => new Color32(0, 100, 0, _alpha); 52 | public static Color lime => new Color32(0, 255, 0, _alpha); 53 | 54 | public static Color violet => new Color32(238, 130, 238, _alpha); 55 | public static Color darkviolet => new Color32(148, 0, 211, _alpha); 56 | 57 | public static Color orangered => new Color32(255, 69, 0, _alpha); 58 | public static Color blueviolet => new Color32(138, 43, 226, _alpha); 59 | public static Color greenyellow => new Color32(173, 255, 47, _alpha); 60 | public static Color yellowgreen => new Color32(154, 205, 50, _alpha); 61 | 62 | public static Color black => new Color32(0, 0, 0, _alpha); 63 | public static Color gray => new Color32(128, 128, 128, _alpha); 64 | public static Color white => new Color32(255, 255, 255, _alpha); 65 | 66 | #pragma warning restore IDE1006 // Naming Styles 67 | #endregion 68 | 69 | public static bool IsSunshine => _isSunshine; 70 | public static bool IsSunshineEx => _isSunshineEx; 71 | 72 | /// 73 | /// Save information for template.xml 74 | /// 75 | internal static void SaveAnimInfo(List[] lstAnimInfo) 76 | { 77 | var total = 0; 78 | 79 | // id, mode, 80 | // nameAnimation (Japanese name), path, posture, 81 | // numCtrl, kindHoshi, 82 | // hoshiLoopActionS, isFemaleInitiative, 83 | // {category list}, fileSiruPaste 84 | // dicExpTaii[mode][id] 85 | for (var i = 0; i < lstAnimInfo.Length; i++) 86 | { 87 | FileInfo file = new($"lst{i}.csv"); 88 | 89 | if (file.Exists) 90 | { 91 | continue; 92 | } 93 | 94 | var lines = lstAnimInfo[i].Select(x => $"{x.id}, {x.mode}," + 95 | $" {TranslateName(x.nameAnimation, true)}, {x.paramFemale.path.file}," + 96 | $" {x.paramFemale.fileMotionNeck}, {x.posture}," + 97 | $" {x.numCtrl}, {x.kindHoushi}," + 98 | $" {x.houshiLoopActionS}, {x.isFemaleInitiative}," + 99 | $"{CategoryList(x.lstCategory, true)}," + 100 | #if KKS 101 | $" {x.paramFemale.fileSiruPaste}," + 102 | $" {GetExpTaii((int)x.mode, x.id)}"); 103 | #else 104 | $" {x.paramFemale.fileSiruPaste}"); 105 | #endif 106 | File.WriteAllLines($"lst{i}.csv", lines.ToArray()); 107 | total += lines.ToArray().Length; 108 | } 109 | } 110 | 111 | internal static string Translate(string name) 112 | { 113 | if (!TranslationHelper.TryTranslate(name, out var tmp)) 114 | { 115 | return name; 116 | } 117 | 118 | return tmp; 119 | } 120 | 121 | internal static string TranslateName(string animationName, bool original = false) 122 | { 123 | var tmp = Translate(animationName); 124 | if ((tmp == animationName) || !original) 125 | { 126 | return tmp; 127 | } 128 | return $"{tmp} ({animationName})"; 129 | } 130 | 131 | /// 132 | /// Return categories in the string form "{ cat 1, cat 2, ,,,}" 133 | /// 134 | /// 135 | /// 136 | /// 137 | /// 138 | internal static string CategoryList(List categories, bool names = false, bool quotes = true) 139 | { 140 | var tmp = ""; 141 | var first = true; 142 | 143 | foreach (var c in categories) 144 | { 145 | if (first) 146 | { 147 | if (names) 148 | { 149 | tmp += (PositionCategory)c.category; 150 | } 151 | else 152 | { 153 | tmp += c.category.ToString(); 154 | } 155 | first = false; 156 | } 157 | else 158 | { 159 | if (names) 160 | { 161 | tmp += ", " + (PositionCategory)c.category; 162 | } 163 | else 164 | { 165 | tmp += ", " + c.category.ToString(); 166 | } 167 | } 168 | } 169 | return quotes ? "\" { " + tmp + " }\"" : "{ " + tmp + " }"; 170 | } 171 | 172 | /// 173 | /// Ditto 174 | /// 175 | /// 176 | /// 177 | /// 178 | /// 179 | internal static string CategoryList(List categories, bool names = false, bool quotes = true) 180 | { 181 | var tmp = ""; 182 | var first = true; 183 | 184 | foreach (var c in categories) 185 | { 186 | if (first) 187 | { 188 | if (names) 189 | { 190 | tmp += (PositionCategory)c; 191 | } 192 | else 193 | { 194 | tmp += c.ToString(); 195 | } 196 | first = false; 197 | } 198 | else 199 | { 200 | if (names) 201 | { 202 | tmp += ", " + (PositionCategory)c; 203 | } 204 | else 205 | { 206 | tmp += ", " + c.ToString(); 207 | } 208 | } 209 | } 210 | return quotes ? "\" { " + tmp + " }\"" : "{ " + tmp + " }"; 211 | } 212 | 213 | internal static int CountAnimations(List[] lstAnimInfo) 214 | { 215 | var count = 0; 216 | 217 | foreach (var c in lstAnimInfo) 218 | { 219 | count += c.Count; 220 | } 221 | return count; 222 | } 223 | 224 | internal static bool HasMovement(AnimationInfo anim) 225 | { 226 | if (anim?.SwapAnim != null) 227 | { 228 | if (anim.SwapAnim.PositionHeroine != Vector3.zero) 229 | { 230 | return true; 231 | } 232 | if (anim.SwapAnim.PositionPlayer != Vector3.zero) 233 | { 234 | return true; 235 | } 236 | } 237 | return false; 238 | } 239 | 240 | /// 241 | /// Set new original position for characters if there is a move 242 | /// from original position saved 243 | /// 244 | /// 245 | internal static void SetOriginalPositionAll(Vector3 position) 246 | { 247 | if (_flags == null) 248 | { 249 | return; 250 | } 251 | 252 | var heroines = _flags.lstHeroine; 253 | for (var i = 0; i < heroines.Count; i++) 254 | { 255 | if (IsNewPosition(heroines[i].chaCtrl)) 256 | { 257 | GetMoveController(heroines[i].chaCtrl).SetOriginalPosition(position); 258 | } 259 | } 260 | if (IsNewPosition(_flags.player.chaCtrl)) 261 | { 262 | GetMoveController(_flags.player.chaCtrl).SetOriginalPosition(position); 263 | } 264 | } 265 | 266 | /// 267 | /// Determine if there is a change in original position 268 | /// 269 | /// 270 | /// 271 | internal static bool IsNewPosition(ChaControl chaControl) 272 | { 273 | var controller = GetMoveController(chaControl); 274 | var newPosition = chaControl.transform.position; 275 | var originalPosition = controller._originalPosition; 276 | var lastMovePosition = controller._lastMovePosition; 277 | if (newPosition != originalPosition && newPosition != lastMovePosition) 278 | { 279 | return true; 280 | } 281 | return false; 282 | } 283 | #if KKS 284 | /// 285 | /// Returns the experience level needed for the animation to be active using cached 286 | /// dictionary. The system dictionary may be altered. 287 | /// 288 | /// 289 | /// 290 | /// 291 | internal static int GetExpTaii(int mode, int id) 292 | { 293 | if (_dicExpAddTaii != null) 294 | { 295 | if (_dicExpAddTaii.ContainsKey(mode) && _dicExpAddTaii[mode].ContainsKey(id)) 296 | { 297 | return _dicExpAddTaii[mode][id]; 298 | } 299 | return 0; 300 | } 301 | return -1; 302 | } 303 | 304 | internal static int GetExpTaiiT(int mode, int id) 305 | { 306 | if (_dicExpAddTaii != null) 307 | { 308 | if (_dicExpAddTaii.TryGetValue(mode, out var dID)) 309 | { 310 | if (dID.TryGetValue(id, out var idValue)) 311 | { 312 | return idValue; 313 | } 314 | } 315 | return 0; 316 | } 317 | return -1; 318 | } 319 | 320 | /// 321 | /// Save ExpTaii in dictionary 322 | /// 323 | internal static void AlDicExpAddTaii() 324 | { 325 | _alDicExpAddTaii.Clear(); 326 | 327 | foreach (var mode in animationDict.Keys) 328 | { 329 | foreach(var anim in animationDict[mode]) 330 | { 331 | if(!_alDicExpAddTaii.ContainsKey(anim.Guid)) 332 | { 333 | _alDicExpAddTaii.Add(anim.Guid, []); 334 | _alDicExpAddTaii[anim.Guid].Clear(); 335 | } 336 | if(!_alDicExpAddTaii[anim.Guid].ContainsKey((int)anim.Mode)) 337 | { 338 | _alDicExpAddTaii[anim.Guid].Add((int)anim.Mode, []); 339 | _alDicExpAddTaii[anim.Guid][(int)anim.Mode].Clear(); 340 | } 341 | _alDicExpAddTaii[anim.Guid][(int)anim.Mode] 342 | .Add($"{anim.ControllerFemale}{anim.StudioId}", anim.ExpTaii); 343 | } 344 | } 345 | } 346 | #endif 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /src/AnimationLoader.Core/Hooks.AnimationLists.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | using Illusion.Extensions; 6 | 7 | using BepInEx.Logging; 8 | using HarmonyLib; 9 | 10 | namespace AnimationLoader 11 | { 12 | public partial class SwapAnim 13 | { 14 | #if KKS 15 | internal static UsedAnimations _usedAnimations = new(); 16 | #endif 17 | internal static AnimationsUseStats _animationsUseStats = new(); 18 | 19 | internal partial class Hooks 20 | { 21 | internal static bool once = false; 22 | private static List[] _gameAnimations = 23 | new List[8]; 24 | 25 | /// 26 | /// Add new animations to lstAnimInfo (list of all animations available) 27 | /// aibu and sonyu 28 | /// 29 | /// 30 | [HarmonyPostfix] 31 | [HarmonyPatch(typeof(HSceneProc), nameof(HSceneProc.CreateAllAnimationList))] 32 | private static void ExtendList(object __instance) 33 | { 34 | var hSceneTraverse = Traverse.Create(__instance); 35 | var addedAnimations = new StringBuilder(); 36 | var lstAnimInfo = hSceneTraverse 37 | .Field[]>("lstAnimInfo").Value; 38 | 39 | var countGA = 0; 40 | var countAL = 0; 41 | var strTmp = string.Empty; 42 | countGA = Utilities.CountAnimations(lstAnimInfo); 43 | #if DEBUG 44 | Utilities.SaveAnimInfo(lstAnimInfo); 45 | #endif 46 | swapAnimationMapping = 47 | []; 48 | 49 | _gameAnimations = lstAnimInfo; 50 | 51 | if (animationDict.Count > 0) 52 | { 53 | foreach (var anim in animationDict.SelectMany( 54 | e => e.Value, 55 | (e, a) => a 56 | )) 57 | { 58 | var mode = (int)anim.Mode; 59 | if (mode < 0 || mode >= lstAnimInfo.Length) 60 | { 61 | continue; 62 | } 63 | var animListInfo = lstAnimInfo[(int)anim.Mode]; 64 | 65 | var donorInfo = animListInfo 66 | .FirstOrDefault(x => x.id == anim.DonorPoseId)?.DeepCopy(); 67 | 68 | if (donorInfo == null) 69 | { 70 | Log.Level(LogLevel.Error, $"0009: No donor found: " + 71 | $"mode={anim.Mode} DonorPoseId={anim.DonorPoseId} " + 72 | $"animation={anim.AnimationName}"); 73 | continue; 74 | } 75 | // 76 | // There are cases where one Id works for the female but not the 77 | // male and vice-versa 78 | // 79 | // Female donor 80 | #if KK 81 | // Don't touch KK for now treat NeckDonorId like previous version 82 | // only apply it to the female 83 | if (anim.NeckDonorId >= 0 && anim.NeckDonorId != anim.DonorPoseId) 84 | { 85 | // PR #23 Change to Log.Level to always show log, 86 | // update log ID's use temp variable to add log to log list 87 | var newNeckDonor = animListInfo 88 | .FirstOrDefault(x => x.id == anim.NeckDonorId); 89 | if (newNeckDonor == null) 90 | { 91 | strTmp = $"0029: Invalid or missing " + 92 | $"NeckDonorId: mode={anim.Mode} " + 93 | $"NeckDonorId={anim.NeckDonorId}"; 94 | Log.Level(LogLevel.Warning, strTmp); 95 | addedAnimations.Append(strTmp); 96 | } 97 | else 98 | { 99 | var newMotionNeck = 100 | newNeckDonor?.paramFemale?.fileMotionNeck; 101 | if (newMotionNeck == null) 102 | { 103 | strTmp = $"0030: NeckDonorId didn't point to" + 104 | $" a usable fileMotionNeck: " + 105 | $"mode={anim.Mode} NeckDonorId={anim.NeckDonorId}"; 106 | Log.Level(LogLevel.Warning, strTmp); 107 | addedAnimations.Append(strTmp); 108 | } 109 | else 110 | { 111 | donorInfo.paramFemale.fileMotionNeck = newMotionNeck; 112 | } 113 | } 114 | } 115 | #else 116 | if (anim.NeckDonorIdFemale >= 0 && anim.NeckDonorIdFemale != anim.DonorPoseId) 117 | { 118 | // PR #23 Change to Log.Level to always show log, update log ID's 119 | // use temp variable to add log to log list 120 | var newNeckDonor = animListInfo 121 | .FirstOrDefault(x => x.id == anim.NeckDonorIdFemale); 122 | if (newNeckDonor == null) 123 | { 124 | strTmp = $"0029: Invalid or missing " + 125 | $"NeckDonorId: mode={anim.Mode} " + 126 | $"NeckDonorId={anim.NeckDonorIdFemale} " + 127 | $"Animation={anim.AnimationName}"; 128 | Log.Level(LogLevel.Warning, strTmp); 129 | addedAnimations.Append(strTmp); 130 | } 131 | else 132 | { 133 | var newMotionNeck = newNeckDonor?.paramFemale?.fileMotionNeck; 134 | if (newMotionNeck == null) 135 | { 136 | strTmp = $"0030: NeckDonorId didn't point to" + 137 | $" a usable fileMotionNeck: " + 138 | $"mode={anim.Mode} NeckDonorId={anim.NeckDonorIdFemale} " + 139 | $"Animation={anim.AnimationName}"; 140 | Log.Level(LogLevel.Warning, strTmp); 141 | addedAnimations.Append(strTmp); 142 | } 143 | else 144 | { 145 | donorInfo.paramFemale.fileMotionNeck = newMotionNeck; 146 | } 147 | } 148 | } 149 | #endif 150 | // Female1 donor 151 | if (anim.ControllerFemale1 != null) 152 | { 153 | if (anim.NeckDonorIdFemale1 >= 0 154 | && anim.NeckDonorIdFemale1 != anim.DonorPoseId) 155 | { 156 | var newNeckDonor = animListInfo 157 | .FirstOrDefault(x => x.id == anim.NeckDonorIdFemale1); 158 | if (newNeckDonor == null) 159 | { 160 | strTmp = $"0029: Invalid or missing " + 161 | $"NeckDonorId: mode={anim.Mode} " + 162 | $"NeckDonorId={anim.NeckDonorIdFemale1} " + 163 | $"Animation={anim.AnimationName}"; 164 | Log.Level(LogLevel.Warning, strTmp); 165 | addedAnimations.Append(strTmp); 166 | } 167 | else 168 | { 169 | var newMotionNeck = 170 | newNeckDonor?.paramFemale1?.fileMotionNeck; 171 | if (newMotionNeck == null) 172 | { 173 | strTmp = $"0030: NeckDonorId didn't point to" + 174 | $" a usable fileMotionNeck: " + 175 | $"mode={anim.Mode} " + 176 | $"NeckDonorId={anim.NeckDonorIdFemale1} " + 177 | $"Animation={anim.AnimationName}"; 178 | Log.Level(LogLevel.Warning, strTmp); 179 | addedAnimations.Append(strTmp); 180 | } 181 | else 182 | { 183 | donorInfo.paramFemale1.fileMotionNeck = 184 | newMotionNeck; 185 | } 186 | } 187 | } 188 | } 189 | 190 | // Male donor did not have NeckDonor applied to it 191 | // TODO: Treat NeckDonorId as global and apply it to everyone 192 | // when there no specific one must remove from manifest 193 | if (anim.NeckDonorIdMale >= 0 194 | && anim.NeckDonorIdMale != anim.DonorPoseId) 195 | { 196 | var newNeckDonor = animListInfo 197 | .FirstOrDefault(x => x.id == anim.NeckDonorIdMale); 198 | if (newNeckDonor == null) 199 | { 200 | strTmp = $"0029B: Invalid or missing " + 201 | $"NeckDonorIdMale: mode={anim.Mode} " + 202 | $"NeckDonorIdMale={anim.NeckDonorIdMale} " + 203 | $"Animation={anim.AnimationName}"; 204 | Log.Level(LogLevel.Warning, strTmp); 205 | addedAnimations.Append(strTmp); 206 | } 207 | else 208 | { 209 | var newMotionNeck = newNeckDonor?.paramMale?.fileMotionNeck; 210 | if (newMotionNeck == null) 211 | { 212 | strTmp = $"0030B: NeckDonorIdMale didn't point to" + 213 | $" a usable fileMotionNeck: " + 214 | $"mode={anim.Mode} " + 215 | $"NeckDonorIdMale={anim.NeckDonorIdMale} " + 216 | $"Animation={anim.AnimationName}"; 217 | Log.Level(LogLevel.Warning, strTmp); 218 | addedAnimations.Append(strTmp); 219 | } 220 | else 221 | { 222 | donorInfo.paramMale.fileMotionNeck = newMotionNeck; 223 | } 224 | } 225 | } 226 | 227 | if (anim.IsFemaleInitiative != null) 228 | { 229 | donorInfo.isFemaleInitiative = anim.IsFemaleInitiative.Value; 230 | } 231 | 232 | if (!string.IsNullOrEmpty(anim.FileSiruPaste)) 233 | { 234 | // Check if FileSiruPaste is on dictionary first 235 | if (SiruPasteFiles.TryGetValue( 236 | anim.FileSiruPaste.ToLower(), out var fileSiruPaste)) 237 | { 238 | donorInfo.paramFemale.fileSiruPaste = fileSiruPaste; 239 | } 240 | else 241 | { 242 | // TODO: Check that it is a valid name in 243 | // anim.FileSiruPaste 244 | donorInfo.paramFemale.fileSiruPaste = 245 | anim.FileSiruPaste.ToLower(); 246 | } 247 | } 248 | 249 | // Category 250 | // int category 251 | // string fileMove 252 | donorInfo.lstCategory = anim.categories.Select(c => 253 | { 254 | var cat = new HSceneProc.Category { 255 | category = (int)c 256 | }; 257 | return cat; 258 | }).ToList(); 259 | 260 | // The mode and kindHoushi is not honored 261 | if (anim.Mode == HFlag.EMode.houshi) 262 | { 263 | donorInfo.kindHoushi = (int)anim.kindHoushi; 264 | } 265 | #if KKS 266 | // Update name so it shows on button text label correctly 267 | donorInfo.nameAnimation = anim.AnimationName; 268 | #endif 269 | animListInfo.Add(donorInfo); 270 | swapAnimationMapping[donorInfo] = anim; 271 | countAL++; 272 | } 273 | 274 | 275 | } 276 | 277 | #if KKS 278 | _animationsUseStats.Init(lstAnimInfo); 279 | #endif 280 | 281 | // log footer 282 | addedAnimations.Append($"\n{countAL + countGA} animations available: Game " + 283 | $"standard = {countGA} " + 284 | $"AnimationLoader = {countAL}\n"); 285 | #if DEBUG 286 | if (!once) 287 | { 288 | #if KKS 289 | Log.Warning($"0012: Added animations:\n{addedAnimations}\n\n" + 290 | $"Stats:\n{_animationsUseStats}"); 291 | #else 292 | Log.Warning($"0012: Added animations:\n{addedAnimations}"); 293 | #endif 294 | var logLines = new StringBuilder(); 295 | 296 | foreach (var fj in _footJobAnimations) 297 | { 298 | logLines.AppendLine(fj.ToString()); 299 | } 300 | Log.Warning($"0012: Foot jobs.\n{logLines}"); 301 | once = true; 302 | } 303 | #else 304 | // For release log animations added 305 | if (!once) 306 | { 307 | Log.Debug($"0012: Added animations:\n{addedAnimations}"); 308 | once = true; 309 | } 310 | #endif 311 | } 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/AnimationLoader.KoikatsuSunshine/Hooks.LoadMotionList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using TMPro; 6 | using UnityEngine; 7 | using UnityEngine.UI; 8 | 9 | using IllusionUtility.GetUtility; 10 | using SceneAssist; 11 | 12 | using HarmonyLib; 13 | 14 | 15 | namespace AnimationLoader 16 | { 17 | public partial class SwapAnim 18 | { 19 | internal static Dictionary> _dicExpAddTaii = []; 20 | internal static Dictionary>> _alDicExpAddTaii = []; 22 | 23 | internal partial class Hooks 24 | { 25 | /// 26 | /// In order to expand more easily had to replicate the original function and 27 | /// adjust to needs. 28 | /// 29 | /// Colors: 30 | /// Enough experience 31 | /// White - game animation 32 | /// Yellow - additional animation 33 | /// Not enough experience 34 | /// Cyan - game animation 35 | /// Pink - additional animation 36 | /// 37 | /// Green - HPoint animations accessed by asking for sex 38 | /// 39 | /// TODO: Work on grid UI this is a maybe. 40 | /// 41 | /// 42 | /// 43 | /// 44 | /// 45 | [HarmonyPostfix] 46 | [HarmonyPatch(typeof(HSprite), nameof(HSprite.LoadMotionList))] 47 | private static void LoadMotionListPostfix( 48 | HSprite __instance, 49 | List _lstAnimInfo, 50 | GameObject _objParent) 51 | { 52 | var buttonParent = _objParent.transform; 53 | var hExp = __instance.flags.lstHeroine[0].hExp; 54 | 55 | if (buttonParent.childCount > 0) 56 | { 57 | // remove buttons 58 | buttonParent.Cast().ToList().ForEach(x => Destroy(x.gameObject)); 59 | } 60 | 61 | if (_lstAnimInfo == null || _lstAnimInfo.Count == 0) 62 | { 63 | return; 64 | } 65 | 66 | var toggleGroup = _objParent.GetComponent(); 67 | var animationOn = false; 68 | var isALAnim = false; 69 | 70 | // Loop through selected animations _lstAnimInfo only has the animations 71 | // selected according to category, experience, etc 72 | for (var index = 0; index < _lstAnimInfo.Count; ++index) 73 | { 74 | isALAnim = false; 75 | swapAnimationMapping.TryGetValue(_lstAnimInfo[index], out var swap); 76 | if (swap is not null) 77 | { 78 | isALAnim = true; 79 | if (!AnimationCheckOk(__instance, swap)) 80 | { 81 | continue; 82 | } 83 | } 84 | 85 | var button = Instantiate(__instance.objMotionListNode); 86 | 87 | var animationInfoComponent = 88 | button.AddComponent(); 89 | 90 | // Assign animation 91 | animationInfoComponent.info = _lstAnimInfo[index]; 92 | // Assign button to parent transform 93 | button.transform.SetParent(buttonParent, false); 94 | 95 | // TextMesh of button 96 | var textMesh = button.transform.FindLoop("TextMeshPro Text"); 97 | 98 | if (textMesh.TryGetComponent(out var label)) 99 | { 100 | // Text label 101 | label.text = animationInfoComponent.info.nameAnimation; 102 | if (isALAnim) 103 | { 104 | // Animations added to the game 105 | label.color = Utilities.yellow; 106 | 107 | if (swap is not null) 108 | { 109 | if (HighLight.Value) 110 | { 111 | if (swap.MotionIKDonor != null) 112 | { 113 | // MotionIK information 114 | label.color = Utilities.gold; 115 | } 116 | else if((swap.ExpTaii >= 5) && (hExp < swap.ExpTaii)) 117 | { 118 | // Foreground color yellow for loaded animations 119 | //label.color = Utilities.orange; 120 | label.color = Utilities.pink; 121 | } 122 | } 123 | } 124 | } 125 | else 126 | { 127 | // game animations 128 | var tmp = Utilities.GetExpTaii((int)animationInfoComponent.info.mode, 129 | animationInfoComponent.info.id); 130 | if (HighLight.Value) 131 | { 132 | // Emphasize effects of the store plug-in 133 | if ((tmp >= 50) && (hExp < tmp)) 134 | { 135 | label.color = Utilities.cyan; 136 | } 137 | var c = animationInfoComponent.info.lstCategory; 138 | if (!_specialAnimation && (c[0].category is 12 or > 1000)) 139 | { 140 | label.color = Utilities.lime; 141 | } 142 | } 143 | } 144 | } 145 | 146 | var toggle = button.GetComponent(); 147 | 148 | if ((toggle is not null) && (toggleGroup is not null)) 149 | { 150 | // Assign button to toggle group 151 | toggle.group = toggleGroup; 152 | // ?? Magic operations 153 | toggle.enabled = false; 154 | toggle.enabled = true; 155 | } 156 | 157 | // Look for New label 158 | var newLabel = button.transform.FindLoop("New"); 159 | 160 | // Activate New label according to previous usage saved in playHlist 161 | if ((bool)(UnityEngine.Object)newLabel) 162 | { 163 | if (isALAnim) 164 | { 165 | // Manage new status for loaded animations it depended 166 | if (_usedAnimations.Keys.Count > 0) 167 | { 168 | newLabel.SetActive(!_usedAnimations.Keys.Contains( 169 | GetAnimationKey(animationInfoComponent.info))); 170 | } 171 | if (__instance.flags.isFreeH && EnableAllFreeH.Value) 172 | { 173 | newLabel.SetActive(false); 174 | } 175 | } 176 | else 177 | { 178 | // 179 | // This dictionary is filled with the id's of animations used at least 180 | // once by category 181 | // 182 | // playHList = 183 | // {0:[0,16,4,3,7], 184 | // 1:[1,33,32,36,59,63,17], 185 | // 2:[44,23,3,12,8,19,9,47,40,1,49,42], 186 | // ..} 187 | // 188 | var playHlist = Manager.Game.globalData.playHList; 189 | 190 | if (!playHlist.TryGetValue( 191 | (int)animationInfoComponent.info.mode, out var intSet)) 192 | { 193 | // Add missing category 194 | playHlist[(int)animationInfoComponent.info.mode] = 195 | intSet = []; 196 | } 197 | // Show new if animation is not in used animation set. 198 | newLabel.SetActive( 199 | !intSet.Contains(animationInfoComponent.info.id)); 200 | } 201 | } 202 | 203 | var nowPosition = button.transform.FindLoop("NowPos"); 204 | 205 | if (nowPosition is not null) 206 | { 207 | // Check animation to see if it correspond to current category 208 | var foundCategory = false; 209 | foreach (var category in _lstAnimInfo[index].lstCategory) 210 | { 211 | if (__instance.lstCategory.Contains(category.category)) 212 | { 213 | foundCategory = true; 214 | break; 215 | } 216 | } 217 | // Turn on change category icon if animation categories list 218 | // does not have the current category 219 | nowPosition.SetActive(!foundCategory); 220 | } 221 | 222 | // Assign actions to take on user interaction 223 | button.GetComponent().listClickAction.Add( 224 | () => __instance.OnChangePlaySelect(button)); 225 | button.GetComponent().listDownAction.Add( 226 | () => __instance.OnMouseDownSlider()); 227 | button.SetActive(true); 228 | 229 | // Highlight current animation 230 | if (_lstAnimInfo[index] == __instance.flags.nowAnimationInfo) 231 | { 232 | if (toggle is not null) 233 | { 234 | toggle.isOn = true; 235 | } 236 | animationOn = true; 237 | } 238 | } 239 | 240 | if (!animationOn) 241 | { 242 | for (var index = 0; index < _objParent.transform.childCount; ++index) 243 | { 244 | var componentInChildren = 245 | _objParent.transform 246 | .GetChild(index) 247 | .GetComponentInChildren(); 248 | if (componentInChildren != null) 249 | { 250 | componentInChildren.OnChangeValueEnable(false); 251 | } 252 | } 253 | } 254 | 255 | // sort buttons by name 256 | if (SortPositions.Value) 257 | { 258 | var allButtons = buttonParent.Cast().OrderBy( 259 | x => x.GetComponentInChildren().text).ToList(); 260 | foreach (var t in allButtons) 261 | { 262 | t.SetAsLastSibling(); 263 | } 264 | } 265 | } 266 | 267 | [HarmonyPostfix] 268 | [HarmonyPatch( 269 | typeof(HSceneProc), 270 | nameof(HSceneProc.LoadAddTaii), 271 | [typeof(List)])] 272 | private static void LoadAddTaiiPostfix( 273 | object __instance, List param) 274 | { 275 | var hsceneTraverse = Traverse.Create(__instance); 276 | var dicExpAddTaii = hsceneTraverse 277 | .Field>>("dicExpAddTaii").Value; 278 | 279 | foreach (var item in dicExpAddTaii) 280 | { 281 | if (_dicExpAddTaii != null) 282 | { 283 | if (!_dicExpAddTaii.ContainsKey(item.Key)) 284 | { 285 | // Save the original dictionary for testing 286 | _dicExpAddTaii.Add( 287 | item.Key, new Dictionary(item.Value)); 288 | } 289 | } 290 | } 291 | } 292 | 293 | internal static bool AnimationCheckOk(HSprite hsprite, SwapAnimationInfo anim) 294 | { 295 | #if DEBUG 296 | if (TestMode.Value) 297 | { 298 | return true; 299 | } 300 | #endif 301 | if (hsprite.flags.isFreeH) 302 | { 303 | if (EnableAllFreeH.Value) 304 | { 305 | return true; 306 | } 307 | // Only show used animations in FreeH 308 | if (_usedAnimations.Keys.Count < 0) 309 | { 310 | return false; 311 | } 312 | if (!_usedAnimations.Keys.Contains(GetAnimationKey(anim))) 313 | { 314 | return false; 315 | } 316 | } 317 | 318 | #if DEBUG 319 | //if (CheckExperience(hsprite, anim) != CheckExperienceT(hsprite, anim)) 320 | //{ 321 | // Log.Level(BepInEx.Logging.LogLevel.Error, $"CheckExperience " + 322 | // $"original={CheckExperience(hsprite, anim)} " + 323 | // $"TryGet={CheckExperienceT(hsprite, anim)}"); 324 | //} 325 | #endif 326 | if (UseAnimationLevels.Value && !CheckExperienceT(hsprite, anim)) 327 | { 328 | // Not enough experience 329 | return false; 330 | } 331 | return true; 332 | } 333 | 334 | /// 335 | /// Checks heroine experience against ExpTaii of swap animation 336 | /// 337 | /// 338 | /// 339 | //internal static bool CheckExperience(HSprite hsprite, SwapAnimationInfo anim) 340 | //{ 341 | // var hExp = hsprite.flags.lstHeroine[0].hExp; 342 | // var expTaii = (double)anim.ExpTaii; 343 | // 344 | // if(_alDicExpAddTaii.ContainsKey(anim.Guid)) 345 | // { 346 | // if (_alDicExpAddTaii[anim.Guid].ContainsKey((int)anim.Mode) 347 | // && _alDicExpAddTaii[anim.Guid][(int)anim.Mode] 348 | // .ContainsKey($"{anim.ControllerFemale}{anim.StudioId}")) 349 | // { 350 | // expTaii = _alDicExpAddTaii[anim.Guid][(int)anim.Mode][$"{anim.ControllerFemale}{anim.StudioId}"]; 351 | // } 352 | // else 353 | // { 354 | // expTaii = -1; 355 | // } 356 | // } 357 | // if ((double)hExp >= expTaii) 358 | // { 359 | // return true; 360 | // } 361 | // return false; 362 | //} 363 | 364 | /// 365 | /// Checks heroine experience against ExpTaii of swap animation 366 | /// 367 | /// 368 | /// 369 | /// 370 | internal static bool CheckExperienceT(HSprite hsprite, SwapAnimationInfo anim) 371 | { 372 | var hExp = hsprite.flags.lstHeroine[0].hExp; 373 | var expTaii = (double)anim.ExpTaii; 374 | 375 | if (_alDicExpAddTaii.TryGetValue(anim.Guid, out var animGuid)) 376 | { 377 | if (animGuid.TryGetValue((int)anim.Mode, out var animMode)) 378 | { 379 | if (animMode.TryGetValue($"{anim.ControllerFemale}{anim.StudioId}", out var value)) 380 | { 381 | expTaii = value; 382 | } 383 | else 384 | { 385 | expTaii = -1; 386 | } 387 | } 388 | } 389 | 390 | if ((double)hExp >= expTaii) 391 | { 392 | return true; 393 | } 394 | return false; 395 | } 396 | 397 | } 398 | } 399 | } 400 | --------------------------------------------------------------------------------