en-core stores characters, groups and placed world content in MySQL through oxmysql. You do not have to import anything: en-core creates and updates its own tables every time it starts.
Setup
- Set
mysql_connection_stringinserver.cfgand make sureoxmysqlstarts before en-core. - Importing
encore.sqlfrom the en-core folder is optional. It documents theplayers,encore_groupsandencore_group_memberstables. It does not includeencore_content, which en-core creates on its own. - If setup fails, the console prints
Database setup failed: ...with a hint to checkmysql_connection_string, and the core will not work.
set mysql_connection_string "mysql://<user>:<password>@localhost/<database>?charset=utf8mb4"
ensure oxmysql
ensure en-ui
ensure en-coreOther EndCore resources create their own tables. Those are covered on each resource's page.
Tables
| Table | Holds | Created by |
|---|---|---|
players | One row per character | server/main.lua |
encore_groups | Player groups | server/groups.lua |
encore_group_members | Group membership and rank | server/groups.lua |
encore_content | Content placed in game | server/content.lua |
players
One row per character. A license can own several characters (3 slots by default).
| Column | Type | Notes |
|---|---|---|
id | INT AUTO_INCREMENT | Primary key |
citizenid | VARCHAR(50) | Unique |
license | VARCHAR(60) | Plain index, so one license can have many characters |
name | VARCHAR(255) | "First Last" |
money | LONGTEXT | JSON, for example {"cash":500,"bank":1000} |
charinfo | LONGTEXT | JSON |
job | LONGTEXT | JSON of the active job |
jobs | LONGTEXT, nullable | JSON map of job name to grade |
metadata | LONGTEXT | JSON of every metadata key |
position | LONGTEXT | JSON { x, y, z, heading } |
last_updated | TIMESTAMP | Updated on every write |
The group a character belongs to is not stored here. It is attached at login from the group tables. See Player data for what the JSON columns contain.
encore_groups
| Column | Type | Notes |
|---|---|---|
id | INT AUTO_INCREMENT | Primary key |
name | VARCHAR(32) | Unique |
tag | VARCHAR(8) | Unique |
created_at | TIMESTAMP |
encore_group_members
| Column | Type | Notes |
|---|---|---|
citizenid | VARCHAR(50) | Primary key, so a character can be in one group |
group_id | INT | Foreign key to encore_groups.id, deleted with the group |
grade | TINYINT, default 0 | The member's rank. Named grade because RANK is a reserved word in MySQL 8. |
joined_at | TIMESTAMP |
encore_content
| Column | Type | Notes |
|---|---|---|
kind | VARCHAR(40) | Part of the primary key |
id | VARCHAR(80) | Part of the primary key |
data | LONGTEXT | JSON |
updated_by | VARCHAR(100), nullable | Who saved it |
updated_at | TIMESTAMP | Updated on every write |
The primary key is (kind, id). See Content registry.
Full schema
This is what en-core creates when the tables do not exist yet:
CREATE TABLE IF NOT EXISTS `players` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`citizenid` VARCHAR(50) NOT NULL,
`license` VARCHAR(60) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`money` LONGTEXT NOT NULL,
`charinfo` LONGTEXT NOT NULL,
`job` LONGTEXT NOT NULL,
`jobs` LONGTEXT,
`metadata` LONGTEXT NOT NULL,
`position` LONGTEXT NOT NULL,
`last_updated` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `citizenid` (`citizenid`),
KEY `license` (`license`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `encore_groups` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(32) NOT NULL,
`tag` VARCHAR(8) NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `name` (`name`),
UNIQUE KEY `tag` (`tag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `encore_group_members` (
`citizenid` VARCHAR(50) NOT NULL PRIMARY KEY,
`group_id` INT NOT NULL,
`grade` TINYINT NOT NULL DEFAULT 0,
`joined_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
KEY `group_id` (`group_id`),
CONSTRAINT `fk_group_members_group` FOREIGN KEY (`group_id`)
REFERENCES `encore_groups` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `encore_content` (
`kind` VARCHAR(40) NOT NULL,
`id` VARCHAR(80) NOT NULL,
`data` LONGTEXT NOT NULL,
`updated_by` VARCHAR(100) DEFAULT NULL,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`kind`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;Boot migrations
Every start, en-core also fixes databases from older builds:
- Creates
playersif it does not exist. - Drops any unique index on
players.licenseand adds a plain index. Early builds limited each account to one character. - If old
gangorgangscolumns exist and areNOT NULL, makes them nullable so new rows can be inserted. They are never deleted and never read. - Creates the group tables, then loads all groups into memory.
The content table is created and loaded separately when the content registry starts.
When data is saved
| When | What is saved |
|---|---|
Every updateInterval (5 minutes by default, config/shared.lua) | Every online player, in one batched query |
| A player disconnects | Their character, including last position |
| A player logs out to character selection | Their character |
/saveall or the SaveAllPlayers() export | Every online player |
| en-core stops | Every online player |
| txAdmin announces a shutdown | Every online player |
| Group changes | Written immediately |
SaveContent / DeleteContent | Written in the background right away |
player.Functions.Save() saves one character on demand. It reads the ped's position on the server and skips an invalid 0, 0, 0 position.
en-core keeps online players in memory and writes over their row when it saves. Don't edit an online character's row directly in the database; your change will be overwritten. Use the exports, or edit while the character is offline.
Examples
List the characters on a license:
SELECT citizenid, name, last_updated
FROM players
WHERE license = 'license:<license>'
ORDER BY id ASC;Find every member of a group with their character name:
SELECT m.citizenid, p.name, m.grade, m.joined_at
FROM encore_group_members m
LEFT JOIN players p ON p.citizenid = m.citizenid
WHERE m.group_id = 4
ORDER BY m.grade DESC;The same from Lua, for an offline edit:
local player = exports['en-core']:GetOfflinePlayer('ENCAB12CD34')
if player then
player.Functions.SetMoney('bank', 5000, 'Refund')
player.Functions.Save()
end