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.
1. The fall
Section titled “1. The fall”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 deathfunction 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!") endendIsPlayerThrown(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.
2. Throwing a rider on the mode’s order
Section titled “2. Throwing a rider on the mode’s order”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 horsefunction OnPlayerDamage(pid, attacker, damage, zone, part, weapon, dtype) if dtype == "smash" and damage >= 25 and GetPlayerMount(pid) then ThrowPlayerFromHorse(pid) endendA 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).
3. Switching the throws off
Section titled “3. Switching the throws off”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 masterend4. Letting players pull riders down
Section titled “4. Letting players pull riders down”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_sightis 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 workend
function OnPlayerPulledDown(pid, victim, id, success) if success then SetPlayerData(victim, "fell_at", GetServerTime()) endendOnPlayerPulledDown(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 horsefunction 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 trueendThe example modes
Section titled “The example modes”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.
What to know
Section titled “What to know”- 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
falsewith 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
OnPlayerSpawnruns again for everyone in the world, so set them there,falseincluded. 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 = falseyour 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 -
DestroyEntityandSetEntityControlleron it wait until it is over, andMountPlayeronto it answersfalseunless 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
OnPlayerPullDownreturnsfalse, 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 ownfalse), 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
OnPlayerPullDownlets the pull through. A script error is logged and the call counts as returning nothing, which is a chance of1(when a script fails). Put the refusals first.
