No description
- JavaScript 70.4%
- Lua 15.9%
- MDX 4.8%
- Vue 4.2%
- TypeScript 1.7%
- Other 2.9%
|
Some checks failed
Deploy to Production / deploy (push) Has been cancelled
Builds NUI frontends, rsync's resources and configs to production, then hot-reloads via RCON without full server restart. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| .vscode | ||
| concept_docs | ||
| concepts | ||
| scripts | ||
| server | ||
| .gitattributes | ||
| .gitignore | ||
| COMMANDS.md | ||
| CONCEPT_INVENTORY.md | ||
| CONCEPT_VIITALS.md | ||
| docker-compose.yaml | ||
| inv_todo.md | ||
| INVENTORY_GUIDE.md | ||
| oldclothes.json | ||
| olditems.json | ||
| README.md | ||
| VISION.md | ||
| zones.md | ||
Project Decay
Setup
- Run the
scripts/Install-Server.ps1script. It downloads the latest server files into theserver-datadirectory. - Run the server using the
scripts/Run-Server.ps1script. - In the txAdmin setup wizard, select "Existing server" and paste in the absolute path of the
serverdirectory.
decay-db — Datenbank-Handler
Alle Datenbank-Operationen laufen ueber decay-db. Das betrifft Migrations, Seeders, den Query Builder, Models und Schema-Introspection. ox_core's eigene SQL-Ausfuehrung ist deaktiviert — die Tabellen werden ausschliesslich durch decay-db verwaltet.
Boot-Reihenfolge (server.cfg)
ensure ox_lib
ensure oxmysql
ensure decay-db # VOR ox_core — blockiert abhaengige Resources bis Migrations fertig
ensure ox_core
ensure ox_target
ensure [decay]
decay-db blockiert ox_core und ox_target automatisch per onResourceStarting, fuehrt alle Migrations und Seeders aus und startet die blockierten Resources danach.
Eigene Resource anbinden
fxmanifest.lua:
fx_version 'bodacious'
game 'gta5'
lua54 'yes'
author 'ProjectDecay'
description 'Meine Resource'
version '1.0.0'
shared_script '@ox_lib/init.lua'
server_script {
'@oxmysql/lib/MySQL.lua',
'@decay-db/lib/init.lua', -- decay-db Library
'migrations/*.lua', -- Migrations automatisch laden
'seeders/*.lua', -- Seeders automatisch laden
'Server/s_main.lua',
}
Server/s_main.lua:
CreateThread(function()
db.awaitReady() -- Wartet bis decay-db fertig ist
db.runMigrations() -- Fuehrt ausstehende Migrations aus
db.runSeeders() -- Fuehrt ausstehende Seeders aus
print('[meine-resource] Bereit')
end)
Migration erstellen
Datei: migrations/001_create_meine_tabelle.lua
db.defineMigration('001', 'create_meine_tabelle', {
up = function(schema)
schema.create('meine_tabelle', function(t)
t:increments('id')
t:unsignedInt('char_id'):notNull()
t:string('name', 50):notNull()
t:json('data'):default('{}')
t:timestamps()
t:index('char_id')
t:foreign('char_id'):references('characters', 'charId')
:onDelete('CASCADE'):onUpdate('CASCADE')
end)
end,
down = function(schema)
schema.dropIfExists('meine_tabelle')
end
})
Column-Typen
| Methode | SQL |
|---|---|
t:increments(name) |
INT UNSIGNED AUTO_INCREMENT PRIMARY KEY |
t:bigIncrements(name) |
BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY |
t:int(name) |
INT |
t:unsignedInt(name) |
INT UNSIGNED |
t:tinyInt(name) |
TINYINT |
t:unsignedTinyInt(name) |
TINYINT UNSIGNED |
t:smallInt(name) |
SMALLINT |
t:bigInt(name) |
BIGINT |
t:float(name) |
FLOAT |
t:double(name) |
DOUBLE |
t:decimal(name, p, s) |
DECIMAL(p,s) |
t:string(name, len) |
VARCHAR(len) |
t:char(name, len) |
CHAR(len) |
t:text(name) |
TEXT |
t:mediumText(name) |
MEDIUMTEXT |
t:longText(name) |
LONGTEXT |
t:boolean(name) |
TINYINT(1) |
t:json(name) |
JSON |
t:timestamp(name) |
TIMESTAMP |
t:date(name) |
DATE |
t:dateTime(name) |
DATETIME |
t:enum(name, values) |
ENUM(...) |
t:blob(name) |
BLOB |
t:timestamps() |
created_at + updated_at |
t:softDeletes() |
deleted_at TIMESTAMP NULL |
Column Modifiers
:notNull() -- NOT NULL
:nullable() -- NULL (default)
:default(value) -- DEFAULT value
:defaultCurrent() -- DEFAULT CURRENT_TIMESTAMP
:defaultCurrentOnUpdate() -- DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
:unique() -- UNIQUE
:after('spalte') -- AFTER spalte (fuer ALTER)
:check('expr') -- CHECK (expr)
:comment('text') -- COMMENT 'text'
Index & Foreign Key
t:index('col1', 'col2') -- INDEX
t:uniqueIndex('col1') -- UNIQUE INDEX
t:fullTextIndex('col1') -- FULLTEXT INDEX
t:compositeKey('col1', 'col2') -- Zusammengesetzter Primary Key
t:foreign('col'):references('tabelle', 'spalte')
:onDelete('CASCADE'):onUpdate('CASCADE')
ALTER TABLE
db.defineMigration('002', 'add_rarity', {
up = function(schema)
schema.alter('player_items', function(t)
t:addColumn('rarity', 'VARCHAR(20)'):default('common'):after('item_name')
t:addIndex('rarity')
end)
end,
down = function(schema)
schema.alter('player_items', function(t)
t:dropIndex('idx_player_items_rarity')
t:dropColumn('rarity')
end)
end
})
Raw SQL (fuer MariaDB-spezifisches)
db.defineMigration('003', 'special_feature', {
up = function(schema)
schema.raw([[
CREATE TABLE IF NOT EXISTS `meine_tabelle` (
-- MariaDB-spezifisches SQL hier
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
]])
end,
down = function(schema)
schema.dropIfExists('meine_tabelle')
end
})
Query Builder
SELECT
-- Alle Zeilen
db.table('player_items'):where('char_id', charId):get()
-- Einzelne Zeile
db.table('player_items'):where('id', itemId):first()
-- Einzelner Wert
db.table('player_items'):where('id', itemId):value('durability')
-- Mit Spalten
db.table('player_items'):select('id', 'item_name', 'amount'):get()
-- Distinct
db.table('player_items'):select('item_name'):distinct():get()
WHERE
:where('col', value) -- col = value
:where('col', '>', value) -- col > value
:orWhere('col', value) -- OR col = value
:whereIn('col', { 'a', 'b', 'c' }) -- col IN (...)
:whereNotIn('col', { 'a', 'b' }) -- col NOT IN (...)
:whereNull('col') -- col IS NULL
:whereNotNull('col') -- col IS NOT NULL
:whereBetween('col', 1, 10) -- col BETWEEN 1 AND 10
:whereRaw('JSON_EXTRACT(data, "$.x") > ?', {5}) -- Raw SQL
-- Gruppiert (Klammern)
:where(function(q)
q:where('status', 'active')
q:orWhere('status', 'pending')
end)
-- => (status = 'active' OR status = 'pending')
INSERT
-- Einzeln (gibt Insert-ID zurueck)
local id = db.table('player_items'):insert({
char_id = charId,
item_name = 'bandage',
amount = 3,
})
-- Mehrere
db.table('player_items'):insertMany({
{ char_id = charId, item_name = 'bandage', amount = 1 },
{ char_id = charId, item_name = 'water', amount = 2 },
})
-- Upsert (INSERT ... ON DUPLICATE KEY UPDATE)
db.table('stats'):upsert(
{ char_id = charId, stat_name = 'kills', value = 1 },
{ 'char_id', 'stat_name' },
{ value = 'value + 1' }
)
UPDATE / DELETE
-- Update (gibt affected rows zurueck)
db.table('player_items'):where('id', itemId):update({ durability = 50 })
-- Increment / Decrement
db.table('player_items'):where('id', itemId):increment('amount', 5)
db.table('player_items'):where('id', itemId):decrement('durability', 10)
-- Delete (WHERE ist Pflicht!)
db.table('player_items'):where('id', itemId):delete()
-- Truncate
db.table('player_items'):truncate()
JOINS
db.table('player_items AS pi')
:join('crafting_recipes AS cr', 'cr.name', '=', 'pi.item_name')
:leftJoin('loot_tables AS lt', 'lt.item', '=', 'pi.item_name')
:select('pi.*', 'cr.category')
:get()
Aggregationen
db.table('player_items'):where('char_id', charId):count()
db.table('player_items'):where('char_id', charId):sum('amount')
db.table('player_items'):where('char_id', charId):avg('durability')
db.table('player_items'):where('char_id', charId):max('durability')
db.table('player_items'):where('char_id', charId):min('durability')
db.table('player_items'):where('char_id', charId):exists()
Pagination
local result = db.table('player_items')
:where('char_id', charId)
:orderBy('created_at', 'DESC')
:paginate(1, 20)
-- result.data = { ... }
-- result.total = 150
-- result.page = 1
-- result.perPage = 20
-- result.lastPage = 8
Transactions
db.transaction(function(tx)
tx:table('accounts'):where('id', senderId):decrement('balance', amount)
tx:table('accounts'):where('id', receiverId):increment('balance', amount)
tx:table('accounts_transactions'):insert({
actorId = charId,
fromId = senderId,
toId = receiverId,
amount = amount,
message = 'Transfer',
})
end)
Async (Callback-Variante)
db.table('player_items'):where('char_id', charId):getAsync(function(items) ... end)
db.table('player_items'):where('id', itemId):firstAsync(function(item) ... end)
db.table('player_items'):where('char_id', charId):countAsync(function(count) ... end)
db.table('player_items'):insertAsync({ ... }, function(id) ... end)
db.table('player_items'):where('id', id):updateAsync({ ... }, function(affected) ... end)
db.table('player_items'):where('id', id):deleteAsync(function(affected) ... end)
Raw Queries
db.raw('SELECT * FROM player_items WHERE MATCH(item_name) AGAINST(? IN BOOLEAN MODE)', { term })
db.rawAsync('SELECT COUNT(*) FROM player_items', {}, function(result) ... end)
Debug
local sql, params = db.table('player_items'):where('char_id', 1):toSQL()
print(sql) -- SELECT * FROM `player_items` WHERE `char_id` = ?
print(params) -- { 1 }
Model / Repository
local PlayerItem = db.defineModel('player_items', {
primaryKey = 'id',
timestamps = true,
softDeletes = false,
fillable = { 'char_id', 'item_name', 'amount', 'durability', 'metadata' },
hidden = {},
defaults = { amount = 1, durability = 100.0 },
casts = {
metadata = 'json',
durability = 'float',
amount = 'integer',
},
validate = {
char_id = { type = 'number', required = true, min = 1 },
item_name = { type = 'string', required = true, maxLen = 50 },
amount = { type = 'number', min = 1, max = 9999 },
},
scopes = {
forCharacter = function(q, charId) return q:where('char_id', charId) end,
usable = function(q) return q:where('durability', '>', 0) end,
},
})
Model-Nutzung
-- Find
local item = PlayerItem:find(42)
local item = PlayerItem:findOrFail(42)
-- Create (mit Validierung)
local item, err = PlayerItem:create({ char_id = 1, item_name = 'bandage', amount = 3 })
if err then print('Fehler: ' .. err) end
-- Update via Instance
item.durability = 50
item:save()
-- Delete
item:destroy()
-- Alle abrufen
local all = PlayerItem:all()
-- Scopes
local items = PlayerItem:scope('forCharacter', charId):scope('usable'):get()
-- Direkter Query Builder
PlayerItem:query():where('rarity', 'military'):count()
-- Hooks
PlayerItem:hook('beforeCreate', function(data) return data end)
PlayerItem:hook('afterCreate', function(data, id) end)
PlayerItem:hook('beforeUpdate', function(data, pk) return data end)
PlayerItem:hook('afterUpdate', function(data, pk) end)
PlayerItem:hook('beforeDelete', function(instance) end)
PlayerItem:hook('afterDelete', function(instance) end)
-- JSON Export
print(item:toJSON())
Seeder
-- seeders/seed_items.lua
db.defineSeeder('basis_items', {
table = 'items',
runWhen = 'empty', -- 'empty' | 'once' | 'always'
data = {
{ name = 'Bandage', weight = 0.2 },
{ name = 'Wasser', weight = 0.5 },
}
})
-- Mit Custom Funktion
db.defineSeeder('komplexe_daten', {
runWhen = 'once',
fn = function()
db.table('config'):insert({ key = 'version', value = '1.0' })
end
})
Schema Introspection
-- Tabellen auflisten
local tables = db.listTables()
-- Tabelle existiert?
db.tableExists('player_items')
db.columnExists('player_items', 'durability')
-- Tabelle beschreiben
local columns = db.describeTable('player_items')
-- Vollstaendige Info (Columns, Indexes, CREATE Statement)
local info = db.introspect('player_items')
-- Foreign Keys
local fks = db.getForeignKeys('player_items')
-- Tabellengroesse
local size = db.getTableSize('player_items')
-- size.rows, size.data_kb, size.index_kb, size.total_kb
Console Commands
| Command | Beschreibung |
|---|---|
decay-db:status [resource] |
Migration-Status anzeigen |
decay-db:migrate <resource> |
Ausstehende Migrations ausfuehren |
decay-db:rollback <resource> [steps] |
Migrations zurueckrollen |
decay-db:fresh <resource> |
Alle Migrations zurueck + neu |
decay-db:seed <resource> |
Seeders ausfuehren |
decay-db:tables |
Alle Tabellen mit Groesse |
decay-db:describe <tabelle> |
Tabelle detailliert anzeigen |
decay-db:generate <tabelle> |
Migration aus bestehender Tabelle generieren |
decay-db:dump <tabelle> |
CREATE TABLE Statement ausgeben |
Hooks & Debug
-- Slow Query Warnung (Default: 150ms)
db.onSlowQuery(100, function(sql, params, duration)
print(('[SLOW %dms] %s'):format(duration, sql))
end)
-- Globale Query-Hooks
db.hook('beforeQuery', function(sql, params) end)
db.hook('afterQuery', function(sql, params, duration, result) end)
-- Debug-Modus (alle Queries loggen)
-- In server.cfg: set decay_db_debug "true"
Praxisbeispiel: Komplette Resource
-- ═══════════════════════════════════════════════
-- Server/s_main.lua
-- ═══════════════════════════════════════════════
-- Model definieren
local PlayerItem = db.defineModel('player_items', {
fillable = { 'char_id', 'item_name', 'amount', 'durability', 'metadata' },
defaults = { amount = 1, durability = 100.0 },
casts = { metadata = 'json', durability = 'float', amount = 'integer' },
validate = {
char_id = { type = 'number', required = true, min = 1 },
item_name = { type = 'string', required = true, maxLen = 50 },
amount = { type = 'number', min = 1, max = 9999 },
},
scopes = {
forCharacter = function(q, charId) return q:where('char_id', charId) end,
usable = function(q) return q:where('durability', '>', 0) end,
},
})
-- Startup
CreateThread(function()
db.awaitReady()
db.runMigrations()
db.runSeeders()
print('[meine-resource] Bereit')
end)
-- Callback: Items abrufen
lib.callback.register('inventory:getItems', function(source)
local player = exports.ox_core:GetPlayer(source)
if not player then return {} end
return PlayerItem:scope('forCharacter', player.charId)
:scope('usable')
:orderBy('slot', 'ASC')
:get()
end)
-- Event: Item bewegen
RegisterNetEvent('inventory:moveItem')
AddEventHandler('inventory:moveItem', function(data)
local src = source
if not data or type(data) ~= 'table' then return end
local player = exports.ox_core:GetPlayer(src)
if not player then return end
local item = PlayerItem:find(data.itemId)
if not item or item.char_id ~= player.charId then return end
item.slot = data.targetSlot
item:save()
end)
-- Transaction: Sicherer Item-Transfer
local function transferItem(fromCharId, toCharId, itemId, amount)
return db.transaction(function(tx)
tx:table('player_items')
:where('id', itemId)
:where('char_id', fromCharId)
:decrement('amount', amount)
tx:table('player_items'):insert({
char_id = toCharId,
item_name = 'transferred_item',
amount = amount,
})
end)
end