Skip to content

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.


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.

  • 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 that esx_skin and /skin understand; on QBCore and Qbox it drives the ped directly, needs no appearance resource, and writes to playerskins so 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.jpg and every player sees it, wherever they logged out — no config edit, the same wiring-by-file-name as property.jpg. To keep it elsewhere or use .png/.webp, set Config.Spawns.lastImage, which is the card’s equivalent of image on a location. With no file and no option set, the card borrows the photo of whichever configured location the character logged out nearest to.
  • 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.
  • RENTED and SHARED tags 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.custom accepts six optional columns beyond the required ones — interior, rent, shared, district, description and defaultKind — all documented in CONFIG.md. A column named there that does not exist in your database is dropped rather than breaking the query.
  • 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; ReapplyTattoos asks the running one to repaint.
  • Starter outfits built in the creator are published into the server’s wardrobe store where one is running.
  • Config.StarterItems gives a brand new character its first items, against ox_inventory, qb-inventory or the ESX default. One block per framework, each with its own switch; left on "auto" the list stays wherever that framework already keeps it.
  • 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 the outfit_starter_name locale key.

  • Config.Outfits takes 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 reads Style 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 -1 still 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.Outfits is the only table that works this way. Config.Default, Config.Heritage and 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.Debug on rather than done silently.

  • The schema is created on first start. install.sql ships 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 — citizenid on QBCore and Qbox, identifier and owner on ESX — leaves behind every resource that files rows under a different name. npwd is the worst of it: seventeen tables, all keyed on identifier, not one foreign key to players between 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_id or player_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_likes and npwd_twitter_reports all hold a RESTRICT key on npwd_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.DeleteSweep controls it, including an enabled = false that goes back to the framework’s own column alone. The list of column names ships in the code as well as in config.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_codes on Qbox holds a player_outfits.id and 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.ExtraDeleteLinks is where such a relationship is written down; the player_outfit_codes one is built in.

  • Text conversations do not survive the character who was in them. npwd joins a conversation by phone number, so npwd_messages_participants holds 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.PhoneDeleteTables is 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.ExtraDeleteTables entries 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. /mcorphans lists them by table; /mcorphans clean removes 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.

  • 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 — and npwd_twitter_profiles.profile_name is 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. getOrCreateProfile then 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.generateProfileNameFromUsers off 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’s config.json is 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, a maxSuffix ceiling on the numbering, and reclaimOrphans = false for a server that would rather number a live character than clear out a deleted one. mcdiag prints a phone handles line saying which of those states this server is in.

  • 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.
  • One setting, in section 1 of config.lua. Config.Locale names any file in locales/, including one you wrote yourself, and that is the language the whole resource speaks — server messages, chat lines and the character screen alike. "auto" reads server.cfg instead — nuggs:locale, then esx:locale, then qb:locale — for a server that already sets a language there for its other resources. A convar set with set rather than setr cannot be read by clients, so the server replicates whatever it resolves and both halves agree. A config with no Config.Locale in it at all is treated as "auto".
  • locales/*.lua is unencrypted and yours. Copy locales/en.lua to locales/<code>.lua, change the table key on the first line, translate the values. The manifest picks new files up by glob and escrow_ignore covers 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 mcdiag print a locales line naming the active language, where it was configured, and every language that loaded.
  • Config.Locale is matched against what actually loaded without regard to case or to - versus _, and a regional code falls back to its base language, so pt-BR finds locales/pt.lua when there is no locales/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 UI half 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 %s for the place, filled from spawn_property_where_street, spawn_property_where_district or spawn_property_where_unknown. There is a spawn_tag_* for every kind, plus spawn_tag_rented and spawn_tag_shared.
  • outfit_starter_name names the outfit kept for a player who saved none of their own, and UI.outfit_from_suggested is the tooltip on a tile filed from a suggested outfit.
  • customer_client.lua and customer_server.lua ship unencrypted and carry the supported export surface. See INTEGRATION.md.
  • config.lua, config.lua.example, locales/*.lua and the bundled fonts are yours to edit and are not encrypted.
  • mcdiag reprints the startup report at any time, and mcdiag debug toggles Config.Debug without a restart. That block is what to attach to a support ticket.
  • 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 every exports.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 in mcdiag, 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.lua and shared/resource.lua. Both are encrypted and are not in escrow_ignore, so the name cannot be changed by editing them; removing shared/lock.lua from the manifest removes the explanation, not the lock.

    A bracketed parent directory is fine — [qbx]/nuggs_multicharacter is named nuggs_multicharacter. The usual cause is a suffix an unzip added by itself: -main, -master, (1), a version number.