Skip to content

Commit 11052e2

Browse files
committed
wip player creation with state and backward compat
1 parent 66d9435 commit 11052e2

File tree

1 file changed

+343
-0
lines changed

1 file changed

+343
-0
lines changed

server/player.lua

+343
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
-- Backwards Compat
2+
3+
local function extractData(source, prefix)
4+
local state = Player(source).state
5+
local data = {}
6+
7+
for key, value in pairs(state) do
8+
if key:find('^' .. prefix) then
9+
local shortKey = key:gsub(prefix, '')
10+
data[shortKey] = value or 'Unknown'
11+
end
12+
end
13+
14+
return data
15+
end
16+
17+
local function CreateLegacyPlayerData(player)
18+
local source = player.source
19+
local state = Player(source).state
20+
local legacy = {}
21+
legacy.license = QBCore.Functions.GetIdentifier(source, 'license')
22+
legacy.source = source
23+
legacy.name = GetPlayerName(source)
24+
legacy.citizenid = state['citizenid']
25+
legacy.money = extractData(source, 'money:')
26+
legacy.job = extractData(source, 'job:')
27+
legacy.gang = extractData(source, 'gang:')
28+
legacy.metadata = extractData(source, 'metadata:')
29+
legacy.charinfo = extractData(source, 'charinfo:')
30+
legacy.position = extractData(source, 'position:')
31+
32+
setmetatable(legacy, {
33+
__index = function(tbl, key)
34+
if key == 'money' then
35+
return extractData(source, 'money:')
36+
elseif key == 'job' then
37+
return extractData(source, 'job:')
38+
elseif key == 'gang' then
39+
return extractData(source, 'gang:')
40+
elseif key == 'metadata' then
41+
return extractData(source, 'metadata:')
42+
elseif key == 'charinfo' then
43+
return extractData(source, 'charinfo:')
44+
elseif key == 'position' then
45+
return extractData(source, 'position:')
46+
else
47+
return rawget(tbl, key)
48+
end
49+
end,
50+
__newindex = function(tbl, key, value)
51+
local state = Player(source).state
52+
if key == 'money' and type(value) == 'table' then
53+
for subkey, subvalue in pairs(value) do
54+
state:set('money:' .. subkey, subvalue, true)
55+
end
56+
elseif key == 'job' and type(value) == 'table' then
57+
for subkey, subvalue in pairs(value) do
58+
state:set('job:' .. subkey, subvalue, true)
59+
end
60+
elseif key == 'gang' and type(value) == 'table' then
61+
for subkey, subvalue in pairs(value) do
62+
state:set('gang:' .. subkey, subvalue, true)
63+
end
64+
elseif key == 'metadata' and type(value) == 'table' then
65+
for subkey, subvalue in pairs(value) do
66+
state:set('metadata:' .. subkey, subvalue, true)
67+
end
68+
elseif key == 'charinfo' and type(value) == 'table' then
69+
for subkey, subvalue in pairs(value) do
70+
state:set('charinfo:' .. subkey, subvalue, true)
71+
end
72+
elseif key == 'position' and type(value) == 'table' then
73+
for subkey, subvalue in pairs(value) do
74+
state:set('position:' .. subkey, subvalue, true)
75+
end
76+
else
77+
rawset(tbl, key, value)
78+
end
79+
end,
80+
})
81+
return legacy
82+
end
83+
84+
-- JSON STUFF
85+
86+
local DynamicDefaults = {
87+
['citizenid'] = QBCore.Functions.CreateCitizenId,
88+
['charinfo.phone'] = QBCore.Functions.CreatePhoneNumber,
89+
['charinfo.account'] = QBCore.Functions.CreateAccountNumber,
90+
['metadata.bloodtype'] = function() return QBCore.Functions.GetRandomElement(QBCore.Config.Player.Bloodtypes) end,
91+
['metadata.fingerprint'] = QBCore.Functions.CreateFingerId,
92+
['metadata.walletid'] = QBCore.Functions.CreateWalletId
93+
}
94+
95+
local function ApplyDynamicDefaults(target)
96+
for field, func in pairs(DynamicDefaults) do
97+
local ref = target
98+
local keys = {}
99+
for key in field:gmatch('[^.]+') do table.insert(keys, key) end
100+
101+
for i = 1, #keys - 1 do
102+
ref[keys[i]] = ref[keys[i]] or {}
103+
ref = ref[keys[i]]
104+
end
105+
106+
ref[keys[#keys]] = ref[keys[#keys]] or func()
107+
end
108+
end
109+
110+
local function LoadPlayerDefaults()
111+
local success, defaults = pcall(function()
112+
return json.decode(LoadResourceFile(resourceName, 'shared/player_defaults.json'))
113+
end)
114+
115+
if not success or not defaults or not next(defaults) then
116+
print('^1[ERROR]^0 Could not load player_defaults.json. Ensure the file is valid JSON and not empty.')
117+
return {}
118+
end
119+
120+
ApplyDynamicDefaults(defaults)
121+
return defaults
122+
end
123+
124+
local function MergePlayerData(target, defaults, debug)
125+
for key, value in pairs(defaults) do
126+
if type(value) == 'table' then
127+
target[key] = target[key] or {}
128+
MergePlayerData(target[key], value, debug)
129+
else
130+
if not debug then
131+
if target[key] == nil then
132+
print(('^2[INFO]^0 Added new data field: %s = %s'):format(key, tostring(value)))
133+
elseif target[key] ~= value then
134+
print(('^3[INFO]^0 Updated data field: %s = %s'):format(key, tostring(value)))
135+
end
136+
end
137+
target[key] = (target[key] == nil or target[key] == '') and value or target[key]
138+
end
139+
end
140+
end
141+
142+
-- Player Creation
143+
144+
local function InitializePlayerStateBag(source, playerData)
145+
local state = Player(source).state
146+
local defaults = LoadPlayerDefaults()
147+
148+
for _, key in pairs(GetStateBagKeys('player:' .. source)) do
149+
if not defaults[key] then
150+
print(('^1[INFO]^0 Removed outdated data field: %s'):format(key))
151+
end
152+
state:set(key, nil, true)
153+
end
154+
155+
MergePlayerData(playerData, defaults)
156+
157+
local function populatePlayerState(data, prefix)
158+
for key, value in pairs(data) do
159+
if type(value) == 'table' then
160+
populatePlayerState(value, prefix .. key .. ':')
161+
else
162+
state:set(prefix .. key, value, true)
163+
end
164+
end
165+
end
166+
167+
populatePlayerState(playerData, '')
168+
end
169+
170+
function QBCore.Player.Login(source, citizenid, newData)
171+
if not source or source == '' then
172+
QBCore.ShowError(resourceName, 'ERROR QBCORE.PLAYER.LOGIN - NO SOURCE GIVEN!')
173+
return false
174+
end
175+
176+
if citizenid then
177+
local license = QBCore.Functions.GetIdentifier(source, 'license')
178+
local data = MySQL.prepare.await('SELECT * FROM players WHERE citizenid = ?', { citizenid })
179+
180+
if not data then
181+
print('^1[ERROR]^0 Failed to load data for Citizen ID:', citizenid)
182+
DropPlayer(source, 'Failed to load your character data. Please contact staff.')
183+
return false
184+
end
185+
186+
if data and license == data.license then
187+
local success, playerData = pcall(function()
188+
return {
189+
money = json.decode(data.money) or {},
190+
job = json.decode(data.job) or {},
191+
gang = json.decode(data.gang) or {},
192+
position = json.decode(data.position) or {},
193+
metadata = json.decode(data.metadata) or {},
194+
charinfo = json.decode(data.charinfo) or {}
195+
}
196+
end)
197+
198+
if not success then
199+
print('^1[ERROR]^0 Failed to decode player data for Citizen ID:', citizenid)
200+
DropPlayer(source, 'Failed to decode your character data. Please contact staff.')
201+
return false
202+
end
203+
204+
InitializePlayerStateBag(source, playerData)
205+
else
206+
DropPlayer(source, Lang:t('info.exploit_dropped'))
207+
TriggerEvent('qb-log:server:CreateLog', 'anticheat', 'Anti-Cheat', 'white', GetPlayerName(source) .. ' Has Been Dropped For Character Joining Exploit', false)
208+
end
209+
else
210+
InitializePlayerStateBag(source, newData or {})
211+
end
212+
213+
return true
214+
end
215+
216+
function QBCore.Player.CreatePlayer(PlayerData, Offline)
217+
local self = {}
218+
self.Offline = Offline
219+
self.Functions = {}
220+
221+
function self.Functions.Notify(text, type, length)
222+
TriggerClientEvent('QBCore:Notify', PlayerData.source, text, type, length)
223+
end
224+
225+
function self.Functions.AddMethod(methodName, handler)
226+
self.Functions[methodName] = handler
227+
end
228+
229+
function self.Functions.AddField(fieldName, data)
230+
self[fieldName] = data
231+
local state = Player(PlayerData.source).state
232+
state:set(fieldName, data, true)
233+
end
234+
235+
local function CreateDynamicGetSet(prefix)
236+
return {
237+
Get = function(key)
238+
local state = Player(PlayerData.source).state
239+
return state[prefix .. key] or nil
240+
end,
241+
242+
Set = function(key, value)
243+
local state = Player(PlayerData.source).state
244+
state:set(prefix .. key, value, true)
245+
return true
246+
end
247+
}
248+
end
249+
250+
local function CreateDynamicIncremental(prefix)
251+
return {
252+
Get = function(key)
253+
local state = Player(PlayerData.source).state
254+
return state[prefix .. key] or 0
255+
end,
256+
257+
Set = function(key, value)
258+
local state = Player(PlayerData.source).state
259+
state:set(prefix .. key, value, true)
260+
return true
261+
end,
262+
263+
Add = function(key, value)
264+
local state = Player(PlayerData.source).state
265+
local currentValue = state[prefix .. key] or 0
266+
state:set(prefix .. key, currentValue + value, true)
267+
return true
268+
end,
269+
270+
Remove = function(key, value)
271+
local state = Player(PlayerData.source).state
272+
local currentValue = state[prefix .. key] or 0
273+
if currentValue < value then return false end
274+
state:set(prefix .. key, currentValue - value, true)
275+
return true
276+
end
277+
}
278+
end
279+
280+
self.Metadata = CreateDynamicGetSet('metadata:')
281+
self.Job = CreateDynamicGetSet('job:')
282+
self.Gang = CreateDynamicGetSet('gang:')
283+
self.Money = CreateDynamicIncremental('money:')
284+
self.CharInfo = CreateDynamicGetSet('charinfo:')
285+
self.Position = CreateDynamicGetSet('position:')
286+
self.PlayerData = CreateLegacyPlayerData(PlayerData)
287+
288+
if not self.Offline then
289+
QBCore.Players[PlayerData.source] = self
290+
QBCore.Player.Save(PlayerData.source)
291+
TriggerEvent('QBCore:Server:PlayerLoaded', self)
292+
end
293+
294+
return self
295+
end
296+
297+
function QBCore.Player.CheckPlayerData(source, PlayerData)
298+
PlayerData = PlayerData or {}
299+
local Offline = not source
300+
301+
if source then
302+
PlayerData.source = source
303+
PlayerData.license = PlayerData.license or QBCore.Functions.GetIdentifier(source, 'license')
304+
PlayerData.name = GetPlayerName(source)
305+
end
306+
307+
InitializePlayerStateBag(source, PlayerData)
308+
309+
return QBCore.Player.CreatePlayer(PlayerData, Offline)
310+
end
311+
312+
local function BuildPlayerSaveData(source)
313+
local state = Player(source).state
314+
315+
return {
316+
citizenid = state['citizenid'],
317+
money = json.encode(extractData(source, 'money:')),
318+
metadata = json.encode(extractData(source, 'metadata:')),
319+
position = json.encode(extractData(source, 'position:')),
320+
job = json.encode(extractData(source, 'job:')),
321+
gang = json.encode(extractData(source, 'gang:'))
322+
}
323+
end
324+
325+
function QBCore.Player.Save(source)
326+
local saveData = BuildPlayerSaveData(source)
327+
328+
if not saveData.citizenid or not saveData.license then
329+
print('^1[ERROR]^0 Skipped saving data for player [%s] due to missing critical data.'):format(GetPlayerName(source))
330+
return
331+
end
332+
333+
MySQL.insert(
334+
'INSERT INTO players (citizenid, money, metadata, position, job, gang) VALUES (:citizenid, :money, :metadata, :position, :job, :gang) ON DUPLICATE KEY UPDATE money = :money, metadata = :metadata, position = :position, job = :job, gang = :gang',
335+
saveData
336+
)
337+
338+
if GetResourceState('qb-inventory') ~= 'missing' then
339+
exports['qb-inventory']:SaveInventory(source)
340+
end
341+
342+
QBCore.ShowSuccess(resourceName, 'Player ' .. GetPlayerName(source) .. ' data saved successfully.')
343+
end

0 commit comments

Comments
 (0)