SoundCalls for a custom race

Started by tistol, May 25, 2018, 03:17:23 PM

Previous topic - Next topic

tistol

Hello.
First of all I want to clarify that I am the definition of C# and .xml newb, so please forgive me for not using the professional nomenclature.
For some time now i've been modifying the MAI mod (personal use only, please don't sue me) to make the mod's pawn more mechanoid like. Identical to a scyther actually.
While I successfully changed the flesh type, textures, body parts and what not, I have had a small problem with giving my scythers an idle sound.
The death, hit and other "reaction" sounds work perfectly well, but the SoundCall lines that I have copied directly from mechanoid race file and pasted in both the base pawn section and the "child" pawn section, doesn't seem to do anything.

The question is whether there's something I missed that stops the SoundCall from working, or is it simply not possible for custom / humanlike races to have idle sounds ?

jamaicancastle

I don't think humanlikes randomly vocalize the way animals do. You could probably change that, but it would definitely require C#.

tistol

Quote from: jamaicancastle on May 26, 2018, 06:30:29 AM
I don't think humanlikes randomly vocalize the way animals do. You could probably change that, but it would definitely require C#.
Is there any specific direction I can look in to make those changes, or am I basically entering the C# terra incognita with this one?

jamaicancastle

Quote from: tistol on May 26, 2018, 08:58:51 AM
Quote from: jamaicancastle on May 26, 2018, 06:30:29 AM
I don't think humanlikes randomly vocalize the way animals do. You could probably change that, but it would definitely require C#.
Is there any specific direction I can look in to make those changes, or am I basically entering the C# terra incognita with this one?
The first thing to do is fire up your trusty decompiler (you can find links to several in Jec's pinned tutorial; I use ILSpy myself). Because we know the sounds are included in a ThingDef, it seems logical to start there.

All def classes have the same basic structure as their corresponding XML - they have to, so the game knows what data goes where. We're looking for ThingDef -> race -> lifeStageAge -> soundCall. So, starting in the ThingDef class, we find the "race" field, which is of the RaceProperties class. That class in turn has a list of the LifeStageAge class, which finally has the SoundDef we're looking for.

Now that we see where the def ends up in code, we want to see what goes looking for it. In ILSpy, this is where the Analyze command comes into play. From the left-hand menu, expand LifeStageAge, right-click on soundCall, and choose "Analyze". A pane will open on the bottom showing everything that refers to soundCall, grouped by the type of interaction (reading the value, setting the value, etc.). It turns out only one thing does so - Pawn_CallTracker.

Pawn_CallTracker is one of several helper classes that get attached to pawns - pawns also have StoryTrackers, ApparelTrackers, and what have you. Analyzing Pawn_CallTracker itself, we can see (using the "instantiated by" category) that these are all made by a utility function, AddComponentsForSpawn. In particular, towards the bottom there's this block:
if (pawn.RaceProps.intelligence <= Intelligence.ToolUser && pawn.caller == null)
{
pawn.caller = new Pawn_CallTracker(pawn);
}


So there's our answer: if the pawn has humanlike intelligence, it doesn't create a Pawn_CallTracker and thus nothing is making random vocalizations for it. To enable vocalizations, we just need to add a Pawn_CallTracker to the pawn and (in theory) everything else will take care of itself.

There are several equally good ways to do this, depending on exactly how the androids are implemented. Probably the easiest is to pick up Harmony and add a postfix on the AddComponents function that essentially says "if this is an android(, and it doesn't have one), give it a Pawn_CallTracker".

tistol

Quote from: jamaicancastle on May 26, 2018, 09:54:28 AM
Quote from: tistol on May 26, 2018, 08:58:51 AM
Quote from: jamaicancastle on May 26, 2018, 06:30:29 AM
I don't think humanlikes randomly vocalize the way animals do. You could probably change that, but it would definitely require C#.
Is there any specific direction I can look in to make those changes, or am I basically entering the C# terra incognita with this one?
The first thing to do is fire up your trusty decompiler (you can find links to several in Jec's pinned tutorial; I use ILSpy myself). Because we know the sounds are included in a ThingDef, it seems logical to start there.

All def classes have the same basic structure as their corresponding XML - they have to, so the game knows what data goes where. We're looking for ThingDef -> race -> lifeStageAge -> soundCall. So, starting in the ThingDef class, we find the "race" field, which is of the RaceProperties class. That class in turn has a list of the LifeStageAge class, which finally has the SoundDef we're looking for.

Now that we see where the def ends up in code, we want to see what goes looking for it. In ILSpy, this is where the Analyze command comes into play. From the left-hand menu, expand LifeStageAge, right-click on soundCall, and choose "Analyze". A pane will open on the bottom showing everything that refers to soundCall, grouped by the type of interaction (reading the value, setting the value, etc.). It turns out only one thing does so - Pawn_CallTracker.

Pawn_CallTracker is one of several helper classes that get attached to pawns - pawns also have StoryTrackers, ApparelTrackers, and what have you. Analyzing Pawn_CallTracker itself, we can see (using the "instantiated by" category) that these are all made by a utility function, AddComponentsForSpawn. In particular, towards the bottom there's this block:
if (pawn.RaceProps.intelligence <= Intelligence.ToolUser && pawn.caller == null)
{
pawn.caller = new Pawn_CallTracker(pawn);
}


So there's our answer: if the pawn has humanlike intelligence, it doesn't create a Pawn_CallTracker and thus nothing is making random vocalizations for it. To enable vocalizations, we just need to add a Pawn_CallTracker to the pawn and (in theory) everything else will take care of itself.

There are several equally good ways to do this, depending on exactly how the androids are implemented. Probably the easiest is to pick up Harmony and add a postfix on the AddComponents function that essentially says "if this is an android(, and it doesn't have one), give it a Pawn_CallTracker".

Phew, after some time I've managed to locate the AddComponents function and I indeed see all of the trackers that can be added.
The "Intelligence.ToolUser" part is replaced by "1" in my file, so HumanLike probably is "2". The problem is, I have absolutely no clue about adding a few additional lines in there. I have downloaded Harmony, but apparently it's meant to be helpful for those who already have some experience in C#. The online tutorials don't really help, but I know that I have to somehow create a file that will connect with the AddComponents function and add a section that includes HumanLike intelligence using CallTracker.
Any additional help will be greatly appreciated!

jamaicancastle

In order to make a Harmony patch, you basically need four things:
- You need to identify the function that's going to be patched.
- You need to identify how the patch will be applied in relation to that function.
- You need to actually write the path.
- And you need to tell the game to run the patch.
Fortunately all of these are pretty straightforward.

We'll start with the last requirement. To initialize Harmony in your mod, you need to include the following code:
    [StaticConstructorOnStartup]
    internal static class HarmonyInit
    {
    static HarmonyInit()
        {
            HarmonyInstance harmonyInstance = HarmonyInstance.Create("your_instance_name");
            harmonyInstance.PatchAll(Assembly.GetExecutingAssembly());
        }
    }

All this does it look through your compiled assembly and execute all the Harmony patches it can find when the game loads. The class name is totally arbitrary; C# wants everything to be in a class, but it doesn't matter in this case what that class is. "your_instance_name" should be unique across all mods - so you'll generally want to include your name or username, and your mod's name.

To actually designate a class as being a Harmony patch, you need to use annotations. This looks like so:
    [HarmonyPatch(typeof(TypeToPatch), "NameOfMethodToPatch")]
    class YourPatchClass
    {

Again, the class name is arbitrary; name it something that will help you remember what it is it's patching. The annotation in [brackets] is how Harmony actually identifies the method. Normally just the type and name are sufficient. There's a third component to annotations you might see looking through other people's patches, which corresponds to the method's signature (the list of parameters it takes, by type); this is used when there are multiple methods with the same name.

Next you want to set the type of your patch. You do this by making specific methods within your patch class: Prefix(), Postfix(), or Transpile(). You can have more than one, if you want. The behavior of each one is different:
- Prefix is a normal function that happens before the method in question. It can return true or false; if it returns false, the method (and any other Harmony patches it has) will stop running.
- Postfix is a normal function that happens after the method has already run.
- Transpile is a more advanced way of patching that allows you to make changes to a specific line inside the method - this is mostly useful when you want to remove or change something from a very long method.

In this case, a postfix is probably what you want - you want it to do what it normally does, and then have your CallTracker come along for the ride. Once you have a Postfix method, you're ready to write the meat of the patch.

Normally when you're patching, you'll want to refer to certain variables the method itself has access to. Prefixes and postfixes can take any of the following parameters, and Harmony will automatically fill them with the correct information:
- Any of the parameters of the original method, by their type and name
- The object the original method belongs to (if it's not static), by its type and the name __instance
- The return value of the original method (if it's not void), by its type and the name __result

In this case, AddComponentsForSpawn is both static and void, so there's no instance or result. There is, however, one parameter of the original method that we want, the pawn it's applied to. Having gotten that pawn, we check if it's an android and stick a CallTracker on it. So our patch function will look like this:

    public static void Postfix(ref Pawn pawn)
    {
if (pawn.def.defName == "android's defName" && pawn.caller == null)
{
pawn.caller = new Pawn_CallTracker(pawn);
}
    }

Where "android's defName" is of course replaced by the actual defName in question. You can use whatever other comparison you want, as long as it makes sense (just remember that pawns without call sounds - like most humanlikes - will complain if they're given a CallTracker). The null check will make sure that a pawn isn't given two CallTrackers by mistake.

crustymonkey

Okay, you were saying on the subreddit that you might need walked through some stuff. How lost are you? Do you have an IDE picked out yet? I use MonoDevelop since I'm in linux fairly often and it makes it easy to decompile and look at the rimworld code. Visual Studio is popular as well. Where are you stuck right now?

tistol

Quote from: crustymonkey on May 28, 2018, 09:58:16 AM
Okay, you were saying on the subreddit that you might need walked through some stuff. How lost are you? Do you have an IDE picked out yet? I use MonoDevelop since I'm in linux fairly often and it makes it easy to decompile and look at the rimworld code. Visual Studio is popular as well. Where are you stuck right now?
I have already downloaded Visual Studio Code.
Now the thing is, I have to copy the template provided above and change some things accordingly to the mod, which is fairly easy. I'm stuck at saving the code and doing something with it. I don't know whether I should save it as .xml, .cs, where to put it and such. Also I have no clue about the "using" entries in the code. I know there must be a "using Harmony;" line, but besides that?
Also, I might have botched the code itself as I'm basically filling the blanks with anything that seems connected to the issue.

[attachment deleted due to age]

crustymonkey

This code should be saved as .cs. If you see an option to start a c-sharp project just do that, so any files you create will end with .cs by default and the compiler will recognize them as such and compile to a .dll. Or in Jecrell's words
"Make a new Visual C# Class Library .NET Framework project."

I'm not as familiar with visual studio as I am with MonoDevelop but Jecrell has some fine tutorials on this very forum here.
https://ludeon.com/forums/index.php?topic=33219.msg338631#msg338631
You'll want to set references to Assembly-CSharp.dll and UnityEngine.dll in your RimWorld\RimWorldWin_Data\Managed. This way your code can "play" with the vanilla rimworld code. You'll probably also want a "using RimWorld;" and "using Verse;" at least up top. Once I get around to setting up this computer at my Mom's house I'll take a look at your code myself and see what sort of red squiggly lines pop up.

tistol

Quote from: crustymonkey on May 28, 2018, 01:28:19 PM
This code should be saved as .cs. If you see an option to start a c-sharp project just do that, so any files you create will end with .cs by default and the compiler will recognize them as such and compile to a .dll. Or in Jecrell's words
"Make a new Visual C# Class Library .NET Framework project."

I'm not as familiar with visual studio as I am with MonoDevelop but Jecrell has some fine tutorials on this very forum here.
https://ludeon.com/forums/index.php?topic=33219.msg338631#msg338631
You'll want to set references to Assembly-CSharp.dll and UnityEngine.dll in your RimWorld\RimWorldWin_Data\Managed. This way your code can "play" with the vanilla rimworld code. You'll probably also want a "using RimWorld;" and "using Verse;" at least up top. Once I get around to setting up this computer at my Mom's house I'll take a look at your code myself and see what sort of red squiggly lines pop up.

Well apparently my Visual Studio won't let me set a reference, and the things present in the tutorial aren't even in my program. These options are not there, at all. I think I'll wait with you for your machine to be set up and maybe in the meantime make an appointment to manage my anger, as the constant troubleshooting with lacking program gave me quite a bit of noob irritation.

[attachment deleted due to age]

crustymonkey

Okay, managed to use your code in a solution, got rid of the red squiggles and runtime error messages. Dunno if it works though since I don't have any xml patches you might have made and my folks somehow managed to break the speakers again since the last time I visited. Anyway, I'll leave what I've cobbled together with the code you provided here. I left the about, defs, and patches folders blank since you'll probably fill those in yourself. To attempt to open the solution proper like in visual studio or what have you, you'll want to click on Source\MAISound\MAISound.sln Hopefully, that'll work out and if you need to tweak the code farther and recompile you'll be able to do so. Best of luck and remember that hopping into modding with almost no programming experience, you'll need to maintain a zen-like resolve.

[attachment deleted due to age]

tistol

Quote from: crustymonkey on May 29, 2018, 02:21:35 PM
Okay, managed to use your code in a solution, got rid of the red squiggles and runtime error messages. Dunno if it works though since I don't have any xml patches you might have made and my folks somehow managed to break the speakers again since the last time I visited. Anyway, I'll leave what I've cobbled together with the code you provided here. I left the about, defs, and patches folders blank since you'll probably fill those in yourself. To attempt to open the solution proper like in visual studio or what have you, you'll want to click on Source\MAISound\MAISound.sln Hopefully, that'll work out and if you need to tweak the code farther and recompile you'll be able to do so. Best of luck and remember that hopping into modding with almost no programming experience, you'll need to maintain a zen-like resolve.

Oh my, it's working perfectly.
Thank you for going through the trouble of making this all by yourself to aid a more-than-a-novice anon!

After taking a look at what you have managed to concoct there I think I'll stick with my Python lists for a little longer before going into C# though.

crustymonkey