> For the complete documentation index, see [llms.txt](https://jesus-gimenez.gitbook.io/xex-scripts/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://jesus-gimenez.gitbook.io/xex-scripts/premium-mods/weapon-licenses-and-shooting-range.md).

# Weapon Licenses & Shooting Range

***

<figure><img src="/files/cAFxWlnq2LIyPhmtzid5" alt=""><figcaption></figcaption></figure>

## Overview

XeX Weapon License is a premium weapon licensing system for FiveM servers featuring a modern glassmorphism UI for theoretical tests and an interactive shooting range for practical exams. Players must pass a theoretical multiple-choice exam before being allowed to attempt the practical shooting test. An optional practice mode provides a timed shooting range experience.

### Supported Frameworks

| Framework  | Version | Status         |
| ---------- | ------- | -------------- |
| ESX Legacy | 1.6.0+  | ✅ Full Support |
| QBCore     | Latest  | ✅ Full Support |
| QBox       | Latest  | ✅ Full Support |

***

## Features Summary

### Core Features

| Category     | Feature                | Description                                                               |
| ------------ | ---------------------- | ------------------------------------------------------------------------- |
| **UI**       | Glassmorphism Design   | Modern dark translucent glass-effect test interface                       |
| **UI**       | Multi-language         | Dynamic EN/ES question banks and UI strings                               |
| **UI**       | Interactive Options    | Clickable card-based answer selection with visual feedback                |
| **UI**       | Animated Progress      | Smooth progress bar with section transitions                              |
| **UI**       | Result Screens         | Animated pass/fail screens with SVG check/cross icons                     |
| **UI**       | Countdown Overlay      | Full-screen countdown timer before shooting tests                         |
| **UI**       | Sound Effects          | Countdown siren audio during test preparation                             |
| **Test**     | Theoretical Exam       | 9-question multiple choice test with configurable pass threshold (5/9)    |
| **Test**     | Question Randomization | Questions selected randomly from the pool without repetition              |
| **Test**     | Practical Exam         | Timed shooting range with random target spawns at 17 positions            |
| **Test**     | Configurable Timer     | Adjustable seconds to complete practical test (0 = unlimited)             |
| **Practice** | Shooting Practice      | Optional practice mode (configurable price or free)                       |
| **Practice** | License Gate           | Optionally require weapon license to access practice range                |
| **Security** | Server Validation      | License grants validated server-side with exam state tracking             |
| **Security** | Anti-Exploit           | Players must pay before receiving licenses — `playerInExam` state tracked |
| **Security** | Mutual Exclusion       | Cannot be in test and practice simultaneously                             |
| **Safety**   | Disarm on Exit         | Auto-remove weapon if player leaves shooting area boundary                |
| **Safety**   | NUI Cleanup            | Proper cleanup on resource stop to prevent stuck UI overlays              |
| **Safety**   | Target Cleanup         | Smart `cleanTargets` — only removes target props within 30m of player     |
| **System**   | Auto Updater           | JSON-based version checker with styled console banner output              |
| **System**   | Blip System            | Configurable map blips for all 3 locations (theory, practical, practice)  |
| **Config**   | Fully Configurable     | Prices, locations, timers, weapons, blips, target positions               |
| **Perf**     | Optimized              | Sleep-based loops (1500ms idle), 1ms only near markers                    |

***

## Installation

### Requirements

* FiveM Server
* ESX Legacy **or** QBCore
* MySQL database (via oxmysql or mysql-async)
* `esx_license` (ESX only — required for license storage)
* `esx_menu_default` (ESX only — required for confirmation menus)
* Optional: `qb-menu` (QBCore — for confirmation dialogs)

### Quick Start

{% stepper %}
{% step %}
Extract to your resources/\[xex]/ folder and rename folder to 'xex\_weaponlicense' if needed.
{% endstep %}

{% step %}
Import `weapon_licenses.sql` into your database (ESX only).
{% endstep %}

{% step %}
Add to server.cfg:

{% code title="server.cfg" %}

```cfg
ensure xex_weaponlicense
```

{% endcode %}
{% endstep %}

{% step %}
Configure `config.lua` (framework, prices, locations).
{% endstep %}

{% step %}
Restart the server.
{% endstep %}
{% endstepper %}

### Recommended Resource Order

```cfg
# server.cfg — ESX
ensure oxmysql
ensure es_extended
ensure esx_license        # Required for ESX license storage
ensure esx_menu_default   # Required for ESX confirmation menus
ensure xex_weaponlicense

# server.cfg — QBCore
ensure oxmysql
ensure qb-core
ensure qb-menu            # For confirmation dialogs
ensure xex_weaponlicense
```

***

## Configuration

### General Settings

```lua
Config.DrawDistance = 15.0           -- Distance (units) to show interaction markers
Config.Language = 'en'              -- 'en' or 'es' (controls UI + questions + locales)
Config.CheckForUpdates = true       -- Auto version check on resource start
```

| Option            | Default | Description                                                                |
| ----------------- | ------- | -------------------------------------------------------------------------- |
| `DrawDistance`    | `15.0`  | Distance in GTA units to start rendering markers and enabling interactions |
| `Language`        | `'en'`  | Controls all text: locales, UI strings, and question bank                  |
| `CheckForUpdates` | `true`  | Checks GitHub Gist for new versions on startup                             |

### Framework Settings

```lua
Config.Framework = 'auto'                 -- 'auto' | 'esx' | 'qb'
Config.GetSharedObjectfunction = true     -- true for modern ESX exports (recommended)
```

| Value    | Framework                           | Notes                                      |
| -------- | ----------------------------------- | ------------------------------------------ |
| `'auto'` | Auto-detect: QBCore → ESX (default) |                                            |
| `'esx'`  | ESX Legacy                          | Requires `esx_license` for license storage |
| `'qb'`   | QBCore                              | Uses player metadata for license storage   |

| `GetSharedObjectfunction` | Behavior                                                     |
| ------------------------- | ------------------------------------------------------------ |
| `true`                    | Uses `exports["es_extended"]:getSharedObject()` (modern ESX) |
| `false`                   | Uses `TriggerEvent('esx:getSharedObject', ...)` (legacy ESX) |

### Theoretical Test

```lua
Config.WeaponLicenses.Theoretical = {
    Price = 15000,                              -- Cost to attempt the test ($)
    Location = vector3(22.0, -1107.2, 29.8),    -- Marker/interaction position
    Blip = {
        Enabled = false,                        -- Show blip on map
        Scale = 0.7,                            -- Blip size
        Color = 3,                              -- GTA blip color index
        Sprite = 150,                           -- GTA blip sprite ID
        Location = vector3(22.0, -1107.2, 29.8) -- Blip position
    }
}
```

| Option     | Default                        | Description                               |
| ---------- | ------------------------------ | ----------------------------------------- |
| `Price`    | `15000`                        | Cost in dollars. Set to `0` for free.     |
| `Location` | `vector3(22.0, -1107.2, 29.8)` | World position for the interaction marker |

**Test Parameters (in `ui/scripts.js`):**

| Parameter            | Value | Description                      |
| -------------------- | ----- | -------------------------------- |
| `nbQuestionToAnswer` | `9`   | Total questions per test attempt |
| `nbAnswerNeeded`     | `5`   | Minimum correct answers to pass  |

### Practical Test

```lua
Config.WeaponLicenses.Practical = {
    Price = 85000,                                  -- Cost to attempt the exam ($)
    Location = vector3(14.39, -1097.57, 29.83),     -- Marker position
    TimeToFailTest = 12,                            -- Seconds to complete (0 = unlimited)
    TotalTargetsToEnd = 10,                         -- Targets to hit to pass
    CountDownInterval = 5,                          -- Countdown seconds before test starts
    Weapon = 'WEAPON_PISTOL',                       -- Weapon given during test
    Blip = {
        Enabled = false,
        Scale = 0.7,
        Color = 3,
        Sprite = 150,
        Location = vector3(14.39, -1097.57, 29.83)
    }
}
```

| Option              | Default                           | Description                                                                            |
| ------------------- | --------------------------------- | -------------------------------------------------------------------------------------- |
| `Price`             | `85000`                           | Cost in dollars. Player must have this in cash.                                        |
| `Location`          | `vector3(14.39, -1097.57, 29.83)` | Marker position for practical test                                                     |
| `TimeToFailTest`    | `12`                              | Seconds to hit all targets. `0` = no time limit. Lower = harder.                       |
| `TotalTargetsToEnd` | `10`                              | Number of targets to hit to pass. Targets appear one at a time.                        |
| `CountDownInterval` | `5`                               | Seconds of countdown before test begins (`5, 4, 3, 2, 1, GO!`)                         |
| `Weapon`            | `'WEAPON_PISTOL'`                 | Weapon hash given to player. [Full list](https://wiki.rage.mp/index.php?title=Weapons) |

### Practice Mode

```lua
Config.WeaponPracticePoint = {
    Enabled = true,                                -- Enable/disable the practice point
    NeedsLicenseToUse = false,                     -- Require practical license to practice
    Price = 0,                                      -- Cost per session (0 = free)
    Location = vector3(12.67, -1096.94, 29.8),     -- Marker position
    SecondsToEndPractice = 20,                     -- Duration of practice session
    TotalTargetsToEnd = 25,                        -- Total targets in session
    Weapon = 'WEAPON_PISTOL',                      -- Weapon given during practice
    Blip = {
        Enabled = true,
        Scale = 0.7,
        Color = 3,
        Sprite = 150,
        Location = vector3(12.67, -1096.94, 29.8)
    }
}
```

| Option                 | Default           | Description                                                                             |
| ---------------------- | ----------------- | --------------------------------------------------------------------------------------- |
| `Enabled`              | `true`            | Show/hide the practice point entirely                                                   |
| `NeedsLicenseToUse`    | `false`           | When `true`, player must own `practical_weapons` license (ESX) or `weapon` license (QB) |
| `Price`                | `0`               | Cost per session. `0` = free.                                                           |
| `SecondsToEndPractice` | `20`              | Duration in seconds. `0` = unlimited.                                                   |
| `TotalTargetsToEnd`    | `25`              | Maximum targets in a session                                                            |
| `Weapon`               | `'WEAPON_PISTOL'` | Weapon provided during practice                                                         |

### Safety Settings

```lua
Config.DisarmIfExits = true                                             -- Disarm if player leaves range
Config.DoorPointToDisarm = vector3(7.49, -1098.90, 28.80)              -- Exit detection point
```

| Option              | Default                          | Description                                                           |
| ------------------- | -------------------------------- | --------------------------------------------------------------------- |
| `DisarmIfExits`     | `true`                           | Auto-remove weapon and end session if player approaches the exit door |
| `DoorPointToDisarm` | `vector3(7.49, -1098.90, 28.80)` | Coordinate that triggers disarm when within 1.3 units                 |

### Target Positions

The shooting range has **17 predefined target spawn positions**. Targets appear randomly at one of these positions, never repeating the same position consecutively:

```lua
Config.WeaponLicenseProp = "prop_range_target_03"  -- GTA prop model for targets
Config.WeaponLicensePropPosition = {
    { x = 12.50, y = -1088.64, z = 31.83 },   -- Position 1
    { x = 14.25, y = -1089.40, z = 31.83 },   -- Position 2
    { x = 16.08, y = -1090.03, z = 31.83 },   -- Position 3
    -- ... 14 more positions
}
```

### Blip Configuration

Each location (Theoretical, Practical, Practice) has its own blip settings:

```lua
Blip = {
    Enabled = false,      -- Show/hide blip on map
    Scale = 0.7,          -- Blip size (0.1 - 2.0)
    Color = 3,            -- GTA blip color index
    Sprite = 150,         -- GTA blip sprite ID (150 = gun store)
    Location = vector3()  -- Blip position on map
}
```

| Property   | Type    | Description                          |
| ---------- | ------- | ------------------------------------ |
| `Enabled`  | bool    | Whether to show the blip on the map  |
| `Scale`    | float   | Size of the blip icon                |
| `Color`    | int     | GTA blip color index (0-85)          |
| `Sprite`   | int     | GTA blip sprite ID                   |
| `Location` | vector3 | World coordinates for blip placement |

***

## Database Setup

### ESX Only

Run the SQL file included in the resource (`weapon_licenses.sql`):

```sql
INSERT INTO `licenses` (`type`, `label`) VALUES
('theoretical_weapons', 'Theoretical Weapon Exam'),
('practical_weapons', 'Practical Weapon Exam');
```

This inserts two new license types into the existing `licenses` table used by `esx_license`.

### QBCore

No SQL import needed. QBCore stores licenses in player metadata:

```lua
Player.PlayerData.metadata["licences"]["theoretical_weapons"] = true
Player.PlayerData.metadata["licences"]["weapon"] = true
```

> Note: QBCore uses `"weapon"` (not `"practical_weapons"`) for the practical license key.

***

## How It Works

### Complete Flow

{% stepper %}
{% step %}
Resource Start

* ESX: Loads esx\_license integration
* Server initializes `shootingRangeFree = true`
* Auto updater checks for new version (5s delay)
* Blips created on client for enabled locations
  {% endstep %}

{% step %}
Player Loads (ESX only)

* Server fetches player's existing licenses via esx\_license
* Sends license list to client via `xex_weaponlicense:loadLicenses`
* Client builds `ownedLicenses` table for local checks
  {% endstep %}

{% step %}
Near Markers (Player approaches)

* Sleep drops from 1500ms to 1ms for responsive interaction
* Markers rendered (green arrow, type 2)
* Help notification shows "Press \[E] to start..."
* All markers hidden during active sessions (mutual exclusion)
  {% endstep %}

{% step %}
Theoretical Test

* Player presses \[E] near theory marker
* Confirmation menu with price → Server validates payment
* NUI opens with glassmorphism test UI
* 9 random questions from question bank
* Pass (≥5/9) → License granted server-side
* Fail → Can retry (pay again)
  {% endstep %}

{% step %}
Practical Test

* Requires theoretical license first
* Player presses \[E] near practical marker
* Server confirms payment + shooting range is free
* Countdown (5, 4, 3, 2, 1...) with siren sound
* Random targets spawn one at a time
* Hit target → next target spawns at random position
* Hit all 10 → Pass / Time runs out → Fail
* Weapon removed, range freed
  {% endstep %}

{% step %}
Practice Mode

* Independent of license flow
* Optional license requirement
* Same shooting mechanics with different timer/target counts
  {% endstep %}
  {% endstepper %}

### License Flow (ESX)

{% stepper %}
{% step %}
Theoretical Test

* Price: $15,000
* 9 questions, pass requires 5/9
* On pass: grant `theoretical_weapons` license (ESX)
* On fail: can retry (pay again)
  {% endstep %}

{% step %}
Practical Test

* Requires `theoretical_weapons`
* Price: $85,000
* 10 targets, 12s timer
* On pass: grant `practical_weapons` license (ESX)
* On fail: can retry (pay again)
  {% endstep %}
  {% endstepper %}

### License Flow (QBCore)

Same flow but with different license keys:

* Theoretical: `metadata.licences.theoretical_weapons = true`
* Practical: `metadata.licences.weapon = true`

### Mutual Exclusion

The system prevents overlapping activities with clear checks:

| Current Activity | Can Start Theory? | Can Start Practical? | Can Start Practice? |
| ---------------- | ----------------- | -------------------- | ------------------- |
| None             | ✅                 | ✅                    | ✅                   |
| Theoretical Test | ❌                 | ❌                    | ❌                   |
| Practical Test   | ❌                 | ❌                    | ❌                   |
| Practice         | ❌                 | ❌                    | ❌                   |

Additionally:

* All 3 interaction markers are hidden during any active session
* The shooting range is globally locked — only one player can use it at a time
* Notification shown: "You are already in an activity. Finish it first."

### Theoretical Test UI

The NUI test interface has 4 sections with smooth transitions:

1. Welcome — Info cards showing: 9 questions, 5/9 required, Unlimited time
2. Questions — One at a time, 4 clickable answer cards, progress bar
3. Pass — Animated green checkmark, congratulations message
4. Fail — Animated red cross, "try again" message

### Practical Test Mechanics

1. Player pays → server locks shooting range (`shootingRangeFree = false`)
2. Player receives weapon (200 ammo) via `GiveWeaponToPed`
3. Countdown overlay: 5... 4... 3... 2... 1... (with siren sound)
4. First target spawns at random position from 17 predefined slots
5. Target hit detection via `HasEntityBeenDamagedByAnyPed`
6. Hit → target destroyed → next spawns at different random position
7. Pass: All targets hit / Fail: Timer runs out
8. Weapon removed, target cleanup, range unlocked

### Practice Mode Mechanics

* Same shooting mechanics as practical test
* Configurable independent duration and target count
* Can be free or paid
* Optionally requires weapon license
* Does not grant any license on completion
* Shows hit count on completion

***

## NUI Interface

### Welcome Section

| Element      | Description                                                                   |
| ------------ | ----------------------------------------------------------------------------- |
| Header       | Shield icon + "Weapon License" + "Theoretical Examination" + "OFFICIAL" badge |
| Weapon Icon  | `weapon.png` header image                                                     |
| Welcome Text | Explains what the test is about                                               |
| Info Cards   | 3 cards: Questions (9), Required (5/9), Time Limit (Unlimited)                |
| Start Button | "Start Test" with arrow icon                                                  |

### Question Section

| Element          | Description                                                                       |
| ---------------- | --------------------------------------------------------------------------------- |
| Question Counter | "Question 1 of 9"                                                                 |
| Score Display    | "X/Y correct"                                                                     |
| Progress Bar     | Animated width based on current question                                          |
| Question Text    | Current question text                                                             |
| Answer Options   | 4 clickable cards. Green highlight on correct, red on incorrect.                  |
| Next Button      | "Next Question" / "Finish Test" on last question. Disabled until answer selected. |

### Result Screens

| Screen | Elements                                                                                                           |
| ------ | ------------------------------------------------------------------------------------------------------------------ |
| Pass   | Animated green checkmark SVG, "Test Passed!", congratulations text, "Close" button → triggers `close` NUI callback |
| Fail   | Animated red cross SVG, "Test Failed", encouragement text, "Close" button → triggers `kick` NUI callback           |

### Countdown Counter

Full-screen overlay showing numbers `5... 4... 3... 2... 1...` with countdown sound (`siren.mp3`). Used before both practical test and practice sessions.

***

## Security

### Server-Side Validation

| Protection             | Description                                                                                                      |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
| License Type Whitelist | Only `theoretical_weapons` and `practical_weapons` (ESX) or `theoretical_weapons` and `weapon` (QB) are accepted |
| Exam State Tracking    | `playerInExam[source]` tracks which players have paid — license only granted if player is in exam state          |
| Payment First          | Server processes payment before any client action begins                                                         |
| Source Validation      | `xex_weaponlicense:addLicense` validates the `source` player ID                                                  |
| Disconnect Cleanup     | `playerInExam[src]` cleared on `playerDropped` event                                                             |
| Console Warnings       | Invalid license grant attempts are logged with `^1WARNING` messages                                              |

### Anti-Exploit Measures

| Measure              | Description                                                                                                      |
| -------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Pay-Before-Grant     | Players cannot receive licenses without paying — server tracks exam state                                        |
| Global Range Lock    | `shootingRangeFree` boolean prevents multiple players from using range simultaneously                            |
| Smart Target Cleanup | `cleanTargets` only removes `prop_range_target_03` props within 30m of the player — doesn't delete other objects |
| Disarm on Exit       | If `Config.DisarmIfExits = true`, weapon is removed when player approaches exit point                            |
| NUI Cleanup          | `onResourceStop` handler closes NUI, removes weapons, and deletes leftover target objects                        |

***

## Exports & Events

### Server Callbacks (ESX)

| Callback                                 | Parameters                                                 | Returns             | Description                                                                                                       |
| ---------------------------------------- | ---------------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `xex_weaponlicense:isPaid`               | `typeLicense` (`'theoretical'`/`'practical'`/`'practice'`) | `boolean`           | Validates payment, removes money, sets exam state (theoretical/practical only — practice does not set exam state) |
| `xex_weaponlicense:isPaidAndFreeToShoot` | `typeLicense`                                              | `boolean, message?` | Same as isPaid but also checks if shooting range is free                                                          |
| `xex_weaponlicense:toggleShooting`       | `toggle` (bool)                                            | —                   | Sets `shootingRangeFree` state and cleans up targets                                                              |

### Server Callbacks (QBCore)

| Callback                                 | Parameters    | Returns             | Description                                                    |
| ---------------------------------------- | ------------- | ------------------- | -------------------------------------------------------------- |
| `xex_weaponlicense:isPaid`               | `typeLicense` | `boolean`           | Validates cash payment via `Player.Functions.GetMoney('cash')` |
| `xex_weaponlicense:isPaidAndFreeToShoot` | `typeLicense` | `boolean, message?` | Payment check + range availability                             |
| `xex_weaponlicense:toggleShooting`       | `toggle`      | —                   | Sets range state and cleans targets                            |

### Server Events

| Event                          | Parameters         | Direction       | Description                                 |
| ------------------------------ | ------------------ | --------------- | ------------------------------------------- |
| `xex_weaponlicense:addLicense` | `type` (string)    | Client → Server | Grants license after server-side validation |
| `esx_license:addLicense`       | `source, type, cb` | Internal (ESX)  | Triggers client license update              |
| `playerDropped`                | —                  | FiveM native    | Cleans up `playerInExam` state              |

### Client Events

| Event                                             | Data               | Description                                                              |
| ------------------------------------------------- | ------------------ | ------------------------------------------------------------------------ |
| `xex_weaponlicense:loadLicenses`                  | `licenses` (array) | ESX only — loads player's existing licenses on join                      |
| `xex_weaponlicense:addLicense`                    | `type` (string)    | ESX only — updates local `ownedLicenses` table after server grant        |
| `xex_weaponlicense:client:confirmMenu`            | —                  | QBCore only — handles practical exam payment confirmation from qb-menu   |
| `xex_weaponlicense:client:confirmMenuTheoretical` | —                  | QBCore only — handles theoretical exam payment confirmation from qb-menu |
| `xex_weaponlicense:client:confirmMenuPay`         | —                  | QBCore only — handles practice session payment confirmation from qb-menu |

### NUI Callbacks

| Callback   | Trigger                            | Description                                            |
| ---------- | ---------------------------------- | ------------------------------------------------------ |
| `question` | User clicks "Start Test"           | Transitions from welcome to first question section     |
| `close`    | User clicks "Close" on pass screen | Stops theory test, grants license, closes NUI          |
| `kick`     | User clicks "Close" on fail screen | Stops theory test without granting license, closes NUI |

### NUI Messages (Lua → JavaScript)

| Message        | Data         | Description                        |
| -------------- | ------------ | ---------------------------------- |
| `openQuestion` | `true/false` | Shows/hides the test NUI           |
| `language`     | `'en'/'es'`  | Sets the UI language               |
| `counter`      | `number`     | Shows countdown overlay with value |
| `counterClose` | `true`       | Hides countdown overlay            |
| `openSection`  | `'question'` | Transitions to question section    |

### Server Exports

| Export            | Description                                                 |
| ----------------- | ----------------------------------------------------------- |
| `CheckForUpdates` | Programmatically trigger update check from another resource |

### Console Commands

| Command                | Access                              | Description                          |
| ---------------------- | ----------------------------------- | ------------------------------------ |
| `weaponlicense_update` | Server console only (`source == 0`) | Manually triggers the update checker |

***

## Localization

### Supported Languages

| Code | Language |
| ---- | -------- |
| `en` | English  |
| `es` | Español  |

### Locale Keys Reference

#### Client Strings

| Key                           | EN                                                     | Description                         |
| ----------------------------- | ------------------------------------------------------ | ----------------------------------- |
| `practical_test_blip_text`    | "Practical Weapons Exam"                               | Map blip label                      |
| `theoretical_test_blip_text`  | "Theoretical Weapons Test"                             | Map blip label                      |
| `ammo_practice_blip_text`     | "Shooting Practice"                                    | Map blip label                      |
| `practical_started`           | "Practical Weapons Exam started"                       | Notification on test start          |
| `practice_started`            | "Shooting practice started!"                           | Notification on practice start      |
| `weapon_license`              | "Weapon Licenses"                                      | Notification title                  |
| `practical_exam_notif`        | "Practical exam: "                                     | Advanced notification subtitle      |
| `theoretical_exam_notif`      | "Theoretical exam: "                                   | Advanced notification subtitle      |
| `practical_training_notif`    | "Shooting practice: "                                  | Advanced notification subtitle      |
| `passed_notif`                | "Passed"                                               | Pass notification                   |
| `passed_notif_training`       | "Good Job you hit all the targets!"                    | Practice complete message           |
| `failed_notif_training`       | "Good Job! You hit %s targets"                         | Practice result with count          |
| `failed_simple_notif`         | "~~r~~Failed"                                          | Theory fail notification            |
| `failed_notif`                | "Failed. You missed %s shots"                          | Practical fail with remaining count |
| `press_practical_exam`        | "Press \[E] to start practical weapons test"           | Help notification                   |
| `press_theoretical_exam`      | "Press \[E] to start theoretical weapons test"         | Help notification                   |
| `press_to_start_practice`     | "Press \[E] to start shooting practice"                | Help notification                   |
| `practical_exam_confirm`      | "Confirm Weapon Practical license pay for: %s$?"       | Menu title                          |
| `theoretical_exam_confirm`    | "Confirm Weapon Theoretical license pay for: %s$?"     | Menu title                          |
| `practice_confirm`            | "Confirm shooting practice session for: %s$?"          | Menu title                          |
| `yes` / `no`                  | "Yes" / "No"                                           | Confirmation buttons                |
| `no_license`                  | "You don't have weapon license..."                     | Practice gate message               |
| `enough_money`                | "You don't have enough money"                          | Payment failure                     |
| `already_got_practical`       | "You already have weapon practical license"            | Already owned                       |
| `already_got_theoretical`     | "You already have weapon theoretical license"          | Already owned                       |
| `obtain_theoretical_before`   | "Before you have to obtain the theoretical test"       | Prerequisite message                |
| `remaining_seconds_practical` | "You are left with %ss to finish the test"             | Timer HUD                           |
| `remaining_seconds_practice`  | "You are left with %ss to finish the practice"         | Timer HUD                           |
| `shotting_range_not_free`     | "There is already someone using the shooting range..." | Range occupied                      |
| `too_far`                     | "You went too far from the firing zone"                | Exit detection                      |
| `busy_in_activity`            | "You are already in an activity..."                    | Mutual exclusion                    |

#### Server Strings

| Key          | EN              | Description           |
| ------------ | --------------- | --------------------- |
| `paid`       | "You paid: %s$" | Payment confirmation  |
| `bought_log` | "Exam Purchase" | Transaction log label |

### UI Text Strings

The NUI interface has its own i18n system in `ui/scripts.js`:

| Key            | EN                              | ES                                  |
| -------------- | ------------------------------- | ----------------------------------- |
| `title`        | "Weapon License"                | "Licencia de Armas"                 |
| `subtitle`     | "Theoretical Examination"       | "Examen Teórico"                    |
| `badge`        | "OFFICIAL"                      | "OFICIAL"                           |
| `welcomeTitle` | "Welcome to the License Center" | "Bienvenido al Centro de Licencias" |
| `startBtn`     | "Start Test"                    | "Comenzar Test"                     |
| `nextBtn`      | "Next Question"                 | "Siguiente Pregunta"                |
| `finishBtn`    | "Finish Test"                   | "Finalizar Test"                    |
| `questionOf`   | "Question %d of %d"             | "Pregunta %d de %d"                 |
| `scoreLabel`   | "%d/%d correct"                 | "%d/%d correctas"                   |
| `passTitle`    | "Test Passed!"                  | "¡Test Aprobado!"                   |
| `failTitle`    | "Test Failed"                   | "Test Suspendido"                   |
| `close`        | "Close"                         | "Cerrar"                            |

### Adding Languages

1. Add a new language block in `locales.lua` with all keys
2. Add corresponding questions in `ui/questions.js` under a new language key
3. Add UI text strings in `ui/scripts.js` under `UIText`
4. Set `Config.Language` to your new language code

***

## Customizing Questions

### Question Bank Structure

Edit `ui/questions.js` to modify or add questions:

```javascript
const Questions = {
    en: [
        {
            question: "Should I get a gun permit to carry them on a public road?",
            options: [
                "No, only a permit is needed in rural areas",
                "No, you only need permission in public buildings",
                "Yes, but only if I have penal antecedents",
                "Yes, always"
            ],
            answer: 3  // 0-indexed (0=A, 1=B, 2=C, 3=D)
        },
        // ... more questions
    ],
    es: [
        // Spanish question bank
    ]
};
```

### Default Questions (English)

The question bank covers these topics:

* Gun permit requirements for public carrying
* Maximum weapons for self-protection
* Self-defense legal requirements
* Criminal record restrictions
* Hunting weapon urban use restrictions
* And more...

### Modifying Test Parameters

Edit `CONFIG` in `ui/scripts.js`:

```javascript
const CONFIG = {
    nbQuestionToAnswer: 9,   // Total questions per test
    nbAnswerNeeded: 5,       // Minimum correct to pass
    lang: 'en'               // Default language (overridden by Lua config)
};
```

| Parameter            | Default | Description                              |
| -------------------- | ------- | ---------------------------------------- |
| `nbQuestionToAnswer` | `9`     | How many questions are asked per attempt |
| `nbAnswerNeeded`     | `5`     | Minimum correct answers required to pass |

> Note: Ensure your question bank has at least `nbQuestionToAnswer` questions per language.

***

## File Structure

```
xex_weaponlicense/
├── fxmanifest.lua              # Resource manifest (cerulean, lua54)
├── config.lua                  # All configuration options
├── locales.lua                 # EN/ES client & server translations
├── DOCS.md                     # This documentation
├── readme.md                   # Quick reference & changelog
├── weapon_licenses.sql         # ESX database setup
├── client/
│   ├── esx.lua                 # ESX client logic (494 lines)
│   │                           #   - Marker rendering & interaction
│   │                           #   - Theoretical test NUI control
│   │                           #   - Practical test loop & target spawning
│   │                           #   - Practice mode loop
│   │                           #   - Mutual exclusion checks
│   │                           #   - Disarm on exit detection
│   │                           #   - NUI callbacks (question, close, kick)
│   │                           #   - Resource stop cleanup
│   ├── qb.lua                  # QBCore client logic (539 lines)
│   │                           #   - Same as ESX with QB notification system
│   │                           #   - qb-menu integration for confirmations
│   │                           #   - DrawText for help notifications
│   └── blips.lua               # Map blip creation for all 3 locations
├── server/
│   ├── esx.lua                 # ESX server callbacks & security
│   │                           #   - isPaid, isPaidAndFreeToShoot, toggleShooting
│   │                           #   - addLicense with validation
│   │                           #   - playerInExam state tracking
│   ├── qb.lua                  # QBCore server callbacks & security
│   │                           #   - Same callbacks with QB Player functions
│   │                           #   - metadata-based license storage
│   ├── scrow.lua               # Shared server functions
│   │                           #   - ESX playerLoaded license sync
│   │                           #   - cleanTargets (smart prop cleanup)
│   └── updater.lua             # Auto update checker
│                               #   - JSON endpoint version comparison
│                               #   - Styled console banner output
│                               #   - CheckForUpdates export
│                               #   - weaponlicense_update command
└── ui/
    ├── ui.html                 # Main NUI interface (145 lines)
    │                           #   - Welcome, Question, Pass, Fail sections
    │                           #   - Countdown counter overlay
    ├── styles.css              # Glassmorphism styling
    │                           #   - Dark theme with glass blur effects
    │                           #   - Inter font (Google Fonts)
    │                           #   - Animated transitions & hover effects
    ├── scripts.js              # UI controller & logic (337 lines)
    │                           #   - CONFIG (nbQuestions, nbAnswerNeeded)
    │                           #   - UIText i18n strings (EN/ES)
    │                           #   - Question randomization
    │                           #   - Answer validation & scoring
    │                           #   - Section transitions
    │                           #   - NUI message handling
    ├── questions.js            # Multi-language question bank (EN/ES)
    ├── questions_es.js         # Legacy Spanish question bank (unused — kept for reference)
    ├── weapon.png              # Header icon
    ├── license.md              # License terms
    └── sounds/
        └── siren.mp3           # Countdown alarm sound
```

***

## Troubleshooting

| Issue                            | Solution                                                                                |
| -------------------------------- | --------------------------------------------------------------------------------------- |
| NUI stuck on screen              | Restart the resource: `ensure xex_weaponlicense`                                        |
| Questions not showing            | Check `Config.Language` matches a key in `questions.js`. Verify the UI HTML is loading. |
| License not saving (ESX)         | Verify `esx_license` is running and `weapon_licenses.sql` was imported                  |
| License not saving (QB)          | Check `qb-core` metadata system is working. Verify player data persists.                |
| Targets not spawning             | Check `Config.WeaponLicensePropPosition` coordinates are accessible                     |
| "Shooting range not free"        | Another player is using it. Wait or restart resource to reset `shootingRangeFree`.      |
| Blips not showing                | Set `Blip.Enabled = true` in the relevant config section                                |
| UI not loading fonts             | Server needs internet access for Google Fonts (Inter)                                   |
| Countdown sound not playing      | Check `ui/sounds/siren.mp3` exists. Browser audio may be blocked.                       |
| Weapon not given                 | Verify `Config.Weapon` is a valid GTA weapon hash string                                |
| Player not disarmed on exit      | Check `Config.DisarmIfExits = true` and `DoorPointToDisarm` coordinates                 |
| "You are already in an activity" | Mutual exclusion — finish current test/practice first                                   |
| Theory test auto-fails           | Ensure player has at least `nbAnswerNeeded` correct answers (5/9 by default)            |
| ESX getSharedObject error        | Set `Config.GetSharedObjectfunction = true` for modern ESX                              |
| qb-menu not opening              | Ensure `qb-menu` resource is started before `xex_weaponlicense`                         |

***

## Changelog

### v2.1.0

* Professional UI redesign with glassmorphism design system
* New JSON-based auto updater with styled console banner
* Mutual exclusion between practice and test modes
* Server-side license validation with `playerInExam` state tracking
* Anti-exploit protection — payment required before license grant
* NUI cleanup on `onResourceStop` to prevent stuck UI
* Fixed typos and removed unused variables
* Improved `cleanTargets` — only removes target props within 30m
* Multi-language UI support (EN/ES) with `UIText` system
* Merged question banks into single file with language keys
* Comprehensive DOCS.md documentation

### v2.0.0

* Fix weapon reload on each round
* Fix language typos
* Remove unnecessary validation

### v1.1.5

* QBCore import fix

### v1.1.4

* Added support to ESX `getSharedObject` export

### v1.1.3

* Fix to prevent targets from getting stuck

### v1.1.2

* ESX fix on license check + Removed server escrow

### v1.1.1

* Added readme + versioning fix + notification fix

### v1.1.0

* Added QBCore integration

***

## Support

| Channel | Link                                                   |
| ------- | ------------------------------------------------------ |
| Discord | [discord.gg/9yY3Jxnhh8](https://discord.gg/9yY3Jxnhh8) |

***


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://jesus-gimenez.gitbook.io/xex-scripts/premium-mods/weapon-licenses-and-shooting-range.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
