254 lines
9.2 KiB
Lua
254 lines
9.2 KiB
Lua
local function cleanName(value)
|
|
if type(value) ~= 'string' then return nil end
|
|
value = value:gsub('^%s+', ''):gsub('%s+$', '')
|
|
if #value < 2 or #value > 32 then return nil end
|
|
if not value:match("^[%aÀ-ÿ'%- ]+$") then return nil end
|
|
return value
|
|
end
|
|
|
|
local function validDate(value)
|
|
if type(value) ~= 'string' then return false end
|
|
local year, month, day = value:match('^(%d%d%d%d)%-(%d%d)%-(%d%d)$')
|
|
year, month, day = tonumber(year), tonumber(month), tonumber(day)
|
|
if not year or year < 1900 or year > 2010 then return false end
|
|
if not month or month < 1 or month > 12 then return false end
|
|
if not day or day < 1 or day > 31 then return false end
|
|
|
|
local daysInMonth = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
|
|
local leapYear = year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0)
|
|
if leapYear then daysInMonth[2] = 29 end
|
|
return day <= daysInMonth[month]
|
|
end
|
|
|
|
local function normalizeDate(value)
|
|
if type(value) == 'number' then
|
|
local seconds = value > 100000000000 and math.floor(value / 1000) or math.floor(value)
|
|
return os.date('!%Y-%m-%d', seconds)
|
|
end
|
|
|
|
local text = tostring(value or '')
|
|
local numeric = tonumber(text)
|
|
if numeric then
|
|
local seconds = numeric > 100000000000 and math.floor(numeric / 1000) or math.floor(numeric)
|
|
return os.date('!%Y-%m-%d', seconds)
|
|
end
|
|
|
|
return text:match('^(%d%d%d%d%-%d%d%-%d%d)') or text
|
|
end
|
|
|
|
local function accountNumberFor(characterId)
|
|
return ('LS%010d'):format(tonumber(characterId))
|
|
end
|
|
|
|
local function phoneNumberFor(characterId)
|
|
return ('%s-%04d'):format(DevXConfig.PhonePrefix or '555', tonumber(characterId))
|
|
end
|
|
|
|
local function decodeMetadata(value)
|
|
if type(value) ~= 'string' or value == '' then return {} end
|
|
local ok, result = pcall(json.decode, value)
|
|
return ok and type(result) == 'table' and result or {}
|
|
end
|
|
|
|
local function listCharacters(license)
|
|
local rows = MySQL.query.await([[
|
|
SELECT id, slot, first_name AS firstName, last_name AS lastName,
|
|
date_of_birth AS dateOfBirth, sex, model, phone_number AS phoneNumber, metadata
|
|
FROM devx_characters
|
|
WHERE owner_license = ? AND deleted_at IS NULL
|
|
ORDER BY slot ASC
|
|
]], { license }) or {}
|
|
|
|
for _, character in ipairs(rows) do
|
|
local metadata = decodeMetadata(character.metadata)
|
|
character.id = tonumber(character.id)
|
|
character.slot = tonumber(character.slot)
|
|
character.introCompleted = metadata.introCompleted == true
|
|
character.metadata = nil
|
|
character.dateOfBirth = normalizeDate(character.dateOfBirth)
|
|
if not character.phoneNumber or character.phoneNumber == '' then
|
|
character.phoneNumber = phoneNumberFor(character.id)
|
|
MySQL.update.await(
|
|
'UPDATE devx_characters SET phone_number = ? WHERE id = ?',
|
|
{ character.phoneNumber, character.id }
|
|
)
|
|
end
|
|
end
|
|
|
|
return rows
|
|
end
|
|
|
|
DevXRegisterCallback('characters:list', function(source)
|
|
local license = exports.devx_core:GetLicense(source)
|
|
if not license then return { ok = false, error = 'Keine Lizenzkennung.' } end
|
|
|
|
return {
|
|
ok = true,
|
|
characters = listCharacters(license),
|
|
maxCharacters = DevXConfig.MaxCharacters
|
|
}
|
|
end)
|
|
|
|
DevXRegisterCallback('characters:create', function(source, payload)
|
|
local license = exports.devx_core:GetLicense(source)
|
|
if not license then return { ok = false, error = 'Keine Lizenzkennung.' } end
|
|
|
|
local firstName = cleanName(payload.firstName)
|
|
local lastName = cleanName(payload.lastName)
|
|
local sex = payload.sex
|
|
local dateOfBirth = payload.dateOfBirth
|
|
|
|
if not firstName or not lastName then
|
|
return { ok = false, error = 'Vor- und Nachname sind ungültig.' }
|
|
end
|
|
if sex ~= 'm' and sex ~= 'f' and sex ~= 'x' then
|
|
return { ok = false, error = 'Ungültige Geschlechtsangabe.' }
|
|
end
|
|
if not validDate(dateOfBirth) then
|
|
return { ok = false, error = 'Geburtsdatum ist ungültig.' }
|
|
end
|
|
|
|
local existing = listCharacters(license)
|
|
if #existing >= DevXConfig.MaxCharacters then
|
|
return { ok = false, error = 'Alle Charakterplätze sind belegt.' }
|
|
end
|
|
|
|
local used = {}
|
|
for _, character in ipairs(existing) do used[tonumber(character.slot)] = true end
|
|
local slot = 1
|
|
while used[slot] do slot = slot + 1 end
|
|
|
|
local model = sex == 'f' and 'mp_f_freemode_01' or 'mp_m_freemode_01'
|
|
local metadata = json.encode({ introCompleted = false, rpXp = 0 })
|
|
|
|
local characterId = tonumber(MySQL.insert.await([[
|
|
INSERT INTO devx_characters
|
|
(owner_license, slot, first_name, last_name, date_of_birth, sex, model, cash, metadata)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
]], {
|
|
license, slot, firstName, lastName, dateOfBirth, sex, model,
|
|
DevXConfig.StartCash, metadata
|
|
}))
|
|
|
|
if not characterId or characterId < 1 then
|
|
return { ok = false, error = 'Charakter konnte nicht in der Datenbank erstellt werden.' }
|
|
end
|
|
|
|
local phoneNumber = phoneNumberFor(characterId)
|
|
MySQL.update.await('UPDATE devx_characters SET phone_number = ? WHERE id = ?', {
|
|
phoneNumber, characterId
|
|
})
|
|
|
|
local accountNumber = accountNumberFor(characterId)
|
|
local accountId = tonumber(MySQL.insert.await([[
|
|
INSERT INTO devx_bank_accounts
|
|
(character_id, account_number, account_name, account_type, balance)
|
|
VALUES (?, ?, ?, 'personal', ?)
|
|
]], {
|
|
characterId, accountNumber, ('%s %s'):format(firstName, lastName), DevXConfig.StartBank
|
|
}))
|
|
|
|
if not accountId or accountId < 1 then
|
|
MySQL.update.await('DELETE FROM devx_characters WHERE id = ?', { characterId })
|
|
return { ok = false, error = 'Bankkonto konnte nicht erstellt werden.' }
|
|
end
|
|
|
|
MySQL.insert.await([[
|
|
INSERT INTO devx_bank_transactions
|
|
(account_id, direction, amount, balance_after, counterparty, reference)
|
|
VALUES (?, 'credit', ?, ?, 'Stadtverwaltung', 'Startguthaben')
|
|
]], { accountId, DevXConfig.StartBank, DevXConfig.StartBank })
|
|
|
|
return {
|
|
ok = true,
|
|
characterId = characterId,
|
|
phoneNumber = phoneNumber,
|
|
characters = listCharacters(license)
|
|
}
|
|
end)
|
|
|
|
DevXRegisterCallback('characters:select', function(source, payload)
|
|
local characterId = tonumber(payload.characterId)
|
|
if not characterId then return { ok = false, error = 'Ungültiger Charakter.' } end
|
|
|
|
local player, errorMessage = exports.devx_core:ActivateCharacter(source, characterId)
|
|
if not player then return { ok = false, error = errorMessage } end
|
|
|
|
return { ok = true, player = player }
|
|
end)
|
|
|
|
DevXRegisterCallback('characters:completeIntro', function(source)
|
|
local player = exports.devx_core:GetPlayer(source)
|
|
if not player then return { ok = false, error = 'Kein Charakter aktiv.' } end
|
|
|
|
local saved = exports.devx_core:SetMetadataValue(source, 'introCompleted', true)
|
|
if not saved then
|
|
return { ok = false, error = 'Introstatus konnte nicht gespeichert werden.' }
|
|
end
|
|
|
|
local finalSpawn = DevXCharacterConfig.Intro.finalSpawn
|
|
player.position = {
|
|
x = finalSpawn.x,
|
|
y = finalSpawn.y,
|
|
z = finalSpawn.z,
|
|
heading = finalSpawn.heading
|
|
}
|
|
|
|
MySQL.update.await(
|
|
'UPDATE devx_characters SET position = ? WHERE id = ?',
|
|
{ json.encode(player.position), player.characterId }
|
|
)
|
|
|
|
return { ok = true }
|
|
end)
|
|
|
|
DevXRegisterCallback('characters:delete', function(source, payload)
|
|
local license = exports.devx_core:GetLicense(source)
|
|
local characterId = tonumber(payload.characterId)
|
|
if not license or not characterId then
|
|
return { ok = false, error = 'Ungültige Anfrage.' }
|
|
end
|
|
|
|
local character = MySQL.single.await([[
|
|
SELECT id, first_name, last_name
|
|
FROM devx_characters
|
|
WHERE id = ? AND owner_license = ? AND deleted_at IS NULL
|
|
LIMIT 1
|
|
]], { characterId, license })
|
|
if not character then return { ok = false, error = 'Charakter wurde nicht gefunden.' } end
|
|
|
|
local ownedProperties = tonumber(MySQL.scalar.await([[
|
|
SELECT COUNT(*) FROM devx_property_owners WHERE character_id = ?
|
|
]], { characterId })) or 0
|
|
|
|
if ownedProperties > 0 then
|
|
return { ok = false, error = 'Verkaufe zuerst alle Wohnungen dieses Charakters.' }
|
|
end
|
|
|
|
local changed = MySQL.update.await([[
|
|
DELETE FROM devx_characters
|
|
WHERE id = ? AND owner_license = ?
|
|
]], { characterId, license })
|
|
|
|
if not changed or changed < 1 then
|
|
return { ok = false, error = 'Charakter konnte nicht gelöscht werden.' }
|
|
end
|
|
|
|
print(('[devx_characters] %s %s (ID %s) wurde von Lizenz %s gelöscht.'):format(
|
|
character.first_name, character.last_name, characterId, license
|
|
))
|
|
|
|
return { ok = true, characters = listCharacters(license) }
|
|
end)
|
|
|
|
RegisterCommand('resetintro', function(source, args)
|
|
if source ~= 0 and not IsPlayerAceAllowed(source, 'devx.admin') then return end
|
|
|
|
local target = tonumber(args[1]) or source
|
|
if not target or target == 0 then return end
|
|
|
|
local player = exports.devx_core:GetPlayer(target)
|
|
if not player then return end
|
|
exports.devx_core:SetMetadataValue(target, 'introCompleted', false)
|
|
end, false)
|