From a597e161ea6b8a770931a44a0244f912768f9996 Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" <159480018+leonw21342315@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:46:39 +0200 Subject: [PATCH] ADD - integrate TGIANN House (#47) Authorize owner properties from the documented TGIANN schema and enrich entrance waypoints through the published client export. Keep unsupported key, lock, CCTV, and garage capabilities disabled. --- README.md | 4 +- .../src/housingProviders.contract.test.ts | 63 +++++++++ sky_phone/config/config.lua | 4 +- .../source/bridge/client/housing/tgiann.lua | 73 +++++++++++ .../source/bridge/server/housing/tgiann.lua | 124 ++++++++++++++++++ sky_phone/source/shared/config_default.lua | 4 +- 6 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 frontend/src/housingProviders.contract.test.ts create mode 100644 sky_phone/source/bridge/client/housing/tgiann.lua create mode 100644 sky_phone/source/bridge/server/housing/tgiann.lua diff --git a/README.md b/README.md index 7606ef3..902ee91 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Sky Phone is built to be the **free FiveM phone you can choose without accepting | **Inventories** | ak47_inventory, codem-inventory, core_inventory, jaksam_inventory, jpr-inventory, lj-inventory, mf-inventory, one_inventory, origen_inventory, ox_inventory, ps-inventory, qb-inventory, qs-inventory, smx-inventory, tgiann-inventory, hex_4_inventory, and native ESX inventory | | **Calls** | YACA, PMA Voice, SaltyChat | | **Radio** | YACA, PMA Voice, SaltyChat | -| **Housing** | RTX Housing, Quasar Housing, VMS Housing, RX Housing, NoLag Properties, SN Properties, ESX Property, qbx_properties | +| **Housing** | RTX Housing, Quasar Housing, TGIANN House, VMS Housing, RX Housing, NoLag Properties, SN Properties, ESX Property, qbx_properties | | **Garages** | Built-in/custom data and a broad set of popular garage providers configured through the bridge | | **Custom app contracts** | Sky Phone, LB Phone, 17Movement, High Phone, Quasar Smartphone, YSeries | | **Languages** | English, German | @@ -650,7 +650,7 @@ Select the provider under `Config.Garage.System`. Vehicle images use the configu ### Housing -Select `rtx`, `quasar`, `vms`, `rx`, `nolag`, `sn`, `esx_property`, or `qbx_properties` under `Config.Housing.System`. Automatic mode uses `Config.Housing.AutoPriority` and keeps the existing `esx_property` and `qbx_properties` defaults ahead of newly supported providers. Select a provider explicitly when multiple housing resources are running. Each bridge exposes only the capabilities supported by the documented provider API. +Select `rtx`, `quasar`, `tgiann`, `vms`, `rx`, `nolag`, `sn`, `esx_property`, or `qbx_properties` under `Config.Housing.System`. Automatic mode uses `Config.Housing.AutoPriority` and keeps the existing `esx_property` and `qbx_properties` defaults ahead of newly supported providers. Select a provider explicitly when multiple housing resources are running. Each bridge exposes only the capabilities supported by the documented provider API. The TGIANN bridge expects `tgiann-core` to start before `tgiann-house`, lists server-authorized owner properties, and supports entrance waypoints; TGIANN does not publish stable external contracts for keyholders, lock controls, CCTV, or live garage status. ### Companies diff --git a/frontend/src/housingProviders.contract.test.ts b/frontend/src/housingProviders.contract.test.ts new file mode 100644 index 0000000..172b887 --- /dev/null +++ b/frontend/src/housingProviders.contract.test.ts @@ -0,0 +1,63 @@ +import { readFileSync } from 'node:fs' + +import { describe, expect, it } from 'vitest' + +const readResourceFile = (path: string) => + readFileSync( + new URL(`../../sky_phone/${path}`, import.meta.url), + 'utf8', + ).replace(/\r\n/g, '\n') + +describe('housing provider contracts', () => { + it('registers TGIANN House with server-authorized owner properties', () => { + const adapter = readResourceFile('source/bridge/server/housing/tgiann.lua') + + expect(adapter).toContain('local resource_name = "tgiann-house"') + expect(adapter).toContain('FROM `tgiann_house`') + expect(adapter).toContain('WHERE `owner` = ?') + expect(adapter).toContain('Bridge.Framework.GetIdentifier(source)') + expect(adapter).toContain( + 'Bridge.Housing.RegisterProvider(provider_name, {', + ) + expect(adapter).toContain('return nil, "property_access_denied"') + expect(adapter).not.toContain('`houseKeys`') + expect(adapter).not.toContain('tgiann-house:getPlayerHouses') + }) + + it('uses the published client export for entrance waypoints only', () => { + const adapter = readResourceFile('source/bridge/client/housing/tgiann.lua') + + expect(adapter).toContain( + 'return exports[resource_name]:getHouseData(house)', + ) + expect(adapter).toContain( + 'Bridge.Normalize.Coordinates(house_data.doorCoord)', + ) + expect(adapter).toContain( + 'Bridge.Housing.RegisterClientProvider(provider_name, {', + ) + expect(adapter).toContain('SetNewWaypoint(') + expect(adapter).not.toContain('enterHouse(') + expect(adapter).not.toContain('forceOpenDoorHouse') + }) + + it('advertises TGIANN in housing configuration and documentation', () => { + const config = readResourceFile('config/config.lua') + const readme = readFileSync( + new URL('../../README.md', import.meta.url), + 'utf8', + ) + + expect(config).toContain( + '"rtx", "quasar", "tgiann", "vms", "rx", "nolag", "sn"', + ) + expect(readme).toContain('Quasar Housing, TGIANN House, VMS Housing') + expect(readme).toContain('`quasar`, `tgiann`, `vms`') + expect(readme).toContain( + 'expects `tgiann-core` to start before `tgiann-house`', + ) + expect(readme).toContain( + 'lists server-authorized owner properties, and supports entrance waypoints', + ) + }) +}) diff --git a/sky_phone/config/config.lua b/sky_phone/config/config.lua index 3b1c588..6035ab0 100644 --- a/sky_phone/config/config.lua +++ b/sky_phone/config/config.lua @@ -450,8 +450,8 @@ Config.Garage = { } Config.Housing = { - System = "auto", -- auto, rtx, quasar, vms, rx, nolag, sn, esx_property, qbx_properties - AutoPriority = { "esx_property", "qbx_properties", "rtx", "quasar", "vms", "rx", "nolag", "sn" }, + System = "auto", -- auto, rtx, quasar, tgiann, vms, rx, nolag, sn, esx_property, qbx_properties + AutoPriority = { "esx_property", "qbx_properties", "rtx", "quasar", "tgiann", "vms", "rx", "nolag", "sn" }, MaximumProperties = 50, OverviewRequestsPerMinute = 30, ActionsPerMinute = 12, diff --git a/sky_phone/source/bridge/client/housing/tgiann.lua b/sky_phone/source/bridge/client/housing/tgiann.lua new file mode 100644 index 0000000..c4e6537 --- /dev/null +++ b/sky_phone/source/bridge/client/housing/tgiann.lua @@ -0,0 +1,73 @@ +local provider_name = "tgiann" +local resource_name = "tgiann-house" + +local function house_reference(value) + if type(value) ~= "string" then + return nil + end + return value ~= "" and value or nil +end + +local function house_entrance(house) + local success, house_data = pcall(function() + return exports[resource_name]:getHouseData(house) + end) + if not success then + Bridge.Debug( + "error", + "[sky_phone] tgiann-house:getHouseData failed for '%s': %s", + house, + tostring(house_data) + ) + return nil, "provider_error" + end + local entrance = type(house_data) == "table" + and Bridge.Normalize.Coordinates(house_data.doorCoord) or nil + if not entrance then + return nil, "invalid_coordinates" + end + return entrance +end + +local function enrich_overview(properties) + local result = {} + if GetResourceState(resource_name) ~= "started" or type(properties) ~= "table" then + return result + end + + for _, property in ipairs(properties) do + local house = type(property) == "table" and house_reference(property.providerId) or nil + if house and property.id == provider_name .. ":" .. house then + local entrance = house_entrance(house) + if entrance then + result[#result + 1] = { + id = property.id, + entrance = entrance, + } + end + end + end + return result +end + +Bridge.Housing.RegisterClientProvider(provider_name, { + enrich_overview = enrich_overview, + execute = function(action, data) + if GetResourceState(resource_name) ~= "started" then + return false, "provider_unavailable" + end + if action ~= "set_waypoint" then + return false, "capability_unavailable" + end + local house = type(data) == "table" and house_reference(data.providerId) or nil + if not house then + return false, "invalid_property" + end + local entrance, error_code = house_entrance(house) + if not entrance then + return false, error_code + end + SetNewWaypoint(entrance.x + 0.0, entrance.y + 0.0) + return true + end, +}) diff --git a/sky_phone/source/bridge/server/housing/tgiann.lua b/sky_phone/source/bridge/server/housing/tgiann.lua new file mode 100644 index 0000000..b0bdb17 --- /dev/null +++ b/sky_phone/source/bridge/server/housing/tgiann.lua @@ -0,0 +1,124 @@ +local provider_name = "tgiann" +local resource_name = "tgiann-house" + +local function query_owned_houses(identifier) + local success, properties = pcall(function() + return MySQL.query.await([[ + SELECT `name` + FROM `tgiann_house` + WHERE `owner` = ? + ORDER BY `name` ASC + ]], { identifier }) + end) + if not success or type(properties) ~= "table" then + Bridge.Debug("error", "[sky_phone] tgiann-house overview query failed: %s", tostring(properties)) + return nil + end + return properties +end + +local function owns_house(identifier, house) + local success, property = pcall(function() + return MySQL.single.await([[ + SELECT `name` + FROM `tgiann_house` + WHERE `name` = ? AND `owner` = ? + LIMIT 1 + ]], { house, identifier }) + end) + if not success then + Bridge.Debug("error", "[sky_phone] tgiann-house ownership query failed: %s", tostring(property)) + return nil, "provider_error" + end + return type(property) == "table" +end + +local function house_from_property_id(value) + if type(value) ~= "string" then + return nil + end + local house = value:match("^tgiann:(.+)$") + return house and house ~= "" and house or nil +end + +local function normalized_property(house) + return { + id = provider_name .. ":" .. house, + providerId = house, + name = house, + access = "owner", + locked = false, + capabilities = { + lock = false, + keys = false, + waypoint = true, + cctv = false, + garageStatus = false, + }, + cctv = { enabled = false }, + garage = nil, + keys = nil, + } +end + +Bridge.Housing.RegisterProvider(provider_name, { + resource_name = resource_name, + is_available = function() + return GetResourceState(resource_name) == "started" + end, + get_overview = function(source) + local identifier = Bridge.Framework.GetIdentifier(source) + if not identifier then + return nil, "housing_unavailable" + end + local properties = query_owned_houses(identifier) + if not properties then + return nil, "provider_error" + end + + local result = {} + local seen = {} + local maximum = math.max(0, math.floor(tonumber(Config.Housing.MaximumProperties) or 0)) + for _, property in ipairs(properties) do + if #result >= maximum then + break + end + local house = type(property.name) == "string" and property.name or nil + if house and house ~= "" and not seen[house] then + seen[house] = true + result[#result + 1] = normalized_property(house) + end + end + return result + end, + prepare = function(source, action, data) + if action == "toggle_lock" or action == "grant_key" or action == "revoke_key" + or action == "key_candidates" + then + return nil, "capability_unavailable" + end + if action == "open_cctv" then + return nil, "cctv_unavailable" + end + if action ~= "set_waypoint" then + return nil, "invalid_action" + end + + local house = house_from_property_id(data and data.propertyId) + if not house then + return nil, "invalid_property" + end + local identifier = Bridge.Framework.GetIdentifier(source) + if not identifier then + return nil, "housing_unavailable" + end + local owned, error_code = owns_house(identifier, house) + if owned == nil then + return nil, error_code + end + if not owned then + return nil, "property_access_denied" + end + return { providerId = house } + end, +}) diff --git a/sky_phone/source/shared/config_default.lua b/sky_phone/source/shared/config_default.lua index bfa55c2..7f833ab 100644 --- a/sky_phone/source/shared/config_default.lua +++ b/sky_phone/source/shared/config_default.lua @@ -435,8 +435,8 @@ Config.Garage = { } Config.Housing = { - System = "auto", -- auto, rtx, quasar, vms, rx, nolag, sn, esx_property, qbx_properties - AutoPriority = { "esx_property", "qbx_properties", "rtx", "quasar", "vms", "rx", "nolag", "sn" }, + System = "auto", -- auto, rtx, quasar, tgiann, vms, rx, nolag, sn, esx_property, qbx_properties + AutoPriority = { "esx_property", "qbx_properties", "rtx", "quasar", "tgiann", "vms", "rx", "nolag", "sn" }, MaximumProperties = 50, OverviewRequestsPerMinute = 30, ActionsPerMinute = 12,