9LABS Movement · v1.1.1 Lean out of cover. Catch the ledge.
Directional peeking with a collision-aware shoulder camera, automatic engine-driven ledge climbing, and GTA IV-style roof-edge behaviour. Entirely client-side: no server script, no database, no ox_lib.
Introduction
Players lean left or right while aiming, and catch reachable roof edges while airborne — without replacing weapon damage, ammunition, projectiles or the normal GTA firing system.
What is in the resource
- Q/E left and right weapon peeking, in hold or toggle mode, on editable FiveM key bindings.
- A custom streamed male/female freemode lean animation, with per-weapon and per-group profiles.
- Collision-aware body clearance checks and a collision-aware scripted shoulder camera, with an optional custom centre reticle.
- Automatic engine-driven ledge climbing, with configurable reach, direction and fall-speed limits.
- Roof-edge anti-slide behaviour.
- A recoil integration export, client exports, a local lean event and replicated cosmetic state bags.
- Qbox, QBCore, ESX, ox_core, standalone and custom framework gating.
Entirely client-side. No server script, no database, no server callback, no ox_lib
and no required framework. Framework support only decides whether the local character is
loaded, dead, in last stand or handcuffed.
Requirements
A current FiveM build, a freemode ped, and default state bag behaviour.
Client and serverA current FiveM client/server build. Lua 5.4, enabled by the included fxmanifest.lua.
Ped modelA supported freemode ped. The default allowlist is mp_m_freemode_01 and mp_f_freemode_01.
State bagsDefault client-writable player state bags (sv_stateBagStrictMode false) when the replicated peek and parkour states are needed.
DependenciesNone. No database, no server callback, no ox_lib, no framework requirement.
Strict state bag mode. v1.1.1 publishes both replicated values from the owning
client, so it expects the default false. Turning strict mode on means only the
server may modify player state bags — keep it off unless you have built and tested a
server-authoritative replacement for those publications.
Installation
One folder and one line. Start it after your framework when framework-aware state checks are used.
- Extract the delivered
9labs_movement folder into your resources directory. - Start it after your framework.
- Configure
config.lua, then restart the resource after any configuration change. - Reconnect after changing streamed animation files.
ensure qbx_core
ensure 9labs_movement
Standalone installation:
ensure 9labs_movement
Start order matters with framework = 'auto'. Detection happens when the
client script loads, so a Movement that starts first may select standalone mode for that
client session. On a production server, set the framework explicitly.
Keep the resource folder named 9labs_movement for the export examples in this manual to work.
Using it in game
With the default configuration: equip a supported firearm, hold aim, then hold Q or E.
- Equip a supported firearm.
- Hold aim.
- Hold
Q to lean left or E to lean right. - Release the key to return to neutral.
The last direction pressed wins while both keys are held, and releasing the active key in
hold mode hands the lean to the other still-held direction. The pose retracts temporarily if
the player stops aiming or exceeds peek.maxSpeed, then resumes on its own when
they aim again or slow down. Releasing the key while it waits cancels the request.
Key bindings
Players rebind both controls at Settings → Key Bindings → FiveM → 9LABS. The registered commands are:
+9labs_peek_left
-9labs_peek_left
+9labs_peek_right
-9labs_peek_right
peek.keys only supplies the initial defaults for a new binding. FiveM stores each
player's own choice, so changing the defaults later never overwrites keys an existing player
already saved.
Updating
Replace the resource as one versioned set — never mix an old bundle with a new config, runtime or manifest.
Back these up first:
config.lua
client/bridge.lua
locales/
Then merge your configuration and custom bridge changes into the new editable files, and afterwards:
- Restart
9labs_movement at a controlled maintenance time. - Reconnect if the streamed YCD changed.
- Test left and right peek with a pistol and a rifle.
- Test the recoil adapter while neutral and while leaning.
- Test ledge climbing on a low and a high reachable edge.
- Test every resource that blocks Movement.
Modes and gating
Four settings decide when the lean is allowed at all.
Hold or toggle
| Mode | Behaviour |
hold | Lean while the bound key remains held. |
toggle | Press a side once to lean, press the same side again to return to neutral. Pressing the other direction switches straight to it. |
Only those two values are valid; anything else prevents normal initialization.
Aim requirement
With requireAim = true the pose activates only while aiming, and a held request
waits in waiting_aim. Setting it to false removes the explicit aim
requirement — every other eligibility, weapon, movement and camera check still applies.
First person
allowFirstPerson = false by default: first-person view and first-person aim
reject peeking. Turn it on only after testing your camera, weapon and other first-person
resources. Cinematic view stays ineligible either way.
Movement speed
maxSpeed = 4.5 is metres per second. Above it the pose retracts and the reason
becomes waiting_movement; a still-held direction resumes when the player slows
down.
Shoulder camera
A scripted camera that captures the current gameplay view, owns yaw and pitch, and blends back on release.
camera = {
enabled = true,
reticle = false,
mouseSensitivity = 8.0,
controllerSpeed = 140.0,
}
While the lean is active the camera follows a pelvis-relative orbit and checks collision
between the player and the desired view position: a world hit shortens the boom instead of
letting the view through geometry. Body clearance is checked independently for each direction.
What it will not do
- It never hides or fades the local player.
- It never shuts down another resource's scripted camera — if one already owns rendering, the peek request is rejected until it is released.
Reticle
reticle = true draws Movement's small centre reticle while it owns the scripted
camera. The default false keeps the normal HUD reticle.
Turning the camera off
camera.enabled = false keeps body peeking without the scripted shoulder camera.
In that mode ApplyRecoil returns false, because Movement no longer
owns a camera.
Weapon profiles
A profile targets an exact weapon, a weapon group, or the fallback default.
exact WEAPON_* profile -> GROUP_* profile -> default
weapons = {
default = {
enabled = true,
lean = 1.0,
cameraLeft = 0.20,
cameraRight = 0.30,
clearance = 0.42,
},
GROUP_PISTOL = {
lean = 1.0,
cameraLeft = 0.18,
cameraRight = 0.26,
},
GROUP_MG = {
lean = 0.75,
cameraRight = 0.34,
clearance = 0.50,
},
GROUP_SNIPER = {
enabled = false,
},
WEAPON_CARBINERIFLE = {
lean = 0.90,
cameraRight = 0.28,
},
}
| Field | Meaning |
enabled | Whether peeking is allowed with this weapon or profile. |
lean | Animation weight from 0.1 to 1.0. |
cameraLeft | Left-side camera extension, in metres. |
cameraRight | Extension beyond the native right shoulder, in metres. |
clearance | Required free body space beside the player, in metres. |
Missing values inherit from default. The included defaults enable pistols, SMGs,
rifles, shotguns and machine guns, and disable sniper, heavy, thrown, melee and unarmed. The
active profile is refreshed while peeking, so swapping weapons updates enablement, animation
weight, camera placement and clearance immediately.
When peeking is refused
Beyond config and integration blockers, the request is rejected or cancelled while the local player is:
- not loaded, or without player control;
- dead, fatally injured, stunned or ragdolled;
- entering or sitting in a vehicle, or attached to another entity;
- falling, jumping or climbing;
- swimming or underwater;
- in cover or entering cover;
- reloading, handcuffed, using a scenario, or in melee combat;
- in a pause menu, NUI, cutscene or player switch;
- on an unsupported ped model or weapon profile;
- in a blocked state bag, or using an incompatible camera/view mode;
- moving above the configured speed.
Cover shares the key. The default left key is GTA control 44, which is
native cover, so cover input is suppressed while eligible aiming is active.
Automatic ledge climbing
While airborne, the module looks for a near-vertical wall in the configured directions, probes down for a reachable flat top, and asks GTA to run its native TaskClimb immediately.
There is no extra grab key and no custom frozen hanging state — GTA supplies the native
grab-and-pull-up animation.
Detecting a running jump
Wall probes are extended slightly by horizontal player speed, so a running jump is detected
before the ped fully collides with the surface. Forward input while already close to a wall
can satisfy the approach intent even after the collision has killed horizontal velocity —
which helps a player who hits the wall just below an edge, without enabling arbitrary grabs
while deliberately dropping down a wall.
What it rejects
- Surfaces outside the configured height range, and strongly sloped wall faces.
- Tops that are not sufficiently flat, and walls that visibly continue above the detected top.
- An edge the player just jumped from.
- Moving or attached object surfaces.
- Falls faster than
maxFallSpeed, and approaches outside the direction cone. - Peds in invalid movement or framework states, and catches while another scripted camera owns the view.
After a native climb refusal, retries are bounded for the current airborne session, so the
ped cannot repeatedly boost upward against a flat wall.
Eligibility
Climbing is disabled while the player is dead, without control, in menus or cutscenes, inside
or entering a vehicle, attached, swimming, stunned, handcuffed, in cover, using a parachute,
on an unsupported model, or blocked by framework, state bag or export integration.
Ledge option reference
Every value below lives in config.lua and ships with these defaults.
ledge = {
enabled = true,
climbFast = false,
cooldown = 300,
maxReach = 1.60,
minReach = 0.0,
wallDistance = 0.8,
wallProbeHeight = 0.2,
angles = { 0.0, -30.0, 30.0 },
minApproach = 0.25,
noRagdollWhileFalling = true,
maxFallSpeed = 12.0,
debugCommands = false,
}
| Key | Meaning |
enabled | Enables automatic ledge detection and climbing. |
climbFast | Passes the fast/launch flag to GTA's native climb task. |
cooldown | Delay in milliseconds after a completed attempt before another catch. |
maxReach | Highest accepted top above the ped root, in metres. |
minReach | Lowest accepted top relative to the ped root, in metres. |
wallDistance | Base forward distance used to search for a wall. |
wallProbeHeight | Height of the lowest horizontal wall probe above the ped root. |
angles | Directions checked relative to ped heading, in degrees. |
minApproach | Required speed toward the wall, in metres per second. |
noRagdollWhileFalling | Temporarily preserves control during eligible falls. |
maxFallSpeed | Downward speed above which a ledge is not caught. |
debugCommands | Enables the local tuning and visualization commands. |
Anti-slide
GTA normally starts an automatic drop-down at some roof edges. Movement suppresses it so the player keeps their run and jump intent.
The suppression applies while an on-foot player is moving or airborne. It is independent from
peek.enabled, ledge.enabled, SetEnabled and
SetPeekingBlocked — none of those turn it off.
Running another parkour resource? Set antiSlide.enabled = false when a
different resource owns roof-edge behaviour, and consider disabling ledge.enabled
too. Two automatic edge systems at once will fight.
Ledge debug commands
Set ledge.debugCommands = true to tune the geometry live, then copy the values into config.lua.
| Command | Purpose |
/ledgedebug | Toggle probe visualization and the current rejection or state text. |
/ledgeset <key> <value> | Change a supported geometry value for this client session. |
/ledgedump | Print the current tunable values to the F8 console. |
Supported /ledgeset keys:
maxReach
minReach
wallDistance
wallProbeHeight
prediction
topInset
depth
maxFallSpeed
minApproach
originRadius
originHeight
/ledgeset maxReach 1.75
/ledgeset wallDistance 0.9
/ledgedump
Session only. /ledgeset changes affect that client until it reconnects.
The release package ships with debugCommands = false — leave it that way in
production.
config.lua
Every customer-facing setting lives in the Asset Escrow-exempt config.lua, through the global MovementConfig table.
MovementConfig = {
locale = nil,
framework = 'auto',
models = {
'mp_m_freemode_01',
'mp_f_freemode_01',
},
blockedStates = {
'invBusy',
'invOpen',
'isDead',
'inLaststand',
'isCuffed',
'isHandcuffed',
},
peek = {
enabled = true,
mode = 'hold',
keys = { left = 'Q', right = 'E' },
requireAim = true,
allowFirstPerson = false,
maxSpeed = 4.5,
camera = {
enabled = true,
reticle = false,
mouseSensitivity = 8.0,
controllerSpeed = 140.0,
},
weapons = {
-- profiles
},
},
ledge = {
enabled = true,
climbFast = false,
cooldown = 300,
maxReach = 1.60,
minReach = 0.0,
wallDistance = 0.8,
wallProbeHeight = 0.2,
angles = { 0.0, -30.0, 30.0 },
minApproach = 0.25,
noRagdollWhileFalling = true,
maxFallSpeed = 12.0,
debugCommands = false,
},
antiSlide = {
enabled = true,
},
}
Locale
Locale files control the text shown for the registered key mappings. English and German are included.
With locale = nil, Movement reads the ox:locale convar. That does not
require ox_lib — it is an ordinary FiveM convar lookup.
setr ox:locale en
Or select one directly:
locale = 'de'
A missing file or a missing key falls back to the built-in English text.
Framework
The adapter only gates local movement features. It never touches money, inventories, permissions or server ownership.
| Value | Adapter |
auto | Detects Qbox, QBCore, ESX, ox_core, then falls back to standalone. |
qbx | Qbox (qbx_core, optionally qbx_medical). |
qb | QBCore (qb-core). |
esx | ESX (es_extended). |
ox | ox_core. |
standalone | Native client checks only. |
custom | Editable functions in client/bridge.lua. |
Detection order in auto mode:
qbx_core -> qb-core -> es_extended -> ox_core -> standalone
Built-in state checks
| Framework | Loaded | Dead / last stand | Handcuffed |
| Qbox | LocalPlayer.state.isLoggedIn | qbx_medical:IsDead() / IsLaststand() when running | Qbox metadata |
| QBCore | LocalPlayer.state.isLoggedIn | QBCore metadata | QBCore metadata |
| ESX | ESX.IsPlayerLoaded() | ESX.PlayerData.dead | Native cuff check only |
| ox_core | Always loaded | LocalPlayer.state.isDead | Native cuff check only |
| standalone | Always loaded | Native ped death checks | Native cuff check |
| custom | Custom bridge | Custom bridge | Custom bridge |
Custom bridge
Set framework = 'custom' and edit the CUSTOM block at the top of client/bridge.lua.
-- client/bridge.lua, CUSTOM block at the top of the file
function Custom.isLoaded()
return true
end
function Custom.isDead()
return false
end
function Custom.isCuffed()
return false
end
function Custom.onPeekChanged(side)
-- side: -1 left, 0 neutral, 1 right
end
function Custom.notify(message, kind)
end
The first three functions must return real Lua booleans. Custom.onPeekChanged is
called only when the published lean side changes. Custom.notify is available to
the bridge, though normal gameplay does not need a notification provider. The built-in
adapters below the custom block need no changes.
Ped models
The same allowlist gates peeking and ledge climbing, which protects animation alignment on unsupported skeletons.
models = {
'mp_m_freemode_01',
'mp_f_freemode_01',
}
To support another ped, add its model name and then test:
- left and right animation alignment;
- weapon and hand alignment;
- shoulder camera height;
- pelvis, head and clearance probe positions;
- native climb behaviour.
Adding a model only removes the block. It does not retarget the custom animation for
another skeleton — the included YCD is authored for freemode ped skeletons.
State bag blockers
When any listed key is truthy on LocalPlayer.state, both peeking and ledge climbing are blocked.
The default list covers invBusy, invOpen, isDead,
inLaststand, isCuffed and isHandcuffed — useful for
inventories, emotes, restraints and other local gameplay states. To add your own:
-- in the other resource
LocalPlayer.state:set('myCustomMovementBlock', true, false)
-- in MovementConfig
blockedStates = {
'invBusy',
'invOpen',
'myCustomMovementBlock',
}
Prefer the export for temporary blocks. SetPeekingBlocked is usually
safer for a resource-owned state, because Movement clears that block automatically when the
calling resource stops.
Client exports
All public exports are client exports, and the names are case-sensitive.
local side = exports['9labs_movement']:GetPeekSide() -- -1 left, 0 neutral, 1 right
local reason = exports['9labs_movement']:GetPeekReason() -- runtime status string
local ok = exports['9labs_movement']:SetPeekingBlocked(true)
exports['9labs_movement']:SetPeekingBlocked(false)
exports['9labs_movement']:SetEnabled(false)
exports['9labs_movement']:SetEnabled(true)
local handled = exports['9labs_movement']:ApplyRecoil(amount)
GetPeekSide
Returns -1 for left, 0 for neutral, 1 for right. A held
request that is still waiting for aim, speed or clearance returns 0 until the
pose is actually active.
GetPeekReason
| Reason | Meaning |
idle | No peek request has run yet. |
pressed | A valid direction request was accepted. |
peeking | The requested side is active. |
released | The input returned to neutral. |
waiting_aim | The key is held but aim is required. |
waiting_movement | The key is held but the player is moving too quickly. |
clearance_pending | A side clearance query is still resolving. |
body_clearance | Geometry blocks the requested body lean. |
blocked | An export or resource blocker is active. |
parkour | The ledge module took over movement. |
ineligible | A player, weapon, view or gameplay state rejected peeking. |
ped_changed | The local ped changed during the active task. |
animation_interrupted | The animation could not be maintained after bounded retries. |
camera_stopped | The shoulder camera could not continue safely. |
disabled | Movement was disabled through SetEnabled(false). |
error | A protected runtime operation raised an error and was cleaned up. |
Use it for UI hints and diagnostics. Reason strings describe runtime state and may gain values
in later versions, so read GetPeekSide or the change event when your logic only
needs the direction.
SetPeekingBlocked
Adds or removes a blocker owned by the calling client resource. Despite the name it stops both
peeking and ledge climbing, and it does not disable antiSlide. It accepts only a
Lua boolean, and returns true when the caller and value are valid.
Each calling resource owns a separate blocker: resource A cannot clear resource B's block by
passing false. Movement removes a block automatically when its owning resource
stops.
local movementBlocked = false
local function setMovementBlocked(value)
value = value == true
if movementBlocked == value then return end
local ok = exports['9labs_movement']:SetPeekingBlocked(value)
if ok then movementBlocked = value end
end
AddEventHandler('onClientResourceStop', function(resource)
if resource ~= GetCurrentResourceName() then return end
-- Movement also clears this caller automatically.
movementBlocked = false
end)
SetEnabled
Globally enables or disables peeking and ledge climbing on the local client. Only the exact
boolean true enables the modules; it returns nothing, and it does not disable
antiSlide. Prefer SetPeekingBlocked for temporary per-resource
states, and keep SetEnabled for an integration that intentionally owns global
availability.
ApplyRecoil
Returns true only when all of these hold:
- Movement currently owns the rendered scripted camera;
amount is a finite number; - the absolute increment is not greater than 5 degrees.
Otherwise it returns false and the recoil resource should use its normal gameplay-camera fallback.
Recoil resources
Any recoil script that calls SetGameplayCamRelativePitch directly should offer the increment to Movement first.
local function applyCameraRecoil(amount, scale)
if GetResourceState('9labs_movement') == 'started' then
local ok, handled = pcall(function()
return exports['9labs_movement']:ApplyRecoil(amount)
end)
if ok and handled then return end
end
SetGameplayCamRelativePitch(
GetGameplayCamRelativePitch() + amount,
scale
)
end
Replace only the recoil script's pitch-addition call. Keep its own weapon profiles, randomization, cadence and scale values.
| Situation | Result |
| Movement shoulder camera active | Movement consumes the pitch increment. |
| Normal aiming or neutral | The recoil resource uses its native fallback. |
| Movement stopped, or an older version | The protected pcall falls back safely. |
| Invalid or excessive increment | The export returns false and the fallback stays active. |
Never apply the same increment twice. When the export returns true, do
not also write native pitch, or the recoil is doubled. The adapter changes no damage,
ammunition, spread, projectile origin or weapon animation.
Event and state bags
9labs_movement:changed
A local client event, fired only when the published lean side changes. It receives the side and nothing else.
AddEventHandler('9labs_movement:changed', function(side)
if side == -1 then
print('Player started peeking left')
elseif side == 1 then
print('Player started peeking right')
else
print('Player returned to neutral')
end
end)
It is transition-only, so poll GetPeekSide if your integration loads after the
current side was already published. This is a local event, not a network event — never turn it
into a trusted server action with RegisterNetEvent.
Replicated player states
9labs_movement:peek carries -1, 0 or 1, and
9labs_movement:parkour carries false or 'climbing'.
-- local player
local side = LocalPlayer.state['9labs_movement:peek'] or 0
-- another player, on a client
local side = Player(serverId).state['9labs_movement:peek'] or 0
-- on the server
local parkour = Player(source).state['9labs_movement:parkour']
-- false or 'climbing'
Player(...) takes the server id. A GTA player index is a different, client-local
identifier, and should only be fetched when a native actually requires one.
A local HUD indicator
local labels = {
[-1] = 'LEFT',
[0] = 'NEUTRAL',
[1] = 'RIGHT',
}
AddEventHandler('9labs_movement:changed', function(side)
SendNUIMessage({
action = 'movementPeek',
side = side,
label = labels[side] or 'NEUTRAL',
})
end)
Both state bags are client-owned cosmetic state. They help animations, UI and
non-critical effects observe transitions. They are never evidence that a player may deal or
avoid damage, receive items or money, pass an anti-cheat decision, bypass position validation
or claim a shot or climb was legitimate. Any server reward or security decision must run its
own authoritative checks.
Other resources
What to do when something else wants the same input, camera or animation.
Inventory and menu resources
The default blocked-state list already covers invBusy and invOpen.
Add your own state key, or call SetPeekingBlocked, when the resource does not
publish one of the configured keys.
Emote and animation resources
Block Movement before a full-body emote so the animations do not compete, and clear the block
when it ends. Movement cleans up its own lean animation when eligibility is lost.
Camera resources
Movement refuses to steal a foreign scripted camera. Cutscene, character creator, drone and
cinematic resources should call SetPeekingBlocked(true) while active.
Cover resources
The default left key is GTA control 44, which is native cover; Movement suppresses
cover while eligible aiming is active. Rebind left peek, or disable one behaviour, if another
cover script needs exclusive use of that control.
Parkour and recoil resources
Do not run two automatic ledge or roof-edge systems at once — disable ledge.enabled
and/or antiSlide.enabled when another resource owns those mechanics. For recoil,
use the ApplyRecoil adapter: two scripts writing pitch while Movement owns the
camera produces doubled recoil or snapping.
Asset Escrow
The Tebex release is built for Cfx Asset Escrow. These files are declared customer-editable in escrow_ignore:
config.lua
client/bridge.lua
locales/*.json
Protected runtime modules ship inside the bundled client implementation, so the encrypted
resource never tries to load protected Lua through LoadResourceFile or
require at runtime. Normal integrations use the documented exports, event, state
bags and editable bridge — no protected file needs modifying.
About the streamed animation. The YCD is included in the package. Cfx controls which
asset types are eligible for encryption, so do not assume every streamed file is encrypted
merely because the Lua implementation is protected.
Limits
What v1.1.1 deliberately does and does not do.
- It does not replace GTA projectiles or the bullet origin.
- It does not change damage, ammunition, spread or weapon ownership.
- The camera does change the player's rendered aiming view while active.
- The owner's animation and native climb tasks replicate through ordinary GTA/FiveM behaviour.
- Replicated state bags are cosmetic and client-owned.
- There are no server exports and no trusted server events.
- There is no database and no persistent player setting beyond FiveM's own saved key maps.
- Only configured ped models are supported, and first-person peeking is off by default.
- Foreign scripted cameras take priority.
- An early or interrupted return from a lean blends back to the gameplay camera.
- Anti-slide is independent from runtime peek and parkour blockers.
Troubleshooting
Q or E does nothing
- Confirm
9labs_movement is started and peek.enabled = true. - Check the player's own FiveM key bindings rather than assuming the config defaults replaced them.
- Equip a weapon whose exact, group or default profile has
enabled = true. - Aim, if
requireAim = true, and stay below peek.maxSpeed. - Use an allowed ped model, and close NUI, the pause menu, scenarios and other scripted cameras.
- Read
GetPeekReason(), and check F8 for an animation load error.
GetPeekReason returns body_clearance
The side probe found geometry inside that profile's clearance distance. Move away
from the wall, or reduce the profile value carefully. If it appears permanently in open space,
check F8 and confirm the client is running the complete matching v1.1.1 package.
The camera does not move
- Confirm
peek.camera.enabled = true. - Check whether another resource owns a scripted camera.
- Check whether the body lean is blocked by clearance or eligibility.
- Confirm the weapon profile has suitable
cameraLeft and cameraRight values. - Check
GetPeekSide() — a waiting request reports 0 until it is active.
The camera or recoil snaps while firing
Search the recoil resource for direct SetGameplayCamRelativePitch calls, route the
increment through ApplyRecoil, and do not also apply native pitch when the export
returns true.
Lean works with one weapon but not another
Resolution is exact weapon, then group, then default. Check for an exact override and confirm
its group is not disabled — sniper, heavy, thrown, melee and unarmed are off by default.
The player cannot grab a ledge
- Confirm
ledge.enabled = true, and jump toward the wall within one of ledge.angles. - Test an edge inside
minReach..maxReach relative to the ped root. - Approach below
maxFallSpeed and with enough forward intent. - Test a flat, static roof edge rather than a moving or attached prop, and confirm the surface does not continue above the apparent edge.
- Confirm no framework, state bag or export blocker is active, then enable
debugCommands and use /ledgedebug.
The player climbs windows, or repeats upward on a flat wall
Use the complete v1.1.1 package: this version rejects a wall that continues above a false
top and bounds retries within one airborne session. Do not mix its config with an older bundle.
The player slides or drops automatically at roof edges
- Confirm
antiSlide.enabled = true. - Disable other parkour and edge resources while testing.
- Test while moving — anti-slide applies while the player is moving or airborne.
Framework states are ignored
- Start the framework before Movement when using
auto, or better, set it explicitly. - For Qbox death and last-stand checks, make sure
qbx_medical is running. - For a custom framework, return real booleans from the bridge.
- Add your server's restraint and inventory state keys to
blockedStates.
SetPeekingBlocked(false) does not unblock it
Every calling resource owns its own blocker, so another resource may still hold one. A
configured state bag may also still be truthy, and SetEnabled(false) is a separate
global disable.
A custom ped does not animate correctly
The allowlist only permits execution. The included YCD is authored for freemode skeletons, so
either remove the custom model or author and test a compatible animation.
When you ask for support
Include the resource version, the framework and whether it is explicit or auto, the relevant
config.lua section, the ped model and weapon name, the output of
GetPeekSide() and GetPeekReason(), client F8 errors, whether it
affects left, right, ledge climbing or everything, whether it persists with other camera,
recoil, emote and parkour resources stopped, and the exact steps to reproduce it. Never post
Tebex credentials, Cfx license keys, protected code or private server credentials.