fw_core¶
Server-only core API. Version 1.1.0. API major 1.
Source: resources/[framework]/fw_core.
fw_core parses its manifest version, creates fw_players if that table is missing, and admits the connecting player. It has no client script. Gameplay resources register with it so a future API break fails at startup with a named error, instead of failing silently later.
API versioning¶
Manifest version is MAJOR.MINOR.PATCH. The API version is MAJOR.
| Rule | Detail |
|---|---|
| Minor bump | May add exports. Callers of the current API major keep working. |
| Patch bump | Never changes the API. |
| Major bump | A different API. No compatibility shims are kept for the old major. |
RegisterResource |
Takes one positive integer. Succeeds only when it equals the running major — there is no range syntax. |
1.1.0 is API 1. The player exports (GetPlayer, GetPlayerById, GetSourceByPlayerId) were added in 1.1.0 without changing the major.
Startup¶
IsReady() stays false until the manifest version is a valid MAJOR.MINOR.PATCH and sql/players.sql has been applied. A bad version or a failed table setup logs the error and leaves the resource not ready; connecting players are then refused.
The resource yields inside MySQL.ready.await before it becomes ready, while FXServer already reports it as started. A dependent that calls IsReady() once and gives up will run too early — poll it, as fw_characters does, for up to 120 seconds.
On success:
[fw_core][info] ready version=1.1.0 api=1
The local event fw_core:ready fires with (version, apiVersion) right after. A resource started later by ensure has already missed that event and should call IsReady() directly instead.
Debug lines are off unless this convar is 1, true, or yes (checked at the moment each line is written):
set fw_core_debug true
Startup, registration, and errors print regardless of the convar.
Exports¶
All exports are server-side. Where an argument is documented as integer, a whole-number float also works — the implementation normalizes with math.tointeger, so 1 and 1.0 both resolve to 1, while 1.5 is rejected.
| Export | Summary |
|---|---|
IsReady() |
Whether the resource is ready to serve calls. |
GetVersion() |
The manifest version string. |
GetApiVersion() |
The manifest major version as an integer. |
RegisterResource(requiredApiVersion) |
Declares API compatibility for the calling resource. |
GetPlayer(source) |
The connected account record for a server id. |
GetPlayerById(playerId) |
The connected account record for an account id. |
GetSourceByPlayerId(playerId) |
The server id for a connected account id. |
IsReady()¶
Signature
---@return boolean
exports.fw_core:IsReady()
Parameters — none.
Returns
| Type | Description |
|---|---|
boolean |
true once the manifest version parsed and fw_players is usable. false while starting, after a failed database setup, and immediately after fw_core stops. |
Example
if not exports.fw_core:IsReady() then
return
end
GetVersion()¶
Signature
---@return string|nil
exports.fw_core:GetVersion()
Parameters — none.
Returns
| Type | Description |
|---|---|
string |
The manifest version, e.g. '1.1.0'. |
nil |
Only when the manifest version string is missing or not shaped MAJOR.MINOR.PATCH. A database failure does not clear this — IsReady() can be false while this still returns '1.1.0'. |
Example
print(exports.fw_core:GetVersion()) --> '1.1.0'
GetApiVersion()¶
Signature
---@return integer|nil
exports.fw_core:GetApiVersion()
Parameters — none.
Returns
| Type | Description |
|---|---|
integer |
The manifest major version, e.g. 1. |
nil |
Same condition as GetVersion() — an invalid manifest version. |
Prefer this over parsing the string from GetVersion() yourself.
Example
print(exports.fw_core:GetApiVersion()) --> 1
RegisterResource(requiredApiVersion)¶
Signature
---@param requiredApiVersion integer
---@return boolean success
---@return string|nil err
exports.fw_core:RegisterResource(requiredApiVersion)
Records that the calling resource was written for requiredApiVersion. Call it once from that resource's own startup, after core is ready. The caller is read via GetInvokingResource() — there is no resource-name argument, and calling this from fw_core itself always fails.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
requiredApiVersion |
integer |
yes | The API major this resource was written for. Must be a positive whole number; floats with no fraction (1.0) are accepted. |
Returns
| Type | Description |
|---|---|
success: boolean |
true when registration succeeded. |
err: string \| nil |
nil on success; an error string otherwise (see Errors). |
Errors
err value |
Condition |
|---|---|
'fw_core is not ready' |
Core has not finished startup. |
'RegisterResource must be called from another resource' |
The caller was fw_core itself, or GetInvokingResource() was empty. |
'<resource>: required API version must be a positive integer' |
The argument was missing, fractional, zero, negative, or not a number. |
'fw_core API 1 is incompatible with required API 2; <resource> needs a newer core' |
The required integer is higher than the running API. |
'fw_core API 1 is incompatible with required API <n>; update <resource> for API 1' |
The required integer is lower than the running API. |
Every failure is also printed as [fw_core][error] by fw_core itself — the caller doesn't need to log it again, just decide whether to error() or continue.
Calling this again from the same resource replaces the stored requirement and logs updated registration. Stopping the resource removes the registration automatically; there is no unregister export.
Example
local ok, err = exports.fw_core:RegisterResource(1)
if not ok then
print(err)
return
end
GetPlayer(source)¶
Signature
---@param source integer
---@return table|nil player
exports.fw_core:GetPlayer(source)
Returns a copy of the connected account for that server id, read from memory — this never queries the database and never loads a row by license.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
source |
integer |
yes | Server id of the connected player. |
Returns
| Type | Description |
|---|---|
table |
A copy of the player record. |
nil |
source is not attached — this includes the entire playerConnecting deferral. The record exists from playerJoining onward (same moment fw_core:playerLoaded fires). |
Example
local player = exports.fw_core:GetPlayer(source)
if player then
print(('source %d is account %d'):format(source, player.id))
end
GetPlayerById(playerId)¶
Signature
---@param playerId integer
---@return table|nil player
exports.fw_core:GetPlayerById(playerId)
Same record and copy semantics as GetPlayer, looked up by fw_players.id instead of server id.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
playerId |
integer |
yes | fw_players.id of the account. |
Returns
| Type | Description |
|---|---|
table |
A copy of the player record. |
nil |
That account is offline. This is an online-players lookup, not a database read — an id that exists in the table but is offline still returns nil. |
Example
local player = exports.fw_core:GetPlayerById(1)
GetSourceByPlayerId(playerId)¶
Signature
---@param playerId integer
---@return integer|nil source
exports.fw_core:GetSourceByPlayerId(playerId)
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
playerId |
integer |
yes | fw_players.id of the account. |
Returns
| Type | Description |
|---|---|
integer |
The connected server id for that account. |
nil |
That account is offline. |
Example
local target = exports.fw_core:GetSourceByPlayerId(playerId)
if target then
TriggerClientEvent('my_resource:notify', target)
end
Player record¶
GetPlayer, GetPlayerById, fw_core:playerLoaded, and fw_core:playerDropped all pass this same shape. It is always a copy — changing the returned table does not change the cache or the database.
| Field | Type | Meaning |
|---|---|---|
id |
integer |
fw_players.id. Other tables should store this. |
source |
integer |
Current server id. Changes across reconnects. |
license |
string |
license: or license2: value, prefix included. Max 64 bytes. |
steam |
string \| nil |
steam: identifier when present and at most 40 bytes. |
discord |
string \| nil |
discord: identifier when present and at most 40 bytes. |
name |
string |
Player name: control characters removed, trimmed, clipped to 64 UTF-8 characters. Empty becomes 'unknown'. |
role |
string |
Account role. New rows use 'user'. Max 16 characters. |
characterSlots |
integer |
How many characters fw_characters may create. Column default is 5. |
warnings |
integer |
Stored count. fw_core does not act on it. |
banned |
boolean |
Always false on a connected player — a banned license never finishes connect. |
bannedUntil |
integer |
Always 0 on a connected player. |
role, warnings, character_slots, and the ban columns are not writable through any export — change them directly in the table. role, warnings, and a ban are re-read on the account's next connection. fw_characters re-reads character_slots at the moment a character is created, so a slot change applies to the next create without requiring a reconnect.
Connect, join, drop lifecycle¶
Identity comes from GetPlayerIdentifierByType(source, 'license'), falling back to license2 if that's missing. One license cannot hold two server slots — the later connection is dropped with This account is already connected.
playerConnecting answers the deferral with one of:
| Message | Condition |
|---|---|
The server database is not available. |
Schema setup failed. |
The server is still starting. Try again in a moment. |
Schema setup has not finished. |
Rockstar license was not found. Restart RedM and try again. |
No usable license, or the license is longer than 64 bytes. |
You are banned from this server. |
banned is set and banned_until is 0 (no expiry). |
You are banned until YYYY-MM-DD HH:MM UTC. |
banned is set and banned_until is still in the future. |
Could not load your player. |
The admit query threw. |
An expired ban (banned_until > 0 and ≤ os.time()) is cleared on that connection and the player is admitted.
playerJoining attaches the cache, sets the state bag, and fires fw_core:playerLoaded. A failure here drops the player with the same class of message. playerDropped fires fw_core:playerDropped with the copy (before the cache entry is cleared), drops the cache, and updates last_seen.
There is no export to ban, warn, or change role from Lua. Those columns exist for an admin tool to update directly.
Local events¶
Server events — not network events.
AddEventHandler('fw_core:ready', function(version, apiVersion)
-- version: string, e.g. '1.1.0'
-- apiVersion: integer, e.g. 1
end)
AddEventHandler('fw_core:playerLoaded', function(source, player)
-- player: copy of the player record
end)
AddEventHandler('fw_core:playerDropped', function(source, player)
-- player: copy captured before the cache entry was removed
end)
State bag¶
Player(source).state.fwPlayerId is the account id. It replicates to the client:
local playerId = LocalPlayer.state.fwPlayerId
A failed write retries on a generation counter; a newer attach or drop cancels any retry that belonged to the previous value, so a stale retry can never overwrite a current id.
Console command¶
fw_health
Restricted to the server console (source == 0); a player who runs it gets no output.
[fw_core] fw_health
ready: true
version: 1.1.0
api: 1
debug: true
players: 1
id=1 source=4 Player name
registrations: 1
fw_characters api=1
debug reflects the convar at the moment the command runs. players is the in-memory connected count. Each registration line is the resource name and the API major it required.
Files¶
| File | Purpose |
|---|---|
fxmanifest.lua |
Version 1.1.0, dependency oxmysql, server scripts only. |
sql/players.sql |
CREATE TABLE IF NOT EXISTS fw_players. |
server/players.lua |
Connect, cache, state bag, schema. |
server/main.lua |
Version, registration, health command, exports. |