In our last post we created a no spread, no recoil, and increased projectile speed mod for our weapons. In this post we will create a mod to allow us to teleport to the different islands in the game, along with improving our menu’s theme and layout.
Both halves lean on things we have already built. The island teleport turns out to be a method the developers wrote for their own use, the same gift ItemSpawner was two posts ago. And the menu rework is the payoff for a decision from post three: CheatMenu never knew what a cheat was, only that it could Draw itself, so growing it into a tabbed layout touches that one class and adds a single line to each cheat.
Teleporting Between Islands
Searching Island in dnSpy turns up OnlineIslandManager, and it is the same kind of gift ItemSpawner was: the developers left the whole feature sitting in public static methods.
The two pieces of state it owns:
public readonly SyncVar<byte> _curIsland = new SyncVar<byte>(default(SyncTypeSettings));
public readonly SyncVar<byte> _maxIslandUnlocked = new SyncVar<byte>(2, default(SyncTypeSettings));
Both SyncVars, which by now is a familiar shape and a familiar constraint: the server owns them, and every method that writes one is gated on IsServerInitialized. SpawnIsland is the actual mover:
public void SpawnIsland(byte islandIndex)
{
if (!base.IsServerInitialized) return;
this._curIsland.Value = islandIndex;
}
So island teleport is host-only, same rule as money and item spawning. Nothing new there. What is new is that we do not have to call SpawnIsland ourselves, because the class exposes finished helpers:
public static void TpToSpecificIsland(byte to)
{
if (!OnlineIslandManager.Instance) return;
OnlineIslandManager.ToggleTeleportPlayers(true);
OnlineIslandManager.Instance.SpawnIsland(to);
}
public static void TpToNextIsland(bool prev = false)
{
if (!OnlineIslandManager.Instance) return;
OnlineIslandManager.ToggleTeleportPlayers(true);
OnlineIslandManager.Instance.NextIslandInBuild(prev);
}
These do more than set the SyncVar. ToggleTeleportPlayers(true) is the bit we would have missed writing this by hand: it tells the game to bring the players along to the new island rather than leaving them floating where the old one used to be. TpToNextIsland also wraps the index around the build order for us via NextIslandInBuild, so we never have to know how many islands there are. Copying the developers’ own entry point gets both for free, exactly like lifting the height-lift and random-spin out of ItemSpawner last time.
There is one more, and it is the reason to bother reading the whole class:
public void UnlockIsland(byte islandIndex)
{
if (islandIndex > this._maxIslandUnlocked.Value)
this._maxIslandUnlocked.Value = islandIndex;
}
Raise the unlock cap to 255 and every island is available.
The cheat
Cheats/IslandCheat.cs:
using System.Globalization;
using UnityEngine;
namespace HtFMod
{
internal sealed class IslandCheat : Cheat
{
private string _field = "0";
internal override string Label => "Islands";
internal override Category Category => Category.World;
// menu only: the cheat is the buttons, there is nothing sensible to fire blind
internal override void Activate() { }
internal override void Draw()
{
var mgr = OnlineIslandManager.Instance;
// every write is host-gated, so a client just no-ops. Say so rather than
// showing dead buttons.
if (mgr == null) { GUILayout.Label("Islands: not in a game."); return; }
if (!mgr.IsServerInitialized) { GUILayout.Label("Islands: host only."); return; }
GUILayout.Label($"Current island: {OnlineIslandManager.CurIsland}");
GUILayout.BeginHorizontal();
if (GUILayout.Button("< Prev")) OnlineIslandManager.TpToNextIsland(true);
if (GUILayout.Button("Next >")) OnlineIslandManager.TpToNextIsland(false);
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUILayout.Label("Go to", GUILayout.Width(44f));
_field = GUILayout.TextField(_field, GUILayout.Width(48f));
if (GUILayout.Button("Teleport")) GoTo();
GUILayout.EndHorizontal();
if (GUILayout.Button("Unlock all islands"))
{
mgr.UnlockIsland(byte.MaxValue);
ModLog.Info("All islands unlocked.");
}
}
private void GoTo()
{
if (!byte.TryParse(_field, NumberStyles.Integer, CultureInfo.InvariantCulture, out byte index))
{
ModLog.Warning($"\"{_field}\" is not an island number (0-255).");
return;
}
OnlineIslandManager.TpToSpecificIsland(index);
ModLog.Info($"Teleported to island {index}.");
}
}
}
The host check is done by reading OnlineIslandManager.Instance.IsServerInitialized and swapping the whole panel for a one-line label, rather than showing buttons that silently do nothing on a client. It is the same “tell the player why nothing happened” habit the money cheat started, moved into the UI.
Prev and Next are safe on any index because NextIslandInBuild wraps. The typed box is not: a number past the real island count hands the server an index it may not have, so if you want it bulletproof, clamp _field against IslandManager.TotalIslands.

The Menu Needs Tabs
That is cheat number eight, and the flat window has become a problem. Every cheat drew straight into one list:
foreach (var cheat in _cheats)
{
GUILayout.Space(6f);
cheat.Draw();
}
Fine for two cheats. At eight, with the aimbot’s four sliders and the item spawner’s scrolling list, the window is taller than the screen and everything is jumbled together: a money box next to an achievements button next to an aimbot next to an island teleporter. It needs sorting, and the natural axis is what each cheat acts on. Player things, world things, weapon things.
One line per cheat
The whole grouping is a single new member on the base class. Cheat.cs gets a category:
internal enum Category
{
Player,
World,
Weapon,
}
internal abstract class Cheat
{
internal abstract string Label { get; }
// the tab this cheat lives under
internal virtual Category Category => Category.Player;
// ...rest unchanged...
}
virtual with a default, the same call as Draw back in post three: a cheat that does not care compiles anyway, and one that does overrides a single line. Then every cheat declares where it belongs, right under its Label:
internal override string Label => "Aimbot";
internal override Category Category => Category.Weapon;
That is the entire per-cheat cost. The eight sort out as:
- Player: Add money, Unlock achievements
- World: Dump entities, Spawn item, Islands
- Weapon: Aimbot, No recoil, Weapon tweaks
The enum order is deliberate: it is the order the tabs appear in, so reordering the tabs later is reordering the enum, nothing else.
The window draws tabs
CheatMenu does the rest, and it never needs touching again when you add a cheat, exactly as before. It figures out which tabs exist, draws a tab row, and shows only the selected tab’s cheats.
Menu/CheatMenu.cs:
using System;
using System.Collections.Generic;
using UnityEngine;
namespace HtFMod
{
internal sealed class CheatMenu
{
private const int WindowId = 0x48744621;
private const float BodyHeight = 260f;
private readonly IList<Cheat> _cheats;
private readonly Category[] _tabs;
private readonly string[] _tabLabels;
private Rect _window = new Rect(40f, 40f, 320f, 0f);
private int _tab;
private Vector2 _scroll;
private GUIStyle _status;
private static Texture2D _line;
internal CheatMenu(IList<Cheat> cheats)
{
_cheats = cheats;
// one tab per category that actually has a cheat, kept in enum order
var present = new List<Category>();
foreach (Category c in Enum.GetValues(typeof(Category)))
foreach (var cheat in cheats)
if (cheat.Category == c) { present.Add(c); break; }
_tabs = present.ToArray();
_tabLabels = new string[_tabs.Length];
for (int i = 0; i < _tabs.Length; i++)
_tabLabels[i] = _tabs[i].ToString();
}
internal void Draw()
{
_window = GUILayout.Window(WindowId, _window, DrawWindow, "HtF Mod");
}
private void DrawWindow(int id)
{
EnsureStyles();
var mm = MoneyManager.Instance;
var player = ModState.LocalPlayer;
string who = player != null ? player.name : "<none>";
string host = mm != null && mm.IsServerInitialized ? "yes" : "no";
GUILayout.Label($"Player: {who} Host: {host}", _status);
GUILayout.Space(4f);
_tab = GUILayout.Toolbar(_tab, _tabLabels);
if (_tab >= _tabs.Length) _tab = 0; // list changed under us on a reload
GUILayout.Space(6f);
var selected = _tabs[_tab];
_scroll = GUILayout.BeginScrollView(_scroll, GUILayout.Height(BodyHeight));
bool first = true;
foreach (var cheat in _cheats)
{
if (cheat.Category != selected) continue;
if (!first) { GUILayout.Space(8f); Separator(); GUILayout.Space(8f); }
first = false;
cheat.Draw();
}
GUILayout.EndScrollView();
GUI.DragWindow();
}
private void EnsureStyles()
{
if (_status != null) return;
_status = new GUIStyle(GUI.skin.label) { fontSize = 11 };
_status.normal.textColor = new Color(0.7f, 0.7f, 0.7f);
_line = new Texture2D(1, 1);
_line.SetPixel(0, 0, new Color(1f, 1f, 1f, 0.15f));
_line.Apply();
_line.hideFlags = HideFlags.HideAndDontSave;
}
private static void Separator()
{
var r = GUILayoutUtility.GetRect(1f, 1f, GUILayout.ExpandWidth(true));
GUI.DrawTexture(r, _line);
}
}
}
The pieces worth calling out:
GUILayout.Toolbar is IMGUI’s built-in tab row. Hand it the current index and an array of labels, and it returns the index the player clicked. That one call is the whole tab bar; there is no per-tab button wiring, the same way GUILayout.Button was a whole button in one line back in post three.
Tabs are built from the cheats, not hard-coded. The constructor walks the Category enum and keeps only the categories that have at least one cheat. Add a fourth category later, put one cheat in it, and a fourth tab appears on its own. Remove the last cheat from a category and its tab quietly vanishes. CheatMenu still knows nothing about any specific cheat, which was the whole point of the class from post three.
if (_tab >= _tabs.Length) _tab = 0 is a hot-reload guard. Reload with tab 2 selected, and if the new build has fewer tabs the stored index is now out of range and the array lookup would throw. Clamping it costs one line and one comment.
The rest is theming. A dimmer, smaller status line at the top; a scroll view capped at 260px so the item-spawner tab scrolls instead of running off the window; and a faint 1px Separator between cheats drawn as a stretched single-pixel texture, so a busy tab reads as separate blocks. IMGUI styling is verbose but it is all just GUIStyle colours and a texture, built once in EnsureStyles because GUI.skin only exists inside OnGUI.
No headings above each cheat, on purpose. Every cheat’s Draw already prints its own label as a button or a toggle, so a heading would just say “Aimbot” twice.

Where the Code Is
HtFMod/
Cheats/
Cheat.cs + Category enum, virtual Category property
IslandCheat.cs new
MoneyCheat.cs + Category
AchievementCheat.cs + Category
EntityScanCheat.cs + Category
ItemSpawnCheat.cs + Category
AimbotCheat.cs + Category
NoRecoilCheat.cs + Category
WeaponTweaksCheat.cs + Category
Menu/
CheatMenu.cs tabs, scroll view, theming
Plugin.cs + IslandCheat in the list
Adding a cheat is still one file and one line in Plugin.Awake. It now also picks a tab in one more line, and if that line is a new category, the tab bar grows to fit it without any other change.
Next
The mod has money, achievements, an aimbot, no-recoil, weapon tweaks, item spawning, island teleport, and a menu that can hold all of them. What it still cannot do is see: every cheat so far acts on the player or the thing in front of them, but nothing draws the world back. The next post is the ESP, and the two things it needs, CamPosNoEffects for a jitter-free anchor and Entities.CollectAliveCreatures for the list, have been sitting ready since post four.