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