Fixes and improvements, no more FindObjectsOfType

This commit is contained in:
Andrej Stojkovikj committed 2026-10-18 03:16:05 +02:00
1 parent a740fec7c8
commit fb002cbf0c
328 files changed
+4318 -299

No files matched your search

+262 -65
View File
@@ -11,6 +11,7 @@ using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Skopje.Modding;
using UnityEngine;
using UnityEngine.Rendering;
namespace Skopje.CustomPlayerSkins
{
@@ -19,7 +20,7 @@ namespace Skopje.CustomPlayerSkins
{
public const string PluginGuid = "com.skopje.customplayerskins";
public const string PluginName = "Skopje Custom Player Skins";
public const string PluginVersion = "1.0.0";
public const string PluginVersion = "1.3.0";
internal static ManualLogSource LogSource;
internal static ConfigEntry<bool> Enabled;
@@ -33,6 +34,13 @@ namespace Skopje.CustomPlayerSkins
Enabled = Config.Bind("Skin", "Enabled", true, "Replace each player outfit with its own PNG in the CustomSkins folder next to this plugin.");
_harmony = new Harmony(PluginGuid);
var setTexture = AccessTools.Method(typeof(Material), "SetTexture", new[] { typeof(string), typeof(Texture) });
if (setTexture != null)
_harmony.Patch(setTexture, prefix: new HarmonyMethod(typeof(SkinPatches), nameof(SkinPatches.BeforeSetTexture)));
var setTextureId = AccessTools.Method(typeof(Material), "SetTexture", new[] { typeof(int), typeof(Texture) });
if (setTextureId != null)
_harmony.Patch(setTextureId, prefix: new HarmonyMethod(typeof(SkinPatches), nameof(SkinPatches.BeforeSetTextureId)));
var refresh = AccessTools.Method(typeof(SkopjePlayer), "RefreshActiveModels");
if (refresh == null)
LogSource.LogWarning("SkopjePlayer.RefreshActiveModels was not found. The skins are applied when a player spawns.");
@@ -42,13 +50,15 @@ namespace Skopje.CustomPlayerSkins
_onPlayerStarted = DelegateSupport.ConvertDelegate<Il2CppSystem.Action<SkopjePlayer>>(new Action<SkopjePlayer>(SkinReplacer.ApplyPlayer));
ModAPI.add_OnPlayerStartedClient(_onPlayerStarted);
LogSource.LogInfo($"{PluginName} {PluginVersion} loaded. Edit the PNGs in CustomSkins: singleplayer, ranger, medic, engineer, and fighter (the Warrior class).");
LogSource.LogInfo($"{PluginName} {PluginVersion} loaded. Edit the PNGs in CustomSkins: singleplayer, ranger, medic, engineer, fighter (Warrior), prp, and hands.");
}
public override bool Unload()
{
if (_harmony != null)
_harmony.UnpatchSelf();
if (_onPlayerStarted != null)
ModAPI.remove_OnPlayerStartedClient(_onPlayerStarted);
SkinReplacer.Restore();
return true;
}
@@ -60,6 +70,18 @@ namespace Skopje.CustomPlayerSkins
{
SkinReplacer.ApplyPlayer(__instance);
}
public static void BeforeSetTexture(string name, ref Texture value)
{
if (name == "_MainTex")
SkinReplacer.Redirect(ref value);
}
public static void BeforeSetTextureId(int nameID, ref Texture value)
{
if (nameID == Shader.PropertyToID("_MainTex"))
SkinReplacer.Redirect(ref value);
}
}
static class SkinReplacer
@@ -68,6 +90,7 @@ namespace Skopje.CustomPlayerSkins
{
public string FileName;
public string DumpMaterial;
public string SourceTexture;
public string[] Materials;
public Texture2D Texture;
public DateTime WriteTimeUtc;
@@ -80,72 +103,84 @@ namespace Skopje.CustomPlayerSkins
{
FileName = "singleplayer.png",
DumpMaterial = "MAT_Pioneer_Default_Clothes",
SourceTexture = "DIF_Pioneer_Clothes_Default",
Materials = new[] { "MAT_Pioneer_Default_Clothes", "MAT_Pioneer_Default_Clothes_FP" },
},
new Outfit
{
FileName = "ranger.png",
DumpMaterial = "MAT_Pioneer_Ranger_Clothes",
SourceTexture = "DIF_Pioneer_Clothes_Ranger",
Materials = new[] { "MAT_Pioneer_Ranger_Clothes", "MAT_Pioneer_Ranger_Clothes_FP" },
},
new Outfit
{
FileName = "medic.png",
DumpMaterial = "MAT_Pioneer_Medic_Clothes",
SourceTexture = "DIF_Pioneer_Clothes_Medic",
Materials = new[] { "MAT_Pioneer_Medic_Clothes", "MAT_Pioneer_Medic_Clothes_FP" },
},
new Outfit
{
FileName = "engineer.png",
DumpMaterial = "MAT_Pioneer_Engi_Clothes",
SourceTexture = "DIF_Pioneer_Clothes_Engi",
Materials = new[] { "MAT_Pioneer_Engi_Clothes", "MAT_Pioneer_Engi_Clothes_FP" },
},
new Outfit
{
FileName = "fighter.png",
DumpMaterial = "MAT_Pioneer_Fighter_Clothes",
SourceTexture = "DIF_Pioneer_Clothes_Fighter",
Materials = new[] { "MAT_Pioneer_Fighter_Clothes", "MAT_Pioneer_Fighter_Clothes_FP" },
},
new Outfit
{
FileName = "prp.png",
DumpMaterial = "MAT_PRP",
SourceTexture = "DIF_PRP",
Materials = new[] { "MAT_PRP", "MAT_PRP_FP" },
},
new Outfit
{
FileName = "hands.png",
DumpMaterial = "MAT_Pioneer_Naked",
SourceTexture = "DIF_FPSHands_Naked",
Materials = new[] { "MAT_Pioneer_Naked" },
},
};
static readonly Dictionary<int, Texture> _originals = new Dictionary<int, Texture>();
static readonly Dictionary<int, Material> _materials = new Dictionary<int, Material>();
static readonly Dictionary<int, Renderer> _renderers = new Dictionary<int, Renderer>();
static readonly MaterialPropertyBlock _block = new MaterialPropertyBlock();
static float _nextFileCheck;
static bool _loggedMiss;
static bool _loggedApply;
public static void ApplyPlayer(SkopjePlayer player)
{
if (!CustomPlayerSkinsPlugin.Enabled.Value)
{
RevertMaterials();
RevertLive();
return;
}
if (!EnsureTextures())
if (player == null || !EnsureTextures(player))
return;
int applied = ApplyLoadedMaterials();
if (player != null)
int applied = ApplyPlayerRenderers(player);
if (applied > 0 && !_loggedApply)
{
applied += ApplySkins(player.PlayerSkins);
applied += ApplyRenderer(player.FirstPersonRenderer);
applied += ApplyRenderer(player.ThirdPersonRenderer);
}
if (applied == 0 && !_loggedMiss)
{
_loggedMiss = true;
CustomPlayerSkinsPlugin.LogSource.LogWarning("Custom skins are loaded, but the player clothes materials are not in memory yet.");
}
else if (applied > 0)
{
_loggedMiss = false;
CustomPlayerSkinsPlugin.LogSource.LogInfo($"Applied {applied} player clothes material{(applied == 1 ? "" : "s")} from CustomSkins.");
_loggedApply = true;
CustomPlayerSkinsPlugin.LogSource.LogInfo($"Applied {applied} player material{(applied == 1 ? "" : "s")} from CustomSkins.");
}
}
public static void Restore()
{
RevertMaterials();
RevertLive();
_originals.Clear();
_materials.Clear();
_renderers.Clear();
for (int i = 0; i < Outfits.Length; i++)
{
if (Outfits[i].Texture == null)
@@ -155,23 +190,42 @@ namespace Skopje.CustomPlayerSkins
}
}
static void RevertLive()
{
RevertMaterials();
ClearRendererOverrides();
}
static void RevertMaterials()
{
foreach (var pair in _originals)
foreach (var pair in _materials)
{
var material = FindMaterial(pair.Key);
if (material != null)
material.SetTexture("_MainTex", pair.Value);
Material material = pair.Value;
if (material != null && _originals.TryGetValue(pair.Key, out Texture original))
material.SetTexture("_MainTex", original);
}
}
static int ApplyLoadedMaterials()
static void ClearRendererOverrides()
{
int applied = 0;
var materials = Resources.FindObjectsOfTypeAll<Material>();
for (int i = 0; i < materials.Length; i++)
applied += ApplyMaterial(materials[i]);
return applied;
foreach (var pair in _renderers)
{
Renderer renderer = pair.Value;
if (renderer == null)
continue;
Material[] materials = renderer.sharedMaterials;
if (materials == null)
continue;
for (int slot = 0; slot < materials.Length; slot++)
{
Material material = materials[slot];
if (material == null || !_originals.TryGetValue(material.GetInstanceID(), out Texture original))
continue;
renderer.GetPropertyBlock(_block, slot);
_block.SetTexture("_MainTex", original);
renderer.SetPropertyBlock(_block, slot);
}
}
}
static int ApplySkins(Il2CppReferenceArray<PlayerSkin> skins)
@@ -190,16 +244,103 @@ namespace Skopje.CustomPlayerSkins
return applied;
}
static int ApplyPlayerRenderers(SkopjePlayer player)
{
if (player == null)
return 0;
int applied = ApplySkins(player.PlayerSkins);
applied += ApplyRenderer(player.FirstPersonRenderer);
applied += ApplyRenderer(player.ThirdPersonRenderer);
applied += ApplyRenderer(player.ThirdPersonBackpackRenderer);
applied += ApplyOn(player.ThirdPerson);
applied += ApplyOn(player.FirstPerson);
if (player.SkopjeCharacterController != null)
applied += ApplyOn(player.SkopjeCharacterController.MeshRoot);
if (player.UtilityController != null)
applied += ApplyOn(player.UtilityController.FPSHolder);
if (player.WeaponsController != null)
applied += ApplyWeapons(player.WeaponsController);
return applied;
}
static int ApplyWeapons(WeaponsController weapons)
{
if (weapons == null)
return 0;
int applied = 0;
applied += ApplyOn(weapons.Socket);
applied += ApplyOn(weapons.ThrowSocket);
applied += ApplyOn(weapons.PRPObject);
applied += ApplyOn(weapons.BrokenPRPObject);
applied += ApplyOn(weapons.KnifeObject);
var list = weapons.AvailableWeapons;
if (list == null)
return applied;
for (int i = 0; i < list.Count; i++)
{
Weapon weapon = list[i];
if (weapon == null)
continue;
applied += ApplyOn(weapon.WeaponModel);
applied += ApplyOn(weapon.LeftHandWeaponModel);
applied += ApplyOn(weapon.PhysicsWeaponModel);
applied += ApplyOn(weapon.PhysicsLeftHandWeaponModel);
}
return applied;
}
static int ApplyOn(GameObject root)
{
if (root == null)
return 0;
return ApplyUnder(root.transform);
}
static int ApplyOn(Component root)
{
if (root == null)
return 0;
return ApplyUnder(root.transform);
}
static int ApplyUnder(Transform root)
{
if (root == null)
return 0;
Renderer[] found = root.GetComponentsInChildren<Renderer>(true);
int applied = 0;
for (int i = 0; i < found.Length; i++)
applied += ApplyRenderer(found[i]);
return applied;
}
static int ApplyRenderer(Renderer renderer)
{
if (renderer == null)
return 0;
var materials = renderer.sharedMaterials;
int applied = ApplyMaterialSlots(renderer, renderer.sharedMaterials, false);
applied += ApplyMaterialSlots(renderer, renderer.materials, true);
return applied;
}
static int ApplyMaterialSlots(Renderer renderer, Material[] materials, bool propertyBlock)
{
if (materials == null)
return 0;
int applied = 0;
for (int i = 0; i < materials.Length; i++)
{
Outfit outfit = Resolve(materials[i]);
if (outfit == null)
continue;
applied += ApplyMaterial(materials[i]);
_renderers[renderer.GetInstanceID()] = renderer;
if (!propertyBlock)
continue;
renderer.GetPropertyBlock(_block, i);
_block.SetTexture("_MainTex", outfit.Texture);
renderer.SetPropertyBlock(_block, i);
}
return applied;
}
@@ -215,10 +356,8 @@ namespace Skopje.CustomPlayerSkins
static int ApplyMaterial(Material material)
{
if (material == null || !material.HasProperty("_MainTex"))
return 0;
Outfit outfit = FindOutfit(material.name);
if (outfit == null || outfit.Texture == null)
Outfit outfit = Resolve(material);
if (outfit == null)
return 0;
int id = material.GetInstanceID();
@@ -226,6 +365,7 @@ namespace Skopje.CustomPlayerSkins
{
var current = material.GetTexture("_MainTex");
_originals[id] = current;
_materials[id] = material;
CopySampling(outfit.Texture, current);
}
@@ -236,6 +376,54 @@ namespace Skopje.CustomPlayerSkins
return 1;
}
static Outfit Resolve(Material material)
{
if (material == null || !material.HasProperty("_MainTex"))
return null;
Outfit byName = FindOutfit(material.name);
if (byName != null && byName.Texture != null)
return byName;
Texture current = material.GetTexture("_MainTex");
if (current == null)
return null;
for (int i = 0; i < Outfits.Length; i++)
{
Outfit outfit = Outfits[i];
if (outfit.Texture == null)
continue;
if (current == outfit.Texture)
return outfit;
if (TextureNameMatches(current.name, outfit.SourceTexture))
return outfit;
}
return null;
}
public static void Redirect(ref Texture value)
{
if (value == null || !CustomPlayerSkinsPlugin.Enabled.Value)
return;
for (int i = 0; i < Outfits.Length; i++)
{
Outfit outfit = Outfits[i];
if (outfit.Texture == null || value == outfit.Texture)
continue;
if (TextureNameMatches(value.name, outfit.SourceTexture))
{
value = outfit.Texture;
return;
}
}
}
static bool TextureNameMatches(string textureName, string sourceName)
{
if (string.IsNullOrEmpty(textureName) || string.IsNullOrEmpty(sourceName))
return false;
return textureName == sourceName || textureName.StartsWith(sourceName + " ", StringComparison.Ordinal);
}
static Outfit FindOutfit(string materialName)
{
if (string.IsNullOrEmpty(materialName))
@@ -262,19 +450,7 @@ namespace Skopje.CustomPlayerSkins
target.anisoLevel = source.anisoLevel;
}
static Material FindMaterial(int instanceId)
{
var materials = Resources.FindObjectsOfTypeAll<Material>();
for (int i = 0; i < materials.Length; i++)
{
Material material = materials[i];
if (material != null && material.GetInstanceID() == instanceId)
return material;
}
return null;
}
static bool EnsureTextures()
static bool EnsureTextures(Component source)
{
if (AnyLoaded() && Time.unscaledTime < _nextFileCheck)
return true;
@@ -284,7 +460,7 @@ namespace Skopje.CustomPlayerSkins
int ready = 0;
for (int i = 0; i < Outfits.Length; i++)
{
if (EnsureOutfit(Outfits[i], directory))
if (EnsureOutfit(Outfits[i], directory, source))
ready++;
}
@@ -306,12 +482,12 @@ namespace Skopje.CustomPlayerSkins
return false;
}
static bool EnsureOutfit(Outfit outfit, string directory)
static bool EnsureOutfit(Outfit outfit, string directory, Component source)
{
string path = Path.Combine(directory, outfit.FileName);
if (!File.Exists(path))
{
if (!TryDumpSample(outfit, path))
if (!TryDumpSample(outfit, path, source))
return outfit.Texture != null;
}
@@ -348,27 +524,48 @@ namespace Skopje.CustomPlayerSkins
UnityEngine.Object.Destroy(outfit.Texture);
outfit.Texture = texture;
outfit.WriteTimeUtc = writeTime;
_loggedMiss = false;
_loggedApply = false;
CustomPlayerSkinsPlugin.LogSource.LogInfo($"Loaded {outfit.FileName} ({texture.width}x{texture.height})");
return true;
}
static bool TryDumpSample(Outfit outfit, string path)
static Texture FindSourceTexture(Component root, string materialName)
{
Texture source = null;
var materials = Resources.FindObjectsOfTypeAll<Material>();
for (int i = 0; i < materials.Length && source == null; i++)
if (root == null || string.IsNullOrEmpty(materialName))
return null;
Renderer[] renderers = root.GetComponentsInChildren<Renderer>(true);
for (int i = 0; i < renderers.Length; i++)
{
Material material = materials[i];
if (material == null || material.name != outfit.DumpMaterial)
Renderer renderer = renderers[i];
if (renderer == null)
continue;
if (!material.HasProperty("_MainTex"))
Material[] materials = renderer.sharedMaterials;
if (materials == null)
continue;
Texture current = material.GetTexture("_MainTex");
if (_originals.TryGetValue(material.GetInstanceID(), out Texture original))
current = original;
source = current;
for (int slot = 0; slot < materials.Length; slot++)
{
Material material = materials[slot];
if (material == null || !NameIs(material.name, materialName) || !material.HasProperty("_MainTex"))
continue;
Texture current = material.GetTexture("_MainTex");
if (_originals.TryGetValue(material.GetInstanceID(), out Texture original) && original != null)
current = original;
if (current != null)
return current;
}
}
return null;
}
static bool NameIs(string materialName, string expected)
{
return materialName == expected || materialName.StartsWith(expected + " (", StringComparison.Ordinal);
}
static bool TryDumpSample(Outfit outfit, string path, Component root)
{
Texture source = FindSourceTexture(root, outfit.DumpMaterial);
if (source == null)
return false;