# Интеграция CXG-GTFAT с другими ресурсами

Файлы `bridge/*.lua` — это редактируемые точки для адаптации разрешений, хранения данных, уведомлений, станций и форматов внешности. Патчи меню — ручные изменения для конкретных версий; при установке ресурс не изменяет другие ресурсы.

## Меню внешности

Патчи подготовлены для следующих репозиториев и ревизий:

| Меню | Исходная ревизия | Дополнительное действие |
| --- | --- | --- |
| illenium-appearance | 1b003ec169b15f145d519438ba7c6454bf746f27 | Примените Lua-патч сохранения компонентов. |
| fivem-appearance | b06da32881ed49042909b38a778c10dd9bd9eaed | Примените патч и пересоберите игровой TypeScript bundle с помощью `build:game`. |
| qb-clothing | 8cca4009dd473ab30c30e443ecad50830b816ed7 | Примените патч к сохранению скина и комплекта. |
| esx_skin / skinchanger | fe59ca0bd6da59e2ec6eb4a8d06ece312e96ae7a | Путь патча отсчитывается от корня репозитория esx_core. |

В папке `integrations` пакета находятся `illenium-appearance.patch`, `fivem-appearance.patch`, `qb-clothing.patch` и `skinchanger.patch`. Они не применяются автоматически. В checkout меню для разработки проверьте нужный патч перед применением:

```sh
git apply --check /ruta/al/parche-correcto.patch
git apply /ruta/al/parche-correcto.patch
```

Если проверка не прошла, остановитесь и адаптируйте изменение к установленной версии; не применяйте патч принудительно. Форки и новые ревизии могут отличаться. Для патча fivem-appearance требуется пересобрать bundle по инструкции этого меню с помощью `build:game`. Патчи не сертифицируют все версии и не заменяют проверку на сервере.

При сохранении технический decal очищается, чтобы меню не записало его как обычный выбор игрока. Меню со своей таблицей внешности должны вызывать очистку в точке сохранения. Вес хранится отдельно от комплектов.

## Сохранение собственной внешности

Клиентский export `SanitizeAppearance` возвращает копию внешности. Поддерживаются форматы components, qb и esx:

```lua
local appearanceCopy = exports['CXG_GTFAT']:SanitizeAppearance(
    ped,
    appearance,
    'components'
)

GuardarApariencia(appearanceCopy)
```

`GuardarApariencia` — пример вызова; замените его функцией сохранения меню. components принимает массив компонентов или объект со свойством `components`; каждый компонент использует `component_id`, `drawable`, `texture` и необязательно `palette`. Формат qb использует поле `decals` с `item` и `texture`; esx использует `decals_1` и `decals_2`. Export возвращает глубокую копию, сохраняет остальные поля и не меняет ped временно. Если адаптер формата не сработал или формат не распознан, он возвращает неизменённую копию и записывает диагностическое сообщение.

## Регистрация preview ped

Если меню использует ped для предварительного просмотра отдельно от персонажа, зарегистрируйте preview активного персонажа. Снимите регистрацию до удаления сущности:

```lua
local previewPed = ObtenerPreviewActual()

if previewPed and DoesEntityExist(previewPed) then
    exports['CXG_GTFAT']:RegisterPreviewPed(previewPed)
end

-- Al cerrar o cancelar el menú, antes de eliminar el ped:
exports['CXG_GTFAT']:UnregisterPreviewPed(previewPed)
EliminarPreview(previewPed)
```

`ObtenerPreviewActual` и `EliminarPreview` — примеры вызовов; замените их функциями своего меню. Регистрируйте только preview активного персонажа, не мировые ped и не preview других персонажей. Вместо этого можно настроить `Config.GetPreviewPed`, чтобы он возвращал текущий ped или nil. `RefreshAppearance(previewPed)` просит повторно обработать уже управляемый ped, но сам по себе не включает Fat.

## Exports для серверных ресурсов

Серверные exports — это привилегированные API для других ресурсов сервера. Не перенаправляйте их напрямую из события, которое может вызвать клиент.

```lua
local playerSource = source -- ID del jugador en el servidor
local kg, err = exports['CXG_GTFAT']:GetWeight(playerSource)
if kg == nil then
    print('No hay un peso confirmado:', err)
end

local appliedKg, setError = exports['CXG_GTFAT']:SetWeight(playerSource, 92.5)
if appliedKg == nil then
    print('No se pudo fijar el peso:', setError)
end

local addedKg, addError = exports['CXG_GTFAT']:AddWeight(playerSource, -2.0)
if addedKg == nil then
    print('No se pudo sumar peso:', addError)
end

local resetKg, resetError = exports['CXG_GTFAT']:ResetWeight(playerSource)
if resetKg == nil then
    print('No se pudo restablecer el peso:', resetError)
end
```

Аргумент `source` — ID игрока на сервере, а не значение, присланное клиентом. Все значения указываются в кг. `GetWeight` возвращает вес либо nil, error. `SetWeight` и `AddWeight` возвращают нормализованный применённый вес в кг либо nil, error; `SetWeight` отклоняет значения за пределами диапазона и округляет до ближайшего шага, округляя середины вверх. `AddWeight` допускает отрицательное изменение. `ResetWeight` записывает `defaultKg` и возвращает применённый вес в кг; сохранённое значение при этом не удаляется.

Чтобы открыть интерфейс игроку из другого серверного ресурса:

```lua
local playerSource = source -- ID del jugador en el servidor
local opened, err = exports['CXG_GTFAT']:OpenWeightUI(playerSource)
if not opened then
    print('No se pudo abrir la báscula:', err)
end
```

Вторым аргументом можно передать ID станции: `OpenWeightUI`(source, 'gimnasio'). Если станция не указана, действует политика доступа команд, даже если регистрация команд отключена. Результат true подтверждает авторизацию и отправку запроса на открытие, но не отображение интерфейса на клиенте.

### Серверные exports

| Export | Использование |
| --- | --- |
| `GetWeight(source)` | Возвращает кг либо nil, error. |
| `SetWeight(source, kg)` | Устанавливает проверенный вес и возвращает применённое значение в кг либо nil, error. |
| `AddWeight(source, deltaKg)` | Прибавляет изменение и возвращает применённое значение в кг либо nil, error. |
| `ResetWeight(source)` | Сохраняет и возвращает настроенный начальный вес в кг либо nil, error. |
| `GetWeightSettings()` | Возвращает копию действующих ограничений. |
| `OpenWeightUI(source, stationId?)` | Запрашивает открытие весов согласно действующей политике. |
| `RefreshCharacter`(source) | Перезагружает идентификатор и вес после переключения фреймворка на нужного персонажа. |
| `ReconcileStorage`(source) | Сверяет хранилище после неопределённого состояния, если адаптер может подтвердить предыдущие записи. |

`RefreshCharacter` необязателен для интеграций с несколькими персонажами; вызывайте его после того, как фреймворк предоставит нужный идентификатор. Ресурс не подключается к Qbox автоматически.

Ошибки могут включать `not_ready`, `invalid_source`, `player_unavailable`, `character_unavailable`, `invalid_weight`, `out_of_range`, `permission_denied`, `context_denied`, `busy`, `storage_failed`, `storage_timeout`, `storage_unknown` или `not_synced`. Не считайте ошибку хранилища подтверждённым весом и не повторяйте автоматически запись, завершившуюся с `storage_timeout` или `storage_unknown`.

## Клиентские exports

```lua
local kg, err = exports['CXG_GTFAT']:GetWeight()
local settings = exports['CXG_GTFAT']:GetWeightSettings()
local isOpen = exports['CXG_GTFAT']:IsWeightUIOpen()
local closed = exports['CXG_GTFAT']:CloseWeightUI()
local status, statusError = exports['CXG_GTFAT']:GetFatStatus()
local reserved, reservedError = exports['CXG_GTFAT']:GetReservedDecals(PlayerPedId())
```

До получения подтверждённого сервером значения `GetWeight` возвращает nil, '`not_synced`'. `GetFatStatus(ped?)` и `GetReservedDecals(ped)` возвращают информацию только для чтения и не доказывают, что геометрия отобразилась. `GetReservedDecals` можно использовать, чтобы скрывать зарезервированные decal в меню. `RefreshAppearance(previewPed?)` обновляет только управляемую внешность игрока или зарегистрированный preview.

`RegisterPreviewPed(ped)` и `UnregisterPreviewPed(ped)` позволяют меню передать селектору сущность preview. Снять регистрацию может только ресурс, который её установил. Зарегистрированные preview удаляются при остановке ресурса-владельца.

`SanitizeAppearance(ped, appearance, format)` возвращает копию внешности; nil, error не используется для обозначения ошибки. `RegisterPreviewPed` и `UnregisterPreviewPed` возвращают true при успехе и false, если ped не удалось зарегистрировать или удалить.

## Редактируемые bridges

| Файл | Адаптация |
| --- | --- |
| `bridge/server.lua` | `CanAccess`, идентификатор, загрузка/сохранение веса и серверные hooks. |
| `bridge/client.lua` | Preview ped, уведомления и hooks изменений веса/интерфейса. |
| `bridge/interaction.lua` | Регистрация станций в target или замена TextUI. |
| `bridge/appearance.lua` | Чтение/запись decal в собственном формате внешности. |

По умолчанию `CanAccess` распознаёт только ace и everyone. Для jobs, groups или custom реализуйте проверку через реальный API сервера и возвращайте строго true при разрешённом доступе. Исключение или любое другое значение означает отказ. Поля политик job, group и custom интерпретирует ваш адаптер.

Собственный поставщик хранилища должен реализовать сигнатуры `LoadWeight`(`characterId`, context, done), `SaveWeight`(`characterId`, kg, context, done) и, если требуется согласование неопределённых записей, `ReconcileWeight`(`characterId`, context, done). При загрузке вызывается done(true, kg) (если данных нет, используйте kg = nil) либо done(false, '`storage_failed`'). Сохранение вызывает done(true) только после подтверждения или done(false, '`storage_failed`'). Согласование вызывает done(true, true), только если предыдущая запись уже не сможет завершиться позже; если это нельзя гарантировать, вызовите done(false, '`storage_unknown`'). Для действительно асинхронного сохранения установите `AsyncStorage` = true; callback должен завершиться в контексте FiveM, поддерживающем ожидание. API ждёт до 10 секунд и затем сообщает о тайм-ауте. После `storage_timeout` не повторяйте запись автоматически; сначала выполните согласование через поставщика, гарантирующего, что предыдущая запись не применится позже. Не объявляйте поставщика асинхронным, если он возвращает управление до запуска или постановки записи в очередь.

Hooks `OnWeightChanged`, `OnCharacterChanged`, `OnUIOpened`, `OnUIClosed` и Log нужны для наблюдения и ведения журнала. Hooks не выдают доступ и не отменяют подтверждённые операции.

## Проверки перед включением

Патчи и адаптеры необходимо проверить с фактическими версиями и ресурсами сервера. Перед включением интеграции для игроков:

1. Проверьте переключение Normal/Fat на мужском и женском персонаже freemode.
2. При включённом Fat сохраните и снова загрузите внешность и комплект. Убедитесь, что технический decal не записался как обычный выбор и вес хранится отдельно от комплекта.
3. При включённом сохранении по персонажу перезапустите ресурс и подключитесь снова. Убедитесь, что тот же персонаж получил свой вес, а другой его не унаследовал.
4. Откройте меню внешности с зарегистрированным preview и проверьте, что он отображает текущего персонажа. При отмене или закрытии меню снимите регистрацию preview до удаления сущности.

Выполните эти проверки на своём сервере, чтобы проверить конкретное сочетание меню, фреймворка и одежды. Наличие патча само по себе не подтверждает результат.
