first commit
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user