This example uses async operations with callbacks to prevent server lag. This is the recommended approach for production servers.
require("mongo")
local client = MongoDB.Client("mongodb://localhost:27017")
local db = client:Database("gameserver")
local playersDB = db:Collection("players")
-- Create indexes (sync is fine for initialization)
playersDB:CreateIndex({ steamid = 1 }, true, "steamid_unique")
playersDB:CreateIndex({ last_seen = -1 }, false, "last_seen_desc")
hook.Add("PlayerInitialSpawn", "LoadPlayer", function(ply)
local steamid = ply:SteamID()
local nickname = ply:Nick()
-- Use async to prevent server lag on player join
playersDB:FindOneAsync({ steamid = steamid }, function(err, data)
if err then
ErrorNoHalt("Failed to load player " .. nickname .. ": " .. err .. "\n")
ply:Kick("Database error, please rejoin")
return
end
if not data then
-- New player - create default data
playersDB:InsertOneAsync({
steamid = steamid,
username = nickname,
first_join = os.time(),
last_seen = os.time(),
playtime = 0,
level = 1,
credits = 1000,
inventory = {},
stats = { kills = 0, deaths = 0 }
}, function(err, id)
if err then
ErrorNoHalt("Failed to create player " .. nickname .. ": " .. err .. "\n")
else
print("✓ Created new player: " .. nickname)
ply:SetNWInt("Credits", 1000)
ply:SetNWInt("Level", 1)
end
end)
else
-- Existing player - load their data
ply:SetNWInt("Credits", data.credits or 0)
ply:SetNWInt("Level", data.level or 1)
ply:SetNWInt("Playtime", data.playtime or 0)
print("✓ Loaded player: " .. nickname)
-- Update last seen
playersDB:UpdateOneAsync(
{ steamid = steamid },
{
["$set"] = {
last_seen = os.time(),
username = nickname
}
},
false,
function(err, count)
if err then
ErrorNoHalt("Failed to update last_seen for " .. nickname .. ": " .. err .. "\n")
end
end
)
end
end)
end)
hook.Add("PlayerDisconnected", "SavePlayer", function(ply)
local steamid = ply:SteamID()
local nickname = ply:Nick()
-- Save player data asynchronously
playersDB:UpdateOneAsync(
{ steamid = steamid },
{
["$set"] = {
username = nickname,
last_seen = os.time(),
credits = ply:GetNWInt("Credits", 0),
level = ply:GetNWInt("Level", 1)
},
["$inc"] = {
playtime = ply:GetNWInt("SessionPlaytime", 0)
}
},
true, -- upsert
function(err, count)
if err then
ErrorNoHalt("Failed to save player " .. nickname .. ": " .. err .. "\n")
else
print("✓ Saved player data for " .. nickname)
end
end
)
end)
-- Track session playtime
hook.Add("PlayerInitialSpawn", "TrackPlaytime", function(ply)
ply:SetNWInt("SessionPlaytime", 0)
end)
hook.Add("Think", "IncrementPlaytime", function()
for _, ply in ipairs(player.GetAll()) do
if IsValid(ply) and ply:Alive() then
local current = ply:GetNWInt("SessionPlaytime", 0)
ply:SetNWInt("SessionPlaytime", current + 1)
end
end
end)
-- Auto-save every 5 minutes
timer.Create("AutoSavePlaytime", 300, 0, function()
for _, ply in ipairs(player.GetAll()) do
if not IsValid(ply) then continue end
local sessionTime = ply:GetNWInt("SessionPlaytime", 0)
if sessionTime > 0 then
playersDB:UpdateOneAsync(
{ steamid = ply:SteamID() },
{ ["$inc"] = { playtime = sessionTime } },
false,
function(err, count)
if not err then
ply:SetNWInt("SessionPlaytime", 0)
end
end
)
end
end
end)
function AddCredits(ply, amount)
if not IsValid(ply) or amount <= 0 then return end
local steamid = ply:SteamID()
playersDB:UpdateOneAsync(
{ steamid = steamid },
{ ["$inc"] = { credits = amount } },
false,
function(err, count)
if err then
ErrorNoHalt("Failed to add credits: " .. err .. "\n")
ply:ChatPrint("Error adding credits!")
elseif count > 0 then
local newCredits = ply:GetNWInt("Credits", 0) + amount
ply:SetNWInt("Credits", newCredits)
ply:ChatPrint("You received " .. amount .. " credits!")
end
end
)
end
concommand.Add("stats", function(ply)
if not IsValid(ply) then return end
playersDB:FindOneAsync({ steamid = ply:SteamID() }, function(err, data)
if err then
ply:ChatPrint("Error loading stats!")
return
end
if data then
ply:ChatPrint("=== Your Stats ===")
ply:ChatPrint("Level: " .. (data.level or 1))
ply:ChatPrint("Credits: " .. (data.credits or 0))
ply:ChatPrint("Playtime: " .. math.floor((data.playtime or 0) / 60) .. " hours")
ply:ChatPrint("K/D: " .. (data.stats.kills or 0) .. "/" .. (data.stats.deaths or 0))
else
ply:ChatPrint("No stats found!")
end
end)
end)
function GiveExperience(ply, xp)
if not IsValid(ply) or xp <= 0 then return end
playersDB:FindOneAsync({ steamid = ply:SteamID() }, function(err, data)
if err or not data then return end
local currentXP = data.experience or 0
local currentLevel = data.level or 1
local newXP = currentXP + xp
local xpNeeded = currentLevel * 100
if newXP >= xpNeeded then
-- Level up!
local newLevel = currentLevel + 1
playersDB:UpdateOneAsync(
{ steamid = ply:SteamID() },
{
["$set"] = {
level = newLevel,
experience = newXP - xpNeeded
},
["$inc"] = { credits = 500 }
},
false,
function(err, count)
if not err then
ply:SetNWInt("Level", newLevel)
ply:SetNWInt("Credits", ply:GetNWInt("Credits") + 500)
ply:ChatPrint("LEVEL UP! You are now level " .. newLevel)
ply:ChatPrint("Bonus: +500 credits")
end
end
)
else
-- Just add XP
playersDB:UpdateOneAsync(
{ steamid = ply:SteamID() },
{ ["$set"] = { experience = newXP } },
false
)
end
end)
end