Утилиты для патчей
Атрибут RLSetup
Так как RogueLibs обрабатывает всё кастомное как классы, вы можете забыть инициализировать новый класс в Awake
вашего плагина. Именно поэтому тут есть атрибут RLSetup
. Вы можете добавить его к статическому методу и инициализировать ваш кастомный класс там.
public class MyCustomItem : CustomItem
{
[RLSetup]
public static void Setup()
{
RogueLibs.CreateCustomItem<MyCustomItem>()
.WithName(new CustomNameInfo("Name"))
.WithDescription(new CustomNameInfo("Description"))
.WithSprite(Properties.Resources.Sprite)
.WithUnlock(new ItemUnlock());
RogueLibs.CreateCustomName("SomeName", "Dialogue", new CustomNameInfo("Text"));
}
}
Вам просто надо будет вызвать следующий метод в Awake
вашего плагина:
public void Awake()
{
RogueLibs.LoadFromAssembly();
/* ... */
}
Серьёзно, используйте его. Это также помогает с версионингом. Всё в одном месте.
RoguePatcher
RoguePatcher
- маленький вспомогательный класс, делающий написание патчей немного быстрее и проще. Если вам надо больше контроля (порядок патчей, приоритет и т.п.), тогда используйте оригинальные методы Harmony.
- RoguePatcher
- Harmony
RoguePatcher patcher = new RoguePatcher(this);
patcher.Postfix(typeof(StatusEffects), nameof(StatusEffects.hasStatusEffect));
patcher.Postfix(typeof(InvDatabase), nameof(InvDatabase.ChooseArmor), new Type[1] { typeof(string) });
Harmony harmony = new Harmony(pluginGUID);
MethodInfo original = AccessTools.Method(typeof(StatusEffects), nameof(StatusEffects.hasStatusEffect));
MethodInfo patch = AccessTools.Method(GetType(), nameof(MyPatchMethod));
harmony.Patch(original, new HarmonyMethod(patch));
original = AccessTools.Method(typeof(InvDatabase), nameof(InvDatabase.ChooseArmor), new Type[1] { typeof(string) });
patch = AccessTools.Method(GetType(), nameof(MyPatchMethod2));
harmony.Patch(original, new HarmonyMethod(patch));
Вместо указывания названий методов с помощью строк, указывайте их с помощью ключевого слова nameof
. Используйте строковые названия только если метод, который вы хотите пропатчить, не публичный.
Методы-патчи должны иметь следующее имя: <ЦелевойТип>_<ЦелевойМетод>
. В примере выше, RoguePatcher
будет искать методы-патчи с названиями StatusEffects_hasStatusEffect
и InvDatabase_ChooseArmor
в классе вашего плагина.
Вы можете изменить тип, в котором будут искаться методы-патчи. Укажите его в конструкторе или выставьте свойство между патчами:
- RoguePatcher
- Harmony
public class MyCoolPlugin : BaseUnityPlugin
{
public void Awake()
{
RoguePatcher patcher = new RoguePatcher(this, typeof(MyCoolPatches));
patcher.Postfix(typeof(StatusEffects), nameof(StatusEffects.hasStatusEffect));
patcher.TypeWithPatches = typeof(MyEvenCoolerPatches);
patcher.Postfix(typeof(InvDatabase), nameof(InvDatabase.ChooseArmor), new Type[1] { typeof(string) });
}
}
public class MyCoolPatches
{
public static void StatusEffects_hasStatusEffect(StatusEffects __instance)
{
/* ... */
}
}
public class MyEvenCoolerPatches
{
public static void InvDatabase_ChooseArmor(InvDatabase __instance, string previousArmorName)
{
/* ... */
}
}
public class MyCoolPlugin : BaseUnityPlugin
{
public void Awake()
{
Harmony harmony = new Harmony(pluginGUID);
MethodInfo original = AccessTools.Method(typeof(StatusEffects), nameof(StatusEffects.hasStatusEffect));
MethodInfo patch = AccessTools.Method(typeof(MyCoolPatches), nameof(MyPatchMethod));
harmony.Patch(original, new HarmonyMethod(patch));
original = AccessTools.Method(typeof(InvDatabase), nameof(InvDatabase.ChooseArmor), new Type[1] { typeof(string) });
patch = AccessTools.Method(typeof(MyEvenCoolerPatches), nameof(MyPatchMethod2));
harmony.Patch(original, new HarmonyMethod(patch));
}
}
public class MyCoolPatches
{
public static void MyPatchMethod(StatusEffects __instance)
{
/* ... */
}
}
public class MyEvenCoolerPatches
{
public static void MyPatchMethod2(InvDatabase __instance, string previousArmorName)
{
/* ... */
}
}
Вспомогательные методы для транспилирования
Транспиляторы довольно сложные.
Вот пример из RogueLibs:
- Вспомогательные методы
- Harmony
public static IEnumerable<CodeInstruction> StatusEffects_AddStatusEffect(IEnumerable<CodeInstruction> codeEnumerable)
=> codeEnumerable.AddRegionAfter(
new Func<CodeInstruction, bool>[]
{
i => i.IsLdloc(),
i => i.opcode == OpCodes.Ldarg_3,
i => i.opcode == OpCodes.Stfld && i.StoresField(causingAgentField),
},
new Func<CodeInstruction[], CodeInstruction>[]
{
a => a[0],
_ => new CodeInstruction(OpCodes.Ldarg_0),
_ => new CodeInstruction(OpCodes.Call, typeof(RogueLibsPlugin).GetMethod(nameof(SetupEffectHook))),
});
private static readonly FieldInfo causingAgentField = typeof(StatusEffect).GetField(nameof(StatusEffect.causingAgent));
public static IEnumerable<CodeInstruction> StatusEffects_AddStatusEffect(IEnumerable<CodeInstruction> code)
{
bool searching = true;
int current = 0;
CodeInstruction[] matches = new CodeInstruction[after.Length];
foreach (CodeInstruction instr in code)
{
yield return instr;
if (searching)
{
if (current is 0 ? instr.IsLdloc()
: current is 1 ? instr.opcode == OpCodes.Ldarg_3
: instr.opcode == OpCodes.Stfld && instr.StoresField(causingAgentField))
{
matches[current] = instr;
if (++current is 3)
{
searching = false;
yield return matches[0];
yield return new CodeInstruction(OpCodes.Ldarg_0);
yield return new CodeInstruction(OpCodes.Call, typeof(RogueLibsPlugin).GetMethod(nameof(SetupEffectHook)));
}
}
else current = 0;
}
}
}
private static readonly FieldInfo causingAgentField = typeof(StatusEffect).GetField(nameof(StatusEffect.causingAgent));
Да, выглядит просто. Но это только потому что это очень простой пример.
При написании предикатов, помните, что они могут быть вызваны сотни или тысячи раз. Например, вы мож ете заранее вычислить значение FieldInfo
, используемое вашим предикатом, просто положите его в статическое поле, как в примере выше.
Такие тяжёлые вычисления могут стоить вам сотен миллисекунд времени запуска (или даже целые секунды, если вы работаете над крупным проектом).
Вот ещё один пример из RogueLibs:
- Вспомогательные методы
- Harmony
public static IEnumerable<CodeInstruction> Unlocks_LoadInitialUnlocks(IEnumerable<CodeInstruction> codeEnumerable)
=> codeEnumerable.ReplaceRegion(
new Func<CodeInstruction, bool>[]
{
i => i.opcode == OpCodes.Callvirt && i.Calls(List_Unlock_GetEnumerator),
i => i.IsStloc(),
},
new Func<CodeInstruction, bool>[]
{
i => i.opcode == OpCodes.Callvirt,
i => i.opcode == OpCodes.Endfinally,
i => i.opcode == OpCodes.Ldarg_0,
},
new CodeInstruction[]
{
new CodeInstruction(OpCodes.Pop),
new CodeInstruction(OpCodes.Pop),
new CodeInstruction(OpCodes.Call, typeof(RogueLibsPlugin).GetMethod(nameof(LoadUnlockWrappersAndCategorize))),
});
private static readonly MethodInfo List_Unlock_GetEnumerator = typeof(List<Unlock>).GetMethod("GetEnumerator");
public static IEnumerable<CodeInstruction> Unlocks_LoadInitialUnlocks(IEnumerable<CodeInstruction> code)
{
int state = 0;
int current = 0;
CodeInstruction[] beginCache = new CodeInstruction[2];
foreach (CodeInstruction instr in code)
{
if (state is 2)
yield return instr;
else if (state is 0)
{
if (current is 0 ? instr.opcode == OpCodes.Callvirt && instr.Calls(List_Unlock_GetEnumerator)
: instr.IsStloc())
{
beginCache[current] = instr;
if (++current == 2)
{
state = 1;
current = 0;
}
}
else
{
if (current > 0)
{
for (int i = 0; i < current; i++)
yield return beginCache[i];
current = 0;
}
yield return instr;
}
}
else
{
if (current is 0 ? instr.opcode == OpCodes.Callvirt
: current is 1 ? instr.opcode == OpCodes.Endfinally
: instr.opcode == OpCodes.Ldarg_0)
{
if (++current == 3)
{
yield return new CodeInstruction(OpCodes.Pop);
yield return new CodeInstruction(OpCodes.Pop);
yield return new CodeInstruction(OpCodes.Call, typeof(RogueLibsPlugin).GetMethod(nameof(LoadUnlockWrappersAndCategorize)));
}
}
else current = 0;
}
}
}
private static readonly MethodInfo List_Unlock_GetEnumerator = typeof(List<Unlock>).GetMethod("GetEnumerator");
Всё ещё относительно просто. Я просто не хочу тратить своё время на написание действительно сложного примера.