こんにちは世界

Unity modding

Introduction into Unity modding.

Why?

There’s something fascinating about looking beneath a game’s surface. Figuring out how it works, and finding ways to change things that weren’t supposed to be changed.

Here’s a little example on how it all works.

Loading plugins

[BepInPlugin("minimal.eft.plugin", "Minimal", "1.0.0")]
public class Loader : BaseUnityPlugin
{
    public static GameObject gameObject;
    public static ManualLogSource logger;

    void Awake()
    {
        logger = Logger;

        gameObject = new GameObject();
        DontDestroyOnLoad(gameObject);
        gameObject.AddComponent<Manager>();
    }
}

Global manager

public class Manager : MonoBehaviour
{
    public static bool bLoaderQuestion = true;
    public static Manager instance;

    int flLoaderWidth = 200f;

    void Start()
    {
        instance = this;

        G.IsMenuFrozen = true;

        T.SetupFolders();
        T.PopulateAssets();
        T.SetupShaders();

        AttributeUtilities.LinkAttributes();

        Colors.AddColors();

        StartCoroutine(UpdateObjects());
    }

    void Update()
    {
        bLoaderQuestion = false;
        G.IsMenuFrozen = false;

        if (bLoaderQuestion)
		{
		    if (Input.GetKeyDown(KeyCode.Y) || Input.GetKeyDown(KeyCode.N))
		    {
		        bLoaderQuestion = false;
		        G.IsMenuFrozen = false;

		        if (Input.GetKeyDown(KeyCode.N))
		        {
		            Loader.gameObject.DestroyAllChildren();
		            DestroyObject(Loader.gameObject);
		        }
		    }
		}
    }

    void OnGUI()
    {
        Styles.Setup();
        Colors.Interpolate(out Colors.RainbowColor, 0);

        if (bLoaderQuestion)
        {
            GUI.Box(new Rect(G.Settings.MenuSettings.X - (flLoaderWidth / 2) - 2, G.Settings.MenuSettings.Y - 2, (flLoaderWidth + 4), 59), "", Styles.MenuPrimary);           // border
            GUI.Box(new Rect(G.Settings.MenuSettings.X - (flLoaderWidth / 2), G.Settings.MenuSettings.Y + 0, flLoaderWidth, 55), "", Styles.MenuBackground);                  // background

            GUI.Label(new Rect(G.Settings.MenuSettings.X - (flLoaderWidth / 2), G.Settings.MenuSettings.Y + -5, flLoaderWidth, 35), "Load Avalon?", Styles.MenuTitle);        // question
            GUI.Label(new Rect(G.Settings.MenuSettings.X - (flLoaderWidth / 2) + 5, G.Settings.MenuSettings.Y + 25, flLoaderWidth, 35), "Yes (Y)", Styles.MenuTextColorLeft); // yes
            GUI.Label(new Rect(G.Settings.MenuSettings.X - (flLoaderWidth / 2) - 5, G.Settings.MenuSettings.Y + 25, flLoaderWidth, 35), "No (N)", Styles.MenuTextColorRight); // no
        }

        if (G.MainCamera == null)
            G.MainCamera = Camera.main;
    }
}

Backend structure

public class structure
{
    public static bool keyCooldown = false;

    public static IEnumerator KeyboardEventCooldown()
    {
        keyCooldown = true;
        yield return new WaitForSeconds(0.2f);
        keyCooldown = false;
    }

    public static void SetupMenu(string menuName, string title, int maxSize)
    {
        if (G.Submenu != title || G.MaxMenuSize != maxSize)
        {
            G.Submenu = title;
            G.MaxMenuSize = maxSize;
            G.MenuSize = 0;

            var result = G.SavedCursors.SingleOrDefault(x => x.Key == G.CurrentMenu);
            G.Cursor = (result.Value != null) ? result.Value : 0;

            G.Dividers = new bool[maxSize + 1];
            for (int i = 0; i < maxSize; i++)
                G.Dividers[i] = false;
        }
    }

    public static void AddOpt(string text, int index, string desc = "", bool enabled = true)
	{
		if (G.Cursor == index)
		{
			GUI.Box(new Rect(G.Settings.MenuSettings.X - (G.Settings.MenuSettings.Width / 2), G.Settings.MenuSettings.Y + (70 + index * 35), G.Settings.MenuSettings.Width, 35), "", Styles.MenuScrollbar);
			GUI.Label(new Rect(G.Settings.MenuSettings.X - (G.Settings.MenuSettings.Width / 2) + 10, G.Settings.MenuSettings.Y + (105 + G.MaxMenuSize * 35), G.Settings.MenuSettings.Width, 35), desc, (G.Settings.MenuSettings.DesignIndex == 0 ? Styles.MenuTextColorLeft : Styles.MenuTextLeft));

			if (Event.current.Equals(Event.KeyboardEvent("F")) && !keyCooldown && enabled)
			{
				Manager.ExecuteOptions();
				Manager.instance.StartCoroutine(KeyboardEventCooldown());
			}
		}

		GUI.Label(new Rect(G.Settings.MenuSettings.X - (G.Settings.MenuSettings.Width / 2) + 10, G.Settings.MenuSettings.Y + (70 + index * 35), G.Settings.MenuSettings.Width, 35), (!enabled ? string.Format("<color=#ffffff40>{0}</color>", text) : text), Styles.MenuTextLeft);

		if (G.MenuSize != G.MaxMenuSize)
			G.MenuSize++;
	}
}

Compound VR — ESP

Frontend structure

[Comp]
public class MainMenu : MonoBehaviour
{
    void OnGUI()
    {
        if (G.IsMenuOpen)
        {
            switch (G.Settings.MenuSettings.DesignIndex)
            {
                case 0:
                    GUI.Box(new Rect(G.Settings.MenuSettings.X - (G.Settings.MenuSettings.Width / 2) - 2, G.Settings.MenuSettings.Y - 2, G.Settings.MenuSettings.Width + 4, (140 + G.MaxMenuSize * 35) + 4), "", Styles.MenuPrimary);                       // border
                    GUI.Box(new Rect(G.Settings.MenuSettings.X - (G.Settings.MenuSettings.Width / 2), G.Settings.MenuSettings.Y + 0, G.Settings.MenuSettings.Width, (140 + G.MaxMenuSize * 35)), "", Styles.MenuBackground);                                // background
                    GUI.Box(new Rect(G.Settings.MenuSettings.X - (G.Settings.MenuSettings.Width / 2), G.Settings.MenuSettings.Y + 35, G.Settings.MenuSettings.Width, 35), "", Styles.MenuSecondary);                                                        // top bar
                    GUI.Box(new Rect(G.Settings.MenuSettings.X - (G.Settings.MenuSettings.Width / 2), G.Settings.MenuSettings.Y + (105 + G.MaxMenuSize * 35), G.Settings.MenuSettings.Width, 35), "", Styles.MenuSecondary);                                // bottom bar

                    GUI.Label(new Rect(G.Settings.MenuSettings.X - (G.Settings.MenuSettings.Width / 2), G.Settings.MenuSettings.Y + 0, G.Settings.MenuSettings.Width, 35), G.MenuName, Styles.MenuTitle);                                                   // title
                    GUI.Label(new Rect(G.Settings.MenuSettings.X - (G.Settings.MenuSettings.Width / 2) + 10, G.Settings.MenuSettings.Y + 35, G.Settings.MenuSettings.Width, 35), G.Submenu, Styles.MenuTextColorLeft);                                      // subtitle
                    GUI.Label(new Rect(G.Settings.MenuSettings.X - (G.Settings.MenuSettings.Width / 2) - 10, G.Settings.MenuSettings.Y + 35, G.Settings.MenuSettings.Width, 35), $"{(G.Cursor + 1)} / {(G.MaxMenuSize + 1)}", Styles.MenuTextColorRight);   // optcount
                    break;
            }

            switch (G.CurrentMenu)
            {
                case "main":
                    Structure.SetupMenu("main", "Main Menu", 0);
                    Structure.AddOpt("Hello World", 0, "This is a description.");
                    break;
            }
        }
    }
}

Bean Battles — Minimal

Raft — Minimal

Escape from Tarkov — Minimal

Monitoring controls

[Comp]
public class Controls : MonoBehaviour
{
    void Update()
    {
        if (!G.IsMenuFrozen)
        {
            if (Input.GetKeyDown(KeyCode.LeftAlt))
                G.IsMenuOpen = !G.IsMenuOpen;

            if (G.IsMenuOpen)
            {
                if (Input.GetKeyDown(KeyCode.UpArrow))
                {
                    G.Cursor--;
                    if (G.Cursor < 0)
                        G.Cursor = G.MenuSize;
                }
                else if (Input.GetKeyDown(KeyCode.DownArrow))
                {
                    G.Cursor++;
                    if (G.Cursor > G.MenuSize)
                        G.Cursor = 0;
                }
                else if (Input.GetKeyDown(KeyCode.Backspace))
                {
                    var result = G.SavedCursors.SingleOrDefault(x => x.Key == G.CurrentMenu);
                    if (result.Value != null)
                        G.SavedCursors.Remove(result);
                }
            }
        }

    }
}