Skip to content

Picking pockets

Players can pick each other’s pockets with the game’s own “Rob”: the thief holds E at another player’s body, the game’s charge starts - its roll can still get them caught - and then its wheel, the tile game of single player, opens over a few things the other player carries. That is one try. What the wheel takes moves from the victim’s bag to the thief’s. The thief’s game plays it all on their screen; the server makes the offer and moves what is taken. NPC actors can have pockets too.

Pickpocketing is off until a mode switches it on, and the rest is the mode’s: who may rob and be robbed, how much is on the wheel, and what a caught thief or a robbed player gets. The names are in the Pickpocketing group of the server API.

SetPickpocketing(enabled, rules) switches it on or off - off by default. When it is on, the game’s Rob prompt shows on every player’s body to every other player, and on the NPC actors that have pockets (section 7). Every key of rules is optional; these are the defaults, but for deny:

function OnGameModeInit()
SetPickpocketing(true, {
range = 3, -- metres from the body (0.5 to 6)
cooldown = 30, -- seconds before the same thief may rob the same player again (3600 at most, 0 = none)
max_items = 8, -- stacks on the wheel, the purse counted (11 at most)
max_stack = 5, -- the most of one kind of item on the wheel
behind_only = false, -- true: only from within 70 degrees of straight behind
min_seconds = 2.5, -- the shortest try that may take anything (60 at most)
money = false, -- true: the victim's purse is one more entry on the wheel ...
money_share = 0.1, -- ... holding this share of it (0 to 1) ...
money_cap = 100, -- ... up to this many Groschen (6553.5 at most)
actors = true, -- NPC actors with pockets can be robbed
deny = {"Food", "dagger"}, -- item classes, names or categories never offered (none by default)
})
end

A number that cannot be used, or a key left out, is the default; a call without rules keeps the earlier ones. GetPickpocketing reads the switch and the rules back, and switching it off ends the tries that run. range is what the server allows - the game’s own prompt reaches about 2.4 metres. A take that comes sooner than min_seconds is refused and the try ends, so raise that one with care.

While it is on, everyone may rob everyone. SetPlayerPickpocketable(pid, false) takes a player’s pockets out of it: the prompt does not show on their body, every ask is refused and a try on them ends. SetPlayerCanPickpocket(pid, false) takes the right to rob from a player. Both last the player’s session, through respawns, until the mode is reloaded - so set them where the mode knows a player’s role, usually in OnPlayerSpawn:

-- admins and guards (team 1) cannot be robbed; only the thieves' guild (team 2) robs
function OnPlayerSpawn(pid)
SetPlayerPickpocketable(pid, not (IsPlayerAdmin(pid) or GetPlayerTeam(pid) == 1))
SetPlayerCanPickpocket(pid, GetPlayerTeam(pid) == 2)
end

IsPlayerPickpocketable and CanPlayerPickpocket read them back. The server does not look at parties or teams: members of one can rob each other unless the mode says no. A player who is dead, knocked out or carried cannot be robbed (knocking a player out and carrying them).

The thief’s game asks, and nothing plays until the server grants the try. A refusal reaches the thief alone, with a line in their chat saying why (each kind of line at most once in fifteen seconds). The server checks that:

  • pickpocketing is on, the thief may rob, and both stand in the world, in one virtual world, with the victim’s body in the thief’s view;
  • neither is busy: on a horse, in a cart, flying, in a dice match, at a shop, in a dialogue, searching a body, knocked out, carried or carrying someone, in a fall or a pull-down, or in a fight;
  • the victim’s pockets may be picked, or the NPC actor has pockets and actors allows it;
  • the thief stands within range, behind the victim when behind_only is set, and - with the level’s collision geometry and [validation] line_of_sight on - with no wall or closed door between them;
  • nobody else is robbing the victim, and the thief did not rob them in the last cooldown seconds, counted from the start of that try whatever it ended in;
  • there is something to take: a victim who carries only what they wear has nothing on the wheel.

Then the mode has the last word. OnPlayerPickpocketStart(pid, victim, actor) is called only when all of that has passed, and false refuses (a refusal starts no cooldown) - the place for a rule of the mode’s own, such as a safe zone or a guard in sight. victim is the robbed player’s id and actor is -1, or the other way round for an NPC actor.

The server makes the offer when it grants the try, and the thief’s game plays over exactly that. For a player it is what they carry - never what they wear or hold in a hand: one stack for each kind of item, at most max_stack of it, shuffled, and at most max_items stacks. With money the victim’s purse is one more entry in the shuffle, so a full bag can leave it off the wheel (deny does not reach it). The game’s quick-slot containers, item aliases, NPC tools and key rings are never in a pocket, and deny keeps out whatever else you name: an item class, an item name or a category of the item catalogue.

The mode cannot add to the wheel, and the odds of being caught are the game’s own roll. It can refuse the try, keep things off with deny, or veto a stack afterwards.

When the wheel ends, the thief’s game says which of the offered stacks it took. The server refuses a claim that names a stack the offer did not hold, one twice, more than was offered, one sooner than min_seconds after the grant, or one after ninety seconds: nothing moves and the try ends as "cancelled". Otherwise each stack goes to the mode first - OnPlayerPickpocket(pid, victim, actor, class, amount) - and false leaves that stack where it is:

-- outside the thieves' guild (team 2) a thief takes coin only
function OnPlayerPickpocket(pid, victim, actor, class, amount)
local info = GetItemInfo(class)
if GetPlayerTeam(pid) ~= 2 and not (info and info.category == "Money") then
return false
end
end

amount is what would move: it is already cut to what the victim still holds, since they may have eaten, sold or put on something since the offer. The purse is the Groschen class, and its amount is in tenths of a Groschen - 25 is 2.5 - while money_cap and the money functions count Groschen. A veto says nothing by itself, so tell the thief why. What is left moves: the victim’s game gives up those items and the thief gets exactly what it gave up. It is the server’s own move, so the inventory audit explains both sides and nobody is flagged.

OnPlayerPickpocketEnd(pid, victim, actor, outcome, reason, taken) hears every ending. outcome is "took" (taken stacks moved), "nothing" (the wheel ended with nothing taken, or everything was vetoed or gone), "caught" or "cancelled" (the try was broken off). A caught thief is one the game’s own roll caught in the charge, or who failed the tile game: nothing moves, and what they face is the mode’s to decide.

reason says why. For the first three it is the game’s own. A caught thief usually says "detected" (the victim noticed) or "seen" (someone saw); "lost" is the thief walking off or looking away, the game’s own break without a crime - so check the reason before you punish. For "cancelled" it is "broken off" (the thief’s game), one of the server’s words - a death, a departure, a teleport, a mount, a cart, flying, a knock-out, being carried or "carrying" someone, a fight, ninety seconds, a refused claim; the callback’s page lists them - or what the mode gave CancelPickpocket(pid, reason), "mode" when nothing. That call ends the try a player is in, the thief or the one robbed, and the thief’s game ends its minigame at once. GetPickpocketTarget(pid) answers whom a thief robs now: "player" or "actor", and the id.

The server sends the victim no message. Telling them, fining the thief, calling the guards, or a fight with StartFight (it needs [combat] pvp) are all the mode’s. A player in a fight can neither rob nor be robbed, and a fight that begins ends their try:

function OnPlayerPickpocketEnd(pid, victim, actor, outcome, reason, taken)
if victim < 0 then return end -- an NPC actor's pockets: nobody to tell
if outcome == "took" then
SendClientMessage(victim, COLOR_SERVER, "Your pockets feel lighter.")
elseif outcome == "caught" and (reason == "detected" or reason == "seen") then
local fine = math.min(20, GetPlayerMoney(pid)) -- the thief pays the victim, up to 20 Groschen
if fine > 0 then
GivePlayerMoney(pid, -fine)
GivePlayerMoney(victim, fine)
SendClientMessage(victim, COLOR_SERVER, GetPlayerName(pid) .. " was caught and paid you " .. fine .. " Groschen.")
end
end
end

SetActorPocket(actorId, items, money) gives an NPC actor pockets: a list of {class = ..., amount = ..., health = ...} (health 0 to 100, 100 when left out) and a purse in Groschen:

local merchant = CreateActor(nil, x, y, z, 180, "Merchant") -- x, y, z: where it stands
SetActorPocket(merchant, {
{class = "bread", amount = 3},
{class = "apple", amount = 5, health = 80},
}, 12.5) -- and 12.5 Groschen in its purse

The prompt shows on an actor with pockets while pickpocketing is on. The offer is made from the list with the same max_items, max_stack and deny, and the purse is offered whole, up to money_cap, whatever money says. With actors at false every ask for an actor is refused. A take shrinks the pockets and the mode refills them: GetActorPocket reads what is left, and in OnPlayerPickpocketEnd the actor comes as actor, with victim at -1. An empty list is empty pockets (the thief is told there is nothing to take); nil takes them away.

freeroam.lua and basicrp.lua keep six of the rules above (range, cooldown, max_items, max_stack, behind_only, money) in a table (POCKET in Freeroam, PICKPOCKET in BasicRP, both with enabled = false) and switch pickpocketing on from it; both answer an admin’s /pickpocket on|off|status. There is no command to rob - the game’s prompt is the way. Freeroam has nothing for a robbed player or a caught thief. BasicRP allows only robbing from behind (behind_only = true) and adds admins_robbable, on_robbed ("none" by default, "notify" tells a robbed player) and on_caught (for a thief the victim noticed: "notify", the default, tells both, "fight" starts a fight too, "none" says nothing); its /pickpocket admins on|off lets admins be robbed. BasicRP ships with every player an admin (EVERYONE_ADMIN), so out of the box nobody can be robbed there: turn that off, set admins_robbable, or use /pickpocket admins on. duel_arena.lua never switches pickpocketing on.

  • A player’s game must be able to take part. A game from an older or unsupported build cannot rob: the server grants it nothing and says so in its chat. Women rob and are robbed like men.
  • Export the game’s tables first. Without the game’s tables the server cannot tell a quick-slot container or a key ring from any other item, and only class GUIDs work in deny and SetActorPocket.
  • The prompt’s state keys are the server’s. The world state pocket, a player’s pocket and steal and an actor’s pocket show and hide it; do not set them with SetGlobalState, SetPlayerState or SetEntityState.
  • A mode reload switches it off again. Pickpocketing goes back to off with the default rules, everyone can rob and be robbed, the actors’ pockets are gone and a try that runs ends without a word to the mode. The new mode’s OnGameModeInit sets what it wants, and it hears OnPlayerConnect and, for players in the world, OnPlayerSpawn again, so rights set there are set again.
  • Nothing is saved. Tries, rights, cooldowns and pockets are gone when the server stops.