Skip to content

Throwing riders and pulling them from the saddle

A rider does not have to stay in the saddle. In the game, a rider whose head meets a low branch or a beam at speed is thrown, a horse can throw its rider, and a character on foot can take a rider down with the game’s take-down move. All of it is the game’s own: the fall, the lying still and the get-up play on the rider’s screen, and every player who has their body sees the same fall on it.

A mode decides whether it happens, who may do it, how likely it is and what it costs, and can throw or pull a rider on its own order. Throws are on by default, as in the game; pulling riders down is off until a mode allows it. The names are in the Horses group of the server API.

The rider’s own game plays the throw. The server checks it against the horse they rode, has every game that shows their body play the same fall on it, and calls OnPlayerThrown(pid, id, cause, flung). id is the horse they fell from. cause is THROW_HEAD_HIT (a branch or a beam), THROW_HORSE, THROW_MODE (your own order, section 2) or THROW_OTHER (their game threw them and the server could not tell why) - the constants. flung is true for the game’s strong throw, the one a rearing horse gives, backward off the horse’s rear. The server accepts a throw of the game’s own only from the horse it knows they rode, and not within three seconds of their last one: any other is just the rider’s own fall on their screen - the others do not see it and the mode is not told.

The callback runs before OnPlayerDismount of the same fall, and the server may still have the rider in the saddle when it does: use id, not GetPlayerMount. What the fall costs the rider - health, a wound - is for the mode to decide here:

-- a fall hurts: 10 health, 30 when they were flung - never a death
function OnPlayerThrown(pid, id, cause, flung)
SetPlayerHealth(pid, math.max(1, GetPlayerHealth(pid) - (flung and 30 or 10)))
if cause == THROW_HEAD_HIT then SendClientMessage(pid, COLOR_RED, "Mind the branches!") end
end

IsPlayerThrown(pid) is true while the fall lasts: about six seconds, eight when flung, a little longer while their game still shows the fall (twelve at most), or less if they are in the saddle again by then. The server lets the rider’s body move at a horse’s speed meanwhile - a body tumbling along the ground is no speed hack. A death, a respawn, SetPlayerPos or a new ride ends the allowance at once.

ThrowPlayerFromHorse(pid [, flung]) has a rider’s game throw them off their horse, the same fall seen by everyone. By default they are flung backward off the horse’s rear; with flung set to false they are dropped where they sit, as a head hit does. It answers true, or false and a reason you can show: they are not on a horse, are dead, are already falling or being pulled off, the rules say no (section 3), or their game cannot play the throw.

The order goes to the rider’s game, the throw follows a moment later and OnPlayerThrown (THROW_MODE) says it happened. A game that declines it never answers: the order lapses after two seconds and the rider stays in the saddle.

-- a smashing blow on a rider knocks them off the horse
function OnPlayerDamage(pid, attacker, damage, zone, part, weapon, dtype)
if dtype == "smash" and damage >= 25 and GetPlayerMount(pid) then
ThrowPlayerFromHorse(pid)
end
end

A rider cannot be knocked out or carried while they ride. Throw them first, and knock them out when the fall is over (the carrying guide).

Two switches, per rider, both on by default. SetPlayerRiderHeadHit(pid, false) lets the rider pass under a low branch or a beam; a horse that throws them and your own ThrowPlayerFromHorse still work. SetPlayerRiderThrow(pid, false) stops every throw: the head hit, the horse and ThrowPlayerFromHorse, which then answers false and the reason. A pull-down is no throw and neither switch changes it.

They last until you change them or the player leaves, through respawns, and go to the player’s game with every spawn. Set them in OnPlayerSpawn:

local MASTERS = 2 -- the team of the horse masters
function OnPlayerSpawn(pid)
SetPlayerRiderHeadHit(pid, false) -- the branches are only scenery here
SetPlayerRiderThrow(pid, GetPlayerTeam(pid) ~= MASTERS) -- and nothing at all throws a horse master
end

The game’s own take-down move: a player on foot takes hold of a rider from the horse’s side, the two play the game’s pair, and the rider is thrown off - or, when the pull fails, stays in the saddle. Everyone who has the puller, the rider and the horse in view sees it. It is no blow: it does no damage and OnPlayerDamage does not run.

It is off for everyone until a mode allows it, player by player. SetPlayerPullDown(pid, true) shows the game’s “Pull down” prompt on a rider for that player - on foot, close to the horse, with a weapon drawn or bare-handed - and GetPlayerPullDown reads the right back. Set it in OnPlayerSpawn like the throw switches: SetPlayerPullDown(pid, GetPlayerTeam(pid) == 1) lets team 1 do it. The rider needs nothing set: anyone in the saddle can be pulled down by a player you allowed, unless your code says otherwise.

When a player uses the prompt, the server checks the ask before your code hears of it:

  • the player has the right and is on foot, the rider is in the saddle, both are alive and standing in the world in one virtual world, and neither is in a pull-down, a fall, a cart, flying, a dice match, a shop or a dialogue. Nothing else about a player is looked at - not a knock-out, not a carry, not carrying someone - so a mode that wants those refused says no in OnPlayerPullDown;
  • the two are in view of each other;
  • pvp is on and they are neither teammates nor party members, the rules a blow follows ([combat] pvp, SetPlayerTeam, [party] friendly_fire);
  • the weapon can do it, and the horse is near enough and slow enough for it (below);
  • no wall is between them, when [validation] line_of_sight is on (the collision guide; it is off by default).

The reach is the distance from the puller to the horse, and the pace is the horse’s. Both come from the game’s own numbers for the weapon, with 1.5 m and 1 m/s more allowed because the positions the server goes by are a few frames old:

In the puller’s hand Reach The horse’s pace, at most
bare hands, or a weapon not drawn 3.5 m 5 m/s
a sword, sabre, axe or mace 4 m 7 m/s
a longsword 4.5 m 8 m/s
a halberd 5.5 m 9 m/s

A dagger, a flail, a shield, a torch, a bow or a gun cannot pull anyone down. Without the tables export ([combat] tables) the server cannot tell weapons apart and gives any drawn weapon but a torch a sword’s numbers.

Then your code has the last word. OnPlayerPullDown(pid, victim, id) - the puller, the rider, the horse - decides with what it returns:

Return The pull
false is refused, and nothing plays
a number from 0 to 1 works with that chance: 1 always, 0 plays the failed pair (a number outside it counts as the nearer end)
nothing, nil, true works, as 1

In C# the callback returns the chance as a number, and a negative one refuses (the C# API). The server rolls the outcome itself, so every screen plays the same one. A wait between two pulls, a protection time after a fall and a price belong here:

function OnPlayerPullDown(pid, victim, id)
local now = GetServerTime()
if now - (GetPlayerData(pid, "pulled_at") or -60000) < 20000 then return false end -- 20 s between a player's pulls
if now - (GetPlayerData(victim, "fell_at") or -60000) < 30000 then return false end -- a rider is safe for 30 s after a fall
SetPlayerData(pid, "pulled_at", now)
return 0.7 -- seven pulls in ten work
end
function OnPlayerPulledDown(pid, victim, id, success)
if success then SetPlayerData(victim, "fell_at", GetServerTime()) end
end

OnPlayerPulledDown(pid, victim, id, success) tells how it ended. success is true when the rider was thrown off - before the OnPlayerDismount of that fall - and false when the pull failed and the rider is still in the saddle (told when the pair ends, about four and a half seconds after the start). It does not run when the pull-down is cut short before its outcome shows - a death, a disconnect, SetPlayerPos, a respawn, a cart seat, a flight - nor when a pull that was to land never takes the rider out of the saddle. So what you set in OnPlayerPullDown should expire by time, not wait for this callback.

IsPlayerInPullDown is true for both players until a couple of seconds after the pair ends. Meanwhile the server lets both bodies move as the pair moves them, within about eight metres of where they stood.

5. Pulling a rider down on the mode’s order

Section titled “5. Pulling a rider down on the mode’s order”

PullPlayerFromHorse(victim, puller [, success]) is the same move on your word. It skips the weapon’s reach and the horse’s pace, pvp, teams and parties, the wall check, the rider’s SetPlayerRiderThrow setting, the puller’s right and OnPlayerPullDown. success set to false plays the failed pair and the rider stays in the saddle. OnPlayerPulledDown tells the outcome as for any pull.

It answers true, or false and a reason (“The rider is not on a horse.”, “They are 9.1 m apart (6 m at most).”). Both players must meet the first two points above - standing, alive, not busy and in view of each other - and the puller be within six metres of the horse.

-- /unhorse <player>: a guard beside a rider pulls them off the horse
function OnPlayerCommandText(pid, cmd, args)
if cmd ~= "unhorse" then return false end
local rider, why = sscanf(args, "u")
if rider == false then
SendClientMessage(pid, COLOR_RED, "usage: /unhorse <player> - " .. why)
else
local ok, reason = PullPlayerFromHorse(rider, pid)
if not ok then SendClientMessage(pid, COLOR_RED, reason) end
end
return true
end

freeroam.lua and basicrp.lua in the server folder have a PULLDOWN table (enabled = false, chance), hand the right out from it in OnPlayerSpawn and answer OnPlayerPullDown. Freeroam’s chance is 1.0. BasicRP’s is 0.7, with cooldown = 20 seconds between one player’s pulls, protect_seconds = 30 in which a rider who was pulled off cannot be pulled again, and damage = 0 health the fall takes off the rider (never below 1); its OnPlayerPulledDown tells the rider who did it. BasicRP has no command: the game’s prompt is the way. Freeroam gives admins /throw [player] [plain], /pulldown <victim> [puller] [fail] and /pulldowns on|off to try it alone (/horse first, for a rider). Neither mode switches the throws off, and duel_arena.lua leaves all of it alone.

  • A player’s game must be able to take part. A game from an older or unsupported build cannot be thrown on your order or be the puller of a pull-down: the call answers false with the reason. A rider’s or a watcher’s game that cannot play the pair still takes the rider off the horse when a pull lands, without the animation, and shows nothing of one that fails.
  • A reload does not undo the switches. A new join starts with throws on and pull-downs off, and nothing is saved. A mode reload switches none of the three back - the new mode’s OnPlayerSpawn runs again for everyone in the world, so set them there, false included. Lua has no getter for the two throw switches.
  • A pull-down is a fight. The two are in a fight from the start (OnFightStart: "hit" for a player’s pull, "mode" for your order), which ends as any fight does. With [combat] pvp = false your order still plays but starts none.
  • The horse is left alone. After a throw or a pull it stays in the world, its owner’s, with nobody in the saddle. While a pull-down runs - about six and a half seconds - DestroyEntity and SetEntityController on it wait until it is over, and MountPlayer onto it answers false unless it is already the player’s to control.
  • A refusal is told to the puller, never to the rider. When pvp, a team, a party, the weapon, the reach or the pace stops a pull, or OnPlayerPullDown returns false, the server adds a line to the puller’s chat (“Hans is too far to pull off the horse.”, “You cannot pull Hans off the horse.” for your own false), once per line every 15 seconds at most. A wall, a busy player or a missing right only reach the server log.
  • An error in OnPlayerPullDown lets the pull through. A script error is logged and the call counts as returning nothing, which is a chance of 1 (when a script fails). Put the refusals first.