In the last blog post we built an aimbot cheat using the game’s own aim assist. In this blog post we will be spawning items in the world.
In order to do this we will need to answer the following questions:
- How does the game spawn an item?
- What items exist to spawn?
- Where do we put it?

How the Game Spawns an Item
Searching Spawn in dnSpy turns up a class called ItemSpawner. It is a MonoBehaviour the level designers drop into the scene, one per spawn point, and it is the best possible thing to find: a complete, shipped, working example of the exact call we want to make.
public class ItemSpawner : MonoBehaviour
{
public void Start()
{
if (!Server.Instance || !Server.Instance.IsServerInitialized)
{
return;
}
base.StartCoroutine(this.SpawnItem());
}
private IEnumerator SpawnItem()
{
if (!this._itemToSpawn)
{
yield break;
}
yield return new WaitUntil(() => ItemManager.Instance);
while ((!this._onlySpawnOnce || !this._hasSpawned) && this._isActive)
{
if (!this._spawnInstant || this._hasSpawned)
{
yield return new WaitForSeconds(this._spawnDelay);
}
this.CheckSpawnedItems();
int num = 0;
while (num < this._spawnCount && this._itemsSpawned.Count < this._maxItemsSpawned)
{
Vector3 vector = SpawnUtils.GetRandomSpawnPoint(this._spawnBox, this._spawnInAir);
if (!(vector == Vector3.zero))
{
vector += Vector3.up * this._itemToSpawn.ModelHeight;
Quaternion rot = this._useRandomRotation ? Quaternion.Euler(0f, (float)Random.Range(0, 360), 0f) : base.transform.rotation;
Item item = ItemManager.Instance.SpawnNewItem(this._itemToSpawn, vector, rot);
this._itemsSpawned.Add(item);
if (item.Radio)
{
item.Radio.SetStartFrequency((float)this._startFrequencyForRadio);
}
}
num++;
}
this._hasSpawned = true;
}
}
private void CheckSpawnedItems()
{
for (int i = this._itemsSpawned.Count - 1; i >= 0; i--)
{
if (!this._itemsSpawned[i] || !this._itemsSpawned[i].gameObject.activeInHierarchy || this._itemsSpawned[i].Holder || (this._itemsSpawned[i].Creature && this._itemsSpawned[i].Creature.IsDead && !this._spawnsDeadCreature))
{
this._itemsSpawned.RemoveAt(i);
}
}
}
[SerializeField] private Item _itemToSpawn;
[SerializeField] private bool _useRandomRotation = true;
[SerializeField] private int _maxItemsSpawned = 10;
[SerializeField] private float _spawnDelay = 10f;
[SerializeField] [Range(1f, 25f)] private int _spawnCount = 1;
[SerializeField] private BoxCollider _spawnBox;
// ...
}
Most of that is scheduling: a delay between waves, a cap on how many are alive at once, a once-only flag. Strip it out and the spawn itself is three lines, and each one is something we copy.
The gate. Start returns on line one unless Server.Instance.IsServerInitialized. That is the money rule from post one wearing a different class name. Spawning a networked object is the server’s job, the client only gets told about it afterwards, and the game does not even start the coroutine on a client. So: host the lobby, or this whole post does nothing. We will check the same thing the same way, and for the same reason as GiveMoney, purely so the chat says why nothing happened.
The lift. vector += Vector3.up * this._itemToSpawn.ModelHeight. The spawn point is a ground point, and a model placed with its origin on the ground is half inside it. ModelHeight is a per-prefab number the developers filled in so the spawner can raise the item clear of the terrain before physics gets a say. We have no way of knowing that number ourselves, and we do not have to.
The call. ItemManager.Instance.SpawnNewItem(prefab, position, rotation) returns the spawned Item. Public, on the same ItemManager whose Items dictionary post four read. That is the whole spawn.
What There Is to Spawn
Each ItemSpawner points at a single prefab set by hand in the editor, and ItemManager.Items only holds the instances already in the world. There is no list of what could be spawned, so we build one from the prefab assets themselves:
foreach (var item in Resources.FindObjectsOfTypeAll<Item>())
if (!item.gameObject.scene.IsValid())
_prefabs.Add(item);
Resources.FindObjectsOfTypeAll returns every loaded Item, assets included, where FindObjectsOfType only walks the scene. That means it also returns the instances, and scene.IsValid() is how you tell them apart: an instantiated object lives in a scene, a prefab asset does not. It is the most expensive lookup in this series, and it is fine here for the reason post four’s scan was not: it runs once, on a button, and the result is cached. It also only sees prefabs Unity has already loaded, so the list can differ between areas, which is what the Refresh button is for.
Where to Put It
Question three was answered in post four. CameraAccess.Component() hands back the PlayerCamera, and CamTransform is where the camera is and which way it faces. Two metres along forward, raised by ModelHeight exactly as the spawner does, is in front of the player and clear of the ground:
var t = cam.CamTransform;
var pos = t.position + t.forward * 2f + Vector3.up * prefab.ModelHeight;
When spawning more than one, a random offset stops them all landing on the same point and exploding apart. The game uses a box collider for that; Random.insideUnitSphere * 1.5f does the same job without needing one.
The Cheat
One new file, and it is the busiest Draw override in the mod so far. Every cheat until now was a button, or a button and a box. This one is a search box, a count box, a refresh button and a scrolling list, and the list is the interesting part: it is the first thing the menu has drawn that does not fit on the screen.
The file has four parts. State, the menu, the scan, and the spawn.
State
internal sealed class ItemSpawnCheat : Cheat
{
private const float ListHeight = 220f;
// prefab assets, found once and cached. Refresh rebuilds it.
private readonly List<Item> _prefabs = new List<Item>();
private readonly HashSet<string> _seen = new HashSet<string>();
private string _search = "";
private string _countField = "1";
private Vector2 _scroll;
internal override string Label => "Spawn item";
// no hotkey. The cheat is the list, there is nothing to fire blind.
internal override void Activate() => Refresh();
_prefabs is the cache. It is filled by the scan in the next section and read by the menu every frame, which is the whole point of caching it: the scan is expensive and the menu is drawn sixty times a second, so they cannot be the same operation. _seen is scratch for the scan, kept as a field so it is not allocated on every refresh.
The two strings are IMGUI text boxes, and they are strings for the reason post three gave: the box is free text until the moment it is used, so _countField holds whatever the player has typed, including nothing, and is parsed on the click.
_scroll is new. IMGUI keeps no state between frames, so the scroll position of a list is our job to remember. BeginScrollView takes the position in and hands the updated one back, exactly the way GUILayout.Window handed the dragged rectangle back in post three.
Then the two overrides. Label is what every cheat has. Hotkey is deliberately left at the base class default of KeyCode.None, and this is the first cheat where that is the right answer rather than laziness. F5 pays out money because there is one thing to pay. There is no one thing to spawn, so a key would have nothing to do. Activate still has to exist because the base class demands it, and refreshing the list is the most useful thing it can mean.
The menu
internal override void Draw()
{
if (_prefabs.Count == 0) Refresh();
GUILayout.BeginHorizontal();
GUILayout.Label("Search", GUILayout.Width(58f));
_search = GUILayout.TextField(_search);
GUILayout.Label("x", GUILayout.Width(12f));
_countField = GUILayout.TextField(_countField, GUILayout.Width(36f));
if (GUILayout.Button("Refresh", GUILayout.Width(62f))) Refresh();
GUILayout.EndHorizontal();
// the window auto-fits its height, so the list needs a fixed one or it never scrolls
_scroll = GUILayout.BeginScrollView(_scroll, GUILayout.Height(ListHeight));
foreach (var prefab in _prefabs)
{
if (_search.Length > 0 &&
prefab.name.IndexOf(_search, StringComparison.OrdinalIgnoreCase) < 0) continue;
if (GUILayout.Button(prefab.name, GUI.skin.label)) Spawn(prefab);
}
GUILayout.EndScrollView();
}
The first line fills the cache the first time the menu is drawn.
The horizontal row is the same shape as MoneyCheat’s amount row, just wider. Nothing in it spawns anything. The search box narrows the list, the count box says how many, and Refresh rebuilds the cache. All three are read on the next frame by the list below them.
Every entry is a button drawn as a label. GUILayout.Button with GUI.skin.label as the style keeps the click behavior and drops the grey box, so two hundred prefab names read as a list rather than a wall of buttons. The filter is a plain substring match, case-insensitive, and it runs against every entry every frame. That is fine: it is a couple of hundred string comparisons, which is nothing next to what the scan cost once.
To spawn an item/creature we just have to click the dropdown text.
The scan
private void Refresh()
{
_prefabs.Clear();
_seen.Clear();
// walks every loaded object in the process: once, on a click, never in Update
foreach (var item in Resources.FindObjectsOfTypeAll<Item>())
{
if (item.gameObject.scene.IsValid()) continue; // in a scene = instance, not a prefab
if (!_seen.Add(item.name)) continue; // one entry per name
_prefabs.Add(item);
}
_prefabs.Sort((a, b) => string.CompareOrdinal(a.name, b.name));
ModLog.Raw($"{_prefabs.Count} item prefabs loaded");
}
This is the three-line scan from the previous section with two things added. _seen collapses duplicates, because the same prefab can turn up more than once in a full object walk and one Shark in the list is enough.
It logs to Raw rather than Info, because “213 item prefabs loaded” is a diagnostic line and does not belong in the player’s chat.
The spawn
private void Spawn(Item prefab)
{
if (!int.TryParse(_countField, NumberStyles.Integer, CultureInfo.InvariantCulture, out int count) || count < 1)
{
ModLog.Warning($"\"{_countField}\" is not a whole number.");
return;
}
var cam = CameraAccess.Component();
if (cam == null) { ModLog.Warning("No local player."); return; }
if (ItemManager.Instance == null) { ModLog.Warning("ItemManager.Instance is null (not in a game?)"); return; }
// the same gate ItemSpawner.Start uses: spawning is the server's job
if (!Server.Instance || !Server.Instance.IsServerInitialized)
{
ModLog.Warning("Not the server, SpawnNewItem will no-op. Host the lobby.");
return;
}
var t = cam.CamTransform;
int spawned = 0;
for (int i = 0; i < count; i++)
{
var scatter = count > 1 ? UnityEngine.Random.insideUnitSphere * 1.5f : Vector3.zero;
// two metres ahead, lifted by ModelHeight exactly as ItemSpawner does
var pos = t.position + t.forward * 2f + Vector3.up * prefab.ModelHeight + scatter;
var rot = Quaternion.Euler(0f, UnityEngine.Random.Range(0, 360), 0f);
if (ItemManager.Instance.SpawnNewItem(prefab, pos, rot) != null) spawned++;
}
ModLog.Info($"Spawned {spawned}x {prefab.name}");
}
}
The top half is guards, and they are the money guards in the money order. Parse the count first, because a bad count is the player’s typo and the cheapest thing to reject. Then no player, no manager, not the host. The third one asks Server.Instance rather than MoneyManager.Instance because that is the class ItemSpawner.Start asked, and copying the game’s own check is safer than guessing at an equivalent. SpawnNewItem presumably refuses on a client anyway. The guard is there so the chat says why.
The bottom half is ItemSpawner’s inner loop with the box collider swapped for the camera. Read the two side by side:
// ItemSpawner
vector = SpawnUtils.GetRandomSpawnPoint(this._spawnBox, this._spawnInAir);
vector += Vector3.up * this._itemToSpawn.ModelHeight;
rot = Quaternion.Euler(0f, (float)Random.Range(0, 360), 0f);
ItemManager.Instance.SpawnNewItem(this._itemToSpawn, vector, rot);
// ours
pos = t.position + t.forward * 2f + Vector3.up * prefab.ModelHeight + scatter;
rot = Quaternion.Euler(0f, UnityEngine.Random.Range(0, 360), 0f);
ItemManager.Instance.SpawnNewItem(prefab, pos, rot);
Same lift, same spin, same call. The only line that is ours is where the point comes from, and that is CameraAccess from post four doing what it was written for.
scatter only applies when spawning more than one. Ten sharks on the same point are ten colliders overlapping, and Unity’s answer to that is to fling them apart, which is loud and occasionally launches one into orbit. A random offset inside a 1.5 metre sphere gives physics nothing to resolve.
UnityEngine.Random is spelled out because using System brings System.Random into scope as well, and the compiler will not choose for you.
The whole file
Cheats/ItemSpawnCheat.cs:
using System;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;
namespace HtFMod
{
internal sealed class ItemSpawnCheat : Cheat
{
private const float ListHeight = 220f;
// prefab assets, found once and cached. Refresh rebuilds it.
private readonly List<Item> _prefabs = new List<Item>();
private readonly HashSet<string> _seen = new HashSet<string>();
private string _search = "";
private string _countField = "1";
private Vector2 _scroll;
internal override string Label => "Spawn item";
// no hotkey. The cheat is the list, there is nothing to fire blind.
internal override void Activate() => Refresh();
internal override void Draw()
{
if (_prefabs.Count == 0) Refresh();
GUILayout.BeginHorizontal();
GUILayout.Label("Search", GUILayout.Width(58f));
_search = GUILayout.TextField(_search);
GUILayout.Label("x", GUILayout.Width(12f));
_countField = GUILayout.TextField(_countField, GUILayout.Width(36f));
if (GUILayout.Button("Refresh", GUILayout.Width(62f))) Refresh();
GUILayout.EndHorizontal();
// the window auto-fits its height, so the list needs a fixed one or it never scrolls
_scroll = GUILayout.BeginScrollView(_scroll, GUILayout.Height(ListHeight));
foreach (var prefab in _prefabs)
{
if (_search.Length > 0 &&
prefab.name.IndexOf(_search, StringComparison.OrdinalIgnoreCase) < 0) continue;
if (GUILayout.Button(prefab.name, GUI.skin.label)) Spawn(prefab);
}
GUILayout.EndScrollView();
}
private void Refresh()
{
_prefabs.Clear();
_seen.Clear();
// walks every loaded object in the process: once, on a click, never in Update
foreach (var item in Resources.FindObjectsOfTypeAll<Item>())
{
if (item.gameObject.scene.IsValid()) continue; // in a scene = instance, not a prefab
if (!_seen.Add(item.name)) continue; // one entry per name
_prefabs.Add(item);
}
_prefabs.Sort((a, b) => string.CompareOrdinal(a.name, b.name));
ModLog.Raw($"{_prefabs.Count} item prefabs loaded");
}
private void Spawn(Item prefab)
{
if (!int.TryParse(_countField, NumberStyles.Integer, CultureInfo.InvariantCulture, out int count) || count < 1)
{
ModLog.Warning($"\"{_countField}\" is not a whole number.");
return;
}
var cam = CameraAccess.Component();
if (cam == null) { ModLog.Warning("No local player."); return; }
if (ItemManager.Instance == null) { ModLog.Warning("ItemManager.Instance is null (not in a game?)"); return; }
// the same gate ItemSpawner.Start uses: spawning is the server's job
if (!Server.Instance || !Server.Instance.IsServerInitialized)
{
ModLog.Warning("Not the server, SpawnNewItem will no-op. Host the lobby.");
return;
}
var t = cam.CamTransform;
int spawned = 0;
for (int i = 0; i < count; i++)
{
var scatter = count > 1 ? UnityEngine.Random.insideUnitSphere * 1.5f : Vector3.zero;
// two metres ahead, lifted by ModelHeight exactly as ItemSpawner does
var pos = t.position + t.forward * 2f + Vector3.up * prefab.ModelHeight + scatter;
var rot = Quaternion.Euler(0f, UnityEngine.Random.Range(0, 360), 0f);
if (ItemManager.Instance.SpawnNewItem(prefab, pos, rot) != null) spawned++;
}
ModLog.Info($"Spawned {spawned}x {prefab.name}");
}
}
}
Plugin.cs
One line:
_cheats = new List<Cheat>
{
new MoneyCheat(),
new AchievementCheat(),
new EntityScanCheat(),
new AimbotCheat(),
new ItemSpawnCheat(),
};
CheatMenu did not change, Cheat did not change, Plugin.Update did not change. Post two’s refactor keeps paying.
Trying It
Host a lobby, open the menu, and the list fills. Type a few letters and click:

They are real items. They can be picked up, sold, thrown and, since creatures come through the same path, they can also flop around and die. CheckSpawnedItems culling dead creatures is the game’s own admission that this happens.
What a Reload Does and Does Not Undo
Same rules as post three. The prefab cache is a field on the cheat, so a hot reload starts with an empty list and the next menu open rebuilds it. Nothing to recover in Awake.
Everything you spawned stays spawned. Those objects belong to the server now, registered in ItemManager.Items alongside everything the level placed, and unloading the mod does not un-spawn them any more than it un-adds money. There is no OnUnload on this cheat because there is nothing to put back.
Where the Code Is
HtFMod/
Plugin.cs + ItemSpawnCheat in the list
Cheats/
ItemSpawnCheat.cs new
Two files touched. Everything else the post needed, CameraAccess, the menu, the guards, the log, was already there.
Next
The lesson this time is a narrower one than post five’s. When you want to do something the game does, find the place the game does it and read the caller, not just the callee. SpawnNewItem is one line, and on its own it tells you nothing about the host gate, the height lift, or the random spin. ItemSpawner is the developers’ own worked example of calling it correctly, and copying three lines out of it is the entire cheat.
In the next post we will take a look at the Weapon class.