ENH - centralize phone defaults and fixed permissions (#26)

* ENH - centralize phone configuration defaults

* FIX - enforce fixed phone permissions
This commit is contained in:
Leon.Schmidt
2026-08-22 05:02:14 +02:00
committed by GitHub
parent a0f67fae80
commit 4fa4bf3c78
29 changed files with 2006 additions and 76 deletions
+20 -9
View File
@@ -138,10 +138,10 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting
| `smx-inventory` (`smx`) | Yes | Yes | ESX only; one metadata record per configured item name through the player metadata bridge | | `smx-inventory` (`smx`) | Yes | Yes | ESX only; one metadata record per configured item name through the player metadata bridge |
| `lj-inventory` (`lj`) | Yes | Yes | QBCore inventory with item `info` metadata | | `lj-inventory` (`lj`) | Yes | Yes | QBCore inventory with item `info` metadata |
| `qb-inventory` (`qb`) | Yes | Yes | Uses item `info` metadata | | `qb-inventory` (`qb`) | Yes | Yes | Uses item `info` metadata |
| `hex_4_inventory` (`hex`) | **No metadata support** | **No, Unique Phones are not possible** | ESX only; set `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` | | `hex_4_inventory` (`hex`) | **No metadata support** | **No, Unique Phones are not possible** | ESX only; Sky Phone automatically disables unique phones and physical SIM cards |
| Native ESX inventory (`esx`) | **No metadata support** | **No, Unique Phones are not possible** | Count-based items; set `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` | | Native ESX inventory (`esx`) | **No metadata support** | **No, Unique Phones are not possible** | Count-based items; Sky Phone automatically disables unique phones and physical SIM cards |
`hex_4_inventory` and native ESX inventory cannot persist per-item metadata. Unique Phones and physical SIM cards are therefore unavailable with these adapters. `hex_4_inventory` and native ESX inventory cannot persist per-item metadata. Sky Phone therefore forces `Config.Phone.Unique` and `Config.Sim.Enabled` to `false` at runtime whenever either adapter is active.
`Config.Bridge.Inventory = "auto"` detects framework-compatible adapters in the table order. This deliberately matches the Sky inventory priority so a dedicated inventory is selected before a compatibility resource it may run beside. You may configure either the short value shown in parentheses or the exact resource name. `Config.Bridge.Inventory = "auto"` detects framework-compatible adapters in the table order. This deliberately matches the Sky inventory priority so a dedicated inventory is selected before a compatibility resource it may run beside. You may configure either the short value shown in parentheses or the exact resource name.
@@ -216,6 +216,7 @@ The files contain clearly separated sections for:
| Section | Purpose | | Section | Purpose |
| --- | --- | | --- | --- |
| `Config.Bridge` | Framework, inventory, language, callback timeout, and debug mode | | `Config.Bridge` | Framework, inventory, language, callback timeout, and debug mode |
| `Config.CommandPermissions` | Fixed groups for the admin panel, test data, verification commands, and social moderation |
| `Config.Phone` | Phone item, movement, unique-device mode, and development command | | `Config.Phone` | Phone item, movement, unique-device mode, and development command |
| `Config.Sim` | Physical or virtual SIM behavior and number formatting | | `Config.Sim` | Physical or virtual SIM behavior and number formatting |
| `Config.Calls` / `Config.Radio` | Voice providers, call behavior, radio limits, and permissions | | `Config.Calls` / `Config.Radio` | Voice providers, call behavior, radio limits, and permissions |
@@ -241,21 +242,31 @@ Config.PhoneConfigurator = {
} }
``` ```
When enabled, `config.lua` and the server-only `media.lua` are first-run defaults. Sky Phone creates When enabled, the generated `source/shared/config_default.lua` is the shipped first-run baseline.
the `sky_phone_configurator` table automatically, loads its saved values before framework and phone The frontend build recreates this file from `config.lua` and the server-only `media.lua`; do not edit
the generated snapshot directly. Sky Phone creates the `sky_phone_configurator` table automatically,
loads its saved values before framework and phone
modules initialize, and exposes the editor through `/phonepanel`. Nothing autosaves: stage changes modules initialize, and exposes the editor through `/phonepanel`. Nothing autosaves: stage changes
in the Phone Configurator tool and press the green check. Saving verifies both SQL payloads and then in the Phone Configurator tool and press the green check. Saving verifies both SQL payloads and then
applies the new server, client, media, app, item, command, provider, animation, and UI values through applies the new server, client, media, app, item, command, provider, animation, and UI values through
Sky Phone's internal runtime refresh. It does not execute a resource restart command. Sky Phone's internal runtime refresh. It does not execute a resource restart command.
Every `Config.*` value from `config.lua`, including server-only sections, and every value from Every configurable `Config.*` value from `config.lua`, including server-only sections, and every
`Config.Media` is discovered automatically. The bootstrap switch above intentionally remains value from `Config.Media` is discovered automatically. The bootstrap switch and
file-owned because it decides whether SQL configuration is loaded. Lists, nested objects, vectors, `Config.CommandPermissions` intentionally remain file-owned: the switch decides whether SQL
configuration is loaded, while permissions must remain authoritative outside the panel. The fixed
permission table is never displayed or overwritten by the Phone Configurator, and its stable keys do
not change when their commands are renamed in the panel. Lists, nested objects, vectors,
and numeric-keyed Lua tables use structured editors instead of raw JSON. Shipped schema rows stay and numeric-keyed Lua tables use structured editors instead of raw JSON. Shipped schema rows stay
editable but cannot be renamed, converted, or removed. Every list and table still accepts any number editable but cannot be renamed, converted, or removed. Every list and table still accepts any number
of additional rows; administrator-added rows remain removable. Company job keys are intentionally of additional rows; administrator-added rows remain removable. Company job keys are intentionally
fully removable because `Config.Companies.Definitions` is a freely managed job collection. fully removable because `Config.Companies.Definitions` is a freely managed job collection.
ESX and QBCore use the groups listed in `Config.CommandPermissions`. Qbox checks the configured ACE
objects first and then its framework groups. The standard Qbox `permissions.cfg` grants the `admin`
ACE to `group.admin`, so an identifier assigned to `group.admin` can open `/phonepanel` with the
shipped `phonepanel` permission list. Restart `sky_phone` after changing fixed permissions.
Media API keys and server peppers are never returned in plaintext to the NUI. Existing secrets are Media API keys and server peppers are never returned in plaintext to the NUI. Existing secrets are
shown only as configured and are replaced only when an administrator enters a new value. shown only as configured and are replaced only when an administrator enters a new value.
@@ -365,7 +376,7 @@ Do not configure an LB Phone client event or client export. Sky Phone registers
The server registers `Config.Phone.Item` as usable for every supported inventory adapter: `ak47`, `codem`, `core`, `jaksam`, `jpr`, `lj`, `mf`, `one`, `origen`, `ox`, `ps`, `qb`, `qs`, `smx`, `tgiann`, `hex`, and `esx`. Resource startup fails visibly if the selected adapter or its resource is unavailable. The server registers `Config.Phone.Item` as usable for every supported inventory adapter: `ak47`, `codem`, `core`, `jaksam`, `jpr`, `lj`, `mf`, `one`, `origen`, `ox`, `ps`, `qb`, `qs`, `smx`, `tgiann`, `hex`, and `esx`. Resource startup fails visibly if the selected adapter or its resource is unavailable.
The `hex` and `esx` adapters use ESX's count-based item API. They require both `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` because this API cannot persist per-item phone or physical SIM metadata. `auto` selects `hex` when `hex_4_inventory` is started and otherwise falls back to `esx` on an ESX server when no metadata-capable inventory is detected. The `hex` and `esx` adapters use ESX's count-based item API, which cannot persist per-item phone or physical SIM metadata. Sky Phone automatically forces `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` while either adapter is active. `auto` selects `hex` when `hex_4_inventory` is started and otherwise falls back to `esx` on an ESX server when no metadata-capable inventory is detected.
### QBCore-style item tables ### QBCore-style item tables
+40 -5
View File
@@ -2,15 +2,49 @@ const fs = require('node:fs')
const path = require('node:path') const path = require('node:path')
const frontendRoot = __dirname const frontendRoot = __dirname
const resourceDirectory = path.join(frontendRoot, '..', 'sky_phone')
const sourceDirectory = path.join(frontendRoot, 'dist') const sourceDirectory = path.join(frontendRoot, 'dist')
const targetDirectory = path.join( const targetDirectory = path.join(resourceDirectory, 'source', 'html')
frontendRoot, const configDefaultPath = path.join(
'..', resourceDirectory,
'sky_phone',
'source', 'source',
'html', 'shared',
'config_default.lua',
) )
function readLuaSource(...segments) {
return fs
.readFileSync(path.join(resourceDirectory, ...segments), 'utf8')
.replace(/\r\n?/g, '\n')
.trimEnd()
}
function generateConfigDefault() {
const configSource = readLuaSource('config', 'config.lua').replace(
/\n?-- CONFIG_DEFAULT_EXCLUDE_START[\s\S]*?-- CONFIG_DEFAULT_EXCLUDE_END\n?/g,
'\n',
)
const mediaSource = readLuaSource('config', 'media.lua')
const generatedSource = [
'-- GENERATED by frontend/build.cjs from config/config.lua and config/media.lua - do not edit.',
'-- Escrowed snapshot of the shipped defaults used by the Phone Configurator.',
'local realConfig = Config',
'Config = {}',
'',
configSource,
'',
mediaSource,
'',
'ConfigDefaults = Config',
'Config = realConfig',
'',
].join('\n')
fs.mkdirSync(path.dirname(configDefaultPath), { recursive: true })
fs.writeFileSync(configDefaultPath, generatedSource, 'utf8')
console.log(`Generated Phone Configurator defaults at ${configDefaultPath}`)
}
if (!fs.existsSync(sourceDirectory)) { if (!fs.existsSync(sourceDirectory)) {
throw new Error( throw new Error(
'Missing frontend/dist. Run vite build before publishing the NUI.', 'Missing frontend/dist. Run vite build before publishing the NUI.',
@@ -28,4 +62,5 @@ const normalizedIndex = fs
.replace(/\n[ \t]*\n([ \t]*<\/body>)/g, '\n$1') .replace(/\n[ \t]*\n([ \t]*<\/body>)/g, '\n$1')
fs.writeFileSync(targetIndex, normalizedIndex) fs.writeFileSync(targetIndex, normalizedIndex)
generateConfigDefault()
console.log(`Published NUI to ${targetDirectory}`) console.log(`Published NUI to ${targetDirectory}`)
@@ -54,6 +54,14 @@ const config = readFileSync(
new URL('../../../sky_phone/config/config.lua', import.meta.url), new URL('../../../sky_phone/config/config.lua', import.meta.url),
'utf8', 'utf8',
) )
const configDefaultsSource = config.replace(
/\r?\n?-- CONFIG_DEFAULT_EXCLUDE_START[\s\S]*?-- CONFIG_DEFAULT_EXCLUDE_END\r?\n?/g,
'\n',
)
const mediaConfig = readFileSync(
new URL('../../../sky_phone/config/media.lua', import.meta.url),
'utf8',
)
const schema = readFileSync( const schema = readFileSync(
new URL('../../../sky_phone/sql/install.sql', import.meta.url), new URL('../../../sky_phone/sql/install.sql', import.meta.url),
'utf8', 'utf8',
@@ -69,6 +77,17 @@ const configuratorServer = readFileSync(
), ),
'utf8', 'utf8',
) )
const configDefault = readFileSync(
new URL(
'../../../sky_phone/source/shared/config_default.lua',
import.meta.url,
),
'utf8',
)
const frontendBuild = readFileSync(
new URL('../../build.cjs', import.meta.url),
'utf8',
)
const configuratorClient = readFileSync( const configuratorClient = readFileSync(
new URL( new URL(
'../../../sky_phone/source/client/phone_configurator.lua', '../../../sky_phone/source/client/phone_configurator.lua',
@@ -216,12 +235,14 @@ describe('standalone admin panel contracts', () => {
it('authorizes every server request without requiring a phone session', () => { it('authorizes every server request without requiring a phone session', () => {
expect(server).not.toContain('SkyPhone.RequireSession(source)') expect(server).not.toContain('SkyPhone.RequireSession(source)')
expect(server).toContain( expect(server).toContain(
'Bridge.Framework.HasAdminGroup(source, Config.AdminPanel.AdminGroups)', 'Bridge.Framework.HasPermission(source, "phonepanel")',
) )
expect(server).toContain('Config.AdminPanel.ReadRequestsPerMinute') expect(server).toContain('Config.AdminPanel.ReadRequestsPerMinute')
expect(server).toContain('Config.AdminPanel.ActionRequestsPerMinute') expect(server).toContain('Config.AdminPanel.ActionRequestsPerMinute')
expect(server).toContain('Config.AdminPanel.CredentialRevealsPerMinute') expect(server).toContain('Config.AdminPanel.CredentialRevealsPerMinute')
expect(config).toContain('Config.AdminPanel = {') expect(config).toContain('Config.AdminPanel = {')
expect(config).toContain('Config.CommandPermissions = {')
expect(config).not.toContain('AdminGroups =')
}) })
it('opens directly from the configurable command with dedicated focus', () => { it('opens directly from the configurable command with dedicated focus', () => {
@@ -272,6 +293,12 @@ describe('standalone admin panel contracts', () => {
expect(schema).toContain( expect(schema).toContain(
'CREATE TABLE IF NOT EXISTS `sky_phone_configurator`', 'CREATE TABLE IF NOT EXISTS `sky_phone_configurator`',
) )
expect(
manifest.indexOf("'source/shared/config_default.lua'"),
).toBeGreaterThan(manifest.indexOf("'config/media.lua'"))
expect(manifest.indexOf("'source/shared/config_default.lua'")).toBeLessThan(
manifest.indexOf("'source/server/phone_configurator.lua'"),
)
expect( expect(
manifest.indexOf("'source/server/phone_configurator.lua'"), manifest.indexOf("'source/server/phone_configurator.lua'"),
).toBeLessThan(manifest.indexOf("'source/bridge/server/framework.lua'")) ).toBeLessThan(manifest.indexOf("'source/bridge/server/framework.lua'"))
@@ -281,11 +308,36 @@ describe('standalone admin panel contracts', () => {
expect(configuratorServer).toContain('AND `revision` = ?') expect(configuratorServer).toContain('AND `revision` = ?')
expect(configuratorServer).toContain('configurator_enabled') expect(configuratorServer).toContain('configurator_enabled')
expect(configuratorServer).toMatch( expect(configuratorServer).toMatch(
/for key, value in pairs\(Config\)[\s\S]*?key ~= "Media"[\s\S]*?key ~= "PhoneConfigurator"/, /for key, value in pairs\(ConfigDefaults\)[\s\S]*?key ~= "Media"[\s\S]*?key ~= "PhoneConfigurator"/,
) )
expect(configuratorServer).toContain( expect(configuratorServer).toContain(
'default_media = serialize_value(Config.Media)', 'default_media = serialize_value(ConfigDefaults.Media)',
) )
expect(configDefault).toContain(
'GENERATED by frontend/build.cjs from config/config.lua and config/media.lua',
)
expect(configDefault).toContain('local realConfig = Config')
expect(configDefault).toContain('ConfigDefaults = Config')
expect(configDefault.replace(/\r\n?/g, '\n').trimEnd()).toBe(
[
'-- GENERATED by frontend/build.cjs from config/config.lua and config/media.lua - do not edit.',
'-- Escrowed snapshot of the shipped defaults used by the Phone Configurator.',
'local realConfig = Config',
'Config = {}',
'',
configDefaultsSource.replace(/\r\n?/g, '\n').trimEnd(),
'',
mediaConfig.replace(/\r\n?/g, '\n').trimEnd(),
'',
'ConfigDefaults = Config',
'Config = realConfig',
].join('\n'),
)
expect(frontendBuild).toContain("readLuaSource('config', 'config.lua')")
expect(frontendBuild).toContain("readLuaSource('config', 'media.lua')")
expect(frontendBuild).toContain("'config_default.lua'")
expect(configDefault).not.toContain('Config.PhoneConfigurator')
expect(configDefault).not.toContain('Config.CommandPermissions')
expect(configuratorServer).toContain( expect(configuratorServer).toContain(
'build_sections("config", stored_config', 'build_sections("config", stored_config',
) )
+83
View File
@@ -0,0 +1,83 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
function source(path: string): string {
return readFileSync(new URL(path, import.meta.url), 'utf8')
}
const config = source('../../sky_phone/config/config.lua')
const configDefault = source('../../sky_phone/source/shared/config_default.lua')
const framework = source('../../sky_phone/source/bridge/server/framework.lua')
const qbox = source('../../sky_phone/source/bridge/server/frameworks/qbox.lua')
const configurator = source(
'../../sky_phone/source/server/phone_configurator.lua',
)
const configuratorFixture = source('../testserver/configurator-fixture.cjs')
describe('fixed server permissions', () => {
it('defines every protected phone capability only in config.lua', () => {
expect(config).toContain('Config.CommandPermissions = {')
for (const permission of [
'phonepanel',
'phonetestdata',
'fliptokverify',
'picstagramverify',
'picstagramadmin',
]) {
expect(config).toMatch(new RegExp(`\\s${permission} = \\{`))
}
expect(config).not.toContain('AdminGroups =')
expect(configDefault).not.toContain('Config.PhoneConfigurator')
expect(configDefault).not.toContain('Config.CommandPermissions')
expect(configDefault).not.toContain('AdminGroups =')
})
it('keeps fixed permissions outside SQL and removes legacy group fields', () => {
expect(configurator).toContain('key ~= "CommandPermissions"')
expect(configurator).toContain('if key ~= "CommandPermissions" then')
expect(configuratorFixture).toContain('delete config.CommandPermissions')
for (const path of [
'AdminPanel.AdminGroups',
'TestData.AdminGroups',
'FlipTok.AdminGroups',
'Picstagram.AdminGroups',
]) {
expect(configurator).toContain(`["${path}"] = true`)
}
expect(configurator).toContain(
'Phone Configurator enabled: file-based settings from config.lua',
)
expect(configurator).toContain(
'(except Config.CommandPermissions) and media.lua are disabled',
)
})
it('authorizes Qbox through ACE before retaining framework group support', () => {
expect(framework).toContain(
'local groups = Config.CommandPermissions[permission]',
)
expect(qbox).toContain(
'if IsPlayerAceAllowed(tostring(source), group) then',
)
expect(qbox.indexOf('IsPlayerAceAllowed')).toBeLessThan(
qbox.indexOf('exports.qbx_core:HasGroup'),
)
})
it('uses stable permission identifiers for every protected operation', () => {
const expectations = [
['../../sky_phone/source/server/admin.lua', 'phonepanel'],
['../../sky_phone/source/server/testdata.lua', 'phonetestdata'],
['../../sky_phone/source/server/fliptok.lua', 'fliptokverify'],
['../../sky_phone/source/server/picstagram.lua', 'picstagramverify'],
['../../sky_phone/source/server/picstagram.lua', 'picstagramadmin'],
] as const
for (const [path, permission] of expectations) {
expect(source(path)).toContain(
`Bridge.Framework.HasPermission(source, "${permission}")`,
)
}
})
})
+12 -3
View File
@@ -52,7 +52,7 @@ describe('phone inventory contracts', () => {
) )
}) })
it('auto-detects registered inventories and limits metadata-free adapters', () => { it('auto-detects registered inventories and forces metadata-free adapters into compatible modes', () => {
const inventoryBridge = readResourceFile( const inventoryBridge = readResourceFile(
'source/bridge/server/inventory.lua', 'source/bridge/server/inventory.lua',
) )
@@ -65,8 +65,17 @@ describe('phone inventory contracts', () => {
) )
expect(inventoryBridge).toContain('configured_inventory = adapter.name') expect(inventoryBridge).toContain('configured_inventory = adapter.name')
expect(inventoryBridge).toContain('selected_adapter.metadata == false') expect(inventoryBridge).toContain('selected_adapter.metadata == false')
expect(inventoryBridge).toContain('Config.Phone.Unique ~= false') expect(inventoryBridge).toContain('Config.Phone.Unique = false')
expect(inventoryBridge).toContain('Config.Sim.Enabled ~= false') expect(inventoryBridge).toContain('Config.Sim.Enabled = false')
expect(inventoryBridge).toContain(
'does not support item metadata; unique phones and physical SIM cards were disabled automatically',
)
expect(inventoryBridge).toContain(
'AddEventHandler("sky_phone:configurator:serverUpdated"',
)
expect(inventoryBridge).not.toContain(
'cannot store unique phone or physical SIM metadata',
)
}) })
it('provides the LB IsOpen export alias from the authoritative client state', () => { it('provides the LB IsOpen export alias from the authoritative client state', () => {
-1
View File
@@ -945,7 +945,6 @@ const adminPanelFallbackLocales = {
entry: 'Entry', entry: 'Entry',
general: 'General', general: 'General',
subtabs: { subtabs: {
AdminGroups: 'Admin Groups',
Dictionaries: 'Dictionaries', Dictionaries: 'Dictionaries',
Clips: 'Clips', Clips: 'Clips',
Transforms: 'Transforms', Transforms: 'Transforms',
@@ -16,9 +16,7 @@ describe('admin configurator descriptions', () => {
expect(configuratorDescriptionKey('Media.RequestTimeoutMs', 10000)).toBe( expect(configuratorDescriptionKey('Media.RequestTimeoutMs', 10000)).toBe(
'milliseconds', 'milliseconds',
) )
expect(configuratorDescriptionKey('AdminPanel.AdminGroups', [])).toBe( expect(configuratorDescriptionKey('Radio.AllowedJobs', [])).toBe('access')
'access',
)
expect(configuratorDescriptionKey('FiveManage.ApiKey', '')).toBe( expect(configuratorDescriptionKey('FiveManage.ApiKey', '')).toBe(
'credential', 'credential',
) )
@@ -661,6 +661,7 @@ function loadConfiguratorSections() {
) )
const media = mediaRoot.Media const media = mediaRoot.Media
delete config.PhoneConfigurator delete config.PhoneConfigurator
delete config.CommandPermissions
delete config.Media delete config.Media
return [...buildSections('config', config), ...buildSections('media', media)] return [...buildSections('config', config), ...buildSections('media', media)]
} }
@@ -61,14 +61,19 @@ function countStructure(structure: ConfiguratorStructure | undefined): number {
} }
describe('admin configurator fixture', () => { describe('admin configurator fixture', () => {
it('exposes every config.lua root through the full live preview', () => { it('exposes every SQL-managed config.lua root through the full live preview', () => {
const sections = loadConfiguratorSections() const sections = loadConfiguratorSections()
const fields = sections.flatMap((section) => section.fields) const fields = sections.flatMap((section) => section.fields)
const roots = [ const roots = [
...configSource.matchAll(/^\s{0,4}Config\.([A-Za-z0-9_]+)\s*=/gm), ...configSource.matchAll(/^\s{0,4}Config\.([A-Za-z0-9_]+)\s*=/gm),
] ]
.map((match) => match[1]) .map((match) => match[1])
.filter((root) => root !== 'Media' && root !== 'PhoneConfigurator') .filter(
(root) =>
root !== 'Media' &&
root !== 'PhoneConfigurator' &&
root !== 'CommandPermissions',
)
expect(sections).toHaveLength(45) expect(sections).toHaveLength(45)
expect( expect(
+15 -2
View File
@@ -134,7 +134,7 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting
- `hex_4_inventory` - `hex_4_inventory`
- Native ESX inventory - Native ESX inventory
`mf-inventory` and `smx-inventory` are supported with ESX. The native ESX and HEX adapters use count-based items and therefore require unique phones and physical SIM cards to be disabled. `mf-inventory` and `smx-inventory` are supported with ESX. The native ESX and HEX adapters use count-based items, so Sky Phone automatically disables unique phones and physical SIM cards while either adapter is active.
### Voice ### Voice
@@ -196,6 +196,7 @@ The files contain clearly separated sections for:
| Section | Purpose | | Section | Purpose |
| --- | --- | | --- | --- |
| `Config.Bridge` | Framework, inventory, language, callback timeout, and debug mode | | `Config.Bridge` | Framework, inventory, language, callback timeout, and debug mode |
| `Config.CommandPermissions` | Fixed groups for the admin panel, test data, verification commands, and social moderation |
| `Config.Phone` | Phone item, movement, unique-device mode, and development command | | `Config.Phone` | Phone item, movement, unique-device mode, and development command |
| `Config.Sim` | Physical or virtual SIM behavior and number formatting | | `Config.Sim` | Physical or virtual SIM behavior and number formatting |
| `Config.Calls` / `Config.Radio` | Voice providers, call behavior, radio limits, and permissions | | `Config.Calls` / `Config.Radio` | Voice providers, call behavior, radio limits, and permissions |
@@ -211,6 +212,18 @@ The files contain clearly separated sections for:
Restart `sky_phone` after changing Lua configuration. Restart `sky_phone` after changing Lua configuration.
When `Config.PhoneConfigurator.Enabled` is enabled, the generated `source/shared/config_default.lua`
provides the shipped SQL baseline. The frontend build recreates it from `config.lua` and the
server-only `media.lua`; do not edit the generated snapshot directly.
`Config.PhoneConfigurator` and `Config.CommandPermissions` remain file-owned and are omitted from
the generated default snapshot. Permissions are not displayed in the Phone Configurator and stay
authoritative while SQL configuration is enabled. Their stable keys do not change when a command is
renamed in the panel. ESX and QBCore use their framework permissions. Qbox checks the configured ACE
objects first and then its framework groups, so the standard `permissions.cfg` mapping from
`group.admin` to the `admin` ACE works with the shipped `phonepanel` permission list. Restart
`sky_phone` after changing fixed permissions.
### Language ### Language
Available locales: Available locales:
@@ -312,7 +325,7 @@ Do not configure an LB Phone client event or client export. Sky Phone registers
The server registers `Config.Phone.Item` as usable for every supported inventory adapter: `ox`, `qb`, `lj`, `qs`, `codem`, `core`, `mf`, `smx`, `hex`, and `esx`. Resource startup fails visibly if the selected adapter cannot complete that registration. The server registers `Config.Phone.Item` as usable for every supported inventory adapter: `ox`, `qb`, `lj`, `qs`, `codem`, `core`, `mf`, `smx`, `hex`, and `esx`. Resource startup fails visibly if the selected adapter cannot complete that registration.
The `hex` and `esx` adapters use ESX's count-based item API. They require both `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` because this API cannot persist per-item phone or physical SIM metadata. `auto` selects `hex` when `hex_4_inventory` is started and otherwise falls back to `esx` on an ESX server when no metadata-capable inventory is detected. The `hex` and `esx` adapters use ESX's count-based item API, which cannot persist per-item phone or physical SIM metadata. Sky Phone automatically forces `Config.Phone.Unique = false` and `Config.Sim.Enabled = false` while either adapter is active. `auto` selects `hex` when `hex_4_inventory` is started and otherwise falls back to `esx` on an ESX server when no metadata-capable inventory is detected.
### QBCore-style item tables ### QBCore-style item tables
+19 -8
View File
@@ -10,12 +10,27 @@
Keep option names unchanged. Restart sky_phone after editing this file. Keep option names unchanged. Restart sky_phone after editing this file.
]] ]]
-- When enabled, config.lua and media.lua only provide first-run defaults. -- CONFIG_DEFAULT_EXCLUDE_START
-- The active configuration is loaded from SQL and managed through /phonepanel. -- When enabled, the active configuration is loaded from SQL and managed through
-- /phonepanel. Frontend builds snapshot the shipped defaults from config.lua and
-- media.lua into source/shared/config_default.lua.
Config.PhoneConfigurator = { Config.PhoneConfigurator = {
Enabled = true, Enabled = true,
} }
-- Fixed server permissions. These values remain authoritative even while the
-- Phone Configurator is enabled and are intentionally not shown in its panel.
-- Group names use the active framework's permissions. On Qbox they also match
-- ACE objects such as "admin" from the standard permissions.cfg.
Config.CommandPermissions = {
phonepanel = { "god", "superadmin", "admin" },
phonetestdata = { "god", "superadmin", "admin" },
fliptokverify = { "god", "superadmin", "admin" },
picstagramverify = { "god", "superadmin", "admin" },
picstagramadmin = { "god", "superadmin", "admin" },
}
-- CONFIG_DEFAULT_EXCLUDE_END
-- ============================================================================= -- =============================================================================
-- Core, framework and device -- Core, framework and device
-- ============================================================================= -- =============================================================================
@@ -34,7 +49,7 @@ Config.Command = "phone"
Config.Phone = { Config.Phone = {
Item = "phone", Item = "phone",
Unique = true, -- true: data follows each phone item; false: one persistent phone per character; hex/esx require false Unique = true, -- true: data follows each phone item; false: one persistent phone per character; forced false for metadata-free inventories
Keybind = "F1", -- false disables the configurable phone key mapping Keybind = "F1", -- false disables the configurable phone key mapping
AllowMovement = true, -- true: game input stays active while the mobile phone is open AllowMovement = true, -- true: game input stays active while the mobile phone is open
HoldToLook = { HoldToLook = {
@@ -49,7 +64,6 @@ Config.TestData = {
Enabled = false, -- development/test servers only; keep disabled in production Enabled = false, -- development/test servers only; keep disabled in production
Command = "phonetestdata", Command = "phonetestdata",
AdminOnly = false, -- enable only on development servers; every run is scoped to the executing player's phone AdminOnly = false, -- enable only on development servers; every run is scoped to the executing player's phone
AdminGroups = { "admin", "superadmin" },
} }
Config.CustomApps = { Config.CustomApps = {
@@ -76,7 +90,6 @@ Config.Security = {
Config.AdminPanel = { Config.AdminPanel = {
Enabled = true, Enabled = true,
Command = "phonepanel", Command = "phonepanel",
AdminGroups = { "admin", "superadmin" },
MaximumPlayers = 128, MaximumPlayers = 128,
ReadRequestsPerMinute = 60, ReadRequestsPerMinute = 60,
ActionRequestsPerMinute = 30, ActionRequestsPerMinute = 30,
@@ -86,7 +99,7 @@ Config.AdminPanel = {
} }
Config.Sim = { Config.Sim = {
Enabled = true, -- false: devices receive a persistent random number automatically; hex/esx require false Enabled = true, -- false: devices receive a persistent random number automatically; forced false for metadata-free inventories
RegisteredItem = "sky_phone_sim_registered", RegisteredItem = "sky_phone_sim_registered",
AnonymousItem = "sky_phone_sim_anonymous", AnonymousItem = "sky_phone_sim_anonymous",
NumberLength = 10, -- total number of digits, including NumberPrefix NumberLength = 10, -- total number of digits, including NumberPrefix
@@ -485,7 +498,6 @@ Config.FlipTok = {
MaxPostMedia = 10, MaxPostMedia = 10,
MusicTracks = {}, MusicTracks = {},
VerifyCommand = "fliptokverify", VerifyCommand = "fliptokverify",
AdminGroups = { "admin" },
ReportWebhookConvar = "sky_phone_fliptok_report_webhook", ReportWebhookConvar = "sky_phone_fliptok_report_webhook",
} }
@@ -507,7 +519,6 @@ Config.Picstagram = {
ReportDetailsMaxLength = 500, ReportDetailsMaxLength = 500,
ReportReasons = { "spam", "harassment", "dangerous", "illegal", "other" }, ReportReasons = { "spam", "harassment", "dangerous", "illegal", "other" },
VerifyCommand = "picstagramverify", VerifyCommand = "picstagramverify",
AdminGroups = { "admin" },
} }
Config.Feather = { Config.Feather = {
-1
View File
@@ -2139,7 +2139,6 @@ Locales["de"] = {
} }
Locales["de"].Nui.AdminPanel.configurator.table.subtabs = { Locales["de"].Nui.AdminPanel.configurator.table.subtabs = {
AdminGroups = "Admin-Gruppen",
Dictionaries = "Animationsdateien", Dictionaries = "Animationsdateien",
Clips = "Clips", Clips = "Clips",
Transforms = "Transformationen", Transforms = "Transformationen",
-1
View File
@@ -2139,7 +2139,6 @@ Locales["en"] = {
} }
Locales["en"].Nui.AdminPanel.configurator.table.subtabs = { Locales["en"].Nui.AdminPanel.configurator.table.subtabs = {
AdminGroups = "Admin Groups",
Dictionaries = "Dictionaries", Dictionaries = "Dictionaries",
Clips = "Clips", Clips = "Clips",
Transforms = "Transforms", Transforms = "Transforms",
-1
View File
@@ -2139,7 +2139,6 @@ Locales["es"] = {
} }
Locales["es"].Nui.AdminPanel.configurator.table.subtabs = { Locales["es"].Nui.AdminPanel.configurator.table.subtabs = {
AdminGroups = "Grupos de administradores",
Dictionaries = "Diccionarios", Dictionaries = "Diccionarios",
Clips = "Los clips", Clips = "Los clips",
Transforms = "Transformaciones", Transforms = "Transformaciones",
+10 -4
View File
@@ -2,19 +2,25 @@
Sky Phone media configuration Sky Phone media configuration
This file is loaded on the server only. Keep API keys private. When the This file is loaded on the server only. Keep API keys private. When the
phone configurator is enabled in config.lua, these values are first-run phone configurator is enabled in config.lua, the active media configuration
defaults and the active media configuration is loaded from SQL. is loaded from SQL and frontend builds copy the shipped values from this
file into source/shared/config_default.lua.
]] ]]
Config.Media = { Config.Media = {
GiphyApiKey = "", -- Paste the GIPHY API key here. -- Create or sign in to an account at https://developers.giphy.com/, open
-- the Developer Dashboard, choose "Create an API Key", and paste it here.
GiphyApiKey = "",
GifPageSize = 24, GifPageSize = 24,
GifRating = "pg-13", GifRating = "pg-13",
UrlMaxLength = 2048, UrlMaxLength = 2048,
AllowedGifHosts = { "giphy.com" }, AllowedGifHosts = { "giphy.com" },
-- Create or sign in at https://fivemanage.com/, create or select your team,
-- then open Dashboard > Tokens and create a token with Media access. Keep
-- this server-only token private and paste it here.
FiveManage = { FiveManage = {
ApiKey = "", -- Paste a newly generated FiveManage V3 Media API token here. ApiKey = "",
BaseUrl = "https://api.fivemanage.com/api/v3/file", BaseUrl = "https://api.fivemanage.com/api/v3/file",
RequestTimeoutMs = 10000, RequestTimeoutMs = 10000,
UploadTimeoutMs = 25000, UploadTimeoutMs = 25000,
+1
View File
@@ -79,6 +79,7 @@ server_scripts {
'config/locales/en.lua', 'config/locales/en.lua',
'config/locales/de.lua', 'config/locales/de.lua',
'config/locales/es.lua', 'config/locales/es.lua',
'source/shared/config_default.lua',
'source/server/nui_build_check.lua', 'source/server/nui_build_check.lua',
'source/server/update_check.lua', 'source/server/update_check.lua',
'source/bridge/server/database.lua', 'source/bridge/server/database.lua',
@@ -19,3 +19,23 @@ Bridge.Framework.Name = configured_framework
function Bridge.Framework.GetName() function Bridge.Framework.GetName()
return Bridge.Framework.Name return Bridge.Framework.Name
end end
function Bridge.Framework.HasPermission(source, permission)
if type(permission) ~= "string" or permission == "" then
error("[sky_phone] Permission identifiers must be non-empty strings.")
end
local groups = Config.CommandPermissions[permission]
if type(groups) ~= "table" or #groups == 0 then
local message = "[sky_phone] Config.CommandPermissions.%s must contain at least one group."
error(message:format(permission))
end
for index, group in ipairs(groups) do
if type(group) ~= "string" or group == "" then
local message = "[sky_phone] Config.CommandPermissions.%s[%s] must be a non-empty string."
error(message:format(permission, index))
end
end
return Bridge.Framework.HasAdminGroup(source, groups)
end
@@ -24,6 +24,13 @@ function Bridge.Framework.HasAdminGroup(source, groups)
if not player then if not player then
return false return false
end end
for _, group in ipairs(groups) do
if IsPlayerAceAllowed(tostring(source), group) then
return true
end
end
return exports.qbx_core:HasGroup(source, groups) return exports.qbx_core:HasGroup(source, groups)
end end
+21 -4
View File
@@ -61,13 +61,30 @@ end
Bridge.Inventory.Name = configured_inventory Bridge.Inventory.Name = configured_inventory
if selected_adapter.metadata == false then local metadata_free_inventory = selected_adapter.metadata == false
if Config.Phone.Unique ~= false or Config.Sim.Enabled ~= false then
Bridge.Inventory.ConfigurationError = ("[sky_phone] Inventory '%s' cannot store unique phone or physical SIM metadata. Set Config.Phone.Unique = false and Config.Sim.Enabled = false, or configure a metadata-capable inventory.") local function enforce_metadata_compatibility()
:format(configured_inventory) if not metadata_free_inventory then
return
end
local modes_changed = Config.Phone.Unique ~= false or Config.Sim.Enabled ~= false
Config.Phone.Unique = false
Config.Sim.Enabled = false
if modes_changed then
Bridge.Debug(
"warn",
"[sky_phone] Inventory '%s' does not support item metadata; unique phones and physical SIM cards were disabled automatically.",
configured_inventory
)
end end
end end
enforce_metadata_compatibility()
AddEventHandler("sky_phone:configurator:serverUpdated", enforce_metadata_compatibility)
function Bridge.Inventory.NormalizeItem(item, metadata_field, fallback_slot) function Bridge.Inventory.NormalizeItem(item, metadata_field, fallback_slot)
if type(item) ~= "table" then if type(item) ~= "table" then
return nil return nil
+2 -2
View File
@@ -98,7 +98,7 @@ end
local function require_admin(source, operation, maximum) local function require_admin(source, operation, maximum)
if not Config.AdminPanel.Enabled if not Config.AdminPanel.Enabled
or not Bridge.Framework.HasAdminGroup(source, Config.AdminPanel.AdminGroups) or not Bridge.Framework.HasPermission(source, "phonepanel")
then then
Bridge.Debug("warn", "[sky_phone] Rejected admin panel access from source %s.", tostring(source)) Bridge.Debug("warn", "[sky_phone] Rejected admin panel access from source %s.", tostring(source))
return nil, { success = false, error = "not_authorized" } return nil, { success = false, error = "not_authorized" }
@@ -121,7 +121,7 @@ local function run_admin_command(command_source)
TriggerClientEvent("sky_phone:admin:command-error", player_source, "disabled") TriggerClientEvent("sky_phone:admin:command-error", player_source, "disabled")
return return
end end
if not Bridge.Framework.HasAdminGroup(player_source, Config.AdminPanel.AdminGroups) then if not Bridge.Framework.HasPermission(player_source, "phonepanel") then
Bridge.Debug( Bridge.Debug(
"warn", "warn",
"[sky_phone] Rejected admin panel command from source %s.", "[sky_phone] Rejected admin panel command from source %s.",
+1 -1
View File
@@ -943,7 +943,7 @@ local function run_verify_command(source, arguments)
notificationType = notification_type, notificationType = notification_type,
}) })
end end
if source ~= 0 and not Bridge.Framework.HasAdminGroup(source, Config.FlipTok.AdminGroups) then if source ~= 0 and not Bridge.Framework.HasPermission(source, "fliptokverify") then
send_command_feedback(command_locale.noPermission, "error") send_command_feedback(command_locale.noPermission, "error")
Bridge.Debug( Bridge.Debug(
"warn", "warn",
+45 -16
View File
@@ -17,6 +17,21 @@ local updated_at
local updated_by_name local updated_by_name
local is_sequence local is_sequence
local FIXED_CONFIG_PATHS = {
CommandPermissions = true,
["AdminPanel.AdminGroups"] = true,
["TestData.AdminGroups"] = true,
["FlipTok.AdminGroups"] = true,
["Picstagram.AdminGroups"] = true,
}
if configurator_enabled then
print(
"[sky_phone] Phone Configurator enabled: file-based settings from config.lua " ..
"(except Config.CommandPermissions) and media.lua are disabled; SQL configuration is active."
)
end
local CLIENT_CONFIG_KEYS = { local CLIENT_CONFIG_KEYS = {
AdminPanel = true, AdminPanel = true,
Animations = true, Animations = true,
@@ -252,7 +267,7 @@ local function upgrade_legacy_map(defaults, saved)
return { __skyType = "map", entries = entries } return { __skyType = "map", entries = entries }
end end
local function merge_values(defaults, saved, path) local function merge_values(defaults, saved, path, excluded_paths)
path = path or "" path = path or ""
if type(defaults) ~= "table" or type(saved) ~= "table" then if type(defaults) ~= "table" or type(saved) ~= "table" then
return copy_value(saved) return copy_value(saved)
@@ -278,7 +293,12 @@ local function merge_values(defaults, saved, path)
key = entry.key, key = entry.key,
keyType = entry.keyType, keyType = entry.keyType,
value = saved_entry value = saved_entry
and merge_values(entry.value, saved_entry.value, path .. "." .. tostring(entry.key)) and merge_values(
entry.value,
saved_entry.value,
path .. "." .. tostring(entry.key),
excluded_paths
)
or copy_value(entry.value), or copy_value(entry.value),
} }
included[identity] = true included[identity] = true
@@ -298,7 +318,7 @@ local function merge_values(defaults, saved, path)
local merged = copy_value(saved) local merged = copy_value(saved)
for index, child in ipairs(defaults) do for index, child in ipairs(defaults) do
merged[index] = saved[index] ~= nil merged[index] = saved[index] ~= nil
and merge_values(child, saved[index], path .. "." .. tostring(index)) and merge_values(child, saved[index], path .. "." .. tostring(index), excluded_paths)
or copy_value(child) or copy_value(child)
end end
return merged return merged
@@ -309,11 +329,13 @@ local function merge_values(defaults, saved, path)
local merged = copy_value(defaults) local merged = copy_value(defaults)
for key, child in pairs(saved) do for key, child in pairs(saved) do
if merged[key] ~= nil then local child_path = path == "" and tostring(key) or (path .. "." .. tostring(key))
local child_path = path == "" and tostring(key) or (path .. "." .. tostring(key)) if not excluded_paths or not excluded_paths[child_path] then
merged[key] = merge_values(merged[key], child, child_path) if merged[key] ~= nil then
else merged[key] = merge_values(merged[key], child, child_path, excluded_paths)
merged[key] = copy_value(child) else
merged[key] = copy_value(child)
end
end end
end end
return merged return merged
@@ -356,10 +378,12 @@ local function apply_runtime_configuration()
local runtime_config = deserialize_value(stored_config) local runtime_config = deserialize_value(stored_config)
for key, value in pairs(runtime_config) do for key, value in pairs(runtime_config) do
if type(Config[key]) == "table" and type(value) == "table" then if key ~= "CommandPermissions" then
apply_runtime_table(Config[key], value) if type(Config[key]) == "table" and type(value) == "table" then
else apply_runtime_table(Config[key], value)
Config[key] = value else
Config[key] = value
end
end end
end end
local runtime_media = deserialize_value(stored_media) local runtime_media = deserialize_value(stored_media)
@@ -1032,7 +1056,12 @@ local function read_stored_row()
end end
local function apply_stored_row(row) local function apply_stored_row(row)
stored_config = merge_values(default_config, decode_payload(row.config_payload, "config"), "") stored_config = merge_values(
default_config,
decode_payload(row.config_payload, "config"),
"",
FIXED_CONFIG_PATHS
)
stored_media = merge_values(default_media, decode_payload(row.media_payload, "media"), "") stored_media = merge_values(default_media, decode_payload(row.media_payload, "media"), "")
revision = tonumber(row.revision) or 1 revision = tonumber(row.revision) or 1
updated_at = row.updated_at updated_at = row.updated_at
@@ -1040,12 +1069,12 @@ local function apply_stored_row(row)
end end
default_config = {} default_config = {}
for key, value in pairs(Config) do for key, value in pairs(ConfigDefaults) do
if key ~= "Media" and key ~= "PhoneConfigurator" then if key ~= "Media" and key ~= "PhoneConfigurator" and key ~= "CommandPermissions" then
default_config[key] = serialize_value(value) default_config[key] = serialize_value(value)
end end
end end
default_media = serialize_value(Config.Media) default_media = serialize_value(ConfigDefaults.Media)
Bridge.Database.Migrate("sky_phone_configurator", { SkyPhoneConfiguratorSchema }) Bridge.Database.Migrate("sky_phone_configurator", { SkyPhoneConfiguratorSchema })
Bridge.Database.Query(([[ Bridge.Database.Query(([[
+2 -2
View File
@@ -319,7 +319,7 @@ local function create_activity(recipient_id, actor_id, kind, post_id)
end end
local function is_admin(source) local function is_admin(source)
return Bridge.Framework.HasAdminGroup(source, Config.Picstagram.AdminGroups) return Bridge.Framework.HasPermission(source, "picstagramadmin")
end end
Bridge.Callbacks.Register("sky_phone:picstagram:register", function(source, data) Bridge.Callbacks.Register("sky_phone:picstagram:register", function(source, data)
@@ -1464,7 +1464,7 @@ local function run_verify_command(source, args)
notificationType = notification_type, notificationType = notification_type,
}) })
end end
if source ~= 0 and not Bridge.Framework.HasAdminGroup(source, Config.Picstagram.AdminGroups) then if source ~= 0 and not Bridge.Framework.HasPermission(source, "picstagramverify") then
send_command_feedback(command_locale.noPermission, "error") send_command_feedback(command_locale.noPermission, "error")
Bridge.Debug( Bridge.Debug(
"warn", "warn",
+1 -1
View File
@@ -1045,7 +1045,7 @@ local function run_test_data_command(source)
Bridge.Debug("warn", "[sky_phone] The test data command must be run by an in-game player.") Bridge.Debug("warn", "[sky_phone] The test data command must be run by an in-game player.")
return return
end end
if Config.TestData.AdminOnly and not Bridge.Framework.HasAdminGroup(source, Config.TestData.AdminGroups) then if Config.TestData.AdminOnly and not Bridge.Framework.HasPermission(source, "phonetestdata") then
Bridge.Debug("warn", "[sky_phone] Source %s attempted to run the restricted test data command.", tostring(source)) Bridge.Debug("warn", "[sky_phone] Source %s attempted to run the restricted test data command.", tostring(source))
TriggerClientEvent("sky_phone:testdata:feedback", source, false) TriggerClientEvent("sky_phone:testdata:feedback", source, false)
return return
File diff suppressed because it is too large Load Diff
+64
View File
@@ -0,0 +1,64 @@
Config = {
Bridge = {
Framework = "qbox",
},
CommandPermissions = {
phonepanel = { "god", "superadmin", "admin" },
},
}
Bridge = {
Framework = {},
}
function GetResourceState(resource)
assert(resource == "qbx_core")
return "started"
end
function GetPlayers()
return { "1" }
end
local ace_permissions = {}
local framework_permission = false
function IsPlayerAceAllowed(source, permission)
assert(source == "1")
return ace_permissions[permission] == true
end
exports = {
qbx_core = {
GetPlayer = function(_, source)
if tonumber(source) ~= 1 then
return nil
end
return {
PlayerData = {
citizenid = "test-citizen",
},
}
end,
HasGroup = function(_, source, groups)
assert(source == 1)
assert(groups == Config.CommandPermissions.phonepanel)
return framework_permission
end,
},
}
dofile("sky_phone/source/bridge/server/framework.lua")
dofile("sky_phone/source/bridge/server/frameworks/qbox.lua")
ace_permissions.admin = true
assert(Bridge.Framework.HasPermission(1, "phonepanel"), "Qbox ACE permissions must grant access")
ace_permissions.admin = false
framework_permission = true
assert(Bridge.Framework.HasPermission(1, "phonepanel"), "Qbox framework groups must remain supported")
local success = pcall(Bridge.Framework.HasPermission, 1, "missing")
assert(not success, "Missing fixed permission definitions must fail loudly")
print("framework permission tests passed")
+18 -5
View File
@@ -1,3 +1,9 @@
local event_handlers = {}
AddEventHandler = function(event_name, callback)
event_handlers[event_name] = callback
end
local function reset_bridge(inventory_name, unique_phones, sim_cards_enabled) local function reset_bridge(inventory_name, unique_phones, sim_cards_enabled)
Config = { Config = {
Bridge = { Bridge = {
@@ -193,11 +199,18 @@ exports = {
} }
load_inventory_contract("sky_phone/source/bridge/server/inventory/esx.lua") load_inventory_contract("sky_phone/source/bridge/server/inventory/esx.lua")
local ok, configuration_error = pcall(Bridge.Inventory.RegisterUsableItem, "phone", function() assert(Config.Phone.Unique == false)
end) assert(Config.Sim.Enabled == false)
assert(not ok) assert(Bridge.Inventory.ConfigurationError == nil)
assert(configuration_error:find("Config.Phone.Unique = false", 1, true))
assert(configuration_error:find("Config.Sim.Enabled = false", 1, true)) Config.Phone.Unique = true
Config.Sim.Enabled = true
event_handlers["sky_phone:configurator:serverUpdated"]()
assert(Config.Phone.Unique == false)
assert(Config.Sim.Enabled == false)
assert(Bridge.Inventory.RegisterUsableItem("phone", function()
end))
local manifest_file = assert(io.open("sky_phone/fxmanifest.lua", "rb")) local manifest_file = assert(io.open("sky_phone/fxmanifest.lua", "rb"))
local manifest = manifest_file:read("*a") local manifest = manifest_file:read("*a")
+3
View File
@@ -1,3 +1,6 @@
AddEventHandler = function()
end
local function reset_bridge(inventory_name, resource_name, inventory_export, framework_name, extra_exports) local function reset_bridge(inventory_name, resource_name, inventory_export, framework_name, extra_exports)
Config = { Config = {
Bridge = { Inventory = inventory_name }, Bridge = { Inventory = inventory_name },
+1 -2
View File
@@ -25,7 +25,7 @@ Bridge = {
Debug = function() Debug = function()
end, end,
Framework = { Framework = {
HasAdminGroup = function() HasPermission = function()
return true return true
end, end,
}, },
@@ -42,7 +42,6 @@ Bridge = {
Config = { Config = {
AdminPanel = { AdminPanel = {
AdminGroups = { "admin" },
Command = "phoneadmin", Command = "phoneadmin",
Enabled = true, Enabled = true,
}, },