Config

x-gangwar is configured from three files in config/.

FileLoaded onContentDocumented in
config/config.luaSharedSettings, UI, loadout, factions, maps, rewardsThis page and Maps & Rewards
config/locales.luaSharedAll texts, in three groupsLanguage
config/config_server.luaServerDiscord loggingServer 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
}
OptionDefaultDescription
DebugfalseConsole output while placing maps or testing. Off in production
DebugDummyJobgangwar_dummyA stand-in faction so you can start a fight against yourself while testing. Never give this job to a real player
ESX.resourcees_extendedYour ESX resource name
ESX.eventesx:getSharedObjectLegacy event, used when use_export is off
ESX.use_exporttrueFetches 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",
  },
}
GroupOptionDescription
HeaderserverNameLarge heading of the menu
menuSubtitle / scoreSubtitleSecond line in the menu and on the scoreboard
closeKeyThe key shown in the close hint — change it together with the key itself
tabsgangwar, rewards, topfraks, topplayersSwitches whole sections of the menu off. A disabled tab is gone for everyone
killfeedenabled, maxItems, lifetimeKill list during a fight: on/off, entries on screen, milliseconds per entry
scoreboardpanelDurationHow long the result panel stays up, in milliseconds
maxRowsPlayers listed on the scoreboard
liststopPlayers / topFraksLength of the two leaderboards
imagesjobFallback, mapFallback, rewardFallbackPictures 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,
}
OptionDefaultDescription
minMember0Members a faction needs before it may start a fight. 0 disables the check; a failed check shows not_enough
maxMember0Maximum players per side. 0 means no limit
joinTime0.2 * 60 * 1000 = 12 sWindow in which members can join the queue after a fight is announced
maxTime10 * 60 * 1000 = 10 minMaximum length of a fight
preparationTime5000Countdown before the fight actually starts, in milliseconds
topPlayersMinKills1Kills 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,
}
OptionDescription
WeaponsWhat every fighter gets. The key is the weapon, the value the list of attachments — an empty list means none
DefaultWeaponThe weapon a player holds when the fight starts
ItemsItems 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,
  },
}
OptionDefaultDescription
enabledtrueSwitches the whole mechanic off
itemNamecalleritemThe inventory item that calls the vehicle in
vehicleschafter6Model that is spawned
whitelisttwo helicoptersVehicles exempt from the exit timer
times.backi3 * 60 = 180 sTimer for the models listed in backiVehicles
times.other15Timer for every other vehicle
backiVehicleskomodaWhich 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.

FunctionPurpose
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:

GroupCovers
Locales.UIEverything inside the menu: tab names, buttons, area states, the description, stats, shop headings, both leaderboards, the HUD and the scoreboard
Locales.ClientMessages for the player who acts — attack started, rank too low, limit reached, spectate hint, caller timer
Locales.ServerMessages 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.

OptionDefaultDescription
webhook"WEBHOOK"Your Discord webhook URL. Replace the placeholder — logging does nothing until you do
color16766720Embed 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.