diff --git a/GameData/KSPCommunityFixes/Settings.cfg b/GameData/KSPCommunityFixes/Settings.cfg
index d5ceb7d..5ba87e3 100644
--- a/GameData/KSPCommunityFixes/Settings.cfg
+++ b/GameData/KSPCommunityFixes/Settings.cfg
@@ -4,6 +4,9 @@
KSP_COMMUNITY_FIXES
{
+ OnDemandPartTextures = true
+
+
// ##########################
// Major bugfixes
// ##########################
diff --git a/KSPCommunityFixes/KSPCommunityFixes.csproj b/KSPCommunityFixes/KSPCommunityFixes.csproj
index 5ccec49..84d10ec 100644
--- a/KSPCommunityFixes/KSPCommunityFixes.csproj
+++ b/KSPCommunityFixes/KSPCommunityFixes.csproj
@@ -170,6 +170,7 @@
+
diff --git a/KSPCommunityFixes/Performance/FastLoader.cs b/KSPCommunityFixes/Performance/FastLoader.cs
index f26b41a..6563d0c 100644
--- a/KSPCommunityFixes/Performance/FastLoader.cs
+++ b/KSPCommunityFixes/Performance/FastLoader.cs
@@ -303,6 +303,8 @@ static IEnumerator FastAssetLoader(List configFileTypes)
gdb.progressTitle = "Searching assets to load...";
yield return null;
+ OnDemandPartTextures.GetTextures(out HashSet allPartTextures);
+
double nextFrameTime = ElapsedTime + minFrameTimeD;
// Files loaded by our custom loaders
@@ -350,26 +352,29 @@ static IEnumerator FastAssetLoader(List configFileTypes)
}
break;
case FileType.Texture:
+
+ bool isOnDemand = allPartTextures.Contains(file.url);
+
switch (file.fileExtension)
{
case "dds":
- textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureDDS));
+ textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureDDS, isOnDemand));
break;
case "jpg":
case "jpeg":
- textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureJPG));
+ textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureJPG, isOnDemand));
break;
case "mbm":
- textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureMBM));
+ textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureMBM, isOnDemand));
break;
case "png":
- textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TexturePNG));
+ textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TexturePNG, isOnDemand));
break;
case "tga":
- textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureTGA));
+ textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureTGA, isOnDemand));
break;
case "truecolor":
- textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureTRUECOLOR));
+ textureAssets.Add(new RawAsset(file, RawAsset.AssetType.TextureTRUECOLOR, isOnDemand));
break;
default:
unsupportedTextureFiles.Add(file);
@@ -802,6 +807,12 @@ static void ReadAssetsThread(List files, Deque buffer)
{
foreach (RawAsset rawAsset in files)
{
+ if (rawAsset.IsOnDemand)
+ {
+ buffer.AddToFront(rawAsset);
+ continue;
+ }
+
rawAsset.ReadFromDiskWorkerThread();
SpinWait spin = new SpinWait();
@@ -836,7 +847,7 @@ static void ReadAssetsThread(List files, Deque buffer)
///
/// Asset wrapper class, actual implementation of the disk reader, individual texture/model formats loaders
///
- private class RawAsset
+ internal class RawAsset
{
public enum AssetType
{
@@ -881,18 +892,21 @@ public enum Result
private BinaryReader binaryReader;
private Result result;
private string resultMessage;
+ private bool isOnDemand;
public UrlFile File => file;
public Result State => result;
public string Message => resultMessage;
public int DataLength => dataLength;
public string TypeName => assetTypeNames[(int)assetType];
+ public bool IsOnDemand => isOnDemand;
- public RawAsset(UrlFile file, AssetType assetType)
+ public RawAsset(UrlFile file, AssetType assetType, bool isOnDemand = false)
{
this.result = Result.Valid;
this.file = file;
this.assetType = assetType;
+ this.isOnDemand = isOnDemand;
}
private void SetError(string message)
@@ -1002,35 +1016,49 @@ public void LoadAndDisposeMainThread()
if (file.fileType == FileType.Texture)
{
TextureInfo textureInfo;
- switch (assetType)
+
+ if (isOnDemand)
{
- case AssetType.TextureDDS:
- textureInfo = LoadDDS();
- break;
- case AssetType.TextureJPG:
- textureInfo = LoadJPG();
- break;
- case AssetType.TextureMBM:
- textureInfo = LoadMBM();
- break;
- case AssetType.TexturePNG:
- textureInfo = LoadPNG();
- break;
- case AssetType.TexturePNGCached:
- textureInfo = LoadPNGCached();
- break;
- case AssetType.TextureTGA:
- textureInfo = LoadTGA();
- break;
- case AssetType.TextureTRUECOLOR:
- textureInfo = LoadTRUECOLOR();
- break;
- default:
- SetError("Unknown texture format");
- return;
+ textureInfo = new OnDemandTextureInfo(file, assetType);
}
+ else
+ {
+ switch (assetType)
+ {
+ case AssetType.TextureDDS:
+ textureInfo = LoadDDS();
+ break;
+ case AssetType.TextureJPG:
+ textureInfo = LoadJPG();
+ break;
+ case AssetType.TextureMBM:
+ textureInfo = LoadMBM();
+ break;
+ case AssetType.TexturePNG:
+ textureInfo = LoadPNG();
+ break;
+ case AssetType.TexturePNGCached:
+ textureInfo = LoadPNGCached();
+ break;
+ case AssetType.TextureTGA:
+ textureInfo = LoadTGA();
+ break;
+ case AssetType.TextureTRUECOLOR:
+ textureInfo = LoadTRUECOLOR();
+ break;
+ default:
+ SetError("Unknown texture format");
+ return;
+ }
+
+ if (textureInfo.texture.IsNullOrDestroyed())
+ result = Result.Failed;
- if (result == Result.Failed || textureInfo == null || textureInfo.texture.IsNullOrDestroyed())
+ textureInfo.texture.name = file.url;
+ textureInfo.name = file.url;
+ }
+
+ if (result == Result.Failed || textureInfo == null)
{
result = Result.Failed;
if (string.IsNullOrEmpty(resultMessage))
@@ -1038,8 +1066,6 @@ public void LoadAndDisposeMainThread()
}
else
{
- textureInfo.name = file.url;
- textureInfo.texture.name = file.url;
Instance.databaseTexture.Add(textureInfo);
}
}
diff --git a/KSPCommunityFixes/Performance/OnDemandPartTextures.cs b/KSPCommunityFixes/Performance/OnDemandPartTextures.cs
new file mode 100644
index 0000000..884fa8d
--- /dev/null
+++ b/KSPCommunityFixes/Performance/OnDemandPartTextures.cs
@@ -0,0 +1,470 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using HarmonyLib;
+using KSP.UI.Screens;
+using KSPCommunityFixes.QoL;
+using UnityEngine;
+using static KSP.UI.Screens.Settings.SettingsSetup;
+using static KSPCommunityFixes.Performance.KSPCFFastLoader;
+using static KSPCommunityFixes.QoL.NoIVA;
+using static ProceduralSpaceObject;
+
+namespace KSPCommunityFixes.Performance
+{
+ public class OnDemandPartTextures : BasePatch
+ {
+ internal static Dictionary> partsTextures;
+ internal static Dictionary prefabsData = new Dictionary();
+
+ protected override void ApplyPatches(List patches)
+ {
+ MethodInfo m_Instantiate = null;
+ foreach (MethodInfo methodInfo in typeof(UnityEngine.Object).GetMethods(BindingFlags.Public | BindingFlags.Static))
+ {
+ if (methodInfo.IsGenericMethod
+ && methodInfo.Name == nameof(UnityEngine.Object.Instantiate)
+ && methodInfo.GetParameters().Length == 1)
+ {
+ m_Instantiate = methodInfo.MakeGenericMethod(typeof(UnityEngine.Object));
+ break;
+ }
+ }
+
+ patches.Add(new PatchInfo(
+ PatchMethodType.Prefix,
+ m_Instantiate,
+ this, nameof(Instantiate_Prefix)));
+
+ patches.Add(new PatchInfo(
+ PatchMethodType.Postfix,
+ AccessTools.Method(typeof(PartLoader), nameof(PartLoader.ParsePart)),
+ this));
+ }
+
+ static void Instantiate_Prefix(UnityEngine.Object original)
+ {
+ if (original is Part part
+ && part.partInfo != null
+ && part.partInfo.partPrefab.IsNotNullRef() // skip instantiation of the special prefabs (kerbals, flag) during loading
+ && prefabsData.TryGetValue(part.partInfo, out PrefabData prefabData)
+ && !prefabData.areTexturesLoaded)
+ {
+ // load textures
+ // set them on the prefab
+ prefabData.areTexturesLoaded = true;
+ }
+ }
+
+ static void PartLoader_ParsePart_Postfix(AvailablePart __result)
+ {
+ if (!partsTextures.TryGetValue(__result.name, out List textures))
+ return;
+
+ Transform modelTransform = __result.partPrefab.transform.Find("model");
+ if (modelTransform == null)
+ return;
+
+ PrefabData prefabData = null;
+
+ foreach (Renderer renderer in modelTransform.GetComponentsInChildren(true))
+ {
+ if (renderer is ParticleSystemRenderer || renderer.sharedMaterial.IsNullOrDestroyed())
+ continue;
+
+ Material material = renderer.sharedMaterial;
+ MaterialTextures materialTextures = null;
+
+ if (material.HasProperty("_MainTex"))
+ {
+ Texture currentTex = material.GetTexture("_MainTex");
+ if (currentTex.IsNotNullRef())
+ {
+ string currentTexName = currentTex.name;
+ if (OnDemandTextureInfo.texturesByUrl.TryGetValue(currentTexName, out OnDemandTextureInfo textureInfo))
+ {
+ if (prefabData == null)
+ prefabData = new PrefabData();
+
+ if (materialTextures == null)
+ materialTextures = new MaterialTextures(material);
+
+ prefabData.materials.Add(materialTextures);
+ materialTextures.mainTex = textureInfo;
+ }
+
+ }
+ }
+
+ if (material.HasProperty("_BumpMap"))
+ {
+ Texture currentTex = material.GetTexture("_BumpMap");
+ if (currentTex.IsNotNullRef())
+ {
+ string currentTexName = currentTex.name;
+ if (OnDemandTextureInfo.texturesByUrl.TryGetValue(currentTexName, out OnDemandTextureInfo textureInfo))
+ {
+ if (prefabData == null)
+ prefabData = new PrefabData();
+
+ if (materialTextures == null)
+ materialTextures = new MaterialTextures(material);
+
+ prefabData.materials.Add(materialTextures);
+ materialTextures.bumpMap = textureInfo;
+ }
+
+ }
+ }
+
+ if (material.HasProperty("_Emissive"))
+ {
+ Texture currentTex = material.GetTexture("_Emissive");
+ if (currentTex.IsNotNullRef())
+ {
+ string currentTexName = currentTex.name;
+ if (OnDemandTextureInfo.texturesByUrl.TryGetValue(currentTexName, out OnDemandTextureInfo textureInfo))
+ {
+ if (prefabData == null)
+ prefabData = new PrefabData();
+
+ if (materialTextures == null)
+ materialTextures = new MaterialTextures(material);
+
+ prefabData.materials.Add(materialTextures);
+ materialTextures.emissive = textureInfo;
+ }
+
+ }
+ }
+
+ if (material.HasProperty("_SpecMap"))
+ {
+ Texture currentTex = material.GetTexture("_SpecMap");
+ if (currentTex.IsNotNullRef())
+ {
+ string currentTexName = currentTex.name;
+ if (OnDemandTextureInfo.texturesByUrl.TryGetValue(currentTexName, out OnDemandTextureInfo textureInfo))
+ {
+ if (prefabData == null)
+ prefabData = new PrefabData();
+
+ if (materialTextures == null)
+ materialTextures = new MaterialTextures(material);
+
+ prefabData.materials.Add(materialTextures);
+ materialTextures.specMap = textureInfo;
+ }
+
+ }
+ }
+ }
+
+ if (prefabData != null)
+ prefabsData[__result] = prefabData;
+ }
+
+ private static readonly char[] textureSplitChars = { ':', ',', ';' };
+
+ public static void GetTextures(out HashSet allPartTextures)
+ {
+ Dictionary> modelParts = new Dictionary>();
+ Dictionary> partTextureReplacements = new Dictionary>();
+
+ foreach (UrlDir.UrlConfig urlConfig in GameDatabase.Instance.root.AllConfigs)
+ {
+ if (urlConfig.type != "PART")
+ continue;
+
+ bool hasModelInModelNode = false;
+
+ string partName = urlConfig.config.GetValue("name");
+ if (partName == null)
+ continue;
+
+ partName = partName.Replace('_', '.');
+
+ foreach (ConfigNode partNode in urlConfig.config.nodes)
+ {
+ if (partNode.name == "MODEL")
+ {
+ List texturePaths = null;
+ foreach (ConfigNode.Value modelValue in partNode.values)
+ {
+ if (modelValue.name == "model")
+ {
+ hasModelInModelNode = true;
+
+ if (!modelParts.TryGetValue(modelValue.value, out List parts))
+ {
+ parts = new List();
+ modelParts[modelValue.value] = parts;
+ }
+
+ parts.Add(partName);
+ }
+ else if (modelValue.name == "texture")
+ {
+ string[] array = modelValue.value.Split(textureSplitChars, StringSplitOptions.RemoveEmptyEntries);
+ if (array.Length != 2)
+ continue;
+
+ if (texturePaths == null)
+ texturePaths = new List();
+
+ texturePaths.Add(new TextureReplacement(array[0].Trim(), array[1].Trim()));
+ }
+ }
+
+ if (texturePaths != null)
+ partTextureReplacements[partName] = texturePaths;
+ }
+ }
+
+ if (!hasModelInModelNode)
+ {
+ foreach (UrlDir.UrlFile urlFile in urlConfig.parent.parent.files)
+ {
+ if (urlFile.fileExtension == "mu")
+ {
+ if (!modelParts.TryGetValue(urlFile.url, out List parts))
+ {
+ parts = new List();
+ modelParts[urlFile.url] = parts;
+ }
+
+ parts.Add(partName);
+ break; // only first model found should be added
+ }
+ }
+ }
+ }
+
+ partsTextures = new Dictionary>(500);
+ allPartTextures = new HashSet(500);
+
+ HashSet allTextures = new HashSet(2000);
+ HashSet allReplacedTextures = new HashSet(500);
+ List texturePathsBuffer = new List();
+ List textureFileNameBuffer = new List();
+
+ foreach (UrlDir.UrlFile urlFile in GameDatabase.Instance.root.AllFiles)
+ {
+ if (urlFile.fileType == UrlDir.FileType.Texture)
+ {
+ allTextures.Add(urlFile.url);
+ continue;
+ }
+
+ if (urlFile.fileType != UrlDir.FileType.Model)
+ continue;
+
+ if (!modelParts.TryGetValue(urlFile.url, out List parts))
+ continue;
+
+ foreach (string textureFile in MuParser.GetModelTextures(urlFile.fullPath))
+ {
+ string textureFileName = Path.GetFileNameWithoutExtension(textureFile);
+ textureFileNameBuffer.Add(textureFileName);
+ texturePathsBuffer.Add(urlFile.parent.url + "/" + textureFileName);
+ }
+
+ if (texturePathsBuffer.Count == 0)
+ {
+ modelParts.Remove(urlFile.url);
+ continue;
+ }
+
+ foreach (string part in parts)
+ {
+ if (partTextureReplacements.TryGetValue(part, out List textureReplacements))
+ {
+ foreach (TextureReplacement textureReplacement in textureReplacements)
+ {
+ for (int i = 0; i < textureFileNameBuffer.Count; i++)
+ {
+ if (textureReplacement.textureName == textureFileNameBuffer[i])
+ {
+ allReplacedTextures.Add(texturePathsBuffer[i]);
+ texturePathsBuffer[i] = textureReplacement.replacementUrl;
+ }
+ }
+ }
+ }
+
+ if (!partsTextures.TryGetValue(part, out List textures))
+ {
+ textures = new List(texturePathsBuffer);
+ partsTextures[part] = textures;
+ }
+ else
+ {
+ textures.AddRange(texturePathsBuffer);
+ }
+ }
+
+ texturePathsBuffer.Clear();
+ textureFileNameBuffer.Clear();
+ }
+
+ List stringBuffer = new List();
+ foreach (KeyValuePair> partTextures in partsTextures)
+ {
+ for (int i = partTextures.Value.Count; i-- > 0;)
+ {
+ string texture = partTextures.Value[i];
+ if (!allTextures.Contains(texture))
+ {
+ partTextures.Value.RemoveAt(i);
+ }
+ else
+ {
+ allPartTextures.Add(texture);
+ }
+ }
+
+ if (partTextures.Value.Count == 0)
+ stringBuffer.Add(partTextures.Key);
+ }
+
+ for (int i = stringBuffer.Count; i-- > 0;)
+ partsTextures.Remove(stringBuffer[i]);
+
+ stringBuffer.Clear();
+ foreach (string replacedTexture in allReplacedTextures)
+ if (allPartTextures.Contains(replacedTexture))
+ stringBuffer.Add(replacedTexture);
+
+ for (int i = stringBuffer.Count; i-- > 0;)
+ allReplacedTextures.Remove(stringBuffer[i]);
+ }
+ }
+
+
+ //[KSPAddon(KSPAddon.Startup.Instantly, true)]
+ public class OnDemandPartTexturesLoader// : MonoBehaviour
+ {
+
+
+
+
+ // 1
+ // build a > dictionary
+ // 1.a.
+ // - Parse part configs
+ // - find MODEL > model reference
+ // - find MODEL > texture refernces
+ // - find ModulePartVariants > TEXTURE references
+ // - In all found model references :
+ // - find all texture references
+ // Patch Part.Awake(), after RelinkPrefab()
+ // - swap the model textures
+
+ // overview of where textures can come from :
+ // - defined in the model(s) materials
+ // - PART > "mesh" : depreciated/unused
+ // - If no PART > MODEL node : the first found (as ordered in GameDatabase.databaseModel) *.mu model placed in the same directory as the part config (cfg.parent.parent.url)
+ // - If PART > MODEL node(s) : the model defined in the "model" value
+ // - not sure if it is possible to set a path to the texture in the model ? I don't think so, but...
+ // - defined as a texture replacement in the PART > MODEL node
+ // - the replacement is done on the prefab renderers sharedMaterial
+ // - as far as I can tell, this require (a potentially dummy) texture with the same name as what is backed in the model to be sitting next to the model in the same directory
+
+
+ }
+
+ internal class TextureReplacement
+ {
+ public string textureName;
+ public string replacementUrl;
+
+ public TextureReplacement(string textureName, string replacementUrl)
+ {
+ this.textureName = textureName;
+ this.replacementUrl = replacementUrl;
+ }
+ }
+
+ internal class PrefabData
+ {
+ public bool areTexturesLoaded;
+ public List materials;
+
+ public void LoadTextures()
+ {
+
+ }
+ }
+
+ internal class MaterialTextures
+ {
+ private Material material;
+ public OnDemandTextureInfo mainTex;
+ public OnDemandTextureInfo bumpMap;
+ public OnDemandTextureInfo emissive;
+ public OnDemandTextureInfo specMap;
+
+ public MaterialTextures(Material material)
+ {
+ this.material = material;
+ }
+
+ public void LoadTextures()
+ {
+ if (mainTex != null)
+ {
+ mainTex.Load();
+ material.SetTexture("_MainTex", mainTex.texture);
+ }
+
+ if (bumpMap != null)
+ {
+ bumpMap.Load();
+ material.SetTexture("_BumpMap", bumpMap.texture);
+ }
+
+ if (emissive != null)
+ {
+ emissive.Load();
+ material.SetTexture("_Emissive", emissive.texture);
+ }
+
+ if (specMap != null)
+ {
+ specMap.Load();
+ material.SetTexture("_SpecMap", specMap.texture);
+ }
+ }
+ }
+
+ internal class OnDemandTextureInfo : GameDatabase.TextureInfo
+ {
+ public static Dictionary texturesByUrl = new Dictionary();
+
+ private Texture2D dummyTexture;
+
+ private bool isLoaded;
+ private RawAsset.AssetType textureType;
+
+ public OnDemandTextureInfo(UrlDir.UrlFile file, RawAsset.AssetType textureType, bool isNormalMap = false, bool isReadable = false, bool isCompressed = false, Texture2D texture = null)
+ : base(file, texture, isNormalMap, isReadable, isCompressed)
+ {
+ name = file.url;
+ this.textureType = textureType;
+ dummyTexture = new Texture2D(1, 1, TextureFormat.ARGB32, false);
+ dummyTexture.Apply(false, true);
+ dummyTexture.name = name;
+ this.texture = dummyTexture;
+ texturesByUrl.Add(name, this);
+ }
+
+ public void Load()
+ {
+
+ }
+ }
+}