> 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) |

***
