Config
x-gangwar is configured from three files in config/.
| File | Loaded on | Content | Documented in |
|---|---|---|---|
config/config.lua | Shared | Settings, UI, loadout, factions, maps, rewards | This page and Maps & Rewards |
config/locales.lua | Shared | All texts, in three groups | Language |
config/config_server.lua | Server | Discord logging | Server config |
This page covers how a gangwar behaves. The content you place on the map — factions, maps, rewards and upgrades — is on Maps & Rewards.
General
Config.Debug = false
Config.DebugDummyJob = "gangwar_dummy"
Config.ESX = {
event = "esx:getSharedObject",
resource = "es_extended",
use_export = true
}
| Option | Default | Description |
|---|---|---|
Debug | false | Console output while placing maps or testing. Off in production |
DebugDummyJob | gangwar_dummy | A stand-in faction so you can start a fight against yourself while testing. Never give this job to a real player |
ESX.resource | es_extended | Your ESX resource name |
ESX.event | esx:getSharedObject | Legacy event, used when use_export is off |
ESX.use_export | true | Fetches the shared object through the export. Leave on for current ESX versions |
Interface
Config.UI = {
serverName = "X-STUDIOS",
menuSubtitle = "GANGWARS",
scoreSubtitle = "SCOREBOARD",
closeKey = "ESC",
tabs = {
gangwar = true,
rewards = true,
topfraks = true,
topplayers = true,
},
killfeed = { enabled = true, maxItems = 5, lifetime = 6000 },
scoreboard = { panelDuration = 4000, maxRows = 20 },
lists = { topPlayers = 100, topFraks = 100 },
images = {
jobFallback = "./assets/images/placeholder_img.svg",
mapFallback = "./assets/images/maps/map.webp",
rewardFallback = "./assets/images/items/item.webp",
},
}
| Group | Option | Description |
|---|---|---|
| Header | serverName | Large heading of the menu |
menuSubtitle / scoreSubtitle | Second line in the menu and on the scoreboard | |
closeKey | The key shown in the close hint — change it together with the key itself | |
tabs | gangwar, rewards, topfraks, topplayers | Switches whole sections of the menu off. A disabled tab is gone for everyone |
killfeed | enabled, maxItems, lifetime | Kill list during a fight: on/off, entries on screen, milliseconds per entry |
scoreboard | panelDuration | How long the result panel stays up, in milliseconds |
maxRows | Players listed on the scoreboard | |
lists | topPlayers / topFraks | Length of the two leaderboards |
images | jobFallback, mapFallback, rewardFallback | Pictures used when a faction, map or reward has none of its own |
Match settings
Config.Settings = {
minMember = 0,
maxMember = 0,
joinTime = 0.2 * 60 * 1000,
maxTime = 10 * 60 * 1000,
preparationTime = 5000,
topPlayersMinKills = 1,
}
| Option | Default | Description |
|---|---|---|
minMember | 0 | Members a faction needs before it may start a fight. 0 disables the check; a failed check shows not_enough |
maxMember | 0 | Maximum players per side. 0 means no limit |
joinTime | 0.2 * 60 * 1000 = 12 s | Window in which members can join the queue after a fight is announced |
maxTime | 10 * 60 * 1000 = 10 min | Maximum length of a fight |
preparationTime | 5000 | Countdown before the fight actually starts, in milliseconds |
topPlayersMinKills | 1 | Kills needed before a player appears on the leaderboard |
The times are written as a multiplication on purpose — 10 * 60 * 1000 reads as "10 minutes" and stays readable when you change it. Keep that style instead of writing 600000.
Time windows
Config.Time = {
unlimited = true,
allowedTimes = {
-- { start = 0, stop = 19 },
-- { start = 21, stop = 23 },
}
}
With unlimited = true a gangwar can be started around the clock. Set it to false and fill allowedTimes with windows in hours to restrict attacks to your prime time:
Config.Time = {
unlimited = false,
allowedTimes = {
{ start = 14, stop = 19 },
{ start = 20, stop = 23 },
}
}
Outside those hours a player gets the wrong_time message, which takes the allowed times as a placeholder.
Daily limit
Config.GWLimit = {
enabled = false,
max = 3
}
Caps how many gangwars one faction may start per day. Off by default; with enabled = true a faction that is out of attempts gets limit_reached.
The first entry in Config.Upgrades raises this limit to its own max value once a faction unlocks it — the shipped upgrade goes from 3 to 5.
Routing bucket
Config.BaseDimension = 3000
Fights run in their own routing bucket so nobody outside sees them. This is the base value that the buckets are counted up from. Keep the range clear of other resources that move players into buckets.
Loadout
Config.Weapons = {
["GADGET_PARACHUTE"] = {},
["WEAPON_PISTOL"] = { "COMPONENT_PISTOL_CLIP_02" },
["WEAPON_PISTOL50"] = { "COMPONENT_PISTOL50_CLIP_02" },
["WEAPON_PISTOL_MK2"] = {
"COMPONENT_AT_PI_COMP",
"COMPONENT_PISTOL_MK2_CLIP_02",
"COMPONENT_AT_PI_RAIL",
},
}
Config.DefaultWeapon = "WEAPON_PISTOL50"
Config.Items = {
["tracker"] = 1,
["phone"] = 1,
["kokspack"] = 2,
["weedpack"] = 2,
["tilidin"] = 2,
}
| Option | Description |
|---|---|
Weapons | What every fighter gets. The key is the weapon, the value the list of attachments — an empty list means none |
DefaultWeapon | The weapon a player holds when the fight starts |
Items | Items handed out on join, with the amount. The names have to match your inventory exactly |
GADGET_PARACHUTE is in the list because several maps start on a roof or a hill.
Caller item
Config.CallerItem = {
enabled = true,
itemName = "calleritem",
vehicle = `schafter6`,
whitelist = {
[`buzzard2`] = true,
[`supervolito2`] = true,
},
times = {
backi = 3 * 60,
other = 15
},
backiVehicles = {
[`komoda`] = true,
},
}
| Option | Default | Description |
|---|---|---|
enabled | true | Switches the whole mechanic off |
itemName | calleritem | The inventory item that calls the vehicle in |
vehicle | schafter6 | Model that is spawned |
whitelist | two helicopters | Vehicles exempt from the exit timer |
times.backi | 3 * 60 = 180 s | Timer for the models listed in backiVehicles |
times.other | 15 | Timer for every other vehicle |
backiVehicles | komoda | Which models count as a "Backi" and get the longer timer |
The two timers are in seconds and are what the caller_timer and caller_notify messages count down — they decide how long a player may stay inside a vehicle during a fight before having to get out.
Blips and markers
Config.Blips = {
center = { enabled = false, sprite = 12, scale = 0.7, color = 1, label = "GW Mitte" },
attacker = { enabled = false, sprite = 12, scale = 0.7, color = 1, label = "Angreifer" },
defender = { enabled = false, sprite = 12, scale = 0.7, color = 1, label = "Verteidiger" },
}
Three blips during a fight: the centre of the area and the two sides. All three ship disabled — turning them on makes fights readable from the map, which takes a lot of the hunting out of the mode.
Config.Markers = {
menu = {
enabled = true, type = 42, scale = vec3(0.7, 0.7, 0.7),
drawDistance = 10.0, move = false, rotate = false,
color = { r = 255, g = 255, b = 255, a = 120 },
},
join = {
enabled = true, type = 1, scale = vec3(10.0, 10.0, 0.7),
drawDistance = 20.0, move = false, rotate = false,
color = { r = 255, g = 255, b = 255, a = 120 },
}
}
menu is the small marker at a faction's menu point, join the large circle players step into to join the queue — which is why its scale is 10.0 × 10.0 instead of 0.7.
Framework functions
Config.Functions is the layer between x-gangwar and the rest of your server.
| Function | Purpose |
|---|---|
Notify(message) | Normal notification |
Announce(message) | Announcement, e.g. an attack being started |
HelpNotify(message) | The "press E" hint |
OnJoined() | Runs when a player enters a fight |
OnLeft() | Runs when a player leaves |
RevivePlayer(coords) | Revives a player |
OnStartSpectate() / OnStopSpectate() | Spectator mode starts / ends |
CanJoinQueue() | Decides whether a player may join the queue |
UIOpened() / UIClosed() | Empty hooks that fire with the menu |
Notifications
Config.Functions.Notify = function(message)
ESX.ShowNotification(message, "info", 10000, "Gangwar")
end
Config.Functions.HelpNotify = function(message)
if GetResourceState("strada_scriptpack") == "started" then
local ok = pcall(function()
exports["strada_scriptpack"]:HelpNotify("E", message)
end)
if ok then return end
end
if ESX and ESX.ShowHelpNotification then
ESX.ShowHelpNotification(message, false, true, -1)
end
end
Notify and Announce go through ESX out of the box; HelpNotify tries strada_scriptpack first and falls back to ESX. Replace the bodies with your own system.
Revive
Config.Functions.RevivePlayer = function(coords)
TriggerEvent("esx_ambulancejob:revive")
if ESX and ESX.PlayerData then
ESX.PlayerData.dead = false
end
LocalPlayer.state:set("isDead", false, true)
LocalPlayer.state:set("dead", false, true)
return false
end
Called with the coordinates the player should end up at. Besides the revive itself it clears the death state in both the ESX player data and the state bags — keep those lines when you swap the ambulance job, otherwise players stay "dead" for other resources.
Spectating and voice
Config.Functions.OnStartSpectate = function()
if GetResourceState("pma-voice") ~= "started" then return end
CreateThread(function()
pcall(function() exports["pma-voice"]:overrideProximityRange(0) end)
end)
end
Spectators would otherwise hear the fighters they are watching. The default sets the pma-voice proximity range to 0 while spectating, and OnStopSpectate clears the override again. Both are guarded, so they do nothing if you do not run pma-voice.
Join check
Config.Functions.CanJoinQueue = function()
local ped = PlayerPedId()
if IsPedDeadOrDying(ped, false) then
return false
end
return true
end
Return false to keep the player out of the queue. Add your own conditions here:
Config.Functions.CanJoinQueue = function()
if IsPedDeadOrDying(PlayerPedId(), false) then return false end
if LocalPlayer.state.jailed then return false end
return true
end
OnJoined and OnLeft are empty stubs, each with a commented example in the shipped file — a tracker item being used on join, and a weapon resync on leave.
Language
All texts live in config/locales.lua, split into three groups:
| Group | Covers |
|---|---|
Locales.UI | Everything inside the menu: tab names, buttons, area states, the description, stats, shop headings, both leaderboards, the HUD and the scoreboard |
Locales.Client | Messages for the player who acts — attack started, rank too low, limit reached, spectate hint, caller timer |
Locales.Server | Messages sent from the server — queue, announcements, fight results, upgrade purchases |
The file ends with a small helper:
function L(section, key, ...)
local group = Locales[section]
local text = group and group[key]
if not text then
return ("%s.%s"):format(section, key)
end
if select("#", ...) > 0 then
return text:format(...)
end
return text
end
It looks a string up and fills in the placeholders. A missing key renders as Client.some_key on screen instead of throwing an error — a handy way to spot a typo after translating.
Everything ships in German. Translate the values, never the keys, and keep every %s in place:
Locales.Client = {
wait_moment = "Please wait a moment",
rank_too_low = "You need a higher rank to start an attack",
attack_started = "You started the attack. Please wait for a defender",
not_enough = "Your faction needs at least %s members to start the fight",
limit_reached = "Your faction reached its daily gangwar limit",
wrong_time = "You cannot attack right now. Attacks are only possible between %s",
open_menu = "Open gangwar menu",
-- …
}
Watch the strings with several placeholders: states.versus and announce_attack take two, fight_won and fight_lost take the enemy faction and the points, and the timer strings take minutes and seconds separately as %s:%s.
Server config
Config.Logging = {
profile_picture = "",
color = 16766720,
webhook = "WEBHOOK",
}
Discord logging for gangwar events.
| Option | Default | Description |
|---|---|---|
webhook | "WEBHOOK" | Your Discord webhook URL. Replace the placeholder — logging does nothing until you do |
color | 16766720 | Embed colour as a decimal number (16766720 is gold, #FFCC00) |
profile_picture | "" | URL of an image used as the embed avatar. Empty for none |
To convert your own colour, drop the # and read the remaining hex as decimal — 3498DB becomes 3447003.
Theming
The menu's colours sit outside the Lua config, in frontend/assets/css/config.css — 91 lines of CSS variables, and the only frontend file left open:
:root {
--main-color: rgb(255, 255, 255);
--background-base: rgb(0, 0, 0);
--background-glow: rgba(255, 255, 255, 0.15);
…
}
Change the values, keep the names. --main-color is the accent, --background-base the ground, and --background is a stack of radial glows built from --background-glow, so recolouring the glow is usually enough to match the menu to your server.