diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg index 84ffd5a..8f2d4f9 100644 --- a/GameData/KSPCommunityFixes/Settings.cfg +++ b/GameData/KSPCommunityFixes/Settings.cfg @@ -459,6 +459,8 @@ KSP_COMMUNITY_FIXES // but this helps a bit in other cases as well. FloatingOriginPerf = true + PartParsingPerf = true + // ########################## // Modding // ########################## diff --git a/KSPCommunityFixes/BasePatch.cs b/KSPCommunityFixes/BasePatch.cs index 0857f67..5993150 100644 --- a/KSPCommunityFixes/BasePatch.cs +++ b/KSPCommunityFixes/BasePatch.cs @@ -43,6 +43,17 @@ internal class TranspileInDebugAttribute : Attribute { } + /// + /// When applied to a class derived from , the patch won't be automatically applied. + /// To apply the patch, call . Note that if that call happens before ModuleManager + /// has patched the configs (ie, before part compilation), must be overriden + /// to return , or the patch won't be applied. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + internal class ManualPatchAttribute : Attribute + { + } + public abstract class BasePatch { public static readonly string pluginData = "PluginData"; @@ -64,7 +75,7 @@ public static void Patch(Type patchType) if (!patch.IgnoreConfig && !KSPCommunityFixes.enabledPatches.Contains(patchType.Name)) { - Debug.Log($"[KSPCommunityFixes] Patch {patchType.Name} not applied (disabled in Settings.cfg)"); + Debug.Log($"[KSPCommunityFixes] Patch {patchType.Name} not applied (not enabled in Settings.cfg)"); return; } diff --git a/KSPCommunityFixes/KSPCommunityFixes.cs b/KSPCommunityFixes/KSPCommunityFixes.cs index 0853461..4be9996 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.cs +++ b/KSPCommunityFixes/KSPCommunityFixes.cs @@ -14,7 +14,6 @@ public class KSPCommunityFixes : MonoBehaviour public static string LOC_KSPCF_Title = "KSP Community Fixes"; - public static Harmony Harmony { get; private set; } public static HashSet enabledPatches = new HashSet(); @@ -50,6 +49,14 @@ public static Version KspVersion } } + static KSPCommunityFixes() + { + Harmony = new Harmony("KSPCommunityFixes"); +#if DEBUG + Harmony.DEBUG = true; +#endif + } + public static T GetPatchInstance() where T : BasePatch { if (!patchInstances.TryGetValue(typeof(T), out BasePatch instance)) @@ -72,11 +79,6 @@ void Start() DontDestroyOnLoad(this); } - Harmony = new Harmony("KSPCommunityFixes"); - -#if DEBUG - Harmony.DEBUG = true; -#endif LocalizationUtils.GenerateLocTemplateIfRequested(); LocalizationUtils.ParseLocalization(); @@ -113,10 +115,9 @@ public void MMPostLoadCallback() List patchesTypes = new List(); foreach (Type type in Assembly.GetAssembly(basePatchType).GetTypes()) { - if (!type.IsAbstract && type.IsSubclassOf(basePatchType)) + if (!type.IsAbstract && type.IsSubclassOf(basePatchType) && type.GetCustomAttribute() == null) { patchesTypes.Add(type); - } } diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj index f6e1cc7..bd72a13 100644 --- a/KSPCommunityFixes/KSPCommunityFixes.csproj +++ b/KSPCommunityFixes/KSPCommunityFixes.csproj @@ -1,4 +1,4 @@ - + @@ -155,6 +155,8 @@ + + @@ -162,6 +164,7 @@ + @@ -173,6 +176,7 @@ + @@ -254,6 +258,7 @@ + $(SolutionDir) diff --git a/KSPCommunityFixes/Library/MuParser.cs b/KSPCommunityFixes/Library/MuParser.cs new file mode 100644 index 0000000..63ecbfc --- /dev/null +++ b/KSPCommunityFixes/Library/MuParser.cs @@ -0,0 +1,1161 @@ +using PartToolsLib; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using UnityEngine; +using UnityEngine.Rendering; + +namespace KSPCommunityFixes.Library +{ + // For reference on my 5800X3D/DDR4, when disk reading isn't a factor and for loading 485MB worth of models : + // - The stock parser has a throughput of 120 MB/s + // - The KSPCF parser has a throughput of 290 MB/s + // Roadblocks to further optimizations : + // - We can't avoid making a copy of the mesh data, avoiding that would require Unity accepting Span in the various Mesh.Set*() methods + // - For some mesh data, the mu data layout doesn't match a continuous array of structs, so we have to parse those structs one by one + // - Animation parsing is inerently slow due to two strings having to be parsed for every curve, plus other hard to overcome inefficiencies + // - Ideally, the dummy* data structures should avoid manipulating strings, would use dictionaries instead of lists, and could be reused instead of re-instantatied for every model. + // Overall, further optimization probably won't be beneficial anyway, we will almost always be bottlenecked by disk read speed. + + /// + /// Reimplementation of the stock mu model format parser (PartReader). Roughly 60% faster. + /// + internal class MuParser + { + private static readonly UTF8Encoding decoder = new UTF8Encoding(); + private static char[] charBuffer; + + private static int[] intBuffer; + private static Vector2[] vector2Buffer; + private static Vector3[] vector3Buffer; + private static Vector4[] vector4Buffer; + private static Color32[] color32Buffer; + private static Keyframe[][] keyFrameBuffers; + + private static List matDummies; + private static PartReader.TextureDummyList textureDummies; + private static List boneDummies; + + private static string modelDirectoryUrl; + private static byte[] data; + private static unsafe byte* dataPtr; + private static int dataLength; + private static int index; + + private static int version; + + /// + /// Parse a mu model into a GameObject hierarchy + /// + /// GameData relative path to the folder containing the model. For a model known by its , this will be the urlFile.parent.url value. + /// A byte array containing the raw model file data + /// The length of the data in the array. Ignored if zero. + public static unsafe GameObject Parse(string modelDirectoryUrl, byte[] data, int dataLength = 0) + { + if (matDummies == null) + matDummies = new List(); + + if (textureDummies == null) + textureDummies = new PartReader.TextureDummyList(); + + if (boneDummies == null) + boneDummies = new List(); + + GameObject model; + + GCHandle pinnedArray = GCHandle.Alloc(data, GCHandleType.Pinned); + + try + { + MuParser.modelDirectoryUrl = modelDirectoryUrl; + MuParser.data = data; + MuParser.dataLength = dataLength <= 0 ? data.Length : dataLength; + dataPtr = (byte*)pinnedArray.AddrOfPinnedObject(); + index = 0; + + if (ReadInt() != 76543) + throw new Exception("Invalid mu file"); + + version = ReadInt(); + SkipString(); + + model = ReadChild(null); + AffectSkinnedMeshRenderersBones(model); + } + catch (Exception e) + { + model = null; + Debug.LogError($"Model {modelDirectoryUrl} error: {e.Message}\n{e.StackTrace}"); + } + finally + { + pinnedArray.Free(); + MuParser.matDummies.Clear(); + MuParser.boneDummies.Clear(); + MuParser.textureDummies.Clear(); + MuParser.modelDirectoryUrl = null; + MuParser.data = null; + MuParser.dataPtr = null; + MuParser.dataLength = 0; + MuParser.index = 0; + MuParser.version = 0; + } + + return model; + } + + /// + /// Call this to release the memory used by the static buffers. + /// This is safe to use at any point. + /// + public static void ReleaseBuffers() + { + intBuffer = null; + vector2Buffer = null; + vector3Buffer = null; + vector4Buffer = null; + color32Buffer = null; + keyFrameBuffers = null; + charBuffer = null; + matDummies = null; + textureDummies = null; + boneDummies = null; + } + + #region Core methods + + private static GameObject ReadChild(Transform parent) + { + GameObject gameObject = new GameObject(ReadString()); + + gameObject.transform.parent = parent; + gameObject.transform.localPosition = ReadVector3(); + gameObject.transform.localRotation = ReadQuaternion(); + gameObject.transform.localScale = ReadVector3(); + + while (index < dataLength) + { + switch (ReadInt()) + { + case 0: + ReadChild(gameObject.transform); + break; + case 2: + ReadAnimation(gameObject); + break; + case 3: + ReadMeshCollider(gameObject); + break; + case 4: + ReadSphereCollider(gameObject); + break; + case 5: + ReadCapsuleCollider(gameObject); + break; + case 6: + ReadBoxCollider(gameObject); + break; + case 7: + ReadMeshFilter(gameObject); + break; + case 8: + ReadMeshRenderer(gameObject); + break; + case 9: + ReadSkinnedMeshRenderer(gameObject); + break; + case 10: + ReadMaterials(); + break; + case 12: + ReadTextures(gameObject); + break; + case 23: + ReadLight(gameObject); + break; + case 24: + ReadTagAndLayer(gameObject); + break; + case 25: + ReadMeshCollider2(gameObject); + break; + case 26: + ReadSphereCollider2(gameObject); + break; + case 27: + ReadCapsuleCollider2(gameObject); + break; + case 28: + ReadBoxCollider2(gameObject); + break; + case 29: + ReadWheelCollider(gameObject); + break; + case 30: + ReadCamera(gameObject); + break; + case 31: + ReadParticles(gameObject); + break; + case 1: + return gameObject; + } + } + return gameObject; + } + + private static void AffectSkinnedMeshRenderersBones(GameObject model) + { + if (boneDummies.Count > 0) + { + int i = 0; + for (int count = boneDummies.Count; i < count; i++) + { + Transform[] array = new Transform[boneDummies[i].bones.Count]; + int j = 0; + for (int count2 = boneDummies[i].bones.Count; j < count2; j++) + { + array[j] = FindChildByName(model.transform, boneDummies[i].bones[j]); + } + boneDummies[i].smr.bones = array; + } + } + } + + private static Transform FindChildByName(Transform parent, string name) + { + if (parent.name == name) + { + return parent; + } + foreach (Transform item in parent) + { + Transform transform = FindChildByName(item, name); + if (transform != null) + { + return transform; + } + } + return null; + } + + #endregion + + #region Component parsers + + private static void ReadAnimation(GameObject o) + { + Animation animation = o.AddComponent(); + int clipCount = ReadInt(); + bool isInvalid = false; + for (int i = 0; i < clipCount; i++) + { + AnimationClip animationClip = new AnimationClip(); + animationClip.legacy = true; + string clipName = ReadString(); + animationClip.localBounds = new Bounds(ReadVector3(), ReadVector3()); + animationClip.wrapMode = (WrapMode)ReadInt(); + + int curveCount = ReadInt(); + for (int j = 0; j < curveCount; j++) + { + string curvePath = ReadString(); + string curveProperty = ReadString(); + Type curveType = null; + switch (ReadInt()) + { + case 0: + curveType = typeof(Transform); + break; + case 1: + curveType = typeof(Material); + break; + case 2: + curveType = typeof(Light); + break; + case 3: + curveType = typeof(AudioSource); + break; + } + WrapMode preWrapMode = (WrapMode)ReadInt(); + WrapMode postWrapMode = (WrapMode)ReadInt(); + + int keyFrameCount = ReadInt(); + Keyframe[] keyFrames = GetKeyFrameBuffer(keyFrameCount); + for (int k = 0; k < keyFrameCount; k++) + keyFrames[k] = ReadKeyFrame(); + + AnimationCurve animationCurve = new AnimationCurve(keyFrames); + animationCurve.preWrapMode = preWrapMode; + animationCurve.postWrapMode = postWrapMode; + + if (clipName == null || curvePath == null || curveType == null || curveProperty == null) + { + isInvalid = true; + Debug.LogWarning($"{clipName ?? "Null clipName"} : {curvePath ?? "Null curvePath"}, {(curveType == null ? "Null curveType" : curveType.ToString())}, {curveProperty ?? "Null curveProperty"}"); + continue; + } + + animationClip.SetCurve(curvePath, curveType, curveProperty, animationCurve); + } + if (!isInvalid) + { + animation.AddClip(animationClip, clipName); + } + } + string defaultclipName = ReadString(); + if (defaultclipName != string.Empty && !isInvalid) + animation.clip = animation.GetClip(defaultclipName); + + animation.playAutomatically = ReadBool(); + } + + /// + /// Usually, the curve will have less than 10 keyframes, this method will return a + /// cached buffer instead of instantiatiating a new one in such cases. + /// + private static Keyframe[] GetKeyFrameBuffer(int keyFrameCount) + { + if (keyFrameBuffers == null) + { + keyFrameBuffers = new Keyframe[10][]; + keyFrameBuffers[0] = new Keyframe[0]; + keyFrameBuffers[1] = new Keyframe[1]; + keyFrameBuffers[2] = new Keyframe[2]; + keyFrameBuffers[3] = new Keyframe[3]; + keyFrameBuffers[4] = new Keyframe[4]; + keyFrameBuffers[5] = new Keyframe[5]; + keyFrameBuffers[6] = new Keyframe[6]; + keyFrameBuffers[7] = new Keyframe[7]; + keyFrameBuffers[8] = new Keyframe[8]; + keyFrameBuffers[9] = new Keyframe[9]; + } + + if (keyFrameCount < 10) + return keyFrameBuffers[keyFrameCount]; + + return new Keyframe[keyFrameCount]; + } + + private static void ReadMeshCollider(GameObject o) + { + MeshCollider meshCollider = o.AddComponent(); + SkipBool(); // this is actually the "convex" property, but it is always forced to true + meshCollider.convex = true; + meshCollider.sharedMesh = ReadMesh(); + } + + private static void ReadSphereCollider(GameObject o) + { + SphereCollider sphereCollider = o.AddComponent(); + sphereCollider.radius = ReadFloat(); + sphereCollider.center = ReadVector3(); + } + + private static void ReadCapsuleCollider(GameObject o) + { + CapsuleCollider capsuleCollider = o.AddComponent(); + capsuleCollider.radius = ReadFloat(); + capsuleCollider.direction = ReadInt(); + capsuleCollider.center = ReadVector3(); + } + + private static void ReadBoxCollider(GameObject o) + { + BoxCollider boxCollider = o.AddComponent(); + boxCollider.size = ReadVector3(); + boxCollider.center = ReadVector3(); + } + + private static void ReadMeshFilter(GameObject o) + { + o.AddComponent().sharedMesh = ReadMesh(); + } + + private static void ReadMeshRenderer(GameObject o) + { + MeshRenderer meshRenderer = o.AddComponent(); + if (version >= 1) + { + meshRenderer.shadowCastingMode = ReadBool() ? ShadowCastingMode.On : ShadowCastingMode.Off; + meshRenderer.receiveShadows = ReadBool(); + } + int rendererCount = ReadInt(); + for (int i = 0; i < rendererCount; i++) + { + int materialCount = ReadInt(); + while (materialCount >= matDummies.Count) + matDummies.Add(new PartReader.MaterialDummy()); + + matDummies[materialCount].renderers.Add(meshRenderer); + } + } + + private static void ReadSkinnedMeshRenderer(GameObject o) + { + SkinnedMeshRenderer skinnedMeshRenderer = o.AddComponent(); + int rendererCount = ReadInt(); + for (int i = 0; i < rendererCount; i++) + { + int materialCount = ReadInt(); + while (materialCount >= matDummies.Count) + { + matDummies.Add(new PartReader.MaterialDummy()); + } + matDummies[materialCount].renderers.Add(skinnedMeshRenderer); + } + skinnedMeshRenderer.localBounds = new Bounds(ReadVector3(), ReadVector3()); + skinnedMeshRenderer.quality = (SkinQuality)ReadInt(); + skinnedMeshRenderer.updateWhenOffscreen = ReadBool(); + int num3 = ReadInt(); + + PartReader.BonesDummy bonesDummy = new PartReader.BonesDummy(); + bonesDummy.smr = skinnedMeshRenderer; + for (int j = 0; j < num3; j++) + { + bonesDummy.bones.Add(ReadString()); + } + boneDummies.Add(bonesDummy); + skinnedMeshRenderer.sharedMesh = ReadMesh(); + } + + private static void ReadMaterials() + { + int materialCount = ReadInt(); + for (int i = 0; i < materialCount; i++) + { + PartReader.MaterialDummy materialDummy = matDummies[i]; + Material material = version < 4 ? ReadMaterial() : ReadMaterial4(); + + for (int j = materialDummy.renderers.Count; j-- > 0;) + materialDummy.renderers[j].sharedMaterial = material; + + for (int j = materialDummy.particleEmitters.Count; j-- > 0;) + materialDummy.particleEmitters[j].material = material; + } + } + + private static Material ReadMaterial() + { + string name = ReadString(); + ShaderType shaderType = (ShaderType)ReadInt(); + Material material = new Material(ShaderHelpers.GetShader(shaderType)); + switch (shaderType) + { + default: + + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + break; + case ShaderType.Specular: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetColor(ShaderHelpers.SpecColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.ShininessPropId, ReadFloat()); + break; + case ShaderType.Bumped: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + ReadMaterialTexture(material, "_BumpMap", ShaderHelpers.BumpMapPropId); + break; + case ShaderType.BumpedSpecular: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + ReadMaterialTexture(material, "_BumpMap", ShaderHelpers.BumpMapPropId); + material.SetColor(ShaderHelpers.SpecColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.ShininessPropId, ReadFloat()); + break; + case ShaderType.Emissive: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + ReadMaterialTexture(material, "_Emissive", ShaderHelpers.EmissivePropId); + material.SetColor(PropertyIDs._EmissiveColor, ReadColor()); + break; + case ShaderType.EmissiveSpecular: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetColor(ShaderHelpers.SpecColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.ShininessPropId, ReadFloat()); + ReadMaterialTexture(material, "_Emissive", ShaderHelpers.EmissivePropId); + material.SetColor(PropertyIDs._EmissiveColor, ReadColor()); + break; + case ShaderType.EmissiveBumpedSpecular: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + ReadMaterialTexture(material, "_BumpMap", ShaderHelpers.BumpMapPropId); + material.SetColor(ShaderHelpers.SpecColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.ShininessPropId, ReadFloat()); + ReadMaterialTexture(material, "_Emissive", ShaderHelpers.EmissivePropId); + material.SetColor(PropertyIDs._EmissiveColor, ReadColor()); + break; + case ShaderType.AlphaCutout: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetFloat(ShaderHelpers.CutoffPropId, ReadFloat()); + break; + case ShaderType.AlphaCutoutBumped: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + ReadMaterialTexture(material, "_BumpMap", ShaderHelpers.BumpMapPropId); + material.SetFloat(ShaderHelpers.CutoffPropId, ReadFloat()); + break; + case ShaderType.Alpha: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + break; + case ShaderType.AlphaSpecular: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetFloat(ShaderHelpers.GlossPropId, ReadFloat()); + material.SetColor(ShaderHelpers.SpecColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.ShininessPropId, ReadFloat()); + break; + case ShaderType.AlphaUnlit: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetColor(ShaderHelpers.ColorPropId, ReadColor()); + break; + case ShaderType.Unlit: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetColor(ShaderHelpers.ColorPropId, ReadColor()); + break; + case ShaderType.ParticleAlpha: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetColor(ShaderHelpers.ColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.InvFadePropId, ReadFloat()); + break; + case ShaderType.ParticleAdditive: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + material.SetColor(ShaderHelpers.ColorPropId, ReadColor()); + material.SetFloat(ShaderHelpers.InvFadePropId, ReadFloat()); + break; + case ShaderType.BumpedSpecularMap: + ReadMaterialTexture(material, "_MainTex", ShaderHelpers.MainTexPropId); + ReadMaterialTexture(material, "_BumpMap", ShaderHelpers.BumpMapPropId); + ReadMaterialTexture(material, "_SpecMap", ShaderHelpers.SpecMapPropId); + material.SetFloat(ShaderHelpers.SpecTintPropId, ReadFloat()); + material.SetFloat(ShaderHelpers.ShininessPropId, ReadFloat()); + break; + } + + material.name = name; + return material; + } + + private static Material ReadMaterial4() + { + string matName = ReadString(); + string shaderName = ReadString(); + int propertyCount = ReadInt(); + Material material = new Material(Shader.Find(shaderName)); + material.name = matName; + for (int i = 0; i < propertyCount; i++) + { + string text = ReadString(); + switch (ReadInt()) + { + case 0: + material.SetColor(text, ReadColor()); + break; + case 1: + material.SetVector(text, ReadVector4()); + break; + case 2: + material.SetFloat(text, ReadFloat()); + break; + case 3: + material.SetFloat(text, ReadFloat()); + break; + case 4: + ReadMaterialTexture(material, text, Shader.PropertyToID(text)); + break; + } + } + return material; + } + + private static void ReadMaterialTexture(Material mat, string textureName, int textureId) + { + // we would need to reimplement the whole texture/material dummy thing to get ride of + // having to use the texture name. Probably doesn't matter much... + textureDummies.AddTextureDummy(ReadInt(), mat, textureName); + mat.SetTextureScale(textureId, ReadVector2()); + mat.SetTextureOffset(textureId, ReadVector2()); + } + + private static void ReadTextures(GameObject o) + { + int texCount = ReadInt(); + if (texCount != textureDummies.Count) + { + Debug.LogError("TextureError: " + texCount + " " + textureDummies.Count); + return; + } + for (int i = 0; i < texCount; i++) + { + string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(ReadString()); + TextureType textureType = (TextureType)ReadInt(); + string url = modelDirectoryUrl + "/" + fileNameWithoutExtension; + Texture2D texture = GameDatabase.Instance.GetTexture(url, textureType == TextureType.NormalMap); + if (texture.IsNullOrDestroyed()) + { + Debug.LogError($"Texture '{url}' not found!"); + continue; + } + int j = 0; + for (int materialCount = textureDummies[i].Count; j < materialCount; j++) + { + PartReader.TextureMaterialDummy textureMaterialDummy = textureDummies[i][j]; + int k = 0; + for (int texPropCount = textureMaterialDummy.shaderName.Count; k < texPropCount; k++) + { + string texProperty = textureMaterialDummy.shaderName[k]; + textureMaterialDummy.material.SetTexture(texProperty, texture); + } + } + } + } + + private static void ReadLight(GameObject o) + { + Light light = o.AddComponent(); + + light.type = (LightType)ReadInt(); + light.intensity = ReadFloat(); + light.range = ReadFloat(); + light.color = ReadColor(); + light.cullingMask = ReadInt(); + + if (version > 1) + light.spotAngle = ReadFloat(); + } + + private static void ReadTagAndLayer(GameObject o) + { + o.tag = ReadString(); + o.layer = ReadInt(); + } + + private static void ReadMeshCollider2(GameObject o) + { + MeshCollider meshCollider = o.AddComponent(); + meshCollider.convex = true; + meshCollider.isTrigger = ReadBool(); + SkipBool(); // this is actually the "convex" property, but it is always forced to true; + meshCollider.sharedMesh = ReadMesh(); + } + + private static void ReadSphereCollider2(GameObject o) + { + SphereCollider sphereCollider = o.AddComponent(); + sphereCollider.isTrigger = ReadBool(); + sphereCollider.radius = ReadFloat(); + sphereCollider.center = ReadVector3(); + } + + private static void ReadCapsuleCollider2(GameObject o) + { + CapsuleCollider capsuleCollider = o.AddComponent(); + capsuleCollider.isTrigger = ReadBool(); + capsuleCollider.radius = ReadFloat(); + capsuleCollider.height = ReadFloat(); + capsuleCollider.direction = ReadInt(); + capsuleCollider.center = ReadVector3(); + } + + private static void ReadBoxCollider2(GameObject o) + { + BoxCollider boxCollider = o.AddComponent(); + boxCollider.isTrigger = ReadBool(); + boxCollider.size = ReadVector3(); + boxCollider.center = ReadVector3(); + } + + private static void ReadWheelCollider(GameObject o) + { + WheelCollider wheelCollider = o.AddComponent(); + wheelCollider.mass = ReadFloat(); + wheelCollider.radius = ReadFloat(); + wheelCollider.suspensionDistance = ReadFloat(); + wheelCollider.center = ReadVector3(); + wheelCollider.suspensionSpring = new JointSpring + { + spring = ReadFloat(), + damper = ReadFloat(), + targetPosition = ReadFloat() + }; + wheelCollider.forwardFriction = new WheelFrictionCurve + { + extremumSlip = ReadFloat(), + extremumValue = ReadFloat(), + asymptoteSlip = ReadFloat(), + asymptoteValue = ReadFloat(), + stiffness = ReadFloat() + }; + wheelCollider.sidewaysFriction = new WheelFrictionCurve + { + extremumSlip = ReadFloat(), + extremumValue = ReadFloat(), + asymptoteSlip = ReadFloat(), + asymptoteValue = ReadFloat(), + stiffness = ReadFloat() + }; + wheelCollider.enabled = false; + } + + private static void ReadCamera(GameObject o) + { + Camera camera = o.AddComponent(); + camera.clearFlags = (CameraClearFlags)ReadInt(); + camera.backgroundColor = ReadColor(); + camera.cullingMask = ReadInt(); + camera.orthographic = ReadBool(); + camera.fieldOfView = ReadFloat(); + camera.nearClipPlane = ReadFloat(); + camera.farClipPlane = ReadFloat(); + camera.depth = ReadFloat(); + camera.allowHDR = false; + camera.enabled = false; + } + + private static void ReadParticles(GameObject o) + { + KSPParticleEmitter kSPParticleEmitter = o.AddComponent(); + kSPParticleEmitter.emit = ReadBool(); + kSPParticleEmitter.shape = (KSPParticleEmitter.EmissionShape)ReadInt(); + kSPParticleEmitter.shape3D.x = ReadFloat(); + kSPParticleEmitter.shape3D.y = ReadFloat(); + kSPParticleEmitter.shape3D.z = ReadFloat(); + kSPParticleEmitter.shape2D.x = ReadFloat(); + kSPParticleEmitter.shape2D.y = ReadFloat(); + kSPParticleEmitter.shape1D = ReadFloat(); + kSPParticleEmitter.color = ReadColor(); + kSPParticleEmitter.useWorldSpace = ReadBool(); + kSPParticleEmitter.minSize = ReadFloat(); + kSPParticleEmitter.maxSize = ReadFloat(); + kSPParticleEmitter.minEnergy = ReadFloat(); + kSPParticleEmitter.maxEnergy = ReadFloat(); + kSPParticleEmitter.minEmission = ReadInt(); + kSPParticleEmitter.maxEmission = ReadInt(); + kSPParticleEmitter.worldVelocity.x = ReadFloat(); + kSPParticleEmitter.worldVelocity.y = ReadFloat(); + kSPParticleEmitter.worldVelocity.z = ReadFloat(); + kSPParticleEmitter.localVelocity.x = ReadFloat(); + kSPParticleEmitter.localVelocity.y = ReadFloat(); + kSPParticleEmitter.localVelocity.z = ReadFloat(); + kSPParticleEmitter.rndVelocity.x = ReadFloat(); + kSPParticleEmitter.rndVelocity.y = ReadFloat(); + kSPParticleEmitter.rndVelocity.z = ReadFloat(); + kSPParticleEmitter.emitterVelocityScale = ReadFloat(); + kSPParticleEmitter.angularVelocity = ReadFloat(); + kSPParticleEmitter.rndAngularVelocity = ReadFloat(); + kSPParticleEmitter.rndRotation = ReadBool(); + kSPParticleEmitter.doesAnimateColor = ReadBool(); + kSPParticleEmitter.colorAnimation = new Color[5]; + for (int i = 0; i < 5; i++) + { + kSPParticleEmitter.colorAnimation[i] = ReadColor(); + } + kSPParticleEmitter.worldRotationAxis.x = ReadFloat(); + kSPParticleEmitter.worldRotationAxis.y = ReadFloat(); + kSPParticleEmitter.worldRotationAxis.z = ReadFloat(); + kSPParticleEmitter.localRotationAxis.x = ReadFloat(); + kSPParticleEmitter.localRotationAxis.y = ReadFloat(); + kSPParticleEmitter.localRotationAxis.z = ReadFloat(); + kSPParticleEmitter.sizeGrow = ReadFloat(); + kSPParticleEmitter.rndForce.x = ReadFloat(); + kSPParticleEmitter.rndForce.y = ReadFloat(); + kSPParticleEmitter.rndForce.z = ReadFloat(); + kSPParticleEmitter.force.x = ReadFloat(); + kSPParticleEmitter.force.y = ReadFloat(); + kSPParticleEmitter.force.z = ReadFloat(); + kSPParticleEmitter.damping = ReadFloat(); + kSPParticleEmitter.castShadows = ReadBool(); + kSPParticleEmitter.recieveShadows = ReadBool(); + kSPParticleEmitter.lengthScale = ReadFloat(); + kSPParticleEmitter.velocityScale = ReadFloat(); + kSPParticleEmitter.maxParticleSize = ReadFloat(); + switch (ReadInt()) + { + default: + kSPParticleEmitter.particleRenderMode = ParticleSystemRenderMode.Billboard; + break; + case 3: + kSPParticleEmitter.particleRenderMode = ParticleSystemRenderMode.Stretch; + break; + case 4: + kSPParticleEmitter.particleRenderMode = ParticleSystemRenderMode.HorizontalBillboard; + break; + case 5: + kSPParticleEmitter.particleRenderMode = ParticleSystemRenderMode.VerticalBillboard; + break; + } + kSPParticleEmitter.uvAnimationXTile = ReadInt(); + kSPParticleEmitter.uvAnimationYTile = ReadInt(); + kSPParticleEmitter.uvAnimationCycles = ReadInt(); + int num = ReadInt(); + while (num >= matDummies.Count) + { + matDummies.Add(new PartReader.MaterialDummy()); + } + matDummies[num].particleEmitters.Add(kSPParticleEmitter); + } + + #endregion + + #region Mesh parsing + + private static Mesh ReadMesh() + { + Mesh mesh = new Mesh(); + EntryType entryType = (EntryType)ReadInt(); + if (entryType != EntryType.MeshStart) + { + Debug.LogError("Mesh Error"); + return null; + } + + int size = ReadInt(); + SkipInt(); + + int subMeshIndex = 0; + while ((entryType = (EntryType)ReadInt()) != EntryType.MeshEnd) + { + switch (entryType) + { + case EntryType.MeshVertexColors: + { + FillColor32Buffer(size); + mesh.SetColors(color32Buffer, 0, size); + + break; + } + case EntryType.MeshVerts: + { + FillVector3Buffer(size); + mesh.SetVertices(vector3Buffer, 0, size); + + break; + } + case EntryType.MeshUV: + { + FillVector2Buffer(size); + mesh.SetUVs(0, vector2Buffer, 0, size); + break; + } + case EntryType.MeshUV2: + { + FillVector2Buffer(size); + mesh.SetUVs(1, vector2Buffer, 0, size); + break; + } + case EntryType.MeshNormals: + { + FillVector3Buffer(size); + mesh.SetNormals(vector3Buffer, 0, size); + break; + } + case EntryType.MeshTangents: + { + FillVector4Buffer(size); + mesh.SetTangents(vector4Buffer, 0, size); + break; + } + case EntryType.MeshTriangles: + { + if (mesh.subMeshCount == subMeshIndex) + mesh.subMeshCount++; + + int triangleCount = ReadInt(); + FillIntBuffer(triangleCount); + mesh.SetTriangles(intBuffer, 0, triangleCount, subMeshIndex); + subMeshIndex++; + break; + } + case EntryType.MeshBoneWeights: + { + BoneWeight[] boneWeights = new BoneWeight[size]; + for (int i = 0; i < size; i++) + boneWeights[i] = ReadBoneWeight(); + + mesh.boneWeights = boneWeights; + break; + } + case EntryType.MeshBindPoses: + { + int bindPosesCount = ReadInt(); + Matrix4x4[] bindPoses = new Matrix4x4[bindPosesCount]; + for (int i = 0; i < bindPosesCount; i++) + bindPoses[i] = ReadMatrix4x4(); + + mesh.bindposes = bindPoses; + break; + } + } + } + mesh.RecalculateBounds(); + return mesh; + } + + // note : to fill mesh data, we take advantage of the fact that the various + // structs (int, vector3, etc) are sequentially packed in the raw data array. + // Ideally, we would pass the raw data array with a pointer offset to the various + // mesh.Set*() methods, but that would require those methods accepting a Span + // that we would built from an arbitrary pointer offset. + // So we fallback to a memcopy of the raw data to a buffer array that we then + // pass to the unity methods. + + private static unsafe void FillIntBuffer(int intCount) + { + int byteCount = intCount * 4; + int valIdx = Advance(byteCount); + + if (intBuffer == null || intBuffer.Length < intCount) + intBuffer = new int[(int)(intCount * 1.5)]; + + fixed (int* intBufferPtr = intBuffer) + Buffer.MemoryCopy(dataPtr + valIdx, intBufferPtr, byteCount, byteCount); + } + + private static unsafe void FillVector2Buffer(int vector2Count) + { + int byteCount = vector2Count * 8; + int valIdx = Advance(byteCount); + + if (vector2Buffer == null || vector2Buffer.Length < vector2Count) + vector2Buffer = new Vector2[(int)(vector2Count * 1.5)]; + + fixed (Vector2* vector2BufferPtr = vector2Buffer) + Buffer.MemoryCopy(dataPtr + valIdx, vector2BufferPtr, byteCount, byteCount); + } + + private static unsafe void FillVector3Buffer(int vector3Count) + { + int byteCount = vector3Count * 12; + int valIdx = Advance(byteCount); + + if (vector3Buffer == null || vector3Buffer.Length < vector3Count) + vector3Buffer = new Vector3[(int)(vector3Count * 1.5)]; + + fixed (Vector3* vector3BufferPtr = vector3Buffer) + Buffer.MemoryCopy(dataPtr + valIdx, vector3BufferPtr, byteCount, byteCount); + } + + private static unsafe void FillVector4Buffer(int vector4Count) + { + int byteCount = vector4Count * 16; + int valIdx = Advance(byteCount); + + if (vector4Buffer == null || vector4Buffer.Length < vector4Count) + vector4Buffer = new Vector4[(int)(vector4Count * 1.5)]; + + fixed (Vector4* vector4BufferPtr = vector4Buffer) + Buffer.MemoryCopy(dataPtr + valIdx, vector4BufferPtr, byteCount, byteCount); + } + + private static unsafe void FillColor32Buffer(int color32Count) + { + int byteCount = color32Count * 4; + int valIdx = Advance(byteCount); + + if (color32Buffer == null || color32Buffer.Length < color32Count) + color32Buffer = new Color32[(int)(color32Count * 1.5)]; + + fixed (Color32* color32BufferPtr = color32Buffer) + Buffer.MemoryCopy(dataPtr + valIdx, color32BufferPtr, byteCount, byteCount); + } + + + #endregion + + #region Base binary reader infrastructure + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Advance(int bytes) + { + int currentIndex = index; + int nextIndex = currentIndex + bytes; + if (nextIndex > dataLength) + ThrowEndOfDataException(); + + index = nextIndex; + return currentIndex; + } + + private static void ThrowEndOfDataException() + { + throw new InvalidOperationException("Unable to read beyond the end of the data"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe byte ReadByte() + { + int valIdx = Advance(1); + return *(dataPtr + valIdx); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SkipInt() + { + Advance(4); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe int ReadInt() + { + int valIdx = Advance(4); + return *(int*)(dataPtr + valIdx); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe float ReadFloat() + { + int valIdx = Advance(4); + return *(float*)(dataPtr + valIdx); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SkipBool() + { + Advance(1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe bool ReadBool() + { + int valIdx = Advance(1); + return *(dataPtr + valIdx) != 0; + } + + private static void SkipString() + { + Advance(Read7BitEncodedInt()); + } + + private static string ReadString() + { + int strByteLength = Read7BitEncodedInt(); + + if (strByteLength < 0) + throw new Exception("Invalid string length"); + + if (strByteLength == 0) + return string.Empty; + + ExpandCharBuffer(strByteLength); + + int currentPos = index; + Advance(strByteLength); + + int charCount = decoder.GetChars(data, currentPos, strByteLength, charBuffer, 0); + return new string(charBuffer, 0, charCount); + } + + private static void ExpandCharBuffer(int length) + { + if (charBuffer == null || charBuffer.Length < length) + charBuffer = new char[(int)(length * 1.5)]; + } + + private static int Read7BitEncodedInt() + { + int num = 0; + int num2 = 0; + byte b; + do + { + if (num2 == 35) + { + throw new FormatException("Too many bytes in what should have been a 7 bit encoded Int32."); + } + b = ReadByte(); + num |= (b & 0x7F) << num2; + num2 += 7; + } + while ((b & 0x80u) != 0); + return num; + } + + private static unsafe Vector2 ReadVector2() + { + int valIdx = Advance(8); + return *(Vector2*)(dataPtr + valIdx); + } + + private static unsafe Vector3 ReadVector3() + { + int valIdx = Advance(12); + return *(Vector3*)(dataPtr + valIdx); + } + + private static unsafe Vector4 ReadVector4() + { + int valIdx = Advance(16); + return *(Vector4*)(dataPtr + valIdx); + } + + private static unsafe Quaternion ReadQuaternion() + { + int valIdx = Advance(16); + return *(Quaternion*)(dataPtr + valIdx); + } + + private static unsafe Color ReadColor() + { + int valIdx = Advance(16); + return *(Color*)(dataPtr + valIdx); + } + + private static unsafe Color32 ReadColor32() + { + int valIdx = Advance(4); + return *(Color32*)(dataPtr + valIdx); + } + + private static unsafe BoneWeight ReadBoneWeight() + { + int valIdx = Advance(32); + // data isn't packed with the same layout as the struct, so we fallback to setting every field + return new BoneWeight() + { + boneIndex0 = *(int*)(dataPtr + valIdx), + weight0 = *(float*)(dataPtr + valIdx + 4), + boneIndex1 = *(int*)(dataPtr + valIdx + 8), + weight1 = *(float*)(dataPtr + valIdx + 12), + boneIndex2 = *(int*)(dataPtr + valIdx + 16), + weight2 = *(float*)(dataPtr + valIdx + 20), + boneIndex3 = *(int*)(dataPtr + valIdx + 24), + weight3 = *(float*)(dataPtr + valIdx + 28) + }; + } + + private static unsafe Matrix4x4 ReadMatrix4x4() + { + int valIdx = Advance(64); + // data isn't packed with the same layout as the struct, so we fallback to setting every field + return new Matrix4x4() + { + m00 = *(float*)(dataPtr + valIdx), + m01 = *(float*)(dataPtr + valIdx + 4), + m02 = *(float*)(dataPtr + valIdx + 8), + m03 = *(float*)(dataPtr + valIdx + 12), + m10 = *(float*)(dataPtr + valIdx + 16), + m11 = *(float*)(dataPtr + valIdx + 20), + m12 = *(float*)(dataPtr + valIdx + 24), + m13 = *(float*)(dataPtr + valIdx + 28), + m20 = *(float*)(dataPtr + valIdx + 32), + m21 = *(float*)(dataPtr + valIdx + 36), + m22 = *(float*)(dataPtr + valIdx + 40), + m23 = *(float*)(dataPtr + valIdx + 44), + m30 = *(float*)(dataPtr + valIdx + 48), + m31 = *(float*)(dataPtr + valIdx + 52), + m32 = *(float*)(dataPtr + valIdx + 56), + m33 = *(float*)(dataPtr + valIdx + 60) + }; + } + + private static unsafe Keyframe ReadKeyFrame() + { + // this is encoded as 4 floats (16 bytes), but there is 4 bytes of padding at the end + int valIdx = Advance(20); + return new Keyframe( + *(float*)(dataPtr + valIdx), + *(float*)(dataPtr + valIdx + 4), + *(float*)(dataPtr + valIdx + 8), + *(float*)(dataPtr + valIdx + 12)); + } + + #endregion + } +} diff --git a/KSPCommunityFixes/Library/ShaderHelpers.cs b/KSPCommunityFixes/Library/ShaderHelpers.cs new file mode 100644 index 0000000..3b11f4d --- /dev/null +++ b/KSPCommunityFixes/Library/ShaderHelpers.cs @@ -0,0 +1,128 @@ +using PartToolsLib; +using System.Collections.Generic; +using UnityEngine; + +namespace KSPCommunityFixes.Library +{ + internal class ShaderHelpers + { + private static bool shadersPopulated = false; + + private static Shader _kspDiffuse; + private static Shader _kspSpecular; + private static Shader _kspBumped; + private static Shader _kspBumpedSpecular; + private static Shader _kspEmissiveDiffuse; + private static Shader _kspEmissiveSpecular; + private static Shader _kspEmissiveBumpedSpecular; + private static Shader _kspAlphaCutoff; + private static Shader _kspAlphaCutoffBumped; + private static Shader _kspAlphaTranslucent; + private static Shader _kspAlphaTranslucentSpecular; + private static Shader _kspAlphaUnlitTransparent; + private static Shader _kspUnlit; + private static Shader _kspParticlesAlphaBlended; + private static Shader _kspParticulesAdditive; + private static Shader _kspBumpedSpecularMapped; + + private static void PopulateShaders() + { + PopulateShader("KSP/Diffuse", ref _kspDiffuse); + PopulateShader("KSP/Specular", ref _kspSpecular); + PopulateShader("KSP/Bumped", ref _kspBumped); + PopulateShader("KSP/Bumped Specular", ref _kspBumpedSpecular); + PopulateShader("KSP/Emissive/Diffuse", ref _kspEmissiveDiffuse); + PopulateShader("KSP/Emissive/Specular", ref _kspEmissiveSpecular); + PopulateShader("KSP/Emissive/Bumped Specular", ref _kspEmissiveBumpedSpecular); + PopulateShader("KSP/Alpha/Cutoff", ref _kspAlphaCutoff); + PopulateShader("KSP/Alpha/Cutoff Bumped", ref _kspAlphaCutoffBumped); + PopulateShader("KSP/Alpha/Translucent", ref _kspAlphaTranslucent); + PopulateShader("KSP/Alpha/Translucent Specular", ref _kspAlphaTranslucentSpecular); + PopulateShader("KSP/Alpha/Unlit Transparent", ref _kspAlphaUnlitTransparent); + PopulateShader("KSP/Unlit", ref _kspUnlit); + PopulateShader("KSP/Particles/Alpha Blended", ref _kspParticlesAlphaBlended); + PopulateShader("KSP/Particles/Additive", ref _kspParticulesAdditive); + PopulateShader("KSP/Bumped Specular (Mapped)", ref _kspBumpedSpecularMapped); + shadersPopulated = true; + } + + private static void PopulateShader(string shaderName, ref Shader shaderRef) + { + shaderRef = Shader.Find(shaderName); + shaders[shaderName] = shaderRef; + } + + private static Dictionary shaders = new Dictionary(); + + public static Shader GetShader(ShaderType shaderType) + { + if (!shadersPopulated) + PopulateShaders(); + + switch (shaderType) + { + case ShaderType.Diffuse: + default: + return _kspDiffuse; + case ShaderType.Specular: + return _kspSpecular; + case ShaderType.Bumped: + return _kspBumped; + case ShaderType.BumpedSpecular: + return _kspBumpedSpecular; + case ShaderType.Emissive: + return _kspEmissiveDiffuse; + case ShaderType.EmissiveSpecular: + return _kspEmissiveSpecular; + case ShaderType.EmissiveBumpedSpecular: + return _kspEmissiveBumpedSpecular; + case ShaderType.AlphaCutout: + return _kspAlphaCutoff; + case ShaderType.AlphaCutoutBumped: + return _kspAlphaCutoffBumped; + case ShaderType.Alpha: + return _kspAlphaTranslucent; + case ShaderType.AlphaSpecular: + return _kspAlphaTranslucentSpecular; + case ShaderType.AlphaUnlit: + return _kspAlphaUnlitTransparent; + case ShaderType.Unlit: + return _kspUnlit; + case ShaderType.ParticleAlpha: + return _kspParticlesAlphaBlended; + case ShaderType.ParticleAdditive: + return _kspParticulesAdditive; + case ShaderType.BumpedSpecularMap: + return _kspBumpedSpecularMapped; + } + } + + public static Shader GetShader(string shaderName) + { + if (!shadersPopulated) + PopulateShaders(); + + if (!shaders.TryGetValue(shaderName, out Shader shader)) + { + shader = Shader.Find(shaderName); + if (shader.IsNotNullRef()) + shaders.Add(shaderName, shader); + } + + return shader; + } + + public static readonly int MainTexPropId = Shader.PropertyToID("_MainTex"); + public static readonly int BumpMapPropId = Shader.PropertyToID("_BumpMap"); + public static readonly int EmissivePropId = Shader.PropertyToID("_Emissive"); + public static readonly int SpecMapPropId = Shader.PropertyToID("_SpecMap"); + + public static readonly int SpecColorPropId = Shader.PropertyToID("_SpecColor"); + public static readonly int ShininessPropId = Shader.PropertyToID("_Shininess"); + public static readonly int CutoffPropId = Shader.PropertyToID("_Cutoff"); + public static readonly int GlossPropId = Shader.PropertyToID("_Gloss"); + public static readonly int ColorPropId = Shader.PropertyToID("_Color"); + public static readonly int SpecTintPropId = Shader.PropertyToID("_SpecTint"); + public static readonly int InvFadePropId = Shader.PropertyToID("_InvFade"); + } +} diff --git a/KSPCommunityFixes/Library/StaticHelpers.cs b/KSPCommunityFixes/Library/StaticHelpers.cs index eeba323..e102788 100644 --- a/KSPCommunityFixes/Library/StaticHelpers.cs +++ b/KSPCommunityFixes/Library/StaticHelpers.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using HarmonyLib; using KSP.UI.TooltipTypes; +using PartToolsLib; using UnityEngine; namespace KSPCommunityFixes diff --git a/KSPCommunityFixes/Performance/FastLoader.cs b/KSPCommunityFixes/Performance/FastLoader.cs index 7c46a24..714cb04 100644 --- a/KSPCommunityFixes/Performance/FastLoader.cs +++ b/KSPCommunityFixes/Performance/FastLoader.cs @@ -1,6 +1,7 @@ // #define DEBUG_TEXTURE_CACHE using DDSHeaders; +using Expansions; using HarmonyLib; using KSP.Localization; using KSPCommunityFixes.Library.Buffers; @@ -29,6 +30,93 @@ namespace KSPCommunityFixes.Performance { + [KSPAddon(KSPAddon.Startup.MainMenu, true)] + internal class KSPCFFastLoaderReport : MonoBehaviour + { + internal static float initialConfigLoadTime; + internal static Stopwatch wSecondConfigLoad = new Stopwatch(); + internal static Stopwatch wConfigTranslate = new Stopwatch(); + internal static Stopwatch wAssetsLoading = new Stopwatch(); + internal static Stopwatch wAudioLoading = new Stopwatch(); + internal static Stopwatch wTextureLoading = new Stopwatch(); + internal static Stopwatch wModelLoading = new Stopwatch(); + internal static Stopwatch wAssetBundleLoading = new Stopwatch(); + internal static Stopwatch wGamedatabaseLoading = new Stopwatch(); + internal static Stopwatch wBuiltInPartsCopy = new Stopwatch(); + internal static Stopwatch wPartConfigExtraction = new Stopwatch(); + internal static Stopwatch wPartCompilationLoading = new Stopwatch(); + internal static Stopwatch wInternalCompilationLoading = new Stopwatch(); + internal static Stopwatch wExpansionLoading = new Stopwatch(); + internal static Stopwatch wPSystemSetup = new Stopwatch(); + + internal static long audioBytesLoaded; + internal static int texturesLoaded; + internal static long texturesBytesLoaded; + internal static int modelsLoaded; + internal static long modelsBytesLoaded; + + void Start() + { + float totalLoadingTime = Time.realtimeSinceStartup; + int totalPartsLoaded = 0; + int totalModulesLoaded = 0; + foreach (AvailablePart availablePart in PartLoader.Instance.loadedParts) + { + if (availablePart.partPrefab.IsNotNullOrDestroyed()) + { + totalPartsLoaded++; + totalModulesLoaded += availablePart.partPrefab.modules.Count; + } + } + + int totalInternalsLoaded = PartLoader.Instance.internalParts.Count; + int totalInternalPropsLoaded = PartLoader.Instance.internalProps.Count; + + string log = + $"[KSPCF:FastLoader] {SystemInfo.processorType} | {SystemInfo.systemMemorySize} MB | {SystemInfo.graphicsDeviceName} ({SystemInfo.graphicsMemorySize} MB)\n" + + $"Total loading time to main menu : {totalLoadingTime:F3}s\n" + + $"- Configs and assemblies loaded in {initialConfigLoadTime:F3}s\n" + + $"- Configs reload done in {wSecondConfigLoad.Elapsed.TotalSeconds:F3}s\n" + + $"- Configs translated in {wConfigTranslate.Elapsed.TotalSeconds:F3}s\n" + + $"- {KSPCFFastLoader.loadedAssetCount} assets loaded in {wAssetsLoading.Elapsed.TotalSeconds:F3}s :\n" + + $" - {KSPCFFastLoader.audioFilesLoaded} audio assets ({StaticHelpers.HumanReadableBytes(audioBytesLoaded)}) in {wAudioLoading.Elapsed.TotalSeconds:F3}s, {StaticHelpers.HumanReadableBytes((long)(audioBytesLoaded / wAudioLoading.Elapsed.TotalSeconds))}/s\n" + + $" - {texturesLoaded} texture assets ({StaticHelpers.HumanReadableBytes(texturesBytesLoaded)}) in {wTextureLoading.Elapsed.TotalSeconds:F3}s, {StaticHelpers.HumanReadableBytes((long)(texturesBytesLoaded / wTextureLoading.Elapsed.TotalSeconds))}/s\n" + + $" - {modelsLoaded} model assets ({StaticHelpers.HumanReadableBytes(modelsBytesLoaded)}) in {wModelLoading.Elapsed.TotalSeconds:F3}s, {StaticHelpers.HumanReadableBytes((long)(modelsBytesLoaded / wModelLoading.Elapsed.TotalSeconds))}/s\n" + + $"- Asset bundles loaded in {wAssetBundleLoading.Elapsed.TotalSeconds:F3}s\n" + + $"- GameDatabase (configs, resources, traits, upgrades...) loaded in {wGamedatabaseLoading.Elapsed.TotalSeconds:F3}s\n" + + $"- Built-in parts copied in {wBuiltInPartsCopy.Elapsed.TotalSeconds:F3}s\n" + + $"- Part and internal configs extracted in {wPartConfigExtraction.Elapsed.TotalSeconds:F3}s\n" + + $"- {totalPartsLoaded} parts and {totalModulesLoaded} modules compiled in {wPartCompilationLoading.Elapsed.TotalSeconds:F3}s\n" + + $" - {totalModulesLoaded / (float)totalPartsLoaded:F1} modules/part, {wPartCompilationLoading.Elapsed.TotalMilliseconds / totalPartsLoaded:F3} ms/part, {wPartCompilationLoading.Elapsed.TotalMilliseconds / totalModulesLoaded:F3} ms/module\n" + + $" - PartIcon compilation : {PartParsingPerf.iconCompilationWatch.Elapsed.TotalSeconds:F3}s\n" + + $"- {totalInternalsLoaded} internal spaces and {totalInternalPropsLoaded} props compiled in {wInternalCompilationLoading.Elapsed.TotalSeconds:F3}s\n"; + + if (ExpansionsLoader.expansionsInfo.Count > 0) + log += $"- {ExpansionsLoader.expansionsInfo.Count} DLC ({ExpansionsLoader.expansionsInfo.Values.Join(info => info.DisplayName)}) loaded in {wExpansionLoading.Elapsed.TotalSeconds:F3}s\n"; + + log += + $"- Planetary system loaded in {wPSystemSetup.Elapsed.TotalSeconds:F3}s"; + + Debug.Log(log); + Debug.Log($"Texture queries : {GameDatabasePerf.txcallCount}, slow path : {GameDatabasePerf.txMissCount} ({GameDatabasePerf.txMissCount / (float)GameDatabasePerf.txcallCount:P2})"); + Destroy(gameObject); + } + } + + [KSPAddon(KSPAddon.Startup.PSystemSpawn, true)] + internal class KSPCFFastLoaderPSystemSetup : MonoBehaviour + { + internal static void PSystemManager_Awake_Prefix() + { + KSPCFFastLoaderReport.wPSystemSetup.Start(); + } + + void OnDestroy() + { + KSPCFFastLoaderReport.wPSystemSetup.Stop(); + } + } + [KSPAddon(KSPAddon.Startup.Instantly, true)] internal class KSPCFFastLoader : MonoBehaviour { @@ -59,8 +147,14 @@ internal class KSPCFFastLoader : MonoBehaviour // min amount of files to try to keep in memory, regardless of maxBufferSize private const int minFileRead = 10; - private static Harmony harmony; - private static string HarmonyID => typeof(KSPCFFastLoader).FullName; + private static Harmony persistentHarmony; + private static string PersistentHarmonyID => typeof(KSPCFFastLoader).FullName; + + private static Harmony assetAndPartLoaderHarmony; + private static string AssetAndPartLoaderHarmonyID => typeof(KSPCFFastLoader).FullName + "AssetAndPartLoader"; + + private static Harmony expansionsLoaderHarmony; + private static string ExpansionsLoaderHarmonyID => typeof(KSPCFFastLoader).FullName + "ExpansionsLoader"; public static KSPCFFastLoader loader; @@ -81,7 +175,12 @@ internal class KSPCFFastLoader : MonoBehaviour private Dictionary textureCacheData; private HashSet textureDataIds; private bool cacheUpdated = false; - + + internal static Dictionary modelsByUrl; + internal static Dictionary modelsByDirectoryUrl; + internal static Dictionary urlFilesByModel; + internal static Dictionary texturesByUrl; + private void Awake() { if (KSPCommunityFixes.KspVersion < new Version(1, 12, 3)) @@ -91,22 +190,35 @@ private void Awake() return; } + KSPCFFastLoaderReport.initialConfigLoadTime = Time.realtimeSinceStartup; + Debug.Log("[KSPCF] Injecting FastLoader..."); loader = this; IsPatchEnabled = true; - harmony = new Harmony(HarmonyID); + + // Patch the various GameDatabase.GetModel/GetTexture methods to use the FastLoader dictionaries + BasePatch.Patch(typeof(GameDatabasePerf)); + + persistentHarmony = new Harmony(PersistentHarmonyID); + + MethodInfo m_PSystemManager_Awake = AccessTools.Method(typeof(PSystemManager), nameof(PSystemManager.Awake)); + MethodInfo p_PSystemManager_Awake = AccessTools.Method(typeof(KSPCFFastLoaderPSystemSetup), nameof(KSPCFFastLoaderPSystemSetup.PSystemManager_Awake_Prefix)); + persistentHarmony.Patch(m_PSystemManager_Awake, new HarmonyMethod(p_PSystemManager_Awake)); + + assetAndPartLoaderHarmony = new Harmony(AssetAndPartLoaderHarmonyID); MethodInfo m_GameDatabase_SetupMainLoaders = AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.SetupMainLoaders)); MethodInfo t_GameDatabase_SetupMainLoaders = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_SetupMainLoaders_Prefix)); - harmony.Patch(m_GameDatabase_SetupMainLoaders, new HarmonyMethod(t_GameDatabase_SetupMainLoaders)); + assetAndPartLoaderHarmony.Patch(m_GameDatabase_SetupMainLoaders, new HarmonyMethod(t_GameDatabase_SetupMainLoaders)); MethodInfo m_GameDatabase_LoadAssetBundleObjects_MoveNext = AccessTools.EnumeratorMoveNext(AccessTools.Method(typeof(GameDatabase), nameof(GameDatabase.LoadAssetBundleObjects))); - MethodInfo t_GameDatabase_LoadAssetBundleObjects_MoveNext = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_LoadAssetBundleObjects_MoveNext_Prefix)); - harmony.Patch(m_GameDatabase_LoadAssetBundleObjects_MoveNext, new HarmonyMethod(t_GameDatabase_LoadAssetBundleObjects_MoveNext)); + MethodInfo pr_GameDatabase_LoadAssetBundleObjects_MoveNext = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_LoadAssetBundleObjects_MoveNext_Prefix)); + MethodInfo po_GameDatabase_LoadAssetBundleObjects_MoveNext = AccessTools.Method(typeof(KSPCFFastLoader), nameof(GameDatabase_LoadAssetBundleObjects_MoveNext_Postfix)); + assetAndPartLoaderHarmony.Patch(m_GameDatabase_LoadAssetBundleObjects_MoveNext, new HarmonyMethod(pr_GameDatabase_LoadAssetBundleObjects_MoveNext), new HarmonyMethod(po_GameDatabase_LoadAssetBundleObjects_MoveNext)); MethodInfo m_PartLoader_StartLoad = AccessTools.Method(typeof(PartLoader), nameof(PartLoader.StartLoad)); MethodInfo t_PartLoader_StartLoad = AccessTools.Method(typeof(KSPCFFastLoader), nameof(PartLoader_StartLoad_Transpiler)); - harmony.Patch(m_PartLoader_StartLoad, null, null, new HarmonyMethod(t_PartLoader_StartLoad)); + assetAndPartLoaderHarmony.Patch(m_PartLoader_StartLoad, null, null, new HarmonyMethod(t_PartLoader_StartLoad)); PatchStartCoroutineInCoroutine(AccessTools.Method(typeof(PartLoader), nameof(PartLoader.CompileParts))); PatchStartCoroutineInCoroutine(AccessTools.Method(typeof(DragCubeSystem), nameof(DragCubeSystem.SetupDragCubeCoroutine), new[] { typeof(Part) })); @@ -115,7 +227,14 @@ private void Awake() // Fix for issue #114 : Drag cubes are incorrectly calculated with KSPCF 1.24.1 MethodInfo m_DragCubeSystem_RenderDragCubes_MoveNext = AccessTools.EnumeratorMoveNext(AccessTools.Method(typeof(DragCubeSystem), nameof(DragCubeSystem.RenderDragCubes))); MethodInfo m_DragCubeSystem_RenderDragCubes_MoveNext_Transpiler = AccessTools.Method(typeof(KSPCFFastLoader), nameof(DragCubeSystem_RenderDragCubes_MoveNext_Transpiler)); - harmony.Patch(m_DragCubeSystem_RenderDragCubes_MoveNext, null, null, new HarmonyMethod(m_DragCubeSystem_RenderDragCubes_MoveNext_Transpiler)); + assetAndPartLoaderHarmony.Patch(m_DragCubeSystem_RenderDragCubes_MoveNext, null, null, new HarmonyMethod(m_DragCubeSystem_RenderDragCubes_MoveNext_Transpiler)); + + expansionsLoaderHarmony = new Harmony(ExpansionsLoaderHarmonyID); + MethodInfo m_ExpansionsLoader_StartLoad = AccessTools.Method(typeof(ExpansionsLoader), nameof(PartLoader.StartLoad)); + MethodInfo p_ExpansionsLoader_StartLoad = AccessTools.Method(typeof(KSPCFFastLoader), nameof(ExpansionsLoader_StartLoad_Prefix)); + expansionsLoaderHarmony.Patch(m_ExpansionsLoader_StartLoad, new HarmonyMethod(p_ExpansionsLoader_StartLoad)); + GameEvents.OnExpansionSystemLoaded.Add(OnExpansionSystemLoaded); + GameEvents.OnGameDatabaseLoaded.Add(OnGameDatabaseLoaded); configPath = ConfigPath; textureCachePath = Path.Combine(ModPath, "PluginData", "TextureCache"); @@ -151,8 +270,8 @@ void OnDestroy() if (!IsPatchEnabled) return; - harmony.UnpatchAll(HarmonyID); - harmony = null; + assetAndPartLoaderHarmony.UnpatchAll(AssetAndPartLoaderHarmonyID); + assetAndPartLoaderHarmony = null; loader = null; } @@ -256,9 +375,37 @@ static bool GameDatabase_LoadAssetBundleObjects_MoveNext_Prefix(object __instanc return false; } + KSPCFFastLoaderReport.wAssetBundleLoading.Start(); return true; } + static void GameDatabase_LoadAssetBundleObjects_MoveNext_Postfix(object __instance, ref bool __result) + { + if (!__result) + { + KSPCFFastLoaderReport.wAssetBundleLoading.Stop(); + KSPCFFastLoaderReport.wGamedatabaseLoading.Start(); + } + } + + private void OnGameDatabaseLoaded() + { + KSPCFFastLoaderReport.wGamedatabaseLoading.Stop(); + GameEvents.OnGameDatabaseLoaded.Remove(OnGameDatabaseLoaded); + } + + + + + static void ExpansionsLoader_StartLoad_Prefix() => KSPCFFastLoaderReport.wExpansionLoading.Start(); + + private void OnExpansionSystemLoaded() + { + KSPCFFastLoaderReport.wExpansionLoading.Stop(); + expansionsLoaderHarmony.UnpatchAll(ExpansionsLoaderHarmonyID); + GameEvents.OnExpansionSystemLoaded.Remove(OnExpansionSystemLoaded); + } + #endregion #region Asset loader reimplementation (main coroutine) @@ -269,7 +416,7 @@ static bool GameDatabase_LoadAssetBundleObjects_MoveNext_Prefix(object __instanc static double ElapsedTime => Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency; static int totalAssetCount; - static int loadedAssetCount; + internal static int loadedAssetCount; /// /// Custom partial reimplementation of the stock GameDatabase.LoadObjects() coroutine @@ -291,10 +438,14 @@ static IEnumerator FastAssetLoader(List configFileTypes) // However, the full reload means mods can take the opportunity to generate configs/assets on // the fly from Awake() in a Startup.Instantly KSPAddon and have it being loaded. I've found // at least 2 mods doing that, so unfortunately this can't really be optimized... + KSPCFFastLoaderReport.wSecondConfigLoad.Restart(); gdb._root = new UrlDir(gdb.urlConfig.ToArray(), configFileTypes.ToArray()); + KSPCFFastLoaderReport.wSecondConfigLoad.Stop(); // Optimized version of GameDatabase.translateLoadedNodes() + KSPCFFastLoaderReport.wConfigTranslate.Restart(); TranslateLoadedNodes(gdb); + KSPCFFastLoaderReport.wConfigTranslate.Stop(); yield return null; gdb.progressTitle = "Waiting for PNGTextureCache opt-in..."; @@ -304,6 +455,7 @@ static IEnumerator FastAssetLoader(List configFileTypes) gdb.progressTitle = "Searching assets to load..."; yield return null; + KSPCFFastLoaderReport.wAssetsLoading.Restart(); double nextFrameTime = ElapsedTime + minFrameTimeD; // Files loaded by our custom loaders @@ -414,6 +566,7 @@ static IEnumerator FastAssetLoader(List configFileTypes) } gdb.progressTitle = "Loading sound assets..."; + KSPCFFastLoaderReport.wAudioLoading.Restart(); yield return null; // call non-stock audio loaders @@ -501,10 +654,18 @@ static IEnumerator FastAssetLoader(List configFileTypes) // start texture loading gdb.progressFraction = 0.25f; + KSPCFFastLoaderReport.wAudioLoading.Stop(); + KSPCFFastLoaderReport.wTextureLoading.Restart(); gdb.progressTitle = "Loading texture assets..."; yield return null; // call non-stock texture loaders + + // note : we could use the StringComparer.OrdinalIgnoreCase comparer as the dictionary key comparer, + // as this is the comparison that stock is doing. However, profiling show that casing mismatches rarely happen + // (never in stock, 0.22% of calls in a very heavily modded install with a bunch of part mods of varying quality) + // and the overhead of the OrdinalIgnoreCase comparer is offsetting the gains (but a small margin, but still). + texturesByUrl = new Dictionary(allTextureFiles.Count); unsupportedFilesCount = unsupportedTextureFiles.Count; loadersCount = gdb.loadersTexture.Count; @@ -549,6 +710,7 @@ static IEnumerator FastAssetLoader(List configFileTypes) } // call our custom loader + yield return gdb.StartCoroutine(FilesLoader(textureAssets, allTextureFiles, "Loading texture asset")); // write texture cache json to disk @@ -557,10 +719,15 @@ static IEnumerator FastAssetLoader(List configFileTypes) // start model loading gdb.progressFraction = 0.75f; + KSPCFFastLoaderReport.wTextureLoading.Stop(); + KSPCFFastLoaderReport.wModelLoading.Start(); gdb.progressTitle = "Loading model assets..."; yield return null; // call non-stock model loaders + modelsByUrl = new Dictionary(allModelFiles.Count); + modelsByDirectoryUrl = new Dictionary(allModelFiles.Count); + urlFilesByModel = new Dictionary(allModelFiles.Count); unsupportedFilesCount = unsupportedModelFiles.Count; loadersCount = gdb.loadersModel.Count; @@ -607,11 +774,14 @@ static IEnumerator FastAssetLoader(List configFileTypes) // all done, do some cleanup arrayPool = null; + MuParser.ReleaseBuffers(); // stock stuff gdb.lastLoadTime = KSPUtil.SystemDateTime.DateTimeNow(); gdb.progressFraction = 1f; loadObjectsInProgress = false; + KSPCFFastLoaderReport.wModelLoading.Stop(); + KSPCFFastLoaderReport.wAssetsLoading.Stop(); } /// @@ -666,7 +836,8 @@ private static void TranslateLoadedNodes(GameDatabase gdb) #region Asset loader reimplementation (audio loader) static int concurrentAudioCoroutines; - static int audioFilesLoaded; + internal static int audioFilesLoaded; + /// /// Concurrent coroutines (read "multiple coroutines in the same frame") audio loader @@ -677,7 +848,9 @@ static IEnumerator AudioLoader(UrlFile urlFile) try { - string normalizedUri = KSPUtil.ApplicationFileProtocol + new FileInfo(urlFile.fullPath).FullName; + FileInfo fileInfo = new FileInfo(urlFile.fullPath); + KSPCFFastLoaderReport.audioBytesLoaded += fileInfo.Length; + string normalizedUri = KSPUtil.ApplicationFileProtocol + fileInfo.FullName; UnityWebRequest request = UnityWebRequestMultimedia.GetAudioClip(normalizedUri, AudioType.UNKNOWN); yield return request.SendWebRequest(); while (!request.isDone) @@ -832,8 +1005,6 @@ static void ReadAssetsThread(List files, Deque buffer) } } - - /// /// Asset wrapper class, actual implementation of the disk reader, individual texture/model formats loaders /// @@ -1042,6 +1213,9 @@ public void LoadAndDisposeMainThread() textureInfo.name = file.url; textureInfo.texture.name = file.url; Instance.databaseTexture.Add(textureInfo); + texturesByUrl[file.url] = textureInfo; + KSPCFFastLoaderReport.texturesBytesLoaded += dataLength; + KSPCFFastLoaderReport.texturesLoaded++; } } else if (file.fileType == FileType.Model) @@ -1075,6 +1249,13 @@ public void LoadAndDisposeMainThread() model.SetActive(false); Instance.databaseModel.Add(model); Instance.databaseModelFiles.Add(file); + modelsByUrl[file.url] = model; + // if multiple models in the same dir, we only add the first + // to ensure identical behavior as the GameDatabase.GetModelPrefabIn() method + modelsByDirectoryUrl.TryAdd(file.parent.url, model); + urlFilesByModel.Add(model, file); + KSPCFFastLoaderReport.modelsBytesLoaded += dataLength; + KSPCFFastLoaderReport.modelsLoaded++; } } } @@ -1476,83 +1657,9 @@ private TextureInfo LoadTRUECOLOR() return new TextureInfo(file, texture, isNormalMap, !isNormalMap, false); } - private static void InitPartReader() - { - if (PartReader.matDummies == null) - PartReader.matDummies = new List(); - else - PartReader.matDummies.Clear(); - - if (PartReader.boneDummies == null) - PartReader.boneDummies = new List(); - else - PartReader.boneDummies.Clear(); - - if (PartReader.textureDummies == null) - PartReader.textureDummies = new PartReader.TextureDummyList(); - else - PartReader.textureDummies.Clear(); - } - - private static void CleanPartReader() - { - PartReader.matDummies.Clear(); - PartReader.boneDummies.Clear(); - PartReader.textureDummies.Clear(); - } - private GameObject LoadMU() { - InitPartReader(); - PartReader.file = file; - memoryStream = new MemoryStream(buffer, 0, dataLength); - binaryReader = new BinaryReader(memoryStream); - PartToolsLib.FileType fileType = (PartToolsLib.FileType)binaryReader.ReadInt32(); - PartReader.fileVersion = binaryReader.ReadInt32(); - _ = binaryReader.ReadString() + string.Empty; - if (fileType != PartToolsLib.FileType.ModelBinary) - { - SetError($"'{file.url}.mu' is an incorrect type."); - return null; - } - GameObject gameObject = null; - try - { - gameObject = PartReader.ReadChild(binaryReader, null); - if (PartReader.boneDummies.Count > 0) - { - int i = 0; - for (int count = PartReader.boneDummies.Count; i < count; i++) - { - Transform[] array = new Transform[PartReader.boneDummies[i].bones.Count]; - int j = 0; - for (int count2 = PartReader.boneDummies[i].bones.Count; j < count2; j++) - { - array[j] = PartReader.FindChildByName(gameObject.transform, PartReader.boneDummies[i].bones[j]); - } - PartReader.boneDummies[i].smr.bones = array; - } - } - if (PartReader.shaderFallback) - { - Renderer[] componentsInChildren = gameObject.GetComponentsInChildren(); - int k = 0; - for (int num = componentsInChildren.Length; k < num; k++) - { - Renderer renderer = componentsInChildren[k]; - int l = 0; - for (int num2 = renderer.sharedMaterials.Length; l < num2; l++) - { - renderer.sharedMaterials[l].shader = Shader.Find("KSP/Diffuse"); - } - } - } - } - finally - { - CleanPartReader(); - } - return gameObject; + return MuParser.Parse(file.parent.url, buffer, dataLength); } private GameObject LoadDAE() @@ -1712,8 +1819,6 @@ private static IEnumerable PartLoader_StartLoad_Transpiler(IEnu private static IEnumerator PartLoader_CompileAll() { - Stopwatch watch = Stopwatch.StartNew(); - PartLoader instance = PartLoader.Instance; if (instance._recompile) @@ -1722,6 +1827,8 @@ private static IEnumerator PartLoader_CompileAll() } instance.progressTitle = ""; instance.progressFraction = 0f; + KSPCFFastLoaderReport.wBuiltInPartsCopy.Restart(); + // copy the prebuilt parts (eva kerbals and flags) into the loaded part db for (int i = 0; i < instance.initialPartsLength; i++) { AvailablePart availablePart = new AvailablePart(instance.parts[i]); @@ -1752,10 +1859,13 @@ private static IEnumerator PartLoader_CompileAll() } instance.loadedParts.Add(availablePart); } + KSPCFFastLoaderReport.wBuiltInPartsCopy.Stop(); + KSPCFFastLoaderReport.wPartConfigExtraction.Restart(); UrlConfig[] configs = GameDatabase.Instance.GetConfigs("PART"); UrlConfig[] allPropNodes = GameDatabase.Instance.GetConfigs("PROP"); UrlConfig[] allSpaceNodes = GameDatabase.Instance.GetConfigs("INTERNAL"); UrlConfig[] configs2 = GameDatabase.Instance.GetConfigs("VARIANTTHEME"); + KSPCFFastLoaderReport.wPartConfigExtraction.Stop(); int num = configs.Length + allPropNodes.Length + allSpaceNodes.Length; instance.progressDelta = 1f / num; instance.InitializePartDatabase(); @@ -1763,12 +1873,15 @@ private static IEnumerator PartLoader_CompileAll() instance.APFinderByName.Clear(); instance.CompileVariantThemes(configs2); + KSPCFFastLoaderReport.wPartCompilationLoading.Restart(); PartCompilationInProgress = true; IEnumerator compilePartsEnumerator = FrameUnlockedCoroutine(instance.CompileParts(configs)); while (compilePartsEnumerator.MoveNext()) yield return null; PartCompilationInProgress = false; + KSPCFFastLoaderReport.wPartCompilationLoading.Stop(); + KSPCFFastLoaderReport.wInternalCompilationLoading.Restart(); IEnumerator compileInternalPropsEnumerator = FrameUnlockedCoroutine(instance.CompileInternalProps(allPropNodes)); while (compileInternalPropsEnumerator.MoveNext()) yield return null; @@ -1776,16 +1889,12 @@ private static IEnumerator PartLoader_CompileAll() IEnumerator compileInternalSpacesEnumerator = FrameUnlockedCoroutine(instance.CompileInternalSpaces(allSpaceNodes)); while (compileInternalSpacesEnumerator.MoveNext()) yield return null; + KSPCFFastLoaderReport.wInternalCompilationLoading.Stop(); Destroy(loader); instance.SavePartDatabase(); - Debug.Log($"PartLoader: {configs.Length} parts compiled"); - Debug.Log($"PartLoader: {allPropNodes.Length} internal props compiled"); - Debug.Log($"PartLoader: {allSpaceNodes.Length} internal spaces compiled"); - Debug.Log($"PartLoader: compilation took {watch.Elapsed.TotalSeconds:F3}s"); - instance._recompile = false; PartUpgradeManager.Handler.LinkUpgrades(); GameEvents.OnUpgradesLinked.Fire(); @@ -1805,7 +1914,7 @@ private static IEnumerator PartLoader_CompileAll() private static void PatchStartCoroutineInCoroutine(MethodInfo coroutine) { MethodInfo t_StartCoroutinePassThroughTranspiler = AccessTools.Method(typeof(KSPCFFastLoader), nameof(StartCoroutinePassThroughTranspiler)); - harmony.Patch(AccessTools.EnumeratorMoveNext(coroutine), null, null, new HarmonyMethod(t_StartCoroutinePassThroughTranspiler)); + assetAndPartLoaderHarmony.Patch(AccessTools.EnumeratorMoveNext(coroutine), null, null, new HarmonyMethod(t_StartCoroutinePassThroughTranspiler)); } /// @@ -2440,6 +2549,8 @@ public void Show() #endregion + + } #if DEBUG diff --git a/KSPCommunityFixes/Performance/GameDatabasePerf.cs b/KSPCommunityFixes/Performance/GameDatabasePerf.cs new file mode 100644 index 0000000..abdb759 --- /dev/null +++ b/KSPCommunityFixes/Performance/GameDatabasePerf.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using static GameDatabase; +using static UrlDir; + +namespace KSPCommunityFixes.Performance +{ + // see https://github.com/KSPModdingLibs/KSPCommunityFixes/pull/269 + // this patch is applied manually on launch from the FastLoader patch. + // It has no entry in settings.cfg and can't be disabled. + [ManualPatch] + internal class GameDatabasePerf : BasePatch + { + protected override bool IgnoreConfig => true; + + protected override void ApplyPatches() + { + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetModelPrefab)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetModelPrefabIn)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetModel)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetModelIn)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetModelFile), new[] { typeof(GameObject) }); + // we don't patch the GetModelFile(string) variant as it would require an additional dictionary, + // is unused in stock and very unlikely to ever be used by anyone. + + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetTextureInfo)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetTextureInfoIn)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetTexture)); + AddPatch(PatchType.Override, typeof(GameDatabase), nameof(GameDatabase.GetTextureIn)); + // For the same reasons, we don't patch GetTextureFile(string) + } + + static GameObject GameDatabase_GetModelPrefab_Override(GameDatabase gdb, string url) + { + if (url == null) + return null; + + if (!KSPCFFastLoader.modelsByUrl.TryGetValue(url, out GameObject result)) + { + // We need a fallback because models are also added from asset bundles, + // and because anyone could be adding models as the databaseModel list is public + List models = gdb.databaseModel; + for (int i = models.Count; i-- > 0;) + { + if (models[i].name == url) + { + result = models[i]; + KSPCFFastLoader.modelsByUrl.Add(url, result); + break; + } + } + } + + return result; + } + + static GameObject GameDatabase_GetModelPrefabIn_Override(GameDatabase gdb, string url) + { + if (url == null) + return null; + + if (!KSPCFFastLoader.modelsByDirectoryUrl.TryGetValue(url, out GameObject result)) + { + // We need a fallback because models are also added from asset bundles, + // and because anyone could be adding models as the databaseModel list is public + List models = gdb.databaseModel; + for (int i = models.Count; i-- > 0;) + { + string modelName = models[i].name; + if (modelName.Substring(0, modelName.LastIndexOf('/')) == url) + { + result = models[i]; + KSPCFFastLoader.modelsByDirectoryUrl.Add(url, result); + break; + } + } + } + + return result; + } + + static GameObject GameDatabase_GetModel_Override(GameDatabase gdb, string url) + { + GameObject prefab = GameDatabase_GetModelPrefab_Override(gdb, url); + if (prefab.IsNullRef()) + return null; + + return UnityEngine.Object.Instantiate(prefab); + } + + static GameObject GameDatabase_GetModelIn_Override(GameDatabase gdb, string url) + { + GameObject prefab = GameDatabase_GetModelPrefabIn_Override(gdb, url); + if (prefab.IsNullRef()) + return null; + + return UnityEngine.Object.Instantiate(prefab); + } + + static UrlFile GameDatabase_GetModelFile_Override(GameDatabase gdb, GameObject modelPrefab) + { + if (modelPrefab.IsNullRef()) + return null; + + if (!KSPCFFastLoader.urlFilesByModel.TryGetValue(modelPrefab, out UrlFile result)) + { + // We need a fallback because models are also added from asset bundles, + // and because anyone could be adding models as the databaseModel list is public + List models = gdb.databaseModel; + for (int i = models.Count; i-- > 0;) + { + if (models[i] == modelPrefab) + { + result = gdb.databaseModelFiles[i]; + KSPCFFastLoader.urlFilesByModel.Add(modelPrefab, result); + break; + } + } + } + + return result; + } + + internal static int txcallCount; + internal static int txMissCount; + + static TextureInfo GameDatabase_GetTextureInfo_Override(GameDatabase gdb, string url) + { + txcallCount++; + if (url == null) + return null; + + if (gdb.flagSwaps.TryGetValue(url, out string newUrl)) + url = newUrl; + + if (!KSPCFFastLoader.texturesByUrl.TryGetValue(url, out TextureInfo result)) + { + for (int i = gdb.databaseTexture.Count; i-- > 0;) + { + if (string.Equals(url, gdb.databaseTexture[i].name, StringComparison.OrdinalIgnoreCase)) + { + result = gdb.databaseTexture[i]; + KSPCFFastLoader.texturesByUrl.Add(url, result); + txMissCount++; + break; + } + } + } + return result; + } + + static TextureInfo GameDatabase_GetTextureInfoIn_Override(GameDatabase gdb, string url, string textureName) + { + txcallCount++; + if (url == null || textureName == null) + return null; + + url = url.Substring(0, url.LastIndexOf('/') + 1) + textureName; + + if (gdb.flagSwaps.TryGetValue(url, out string newUrl)) + url = newUrl; + + if (!KSPCFFastLoader.texturesByUrl.TryGetValue(url, out TextureInfo result)) + { + for (int i = gdb.databaseTexture.Count; i-- > 0;) + { + if (string.Equals(url, gdb.databaseTexture[i].name, StringComparison.OrdinalIgnoreCase)) + { + result = gdb.databaseTexture[i]; + KSPCFFastLoader.texturesByUrl.Add(url, result); + txMissCount++; + break; + } + } + } + + return result; + } + + static Texture2D GameDatabase_GetTexture_Override(GameDatabase gdb, string url, bool asNormalMap) + { + txcallCount++; + if (url == null) + return null; + + if (gdb.flagSwaps.TryGetValue(url, out string newUrl)) + url = newUrl; + + if (!KSPCFFastLoader.texturesByUrl.TryGetValue(url, out TextureInfo textureInfo)) + { + for (int i = gdb.databaseTexture.Count; i-- > 0;) + { + if (string.Equals(url, gdb.databaseTexture[i].name, StringComparison.OrdinalIgnoreCase)) + { + textureInfo = gdb.databaseTexture[i]; + KSPCFFastLoader.texturesByUrl.Add(url, textureInfo); + txMissCount++; + break; + } + } + } + + if (textureInfo == null) + return null; + + return asNormalMap ? textureInfo.normalMap : textureInfo.texture; + } + + static Texture2D GameDatabase_GetTextureIn_Override(GameDatabase gdb, string url, string textureName, bool asNormalMap) + { + txcallCount++; + if (url == null || textureName == null) + return null; + + url = url.Substring(0, url.LastIndexOf('/') + 1) + textureName; + + if (gdb.flagSwaps.TryGetValue(url, out string newUrl)) + url = newUrl; + + if (!KSPCFFastLoader.texturesByUrl.TryGetValue(url, out TextureInfo textureInfo)) + { + for (int i = gdb.databaseTexture.Count; i-- > 0;) + { + if (string.Equals(url, gdb.databaseTexture[i].name, StringComparison.OrdinalIgnoreCase)) + { + textureInfo = gdb.databaseTexture[i]; + KSPCFFastLoader.texturesByUrl.Add(url, textureInfo); + txMissCount++; + break; + } + } + } + + if (textureInfo == null) + return null; + + return asNormalMap ? textureInfo.normalMap : textureInfo.texture; + } + } +} diff --git a/KSPCommunityFixes/Performance/MinorPerfTweaks.cs b/KSPCommunityFixes/Performance/MinorPerfTweaks.cs index f90a245..a3117ba 100644 --- a/KSPCommunityFixes/Performance/MinorPerfTweaks.cs +++ b/KSPCommunityFixes/Performance/MinorPerfTweaks.cs @@ -1,4 +1,5 @@ -using System; +using HarmonyLib; +using System; using System.Collections.Generic; using UnityEngine; @@ -10,11 +11,25 @@ internal class MinorPerfTweaks : BasePatch protected override void ApplyPatches() { + AddPatch(PatchType.Override, AccessTools.PropertyGetter(typeof(FlightGlobals), nameof(FlightGlobals.fetch)), nameof(FlightGlobals_fetch_Override)); + AddPatch(PatchType.Override, typeof(Part), nameof(Part.isKerbalEVA)); AddPatch(PatchType.Override, typeof(VolumeNormalizer), nameof(VolumeNormalizer.Update)); } + // When FlightGlobals._fetch is null/destroyed, the stock "fetch" getter fallback to a FindObjectOfType() + // call. This is extremly slow, and account for ~10% of the total loading time (7 seconds) of the total + // launch > main menu on stock + BDB install, due to being called during part compilation. + // The _fetch field is acquired from Awake() and set to null in OnDestroy(), so there is no real reason for this. + // The only behavior change I can think of would be something calling fetch in between the OnDestroy() + // call and the effective destruction of the native object. In any case, this can be qualified as a bug, + // as the flightglobal instance left accessible will be in quite invalid state. + private static FlightGlobals FlightGlobals_fetch_Override() + { + return FlightGlobals._fetch.IsNullOrDestroyed() ? null : FlightGlobals._fetch; + } + // Called (sometimes multiple times) in Part.FixedUpdate() public static bool Part_isKerbalEVA_Override(Part part) { diff --git a/KSPCommunityFixes/Performance/PartParsingPerf.cs b/KSPCommunityFixes/Performance/PartParsingPerf.cs new file mode 100644 index 0000000..51241b9 --- /dev/null +++ b/KSPCommunityFixes/Performance/PartParsingPerf.cs @@ -0,0 +1,666 @@ +using KSPCommunityFixes.Library; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Reflection.Emit; +using UnityEngine; +using UObject = UnityEngine.Object; + +namespace KSPCommunityFixes.Performance +{ + internal class PartParsingPerf : BasePatch + { + internal static Stopwatch iconCompilationWatch = new Stopwatch(); + + protected override void ApplyPatches() + { + AddPatch(PatchType.Override, typeof(PartLoader), nameof(PartLoader.CreatePartIcon)); + + AddPatch(PatchType.Override, typeof(PartLoader), nameof(PartLoader.ApplyPartValue)); + } + + static List componentBuffer = new List(); + static List iconHiddenComponentBuffer = new List(); + static List colliderObjectsToDestroy = new List(); + static List iconRenderers = new List(); + static List materialBuffer = new List(); + + private static bool shaderRefsAcquired; + private static Shader shader_ScreenSpaceMaskSpecular; + private static Shader shader_ScreenSpaceMaskBumpedSpecularTransparent; + private static Shader shader_ScreenSpaceMaskBumped; + private static Shader shader_ScreenSpaceMaskAlphaCutoffBackground; + private static Shader shader_ScreenSpaceMaskUnlit; + private static Shader shader_ScreenSpaceMask; + + private static Shader GetIconShader(string materialShaderName) + { + if (!shaderRefsAcquired) + { + shader_ScreenSpaceMaskSpecular = Shader.Find("KSP/ScreenSpaceMaskSpecular"); + shader_ScreenSpaceMaskBumpedSpecularTransparent = Shader.Find("KSP/ScreenSpaceMaskBumpedSpecular(Transparent)"); + shader_ScreenSpaceMaskBumped = Shader.Find("KSP/ScreenSpaceMaskBumped"); + shader_ScreenSpaceMaskAlphaCutoffBackground = Shader.Find("KSP/ScreenSpaceMaskAlphaCutoffBackground"); + shader_ScreenSpaceMaskUnlit = Shader.Find("KSP/ScreenSpaceMaskUnlit"); + shader_ScreenSpaceMask = Shader.Find("KSP/ScreenSpaceMask"); + shaderRefsAcquired = true; + } + + if (materialShaderName == "KSP/Bumped Specular (Mapped)") + return shader_ScreenSpaceMaskSpecular; + if (materialShaderName == "KSP/Bumped Specular (Transparent)") + return shader_ScreenSpaceMaskBumpedSpecularTransparent; + if (materialShaderName.Contains("Bumped")) + return shader_ScreenSpaceMaskBumped; + if (materialShaderName.Contains("KSP/Alpha/CutoffBackground")) + return shader_ScreenSpaceMaskAlphaCutoffBackground; + if (materialShaderName == "KSP/Unlit") + return shader_ScreenSpaceMaskUnlit; + + return shader_ScreenSpaceMask; + } + + private static GameObject PartLoader_CreatePartIcon_Override(PartLoader partLoader, GameObject newPart, out float iconScale) + { + iconCompilationWatch.Start(); + GameObject iconPartObject = UObject.Instantiate(newPart); + iconPartObject.SetActive(true); + Part iconPart = iconPartObject.GetComponent(); + + if (iconPart.IsNotNullOrDestroyed()) + { + int i = 0; + for (int count = iconPart.Modules.Count; i < count; i++) + iconPart.Modules[i].OnIconCreate(); + } + + Bounds partBounds = default; + iconPartObject.GetComponentsInChildren(false, componentBuffer); + try + { + for (int i = componentBuffer.Count; i-- > 0;) + { + Component c = componentBuffer[i]; + if (c is Part + || c is PartModule + || c is EffectBehaviour + || c is WheelCollider + || c is SmokeTrailControl + || c is FXPrefab + || c is ParticleSystem + || c is Light + || c is Animation + || c is DAE) + { + UObject.DestroyImmediate(c, false); + } + else if (!c.IsDestroyed() && (c is Renderer || c is MeshFilter)) + { + // we are adding renderers that will potentially be destroyed latter + if (!(c is MeshFilter)) + iconRenderers.Add((Renderer)c); + + if ((c is MeshRenderer || c is SkinnedMeshRenderer || c is MeshFilter) && c.gameObject.CompareTag("Icon_Hidden")) + { + c.gameObject.GetComponentsInChildren(false, iconHiddenComponentBuffer); + + for (int j = iconHiddenComponentBuffer.Count; j-- > 0;) + { + Component child = iconHiddenComponentBuffer[j]; + if (!child.IsDestroyed() && (child is MeshRenderer || child is SkinnedMeshRenderer || child is MeshFilter)) + UObject.DestroyImmediate(child, false); + } + + iconHiddenComponentBuffer.Clear(); + UObject.DestroyImmediate(c, false); + } + } + else if (c is Collider) + { + if (c.gameObject.name == "collider") + colliderObjectsToDestroy.Add(c.gameObject); + else + UObject.DestroyImmediate(c, false); + } + } + + for (int i = colliderObjectsToDestroy.Count; i-- > 0;) + UObject.DestroyImmediate(colliderObjectsToDestroy[i]); + + iconPartObject.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity); + + bool first = true; + for (int i = iconRenderers.Count; i-- > 0;) + { + Renderer renderer = iconRenderers[i]; + if (renderer.IsDestroyed()) + continue; + + if (!(renderer is ParticleSystemRenderer)) + { + if (first) + { + first = false; + partBounds = renderer.bounds; + } + else + { + partBounds.Encapsulate(renderer.bounds); + } + } + + renderer.GetSharedMaterials(materialBuffer); + for (int j = materialBuffer.Count; j-- > 0;) + { + Material partMaterial = materialBuffer[j]; + if (partMaterial.IsNullOrDestroyed()) + continue; + + Material iconMaterial = new Material(GetIconShader(partMaterial.shader.name)); + iconMaterial.name = partMaterial.name; + iconMaterial.CopyPropertiesFromMaterial(partMaterial); + if (iconMaterial.HasProperty(ShaderHelpers.ColorPropId)) + { + Color originalColor = iconMaterial.color; + iconMaterial.SetColor(PropertyIDs._Color, new Color( + originalColor.r < 0.5f ? 0.5f : originalColor.r, + originalColor.g < 0.5f ? 0.5f : originalColor.g, + originalColor.b < 0.5f ? 0.5f : originalColor.b)); + } + else + { + iconMaterial.SetColor(PropertyIDs._Color, Color.white); + } + + iconMaterial.SetFloat(PropertyIDs._Multiplier, partLoader.shaderMultiplier); + iconMaterial.SetFloat(PropertyIDs._MinX, 0f); + iconMaterial.SetFloat(PropertyIDs._MaxX, 1f); + iconMaterial.SetFloat(PropertyIDs._MinY, 0f); + iconMaterial.SetFloat(PropertyIDs._MaxY, 1f); + materialBuffer[j] = iconMaterial; + } + + if (materialBuffer.Count == 1) + renderer.sharedMaterial = materialBuffer[0]; + else + renderer.sharedMaterials = materialBuffer.ToArray(); + + materialBuffer.Clear(); + } + + } + finally + { + componentBuffer.Clear(); + iconHiddenComponentBuffer.Clear(); + colliderObjectsToDestroy.Clear(); + iconRenderers.Clear(); + materialBuffer.Clear(); + } + + Vector3 size = partBounds.size; + float x = Math.Abs(size.x); + float y = Math.Abs(size.y); + float z = Math.Abs(size.z); + iconScale = x > y ? x : y; + iconScale = iconScale > z ? iconScale : z; + iconScale = 1f / iconScale; + + GameObject iconObject = new GameObject(iconPartObject.name + " icon"); + iconPartObject.transform.parent = iconObject.transform; + iconPartObject.transform.localScale = Vector3.one * iconScale; + iconPartObject.transform.localPosition = partBounds.center * (0f - iconScale); + iconObject.transform.parent = partLoader.transform; + iconObject.SetActive(false); + iconPartObject.SetActive(true); + //__result = iconObject; + iconCompilationWatch.Stop(); + return iconObject; + } + + private static Dictionary partFieldsSetters = new Dictionary(300); + private static Dictionary compoundPartFieldsSetters = new Dictionary(300); + private static Dictionary> unknownPartFieldsSetters = new Dictionary>(); + + private abstract class PartFieldSetter + { + public abstract bool SetField(object instance, string value); + } + + private sealed class FloatSetter : PartFieldSetter + { + private Action setter; + + public FloatSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + float value = float.Parse(strValue); + setter(instance, value); + return true; + } + } + + private sealed class DoubleSetter : PartFieldSetter + { + private Action setter; + + public DoubleSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + double value = double.Parse(strValue); + setter(instance, value); + return true; + } + } + + private sealed class IntSetter : PartFieldSetter + { + private Action setter; + + public IntSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + int value = int.Parse(strValue); + setter(instance, value); + return true; + } + } + + private sealed class BoolSetter : PartFieldSetter + { + private Action setter; + + public BoolSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + bool value = bool.Parse(strValue); + setter(instance, value); + return true; + } + } + + private sealed class StringSetter : PartFieldSetter + { + private Action setter; + + public StringSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + setter(instance, strValue); + return true; + } + } + + private sealed class EnumSetter : PartFieldSetter + { + private Action setter; + private Type enumType; + + public EnumSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceBoxedValueSetter(fieldInfo); + enumType = fieldInfo.FieldType; + } + + public override bool SetField(object instance, string strValue) + { + object value = Enum.Parse(enumType, strValue); + setter(instance, value); + return true; + } + } + + private sealed class Vector2Setter : PartFieldSetter + { + private Action setter; + + public Vector2Setter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + string[] array = strValue.Split(','); + if (array.Length < 2) + { + PDebug.Log($"WARNING: {strValue} is nor formatted properly! proper format for Vector2 is x,y"); + return false; + } + setter(instance, new Vector2(float.Parse(array[0]), float.Parse(array[1]))); + return true; + } + } + + private sealed class Vector3Setter : PartFieldSetter + { + private Action setter; + + public Vector3Setter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + string[] array = strValue.Split(','); + if (array.Length < 3) + { + PDebug.Log($"WARNING: {strValue} is nor formatted properly! proper format for Vector3 is x,y,z"); + return false; + } + setter(instance, new Vector3(float.Parse(array[0]), float.Parse(array[1]), float.Parse(array[2]))); + return true; + } + } + + private sealed class Vector4Setter : PartFieldSetter + { + private Action setter; + + public Vector4Setter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + string[] array = strValue.Split(','); + if (array.Length < 4) + { + PDebug.Log($"WARNING: {strValue} is nor formatted properly! proper format for Vector4 is x,y,z,w"); + return false; + } + setter(instance, new Vector4(float.Parse(array[0]), float.Parse(array[1]), float.Parse(array[2]), float.Parse(array[3]))); + return true; + } + } + + private sealed class QuaternionSetter : PartFieldSetter + { + private Action setter; + + public QuaternionSetter(FieldInfo fieldInfo) + { + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + string[] array = strValue.Split(','); + if (array.Length < 4) + { + PDebug.Log($"WARNING: {strValue} is nor formatted properly! proper format for Quaternion is angle(deg),x,y,z"); + return false; + } + setter(instance, Quaternion.AngleAxis(float.Parse(array[0]), new Vector3(float.Parse(array[1]), float.Parse(array[2]), float.Parse(array[3])))); + return true; + } + } + + private sealed class DragModelTypeSetter : PartFieldSetter + { + public override bool SetField(object instance, string value) + { + Part part = (Part)instance; + switch (value.ToUpper()) + { + case "SPHERICAL": + part.dragModel = Part.DragModel.SPHERICAL; + break; + case "NONE": + case "OVERRIDE": + part.dragModel = Part.DragModel.NONE; + break; + case "CYLINDRICAL": + part.dragModel = Part.DragModel.CYLINDRICAL; + break; + case "CONIC": + part.dragModel = Part.DragModel.CONIC; + break; + default: + part.dragModel = Part.DragModel.CUBE; + break; + } + return true; + } + } + + private sealed class PartRendererBoundsIgnoreTypeSetter : PartFieldSetter + { + public override bool SetField(object instance, string value) + { + Part part = (Part)instance; + string[] splitted = value.Split(','); + part.partRendererBoundsIgnore = new List(); + for (int i = 0; i < splitted.Length; i++) + { + part.partRendererBoundsIgnore.Add(splitted[i].Trim()); + } + return true; + } + } + + private sealed class AnonymousSetter : PartFieldSetter + { + private Action setter; + //private Func parser; + private MethodInfo parserMethod; + private string[] valueBuffer = new string[1]; + + public AnonymousSetter(FieldInfo fieldInfo, MethodInfo parserMethod) + { + //parser = (Func)Delegate.CreateDelegate(typeof(Func), parserMethod); + this.parserMethod = parserMethod; + if (fieldInfo.FieldType.IsValueType) + setter = CreateInstanceBoxedValueSetter(fieldInfo); + else + setter = CreateInstanceSetter(fieldInfo); + } + + public override bool SetField(object instance, string strValue) + { + //object value = parser(strValue); + valueBuffer[0] = strValue; + object value = parserMethod.Invoke(null, valueBuffer); + setter(instance, value); + return true; + } + } + + private static Action CreateInstanceSetter(FieldInfo field) + { + string methodName = field.ReflectedType.FullName + ".set_" + field.Name; + DynamicMethod setterMethod = new DynamicMethod(methodName, null, new Type[2] { typeof(object), typeof(T) }, true); + ILGenerator gen = setterMethod.GetILGenerator(); + gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Ldarg_1); + gen.Emit(OpCodes.Stfld, field); + gen.Emit(OpCodes.Ret); + return (Action)setterMethod.CreateDelegate(typeof(Action)); + } + + private static Action CreateInstanceBoxedValueSetter(FieldInfo field) + { + string methodName = field.ReflectedType.FullName + ".set_" + field.Name; + DynamicMethod setterMethod = new DynamicMethod(methodName, null, new Type[2] { typeof(object), typeof(object) }, true); + ILGenerator gen = setterMethod.GetILGenerator(); + gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Ldarg_1); + gen.Emit(OpCodes.Unbox_Any, field.FieldType); + gen.Emit(OpCodes.Stfld, field); + gen.Emit(OpCodes.Ret); + return (Action)setterMethod.CreateDelegate(typeof(Action)); + } + + private static bool partFieldDictionariesAreBuilt; + + private static void BuildPartFieldDictionaries() + { + PopulatePartFieldDictionary(typeof(Part), partFieldsSetters); + PopulatePartFieldDictionary(typeof(CompoundPart), compoundPartFieldsSetters); + + foreach (AssemblyLoader.LoadedAssembly assembly in AssemblyLoader.loadedAssemblies.assemblies) + { + if (assembly.typesDictionary.TryGetValue(typeof(Part), out Dictionary partTypes)) + { + foreach (Type unknownPartType in partTypes.Values) + { + if (unknownPartType != typeof(Part) && unknownPartType != typeof(CompoundPart)) + { + Dictionary unknownTypeDict = new Dictionary(300); + unknownPartFieldsSetters.Add(unknownPartType, unknownTypeDict); + PopulatePartFieldDictionary(unknownPartType, unknownTypeDict); + } + } + } + } + } + + private static void PopulatePartFieldDictionary(Type partType, Dictionary typeSetterDict) + { + foreach (FieldInfo fieldInfo in partType.GetFields(BindingFlags.Instance | BindingFlags.Public)) + { + PartFieldSetter setter; + + string fieldName = fieldInfo.Name; + + if (fieldName == "dragModelType") + { + setter = new DragModelTypeSetter(); + } + else if (fieldName == "partRendererBoundsIgnore") + { + setter = new PartRendererBoundsIgnoreTypeSetter(); + } + else if (fieldName == "iconCenter" + || fieldName == "alphaCutoff" + || fieldName == "scale" + || fieldName == "texture" + || fieldName == "normalmap" + || fieldName == "name" + || fieldName == "specPower" + || fieldName == "rimFalloff" + || fieldName == "mesh" + || fieldName == "subcategory" + || fieldName == "module" + || fieldName == "exportScale" + || fieldName.StartsWith("node") + || fieldName.StartsWith("fx") + || fieldName.StartsWith("sound")) + { + // ignored field, just have a null entry + setter = null; + } + else + { + Type fieldType = fieldInfo.FieldType; + if (fieldType == typeof(float)) + { + setter = new FloatSetter(fieldInfo); + } + else if (fieldType == typeof(double)) + { + setter = new DoubleSetter(fieldInfo); + } + else if (fieldType == typeof(int)) + { + setter = new IntSetter(fieldInfo); + } + else if (fieldType == typeof(bool)) + { + setter = new BoolSetter(fieldInfo); + } + else if (fieldType == typeof(string)) + { + setter = new StringSetter(fieldInfo); + } + else if (fieldType == typeof(Vector2)) + { + setter = new Vector2Setter(fieldInfo); + } + else if (fieldType == typeof(Vector3)) + { + setter = new Vector3Setter(fieldInfo); + } + else if (fieldType == typeof(Vector4)) + { + setter = new Vector4Setter(fieldInfo); + } + else if (fieldType == typeof(Quaternion)) + { + setter = new QuaternionSetter(fieldInfo); + } + else if (fieldType.IsEnum) + { + setter = new EnumSetter(fieldInfo); + } + else + { + MethodInfo parseMethod = fieldType.GetMethod("Parse", new[] { typeof(string) }); + if (parseMethod == null) + { + setter = null; + } + else + { + setter = new AnonymousSetter(fieldInfo, parseMethod); + } + } + } + + typeSetterDict[fieldInfo.Name] = setter; + } + } + + static bool PartLoader_ApplyPartValue_Override(Part part, ConfigNode.Value nodeValue) + { + if (!partFieldDictionariesAreBuilt) + { + partFieldDictionariesAreBuilt = true; + BuildPartFieldDictionaries(); + } + + Type partType = part.GetType(); + Dictionary partFieldSetterDict; + if (partType == typeof(Part)) + partFieldSetterDict = partFieldsSetters; + else if (partType == typeof(CompoundPart)) + partFieldSetterDict = compoundPartFieldsSetters; + else if (!unknownPartFieldsSetters.TryGetValue(partType, out partFieldSetterDict)) + throw new Exception($"Unknown part type: '{partType}'"); + + string valueName = nodeValue.name; + if (partFieldSetterDict.TryGetValue(valueName, out PartFieldSetter setter)) + { + if (setter == null) + return false; + + return setter.SetField(part, nodeValue.value); + } + + return false; + } + } +}