[C#] How to Add Button in Bottom-Right Corner

Started by maarx1337, March 04, 2017, 03:51:45 PM

Previous topic - Next topic

maarx1337

In the bottom-right corner of the game window are seven (7) buttons. I list the buttons at the bottom of this post in case you are not sure what I am referring to.

I would like for my mod to add another button here. How do I go about doing this?

Currently my mod is just a MapComponent so I am afraid I am not currently in the right scope for this.

So if anybody could point me in the right direction or throw me an example, that would be great.


For clarification, the buttons I am referring to are:


  • Toggle visibility of the learning helper when it is empty.
  • Toggle visibility of zones.
  • Toggle the environment display.
  • Toggle colonist bar.
  • Toggle visibility of roofs.
  • Toggle automatically expanding the home area around new constructions.
  • Toggle categorized mode on the resource readout.

RawCode

all code, including one used to render buttons and everything else stored inside
assembly-csharp.dll

you need dnspy\ilspy\anyother decompiler and notepad++ or similar text editor to access and navigate "source".

maarx1337

Yes, I have ILSpy, and I've looked at the source, but cannot find the entry point to add a button like this. :(

RawCode

please provide results of your research, what keywords you has used, what do you means under "entry point"?

(probably you want staticconstructoronstartup)

maarx1337

I see that Steam mod Cleaning Area adds such a button.

Decompiling this mod in ILSpy, it seems the button is created and added in method ...

public static void Postfix(WidgetRow row, bool worldView)

... with key line being ...

row.ToggleableIcon(ref PlaySettingsPatch.showFloatingMenu, CleaningAreaLoader.cleaningTexture, "SelectCleaningArea".Translate(), SoundDefOf.MouseoverToggle, null);

... but if I add a method with same signature ...

public static void Postfix(WidgetRow row, bool worldView)
{
        Log.Message("Hello from postfix!");
}


... to my class, the code is never ran.

This is not entirely surprising, since my class is a MapComponent, probably not the right type.

I run Search in ILSpy for "PlaySettingsPatch" but find no other code to give hint as to where "PlaySettingsPatch" is called / instantiated from.

I suspect it has to do with line ...

[HarmonyPatch(typeof(PlaySettings)), HarmonyPatch("DoPlaySettingsGlobalControls")]

... which seems to be a third party library anyways, and I did not really want to add a third party dependency to my mod for something which I think should be simple.

So, I step back from example mod "Cleaning Area", taking with me the hints of ...

public static void Postfix(WidgetRow row, bool worldView)

... and ...

row.ToggleableIcon(

... and search the vanilla DLLs for this in ILSpy ...

... I find Verse.WidgetRow easily enough, with ToggleableIcon(, this is definitely what I am looking for ...

... but it doesn't point me towards the upstream code which actually calls / creates these and how ...

... I search vanilla DLLs for "Postfix" and get no hits, implying this whole method might be part of Harmony library, so abandon that route ...

Taking a different route, looking at the vanilla buttons bulleted in my first post, and searching on their text, I blindly stumble upon RimWorld.PlaySettings, which looks great ...

... it has bools for each of the buttons, and I find method of interest ...

public void DoPlaySettingsGlobalControls(WidgetRow row, bool worldView)

... gee this looks familiar, we are probably on a right path, it again adds the buttons with ...

row.ToggleableIcon(

... and the whole class is ...

public sealed class PlaySettings : IExposable

... but if I add similar entire test class to my mod ...

    class MyTestIExposable : IExposable
    {
        public void DoPlaySettingsGlobalControls(WidgetRow row, bool worldView)
        {
            Log.Message("Hello from DoPlaySettingsGlobalControls");
        }

        public void ExposeData()
        {
            //throw new NotImplementedException();
        }
    }


... the code is still not actually run. This is not entirely surprising, presumably something upstream has to actually call this, but I am no closer to understanding where from ...

I would love to find where this method is actually called from, but doing a Search in ILSpy for "DoPlaySettingsGlobalControls" just returns the method itself, and doesn't return examples of where the method is called from.

Same with "WidgetRow", doing a Search for this in ILSpy returns Verse.WidgetRow itself, but doesn't return upstream examples of where it is used and how.

Hatti

Please be aware of, that the harmony have a huge bug atm which disallows mod to patch the same methods.
If you do your tests, be sure, you disable ever other mod such as CleaningArea and N.V.A cause the game will throw massiv errors if 2 mods patch the same method.

and its not only putting those methods into a mod. As you already figured out, it has something to do with

[HarmonyPatch(typeof(PlaySettings)), HarmonyPatch("DoPlaySettingsGlobalControls")]

Please get known to the harmony library. It provides a VERY good documentation. (But be aware of, that it DOES NOT do its job what its purposed to due to the bug)

maarx1337


RawCode

you can't override methods not explicitly marked virtual, please read c# documentation for more info about.
and you can't override methods by merely providing same signature method somewhere else, this also perfectly covered by c# documentation.

harmony is OK, documentation is nice, but entire code is emitting jump into functionpointer of original method and nothing more, everything else is sugar.

maarx1337

The tool "ILSpy" seemed insufficient for this purpose. I was unable to find the WidgetRow object that I wanted to add ToggleableIcon(...) to, unable to find correct code path to reach it.

I downloaded .NET Reflector with CodeSearch plugin which allows full-text-search on decompiled code, not just search on Type and Member like ILSpy.

Using this tool, I investigated a bit further and found:

The WidgetRow object we are looking to add a ToggleableIcon(...) to, only exists as variable within object GlobalControls. We can get to GlobalControls object from anywhere using code ...

Find.MapUI.globalControls

... but the object "WidgetRow" we are seeking is declared as private variable in code ...

private WidgetRow rowVisibility;

... we can technically access the private variable using reflection, and so the following code gets us where we want to be, at least at compile-time ...

WidgetRow theRow;
theRow = (WidgetRow) typeof(GlobalControls).GetField("rowVisibility", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(Find.MapUI.globalControls);
theRow.ToggleableIcon(ref this.myToggle, resetButton, "myTooltip");


... but at run-time this code throws stack trace error ...

System.ArgumentException: You can only call GUI functions from inside OnGUI.

... which is unsurprising, considering the object was probably set to private for a reason and we just recklessly disregard that.

So I am beginning to understand that what seemed like a simple task is actually not so simple, we have strayed into code which is obviously not intended to be changed in a mod, we are probably in the wrong thread or something, which explains why others accomplished this task only using third-party library, and I am probably in over my head.

I could obviously use the same third-party libraries as others to accomplish this task, but as noted above, it seems there are compatibility issues if multiple mods try to do this same thing.

My need for the button in this specific location is not so great that I should introduce third party library requirements or introduce compatibility issues with other mods. I thought this was a simple task and clearly it is not. I will probably just abandon this route and add a button using a more traditional approach, like adding a MainTab or something.

Thanks to everybody for patiently humoring my nonsense.

RawCode

Quoteit seems there are compatibility issues if multiple mods try to do this same thing.

unrelated to harmony itself, this is how code injection method harmony based on works.