Skip to content

Parties

Players group up, the server keeps the group and shows every member the others’ health and stamina in party frames on the left of the screen, and the game mode decides everything else. Every callback and function below has its own page in the index.

A small group of players - five by default - with a leader. It comes into being when the first invitation is accepted and ends when it falls to one member: there is no party of one. A player is in at most one party and has at most one invitation pending. Any member may invite; whether that stays so is the mode’s rule (see OnPartyInvite).

The server does these things by itself:

  • No friendly fire. A hit between two members is refused before it ever reaches OnPlayerDamage, the way a hit between teammates is, and starts no fight. [party] friendly_fire = true gives it back.
  • The frames. Each member sees the others as a column of frames under the game’s HUD: name, a label the mode may set, health and stamina bars, injury and bleeding marks, the leader’s name in gold, greyed while dead or loading. The bars stay live at any distance - a member who walks off, or is in another virtual world, is still on the frame. Nothing on the frame says where a member is: no distance, no direction.
  • The invitation. The invited player gets a toast with a countdown and two keys to accept or decline (Y and N; KcdMp_party_keys <accept> <decline> in their console changes them); the mode’s own /accept command answers the same invitation. An invitation times out after [party] invite_timeout seconds and is withdrawn when the party fills or disbands.
  • The leader. When the leader leaves, the next member in join order leads ([party] leader_leaves = "disband" ends the party instead). A disconnect leaves the party at once; nothing is remembered across sessions.

Everything a player types is the mode’s. The server ships no party commands: /invite, /accept, /decline, /leave, /kick, /leader, /p are a dozen lines of the mode (the example below), which is where every rule lives - who may invite whom, a level gate, leader-only invitations, what the party chat looks like. The shipped freeroam mode carries these commands as its example; a server owner keeps, changes or drops them.

A client script sees the party too - KcdMp.party and the hook KcdMp.on_party_change in the client API - so a mode that wants frames of its own hides the server’s with ShowPartyFrames(pid, false) and draws from that.

Parties and teams are separate things: a party is a group with a leader, frames and invitations; a team is a number that turns friendly fire off. A mode may use both.

Callback When
OnPartyInvite(from, target) before the invitation is sent; return false refuses it - the place for leader-only invitations or any other rule
OnPartyInviteResponse(from, target, answer) "accepted", "declined", "timeout", or "cancelled" (the party filled or disbanded first, or a side disconnected)
OnPartyCreate(party, leader) the group exists; fires before the first two joins
OnPlayerJoinParty(party, pid, reason) every member arrives through here, the leader too: "create", "invite" or "mode"
OnPlayerLeaveParty(party, pid, reason) "left", "kicked", "disconnect" or "disband"
OnPartyLeaderChange(party, pid, previous) a hand-over, by the mode or by the leader leaving
OnPartyDisband(party, reason) "empty" (down to one member), "mode" or "leader"; fires last, after every leave

A party is a number; ids are never reused while the server runs, so an id a mode kept never points at a newer group.

Function Returns What it does
InviteToParty(from, target) true, or false, reason sends the invitation; reason is "not connected", "self", "in a party", "full", "pending" or "refused" (OnPartyInvite said no)
AcceptPartyInvite(pid) boolean the mode’s /accept; joins the inviter’s party, creating it when they have none
DeclinePartyInvite(pid) boolean the mode’s /decline
GetPlayerPartyInvite(pid) from, seconds or nil the pending invitation
AddPlayerToParty(host, pid) boolean no invitation: puts pid into host’s party, making it when there is none - a queue, a raid maker
RemovePlayerFromParty(pid, reason) boolean "left" (the default) or "kicked"
SetPartyLeader(party, pid) boolean pid must be a member
DisbandParty(party) boolean everyone leaves with "disband"
GetPlayerParty(pid) number or nil
GetPartyLeader(party) number or nil
GetPartyMembers(party) table of pids in join order
GetPartySize(party) / IsPartyFull(party) number / boolean against [party] max_size
ArePartyMembers(a, b) boolean for the mode’s own rules - loot, chat, a duel refused between friends
GetParties() table of party ids
SetPartyName(party, text) / GetPartyName(party) the title over the frames; "" none
SetPartyMemberLabel(pid, text) / GetPartyMemberLabel(pid) boolean / string a line under the name in the frame - a role, a score; "" none
ShowPartyFrames(pid, shown) boolean per player; [party] frames is the default for everyone
SendPartyMessage(party, colour, text) boolean one chat line to every member - the /p command is the mode’s
SetPartyData(party, key, value) / GetPartyData(party, key) the mode’s private storage on a party, gone with it

The C# tier has the same names on IServerApi and IGameMode, with IParty (Id, Leader, Members, Name, Data) and IPlayer.Party / IPlayer.PartyLabel.

[party] in server.toml (the reference page):

Key Default Meaning
max_size 5 members per party
invite_timeout 30 seconds until a pending invitation times out
frames true the frames are on for everyone until the mode says otherwise per player
leader_leaves "next" "next" hands the lead to the next member in join order; "disband" ends the party
friendly_fire false true lets members hurt each other

What the shipped freeroam mode carries - /invite <name>, /accept, /decline, /leave, /kick <name>, /leader <name>, /disband and /p <text> - with GetPlayerId for the name and the false, reason of InviteToParty for the refusal:

function OnPlayerCommandText(pid, cmd, args)
if cmd == "invite" then
local target = GetPlayerId(args)
if not target then return SendClientMessage(pid, COLOUR_RED, "No such player.") end
local ok, reason = InviteToParty(pid, target)
if ok then SendClientMessage(pid, COLOUR_SERVER, "Invited " .. GetPlayerName(target) .. ".")
else SendClientMessage(pid, COLOUR_RED, "Cannot invite: " .. reason .. ".") end
return true
elseif cmd == "accept" then
if not AcceptPartyInvite(pid) then SendClientMessage(pid, COLOUR_RED, "Nobody invited you.") end
return true
elseif cmd == "decline" then DeclinePartyInvite(pid) return true
elseif cmd == "leave" then
if not RemovePlayerFromParty(pid, "left") then SendClientMessage(pid, COLOUR_RED, "You are in no party.") end
return true
elseif cmd == "kick" or cmd == "leader" then
local party, target = GetPlayerParty(pid), GetPlayerId(args)
if not party or GetPartyLeader(party) ~= pid then return SendClientMessage(pid, COLOUR_RED, "You lead no party.") end
if not target or GetPlayerParty(target) ~= party then return SendClientMessage(pid, COLOUR_RED, "Not in your party.") end
if cmd == "kick" then RemovePlayerFromParty(target, "kicked") else SetPartyLeader(party, target) end
return true
elseif cmd == "disband" then
local party = GetPlayerParty(pid)
if party and GetPartyLeader(party) == pid then DisbandParty(party) end
return true
elseif cmd == "p" then
local party = GetPlayerParty(pid)
if not party then return SendClientMessage(pid, COLOUR_RED, "You are in no party.") end
SendPartyMessage(party, 0x80C0FFFF, "[Party] " .. GetPlayerName(pid) .. ": " .. args)
return true
end
return false
end
function OnPartyInvite(from, target)
SendClientMessage(target, COLOUR_SERVER, GetPlayerName(from) .. " invites you to a party - /accept or /decline.")
end
function OnPlayerJoinParty(party, pid, reason)
SendPartyMessage(party, COLOUR_SERVER, GetPlayerName(pid) .. " joined the party.")
end
function OnPlayerLeaveParty(party, pid, reason)
if reason ~= "disband" then SendPartyMessage(party, COLOUR_SERVER, GetPlayerName(pid) .. " left the party (" .. reason .. ").") end
end

Leader-only invitations are one more line at the top of OnPartyInvite: if GetPlayerParty(from) and GetPartyLeader(GetPlayerParty(from)) ~= from then return false end.