Part 1 final build: Los Santos RP written on camera by the agent

rp_core (sessions, scrypt auth, self-written MySQL-wire driver rp_db),
county-records NUI: residency-file auth, identity-record character
intake, survey-grid spawn, procedural night-city loading screen.
Secrets replaced with .example files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 19:27:48 +00:00
co-authored by Claude Fable 5
parent cb0649016c
commit 80c1d75d2f
41 changed files with 6801 additions and 0 deletions
@@ -0,0 +1,11 @@
fx_version 'cerulean'
game 'gta5'
name 'rp_dbtest'
description 'Self-test for the rp_db driver. Not started in production.'
version '1.0.0'
server_scripts {
'@rp_db/lib/db.lua',
'server/test.lua',
}
@@ -0,0 +1,122 @@
-- Self-test for the hand-written MySQL driver. Run with: ensure rp_dbtest
local passed, failed = 0, 0
local function check(name, ok, detail)
if ok then
passed = passed + 1
print(('^2 PASS^7 %s'):format(name))
else
failed = failed + 1
print(('^1 FAIL^7 %s^1 %s^7'):format(name, detail or ''))
end
end
CreateThread(function()
print('^5[dbtest]^7 waiting for the pool...')
if not DB.WaitReady(30000) then
print('^1[dbtest] database never became ready^7')
return
end
-- 1. round trip through the text protocol -------------------------------
local two = DB.Scalar('SELECT 1 + 1')
check('scalar arithmetic', two == 2, ('got %s (%s)'):format(tostring(two), type(two)))
local row = DB.Single('SELECT ? AS s, ? AS n, ? AS f', { 'hello', 42, 1.5 })
check('typed columns', row and row.s == 'hello' and row.n == 42 and row.f == 1.5,
row and json.encode(row) or 'nil')
-- 2. hostile strings must survive verbatim ------------------------------
DB.Update("DELETE FROM accounts WHERE username LIKE 'ztest%'")
local nasty = [[O'Brien \ "quoted" ]] .. '\n\t' .. [[ 100% ` ; -- /* */ ?]]
local id = DB.Insert(
'INSERT INTO accounts (username, password_hash) VALUES (?, ?)',
{ 'ztest_a', nasty })
check('insert returns id', type(id) == 'number' and id > 0, tostring(id))
local back = DB.Scalar('SELECT password_hash FROM accounts WHERE id = ?', { id })
check('hostile string round trip', back == nasty,
('stored %q'):format(tostring(back)))
-- 3. a parameter can never become SQL -----------------------------------
local inject = "'); DROP TABLE accounts; -- "
DB.Insert('INSERT INTO accounts (username, password_hash) VALUES (?, ?)',
{ 'ztest_b', inject })
local stillThere = DB.Scalar(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'accounts'")
check('injection is inert', stillThere == 1, 'accounts table missing!')
local storedInject = DB.Scalar("SELECT password_hash FROM accounts WHERE username = 'ztest_b'")
check('injection stored literally', storedInject == inject, tostring(storedInject))
-- 4. question marks inside literals must not shift binding ---------------
local q = DB.Single("SELECT 'what? really?' AS lit, ? AS bound", { 'bound-value' })
check('literal ? not treated as placeholder',
q and q.lit == 'what? really?' and q.bound == 'bound-value',
q and json.encode(q) or 'nil')
-- 5. NULL handling -------------------------------------------------------
DB.Update('UPDATE accounts SET last_ip = ? WHERE id = ?', { DB.NULL, id })
local isNull = DB.Scalar('SELECT last_ip IS NULL FROM accounts WHERE id = ?', { id })
check('DB.NULL writes a real NULL', isNull == 1, tostring(isNull))
local nilRow = DB.Single('SELECT last_ip FROM accounts WHERE id = ?', { id })
check('NULL reads back as nil', nilRow ~= nil and nilRow.last_ip == nil,
nilRow and json.encode(nilRow) or 'nil')
-- 6. unicode -------------------------------------------------------------
local uni = 'Ünïcødé — 日本語 — 🚓'
DB.Update('UPDATE accounts SET password_hash = ? WHERE id = ?', { uni, id })
check('utf8mb4 round trip', DB.Scalar('SELECT password_hash FROM accounts WHERE id = ?', { id }) == uni)
-- 7. empty result sets ---------------------------------------------------
local none = DB.Query('SELECT * FROM accounts WHERE id = ?', { -1 })
check('empty select returns empty table', type(none) == 'table' and #none == 0)
check('missing single returns nil', DB.Single('SELECT * FROM accounts WHERE id = ?', { -1 }) == nil)
-- 8. transactions --------------------------------------------------------
local ok = DB.Transaction({
{ query = 'UPDATE accounts SET role = ? WHERE id = ?', values = { 'admin', id } },
{ query = 'UPDATE accounts SET banned = ? WHERE id = ?', values = { 1, id } },
})
local after = DB.Single('SELECT role, banned FROM accounts WHERE id = ?', { id })
check('transaction commits', ok and after.role == 'admin' and after.banned == 1,
json.encode(after or {}))
local bad, err = DB.Transaction({
{ query = 'UPDATE accounts SET role = ? WHERE id = ?', values = { 'mod', id } },
{ query = 'UPDATE accounts SET nonexistent_column = 1 WHERE id = ?', values = { id } },
})
local rolled = DB.Scalar('SELECT role FROM accounts WHERE id = ?', { id })
check('failed transaction rolls back', bad == false and rolled == 'admin',
('role is now %s'):format(tostring(rolled)))
-- 9. errors surface instead of failing silently --------------------------
local res, qerr = DB.Query('SELECT * FROM table_that_does_not_exist')
check('bad query returns an error', res == nil and type(qerr) == 'string' and #qerr > 0,
tostring(qerr))
-- 10. wrong parameter count is caught, not guessed -----------------------
local _, cerr = DB.Query('SELECT ?, ?', { 1 })
check('parameter count mismatch reported', cerr ~= nil, tostring(cerr))
-- 11. a large payload exercises multi-packet framing in both directions --
local big = string.rep('x', 700000)
check('700KB payload round trip', DB.Scalar('SELECT ? AS big', { big }) == big)
-- 12. concurrency: many queries in flight across the pool ----------------
local done, results = 0, {}
for i = 1, 40 do
DB.ScalarAsync('SELECT ?', { i }, function(v) done = done + 1; results[i] = v end)
end
local waited = 0
while done < 40 and waited < 10000 do Wait(50); waited = waited + 50 end
local allGood = done == 40
for i = 1, 40 do if results[i] ~= i then allGood = false end end
check('40 concurrent queries all answered correctly', allGood, ('done=%d'):format(done))
DB.Update("DELETE FROM accounts WHERE username LIKE 'ztest%'")
print(('^5[dbtest]^7 ==== %d passed, %d failed ===='):format(passed, failed))
if failed == 0 then print('^2[dbtest] driver OK^7') else print('^1[dbtest] DRIVER HAS PROBLEMS^7') end
end)