Harmony Patching Transpilers - Identifying a Switch Statement and it's cases

I am having issues accessing the cases of a switch statement and adding to them with a Harmony Transpiler. I was thinking that their may be specific op codes that pertain to cases but it seems that in IL, the operand of the switch stores the indexs of the case statements. Is there a way to, using the operand of the switch statement, figure out what case number is what and also be able to add a default case?
99 Replies
333fred
333fred2mo ago
I expect the answer is no in general: while there may be a way to do this when you specifically have a switch IL op code, there is no guarantee that a switch in C# will get compiled to a switch in IL
Frozen Breadstick
Frozen BreadstickOP2mo ago
What if i know that there is a switch in the IL just theoretically
Frozen Breadstick
Frozen BreadstickOP2mo ago
No description
Frozen Breadstick
Frozen BreadstickOP2mo ago
I know that this will be present in the compiled version
333fred
333fred2mo ago
You'll probably want to look at what other decompilers do, like ilspy, and see what you can crib from them
Frozen Breadstick
Frozen BreadstickOP2mo ago
DNSpy is not reliable in this case?
333fred
333fred2mo ago
It's not open source so you can't look at it? Oh, wait, I'm thinking of dotpeek Yeah, it would probably be fine, though it is archived
Frozen Breadstick
Frozen BreadstickOP2mo ago
I am using DNSpy to see the code It has obfuscated names but that is it
Frozen Breadstick
Frozen BreadstickOP2mo ago
No description
333fred
333fred2mo ago
Ok, this is when I ask: what are you decompiling
Frozen Breadstick
Frozen BreadstickOP2mo ago
A C# executable
333fred
333fred2mo ago
We both know that is not what I'm asking
Frozen Breadstick
Frozen BreadstickOP2mo ago
It is a game from years ago A game called SpeedRunners Uses the FNA/XNA microsoft framework .Net 4.7.2 I am sorry I am not 100% sure what else you could be asking
333fred
333fred2mo ago
What I'm generally asking is if you're developing cheats for use in an online game against the terms of that game
Frozen Breadstick
Frozen BreadstickOP2mo ago
No, I am developping a modding framework I have direct contact with and permission from the developer This game is like 12 years old We have a small community around it and myself and a small team are designing a framework The item system is super super messed up, so we have to resort to Transpilers to get it to work, but I am currently creating a system for adding custom items
333fred
333fred2mo ago
I see, ok Just making sure we aren't violating server rules here, that's all
Frozen Breadstick
Frozen BreadstickOP2mo ago
All good, I can understadn the concern
Aaron
Aaron2mo ago
let me copy this message out of chat then if you had this
switch (i)
{
case 1:
thing;
break;
case 2:
thing2;
break;
case 3:
thing3;
break;
default:
other;
break;
}
switch (i)
{
case 1:
thing;
break;
case 2:
thing2;
break;
case 3:
thing3;
break;
default:
other;
break;
}
the IL is
ldarg i
ldc.i4.1
sub // subtract 1 to make it start at 0
switch (case1Label, case2Label, case3Label)

// default case is here, its when you get when the value is > N, it falls through to here
// other
br end

case1Label:
// thing1
br end

case2Label:
// thing2
br end

case3Label:
// thing3
br end

end:
ldarg i
ldc.i4.1
sub // subtract 1 to make it start at 0
switch (case1Label, case2Label, case3Label)

// default case is here, its when you get when the value is > N, it falls through to here
// other
br end

case1Label:
// thing1
br end

case2Label:
// thing2
br end

case3Label:
// thing3
br end

end:
Frozen Breadstick
Frozen BreadstickOP2mo ago
Yeah I saw this But i see now labels in IL
Aaron
Aaron2mo ago
labels arent really an IL thing IL only uses offsets
Frozen Breadstick
Frozen BreadstickOP2mo ago
The Operand of the Switch IL just contains the index and offset Ah right
Aaron
Aaron2mo ago
but it is convenient shorthand supported by ilasm
Frozen Breadstick
Frozen BreadstickOP2mo ago
So should I be using the offset extracted from the operand to try and identify where cases are
Aaron
Aaron2mo ago
Cecil will be giving you them as Instruction objects, iirc which you can check the index of (if thats what youre using)
Frozen Breadstick
Frozen BreadstickOP2mo ago
I use DNSpy
333fred
333fred2mo ago
They're using Harmony Whatever that exposes
Frozen Breadstick
Frozen BreadstickOP2mo ago
Oop[s yeah harmony mb I forgot what Cecil is im a bit tired sorry lol Transpilers return an IEnumerable of Opcodes
C#
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
var codes = new List<CodeInstruction>(instructions);

var fileLogMethod = AccessTools.Method(typeof(System.IO.File), nameof(System.IO.File.AppendAllText), new[] { typeof(string), typeof(string) });

codes.Insert(321, new CodeInstruction(OpCodes.Ldstr, @"C:\temp\harmony_log.txt")); //I couldn't get console logging to work, file logging instead.
codes.Insert(322, new CodeInstruction(OpCodes.Ldstr, "Case 1\n"));
codes.Insert(323, new CodeInstruction(OpCodes.Call, fileLogMethod));

return codes;
}
C#
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
var codes = new List<CodeInstruction>(instructions);

var fileLogMethod = AccessTools.Method(typeof(System.IO.File), nameof(System.IO.File.AppendAllText), new[] { typeof(string), typeof(string) });

codes.Insert(321, new CodeInstruction(OpCodes.Ldstr, @"C:\temp\harmony_log.txt")); //I couldn't get console logging to work, file logging instead.
codes.Insert(322, new CodeInstruction(OpCodes.Ldstr, "Case 1\n"));
codes.Insert(323, new CodeInstruction(OpCodes.Call, fileLogMethod));

return codes;
}
Aaron
Aaron2mo ago
harmony gives them to you with their own wrapper over CodeInstruction
Frozen Breadstick
Frozen BreadstickOP2mo ago
Yes
Aaron
Aaron2mo ago
i think yeah they use Labels
Frozen Breadstick
Frozen BreadstickOP2mo ago
I see When i used labels it did not find anything
Aaron
Aaron2mo ago
https://harmony.pardeike.net/articles/patching-transpiler-codes.html they turn all offsets into labels and back for you labels just being a wrapper over the instruction theyre targeting, so you can swap out the target easily
Frozen Breadstick
Frozen BreadstickOP2mo ago
Ah i see, so the label for an offset would match "1" as the case?
Aaron
Aaron2mo ago
mmm this would just give you a Label[] when you look at the switch
Frozen Breadstick
Frozen BreadstickOP2mo ago
The easiest method for me here was that I was just going to define a default case for the switch (As one doesnt exist and will never exist)
Aaron
Aaron2mo ago
then just find the switch opcode and put stuff before the br that comes right after it
Frozen Breadstick
Frozen BreadstickOP2mo ago
And then put my own custom switch statement in there that is generated based off the amoutn of custom items being added I see i see
Aaron
Aaron2mo ago
between these two
No description
Frozen Breadstick
Frozen BreadstickOP2mo ago
Right, and I wouldn't have to edit the operand?
Aaron
Aaron2mo ago
yes any time its given a number that isnt in the range it just goes to the next instruction
Frozen Breadstick
Frozen BreadstickOP2mo ago
Gotcha gotcha, that makes sense Ah soooo then I can add my own switch statement as the default case for switching modded items
Aaron
Aaron2mo ago
i would recommend just immediately calling a helper method, if possible instead of writing a bunch of IL
Frozen Breadstick
Frozen BreadstickOP2mo ago
Yeah that was my plan I was going to insert a hook here to run a helper
Aaron
Aaron2mo ago
also damn if youre in contact with these developers try to get something that isnt obfuscated :kekw:
Frozen Breadstick
Frozen BreadstickOP2mo ago
I did ask about this but he is hesitant to give away the code Which is fair enough He provides us snippets when we are stuck but being obfuscated isnt the WORST thing in the world, we have made pretty significant attempts to rename plenty of variables and methods so that we can understand what is going on let me give it a go! Then i will resolve the issue thank you guys so much for your help! @Aaron It worked THANK YOU SO MUCH OpCodes are a bit new to me
Aaron
Aaron2mo ago
:c0_catboysalute:
Frozen Breadstick
Frozen BreadstickOP2mo ago
I am a robotics engineer who works primarily in C++ so this has been a learnign experience
Aaron
Aaron2mo ago
the harmony discord is a fairly active place https://discord.gg/xXgghXR
Frozen Breadstick
Frozen BreadstickOP2mo ago
I COULD NOT find a harmony discord thank you so much
Aaron
Aaron2mo ago
theres a bunch of others for C# modding depending on the area, but most of the ones i could link right now are unity related, or at a lower level than Harmony
Frozen Breadstick
Frozen BreadstickOP2mo ago
Yeah this game isnt unity So good if there is just a generic one @cat I was wondering if you could assist further, I have built a decentish framework around adding custom items But I have a question regarding accessing the variable that is switched on The harmony server has had no active responses lol I need to essentially access and set the value of the variable that is switched on, but I am unsure how to extract that from the opcodes
Frozen Breadstick
Frozen BreadstickOP2mo ago
No description
Frozen Breadstick
Frozen BreadstickOP2mo ago
I see the variable is loaded above
Aaron
Aaron2mo ago
yeah it's just in local 1 the sub is just to make the switch work, since it's 0 based, and they have no 0 case
Frozen Breadstick
Frozen BreadstickOP2mo ago
oh ok ohhhhhh Shit ok yeah that makes sense so i can actually just acess it as local 1
Aaron
Aaron2mo ago
yeah you can just ldloc.1 before your call or, if you also want to change it in the method, ldloca to pass it by ref
Frozen Breadstick
Frozen BreadstickOP2mo ago
Ah ok ok ok @cat The reference for writing doesnt seem to work Forgive me if I have done this wrong but: `
C#
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
var codes = new List<CodeInstruction>(instructions);

var fileLogMethod = AccessTools.Method(typeof(System.IO.File), nameof(System.IO.File.AppendAllText), new[] { typeof(string), typeof(string) });

var broadcastMethod = AccessTools.Method(typeof(PlayerControlsHandlerPatch), nameof(PlayerControlsHandlerPatch.BroadcastModdedItemUsed));

int index = 0;

for(int i = 0; i < codes.Count; i++)
{
if (codes[i].opcode == OpCodes.Switch)
{
index = i; break; //Find the switch opcode
}
}

codes.Insert(index + 1, new CodeInstruction(OpCodes.Ldloca_S, 1));
codes.Insert(index + 2, new CodeInstruction(OpCodes.Call, broadcastMethod));

return codes;
}

public static void BroadcastModdedItemUsed(ref byte ID)
{
if (ID == 0) return;
Console.WriteLine("Default Case");
GetService<IItemService>()?.UseModdedItem();
GetService<ModdingAPI>()?.Notify(new ModdedItemUsedEventArgs());
ID = 1;
}
C#
static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
var codes = new List<CodeInstruction>(instructions);

var fileLogMethod = AccessTools.Method(typeof(System.IO.File), nameof(System.IO.File.AppendAllText), new[] { typeof(string), typeof(string) });

var broadcastMethod = AccessTools.Method(typeof(PlayerControlsHandlerPatch), nameof(PlayerControlsHandlerPatch.BroadcastModdedItemUsed));

int index = 0;

for(int i = 0; i < codes.Count; i++)
{
if (codes[i].opcode == OpCodes.Switch)
{
index = i; break; //Find the switch opcode
}
}

codes.Insert(index + 1, new CodeInstruction(OpCodes.Ldloca_S, 1));
codes.Insert(index + 2, new CodeInstruction(OpCodes.Call, broadcastMethod));

return codes;
}

public static void BroadcastModdedItemUsed(ref byte ID)
{
if (ID == 0) return;
Console.WriteLine("Default Case");
GetService<IItemService>()?.UseModdedItem();
GetService<ModdingAPI>()?.Notify(new ModdedItemUsedEventArgs());
ID = 1;
}
Aaron
Aaron2mo ago
uhhh is anything actually using local 1 after that point
Frozen Breadstick
Frozen BreadstickOP2mo ago
After which point I was confused because i thought sub wouldve popped the stack i.e: i wouldve needed to store the variable again @333fred Do you know anything about this? Harmony opcode insertion mb, i am confusing things together I thought using the Call opcode popped anything loaded onto the stack as arguments
Aaron
Aaron2mo ago
it does I mean is local 1 actually used after this point by anything in the method
Frozen Breadstick
Frozen BreadstickOP2mo ago
well BroadcastModdedItemUsed is the method referenced by broadcastMethod I thought adding the "ref byte ID" would allow me to manipulate local 1
Aaron
Aaron2mo ago
it does but if nothing reads local 1, that doesn't matter
Frozen Breadstick
Frozen BreadstickOP2mo ago
of course, because it is an address ahhhhh C#
Aaron
Aaron2mo ago
so does anything use local 1 after you change it
Frozen Breadstick
Frozen BreadstickOP2mo ago
Well i guess because it is loaded into local storage not really
Aaron
Aaron2mo ago
or do they all load the field again
Frozen Breadstick
Frozen BreadstickOP2mo ago
The field is loaded again This is why ah yes Ok so i need to actually modify the global variable value not the local copy in the method at this stage
Aaron
Aaron2mo ago
seems so
Frozen Breadstick
Frozen BreadstickOP2mo ago
Do you knpow how I owuld go about doing that
Aaron
Aaron2mo ago
should just be able to ldloc.0 and write to the same field normally assuming you are referencing this DLL, anyway
Frozen Breadstick
Frozen BreadstickOP2mo ago
the value I am accessing is from the game .exe file the one i load the address of
Aaron
Aaron2mo ago
yeah, are you referencing the exe
Frozen Breadstick
Frozen BreadstickOP2mo ago
I assume so? Given that everything works correctly
Aaron
Aaron2mo ago
then yeah you can just ldloc.0 and take that class as the parameter instead and write to the field in your method
Frozen Breadstick
Frozen BreadstickOP2mo ago
Ah ok, i will see if i can figure it out, should be straight forward enough Do i need to know the name of the field for this
Aaron
Aaron2mo ago
yes
Frozen Breadstick
Frozen BreadstickOP2mo ago
shit ok
Aaron
Aaron2mo ago
I mean if you really have to, you can have the method return the new value and do the write in IL with stfld instead
Frozen Breadstick
Frozen BreadstickOP2mo ago
Literally all of our other code manages to be refelction based, so im gonna have to find a work around We don't use any obfuscated names anywhere currently i can write to the global field with stfld? I need value persistence between method calls that's the issue
Aaron
Aaron2mo ago
it's a field on a class, no?
Frozen Breadstick
Frozen BreadstickOP2mo ago
yes
Aaron
Aaron2mo ago
then yeah, you write to it with stfld if it's an instance field, you have to push the instance you want to write to before the new field value
Frozen Breadstick
Frozen BreadstickOP2mo ago
so my method will return a value and i can use stfld on stloc.1 with the returned value? and that means i dont need to know the name Apologies if I am getting this wrong It's just a "protected byte"
Aaron
Aaron2mo ago
you would do
ldarg.0 // the class instance to store into
ldloc.1
call uint8 YourHelper(uint8)
stfld theField
ldarg.0 // the class instance to store into
ldloc.1
call uint8 YourHelper(uint8)
stfld theField
Frozen Breadstick
Frozen BreadstickOP2mo ago
oh so i still need the obfsucated field name
Aaron
Aaron2mo ago
I mean you could get the field from the earlier ldfld instead of using the name directly
Frozen Breadstick
Frozen BreadstickOP2mo ago
that would be what i need to do i reckon
Aaron
Aaron2mo ago
but I would be surprised if never using any obfuscated name ever is maintainable in the long run
Frozen Breadstick
Frozen BreadstickOP2mo ago
We have been very successful thus far quite a good portion of the game has been unpacked and proxied without obfuscated names Just a lot of IL pattern searching Ok so do i need to add instructions after the ldfld from earlier to allow this value to be accessed later on to do it without the obfuscated name
Aaron
Aaron2mo ago
no, you can just steal the operand is all I meant
Frozen Breadstick
Frozen BreadstickOP2mo ago
I am not following, sorry, if this is too many questions its ok if you wanna dip lol
Aaron
Aaron2mo ago
so, you have the list of instructions
Frozen Breadstick
Frozen BreadstickOP2mo ago
Yes
Aaron
Aaron2mo ago
you know where the ldfld is for the field you want to set
Frozen Breadstick
Frozen BreadstickOP2mo ago
yes is that maintained after stloc.1
Aaron
Aaron2mo ago
you can take that instruction's Operand and use that as the operand for an stfld, to tell it to store to the same field
Frozen Breadstick
Frozen BreadstickOP2mo ago
OHHH OF COURSE YES right i misunderstood the way you said the message before Yes yes yes, ok thank you very much
C#
int index = 0;
int fldIndex = 0;

for(int i = 0; i < codes.Count; i++)
{
if (codes[i].opcode == OpCodes.Switch)
{
index = i;
for(int j = index; j > 1; j--)
{
if (codes[j].opcode == OpCodes.Ldfld)
{
fldIndex = j;
break;
}
}
break; //Find the switch opcode
}
}

var fldOperand = codes[fldIndex].operand;

codes.Insert(index + 1, new CodeInstruction(OpCodes.Ldloc_0));
codes.Insert(index + 2, new CodeInstruction(OpCodes.Ldloca_S, (byte)1));
codes.Insert(index + 3, new CodeInstruction(OpCodes.Call, broadcastMethod));
codes.Insert(index + 4, new CodeInstruction(OpCodes.Stfld, fldOperand));

return codes;
}

public static byte BroadcastModdedItemUsed(ref byte ID)
{
if (ID == 0) return 0;
Console.WriteLine("Default Case");
GetService<IItemService>()?.UseModdedItem();
GetService<ModdingAPI>()?.Notify(new ModdedItemUsedEventArgs());
return 1;
}
C#
int index = 0;
int fldIndex = 0;

for(int i = 0; i < codes.Count; i++)
{
if (codes[i].opcode == OpCodes.Switch)
{
index = i;
for(int j = index; j > 1; j--)
{
if (codes[j].opcode == OpCodes.Ldfld)
{
fldIndex = j;
break;
}
}
break; //Find the switch opcode
}
}

var fldOperand = codes[fldIndex].operand;

codes.Insert(index + 1, new CodeInstruction(OpCodes.Ldloc_0));
codes.Insert(index + 2, new CodeInstruction(OpCodes.Ldloca_S, (byte)1));
codes.Insert(index + 3, new CodeInstruction(OpCodes.Call, broadcastMethod));
codes.Insert(index + 4, new CodeInstruction(OpCodes.Stfld, fldOperand));

return codes;
}

public static byte BroadcastModdedItemUsed(ref byte ID)
{
if (ID == 0) return 0;
Console.WriteLine("Default Case");
GetService<IItemService>()?.UseModdedItem();
GetService<ModdingAPI>()?.Notify(new ModdedItemUsedEventArgs());
return 1;
}
I think i am doing something wrong
Aaron
Aaron2mo ago
you need an ldarg_0 before the ldloc you're writing to an instance field, so you need an instance (you also don't really need to be using ldloca/ref anymore)
Frozen Breadstick
Frozen BreadstickOP2mo ago
easy as ill try this in a few hours once i get back to my ocmputer Just tested, it works, thank you, i love you, you are amazing!

Did you find this page helpful?