diff --git a/05-frameworks/01-qbox-basics.md b/05-frameworks/01-qbox-basics.md index 76f7725..cae9134 100644 --- a/05-frameworks/01-qbox-basics.md +++ b/05-frameworks/01-qbox-basics.md @@ -4,14 +4,26 @@ A **framework** in FiveM is the resource that owns "what is a player". It defines the player object, jobs, gangs, money, inventory hooks, character creation, and the events that fire on login/logout. +Think of it as the server's operating layer for gameplay logic: your scripts plug into the framework instead of reinventing player/account/job systems every time. + **Qbox (`qbx_core`)** is the most active framework in the FiveM scene right now. It's a modern fork of QBCore - same shape, same concepts, slightly cleaner API. -If your server runs: -- **Qbox** → use `exports.qbx_core:GetPlayer(src)` -- **QBCore** (older) → use `QBCore.Functions.GetPlayer(src)` (Qbox ships a bridge so old code still works) -- **ESX** → totally different API, this lesson doesn't cover it +### What Makes Qbox Different + +- It is a modern fork of QBCore with cleaner internals and active maintenance. +- It keeps a compatibility bridge for many QBCore-style APIs/events, so older resources usually need fewer rewrites. +- It is commonly paired with the ox ecosystem (`ox_lib`, `ox_inventory`, `ox_target`) in newer stacks. + +### Who Made It + +Qbox is built and maintained by the **Qbox Project** community/team. + +- Qbox docs: [https://docs.qbox.re/](https://docs.qbox.re/) +- Qbox project: [https://github.com/Qbox-project](https://github.com/Qbox-project) + +Qbox came from the QBCore ecosystem, so you'll still see shared patterns and names. -This lesson assumes Qbox. +This lesson assumes the latest version of Qbox. --- @@ -125,16 +137,20 @@ RegisterNetEvent('QBCore:Client:OnMoneyChange', function(type, amount, isRemoved end) ``` -Qbox keeps the old QBCore event names for compatibility. +Qbox keeps the old QBCore event names for compatibility. For "player loaded" keep using `QBCore:Client:OnPlayerLoaded` above - Qbox hasn't shipped a renamed replacement. -Newer Qbox-specific events: +Qbox-native events you can also listen to: ```lua -RegisterNetEvent('qbx_core:client:playerLoaded', function(data) end) +-- ↓ fires when the player logs out / disconnects RegisterNetEvent('qbx_core:client:playerLoggedOut', function() end) -``` -Use the newer ones when available. +-- ↓ fires when a metadata key changes. key, previous value, new value +RegisterNetEvent('qbx_core:client:onSetMetaData', function(key, oldVal, newVal) end) + +-- ↓ fires when a job/gang grade is updated +RegisterNetEvent('qbx_core:client:onGroupUpdate', function(group, grade) end) +``` --- @@ -272,12 +288,18 @@ lib.notify({ ## Commands With Permission +Qbox registers commands through **ox_lib's `lib.addCommand`**, not a `qbx_core` export: + ```lua --- ↓ Qbox helper: register a command, last arg is the required permission group -exports.qbx_core:CreateCommand('adminpanel', 'Open admin panel', {}, false, function(source, args) - -- ↑ source = who ran it, args = arg table +-- ↓ name, options table, handler +lib.addCommand('adminpanel', { + help = 'Open admin panel', + restricted = 'group.admin', -- ACE group required to run it + params = {}, -- positional args (none here) +}, function(source, args, raw) + -- ↑ source = who ran it, args = parsed params, raw = full string -- command body -end, 'admin') -- requires the 'admin' group +end) ``` Or use FiveM's `RegisterCommand` with **ACE** (Access Control Entries) for permissions: @@ -371,9 +393,10 @@ Client PlayerData is for display. **Authoritative checks happen server-side.** A - [Qbox Docs](https://docs.qbox.re/) - [qbx_core GitHub](https://github.com/Qbox-project/qbx_core) - read the source - [QBCore Docs (legacy)](https://docs.qbcore.org/) - same shape, older API +- [Qbox Project (GitHub org)](https://github.com/Qbox-project) - [GetPlayerIdentifierByType native](https://docs.fivem.net/natives/?_0xA61C8FCDFF1206F4) - [Resource ACE (permissions)](https://docs.fivem.net/docs/server-manual/setting-up-a-server/#permissions) --- -Next folder: [`06-ox-libraries/`](../06-ox-libraries/) - start with [`01-ox-lib.md`](../06-ox-libraries/01-ox-lib.md) +Next: [`05-frameworks/02-esx-basics.md`](02-esx-basics.md) diff --git a/05-frameworks/02-esx-basics.md b/05-frameworks/02-esx-basics.md new file mode 100644 index 0000000..70399f5 --- /dev/null +++ b/05-frameworks/02-esx-basics.md @@ -0,0 +1,320 @@ +# 02. ESX Basics + +## Plain English + +A **framework** in FiveM is the resource that owns "what is a player". It defines the player object, jobs, gangs, money, inventory hooks, character creation, and the events that fire on login/logout. + +Think of it as the server's operating layer for gameplay logic: your scripts plug into the framework instead of reinventing player/account/job systems every time. + +**ESX (`es_extended`)** is one of the oldest and most widely used FiveM RP frameworks. + +Like Qbox/QBCore, ESX owns the player model: identity, jobs, money/accounts, inventory hooks, and player lifecycle events. + +### What Makes ESX Different + +- It has the longest legacy footprint in RP servers, so there are many existing ESX resources and tutorials. +- It is account-oriented (`bank`, `money`, optionally `black_money`) with the `xPlayer` API style. +- Compared to Qbox/QBCore, naming and event conventions differ, so copy-pasting framework code between them usually fails without adaptation. + +### Who Made It + +ESX started in the ESX community and is actively maintained in modern form by the **esx-framework** team. + +- ESX docs: [https://documentation.esx-framework.org/](https://documentation.esx-framework.org/) +- ESX framework org: [https://github.com/esx-framework](https://github.com/esx-framework) + +This lesson assumes you have the latest version of ESX Legacy (`es_extended`) and Lua resources. + +### Shared Object Access: Export vs Event + +Use export access as your default in modern ESX Legacy: + +```lua +local ESX = exports['es_extended']:getSharedObject() +``` + +Old scripts often use the event pattern: + +```lua +TriggerEvent('esx:getSharedObject', function(obj) + ESX = obj +end) +``` + +Why export is preferred: + +1. Clear dependency: you directly reference `es_extended`. +2. Better reliability: export access is the standard pattern in modern ESX resources. +3. Safer migration path: many servers block, remove, or no-op `esx:getSharedObject` to reduce legacy attack surface and avoid old resource assumptions. + +Important compatibility note: + +- Some `es_extended` versions and forks still provide the event for backward compatibility. +- Even when it exists, treat it as legacy behavior, not your default API. +- For new code, always use exports first. + +### Legacy EssentialMode Setup (Open If This Matches Your Server) + +If your stack still depends on EssentialMode resources, your server is on a legacy path and should be upgraded. + +EssentialMode is outdated for modern FiveM stacks (latest release on the original repository: **May 2020**). + +What this usually looks like: + +- Core resources built around `essentialmode` plus early ESX scripts. +- Old dependencies such as `es_admin2`, `esplugin_mysql`, and `mysql-async`. +- Many scripts relying on legacy globals/events instead of exports. + +How to check if you have this legacy setup: + +1. Check `server.cfg` (and split cfg files) for `ensure essentialmode`, `ensure esplugin_mysql`, and older `es_*` resources. +2. Search your resources/manifests for `essentialmode`, `esplugin_mysql`, and `mysql-async` references. + +If you find multiple hits, prioritize migration to current ESX Legacy immediately because this old stack has higher security and compatibility risk. + +#### How to check your ESX version: + +Open `es_extended/fxmanifest.lua` and look for a version field. + +No clear version marker plus lots of legacy patterns usually means an old ESX build or a heavily modified fork. + +--- + +## Get The Player Object (Server Side) + +```lua +-- ↓ get the shared ESX object once (server file scope) +local ESX = exports['es_extended']:getSharedObject() + +-- ↓ later, inside an event/command where you have `source` +local src = source +local xPlayer = ESX.GetPlayerFromId(src) + +-- ↓ ALWAYS nil-check. Can be nil during load/disconnect race windows. +if not xPlayer then return end + +-- ↓ common identity fields +print(xPlayer.identifier) -- stable player identifier (license based in modern setups) +print(xPlayer.getName()) -- character display name + +-- ↓ job info +local job = xPlayer.getJob() -- table: name, label, grade, grade_name, ... +print(job.name) +print(job.grade) + +-- ↓ account balances +print(xPlayer.getMoney()) -- cash (if your server uses cash) +print(xPlayer.getAccount('bank').money) -- bank account +``` + +`xPlayer` is your trusted server-side player object. Client values are never authoritative. + +--- + +## Money And Accounts + +ESX usually separates money into accounts (`money`, `bank`, sometimes `black_money`). + +```lua +local src = source +local xPlayer = ESX.GetPlayerFromId(src) +if not xPlayer then return end + +-- ↓ cash helper methods +xPlayer.addMoney(250) -- add cash + +-- ↓ safe remove pattern: check first, then remove +local cash = xPlayer.getMoney() +if cash < 100 then + -- not enough cash + return +end +xPlayer.removeMoney(100) + +-- ↓ account-specific methods +xPlayer.addAccountMoney('bank', 500) +xPlayer.removeAccountMoney('bank', 200) + +-- ↓ read account value +local bank = xPlayer.getAccount('bank').money +print(('bank now: %d'):format(bank)) +``` + +Use explicit server-side checks before any remove operation. Never trust a client-reported balance. + +--- + +## Job Functions + +```lua +local xPlayer = ESX.GetPlayerFromId(source) +if not xPlayer then return end + +-- ↓ assign job and grade +xPlayer.setJob('police', 2) + +-- ↓ read job state +local job = xPlayer.getJob() +if job.name == 'police' and job.grade >= 2 then + -- allowed to access police-grade-2+ features +end +``` + +`job.grade` is numeric in most ESX setups. Keep checks numeric on the server. + +--- + +## Inventory Helpers (Common ESX Pattern) + +Exact behavior depends on your inventory resource (default ESX inventory vs ox_inventory bridge), but these are common patterns: + +```lua +local xPlayer = ESX.GetPlayerFromId(source) +if not xPlayer then return end + +-- ↓ read item count +local bread = xPlayer.getInventoryItem('bread') +print(bread.count) + +-- ↓ can carry check (if available in your setup) +if xPlayer.canCarryItem and not xPlayer.canCarryItem('bread', 1) then + return +end + +-- ↓ add/remove item +xPlayer.addInventoryItem('bread', 1) +xPlayer.removeInventoryItem('bread', 1) +``` + +Always test these on your exact stack. Inventory APIs vary between servers. + +--- + +## Client Data (For UI Only) + +```lua +-- ↓ ESX object on client +local ESX = exports['es_extended']:getSharedObject() + +-- ↓ current player's synced data +local pdata = ESX.GetPlayerData() +print(pdata.job and pdata.job.name) +``` + +Use client data for display (HUD/menu hints). Use server data for permissions, money, and item security. + +--- + +## Events You Will Use Constantly + +```lua +-- ↓ client: player finished loading +RegisterNetEvent('esx:playerLoaded', function(playerData) + print('loaded as', playerData.job and playerData.job.name) +end) + +-- ↓ client: job changed +RegisterNetEvent('esx:setJob', function(job) + print('new job:', job.name, job.grade) +end) +``` + +```lua +-- ↓ server: player dropped/disconnected (FiveM base event) +AddEventHandler('playerDropped', function(reason) + local src = source + -- cleanup server-side caches tied to src +end) +``` + +Use FiveM base events plus ESX events together. ESX names can vary by version, so confirm on your server docs. + +--- + +## Permission Checks + +```lua +local xPlayer = ESX.GetPlayerFromId(source) +if not xPlayer then return end + +-- ↓ framework job/grade gate +local job = xPlayer.getJob() +if job.name ~= 'police' or job.grade < 2 then + return +end + +-- ↓ optional admin group check (depends on your permissions setup) +if xPlayer.getGroup and xPlayer.getGroup() ~= 'admin' then + return +end +``` + +Server-side permission gates are mandatory. Client-side gates are only UX. + +--- + +## Online Players Loop + +```lua +local ESX = exports['es_extended']:getSharedObject() + +-- ↓ preferred ESX helper when available +for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do + print(xPlayer.source, xPlayer.getName()) +end + +-- ↓ universal fallback: FiveM GetPlayers + resolve each +for _, idStr in ipairs(GetPlayers()) do + local src = tonumber(idStr) + local p = ESX.GetPlayerFromId(src) + if p then + print(src, p.identifier) + end +end +``` + +For large loops, avoid doing heavy DB work inside each iteration. + +--- + +## Notifications + +```lua +-- ↓ classic ESX notify +TriggerClientEvent('esx:showNotification', source, 'Purchase complete') +``` + +Pick one UI style and stay consistent across your resources. + +--- + +## Security Notes (Read Twice) + +1. Start every server net event with `local src = source`. +2. Resolve `xPlayer` from `src` on the server, not from client-sent identifiers. +3. Re-check money/items/job on the server right before mutation. +4. Never trust client-sent price, quantity, or grade. +5. Parameterize every SQL query (no string concatenation). + +--- + +## TL;DR + +- `ESX.GetPlayerFromId(src)` is the core server entrypoint. +- Use `xPlayer` methods for money, jobs, and inventory. +- Client `ESX.GetPlayerData()` is display-only, never security. +- Job/admin checks belong on server handlers. +- ESX APIs vary by fork/version, so confirm function names on your stack. + +--- + +## Sources + +- [ESX Documentation](https://documentation.esx-framework.org/) +- [es_extended (GitHub)](https://github.com/esx-framework/esx_core/tree/main/%5Bcore%5D/es_extended) +- [esx-framework (GitHub org)](https://github.com/esx-framework) +- [FiveM Scripting Docs](https://docs.fivem.net/docs/scripting-manual/) + +--- + +Next: [`05-frameworks/03-qbcore-basics.md`](03-qbcore-basics.md) \ No newline at end of file diff --git a/05-frameworks/03-qbcore-basics.md b/05-frameworks/03-qbcore-basics.md new file mode 100644 index 0000000..2103f97 --- /dev/null +++ b/05-frameworks/03-qbcore-basics.md @@ -0,0 +1,247 @@ +# 03. QBCore Basics + +## Plain English + +A **framework** in FiveM is the resource that owns "what is a player". It defines the player object, jobs, gangs, money, inventory hooks, character creation, and the events that fire on login/logout. + +Think of it as the server's operating layer for gameplay logic: your scripts plug into the framework instead of reinventing player/account/job systems every time. + +**QBCore (`qb-core`)** is one of the most used RP frameworks and the base that inspired Qbox. + +### What Makes QBCore Different + +- Huge ecosystem: many public scripts are written for QBCore first. +- Familiar `QBCore.Functions` API and `PlayerData` shape used by years of tutorials/resources. +- Compared to Qbox, QBCore is older and less "bridge-first" modernized, but still very common in production servers. + +### Who Made It + +QBCore is built and maintained by the **QBCore Framework** community/team. + +- QBCore docs: [https://docs.qbcore.org/](https://docs.qbcore.org/) +- QBCore GitHub org: [https://github.com/qbcore-framework](https://github.com/qbcore-framework) + + +This lesson assumes you have the latest version of QBCore. + +--- + +## Get Core Object And Player (Server Side) + +```lua +-- ↓ get core object once in server scope +local QBCore = exports['qb-core']:GetCoreObject() + +-- ↓ later in an event/command with source +local src = source +local player = QBCore.Functions.GetPlayer(src) + +-- ↓ ALWAYS nil-check for join/disconnect race windows +if not player then return end + +-- ↓ read common data from PlayerData +print(player.PlayerData.citizenid) +print(player.PlayerData.license) +print(player.PlayerData.name) +print(player.PlayerData.money.cash) +print(player.PlayerData.money.bank) +print(player.PlayerData.job.name) +print(player.PlayerData.job.grade.level) +print(player.PlayerData.metadata.hunger) +``` + +--- + +## Money Functions + +```lua +local player = QBCore.Functions.GetPlayer(source) +if not player then return end + +-- ↓ add money to account +player.Functions.AddMoney('cash', 100, 'shop_refund') +player.Functions.AddMoney('bank', 500, 'salary') + +-- ↓ remove money. returns boolean in typical QBCore setups +local ok = player.Functions.RemoveMoney('cash', 80, 'shop_buy_bread') +if not ok then + return +end + +-- ↓ read balances +local cash = player.PlayerData.money.cash +local bank = player.Functions.GetMoney('bank') + +-- ↓ set balance directly (admin/migration use) +player.Functions.SetMoney('cash', 1000, 'admin_set') +``` + +Always check return values and validate price/amount on the server. + +--- + +## Job Functions + +```lua +local player = QBCore.Functions.GetPlayer(source) +if not player then return end + +-- ↓ assign job and grade level +player.Functions.SetJob('police', 2) + +-- ↓ toggle duty state +player.Functions.SetJobDuty(true) + +-- ↓ read job data +local job = player.PlayerData.job +if job.name == 'police' and job.grade.level >= 2 then + -- permitted for police grade 2+ +end +``` + +--- + +## Client Player Data (Display Only) + +```lua +-- ↓ on client +local QBCore = exports['qb-core']:GetCoreObject() +local pdata = QBCore.Functions.GetPlayerData() + +print(pdata.citizenid) +print(pdata.job and pdata.job.name) +``` + +Client data is for UI/UX. Server checks are the real security boundary. + +--- + +## Common Client Events + +```lua +RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() + print('character loaded') +end) + +RegisterNetEvent('QBCore:Client:OnJobUpdate', function(job) + print('job updated:', job.name) +end) + +RegisterNetEvent('QBCore:Client:OnMoneyChange', function(type, amount, isRemoved) + print(type, amount, isRemoved) +end) +``` + +--- + +## Server Lifecycle Events + +```lua +AddEventHandler('QBCore:Server:OnPlayerLoaded', function(player) + -- initialize player-specific cache/state +end) + +AddEventHandler('QBCore:Server:OnPlayerUnload', function(src) + -- cleanup player-specific cache/state +end) +``` + +--- + +## Permission Checks + +```lua +local player = QBCore.Functions.GetPlayer(source) +if not player then return end + +if player.PlayerData.job.name ~= 'police' then return end +if player.PlayerData.job.grade.level < 2 then return end +``` + +Do this server-side in the handler that mutates data. + +--- + +## Loop Online Players + +```lua +local QBCore = exports['qb-core']:GetCoreObject() + +-- ↓ helper table of online players +local players = QBCore.Functions.GetQBPlayers() +for src, player in pairs(players) do + print(src, player.PlayerData.citizenid) +end + +-- ↓ universal fallback +for _, pidStr in ipairs(GetPlayers()) do + local p = QBCore.Functions.GetPlayer(tonumber(pidStr)) + if p then + print(p.PlayerData.citizenid) + end +end +``` + +--- + +## Notifications + +```lua +-- ↓ framework-style notify event +TriggerClientEvent('QBCore:Notify', source, 'Purchase complete', 'success') + +-- ↓ modern alternative if ox_lib is installed +TriggerClientEvent('ox_lib:notify', source, { + title = 'Shop', + description = 'You bought bread', + type = 'success', +}) +``` + +--- + +## Common Mistakes + +### 1. Mutating PlayerData directly + +```lua +-- BAD: does not reliably persist/sync +player.PlayerData.money.cash = 999999 + +-- GOOD: use framework functions +player.Functions.SetMoney('cash', 500, 'admin_fix') +``` + +### 2. Skipping nil checks + +```lua +local player = QBCore.Functions.GetPlayer(source) +if not player then return end +``` + +### 3. Trusting client values + +Never trust client-sent price, quantity, job grade, or item count. + +--- + +## TL;DR + +- Get core with `exports['qb-core']:GetCoreObject()`. +- Resolve players with `QBCore.Functions.GetPlayer(src)`. +- Use `player.Functions` for money/job mutations, not direct table edits. +- Client `GetPlayerData()` is display only. +- QBCore has the biggest legacy script ecosystem and docs footprint. + +--- + +## Sources + +- [QBCore Docs](https://docs.qbcore.org/) +- [qb-core repository](https://github.com/qbcore-framework/qb-core) +- [QBCore Framework org](https://github.com/qbcore-framework) +- [FiveM Scripting Docs](https://docs.fivem.net/docs/scripting-manual/) + +--- + +Next: [`06-ox-libraries/01-ox-lib.md`](../06-ox-libraries/01-ox-lib.md) \ No newline at end of file diff --git a/INDEX.md b/INDEX.md index 3add53b..1ce35d1 100644 --- a/INDEX.md +++ b/INDEX.md @@ -21,17 +21,19 @@ Read in order. ~2-4 hours of reading + as long as you want for the projects. 11. [`04-database/01-oxmysql-basics.md`](04-database/01-oxmysql-basics.md) - talk to MySQL 12. [`04-database/02-queries-and-security.md`](04-database/02-queries-and-security.md) - don't get SQL-injected 13. [`05-frameworks/01-qbox-basics.md`](05-frameworks/01-qbox-basics.md) - player object, jobs, money -14. [`06-ox-libraries/01-ox-lib.md`](06-ox-libraries/01-ox-lib.md) - the swiss-army library -15. [`06-ox-libraries/02-ox-target.md`](06-ox-libraries/02-ox-target.md) - third-eye targeting -16. [`06-ox-libraries/03-inventories.md`](06-ox-libraries/03-inventories.md) - items -17. [`07-nui/01-nui-basics.md`](07-nui/01-nui-basics.md) - HTML UI inside the game -18. [`07-nui/02-react-nui.md`](07-nui/02-react-nui.md) - production NUI with React -19. [`08-security/01-security-checklist.md`](08-security/01-security-checklist.md) - the audit list -20. [`09-performance/01-threads-and-waits.md`](09-performance/01-threads-and-waits.md) - don't tank FPS -21. [`09-performance/02-optimization-patterns.md`](09-performance/02-optimization-patterns.md) - make it fast -22. [`10-first-projects/01-hello-resource.md`](10-first-projects/01-hello-resource.md) - your first resource -23. [`10-first-projects/02-shop.md`](10-first-projects/02-shop.md) - full shop with security -24. [`10-first-projects/03-nui-menu.md`](10-first-projects/03-nui-menu.md) - HTML menu +14. [`05-frameworks/02-esx-basics.md`](05-frameworks/02-esx-basics.md) - xPlayer, money accounts, jobs +15. [`05-frameworks/03-qbcore-basics.md`](05-frameworks/03-qbcore-basics.md) - core object, PlayerData, jobs, money +16. [`06-ox-libraries/01-ox-lib.md`](06-ox-libraries/01-ox-lib.md) - the swiss-army library +17. [`06-ox-libraries/02-ox-target.md`](06-ox-libraries/02-ox-target.md) - third-eye targeting +18. [`06-ox-libraries/03-inventories.md`](06-ox-libraries/03-inventories.md) - items +19. [`07-nui/01-nui-basics.md`](07-nui/01-nui-basics.md) - HTML UI inside the game +20. [`07-nui/02-react-nui.md`](07-nui/02-react-nui.md) - production NUI with React +21. [`08-security/01-security-checklist.md`](08-security/01-security-checklist.md) - the audit list +22. [`09-performance/01-threads-and-waits.md`](09-performance/01-threads-and-waits.md) - don't tank FPS +23. [`09-performance/02-optimization-patterns.md`](09-performance/02-optimization-patterns.md) - make it fast +24. [`10-first-projects/01-hello-resource.md`](10-first-projects/01-hello-resource.md) - your first resource +25. [`10-first-projects/02-shop.md`](10-first-projects/02-shop.md) - full shop with security +26. [`10-first-projects/03-nui-menu.md`](10-first-projects/03-nui-menu.md) - HTML menu --- @@ -98,9 +100,11 @@ MySQL via oxmysql. Query patterns, parameterization, race-condition prevention. - [`02-queries-and-security.md`](04-database/02-queries-and-security.md) ### [05-frameworks](05-frameworks/) -Qbox: player object, jobs, gangs, money, metadata. +Qbox + ESX + QBCore: player object, jobs, money/accounts, metadata. - [`01-qbox-basics.md`](05-frameworks/01-qbox-basics.md) +- [`02-esx-basics.md`](05-frameworks/02-esx-basics.md) +- [`03-qbcore-basics.md`](05-frameworks/03-qbcore-basics.md) ### [06-ox-libraries](06-ox-libraries/) The libraries 90% of modern resources use. diff --git a/README.md b/README.md index debbc1c..ee6f204 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ VS Code extensions worth installing day one: 02-events → how scripts talk to each other 03-natives → the GTA V API 04-database → MySQL with oxmysql -05-frameworks → Qbox / QBCore basics +05-frameworks → Qbox / QBCore / ESX basics 06-ox-libraries → ox_lib, ox_target, ox_inventory 07-nui → UI (HTML/CSS/JS, then React) 08-security → don't get exploited