legacy-cityhall
Troubleshooting
Known gotchas and pitfalls when running or integrating with legacy-cityhall.
Troubleshooting
Startup / dependency ordering
legacy-libmust start beforelegacy-cityhall. The client script callsexports['legacy-lib']:Target()at resource start to wire up NPC interactions — iflegacy-libisn't running yet, this errors.
NPCs, blips, and config changes
- NPCs and blips are only spawned once, 1000ms after resource start. There's no exported function to force a respawn — if you edit
Config.Locations, you must restart the resource. LoadModel()blocks the calling thread for up to 5000 ticks waiting for a ped model to stream in. An invalid or slow model prints a warning and delays that one location, but doesn't stop others from spawning.- Every entry in
Config.Locationsneeds all ofcoords,model,label,scenario, andblippopulated — there's no nil-checking, so a missingbliptable or bad sprite/color can error or produce a broken blip.
NUI issues
OpenUI,CloseUI,LoadModel,SpawnLocations, andCleanupare local, not exported — you cannot call them from another resource. Use the NPC interaction or replicate the open sequence withlib.callback.await('legacy-cityhall:getInitData', false)+SendNUIMessage.- Every NUI callback (e.g.
submitApplication,orderDocument,registerJob) forwards to a like-named server callback prefixedlegacy-cityhall:vialib.callback.await. If you rename or remove a server callback, update the matching client-sideRegisterNUICallbackor the NUI action will hang / returnnilwith no error. isOpenis a module-local flag on the client. If the NUI closes itself without going through theclosecallback (out-of-bandSetNuiFocuschange),isOpencan get stucktrueand block the nextOpenUI()call untilonResourceStop's cleanup runs.- The web UI (
web/dist) is a build artifact. Editing files underweb/srchas no effect in-game until you runnpm run build. If the NUI shows nothing and there's no Lua-side error, check thatweb/distwas actually rebuilt and shipped. - Never run
npm run devagainst a live FiveM client — it's a browser-only dev server, not something CEF can load as the shipped NUI. vite.config.jssetsbase: './'— required for NUI asset paths to resolve in CEF. Changing it to an absolute path breaks the UI in-game.- If themed colors don't appear after a rebuild, check that the CSS variables (
--accent,--accent-gradient) referenced bytailwind.config.jsare actually defined in your stylesheet — the Tailwind config alone doesn't supply them.
Locale strings
locales/en.jsonkeys (e.g.notify_app_accepted,notify_doc_issued,notify_insufficient_funds) are filled with%splaceholders by server-side code, positionally. Renaming or removing a key without updating the corresponding server/client reference will silently break that notification — it'll show the raw key or blank text instead of erroring.- Locale selection requires both a valid
locales/<name>.jsonfile present andConfig.Localeset to match its filename stem (e.g.'en'). There's no locale loader/index symbol to inspect at runtime.
Admin permissions
Config.AdminGroupsgates all admin-only exports/callbacks via ACE groups. If a group name here doesn't match an actual ACE group assigned on your server, admins are silently denied — there's no config-time validation or obvious error.
Document provider
Config.DocumentProvideronly meaningfully supports'legacy-idcard'in this version. Setting it to anything else (including the mentioned-but-unimplemented'qbx_idcard') will likely break document issuance/verification since no other provider integration ships with this version.
Direct exports vs. callbacks diverge in behavior
RegisterJob(export) does not reactivate a soft-deleted job — it simply no-ops (nil) if acityhall_jobsrow with that name already exists. Use thelegacy-cityhall:registerJobcallback if you need reactivation.IssueDocument(export) skips pricing, provider invocation, licence checks, background-check status, and serial uniqueness checking (unlike theorderDocumentcallback).issued_byis hardcoded to'system'.RevokeDocument(export) performs no permission check, doesn't callProvider.revoke, and doesn't notify the affected player — the caller is fully responsible for gating access.UpdateApplicationStatus(export) bypasses the status-transition state machine used by the callback and does not auto-hire the player onaccepted— you must call the hiring logic yourself.CreateApplication(export) performs none of the framework job/cooldown/max-active-application checks that thesubmitApplicationcallback enforces.
Database / schema
sql/schema.sqlstarts withDROP TABLE IF EXISTSfor all 7 tables in reverse dependency order. Re-running it on a live install wipes all data with no confirmation prompt.- Foreign keys cascade: deleting a
cityhall_jobsrow silently deletes its listing and all applications; deleting an application silently deletes its notes. - Seed files (
sql/seed_jobs*.sql) only insert intocityhall_jobs/cityhall_listings— they do not create the job in your framework's job list. You must add matching entries (e.g. inqbx_core/shared/jobs.lua) yourself. seed_jobs_17mov.sql'selectricianjob has an undocumented hard dependency on the separate Howdy-Minigame resource plus a manualALTER TABLE players ADD COLUMN electrocourses int(11) DEFAULT 0;— omitting either won't break the seed import but will break the electrician job flow at runtime.seed_jobs_sadot.sqldefaultssadottotype='public'(self-hire, no vetting) — for a money-handling job you likely want to manually edit the SQL before import totype='application'andrequire_interview=1.job_nameis unique acrosscityhall_jobs. If two seed files target the samejob_name, both imports succeed (ON DUPLICATE KEY UPDATE) but the second import silently overwrites the first's label/type/is_active.