Skip to content

Commit 70eb35e

Browse files
authored
Merge pull request KaBooMa#56 from JumbleBumble/ProductEffects
2 parents c2f34a4 + 9bc78b3 commit 70eb35e

File tree

3 files changed

+545
-0
lines changed

3 files changed

+545
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
#if (IL2CPPMELON)
2+
using S1PlayerScripts = Il2CppScheduleOne.PlayerScripts;
3+
using S1NPCs = Il2CppScheduleOne.NPCs;
4+
using S1Product = Il2CppScheduleOne.Product;
5+
using S1Properties = Il2CppScheduleOne.Effects;
6+
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
7+
using S1PlayerScripts = ScheduleOne.PlayerScripts;
8+
using S1NPCs = ScheduleOne.NPCs;
9+
using S1Product = ScheduleOne.Product;
10+
using S1Properties = ScheduleOne.Effects;
11+
#endif
12+
using System;
13+
using System.Collections.Generic;
14+
using System.Collections;
15+
using System.Linq;
16+
using System.Reflection;
17+
using HarmonyLib;
18+
using S1API.Entities;
19+
using S1API.Logging;
20+
using S1API.Properties;
21+
using S1API.Products;
22+
23+
namespace S1API.Internal.Patches
24+
{
25+
/// <summary>
26+
/// INTERNAL: Intercepts product instance effect application and routes through registered callbacks.
27+
/// </summary>
28+
[HarmonyPatch]
29+
internal static class ProductEffectPatches
30+
{
31+
private static readonly Log Logger = new Log("ProductEffectPatches");
32+
private static Dictionary<string, Type>? _npcWrapperTypeById;
33+
34+
/// <summary>
35+
/// Targets all concrete product instance ApplyEffectsToPlayer and ApplyEffectsToNPC implementations.
36+
/// </summary>
37+
private static IEnumerable<MethodBase> TargetMethods()
38+
{
39+
var playerType = typeof(S1PlayerScripts.Player);
40+
var npcType = typeof(S1NPCs.NPC);
41+
42+
return typeof(S1Product.ProductItemInstance).Assembly
43+
.GetTypes()
44+
.Where(type => typeof(S1Product.ProductItemInstance).IsAssignableFrom(type))
45+
.SelectMany(type => new[]
46+
{
47+
type.GetMethod(
48+
"ApplyEffectsToPlayer",
49+
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
50+
null,
51+
new[] { playerType },
52+
null),
53+
type.GetMethod(
54+
"ApplyEffectsToNPC",
55+
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
56+
null,
57+
new[] { npcType },
58+
null)
59+
})
60+
.Where(method => method != null)
61+
.Cast<MethodBase>();
62+
}
63+
64+
/// <summary>
65+
/// Intercepts product effect application for players and NPCs and executes callbacks per effect ID.
66+
/// Unhandled effects fall back to base game behavior (effect.ApplyToPlayer).
67+
/// </summary>
68+
/// <param name="__instance">The product instance applying effects.</param>
69+
/// <param name="__0">The first argument (player or NPC receiving effects).</param>
70+
/// <returns><c>false</c> when handled here; <c>true</c> to run original method.</returns>
71+
[HarmonyPrefix]
72+
private static bool ApplyEffects_Prefix(S1Product.ProductItemInstance __instance, object __0)
73+
{
74+
var target = __0;
75+
76+
if (__instance == null || target == null)
77+
return true;
78+
79+
var effects = ResolveEffects(__instance);
80+
if (effects == null)
81+
return true;
82+
83+
var localPlayer = Player.All.FirstOrDefault(p => p.IsLocal);
84+
var targetPlayer = target as S1PlayerScripts.Player;
85+
var targetNpc = target as S1NPCs.NPC;
86+
87+
if (targetPlayer == null && targetNpc == null)
88+
return true;
89+
90+
if (targetPlayer != null && (localPlayer == null || targetPlayer != localPlayer.S1Player))
91+
return true;
92+
93+
var invokedEffectIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
94+
95+
for (var i = 0; i < effects.Count; i++)
96+
{
97+
var effect = effects[i];
98+
if (effect == null)
99+
continue;
100+
101+
var effectId = effect.ID;
102+
if (string.IsNullOrWhiteSpace(effectId) || !invokedEffectIds.Add(effectId))
103+
continue;
104+
105+
try
106+
{
107+
if (targetPlayer != null)
108+
{
109+
var handled = ProductManager.TryInvokeEffectCallback(effectId, localPlayer!, out var allowDefaultEffect);
110+
if (!handled || allowDefaultEffect)
111+
effect.ApplyToPlayer(targetPlayer);
112+
}
113+
else
114+
{
115+
var apiNpc = ResolveApiNpc(targetNpc);
116+
117+
var allowDefaultEffect = false;
118+
var handled = apiNpc != null && ProductManager.TryInvokeNpcEffectCallback(effectId, apiNpc, out allowDefaultEffect);
119+
if (!handled || allowDefaultEffect)
120+
effect.ApplyToNPC(targetNpc);
121+
}
122+
}
123+
catch (Exception ex)
124+
{
125+
Logger.Error($"Exception while invoking effect callback for '{effectId}': {ex.Message}");
126+
Logger.Error(ex.StackTrace ?? string.Empty);
127+
}
128+
}
129+
130+
return false;
131+
}
132+
133+
private static List<S1Properties.Effect>? ResolveEffects(S1Product.ProductItemInstance productInstance)
134+
{
135+
try
136+
{
137+
var wrappedInstance = new ProductInstance(productInstance);
138+
var fromWrapper = wrappedInstance.Properties
139+
.OfType<ProductPropertyWrapper>()
140+
.Select(property => property.InnerProperty)
141+
.Where(effect => effect != null)
142+
.ToList();
143+
144+
if (fromWrapper.Count > 0)
145+
return fromWrapper;
146+
}
147+
catch (Exception ex)
148+
{
149+
Logger.Error($"ResolveEffects wrapper path failed: {ex.Message}");
150+
}
151+
152+
var fromInstance = ExtractEffectsFromObject(TryGetPropertyValue(productInstance, "Properties"));
153+
if (fromInstance != null && fromInstance.Count > 0)
154+
return fromInstance;
155+
156+
var definition = productInstance.Definition;
157+
var fromDefinition = ExtractEffectsFromObject(TryGetPropertyValue(definition, "Properties"));
158+
if (fromDefinition != null && fromDefinition.Count > 0)
159+
return fromDefinition;
160+
161+
return null;
162+
}
163+
164+
private static object? TryGetPropertyValue(object? instance, string propertyName)
165+
{
166+
if (instance == null)
167+
return null;
168+
169+
var property = instance.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
170+
return property?.GetValue(instance);
171+
}
172+
173+
private static List<S1Properties.Effect>? ExtractEffectsFromObject(object? propertiesObject)
174+
{
175+
if (propertiesObject == null)
176+
return null;
177+
178+
var results = new List<S1Properties.Effect>();
179+
180+
if (propertiesObject is IEnumerable enumerable)
181+
{
182+
foreach (var entry in enumerable)
183+
{
184+
if (entry is S1Properties.Effect effect)
185+
results.Add(effect);
186+
}
187+
}
188+
189+
return results;
190+
}
191+
192+
private static NPC? ResolveApiNpc(S1NPCs.NPC? targetNpc)
193+
{
194+
if (targetNpc == null)
195+
return null;
196+
197+
var byRefOrId = NPC.All.FirstOrDefault(npc =>
198+
npc?.S1NPC == targetNpc ||
199+
(!string.IsNullOrWhiteSpace(npc?.S1NPC?.ID) &&
200+
!string.IsNullOrWhiteSpace(targetNpc.ID) &&
201+
string.Equals(npc.S1NPC.ID, targetNpc.ID, StringComparison.OrdinalIgnoreCase)));
202+
203+
if (byRefOrId != null)
204+
return byRefOrId;
205+
206+
if (string.IsNullOrWhiteSpace(targetNpc.ID))
207+
return null;
208+
209+
try
210+
{
211+
var wrapperTypeById = GetNpcWrapperTypeById();
212+
if (!wrapperTypeById.TryGetValue(targetNpc.ID, out var wrapperType))
213+
return null;
214+
215+
var created = Activator.CreateInstance(wrapperType, nonPublic: true) as NPC;
216+
if (created?.S1NPC == targetNpc)
217+
return created;
218+
219+
return NPC.All.FirstOrDefault(npc => npc?.S1NPC == targetNpc);
220+
}
221+
catch (Exception ex)
222+
{
223+
Logger.Error($"NPC intercept: wrapper creation failed for '{targetNpc.ID}': {ex.Message}");
224+
return null;
225+
}
226+
}
227+
228+
private static Dictionary<string, Type> GetNpcWrapperTypeById()
229+
{
230+
if (_npcWrapperTypeById != null)
231+
return _npcWrapperTypeById;
232+
233+
var map = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
234+
var npcBaseType = typeof(NPC);
235+
236+
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
237+
{
238+
Type[] types;
239+
try
240+
{
241+
types = assembly.GetTypes();
242+
}
243+
catch
244+
{
245+
continue;
246+
}
247+
248+
foreach (var type in types)
249+
{
250+
if (type == null || type.IsAbstract || !npcBaseType.IsAssignableFrom(type))
251+
continue;
252+
253+
var npcIdProperty = type.GetProperty("NPCId", BindingFlags.Public | BindingFlags.Static);
254+
if (npcIdProperty == null || npcIdProperty.PropertyType != typeof(string))
255+
continue;
256+
257+
var npcId = npcIdProperty.GetValue(null) as string;
258+
if (string.IsNullOrWhiteSpace(npcId))
259+
continue;
260+
261+
if (!map.ContainsKey(npcId))
262+
map[npcId] = type;
263+
}
264+
}
265+
266+
_npcWrapperTypeById = map;
267+
return map;
268+
}
269+
}
270+
}

0 commit comments

Comments
 (0)