Making custom overlay display step by step

Started by RawCode, August 09, 2021, 08:47:36 AM

Previous topic - Next topic

RawCode

1) Open the game;
2) Hover over existing overlay display, like "beauty";
Toggle beauty display
3) Search this string inside data folder
   
<KeyBindingDef ParentName="GameKeyBinding">
    <defName>ToggleBeautyDisplay</defName>
    <label>toggle beauty display</label>
    <defaultKeyCodeA>T</defaultKeyCodeA>
  </KeyBindingDef>

If you are using non English, you will find this string with extra step, all translations are part of data.

4) Not very useful, but this is exactly how modding is done, first you must understand how exactly game works in order to change anything.

public void DoPlaySettingsGlobalControls(WidgetRow row, bool worldView)


5) Feature is "hardcoded" and integral part of UI itself, it cannot be changed without code injection.

6) Pressing button flips "showBeauty" nothing more, actual work done elsewhere:
BeautyDrawer
EnvironmentStatsDrawer

7) Method
DrawBeautyAroundMouse
is rather suspicious, dunno what can that method actually do, there are no comments in disassembly after all, probably we will never know!

8) Tracking how this method is called lead us to this cozy list:

unity GUI loop
OnGUI
UIRootOnGUI
MapInterfaceOnGUI_BeforeMainTabs
BeautyDrawerOnGUI


7) There are other mods with overlay and GUI features, i checked few and they use postfix to inject into GUI rendering, this is okay, but not needed, you can inject into Unity GUI loop directly.
GUI loop called ~60 times per second, you should never perform anything heavy inside GUI loop, you can and will slowdown entire game.

8) How exactly it works and why it works - google is your very best friend, this is part of Unity development and not related to modding or Rimworld, this is how Unity games are made.

9) Copy classes from vanilla, make some changes, tech demo ready:

Code injection part A:

public class XToken : MonoBehaviour
{
public void OnGUI()
{
CoverDrawer.DrawCoverAroundMouse();
}
}


Part B:
(can be called from any place you like, should be called only once obviously)

GameObject go = new GameObject("TOKEN");
go.AddComponent<XToken>();
UnityEngine.Object.DontDestroyOnLoad(go);
go.SetActive(true);


Payload:

public static class CoverDrawer
{
public static bool ShouldShow()
{
//if you think that this code is super bad, check assembly image of it first
while (true)
{
if (Find.CurrentMap == null)
break;
if (UI.MouseCell() == IntVec3.Invalid)
break;
if (Mouse.IsInputBlockedNow)
break;
return UI.MouseCell().InBounds(Find.CurrentMap);
}

return false;
}
public static void DrawCoverAroundMouse()
{
if (!ShouldShow())
return;


IntVec3 rt = UI.MouseCell();
if (!rt.IsValid)
return;

int z = 0;
for (int i = 0; i < 25; i++)
{
IntVec3 intVec = rt + GenRadial.RadialPattern[i];
if (intVec.InBounds(Find.CurrentMap))
{
List<Thing> list = Find.CurrentMap.thingGrid.ThingsListAt(intVec);
foreach (Thing t in list)
{
//this is not cover, thx i know
z += t.def.CostStuffCount;
}
GenMapUI.DrawThingLabel(GenMapUI.LabelDrawPosFor(intVec), Mathf.RoundToInt(z).ToStringCached(), Color.white);
z = 0;
}
}
}
}