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.

  1. Extract the delivered 9labs_movement folder into your resources directory.
  2. Start it after your framework.
  3. Configure config.lua, then restart the resource after any configuration change.
  4. 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.

  1. Equip a supported firearm.
  2. Hold aim.
  3. Hold Q to lean left or E to lean right.
  4. 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:

  1. Restart 9labs_movement at a controlled maintenance time.
  2. Reconnect if the streamed YCD changed.
  3. Test left and right peek with a pistol and a rifle.
  4. Test the recoil adapter while neutral and while leaning.
  5. Test ledge climbing on a low and a high reachable edge.
  6. Test every resource that blocks Movement.

Modes and gating

Four settings decide when the lean is allowed at all.

Hold or toggle

ModeBehaviour
holdLean while the bound key remains held.
togglePress 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,
    },
}
FieldMeaning
enabledWhether peeking is allowed with this weapon or profile.
leanAnimation weight from 0.1 to 1.0.
cameraLeftLeft-side camera extension, in metres.
cameraRightExtension beyond the native right shoulder, in metres.
clearanceRequired 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,
}
KeyMeaning
enabledEnables automatic ledge detection and climbing.
climbFastPasses the fast/launch flag to GTA's native climb task.
cooldownDelay in milliseconds after a completed attempt before another catch.
maxReachHighest accepted top above the ped root, in metres.
minReachLowest accepted top relative to the ped root, in metres.
wallDistanceBase forward distance used to search for a wall.
wallProbeHeightHeight of the lowest horizontal wall probe above the ped root.
anglesDirections checked relative to ped heading, in degrees.
minApproachRequired speed toward the wall, in metres per second.
noRagdollWhileFallingTemporarily preserves control during eligible falls.
maxFallSpeedDownward speed above which a ledge is not caught.
debugCommandsEnables 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.

CommandPurpose
/ledgedebugToggle probe visualization and the current rejection or state text.
/ledgeset <key> <value>Change a supported geometry value for this client session.
/ledgedumpPrint 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.

ValueAdapter
autoDetects Qbox, QBCore, ESX, ox_core, then falls back to standalone.
qbxQbox (qbx_core, optionally qbx_medical).
qbQBCore (qb-core).
esxESX (es_extended).
oxox_core.
standaloneNative client checks only.
customEditable functions in client/bridge.lua.

Detection order in auto mode:

qbx_core -> qb-core -> es_extended -> ox_core -> standalone

Built-in state checks

FrameworkLoadedDead / last standHandcuffed
QboxLocalPlayer.state.isLoggedInqbx_medical:IsDead() / IsLaststand() when runningQbox metadata
QBCoreLocalPlayer.state.isLoggedInQBCore metadataQBCore metadata
ESXESX.IsPlayerLoaded()ESX.PlayerData.deadNative cuff check only
ox_coreAlways loadedLocalPlayer.state.isDeadNative cuff check only
standaloneAlways loadedNative ped death checksNative cuff check
customCustom bridgeCustom bridgeCustom 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

ReasonMeaning
idleNo peek request has run yet.
pressedA valid direction request was accepted.
peekingThe requested side is active.
releasedThe input returned to neutral.
waiting_aimThe key is held but aim is required.
waiting_movementThe key is held but the player is moving too quickly.
clearance_pendingA side clearance query is still resolving.
body_clearanceGeometry blocks the requested body lean.
blockedAn export or resource blocker is active.
parkourThe ledge module took over movement.
ineligibleA player, weapon, view or gameplay state rejected peeking.
ped_changedThe local ped changed during the active task.
animation_interruptedThe animation could not be maintained after bounded retries.
camera_stoppedThe shoulder camera could not continue safely.
disabledMovement was disabled through SetEnabled(false).
errorA 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.

SituationResult
Movement shoulder camera activeMovement consumes the pitch increment.
Normal aiming or neutralThe recoil resource uses its native fallback.
Movement stopped, or an older versionThe protected pcall falls back safely.
Invalid or excessive incrementThe 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.