· 8 years ago · Mar 27, 2018, 07:40 PM
1--[[---------------------------------------------------------------------------
2Functions and variables
3---------------------------------------------------------------------------]]
4local setUpNonOwnableDoors,
5 setUpTeamOwnableDoors,
6 setUpGroupDoors,
7 migrateDB
8
9--[[---------------------------------------------------------
10 Database initialize
11 ---------------------------------------------------------]]
12function DarkRP.initDatabase()
13 MySQLite.begin()
14 -- Gotta love the difference between SQLite and MySQL
15 local AUTOINCREMENT = MySQLite.isMySQL() and "AUTO_INCREMENT" or "AUTOINCREMENT"
16
17 -- Table that holds all position data (jail, spawns etc.)
18 -- Queue these queries because other queries depend on the existence of the darkrp_position table
19 -- Race conditions could occur if the queries are executed simultaneously
20 MySQLite.queueQuery([[
21 CREATE TABLE IF NOT EXISTS darkrp_position(
22 id INTEGER NOT NULL PRIMARY KEY ]] .. AUTOINCREMENT .. [[,
23 map VARCHAR(45) NOT NULL,
24 type CHAR(1) NOT NULL,
25 x INTEGER NOT NULL,
26 y INTEGER NOT NULL,
27 z INTEGER NOT NULL
28 );
29 ]])
30
31 -- team spawns require extra data
32 MySQLite.queueQuery([[
33 CREATE TABLE IF NOT EXISTS darkrp_jobspawn(
34 id INTEGER NOT NULL PRIMARY KEY,
35 team INTEGER NOT NULL
36 );
37 ]])
38
39 if MySQLite.isMySQL() then
40 MySQLite.queueQuery([[
41 SELECT NULL FROM information_schema.TABLE_CONSTRAINTS WHERE
42 CONSTRAINT_SCHEMA = DATABASE() AND
43 CONSTRAINT_NAME = 'fk_darkrp_jobspawn_position' AND
44 CONSTRAINT_TYPE = 'FOREIGN KEY'
45 ]], function(data)
46 if data and data[1] then return end
47
48 MySQLite.query([[
49 ALTER TABLE darkrp_jobspawn ADD CONSTRAINT `fk_darkrp_jobspawn_position` FOREIGN KEY(id) REFERENCES darkrp_position(id)
50 ON UPDATE CASCADE
51 ON DELETE CASCADE;
52 ]])
53 end)
54 end
55
56 MySQLite.query([[
57 CREATE TABLE IF NOT EXISTS playerinformation(
58 uid BIGINT NOT NULL,
59 steamID VARCHAR(50) NOT NULL PRIMARY KEY
60 )
61 ]])
62
63 -- Player information
64 MySQLite.query([[
65 CREATE TABLE IF NOT EXISTS darkrp_player(
66 uid BIGINT NOT NULL PRIMARY KEY,
67 rpname VARCHAR(45),
68 salary INTEGER NOT NULL DEFAULT 45,
69 wallet INTEGER NOT NULL
70 );
71 ]])
72
73 -- Door data
74 MySQLite.query([[
75 CREATE TABLE IF NOT EXISTS darkrp_door(
76 idx INTEGER NOT NULL,
77 map VARCHAR(45) NOT NULL,
78 title VARCHAR(25),
79 isLocked BOOLEAN,
80 isDisabled BOOLEAN NOT NULL DEFAULT FALSE,
81 PRIMARY KEY(idx, map)
82 );
83 ]])
84
85 -- Some doors are owned by certain teams
86 MySQLite.query([[
87 CREATE TABLE IF NOT EXISTS darkrp_doorjobs(
88 idx INTEGER NOT NULL,
89 map VARCHAR(45) NOT NULL,
90 job VARCHAR(255) NOT NULL,
91 PRIMARY KEY(idx, map, job)
92 );
93 ]])
94
95 -- Door groups
96 MySQLite.query([[
97 CREATE TABLE IF NOT EXISTS darkrp_doorgroups(
98 idx INTEGER NOT NULL,
99 map VARCHAR(45) NOT NULL,
100 doorgroup VARCHAR(100) NOT NULL,
101 PRIMARY KEY(idx, map)
102 )
103 ]])
104
105 MySQLite.queueQuery([[
106 CREATE TABLE IF NOT EXISTS darkrp_dbversion(version INTEGER NOT NULL PRIMARY KEY)
107 ]])
108
109 -- Load the last DBVersion into DarkRP.DBVersion, to allow checks to see whether migration is needed.
110 MySQLite.queueQuery([[
111 SELECT MAX(version) AS version FROM darkrp_dbversion
112 ]], function(data) DarkRP.DBVersion = data and data[1] and tonumber(data[1].version) or 0 end)
113
114 MySQLite.queueQuery([[
115 REPLACE INTO darkrp_dbversion VALUES(20160610)
116 ]])
117
118 -- SQlite doesn't really handle foreign keys strictly, neither does MySQL by default
119 -- So to keep the DB clean, here's a manual partial foreign key enforcement
120 -- For now it's deletion only, since updating of the common attribute doesn't happen.
121
122 -- MySQL trigger
123 if MySQLite.isMySQL() then
124 MySQLite.query("show triggers", function(data)
125 -- Check if the trigger exists first
126 if data then
127 for _, v in ipairs(data) do
128 if v.Trigger == "JobPositionFKDelete" then
129 return
130 end
131 end
132 end
133
134 MySQLite.query("SHOW PRIVILEGES", function(privs)
135 if not privs then return end
136
137 local found
138 for _, v in ipairs(privs) do
139 if v.Privilege == "Trigger" then
140 found = true
141 break
142 end
143 end
144
145 if not found then return end
146 MySQLite.query([[
147 CREATE TRIGGER JobPositionFKDelete
148 AFTER DELETE ON darkrp_position
149 FOR EACH ROW
150 IF OLD.type = "T" THEN
151 DELETE FROM darkrp_jobspawn WHERE darkrp_jobspawn.id = OLD.id;
152 END IF
153 ;
154 ]])
155 end)
156 end)
157 else -- SQLite triggers, quite a different syntax
158 MySQLite.query([[
159 CREATE TRIGGER IF NOT EXISTS JobPositionFKDelete
160 AFTER DELETE ON darkrp_position
161 FOR EACH ROW
162 WHEN OLD.type = "T"
163 BEGIN
164 DELETE FROM darkrp_jobspawn WHERE darkrp_jobspawn.id = OLD.id;
165 END;
166 ]])
167 end
168 MySQLite.commit(fp{migrateDB, -- Migrate the database
169 function() -- Initialize the data after all the tables have been created
170 setUpNonOwnableDoors()
171 setUpTeamOwnableDoors()
172 setUpGroupDoors()
173
174 if MySQLite.isMySQL() then -- In a listen server, the connection with the external database is often made AFTER the listen server host has joined,
175 --so he walks around with the settings from the SQLite database
176 for _, v in ipairs(player.GetAll()) do
177 DarkRP.offlinePlayerData(v:SteamID(), function(data)
178 if not data or not data[1] then return end
179
180 local Data = data[1]
181 v:setDarkRPVar("rpname", Data.rpname)
182 v:setSelfDarkRPVar("salary", Data.salary)
183 v:setDarkRPVar("money", Data.wallet)
184 end)
185 end
186 end
187
188 hook.Call("DarkRPDBInitialized")
189 end})
190end
191
192--[[---------------------------------------------------------------------------
193Database migration
194backwards compatibility with older versions of DarkRP
195---------------------------------------------------------------------------]]
196function migrateDB(callback)
197 local migrateCount = 0
198
199 local onFinish = function()
200 migrateCount = migrateCount + 1
201
202 if migrateCount == 2 then callback() end
203 end
204 -- migrte from darkrp_jobown to darkrp_doorjobs
205 MySQLite.tableExists("darkrp_jobown", function(exists)
206 if not exists then return onFinish() end
207
208 MySQLite.begin()
209 -- Create a temporary table that links job IDs to job commands
210 MySQLite.queueQuery("CREATE TABLE IF NOT EXISTS TempJobCommands(id INT NOT NULL PRIMARY KEY, cmd VARCHAR(255) NOT NULL);")
211 if MySQLite.isMySQL() then
212 local jobCommands = {}
213 for k, v in pairs(RPExtraTeams) do
214 table.insert(jobCommands, "(" .. k .. "," .. MySQLite.SQLStr(v.command) .. ")")
215 end
216
217 -- This WOULD work with SQLite if the implementation in GMod wasn't out of date.
218 MySQLite.queueQuery("INSERT IGNORE INTO TempJobCommands VALUES " .. table.concat(jobCommands, ",") .. ";")
219 else
220 for k, v in pairs(RPExtraTeams) do
221 MySQLite.queueQuery("INSERT INTO TempJobCommands VALUES(" .. k .. ", " .. MySQLite.SQLStr(v.command) .. ");")
222 end
223 end
224
225 MySQLite.queueQuery("REPLACE INTO darkrp_doorjobs SELECT darkrp_jobown.idx AS idx, darkrp_jobown.map AS map, TempJobCommands.cmd AS job FROM darkrp_jobown JOIN TempJobCommands ON darkrp_jobown.job = TempJobCommands.id;")
226
227 -- Clean up the transition table and the old table
228 MySQLite.queueQuery("DROP TABLE TempJobCommands;")
229 MySQLite.queueQuery("DROP TABLE darkrp_jobown;")
230 MySQLite.commit(onFinish)
231 end)
232
233 if not DarkRP.DBVersion or (DarkRP.DBVersion < 20160610 and DarkRP.DBVersion ~= 0) then
234 if not MySQLite.isMySQL() then
235 -- darkrp_player used to have a UNIQUE rpname field.
236 -- This sucks, get rid of it
237 MySQLite.query([[PRAGMA foreign_keys=OFF]])
238
239 MySQLite.query([[
240 CREATE TABLE IF NOT EXISTS new_darkrp_player(
241 uid BIGINT NOT NULL PRIMARY KEY,
242 rpname VARCHAR(45),
243 salary INTEGER NOT NULL DEFAULT 45,
244 wallet INTEGER NOT NULL
245 );
246 ]])
247
248 MySQLite.query([[INSERT INTO new_darkrp_player SELECT * FROM darkrp_player]])
249
250 MySQLite.query([[DROP TABLE darkrp_player]])
251
252 MySQLite.query([[ALTER TABLE new_darkrp_player RENAME TO darkrp_player]])
253
254 MySQLite.query([[PRAGMA foreign_keys=ON]])
255
256 onFinish()
257 else
258 -- if only SQLite were this easy
259 MySQLite.query([[DROP INDEX rpname ON darkrp_player]], onFinish)
260 end
261 else
262 onFinish()
263 end
264end
265
266--[[---------------------------------------------------------
267Players
268 ---------------------------------------------------------]]
269function DarkRP.storeRPName(ply, name)
270 if not name or string.len(name) < 2 then return end
271 hook.Call("onPlayerChangedName", nil, ply, ply:getDarkRPVar("rpname"), name)
272 ply:setDarkRPVar("rpname", name)
273
274 MySQLite.query([[UPDATE darkrp_player SET rpname = ]] .. MySQLite.SQLStr(name) .. [[ WHERE UID = ]] .. ply:SteamID64() .. ";")
275 MySQLite.query([[UPDATE darkrp_player SET rpname = ]] .. MySQLite.SQLStr(name) .. [[ WHERE UID = ]] .. ply:UniqueID() .. ";")
276end
277
278function DarkRP.retrieveRPNames(name, callback)
279 MySQLite.query("SELECT COUNT(*) AS count FROM darkrp_player WHERE rpname = " .. MySQLite.SQLStr(name) .. ";", function(r)
280 callback(tonumber(r[1].count) > 0)
281 end)
282end
283
284function DarkRP.offlinePlayerData(steamid, callback, failed)
285 local sid64 = util.SteamIDTo64(steamid)
286 local uniqueid = util.CRC("gm_" .. string.upper(steamid) .. "_gm")
287
288 MySQLite.query(string.format([[REPLACE INTO playerinformation VALUES(%s, %s);]], MySQLite.SQLStr(sid64), MySQLite.SQLStr(steamid)), nil, failed)
289
290 local query = [[
291 SELECT rpname, wallet, salary, "SID64" AS kind
292 FROM darkrp_player
293 where uid = %s
294 UNION
295 SELECT rpname, wallet, salary, "UniqueID" AS kind
296 FROM darkrp_player
297 where uid = %s
298 ;
299 ]]
300
301 MySQLite.query(
302 query:format(sid64, uniqueid),
303 function(data, ...)
304 -- The database has no record of the player data in SteamID64 form
305 -- Otherwise the first row would have kind SID64
306 if data and data[1] and data[1].kind == "UniqueID" then
307 -- The rpname must be unique
308 -- adding a new row with uid = SteamID64, but the same rpname will remove the uid=UniqueID row
309
310 local replquery = [[
311 REPLACE INTO darkrp_player(uid, rpname, wallet, salary)
312 VALUES (%s, %s, %s, %s)
313 ]]
314
315 MySQLite.begin()
316 MySQLite.queueQuery(
317 replquery:format(
318 sid64,
319 data[1].rpname == "NULL" and "NULL" or MySQLite.SQLStr(data[1].rpname),
320 data[1].wallet,
321 data[1].salary
322 ),
323 nil,
324 failed
325 )
326 MySQLite.commit()
327 end
328
329 return callback and callback(data, ...)
330 end
331 , failed
332 )
333end
334
335function DarkRP.retrievePlayerData(ply, callback, failed, attempts, err)
336 attempts = attempts or 0
337
338 if attempts > 3 then return failed(err) end
339
340 DarkRP.offlinePlayerData(ply:SteamID(), callback, function(sqlErr)
341 DarkRP.retrievePlayerData(ply, callback, failed, attempts + 1, sqlErr)
342 end)
343end
344
345function DarkRP.createPlayerData(ply, name, wallet, salary)
346 MySQLite.query([[REPLACE INTO darkrp_player VALUES(]] ..
347 ply:SteamID64() .. [[, ]] ..
348 MySQLite.SQLStr(name) .. [[, ]] ..
349 salary .. [[, ]] ..
350 wallet .. ");")
351
352 -- Backwards compatibility
353 MySQLite.query([[REPLACE INTO darkrp_player VALUES(]] ..
354 ply:UniqueID() .. [[, ]] ..
355 MySQLite.SQLStr(name) .. [[, ]] ..
356 salary .. [[, ]] ..
357 wallet .. ");")
358end
359
360function DarkRP.storeMoney(ply, amount)
361 if not IsValid(ply) then return end
362 if not isnumber(amount) or amount < 0 or amount >= 1 / 0 then return end
363
364 -- Also keep deprecated UniqueID data at least somewhat up to date
365 MySQLite.query([[UPDATE darkrp_player SET wallet = ]] .. amount .. [[ WHERE uid = ]] .. ply:UniqueID() .. [[ OR uid = ]] .. ply:SteamID64())
366end
367
368function DarkRP.storeOfflineMoney(sid64, amount)
369 if isnumber(sid64) or isstring(sid64) and string.len(sid64) < 17 then -- smaller than 76561197960265728 is not a SteamID64
370 DarkRP.errorNoHalt([[Some addon is giving DarkRP.storeOfflineMoney a UniqueID as its first argument, but this function now expects a SteamID64]], 2,
371 { "The function used to take UniqueIDs, but it does not anymore."
372 , "If you are a server owner, please look closely to the files mentioned in this error"
373 , "After all, these files will tell you WHICH addon is doing it"
374 , "This is NOT a DarkRP bug!"
375 , "Your server will continue working normally"
376 , "But whichever addon just tried to store an offline player's money"
377 , "Will NOT take effect!"
378 })
379 end
380
381 -- Also store on deprecated UniqueID
382 local uniqueid = util.CRC("gm_" .. string.upper(util.SteamIDFrom64(sid64)) .. "_gm")
383 MySQLite.query([[UPDATE darkrp_player SET wallet = ]] .. amount .. [[ WHERE uid = ]] .. uniqueid .. [[ OR uid = ]] .. sid64)
384end
385
386local function resetAllMoney(ply,cmd,args)
387 if ply:EntIndex() ~= 0 and not ply:IsSuperAdmin() then return end
388 MySQLite.query("UPDATE darkrp_player SET wallet = " .. GAMEMODE.Config.startingmoney .. " ;")
389 for _, v in ipairs(player.GetAll()) do
390 v:setDarkRPVar("money", GAMEMODE.Config.startingmoney)
391 end
392 if ply:IsPlayer() then
393 DarkRP.notifyAll(0, 4, DarkRP.getPhrase("reset_money", ply:Nick()))
394 else
395 DarkRP.notifyAll(0, 4, DarkRP.getPhrase("reset_money", "Console"))
396 end
397end
398concommand.Add("rp_resetallmoney", resetAllMoney)
399
400function DarkRP.storeSalary(ply, amount)
401 ply:setSelfDarkRPVar("salary", math.floor(amount))
402
403 return amount
404end
405
406function DarkRP.retrieveSalary(ply, callback)
407 if not IsValid(ply) then return 0 end
408
409 local val =
410 ply:getJobTable() and ply:getJobTable().salary or
411 RPExtraTeams[GAMEMODE.DefaultTeam].salary or
412 (GM or GAMEMODE).Config.normalsalary
413
414 if callback then callback(val) end
415
416 return val
417end
418
419--[[---------------------------------------------------------------------------
420Players
421---------------------------------------------------------------------------]]
422local meta = FindMetaTable("Player")
423function meta:restorePlayerData()
424 if not IsValid(self) then return end
425 self.DarkRPUnInitialized = true
426
427 DarkRP.retrievePlayerData(self, function(data)
428 if not IsValid(self) then return end
429
430 self.DarkRPUnInitialized = nil
431
432 local info = data and data[1] or {}
433 if not info.rpname or info.rpname == "NULL" then info.rpname = string.gsub(self:SteamName(), "\\\"", "\"") end
434
435 info.wallet = info.wallet or GAMEMODE.Config.startingmoney
436 info.salary = DarkRP.retrieveSalary(self)
437
438 self:setDarkRPVar("money", tonumber(info.wallet))
439 self:setSelfDarkRPVar("salary", tonumber(info.salary))
440
441 self:setDarkRPVar("rpname", info.rpname)
442
443 if not data then
444 info = hook.Call("onPlayerFirstJoined", nil, self, info) or info
445 DarkRP.createPlayerData(self, info.rpname, info.wallet, info.salary)
446 end
447 end, function(err) -- Retrieving data failed, go on without it
448 self.DarkRPUnInitialized = true -- no information should be saved from here, or the playerdata might be reset
449
450 self:setDarkRPVar("money", GAMEMODE.Config.startingmoney)
451 self:setSelfDarkRPVar("salary", DarkRP.retrieveSalary(self))
452 local name = string.gsub(self:SteamName(), "\\\"", "\"")
453 self:setDarkRPVar("rpname", name)
454
455 self.DarkRPDataRetrievalFailed = true -- marker on the player that says shit is fucked
456 DarkRP.error("Failed to retrieve player information from the database. ", nil, {"This means your database or the connection to your database is fucked.", "This is the error given by the database:\n\t\t" .. tostring(err)})
457 end)
458end
459
460--[[---------------------------------------------------------
461 Doors
462 ---------------------------------------------------------]]
463function DarkRP.storeDoorData(ent)
464 if not ent:CreatedByMap() then return end
465 local map = string.lower(game.GetMap())
466 local nonOwnable = ent:getKeysNonOwnable()
467 local title = ent:getKeysTitle()
468
469 MySQLite.query([[REPLACE INTO darkrp_door VALUES(]] .. ent:doorIndex() .. [[, ]] .. MySQLite.SQLStr(map) .. [[, ]] .. (title and MySQLite.SQLStr(title) or "NULL") .. [[, ]] .. "NULL" .. [[, ]] .. (nonOwnable and 1 or 0) .. [[);]])
470end
471
472function setUpNonOwnableDoors()
473 MySQLite.query("SELECT idx, title, isLocked, isDisabled FROM darkrp_door WHERE map = " .. MySQLite.SQLStr(string.lower(game.GetMap())) .. ";", function(r)
474 if not r then return end
475
476 for _, row in pairs(r) do
477 local e = DarkRP.doorIndexToEnt(tonumber(row.idx))
478
479 if not IsValid(e) then continue end
480 if e:isKeysOwnable() then
481 if tobool(row.isDisabled) then
482 e:setKeysNonOwnable(tobool(row.isDisabled))
483 end
484 if row.isLocked and row.isLocked ~= "NULL" then
485 e:Fire((tobool(row.isLocked) and "" or "un") .. "lock", "", 0)
486 end
487 e:setKeysTitle(row.title ~= "NULL" and row.title or nil)
488 end
489 end
490 end)
491end
492
493local keyValueActions = {
494 ["DarkRPNonOwnable"] = function(ent, val) ent:setKeysNonOwnable(tobool(val)) end,
495 ["DarkRPTitle"] = function(ent, val) ent:setKeysTitle(val) end,
496 ["DarkRPDoorGroup"] = function(ent, val) if RPExtraTeamDoors[val] then ent:setDoorGroup(val) end end,
497 ["DarkRPCanLockpick"] = function(ent, val) ent.DarkRPCanLockpick = tobool(val) end
498}
499
500local function onKeyValue(ent, key, value)
501 if not ent:isDoor() then return end
502
503 if keyValueActions[key] then
504 keyValueActions[key](ent, value)
505 end
506end
507hook.Add("EntityKeyValue", "darkrp_doors", onKeyValue)
508
509function DarkRP.storeTeamDoorOwnability(ent)
510 if not ent:CreatedByMap() then return end
511 local map = string.lower(game.GetMap())
512
513 MySQLite.query("DELETE FROM darkrp_doorjobs WHERE idx = " .. ent:doorIndex() .. " AND map = " .. MySQLite.SQLStr(map) .. ";")
514 for k in pairs(ent:getKeysDoorTeams() or {}) do
515 MySQLite.query("INSERT INTO darkrp_doorjobs VALUES(" .. ent:doorIndex() .. ", " .. MySQLite.SQLStr(map) .. ", " .. MySQLite.SQLStr(RPExtraTeams[k].command) .. ");")
516 end
517end
518
519function setUpTeamOwnableDoors()
520 MySQLite.query("SELECT idx, job FROM darkrp_doorjobs WHERE map = " .. MySQLite.SQLStr(string.lower(game.GetMap())) .. ";", function(r)
521 if not r then return end
522 local map = string.lower(game.GetMap())
523
524 for _, row in pairs(r) do
525 row.idx = tonumber(row.idx)
526
527 local e = DarkRP.doorIndexToEnt(row.idx)
528 if not IsValid(e) then continue end
529
530 local _, job = DarkRP.getJobByCommand(row.job)
531
532 if job then
533 e:addKeysDoorTeam(job)
534 else
535 print(("can't find job %s for door %d, removing from database"):format(row.job, row.idx))
536 MySQLite.query(("DELETE FROM darkrp_doorjobs WHERE idx = %d AND map = %s AND job = %s;"):format(row.idx, MySQLite.SQLStr(map), MySQLite.SQLStr(row.job)))
537 end
538 end
539 end)
540end
541
542function DarkRP.storeDoorGroup(ent, group)
543 if not ent:CreatedByMap() then return end
544 local map = MySQLite.SQLStr(string.lower(game.GetMap()))
545 local index = ent:doorIndex()
546
547 if group == "" or not group then
548 MySQLite.query("DELETE FROM darkrp_doorgroups WHERE map = " .. map .. " AND idx = " .. index .. ";")
549 return
550 end
551
552 MySQLite.query("REPLACE INTO darkrp_doorgroups VALUES(" .. index .. ", " .. map .. ", " .. MySQLite.SQLStr(group) .. ");");
553end
554
555function setUpGroupDoors()
556 local map = MySQLite.SQLStr(string.lower(game.GetMap()))
557 MySQLite.query("SELECT idx, doorgroup FROM darkrp_doorgroups WHERE map = " .. map, function(data)
558 if not data then return end
559
560 for _, row in pairs(data) do
561 local ent = DarkRP.doorIndexToEnt(tonumber(row.idx))
562
563 if not IsValid(ent) or not ent:isKeysOwnable() then
564 continue
565 end
566
567 if not RPExtraTeamDoorIDs[row.doorgroup] then continue end
568 ent:setDoorGroup(row.doorgroup)
569 end
570 end)
571end
572
573hook.Add("PostCleanupMap", "DarkRP.hooks", function()
574 setUpNonOwnableDoors()
575 setUpTeamOwnableDoors()
576 setUpGroupDoors()
577end)