Changelog
Config versions are noted per release. When the console warns that your
config.lua is behind, diff it against config.lua.example and read every entry
between your version and the current one.
1.0.0 — config version 1
Section titled “1.0.0 — config version 1”First public release. fxmanifest.lua declares version '1.0.0' and config.lua
declares Config.Version = 1; the two are checked against each other on start.
Character screen
Section titled “Character screen”- Selection stands every character side by side as a real ped under a fixed camera. Picking a card slides the line along, so slot 1 and slot 20 are framed identically however many slots you sell.
- A nine-step creator drives the live ped. On ESX it drives
skinchanger, so the result stays a normal ESX skin thatesx_skinand/skinunderstand; on QBCore and Qbox it drives the ped directly, needs no appearance resource, and writes toplayerskinsso barbers and clothes shops see it. - Arrival points open as a rail of illustrated cards, with the camera flying out to whichever is highlighted. A location with no screenshot falls back to built-in vector artwork.
- Characters can start at a property they own, read from whichever housing or motel scripts are running.
- The Last Known Location card can have a picture of its own. Drop a file at
html/images/spawns/last.jpgand every player sees it, wherever they logged out — no config edit, the same wiring-by-file-name asproperty.jpg. To keep it elsewhere or use.png/.webp, setConfig.Spawns.lastImage, which is the card’s equivalent ofimageon a location. With no file and no option set, the card borrows the photo of whichever configured location the character logged out nearest to.
Arrival points and properties
Section titled “Arrival points and properties”- Property cards are built from the property rather than the nearest authored arrival point, so the street and district are the ones reported at the front door. A property nowhere near an authored point is still described correctly.
- A property is named for what it is — home, apartment, penthouse, motel, trailer, room, garage, warehouse, office, clubhouse or boat — read from the housing script’s type column, interior name and label, each with its own tag and description.
RENTEDandSHAREDtags on housing scripts that record either.- A housing script that stores its own region or description per property is believed ahead of anything worked out here. On ps-housing that means a card shows what the owner wrote about their own house.
- Card artwork follows the zone the property is in rather than the nearest authored point.
Config.Housing.customaccepts six optional columns beyond the required ones —interior,rent,shared,district,descriptionanddefaultKind— all documented in CONFIG.md. A column named there that does not exist in your database is dropped rather than breaking the query.
Frameworks and integrations
Section titled “Frameworks and integrations”- ESX Legacy, QBCore and Qbox from one build, detected at boot. Override with
Config.Framework. - Clothing, tattoo and housing resources are detected rather than configured, and named in the startup report.
- Dedicated tattoo scripts that own ped decorations are handed ownership instead of
being fought over;
ReapplyTattoosasks the running one to repaint. - Starter outfits built in the creator are published into the server’s wardrobe store where one is running.
Config.StarterItemsgives a brand new character its first items, againstox_inventory,qb-inventoryor the ESX default. One block per framework, each with its own switch; left on"auto"the list stays wherever that framework already keeps it.
Outfits and the wardrobe
Section titled “Outfits and the wardrobe”-
Clicking a suggested outfit files it into a starter outfit slot as well as dressing the character, so a player who picks a look on IDENTITY and never opens the wardrobe panel on CLOTHING still arrives with something in their wardrobe. The look is saved under the preset’s own name.
Clicking the same suggestion twice reuses its slot rather than taking a second, so all four suggestions fill four slots and the wardrobe never holds the same outfit twice. An empty slot is always taken before anything is displaced, and a slot the player saved or named themselves is never touched — pressing SAVE over a filed tile, renaming it or clearing it makes it theirs, and no later click will recycle it. Filed tiles are drawn with a dotted edge so the two kinds are told apart at a glance. Turn it off with
Config.OutfitSlots.autoSaveSuggested. -
A character cannot reach the world with an empty wardrobe. If the player saved nothing at all, what they have on when CREATE is pressed is kept as slot 1. Their clothes are saved either way — this is about the wardrobe having something in it, because the first job uniform, revive or prison change leaves a player with no saved outfit nothing to change back into. Turn it off with
Config.OutfitSlots.guaranteeOutfit; the slot is named by theoutfit_starter_namelocale key. -
Config.Outfitstakes style numbers — the numbers printed on the CLOTHING step — rather than raw drawable indices, because walking the creator and copying what each spinner reads is the only practical way to write a preset. A spinner shows the drawable it holds plus one, so drawable 0 readsStyle 1, and the resource takes that one back off before the value reaches the ped.A style number starts at 1, so anything 0 or below was never on screen: it is left alone and reaches the ped as written, and
-1still means an unworn prop. Any key with a spinner on any creation step may be used, not only the garment keys — the accessories on DETAILS work the same way.Config.Outfitsis the only table that works this way.Config.Default,Config.Heritageand the starter outfit slots hold raw skinchanger values — the game reads and writes those rather than you typing them off the screen, which is the difference. -
A suggested outfit naming a garment your server does not have is reported. It cannot be worn, so the character is dressed in the highest style that does exist — which reads as the preset having odd taste rather than as a number needing correcting. The console names the preset, the key and both numbers, once per preset per session.
-
Clothing is saved at the number that was picked. The server-side whitelist every framework’s save runs through carries ceilings far above anything a ped can currently wear. They exist to keep a hostile number out of the database, not to guess how many garments a server streams — the creator already clamps every value to the live maximum it reads off the ped, which is the only bound that is actually true. This matters most on the female freemode torso, which vanilla alone runs past 400 on, on head overlays, where vanilla makeup alone is past 70, and on any server streaming a clothing or hair pack. A clamp that does change a garment is traced with
Config.Debugon rather than done silently.
Characters, deletion and the database
Section titled “Characters, deletion and the database”-
The schema is created on first start.
install.sqlships as a reference for anyone who would rather run the statements by hand, and to document what the resource does on its own. It does not have to be run. -
Deleting a character removes that character’s rows across the whole schema. Sweeping the framework’s own key column alone —
citizenidon QBCore and Qbox,identifierandowneron ESX — leaves behind every resource that files rows under a different name. npwd is the worst of it: seventeen tables, all keyed onidentifier, not one foreign key toplayersbetween them.The sweep reads the whole schema instead. It finds every table filing rows under any of
citizenid,identifier,owner,user_identifier,charidentifier,charid,char_id,character_id,citizen_idorplayer_identifier; follows every foreign key that would block a delete, from each of those tables and not only from the character row; and runs the whole lot in the order the constraints demand, character row last.That ordering is not a nicety.
npwd_twitter_tweets,npwd_twitter_likesandnpwd_twitter_reportsall hold a RESTRICT key onnpwd_twitter_profiles, so simply adding the profile table to the delete list fails with error 1451 and takes the entire character delete down with it — nothing at all gets removed.Only string columns are ever matched. MySQL compares a string with an integer column by reading the string as
0, so a character key aimed at a numeric column would delete every row holding a zero.Config.DeleteSweepcontrols it, including anenabled = falsethat goes back to the framework’s own column alone. The list of column names ships in the code as well as inconfig.lua, so a server whose config is out of date still deletes properly. -
Rows pointing at a character’s rows through an undeclared relationship.
player_outfit_codeson Qbox holds aplayer_outfits.idand illenium-appearance even names the index after the constraint — then never creates it. Nothing in the database says the two are related and the column is an integer, so neither the constraint walk nor the name sweep can see it, and the code would outlive the outfit.Config.ExtraDeleteLinksis where such a relationship is written down; theplayer_outfit_codesone is built in. -
Text conversations do not survive the character who was in them. npwd joins a conversation by phone number, so
npwd_messages_participantsholds nothing saying whose row it is. The number is read off the character before the row holding it goes, and the rows filed under it are cleared with the rest.Config.PhoneDeleteTablesis the list; npwd’s table is built in. Only the character’s own number is ever swept — a call log keyed on who was rung is somebody else’s row about this character and is not ours to take. -
A delete that quietly removes nothing is reported. A statement can run, raise no error and match no rows because what it was looking for is filed under something the sweep does not recognise. Every swept table is checked afterwards and any that still holds the key is named in the console, with what to do about it. Turn it off with
Config.DeleteSweep.verify = false. -
A table named for deletion that the database does not have does not take the whole transaction with it.
Config.ExtraDeleteTablesentries and links are checked against the schema first, and anything missing is reported once and skipped. -
/mcorphans, for the rows a previous character screen left behind. Every other multicharacter resource deletes on the framework’s own column alone, so a server moving to this one is already carrying rows for characters that no longer exist — and they go on causing the same UNIQUE-index trouble whatever the delete does from here./mcorphanslists them by table;/mcorphans cleanremoves them.A column is only ever cleaned when at least one key in it belongs to a character who still exists. That is what tells
npwd_twitter_profiles.identifier, which is full of character keys, apart from an account-key column, every row of which would otherwise look like an orphan. A column that fails the test is reported and left alone.
Phone handles
Section titled “Phone handles”-
npwd twitter handles are claimed by this resource, before npwd asks for one. npwd builds a handle out of the character’s name the first time the app is opened —
Jessy_Pinker— andnpwd_twitter_profiles.profile_nameis UNIQUE. That INSERT is the only thing that ever creates a profile and there is nothing behind it, so when the name is already on the index the player gets no profile at all:[NPWD] [twitter] [error]: Failed to get or create profile,Duplicate entry 'Jessy_Pinker' for key 'profile_name_UNIQUE'Two things put a name on that index and leave it there: a character deleted by another multicharacter resource, whose npwd rows were never cleared and whose handle is held forever by somebody who no longer exists; and two live characters who are genuinely both called Jessika Brown, which no amount of cleaning up will ever fix.
The handle is worked out and written as each character loads.
getOrCreateProfilethen finds a profile already filed under the character and returns it, and the INSERT that would fail is never reached. A name nobody holds is claimed exactly as npwd would have written it, character for character — the same[^0-9a-z]strip, the same_between the names, the same fall back to the phone number. A name held by a character who no longer exists has that character’s leftovers cleared out first. A name held by a character who does still exist is numbered —Jessy_Pinker2— and the player can rename themselves in the app like anybody else.For everybody after their first login it is one indexed SELECT, and the whole thing turns itself off on a server without npwd installed. Nothing in npwd is edited and nothing in npwd has to be. A server already getting the error needs no clean-up either: the handle is freed the next time the character who wants it logs in.
A server that has turned npwd’s own
twitter.generateProfileNameFromUsersoff is left alone — that setting means players are meant to pick their own handle in the app, and the duplicate npwd reports in that mode is it answering the UI properly rather than the failure above. npwd’sconfig.jsonis read to find out; if it cannot be read, npwd’s own default of on is assumed.Config.PhoneProfiles, on by default, carries the table and column names for a fork that has moved them, amaxSuffixceiling on the numbering, andreclaimOrphans = falsefor a server that would rather number a live character than clear out a deleted one.mcdiagprints aphone handlesline saying which of those states this server is in.
Membership and slots
Section titled “Membership and slots”- Slot counts resolve per account from Tebex purchases, Discord roles, ACE and staff grants, through a cache.
- Tebex package names and prices are pulled from the Plugin API, so a store card cannot advertise a price the checkout disagrees with. Purchases are matched by transaction id, which is UNIQUE, so a retried delivery cannot pay out twice.
- Discord roles map to tiers by id or by name, with roles named after a tier matched automatically.
Languages
Section titled “Languages”- One setting, in section 1 of
config.lua.Config.Localenames any file inlocales/, including one you wrote yourself, and that is the language the whole resource speaks — server messages, chat lines and the character screen alike."auto"readsserver.cfginstead —nuggs:locale, thenesx:locale, thenqb:locale— for a server that already sets a language there for its other resources. A convar set withsetrather thansetrcannot be read by clients, so the server replicates whatever it resolves and both halves agree. A config with noConfig.Localein it at all is treated as"auto". locales/*.luais unencrypted and yours. Copylocales/en.luatolocales/<code>.lua, change the table key on the first line, translate the values. The manifest picks new files up by glob andescrow_ignorecovers the whole folder, so a language you write survives an update.- Every locale file in the folder is loaded at start, whichever language it is
for. The startup report and
mcdiagprint alocalesline naming the active language, where it was configured, and every language that loaded. Config.Localeis matched against what actually loaded without regard to case or to-versus_, and a regional code falls back to its base language, sopt-BRfindslocales/pt.luawhen there is nolocales/pt-br.lua. A locale named in the config with no file behind it is named in the console, with the list of languages that did load, rather than silently ignored.- Missing keys fall back to English per key, on the Lua side and in the interface alike, so a partial translation is safe to ship and a translation written against an older build keeps working when a new string is added. An empty string counts as a key nobody has reached yet, and English is used for it.
- Locale keys are found in both the top-level and the
UIhalf of a locale file, so a key does not have to be in the half the code happens to ask from. spawn_property_blurb_<kind>describes each property kind, each taking a single%sfor the place, filled fromspawn_property_where_street,spawn_property_where_districtorspawn_property_where_unknown. There is aspawn_tag_*for every kind, plusspawn_tag_rentedandspawn_tag_shared.outfit_starter_namenames the outfit kept for a player who saved none of their own, andUI.outfit_from_suggestedis the tooltip on a tile filed from a suggested outfit.
For developers
Section titled “For developers”customer_client.luaandcustomer_server.luaship unencrypted and carry the supported export surface. SeeINTEGRATION.md.config.lua,config.lua.example,locales/*.luaand the bundled fonts are yours to edit and are not encrypted.mcdiagreprints the startup report at any time, andmcdiag debugtogglesConfig.Debugwithout a restart. That block is what to attach to a support ticket.
The folder name is locked
Section titled “The folder name is locked”-
The resource folder must be named
nuggs_multicharacter. Exports are keyed on the folder name and cannot be aliased, so a renamed install would break everyexports.nuggs_multicharacter:...call and every documented integration event silently, hours after the restart that caused it. It is refused outright instead: a boxed error in the server console at boot and once a minute after it, the same banner in every client’s F8 plus a chat line, a leading line inmcdiag, every server event and the character screen inert, and every player turned away at the connection screen with the fix in the message.The check lives in
shared/lock.luaandshared/resource.lua. Both are encrypted and are not inescrow_ignore, so the name cannot be changed by editing them; removingshared/lock.luafrom the manifest removes the explanation, not the lock.A bracketed parent directory is fine —
[qbx]/nuggs_multicharacteris namednuggs_multicharacter. The usual cause is a suffix an unzip added by itself:-main,-master,(1), a version number.
