687 lines
28 KiB
JavaScript
687 lines
28 KiB
JavaScript
module.exports = {
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Player
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Fetches a single player row by their charId.
|
|
* @param {string} charId
|
|
* @returns {Promise<any>}
|
|
*/
|
|
async getPlayer(charId) {
|
|
return await oxmysql.scalar('SELECT * FROM players WHERE charId = ?', [charId]);
|
|
},
|
|
|
|
/**
|
|
* Returns raw retention timestamps for every player in the database.
|
|
* ESX reads from the `users` table; QB/QBX reads from `luxu_stats_player_retention`.
|
|
* @returns {Promise<{ timestamp: number }[]>}
|
|
*/
|
|
async getPlayerRetention() {
|
|
if (Framework.name === 'esx') {
|
|
return /** @type {{ timestamp: number }[]} */ (await oxmysql.query('SELECT * FROM users'));
|
|
}
|
|
return /** @type {{ timestamp: number }[]} */ (
|
|
await oxmysql.query('SELECT * FROM luxu_stats_player_retention')
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Retrieves the saved clothing/skin data for a player.
|
|
* ESX reads `skin` from `users`; QB/QBX reads from `playerskins`.
|
|
* @param {string} charId
|
|
* @returns {Promise<Record<string, any>>}
|
|
*/
|
|
async getPlayerClothing(charId) {
|
|
const ESX_QUERY = `SELECT skin FROM users WHERE identifier = ?`;
|
|
const QB_QUERY = `SELECT skin FROM playerskins WHERE citizenid = ? AND active = ?`;
|
|
|
|
try {
|
|
if (Framework.name === 'esx') {
|
|
const result = await oxmysql.single(ESX_QUERY, [charId]);
|
|
if (result) return JSON.parse(result.skin);
|
|
}
|
|
if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
const result = await oxmysql.single(QB_QUERY, [charId, 1]);
|
|
if (result) return JSON.parse(result.skin);
|
|
}
|
|
return {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Vehicles
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Returns all vehicles of a given model hash, with their owner's charId.
|
|
* ESX reads from `owned_vehicles`; QB/QBX reads from `player_vehicles`.
|
|
* @param {number} modelHash
|
|
* @returns {Promise<{ plate: string, char_id: string }[]>}
|
|
*/
|
|
async getVehicleOwners(modelHash) {
|
|
if (Framework.name === 'esx') {
|
|
return await oxmysql.query(
|
|
'SELECT plate, owner as char_id FROM owned_vehicles WHERE CAST(JSON_UNQUOTE(JSON_EXTRACT(vehicle, "$.model")) AS UNSIGNED) = ?',
|
|
[modelHash]
|
|
);
|
|
}
|
|
return await oxmysql.query(
|
|
'SELECT pv.plate as plate, pv.citizenid as char_id FROM player_vehicles pv WHERE pv.hash = ?',
|
|
[modelHash]
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Returns the vehicle mod/properties data for a given plate.
|
|
* ESX reads `vehicle` from `owned_vehicles`; QB/QBX reads `mods` from `player_vehicles`.
|
|
* @param {string} plate
|
|
* @returns {Promise<Record<string, any> | null>}
|
|
*/
|
|
async getVehicleMods(plate) {
|
|
if (Framework.name === 'esx') {
|
|
const result = await oxmysql.prepare('SELECT vehicle FROM owned_vehicles WHERE plate = ?', [plate]);
|
|
if (!result) return null;
|
|
return JSON.parse(result);
|
|
} else if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
const result = await oxmysql.prepare('SELECT mods FROM player_vehicles WHERE plate = ?', [plate]);
|
|
if (!result) return null;
|
|
return JSON.parse(result);
|
|
}
|
|
return null;
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Offline player operations (all hit the framework player table)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Fetches a full offline player profile by their charId.
|
|
* Returns `null` when no matching record is found.
|
|
* @param {string} charId
|
|
* @returns {Promise<OfflinePlayer | null>}
|
|
*/
|
|
async getOfflinePlayer(charId) {
|
|
if (Framework.name === 'esx') {
|
|
const sql = `
|
|
SELECT
|
|
u.identifier,
|
|
u.accounts,
|
|
u.job,
|
|
u.job_grade,
|
|
u.firstname,
|
|
u.lastname,
|
|
u.phone_number,
|
|
CONCAT(u.firstname, ' ', u.lastname) as name,
|
|
UNIX_TIMESTAMP(u.last_seen) * 1000 as last_seen
|
|
FROM users u
|
|
WHERE u.identifier = ?
|
|
LIMIT 1
|
|
`;
|
|
const result = /** @type {any} */ (await oxmysql.prepare(sql, [charId]));
|
|
if (!result) return null;
|
|
|
|
const uniqueId = result.identifier.split(':')[1] || '';
|
|
const identifiers = Array.from(
|
|
new Set([`license:${uniqueId}`, ...JSON.parse(GetResourceKvpString(`identifiers:${uniqueId}`) ?? '[]')])
|
|
);
|
|
|
|
/** @type {Record<string, number>} */
|
|
let accounts = {};
|
|
try { accounts = typeof result.accounts === 'string' ? JSON.parse(result.accounts) : result.accounts || {}; } catch {}
|
|
|
|
let job = { name: '', label: '', grade: 0 };
|
|
try {
|
|
job = {
|
|
name: result.job || '',
|
|
label: result.job || '',
|
|
grade: typeof result.job_grade === 'number' ? result.job_grade : 0,
|
|
};
|
|
} catch {}
|
|
|
|
return /** @type {OfflinePlayer} */ ({
|
|
name: result.name || `${result.firstname || ''} ${result.lastname || ''}`.trim(),
|
|
uniqueId,
|
|
charId: result.identifier || '',
|
|
identifiers,
|
|
accounts,
|
|
last_seen: result.last_seen || 0,
|
|
job,
|
|
});
|
|
} else {
|
|
const sql = `
|
|
SELECT
|
|
p.citizenid,
|
|
p.license,
|
|
p.money,
|
|
p.job,
|
|
p.gang,
|
|
p.charinfo,
|
|
CONCAT(JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.firstname')), ' ', JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.lastname'))) as name,
|
|
JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.phone')) as phone_number,
|
|
UNIX_TIMESTAMP(p.last_updated) * 1000 as last_seen
|
|
FROM players p
|
|
WHERE p.citizenid = ?
|
|
LIMIT 1
|
|
`;
|
|
const result = /** @type {any} */ (await oxmysql.prepare(sql, [charId]));
|
|
if (!result) return null;
|
|
|
|
const uniqueId = result.license.split(':')[1] || '';
|
|
const identifiers = Array.from(
|
|
new Set([result.license, ...JSON.parse(GetResourceKvpString(`identifiers:${uniqueId}`) ?? '[]')])
|
|
);
|
|
|
|
/** @type {Record<string, number>} */
|
|
let accounts = {};
|
|
try { accounts = typeof result.money === 'string' ? JSON.parse(result.money) : result.money || {}; } catch {}
|
|
|
|
let job = { name: '', label: '', grade: 0 };
|
|
try {
|
|
const p = typeof result.job === 'string' ? JSON.parse(result.job) : result.job;
|
|
if (p) job = { name: p.name || '', label: p.label || p.name || '', grade: typeof p.grade === 'number' ? p.grade : p.grade?.level || 0 };
|
|
} catch {}
|
|
|
|
let gang = { name: '', label: '', grade: 0 };
|
|
try {
|
|
const p = typeof result.gang === 'string' ? JSON.parse(result.gang) : result.gang;
|
|
if (p) gang = { name: p.name || '', label: p.label || p.name || '', grade: typeof p.grade === 'number' ? p.grade : p.grade?.level || 0 };
|
|
} catch {}
|
|
|
|
return /** @type {OfflinePlayer} */ ({
|
|
name: result.name || '',
|
|
uniqueId: result.license.split(':')[1] || '',
|
|
charId: result.citizenid || '',
|
|
identifiers,
|
|
accounts,
|
|
last_seen: result.last_seen || 0,
|
|
job,
|
|
gang,
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Searches offline players by a search term (minimum 2 characters).
|
|
*
|
|
* Override this to customise the search behaviour for your database schema.
|
|
* @param {string} term
|
|
* @returns {Promise<Array<{ name: string, charId: string, identifiers: string[] }>>}
|
|
*/
|
|
async searchOfflinePlayers(term) {
|
|
if (!term || term.length < 2) return [];
|
|
|
|
const rawTerm = term.trim();
|
|
const searchTerm = `%${rawTerm}%`;
|
|
|
|
if (Framework.name === 'esx') {
|
|
const sql = `
|
|
SELECT
|
|
u.identifier,
|
|
u.firstname,
|
|
u.lastname,
|
|
CONCAT(u.firstname, ' ', u.lastname) as name
|
|
FROM users u
|
|
WHERE (
|
|
u.identifier LIKE ? OR
|
|
u.phone_number LIKE ? OR
|
|
CONCAT(u.firstname, ' ', u.lastname) LIKE ?
|
|
)
|
|
LIMIT 10
|
|
`;
|
|
/** @type {{ identifier: string, firstname: string, lastname: string, name: string }[]} */
|
|
const results = await oxmysql.query(sql, [searchTerm, searchTerm, searchTerm]);
|
|
return results.map((row) => {
|
|
/** @type {string[]} */
|
|
const identifiers = [];
|
|
if (row.identifier) identifiers.push(row.identifier);
|
|
return {
|
|
name: row.name || `${row.firstname || ''} ${row.lastname || ''}`.trim(),
|
|
charId: row.identifier || '',
|
|
identifiers,
|
|
};
|
|
});
|
|
} else {
|
|
const sql = `
|
|
SELECT
|
|
p.citizenid,
|
|
p.charinfo,
|
|
CONCAT(JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.firstname')), ' ', JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.lastname'))) as name
|
|
FROM players p
|
|
WHERE (
|
|
p.name LIKE ? OR
|
|
p.citizenid LIKE ? OR
|
|
p.license LIKE ? OR
|
|
p.phone_number LIKE ? OR
|
|
p.charinfo LIKE ?
|
|
)
|
|
LIMIT 10
|
|
`;
|
|
/** @type {{ citizenid: string, charinfo: string, name: string, identifier?: string }[]} */
|
|
const results = await oxmysql.query(sql, [searchTerm, searchTerm, searchTerm, searchTerm, searchTerm]);
|
|
return results.map((row) => {
|
|
/** @type {string[]} */
|
|
const identifiers = [];
|
|
if (row.identifier) identifiers.push(row.identifier);
|
|
return {
|
|
name: row.name || '',
|
|
charId: row.citizenid || '',
|
|
identifiers,
|
|
};
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Permanently deletes a single player row from the framework table.
|
|
* For a full character deletion that includes a transaction with additional
|
|
* resource-specific cleanup queries, use `deleteCharacter` instead.
|
|
* @param {string} charId
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async deleteOfflinePlayer(charId) {
|
|
if (Framework.name === 'esx') {
|
|
return (await oxmysql.update('DELETE FROM users WHERE identifier = ?', [charId])) === 1;
|
|
} else if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
return (await oxmysql.update('DELETE FROM players WHERE citizenid = ?', [charId])) === 1;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Deletes a player character inside a single database transaction.
|
|
*
|
|
* Add any extra cleanup queries here (e.g. removing owned vehicles, housing, etc.).
|
|
* Each query receives the character identifier as its only parameter
|
|
* (ESX = `identifier`, QB/QBX = `citizenid`).
|
|
*
|
|
* @param {string} charId
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async deleteCharacter(charId) {
|
|
/** @type {Array<{ query: string, values: any[] }>} */
|
|
const queries = [];
|
|
|
|
if (Framework.name === 'esx') {
|
|
// Add extra ESX cleanup queries before the main delete:
|
|
queries.push({ query: 'DELETE FROM owned_vehicles WHERE owner = ?', values: [charId] });
|
|
|
|
queries.push({ query: 'DELETE FROM users WHERE identifier = ?', values: [charId] });
|
|
} else if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
// Add extra QB/QBX cleanup queries before the main delete:
|
|
queries.push({ query: 'DELETE FROM player_vehicles WHERE citizenid = ?', values: [charId] });
|
|
|
|
queries.push({ query: 'DELETE FROM players WHERE citizenid = ?', values: [charId] });
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
return !!(await oxmysql.transaction(queries));
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Changes the first/last name of an offline player.
|
|
* @param {string} charId
|
|
* @param {string} newName - Full name as "Firstname Lastname".
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async changeOfflinePlayerName(charId, newName) {
|
|
const [firstname, lastname] = newName.split(' ');
|
|
if (Framework.name === 'esx') {
|
|
return (
|
|
(await oxmysql.update(
|
|
'UPDATE users SET firstname = ?, lastname = ? WHERE identifier = ?',
|
|
[firstname, lastname ?? '', charId]
|
|
)) === 1
|
|
);
|
|
} else if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
return (
|
|
(await oxmysql.update(
|
|
'UPDATE players SET charinfo = JSON_SET(charinfo, "$.firstname", ?, "$.lastname", ?) WHERE citizenid = ?',
|
|
[firstname, lastname, charId]
|
|
)) === 1
|
|
);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Changes the job and grade of an offline player.
|
|
* @param {string} charId
|
|
* @param {string} job
|
|
* @param {number} grade
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async changeOfflinePlayerJob(charId, job, grade) {
|
|
if (Framework.name === 'esx') {
|
|
return (
|
|
(await oxmysql.update(
|
|
'UPDATE users SET job = ?, job_grade = ? WHERE identifier = ?',
|
|
[job, grade, charId]
|
|
)) === 1
|
|
);
|
|
} else if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
return (
|
|
(await oxmysql.update(
|
|
'UPDATE players SET job = JSON_SET(job, "$.name", ?, "$.grade.level", ?) WHERE citizenid = ?',
|
|
[job, grade, charId]
|
|
)) === 1
|
|
);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Changes the gang and grade of an offline player.
|
|
* @param {string} charId
|
|
* @param {string} gang
|
|
* @param {number} grade
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async changeOfflinePlayerGang(charId, gang, grade) {
|
|
if (Framework.name === 'esx') {
|
|
return (
|
|
(await oxmysql.update(
|
|
'UPDATE users SET gang = ?, gang_grade = ? WHERE identifier = ?',
|
|
[gang, grade, charId]
|
|
)) === 1
|
|
);
|
|
} else if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
return (
|
|
(await oxmysql.update(
|
|
'UPDATE players SET gang = JSON_SET(gang, "$.name", ?, "$.grade.level", ?) WHERE citizenid = ?',
|
|
[gang, grade, charId]
|
|
)) === 1
|
|
);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Replaces all account balances for an offline player.
|
|
* @param {string} charId
|
|
* @param {Record<string, number>} accounts
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async changeOfflinePlayerMoney(charId, accounts) {
|
|
if (Framework.name === 'esx') {
|
|
return (
|
|
(await oxmysql.update(
|
|
'UPDATE users SET accounts = ? WHERE identifier = ?',
|
|
[JSON.stringify(accounts), charId]
|
|
)) === 1
|
|
);
|
|
} else if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
return (
|
|
(await oxmysql.update(
|
|
'UPDATE players SET money = ? WHERE citizenid = ?',
|
|
[JSON.stringify(accounts), charId]
|
|
)) === 1
|
|
);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Sets the balance of a single account for an offline player.
|
|
* Reads the current accounts JSON, patches the target key, and writes it back.
|
|
* @param {string} charId
|
|
* @param {string} account
|
|
* @param {number} amount
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async updateOfflinePlayerAccountMoney(charId, account, amount) {
|
|
if (Framework.name === 'esx') {
|
|
const row = await oxmysql.single('SELECT accounts FROM users WHERE identifier = ?', [charId]);
|
|
if (!row) return false;
|
|
const accounts = JSON.parse(row.accounts);
|
|
accounts[account] = amount;
|
|
return (
|
|
(await oxmysql.update(
|
|
'UPDATE users SET accounts = ? WHERE identifier = ?',
|
|
[JSON.stringify(accounts), charId]
|
|
)) === 1
|
|
);
|
|
} else if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
const row = await oxmysql.single('SELECT money FROM players WHERE citizenid = ?', [charId]);
|
|
if (!row) return false;
|
|
const money = JSON.parse(row.money);
|
|
money[account] = amount;
|
|
return (
|
|
(await oxmysql.update(
|
|
'UPDATE players SET money = ? WHERE citizenid = ?',
|
|
[JSON.stringify(money), charId]
|
|
)) === 1
|
|
);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Inventory
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Finds all players that possess a specific item.
|
|
* Uses MariaDB `JSON_CONTAINS` for server-side filtering — significantly faster
|
|
* than fetching every inventory row and filtering in memory.
|
|
* @param {string} itemName
|
|
* @returns {Promise<Array<{ charId: string, name: string, amount: number }>>}
|
|
*/
|
|
async getPlayersWithItem(itemName) {
|
|
if (Framework.name === 'esx') {
|
|
const sql = `
|
|
SELECT
|
|
u.identifier as charId,
|
|
CONCAT(u.firstname, ' ', u.lastname) as name,
|
|
u.inventory
|
|
FROM users u
|
|
WHERE JSON_CONTAINS(u.inventory, JSON_OBJECT('name', ?))
|
|
LIMIT 5000
|
|
`;
|
|
try {
|
|
const results = /** @type {any[]} */ (await oxmysql.query(sql, [itemName]));
|
|
return results.map((row) => {
|
|
try {
|
|
const inventory = typeof row.inventory === 'string' ? JSON.parse(row.inventory) : row.inventory;
|
|
if (!Array.isArray(inventory)) return null;
|
|
let amount = 0;
|
|
for (const item of inventory) {
|
|
if (item && item.name === itemName) amount += item.count || item.amount || 0;
|
|
}
|
|
if (amount <= 0) return null;
|
|
return { charId: row.charId, name: row.name, amount };
|
|
} catch { return null; }
|
|
}).filter(/** @param {any} item */ (item) => item !== null)
|
|
.sort(/** @param {{ amount: number }} a @param {{ amount: number }} b */ (a, b) => b.amount - a.amount);
|
|
} catch { return []; }
|
|
} else if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
const sql = `
|
|
SELECT
|
|
p.citizenid as charId,
|
|
CONCAT(
|
|
JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.firstname')), ' ',
|
|
JSON_UNQUOTE(JSON_EXTRACT(p.charinfo, '$.lastname'))
|
|
) as name,
|
|
p.inventory
|
|
FROM players p
|
|
WHERE JSON_CONTAINS(p.inventory, JSON_OBJECT('name', ?))
|
|
LIMIT 5000
|
|
`;
|
|
try {
|
|
const results = /** @type {any[]} */ (await oxmysql.query(sql, [itemName]));
|
|
return results.map((row) => {
|
|
try {
|
|
const inventory = typeof row.inventory === 'string' ? JSON.parse(row.inventory) : row.inventory;
|
|
if (!Array.isArray(inventory)) return null;
|
|
let amount = 0;
|
|
for (const item of inventory) {
|
|
if (item && item.name === itemName) amount += item.amount || item.count || 0;
|
|
}
|
|
if (amount <= 0) return null;
|
|
return { charId: row.charId, name: row.name, amount };
|
|
} catch { return null; }
|
|
}).filter(/** @param {any} item */ (item) => item !== null)
|
|
.sort(/** @param {{ amount: number }} a @param {{ amount: number }} b */ (a, b) => b.amount - a.amount);
|
|
} catch { return []; }
|
|
}
|
|
return [];
|
|
},
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Statistics (framework tables)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Returns the top 100 players ranked by total money (bank + cash).
|
|
* @returns {Promise<Array<{ name: string, money: number, charId: string }>>}
|
|
*/
|
|
async getRichList() {
|
|
if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
const rows = /** @type {{ money: string, charinfo: string, citizenid: string }[]} */ (
|
|
await oxmysql.query(`
|
|
SELECT money, charinfo, citizenid
|
|
FROM players
|
|
WHERE money IS NOT NULL AND charinfo IS NOT NULL
|
|
AND JSON_EXTRACT(money, '$.bank') + JSON_EXTRACT(money, '$.cash') > 0
|
|
ORDER BY (JSON_EXTRACT(money, '$.bank') + JSON_EXTRACT(money, '$.cash')) DESC
|
|
LIMIT 100
|
|
`)
|
|
);
|
|
return rows.map((r) => {
|
|
try {
|
|
const charinfo = JSON.parse(r.charinfo);
|
|
const money = JSON.parse(r.money);
|
|
return {
|
|
name: `${charinfo.firstname} ${charinfo.lastname}`,
|
|
money: (money.bank || 0) + (money.cash || 0),
|
|
charId: r.citizenid,
|
|
};
|
|
} catch { return null; }
|
|
}).filter(/** @param {any} r */ (r) => r !== null);
|
|
} else if (Framework.name === 'esx') {
|
|
const rows = /** @type {{ firstname: string, lastname: string, accounts: string, identifier: string }[]} */ (
|
|
await oxmysql.query(`
|
|
SELECT firstname, lastname, accounts, identifier
|
|
FROM users
|
|
WHERE accounts IS NOT NULL
|
|
AND JSON_EXTRACT(accounts, '$.bank') + JSON_EXTRACT(accounts, '$.money') > 0
|
|
ORDER BY (JSON_EXTRACT(accounts, '$.bank') + JSON_EXTRACT(accounts, '$.money')) DESC
|
|
LIMIT 100
|
|
`)
|
|
);
|
|
return rows.map((r) => {
|
|
try {
|
|
const accounts = JSON.parse(r.accounts);
|
|
return {
|
|
name: `${r.firstname} ${r.lastname}`,
|
|
money: (accounts.bank || 0) + (accounts.money || 0),
|
|
charId: r.identifier,
|
|
};
|
|
} catch { return null; }
|
|
}).filter(/** @param {any} r */ (r) => r !== null);
|
|
}
|
|
return [];
|
|
},
|
|
|
|
/**
|
|
* Returns a job-name → player-count mapping from the framework player table.
|
|
* @returns {Promise<Record<string, number>>}
|
|
*/
|
|
async getJobDistribution() {
|
|
/** @type {Record<string, number>} */
|
|
const distribution = {};
|
|
if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
const rows = /** @type {{ job_name: string, count: number }[]} */ (
|
|
await oxmysql.query(`
|
|
SELECT JSON_EXTRACT(job, '$.name') as job_name, COUNT(*) as count
|
|
FROM players
|
|
WHERE job IS NOT NULL
|
|
GROUP BY JSON_EXTRACT(job, '$.name')
|
|
`)
|
|
);
|
|
rows.forEach((row) => {
|
|
if (!row.job_name) return;
|
|
const jobName = JSON.parse(row.job_name);
|
|
if (jobName) distribution[jobName] = row.count;
|
|
});
|
|
} else if (Framework.name === 'esx') {
|
|
const rows = /** @type {{ job: string, count: number }[]} */ (
|
|
await oxmysql.query('SELECT job, COUNT(*) as count FROM users GROUP BY job')
|
|
);
|
|
rows.forEach((row) => { distribution[row.job] = row.count; });
|
|
}
|
|
return distribution;
|
|
},
|
|
|
|
/**
|
|
* Returns the total number of distinct players (by license) ever registered.
|
|
* @returns {Promise<number>}
|
|
*/
|
|
async getTotalUniquePlayers() {
|
|
if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
const result = /** @type {{ count: number }[]} */ (
|
|
await oxmysql.query('SELECT COUNT(DISTINCT license) as count FROM players')
|
|
);
|
|
return result[0]?.count ?? 0;
|
|
} else if (Framework.name === 'esx') {
|
|
const result = /** @type {{ count: number }[]} */ (
|
|
await oxmysql.query(
|
|
"SELECT COUNT(DISTINCT SUBSTRING_INDEX(identifier, ':', -1)) as count FROM users WHERE identifier LIKE '%:%'"
|
|
)
|
|
);
|
|
return result[0]?.count ?? 0;
|
|
}
|
|
return 0;
|
|
},
|
|
|
|
/**
|
|
* Returns the total number of character rows (one per character/slot).
|
|
* @returns {Promise<number>}
|
|
*/
|
|
async getTotalCharacters() {
|
|
if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
const result = /** @type {{ count: number }[]} */ (
|
|
await oxmysql.query('SELECT COUNT(*) as count FROM players')
|
|
);
|
|
return result[0]?.count ?? 0;
|
|
} else if (Framework.name === 'esx') {
|
|
const result = /** @type {{ count: number }[]} */ (
|
|
await oxmysql.query('SELECT COUNT(*) as count FROM users')
|
|
);
|
|
return result[0]?.count ?? 0;
|
|
}
|
|
return 0;
|
|
},
|
|
|
|
/**
|
|
* Returns the total bank balance across all players.
|
|
* @returns {Promise<number>}
|
|
*/
|
|
async getTotalBankMoney() {
|
|
if (Framework.name === 'qb' || Framework.name === 'qbx') {
|
|
const result = /** @type {{ total: number }[]} */ (
|
|
await oxmysql.query(
|
|
'SELECT SUM(JSON_EXTRACT(money, "$.bank")) as total FROM players WHERE money IS NOT NULL'
|
|
)
|
|
);
|
|
return result[0]?.total ?? 0;
|
|
} else if (Framework.name === 'esx') {
|
|
const result = /** @type {{ total: number }[]} */ (
|
|
await oxmysql.query(
|
|
'SELECT SUM(JSON_EXTRACT(accounts, "$.bank")) as total FROM users WHERE accounts IS NOT NULL'
|
|
)
|
|
);
|
|
return result[0]?.total ?? 0;
|
|
}
|
|
return 0;
|
|
},
|
|
};
|