Streets of Rogue

Streets of Rogue

Not enough ratings
The Beginner's Addendum for Abbysssal's BepInEx Modding Guide
By Ted Bunny
Something like a FAQ.
   
Award
Favorite
Favorited
Unfavorite
Introduction
This is for beginners who tried out Abbysssal's modding guide and have more questions. He answered a lot of questions for me, so I'm hoping to put the information to good use for the community (and to save him a bit of time).

I don't know if you could call these FAQs, since I have no idea whether they're frequently asked. But I asked them, so hopefully they help someone else out there.

If any of your questions aren't answered here, feel free to ask the Streets of Rogue Discord modding community[discord.gg]


Q: Why should I use Abbysssal's BepInEx guide rather than Angry Engineer's guide?

A: AngryEngineer's guide is good, but it shows how to modify the game files directly. That method is easier to code, but is prone to becoming outdated rather quickly, and does not allow compatibility with other mods. BepInEx modding allows multiple mods at the same time, and is more likely to work between game updates. Also, most of the community has migrated toward BepInEx modding, so you're more likely to find relevant advice if you stick to that.
Setup
Q: I can’t find the “Dependencies Manager” mentioned in the guide. (This refers to an outdated tip in one of the Github ReadMe’s, the Steam guide has the correct directions).

A: Visual Studio moves these things around pretty regularly. In the Solution Explorer, right click anywhere and select Add Reference…


Q: It’s not accepting the BepInDependencies I’ve added.

A: Ensure that using RogueLibsCore is in your file’s dependencies. That one’s not in his screenshots, but will be necessary. Set your version for this to 2.0:
[BepInDependency(RogueLibs.pluginGuid, "2.0")]


Q: I need a DLL! Or some other reference.

A: Abbyssal uploaded them here[drive.google.com].
Modding
For the purpose of these questions, my example will refer to my project of copying a "Use Wrench to Detonate" behavior from the Generator class ("Model Class") to the Stove class (“Target Class”). Stove inherits from ObjectReal, which in turn inherits from PlayfieldObject (Our Base Classes).


Q: How do I know which Class to target for a patch?

A: Generally, in order of preference:
  1. Write a new non-patch method. This means putting this method into your mod without the need to patch it (because it doesn’t replace or add to an existing method), and calling it when needed. This is only appropriate when this method would only be called from your mod. And yes, that means you technically can’t add this method to the target class – so this choice has limited uses.

    E.g., Add a UseWrenchToDetonate(Stove __instance) method for Stove to use, because it does not exist in its Base Classes, and it is only called from within the mod.

  2. If you wish to make narrow changes: prefer the highest-level class possible.

    E.g., Stove inherits from ObjectReal, which inherits from PlayfieldObject. Since all three contain a method for RecycleAwake(), I can patch into Stove. If I want to patch something that only exists in ObjectReal and PlayfieldObject, I need to patch ObjectReal.

  3. If you wish to make sweeping changes to how the game works: prefer the lowest-level class possible. I haven’t done this yet, but the lower you go, the wider the effects will be on the game.


Q: How am I supposed to patch my Target class through its Base class?

A: This is hacky, but remember my comment about choosing your battles? This is an example of a Prefix patch to ObjectReal that simply checks its object type, and operates accordingly:

public static bool ObjectReal_FinishedOperating(ObjectReal __instance) { if (__instance is Stove) { [Here goes my code for the Derived class, Stove] } [And here goes the code for the rest of the method, applying to our current class, ObjectReal] }


Q: When should I use a Prefix or a Postfix?

A: There's likely no right answer to that - it really depends on the code you're working on. But you can have both a Prefix and Postfix to the same method, fortunately.


Q: The Class I'm modding does not have certain variables I need it to have. However, I can't declare variables within a static method. What do?

A: Create a separate non-static class for this purpose. Instantiate that class when your Target class is instantiated, and link them together using a dict. Here's an example:

  1. We create a new Class that will be instantiated and linked to Stove whenever a Stove is created. This should just contain your new variables.
    public class VariablesStove { public PlayfieldObject countdownCauser; public bool mustSpawnExplosionOnClients; public bool noOwnCheckCountdown; public PlayfieldObject savedDamagerObject; }

  2. Next, let's declare the dictionary in our original mod's class:
    public static Dictionary<Stove, VariablesStove> VariablesStove = new Dictionary<Stove, VariablesStove>();

  3. Then we patch Stove to create an instance of the object above, and link them together in a dictionary. This means we can search the dictionary with the Stove as a key, and it will return the VariablesStove linked to it, which contains our variables. This is a Postfix to ObjectReal.Start(), since Stove lacks a method named Start().
    public static void ObjectReal_Start(ObjectReal __instance) { if (__instance is Stove stove) VariablesStove.Add(stove, new VariablesStove()); }

  4. Accessing these variables is simple. Instead of this, which would not be found:

    __instance.savedDamagerObject

    you'd use this instead:

    VariablesStove[__instance].savedDamagerObject

  5. Last, we create FixedUpdate() to ensure that the dictionary is cleared of any destroyed Stoves and its attendant VariablesStoves objects. This will be run automatically by Unity, so just place it in your mod's main class without need to add it to your patch list.
    public void FixedUpdate() { List<Stove> removal = new List<Stove>(); foreach (KeyValuePair<Stove, VariablesStove> pair in VariablesStove) if (pair.Key.isBroken()) removal.Add(pair.Key); foreach (Stove stove in removal) VariablesStove.Remove(stove); }


Q: The variable I need to access exists in my Target Class's base method, but it's protected so I can't access it.

A: In your patch method add ref <Type> ___[variableName]. So instead of this, which causes an access violation:
Stove_FunMethod(bool isFun, Stove __instance) { bool myVar = __instance.variableName; ...

We'd use this:
Stove_FunMethod(bool isFun, Stove __instance, ref bool ___variableName) { bool myVar = ___variableName; ...
NB: That's three underscores for the ref variable, not two. And yes, you'll be able to set as well as get protected variables this way.


Q: How can I add custom Dialogue?

A: See Abbysssal's notes on RogueLibs here[github.com]. Any in-game string of text is referred to as a Name.


Q: How can I access a Base Method (the version of a Method from a Base Class)?

A:
  1. First, verify that it's necessary. The codebase for Streets of Rogue uses base unnecessarily in a few places. Often you can refer to a Variable or Method at your patch's current level, and it will inherit to base anyway. However, if the target Variable or Method exists at both levels, you will need to use the steps below to ensure the Base version is retrieved.

  2. Create a separate class:
    public static class SSS { public static T GetMethodWithoutOverrides<T>(this MethodInfo method, object callFrom) where T : Delegate { IntPtr ptr = method.MethodHandle.GetFunctionPointer(); return (T)Activator.CreateInstance(typeof(T), callFrom, ptr); } }

  3. To access a base class's methods, you'll instantiate the above class. E.g., I want to add Stove.DamagedObject(), but those methods generally begin by calling the base method from ObjectReal:

    MethodInfo damagedObject_base = AccessTools.DeclaredMethod(typeof(ObjectReal), "DamagedObject"); damagedObject_base.GetMethodWithoutOverrides<Action<PlayfieldObject, float>>(__instance).Invoke(damagerObject, damageAmount);

    Note that you must add the method's arguments within the carets (<Action<>>). If there are no arguments, you can use <Action>.

  4. If there are multiple Methods by the same name within the Base class, you can add an optional argument, like so. Methods are ordered as written in the assembly.
    MethodInfo stopInteraction_base = AccessTools.DeclaredMethod(typeof(PlayfieldObject), "StopInteraction", new Type[0]); stopInteraction_base.GetMethodWithoutOverrides<Action>(__instance).Invoke();


Q: But... I didn't ask--

A: Yes, all of your patch methods should be static.


Q: How can I get log messages to debug my code?

A: Add a static object to your main class and instantiate it in Awake():
public class myPatch: BaseUnityPlugin { public static ManualLogSource MyLogger; public void Awake() { MyLogger = Logger; ... } }
Then, anywhere in that class, you can log messages:
MyLogger.LogMessage("Running myMethod");
Error Messages
Q: MethodInfo isn’t found!

A: Add using System.Reflection. Reflection is the term used to refer to the interactions between Base & Derived Classes in C#.


Q: Something else isn't found!

A: See the Setup section for a repository of reference files that SOR's source code depends on.


Q: Is this a typo in SOR's code? Why are there two periods?
particlePosition..ctor(this.tr.position.x, this.tr.position.y, + 0.36f, this.tr.position,z);
A: That's a vector constructor method in Unity. I don't know why it gets to break the rules, but it's not a typo.


Q: Errors say I’m not targeting the right version of .NET Framework for Assembly-CSharp.

A:
  1. In the Solution Explorer, right click your Project (the level under Solution on the file tree, with the C# icon), and select Properties. Under the Application tab, change “Target Framework” to “.NET Framework 4.5.2.”

  2. Add "netstandard.dll" from StreetsOfRogue_Data/Managed as a reference
And no, I don’t know why this works for me, and 3.5 works for him, but it works. In programming, you’ll find you want to pick your battles, so just roll with it.


Q: How do I create a .bin file for images?

A: Simply rename a .jpg or .png file.
Troubleshooting
Any issues that occur that are not flagged by an error message.


Q: My methods do not appear to be patching.

A: If you are using the RogueLibs method of patching methods:
this.PatchPrefix()
You need to indicate the arguments to the patch's target method in PatchPrefix's fifth, optional parameter. I believe this only applies if there are overloads (methods with the same name). So if you wish to patch ObjectReal.DestroyMe(PlayfieldObject), you'd do this:
this.PatchPrefix(typeof(ObjectReal), "DestroyMe", GetType(), "DestroyMe_Patch", new Type[1] { typeof(PlayfieldObject) });
The number in brackets should indicate the number of types provided in the array within the braces.


Q: The Properties folder isn't detected in my project.

A:
  1. Verify that the Default Namespace of your project matches your mod. Go to Project → YourModName Properties → Application. Default Namespace should match the namespace used in your C# file.

  2. If that doesn't work, you may need to add the Properties folder manually. Go to Project → YourModName Properties → Reference Paths, and select your Properties folder.

    In my project, this folder is used for holding a Resources folder, which contains .bin image files for making into sprites.


Q: How should I add a Sprite to my project?

A: I don't know about "should," but I can tell you how you can do it. The example I'm using was for adding a custom item, which resolved errors that arose from the following:
Sprite sprite_spear = RogueUtilities.ConvertToSprite(Properties.Resources.Spear);
  1. Create a .png or .jpg file, place it in your project/Properties/Resources folder, and rename its file extension to .bin.

  2. If Properties isn't detected, there's a fix for that in this guide.

  3. If the image is still not detected, Right click your Resources folder in your Solution Explorer → Add → Existing Item..., and navigate to your image to add it.

  4. Then, open Resources.resx (also in your Solution Explorer), select Add Resource → Add Existing File... and select your image. The errors should be cleared out now.
Best Practices & Timesavers
Q: I sure do hate copying this DLL over every time...

A: You can automate that. Now we'll learn how to do a "Post-build event," which means Visual Studio will execute a configurable batch script whenever you complete a Build successfully.

Go to Project > YourModName Properties and select the "Build Events" tab. Here's a Post-Build script I use in my project, which checks if SOR+BepInEx is installed on this computer, checks if the DLL is present, and then copies it to the appropriate directory.
set gameDirectory="C:\Program Files (x86)\Steam\steamapps\common\Streets of Rogue\BepInEx\plugins" set dll="BunnyMod.dll" if exist %gameDirectory% (ECHO "Destination directory found") ELSE (ECHO "Destination directory not found") if exist %dll% (ECHO "Mod DLL found") ELSE (ECHO "Mod DLL not found") if exist %gameDirectory% copy /y %dll% %gameDirectory%
Now, everytime I Build my mod, it gets automatically applied to SOR.

Q: What if I want it to run Streets of Rogue automatically too?

A: Append this to the end of your post-build event:
Start "" "C:\Program Files (x86)\Steam\steamapps\common\Streets of Rogue\StreetsOfRogue.exe"

Q: That's nice, but it doesn't run from Steam :(

A: I gotchu. Use this instead:
start steam://rungameid/512900

That's it!

[To do: Automate wiping ass for tutorial reader]


Q: Everything seems to work off of strings: Traits, Agents, etc., and a lot of this stuff has different names in the game! Isn't this error-prone?

A: It is massively error-prone! You do not want to be looking far and wide for errors in your code when the issue was a simple typo!

Let's take Trait names as an example. Do you really want to go and look up the code's name for the Share the Health trait? Why don't you try and guess it first? It'll be fun. Answer:
HealthItemsGiveFollowersExtraHealth.

Q: Wait, that wasn't any ♥♥♥♥♥♥♥ fun at all!

A: Exactly. Anyway, here's my workaround.

[Update: I contributed vanilla content classes to RogueLibs' GitHub repo. These string will be integrated into RogueLibs v3, so you won't need to add them manually in the future. If you want them sooner, you can copy-paste them from here: https://github.com/Abbysssal/RogueLibs/blob/v3.0-dev/RogueLibsCore/VanillaStrings.cs .]

First, pick a place to store long ugly lists of strings. I use the bottom of my mod's Header (main) file, since I don't do much work there now that the groundwork of my mod is pretty much where it needs to be.

Then, create a static class to encapsulate the category of strings you want to standardize.

public static class vTraits // Vanilla Traits { public const string AbovetheLaw = "AboveTheLaw", Accurate = "Accurate", Addict = "Addict", ... etc.

Now, you can simply refer to our example above as
vTraits.ShareTheHealth
. You never need to worry about entering a typo, because it simply will not compile if you do.

This is also a good practice for your own custom content. I've moved all my trait and ability names to this method, and it is already proving to be a timesaver.
Feedback
All questions, corrections, and contributions are welcome.

If you want to talk to me or Abbysssal, for questions, comments, or requests, please join the Streets of Rogue Discord here: https://discord.gg/h86pkyBMqP

8 Comments
Ted Bunny  [author] 14 Apr, 2021 @ 9:53am 
@The_Dark_Jumper

Sorry to hear that. Abbysssal really knows his stuff though, so if you contact him at the Discord he might be able to help you find an elegant workaround.
Jumper 14 Apr, 2021 @ 9:07am 
Unfortunately for me, I am not working on modding SOR.
The class that I'm trying to modify is a generic parser and is used with several hundreds of generic types. Covering all possibilities isn't exactly an option for me.
I didn't expect this to be an issue, since {LINK REMOVED} seems to suggest that the patched method would be shared across all non-value Types.

It also states `If the method is a non-generic non-static method of a generic class, you can check the generic type using __instance` which implies that modifying these methods should be possible?

I guess I'll have to look for a different workaround.
Ted Bunny  [author] 14 Apr, 2021 @ 8:52am 
@The_Dark_Jumper

I spoke to Abbysssal about it: apparently this is a known issue in BepInEx.

Here [github.com.cnpmjs.org] is a comment on this topic by the BepInEx creator. TL;DR: It's not something they have control over so it's pretty much a limitation of BepInEx patching.

As for what to do about it? Abbysssal advises that the next best thing would be patching each possibility you expect that Generic to encounter. Yes, it's extra work, but the SOR codebase is *very* hardcoded so Generics are extremely rare. Rare enough that in the nearly 10,000 lines of code I've written for my own mod, I have not encountered this problem.

Again, if you can point to a particular example of what you mean, I might be able to give more specific advice. I hope this helped, though.
Ted Bunny  [author] 14 Apr, 2021 @ 8:32am 
Can you be more specific about which method and class you're referring to? In my experience, whether the class itself is generic or not should not affect your ability to patch its methods. But I don't think I've observed that particular pattern in the SOR codebase.
Jumper 14 Apr, 2021 @ 2:20am 
Alright, but there's still one thing that neither of the guides, nor the Harmony documentation have cleared up for me.

How do I patch a non-generic method of a generic class? (So far I'm consistently getting `NotSupportedException: Specified method is not supported.`)
cya 23 Oct, 2020 @ 8:53pm 
well shit lol.
Ted Bunny  [author] 23 Oct, 2020 @ 4:06pm 
If you're gonna have a black hole, you need to walk it and feed it.
cya 23 Oct, 2020 @ 9:09am 
Instructions unclear, accidentaly created a black hole that is feeding off of people's sanity