Lua safety and limits¶
Enabled mods can change your world and inventories. They must not get access to your computer (files, programs, or system libraries).
Allowed¶
- Functions on your script such as
initialize,use, andhitBlock Worldhelpers (spawn, query tiles, explosions, time)Utilshelpers- The documented object helpers:
self.item,self.armor,self.player,self.creature,self.InWorldStructure,self.flora, andself.projectile - The documented types and values, including query views, opaque handles, vectors, and callback timing values
requireof scripts under the game's Lua folders and your own mod
Lua modules¶
require("helpers") first looks for helpers.lua at your pack root, beside
modinfo.json, then beside the importing script. Both locations must stay
inside your pack. Dotted names select subdirectories:
require("Tools.helpers") looks for Tools/helpers.lua in the same order.
Existing namespace folders and pack-relative slash paths remain valid;
helper names do not need a mod-specific prefix. A missing helper produces
an error instead of searching another mod's files.
Each pack has its own cache of successfully imported helper files. Requiring
the same file again within that pack returns the same value. Two packs can
each have a helpers.lua without sharing that helper's returned table.
This private pack cache is separate from Lua's package.loaded. Helpers
that import other helpers keep the same pack as their owner and use their
own directory as the fallback. Calls to require in later gameplay
callbacks also retain the script's pack and directory.
loadfile("Tools/helpers.lua") uses the same search order but treats its
argument as a literal filename, preserving .lua and dots in filenames.
It returns a fresh compiled chunk each time, rather than a cached helper
value. The chunk keeps its owning pack and file directory when you run it.
Return a table from each helper module and keep imports local:
local helpers = require("helpers")
Scripts in your pack share that cached helper table. Use the entity script's own table for state that belongs to one item or creature. Restart or fully reload game content to recreate the Lua state after editing a loaded helper.
The game's Content/Lua/... modules and available standard Lua libraries
stay shared, along with the Lua interpreter, globals, and engine helpers
such as World. Module ownership does not isolate global variables. Use
local variables to avoid changing another script's globals. There is no
supported API for importing another installed mod's private modules.
Entity identity and targeting¶
Entity views, players,
creatures, and flora share an Id for the
same runtime object. Compare these strings or use them as Lua table keys.
Different wrappers around one entity need not compare equal. IDs are local to
the running game: do not save them or use them as multiplayer identifiers.
TypeId identifies the content definition instead, and HasTag checks an exact
authored tag.
Check IsValid when retaining a value across callbacks. Detached, removed,
replaced, and previous-world objects are invalid. A retained value stays non-nil
after removal. IsDead reports the engine death flag; a living entity can still
be present while Dying. Player health and maximum health are read-only.
Inside an equipped-item callback, this selects a living mob while excluding
the wearer. The mobs category includes human NPCs, so add a content or tag
filter when your mechanic needs a narrower target:
if self.player ~= nil and self.player.IsValid then
local target = World:findClosestTarget(
self.player.CurGlobalCenter, 300, true, self.player, "mobs")
if target ~= nil and target.IsValid then
self.lastTargetId = target.Id
self.lastTargetHealth = target.Health
end
end
The optional trailing arguments are targetKind, typeId, and tag. Kinds
are all (default), players, and mobs; players means actual players.
Type IDs and tags are case-sensitive; nil leaves a filter unset. Both closest
and random searches apply every supplied filter and skip dead/dying targets.
For a pack creature, self.creature:FindTarget(300, "mobs") uses filtered
targeting with line of sight and excludes the searching creature itself. It
can find the same or a different species. Supply typeId, tag, and
ignoreEntity after the kind to narrow the result further. The original
one-argument form retains the engine's legacy player-priority targeting and
same-species exclusion.
Applying and cancelling a status effect¶
Use the exact registered StatusEffectTypeString for a custom effect
(pack.identifier), or a built-in enum name such as JUMP_HEIGHT.
Creating a status handle does not attach it:
if self.player ~= nil then
local effect = World:createStatusEffect("JUMP_HEIGHT", 0.1, 8000, self.player)
if effect ~= nil then
self.player:AddStatusEffect(effect)
self.jumpEffect = effect
end
end
On unequip, cancel the retained application:
if self.jumpEffect ~= nil then
self.jumpEffect:Cancel()
self.jumpEffect = nil
end
Read TypeId, Intensity, Duration (remaining milliseconds), IsActive, and
IsExpired from the handle. A fresh unattached effect is inactive and unexpired.
IsActive means attached and unexpired; a stronger non-stacking effect can
suppress its application while it remains active. An effect removed by
another operation can be inactive without being expired.
Attach to its original target. When applications merge, the handle tracks the
retained effect; cancelling it can remove the shared merged application. An old
handle cannot cancel a later replacement. Current-frame modifiers settle on
the next status update. Custom effect callbacks can use target:Heal(amount)
to heal a Player or EntityView, clamped to maximum health without reviving a
dead, dying, or zero-health target. These operations affect the local simulation.
Structure interaction context¶
Override Structure:use(player) to inspect the interacting Player.
It runs in single-player or on the multiplayer host. Client requests pass the
requester's host-side Player proxy, selected from the transport sender. Clients
do not run use when receiving the resulting native behavior. Normal player
interaction supplies a proxy; actorless engine calls pass nil. Older use()
overrides can ignore the extra argument.
Multiplayer execution context¶
World:isHost() is true in offline play and on the session host, false on a
remote client, and nil without a world host. player.IsLocalPlayer checks local
control; self.item.IsLocallyEquipped checks the local player's displayed held
item, including an active displacing quick slot. A displaced hotbar item is inactive.
Use these checks in callbacks that can also run for remote representations.
-- Initiate from an equipped item callback on its controlling instance.
if self.item.IsLocallyEquipped then
self:performActionNetworked(1)
end
The HeldItem helper invokes performAction(index) locally once, then sends it.
The host invokes it for the sender's current matching item and relays it to the
other clients, excluding the sender. Host-originated actions reach both clients.
Stale item IDs are ignored after a weapon switch. Calling the lower-level
self.item:PerformActionNetworked(index) only sends; it does not invoke locally.
Remote, holstered, and world items cannot originate this action path.
The callback runs on every participating instance. Gate shared world changes
with World:isHost() and owner-only input with IsLocallyEquipped. Replicating
the callback does not automatically replicate arbitrary Lua fields, custom
projectile changes, or every world operation it calls.
Customizing a fired projectile¶
Item:FireProjectile returns a Projectile
handle, or nil if firing is unavailable. In an equipped-item attack callback:
if self.player ~= nil then
local position = self.player.CurGlobalCenter
local shot = self.item:FireProjectile(
"ROUND_9MM", 8, position.X, position.Y, 1, 0)
if shot ~= nil then
local charge = math.max(0, math.min(1, self.charge or 0))
shot.MinDamage = 10 + 30 * charge
shot.MaxDamage = shot.MinDamage
shot.HarmsPlayers = false
shot.HarmsMobs = true
shot.HarmsOwner = false
end
end
The example fires right; supply your weapon's direction components for aiming. Initialization has finished when the handle returns, and the projectile has not yet taken its first simulation update. Projectile callbacks can change these properties again, for example to reduce damage after a ricochet. Damage must be finite and nonnegative; negative and non-finite values are ignored. Setting the minimum above the maximum raises the maximum; setting the maximum below the minimum lowers the minimum.
Collision flags govern automatic hits, including sticky attachment. Hitting
the owner requires its category flag and HarmsOwner; other native exclusions
still apply. HarmsItems covers world items that accept projectile collisions.
These flags do not control terrain collisions or explicit DealDamage calls.
Firing and property changes affect the local simulation; this API does not
automatically replicate a custom shot or its later changes to other peers.
Blocked¶
Scripts cannot:
- Read or write files, start programs, or use debugging tools
- Reach into undocumented game internals
- Load extra Lua files from arbitrary folders
- Load extra system libraries
- Reach around the helpers into hidden game objects
- Use CLR reflection (
GetType) or static members through an object helper - Read or change the native metatable of an object helper or error object
Blocked host libraries are removed from the module cache too, so require
cannot restore io, os, debug, or CLR interop. Object helpers expose only
their documented instance members. Unknown objects and errors remain opaque;
they cannot provide a route into hidden game objects.
Combat callbacks use the documented views and proxies. For example,
meleeStruckDeposit receives an item helper for the deposit,
with no access to its map. Query collections
are read-only and use zero-based indexing. Lists expose Count; arrays expose
Length. A return type ending in |nil means you must check for nil before
using its members. An OwnerToken only carries an
owner identity back to the API; it does not expose hidden entity members.
Vector2 positions and GameTime callback timing values remain available for normal gameplay scripts. Vectors returned from properties are value copies; changing a coordinate on that copy does not move the original entity. Where the property is writable, assign the whole vector back to change it. Consult each member's documented units: world positions use pixels, tile coordinates use tile indexes, and durations may use seconds or milliseconds.
Mod Prep Validate flags many of these as errors before you publish. The game also blocks them while the mod is running. There is no “trusted mod” override.
Your saves are still exposed¶
The sandbox stops a script from using your computer. It does not stop a script from spawning items, dealing damage, or destroying terrain — those commands exist so gameplay mods can work. Only enable mods you trust with your saves.