PalForge
API reference

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 nil

All three hand back nil when the player is not in a world, so check the value before you use it.

FunctionReturnsnil when
Player.character()the local APalPlayerCharacterthere is no valid player pawn
Player.coordinate()a Coordx, y, z in centimetresno pawn, or the location read failed
Player.coordinateOffset(dx, dy, dz)a Coord, offset from the one abovethe 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 table

A 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
end

Fetch 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
end

It 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 cm

Units 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

content/spawn_here.lua
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.

content/spawn_ahead.lua
---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
end

GetActorForwardVector 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.

content/ring.lua
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
end

Guard 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.

content/inventory_report.lua
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.

content/nearest_palbox.lua
---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))
end

Summary

  • Player.coordinate() is where the player stands, in centimetres. 100 is one metre, and z is 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 nil when there is no world, so check before you use the value.
  • What the player is holding is readable: Item.get(id):count(), with nil meaning 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.

On this page