Player
Find out where the player is standing, so you can put things near them
What you can do after this page
- Find out where the player is standing right now
- Drop a pal at the player's feet, or a few metres away
- Place several things in a ring around the player
- Measure how far the player is from a building
- Hold off until the player is really in a world before you do any of it
Where the player is
Player gives you three functions. Call them whenever you need to know where the player is.
local me = Player.character() -- APalPlayerCharacter, or nil
local at = Player.coordinate() -- a Coord in centimetres, or nil
local up = Player.coordinateOffset(0, 0, 200) -- the same point, 2 m higher, or nilAll three hand back nil when the player is not in a world, so check the value before you
use it.
| Function | Returns | nil when |
|---|---|---|
Player.character() | the local APalPlayerCharacter | there is no valid player pawn |
Player.coordinate() | a Coord — x, y, z in centimetres | no pawn, or the location read failed |
Player.coordinateOffset(dx, dy, dz) | a Coord, offset from the one above | the same conditions as coordinate() |
You can reach the functions two ways. Both are the same table:
local api = require("palforge.api")
api.Player.coordinate() -- namespaced access
Player.coordinate() -- the installed global; the same tableA position comes back as a Coord, a small table of three numbers — see
The Coord shape.
character
Player.character() gives you the player's character in the world — the pawn, in Unreal's
words, meaning the actor the player walks around as. It looks the pawn up with
FindFirstOf("PalPlayerCharacter"), checks IsValid() on it, and returns it. Anything that
fails on the way gives nil instead: the lookup runs inside a pcall, so a lookup that
throws comes back as nil rather than as an error.
local me = Player.character()
if me then
Effect.get("example:Regen"):apply(me) -- the effect is defined elsewhere in the pack
endFetch it where you use it. The lookup runs on every call and nothing is cached, and the pawn is built again every time a world loads — so a character you stored in a variable at startup points at something that is gone.
local event = require("palforge.core.event")
-- wrong: captured once, at mod load, when there is no world at all
local me = Player.character()
event.on("tick", function() Effect.get("example:Regen"):apply(me) end)
-- right: re-read per use, and guard
event.every(5000, function()
local me = Player.character()
if me then Effect.get("example:Regen"):apply(me) end
end)What you get back is the game's own actor, not a PalForge object. Anything the game puts on
it is reachable — but those calls are outside this API, so keep them in a pcall (the
"in front of the player" recipe below does exactly that).
coordinate
Player.coordinate() finds the pawn and reads its position. It prefers
K2_GetActorLocation and falls back to GetActorLocation. No pawn, or a read that throws,
gives nil.
The table you get back carries the same three numbers under three key styles, so it drops straight into Lua code, into UE-style code, and into helpers that take positions in order:
local c = Player.coordinate()
if c then
print(c.x, c.y, c.z) -- lowercase, the documented Coord shape
print(c.X, c.Y, c.Z) -- UE-style, same numbers
print(c[1], c[2], c[3]) -- array form, same numbers
endIt is a snapshot, not a live view: a fresh table is built on each call and it does not follow the player. Call it again to re-read.
coordinateOffset
Player.coordinateOffset(dx, dy, dz) is the player's position with the offset added to each
axis — the quick way to say "just over there". It returns nil under exactly the same
conditions as coordinate(), and each argument you leave out counts as 0.
Player.coordinateOffset(300, 0, 0) -- 3 m along +X
Player.coordinateOffset(0, 0, 200) -- 2 m straight up
Player.coordinateOffset(nil, nil, 50) -- only lift by 50 cmUnits are centimetres, so 100 is one metre — the same unit the building grid uses
(core.spatial.GRID_CM is 100, one cell per metre). z is up.
The offset is applied on world axes, not on the player's own. It knows nothing about which
way the player is facing; coordinateOffset(600, 0, 0) is 6 m along +X from the player
whether they are looking at it or away from it. For a point in front of them, see the recipe
below.
The Coord shape
A Coord is a table of three numbers in centimetres. That is the whole shape:
Prop
Type
You can print the same thing from inside the game:
local schema = require("palforge.core.schema")
print(schema.help("Coord"))Coord {
x number (required) world X in centimetres
y number (required) world Y in centimetres
z number (required) world Z in centimetres
}Pal.Handle:spawn is the main thing that eats one, and it accepts both the named form and
the array form:
Pal.get("ChickenPal"):spawn(Player.coordinate()) -- named form, straight through
Pal.get("ChickenPal"):spawn{ at = { x = 12000, y = -3400, z = 500 } }
Pal.get("ChickenPal"):spawn{ at = { 12000, -3400, 500 } } -- array form:spawn tells a coordinate from an options table by looking for x or [1] on it, then
reads each axis as a.x or a[1]. Because coordinate() fills in both key styles, a value
from this module works in either position.
When it returns nil
All three functions return nil whenever there is no valid player pawn:
- at mod load, and on the title screen — no world has been entered yet
- during a load screen, while the pawn is being built
- after leaving a world, before entering the next one
nil is a normal return, not a failure: the FindFirstOf lookup and the location read are
each wrapped in a pcall, so a native call that throws also comes back as nil. That puts
the whole burden on you: check before you use the value.
Passing nil on is usually silent rather than loud. Effect.Handle:apply(target) keys its
application by target or GLOBAL, so apply(Player.character()) with no world applies the
effect to the global bucket instead of to the player, and reports success. Guard first.
To wait for a world, listen on the world.ready channel — the signal PalForge sends once the
player is in a loaded world:
local event = require("palforge.core.event")
local log = require("palforge.utils.log").scope("example")
event.on("world.ready", function()
local at = Player.coordinate()
if not at then return end
log.info(string.format("world ready at %.0f %.0f %.0f", at.x, at.y, at.z))
end)world.ready is driven by a poll of this very pawn: core/event checks
FindFirstOf("PalPlayerCharacter") about once a second and emits the channel after five
consecutive valid polls. So Player.character() can already return a pawn several seconds
before world.ready fires, and it can start returning nil again at any point afterwards.
The channel is a good place to start work; it is not a reason to drop the nil check.
Recipes
Spawn at the player's feet
local event = require("palforge.core.event")
local log = require("palforge.utils.log").scope("example")
---Spawn `charId` where the player stands, lifted 50 cm so it settles onto the ground
---instead of floating. Returns false when there is no world.
local function spawnHere(charId, level)
local at = Player.coordinateOffset(0, 0, 50)
if not at then
log.warn("no world yet - nothing spawned")
return false
end
return Pal.get(charId):spawn{ at = at, level = level or 5 } -- issued; arrives seconds later
end
event.on("world.ready", function()
spawnHere("ChickenPal", 5)
end)A coordinate spawn is not instant, and it is a spawn followed by a move. The game's own call drops the pal beside the player, and PalForge waits for it to appear and then relocates it onto your point — where it lands exactly. All of that takes four to eight seconds, so what you see is the pal turning up next to you and then moving.
:spawn returns before any of it has happened, so the boolean says the call was issued and
nothing more. See Pal.
It does not need the CheatManagerEnabler mod. core/spawn looks for a live
PalCheatManager, then for one on the player controller, and when the session has neither it
builds one itself: StaticConstructObject on the PalPlayerController's own CheatClass,
falling back to /Script/Pal.PalCheatManager and then /Script/Engine.CheatManager, attached to
the controller so it happens once per session rather than once per spawn. What a world spawn does
need is a player controller — no world loaded, or not connected yet, and it answers false
before reaching the game and warns that none could be built.
Spawn in front of the player
There is no ready-made "which way am I facing" helper. Take the forward vector off the character, flatten it to the XY plane and normalise it, then add it to the coordinate.
---The point `distanceCm` ahead of where the player is facing, or nil.
local function inFront(distanceCm)
local me = Player.character()
local at = Player.coordinate()
if not (me and at) then return nil end
local fwd
pcall(function() fwd = me:GetActorForwardVector() end)
local fx, fy = (fwd and fwd.X) or 1.0, (fwd and fwd.Y) or 0.0
local mag = math.sqrt(fx * fx + fy * fy)
if mag < 0.01 then fx, fy, mag = 1.0, 0.0, 1.0 end
fx, fy = fx / mag, fy / mag
return { x = at.x + fx * distanceCm, y = at.y + fy * distanceCm, z = at.z + 50 }
end
local at = inFront(600) -- 6 m ahead
if at then
Pal.get("ChickenPal"):spawn{ at = at, level = 10 } -- lands exactly here, seconds later
endGetActorForwardVector is a raw Unreal call on the pawn, not part of this API, so it stays
inside a pcall with a sensible fallback direction. Only the X and Y components are used:
normalising the full 3D vector would tilt the target up or down with the camera, and the pal
would spawn in the air or under the ground.
A ring of placements around the player
coordinateOffset is at its best when you need several points around one origin.
local ring = { { 400, 0 }, { -400, 0 }, { 0, 400 }, { 0, -400 } }
for _, d in ipairs(ring) do
local at = Player.coordinateOffset(d[1], d[2], 50)
if at then
Pal.get("SheepBall"):spawn{ at = at, level = 3 }
end
endGuard every call on its own, because coordinateOffset re-reads the pawn each time: if the
player leaves the world halfway through the loop, the remaining rounds get nil rather than
a stale position.
What the player is carrying
The local player's inventory is readable, and that read is the one item route on this build
measured end to end. Item.Handle:count() walks the PalUtility CDO to the player, to the
player state, to InventoryData, and asks the game's own CountItemNum: a loaded save answered
135 for Wood as a plain Lua number, with a real object printed at every step of that chain.
local event = require("palforge.core.event")
local log = require("palforge.utils.log").scope("example")
event.on("world.ready", function()
for _, id in ipairs({ "Wood", "Stone", "Berries" }) do
local n = Item.get(id):count()
log.info(string.format("%-8s %s", id, n and tostring(n) or "unknown"))
end
end)nil means unknown, never zero — no world, no player, or a read that would not answer. Guard
it exactly the way you guard Player.coordinate().
Reaching past the API and calling CountItemNum yourself is fine, but the id must be wrapped:
inv:CountItemNum(FName("Wood")) answers, while inv:CountItemNum("Wood") — a bare Lua string
where an FName is declared — faults inside UE4SS's argument marshalling and takes Palworld down,
and pcall never sees it. That happened once, on the line after the read that succeeded. It is
also why the 64-bit sibling CountItemNum64 is called nowhere in PalForge: it is in the class's
function list, its parameter list has never been read, and guessing at a declaration is what cost
the session.
Distance to something in the world
A live building carries pos, the same x, y, z-in-centimetres shape, so a plain distance
check needs nothing else.
---The live PalBoxV2 nearest to the player, plus its distance in centimetres.
local function nearestPalBox()
local me = Player.coordinate()
if not me then return nil end
local best, bestD2
for _, inst in ipairs(Building.get("PalBoxV2"):instances()) do
local p = inst.pos
if p then
local dx, dy, dz = p.x - me.x, p.y - me.y, p.z - me.z
local d2 = dx * dx + dy * dy + dz * dz
if not bestD2 or d2 < bestD2 then best, bestD2 = inst, d2 end
end
end
if not best then return nil end
return best, math.sqrt(bestD2)
end
local box, distance = nearestPalBox()
if box then
print(string.format("nearest PalBox is %.1f m away", distance / 100))
endSummary
Player.coordinate()is where the player stands, in centimetres.100is one metre, andzis up.Player.coordinateOffset(dx, dy, dz)is that point shifted along the world axes — the short way to say "next to me".Player.character()is the player's own actor, for anything you want to apply to them directly.- All three give
nilwhen there is no world, so check before you use the value. - What the player is holding is readable:
Item.get(id):count(), withnilmeaning unknown. - Read them fresh each time. They are snapshots, and the pawn is rebuilt on every world load.
Next, read Pal to see what :spawn does with the coordinate you just found.