How Integrations Work#
Nothing inside Tattoo Creator talks to your framework, clothing script or inventory directly. Each one is reached through a small open bridge file — left unencrypted so you can read and change it — and the right bridge is picked at start by looking at what is already running.
Layer | Supported | Without it |
|---|---|---|
Framework |
| Standalone: players are known by their license and every tattoo is free until you connect an economy. |
Appearance |
| Tattoos still come back after a clothing change, a few seconds later instead of instantly. |
Inventory |
| Nothing is paused while the studio undresses the character. |
Target |
| Key prompts at every point of a studio — the default. |
Text UI |
| ox_lib's text UI and plain 3D text. |
HUD and progress |
| The minimap is hidden in the studio; progress bars use ox_lib. |
Database |
| Required. |
Every optional integration degrades silently. Detection only sees resources that are already started, so anything you want found must be above
ensure qs-tattoo-creatorinserver.cfg.
Frameworks#
ESX, QBCore and Qbox are detected on their own; Qbox runs through the QBCore bridge. Each bridge is a pair of files in custom/<framework>/ — client.lua and server.lua — answering a handful of questions: who this player is, how much money they have, take some, give some, is this player an admin, and what is their name.
Players are known by their ESX identifier and charged from the money or bank account. Admins are the groups in Config.AdminGroups.
Players are known by their citizen id and charged from cash or bank. Admins are players with the god or admin permission, or the command ace.
Players are known by their license: identifier and admins by the command ace. Money always succeeds, so tattoos are free: wire the three money functions in custom/standalone/server.lua to your own economy.
-- custom/standalone/server.lua
function framework:getAccountMoney(source, account) ... end -- account is 'money' or 'bank'
function framework:removeAccountMoney(source, account, amount) ... end -- return true when taken
function framework:addAccountMoney(source, account, amount) ... endRunning a renamed core, or another framework entirely? Copy the closest custom/<framework>/ folder under a new name, change the calls inside, and map your resource to it in config/config.lua:
local frameworks = {
['es_extended'] = 'esx',
['qb-core'] = 'qb',
['qbx_core'] = 'qb',
['my_core'] = 'mycore', -- loads custom/mycore/client.lua and server.lua
}Appearance Scripts#
A finished tattoo is a ped decoration, and clothing scripts clear every decoration whenever they rebuild a character — opening their menu, changing an outfit, reloading the skin. The appearance bridge is what puts ours straight back on top, and it listens for the moment each script finishes:
Script | How tattoos come back |
|---|---|
| Instantly. It announces every clear it makes ( |
| After every skin reload and outfit change. Set |
| After every skin reload, outfit change and clothing load they announce. |
| When it re-applies its own tattoos. |
Anything else | A watchdog notices a missing tattoo within seconds and adds it back, without ever clearing tattoos another script applied. |
How fast the watchdog and the retries run is in Settings → Re-apply.
Two appearance scripts at once#
Detection takes the first one found, which is not necessarily the menu your players open. Name the right one in config/config.lua:
Config.AppearancePin = 'illenium-appearance'Adding your own#
Copy the closest bridge
Every bridge is a small file in
client/custom/appearance/. Copy the one nearest to your script and name it after it.Change the event names
List the events your script fires when it has rebuilt the character. Each one puts our tattoos back:
lua if Config.Appearance ~= 'my-clothing' then return end local reloads = { 'my-clothing:client:skinApplied', 'my-clothing:client:outfitChanged', }Register it for detection
Add one line to the
appearanceslist inconfig/config.lua:lua { 'my-clothing', 'my-clothing' }, -- { resource name, bridge name }
Catalog tattoos in their menus#
Separately, an exported catalog tattoo is written into the appearance script's own tattoo list, so its menu offers it. The list of scripts that receive the catalog is server/custom/appearance.lua:
AppearanceCatalog = {
['illenium-appearance'] = { file = 'shared/tattoos.lua' },
['qs-appearance'] = { file = 'config/tattoos.lua', extra = { cost = 0 } },
-- any script built on illenium's Config.Tattoos layout: one line
-- ['my-appearance'] = { file = 'config/tattoos.lua' },
}The catalog is kept as a marked block at the end of that file; the rest of the file is never touched, and the script is restarted once when the block changes. Scripts that are not installed are skipped. A copy for wiring anything else lands in qs-tattoo-data/integrations/.
Inventories#
Some inventories turn the clothes a character wears into items. While the studio has a body part undressed, such an inventory would read a stripped character and hand out — or take away — clothing nobody touched. qs-inventory is paused automatically for as long as a tattoo menu is open, the same way qs-appearance does it.
For another inventory with the same behaviour, copy client/custom/inventory/standalone.lua, point it at its own switch, and add it to the inventories list in config/config.lua:
if Config.Inventory ~= 'my-inventory' then return end
function ToggleInventoryClothing(state)
exports['my-inventory']:setInClothing(state == true)
endTargets and Prompts#
By default every point of a studio is a key prompt, drawn while the player stands inside the studio area: the NPC ([E] Tattoo studio), the management point, the tattoo spot and a delivered supply crate. The key is Settings → Interaction → Studio key.
With qs-textui running, those prompts become its floating labels with a key badge. With ox_target or qb-target, switch on Settings → Interaction → Use a target resource and every point becomes a target option instead; the box size is set right below it.
Artists on servers without a target resource can also bind
/tattoo_inviteto a key: it invites the nearest player to the studio's tattoo spot from anywhere inside the studio.
Discord Logs#
Paste a webhook address per category in server/webhooks.lua; an empty one is off.
Category | What goes there |
|---|---|
| Tattoos players bought on their own at a studio's NPC, with a picture. |
| Artist sessions started, and tattoos inked by an artist with the price, the artist's share and the studio's share. |
| Tattoos waiting for review, approved and rejected. |
| Studios saved and deleted, tattoos removed by an admin, placement saved, catalog changes, settings changes, and every studio's staff, ranks, till, supplies, levels and upgrades. |
| Every time the tattoo catalog is published, and whether it worked. |
-- server/webhooks.lua
_G['webhook'] = {
purchase = 'https://discord.com/api/webhooks/...',
removal = '',
artist = 'https://discord.com/api/webhooks/...',
moderation = 'https://discord.com/api/webhooks/...',
admin = '',
publish = '',
}
_G['fivemanage'] = {
webhook = '', -- a Fivemanage API token: attaches the tattoo's picture to each log
}Each log names the player with their identifiers; their IP is added only with Settings → General → IP in Discord logs. Logs are queued and sent at Discord's pace, so they never slow the server down.
Your Own Content#
Everything in custom/ is yours and never overwritten by an update.
Fonts
Drop a .ttf, .otf or .woff2 file into custom/fonts/ and register it in custom/config.lua. The family name is what players see in the font picker, and category is one of script, gothic, serif, display or sans.
Config.FontsCustom = {
{ family = 'My Font', source = 'custom', file = 'MyFont.ttf', weights = { 400 }, category = 'display' },
}Gallery designs
Drop an .svg or .png — transparent background, ideally square, at most 2048 px — into custom/gallery/ and register it. category is one of the shipped ones (tribal, floral, animals, skulls, geometric, symbols, oldschool, minimal, ornaments), and tintable lets players recolour it — best with SVGs drawn in currentColor.
Config.GalleryCustom = {
{ id = 'my_logo', category = 'symbols', file = 'my_logo.png', tintable = false, tags = { 'logo' } },
}Image link sites
By default a player may link an image from any site; links into your own machine or local network are always refused. Switch off Settings → Images → Allow any host to accept only the sites in custom/config.lua (subdomains included):
Config.Images.AllowedHosts = {
'i.imgur.com', 'imgur.com', 'cdn.discordapp.com', 'media.discordapp.net',
'r2.fivemanage.com', 'api.fivemanage.com', 'i.ibb.co', 'raw.githubusercontent.com',
'media4.giphy.com',
}What the studio takes off
Config.Studio.Reveal in custom/config.lua lists, per character model and body part, the clothing components set to bare skin and the props removed. Everything is put back exactly as it was when the menu closes; nothing is saved on the player.
-- components: 1 mask, 3 torso/arms, 4 legs, 5 bag, 6 shoes, 7 accessories,
-- 8 undershirt, 9 armor, 10 decals, 11 tops
-- props: 0 hat, 1 glasses, 2 ears, 6 watch, 7 bracelet
left_arm = { components = { [3] = { 15, 0 }, [8] = { 15, 0 }, [11] = { 15, 0 } }, props = { 6, 7 } },The shipped numbers are the base game's bare body, and clothing packs streamed as their own collection leave them alone. A pack that replaces the base files may put a garment on one of these numbers — then use whatever number that pack left bare.
Languages#
41 translations ship in locales/, every text included — the studio, the management menu, the admin panel and the Settings panel's own descriptions: ar bg bn ca cs da de el en es et fa fi fr he hi hr hu id it ja ko lt lv ms nl no pl pt ro ru sk sl sv sw th tr uk ur vi zh.
Pick one in Settings → General → Language; it switches for every player at once. setr qs:locale "de" in server.cfg sets the default for every Quasar resource, and a language saved in the panel still wins over it. To change a wording, edit that language's file — a key missing from a translation falls back to English.



