Skip to content

Commit e7cb886

Browse files
committed
fix: Mono Custom NPCs
1 parent 167bfca commit e7cb886

3 files changed

Lines changed: 162 additions & 56 deletions

File tree

S1API/Entities/NPC.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,9 +1056,6 @@ protected NPC()
10561056

10571057
if (identity != null)
10581058
{
1059-
// On Mono, fields are auto-serialized and available directly
1060-
// On Il2Cpp, fields may not be populated yet, so check registry
1061-
#if IL2CPPMELON
10621059
string prefabName = gameObject.name;
10631060
if (NPCPrefabIdentity.TryGetIdentityFromRegistry(prefabName, out string regId, out string regFirstName, out string regLastName, out Sprite regIcon))
10641061
{
@@ -1069,18 +1066,12 @@ protected NPC()
10691066
}
10701067
else
10711068
{
1072-
// Fallback to component fields if registry lookup fails
1069+
// Fallback to component-backed values if the registry is unavailable.
10731070
id = identity.Id;
10741071
firstName = identity.FirstName;
10751072
lastName = identity.LastName;
10761073
icon = identity.Icon;
10771074
}
1078-
#else
1079-
id = identity.Id;
1080-
firstName = identity.FirstName;
1081-
lastName = identity.LastName;
1082-
icon = identity.Icon;
1083-
#endif
10841075
}
10851076

10861077
// Apply identity values

S1API/Internal/Entities/NPCPrefabIdentity.cs

Lines changed: 129 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,97 @@ namespace S1API.Internal.Entities
3535
internal sealed class NPCPrefabIdentity : MonoBehaviour
3636
{
3737
private static readonly Log Logger = new Log("NPCPrefabIdentity");
38-
39-
// fields for Mono compatibility (auto-serialized there)
40-
internal string Id;
41-
internal string FirstName;
42-
internal string LastName;
43-
internal Sprite Icon;
44-
internal S1AvatarFramework.AvatarSettings AppearanceDefaults;
45-
internal string DealerHomeBuildingName;
46-
private string PrefabName;
47-
48-
// Relationship data fields for Mono compatibility
49-
private float? RelationDelta;
50-
private bool? Unlocked;
51-
private NPCRelationship.UnlockType? UnlockType;
52-
private List<string> ConnectionIDs;
38+
39+
// Mono prefab cloning relies on Unity serialization for these backing fields.
40+
#if IL2CPPMELON
41+
private string? _id;
42+
private string? _firstName;
43+
private string? _lastName;
44+
private Sprite? _icon;
45+
private S1AvatarFramework.AvatarSettings? _appearanceDefaults;
46+
private string? _dealerHomeBuildingName;
47+
private string? _prefabName;
48+
private List<string>? _connectionIds;
49+
#else
50+
[SerializeField] private string? _id;
51+
[SerializeField] private string? _firstName;
52+
[SerializeField] private string? _lastName;
53+
[SerializeField] private Sprite? _icon;
54+
[SerializeField] private S1AvatarFramework.AvatarSettings? _appearanceDefaults;
55+
[SerializeField] private string? _dealerHomeBuildingName;
56+
[SerializeField] private string? _prefabName;
57+
[SerializeField] private List<string>? _connectionIds;
58+
#endif
59+
60+
private float? _relationDelta;
61+
private bool? _unlocked;
62+
private NPCRelationship.UnlockType? _unlockType;
5363

5464
// Static registry to preserve data across network instantiation on Il2Cpp
5565
private static readonly Dictionary<string, IdentityData> _registry = new Dictionary<string, IdentityData>();
5666
private bool _applied;
5767
private AvatarSettingsData _cachedAppearanceDefaults;
5868

69+
internal string? Id
70+
{
71+
get => _id;
72+
set => _id = value;
73+
}
74+
75+
internal string? FirstName
76+
{
77+
get => _firstName;
78+
set => _firstName = value;
79+
}
80+
81+
internal string? LastName
82+
{
83+
get => _lastName;
84+
set => _lastName = value;
85+
}
86+
87+
internal Sprite? Icon
88+
{
89+
get => _icon;
90+
set => _icon = value;
91+
}
92+
93+
internal S1AvatarFramework.AvatarSettings? AppearanceDefaults
94+
{
95+
get => _appearanceDefaults;
96+
set => _appearanceDefaults = value;
97+
}
98+
99+
internal string? DealerHomeBuildingName
100+
{
101+
get => _dealerHomeBuildingName;
102+
set => _dealerHomeBuildingName = value;
103+
}
104+
105+
internal string? PrefabName
106+
{
107+
get => _prefabName;
108+
set => _prefabName = value;
109+
}
110+
111+
private float? RelationDelta
112+
{
113+
get => _relationDelta;
114+
set => _relationDelta = value;
115+
}
116+
117+
private bool? Unlocked
118+
{
119+
get => _unlocked;
120+
set => _unlocked = value;
121+
}
122+
123+
private NPCRelationship.UnlockType? UnlockType
124+
{
125+
get => _unlockType;
126+
set => _unlockType = value;
127+
}
128+
59129
private struct IdentityData
60130
{
61131
internal string Id;
@@ -190,7 +260,7 @@ internal void RegisterToStaticCache(string prefabName)
190260
PrefabName = normalizedName;
191261

192262
// CRITICAL: Always check registry FIRST for connection IDs since they're set via RegisterRelationshipDataToStaticCache
193-
// Component field (this.ConnectionIDs) is never set during prefab configuration in Menu scene
263+
// Component field (_connectionIds) is never set during prefab configuration in Menu scene
194264
// Connection IDs are only stored via RegisterRelationshipDataToStaticCache, so we must preserve them from registry
195265
List<string> connectionIDs = null;
196266
string dealerHomeBuildingName = this.DealerHomeBuildingName;
@@ -220,9 +290,9 @@ internal void RegisterToStaticCache(string prefabName)
220290

221291
// Only use component field if registry doesn't have connection IDs
222292
// (Component field is typically empty during prefab configuration, but check it as fallback)
223-
if ((connectionIDs == null || connectionIDs.Count == 0) && this.ConnectionIDs != null && this.ConnectionIDs.Count > 0)
293+
if ((connectionIDs == null || connectionIDs.Count == 0) && _connectionIds != null && _connectionIds.Count > 0)
224294
{
225-
connectionIDs = new List<string>(this.ConnectionIDs);
295+
connectionIDs = new List<string>(_connectionIds);
226296
}
227297

228298
var identityData = new IdentityData
@@ -340,7 +410,7 @@ private void TryRestoreFromRegistry()
340410
this.RelationDelta = dataRef.RelationDelta;
341411
this.Unlocked = dataRef.Unlocked;
342412
this.UnlockType = dataRef.UnlockType.HasValue ? (NPCRelationship.UnlockType?)dataRef.UnlockType.Value : null;
343-
this.ConnectionIDs = dataRef.ConnectionIDs != null ? new List<string>(dataRef.ConnectionIDs) : null;
413+
_connectionIds = dataRef.ConnectionIDs != null ? new List<string>(dataRef.ConnectionIDs) : null;
344414
PrefabName = dataRef.PrefabName ?? PrefabName;
345415

346416
// Debug log for connection restoration
@@ -383,7 +453,7 @@ private IEnumerator DelayedApply()
383453
private void EnsureRelationshipDataFromRegistry()
384454
{
385455
// Always try to restore to ensure fields are populated (Il2Cpp wipes component fields).
386-
if (ConnectionIDs == null || ConnectionIDs.Count == 0 || !Unlocked.HasValue || !RelationDelta.HasValue || !UnlockType.HasValue)
456+
if (_connectionIds == null || _connectionIds.Count == 0 || !Unlocked.HasValue || !RelationDelta.HasValue || !UnlockType.HasValue)
387457
{
388458
TryRestoreFromRegistry();
389459
}
@@ -489,9 +559,9 @@ internal void ApplyRelationshipDataTo(S1NPCs.NPC npc, bool preserveUnlockState =
489559
if (UnlockType.HasValue)
490560
builder.SetUnlockType(UnlockType.Value);
491561

492-
if (ConnectionIDs != null && ConnectionIDs.Count > 0)
562+
if (_connectionIds != null && _connectionIds.Count > 0)
493563
{
494-
builder.WithConnectionsById(ConnectionIDs);
564+
builder.WithConnectionsById(_connectionIds);
495565
}
496566

497567
builder.ApplyTo(relationData, npc, preserveUnlockState);
@@ -732,11 +802,46 @@ private void EnsureAppearanceDefaults()
732802
#endif
733803
private bool TryGetRegistryData(out IdentityData data)
734804
{
735-
string prefabName = gameObject.name;
805+
string? prefabName = PrefabName;
806+
if (string.IsNullOrEmpty(prefabName))
807+
prefabName = gameObject.name;
808+
809+
if (string.IsNullOrEmpty(prefabName))
810+
{
811+
data = default;
812+
return false;
813+
}
814+
736815
if (prefabName.EndsWith("(Clone)"))
737816
prefabName = prefabName.Substring(0, prefabName.Length - 7);
738817

739-
return _registry.TryGetValue(prefabName, out data);
818+
if (_registry.TryGetValue(prefabName, out data))
819+
return true;
820+
821+
try
822+
{
823+
var npc = GetComponent<S1NPCs.NPC>();
824+
if (npc != null && !string.IsNullOrEmpty(npc.ID))
825+
{
826+
foreach (var kvp in _registry)
827+
{
828+
var entry = kvp.Value;
829+
if (!string.IsNullOrEmpty(entry.Id) && string.Equals(entry.Id, npc.ID, StringComparison.OrdinalIgnoreCase))
830+
{
831+
data = entry;
832+
PrefabName = entry.PrefabName ?? kvp.Key;
833+
return true;
834+
}
835+
}
836+
}
837+
}
838+
catch
839+
{
840+
// ignored
841+
}
842+
843+
data = default;
844+
return false;
740845
}
741846

742847
/// <summary>

S1API/Internal/Patches/AvatarAccessoryPatches.cs

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,29 +53,39 @@ static void Postfix(S1AvatarFramework.Avatar __instance)
5353

5454
for (int i = 0; i < length; i++)
5555
{
56-
var accessory = ((Array)appliedAccessories).GetValue(i);
57-
if (accessory == null) continue;
58-
59-
// Get the AssetPath to check if this is a custom accessory
60-
// On IL2CPP, fields are exposed as properties, so use TryGetFieldOrProperty
61-
string? assetPath = ReflectionUtils.TryGetFieldOrProperty(accessory, "AssetPath") as string;
62-
if (string.IsNullOrEmpty(assetPath)) continue;
63-
64-
// Check if we have texture replacements for this accessory
65-
var textureReplacements = AccessoryFactory.GetTextureReplacements(assetPath);
66-
if (textureReplacements.Count == 0) continue;
67-
68-
// Get the GameObject - accessory is a Component (MonoBehaviour)
69-
// Cast to Component to access gameObject reliably on IL2CPP
70-
GameObject? accessoryObj = null;
71-
if (accessory is Component component)
56+
string? assetPath = null;
57+
58+
try
7259
{
73-
accessoryObj = component.gameObject;
74-
}
75-
if (accessoryObj == null) continue;
60+
var accessory = ((Array)appliedAccessories).GetValue(i);
61+
if (accessory == null) continue;
62+
63+
// Get the AssetPath to check if this is a custom accessory
64+
// On IL2CPP, fields are exposed as properties, so use TryGetFieldOrProperty
65+
assetPath = ReflectionUtils.TryGetFieldOrProperty(accessory, "AssetPath") as string;
66+
if (string.IsNullOrEmpty(assetPath)) continue;
67+
68+
// Check if we have texture replacements for this accessory
69+
var textureReplacements = AccessoryFactory.GetTextureReplacements(assetPath);
70+
if (textureReplacements == null || textureReplacements.Count == 0) continue;
71+
72+
// Get the GameObject - accessory is a Component (MonoBehaviour)
73+
// Cast to Component to access gameObject reliably on IL2CPP
74+
GameObject? accessoryObj = null;
75+
if (accessory is Component component)
76+
{
77+
accessoryObj = component.gameObject;
78+
}
7679

77-
// Apply custom textures to all renderers
78-
ApplyTexturesToAccessoryInstance(accessoryObj, textureReplacements);
80+
if (accessoryObj == null) continue;
81+
82+
// Apply custom textures to all renderers
83+
ApplyTexturesToAccessoryInstance(accessoryObj, textureReplacements);
84+
}
85+
catch (Exception ex)
86+
{
87+
Logger.Warning($"Error applying custom textures to accessory '{assetPath ?? "<unknown>"}': {ex.Message}");
88+
}
7989
}
8090
}
8191
catch (Exception ex)
@@ -88,7 +98,7 @@ private static void ApplyTexturesToAccessoryInstance(
8898
GameObject accessoryObj,
8999
Dictionary<string, Texture2D> textureReplacements)
90100
{
91-
if (accessoryObj == null || textureReplacements.Count == 0)
101+
if (accessoryObj == null || textureReplacements == null || textureReplacements.Count == 0)
92102
return;
93103

94104
// Apply texture to all renderers (including inactive ones)

0 commit comments

Comments
 (0)