legacy-towing
Troubleshooting
Known gotchas and how to diagnose them.
Troubleshooting
Diagnostic command: /hitchdebug
Client-side, read-only, no permission required (client/hitch.lua). Run it while in a vehicle to print:
- Your vehicle's model,
GetVehicleType, and whether you're in the driver seat. - Whether
Config.Hitch.maleBoneexists on your vehicle (or a fallback-to-model-rear warning). - Any trailer currently being towed.
- For every vehicle within 30m: its name, whether it has a usable coupling (bone or
GetVehicleType == 'trailer'fallback), whether it's already attached to something, and the hitch-to-hitch distance.
Use this whenever "the hitch prompt doesn't appear" to tell apart: no tow ball, no coupling on the trailer, wrong seat, or simply out of Config.Hitch.range.
One vehicle attaches at the wrong offset (looks like a slot bug, isn't)
Every offset/rotation value that reaches AttachEntityToEntity (or any float native) must be coerced with + 0.0. This resource runs on lua54, where integers and floats are distinct types. A whole-number offset (e.g. 2 instead of 2.22) that survives a JSON round trip decodes as an integer, and natives silently read it as garbage — typically manifesting as one cradle out of four snapping its vehicle to x=0 while the others work fine. This is a type bug, not a slot-assignment bug. This applies to config values (offsets/rotations/dimensions from rigs.json), attach payloads, and winch/ramp offsets alike.
Attaching / carrying vehicles
- Attachments are applied per-client, not just by whoever parked the vehicle —
client/carry.lualistens to both a statebag (legacyTowCarrier) and the directlegacy-towing:client:carryevent, because attaching an entity is not reliably replicated by the engine. Don't write competing code that also tries to attach carried vehicles. HasRoom()success doesn't guarantee attach success. For slot-mode rigs, the room check and the actual slot claim (legacy-towing:server:freeSlot) are two separate round trips — another player can take the last free cradle in between. Always checkAttachToRig()'s own failure reason.Config.Carry.checkOverlap = falsedisables the footprint/overlap safety check for dynamic (non-slot) rigs entirely — without it, unlimited vehicles can be driven into and ghost through one already loaded on a flatbed, since attached vehicles stop colliding with others.- Slot-mode rigs never run the overlap test (by design), since a vehicle snaps to an authored, non-overlapping cradle.
Admin panel
rigByModel()matches spawned vehicles by model hash proximity search, not the client-side rig registry — a trailer added via "Add the vehicle I'm aiming at" only enters the registry once the config is saved. Don't expect to look up a newly-detected rig by registry before saving.- Saving an empty rig list over a non-empty existing config is accepted (only warned to console, not rejected). Recovery is only possible via
data/rigs.backup.json, which is overwritten on every save — back it up before bulk edits. validate()accepts both the current polygon deck shape (deck.points+minZ/maxZ, ≥3 corners) and a legacy two-corner box (deck.a/deck.b). Hand-authored/migrated JSON that doesn't match either shape produces "needs a deck outline of at least 3 corners", even for what looks like a valid box.
Hitching
- The hitch system deliberately does not require
attach_male/attach_femalebones — it falls back to model dimensions andGetVehicleType. Don't add a bone-presence gate to the prompt; it previously hid the prompt for common vehicles with noattach_malebone that tow fine. - Trailer-in-use detection uses
IsEntityAttachedToAnyVehicle, deliberately notIsVehicleAttachedToTrailer(the latter asks the wrong question and would let you steal a trailer mid-tow). - Before
AttachVehicleToTrailer, the client explicitly requests network control of the target trailer and waits up to ~500ms. Skipping this causes hitching to silently fail near other players with a misleading "back up squarer" message, because a write to an entity owned by another player is silently reverted. Config.Hitch.keydefaults to74(INPUT_VEH_HEADLIGHT/H). WithsuppressKeyConflictleft at its defaulttrue, the raw headlight control is disabled while the prompt shows; setting itfalsemeans pressing H near the prompt also toggles your headlights. This key is only read while the prompt is up — there's no global rebindable keybind.- The whole file no-ops (
Config.Hitchmissing orenabledfalsy) with no other warning — if hitching "doesn't exist", check this first.
Ramps
- Ramp props are frozen world objects, not attached to the trailer — attached objects don't reliably collide with other vehicles, so a welded-on ramp would let cars drive through it. This means ramps do not follow the trailer and are auto-stowed once it drives
Config.Ramps.stowDistanceaway from the drop point. extra/doorramp modes must go throughlegacy-towing:server:rampsand be applied from thelegacyTowRampsstatebag by every client —SetVehicleExtra/vehicle doors do not reliably replicate from a non-owning client. Mutating them directly client-side only shows the change to the player who clicked.SetVehicleExtrais inverted:0turns an extra on,1turns it off. Always useRampSetExtra/RampExtraOn— never call the native directly.Config.Rampsonly applies toramp.mode == 'props'rigs;'extra'/'door'rigs bypass all ofConfig.Ramps's settings.- The stow watcher only reports to the server on an actual measured drive-off past
stowDistance— a rig merely streaming out of one client's range does not clear ramps-down state server-side.
Winch
- The winch is a purely local rope (
AddRopeis not networked) — every client, not just the operator, must run the shared session/rope code inclient/winch.lua, drawing its own rope from the server-broadcast fact of the session (legacy-towing:client:winch/legacyTowWinchstatebag). - Pulling a vehicle in requires the operator to hold client-side network ownership of it. If another player owns the vehicle,
SetEntityVelocitycalls are silently reverted with no error — easy to miss when testing alone, since you almost always own whatever you're standing next to. - The rig is frozen by every client for the session's duration (skipped if a ped is driving it), because freezing only takes effect for whoever owns the entity locally.
- Rope length can only reliably be paid out — shrinking requires deleting and respanning the rope, which is asynchronous (up to ~50 frames) so it never blocks the input loop.
- All rope natives throw a hard native exception (not a soft failure) if called on a non-live rope handle — every call site must be guarded with
DoesRopeExist. Config.Winch.maxMassis checked via the vehicle's handlingfMass(GetVehicleHandlingFloat) — there is no dedicated vehicle-mass native.- Securing a winched vehicle re-runs the full
CanCarry/HasRoom/AttachToRigcheck — a refusal at that point (e.g. a slot rig's last cradle taken in the meantime) leaves the vehicle still hooked rather than silently dropping it.
Server / config loading
- Config (
data/rigs.json) is loaded once, on this resource's own start. Restarting only a dependency, or hand-editingdata/rigs.jsonon disk, will not pick up changes — use the admin save flow or restartlegacy-towingitself. carried/winchesserver-side records are purged every 30 seconds for entities/players that no longer exist — don't rely on them persisting past that window once the underlying entity or player disconnects.- There is no ace/permission check on
legacy-towing:server:attach/detach/winch— these are treated as ordinary gameplay actions. The server does still re-validate entity existence and slot occupancy itself. - Server-to-server reads of carried state are impossible via
lib.callback(ox_lib's server-sidelib.callback.awaitis actuallytriggerClientCallback, which asserts a real player target) — this is exactly why theCarriedBy/HasRigexports exist.
Web / NUI build
- Build output goes to
web/dist(notweb/build) — make sure yourfxmanifest.lua'sui_page/filesreferencedist/, and remember tonpm run buildafter editingweb/srcor the NUI will serve stale assets. vite.config.jssetsbase: './'deliberately — this is required for the NUI to load assets inside the CEF browser; an absolute base will break in-game asset loading.- Accent theme colors (
accent,accent-gradient) are CSS custom properties, not fixed values — they must be injected (e.g. via NUI message fromlegacy-lib's accent color exports) or the theme will not render. - Tailwind's
contentglobs only cover./index.htmland./src/**/*.{js,jsx}— new file extensions (e.g..tsx) or files outsidesrc/won't be scanned, and their classes will be purged from production builds.