Задача и границы разбора#

Игрок переименовывает управляемый корабль через обычный интерфейс X4. Пусть старое имя — «Элита», новое — «Север». Нужно изменить настоящее имя на хосте, доставить его гостю и не применить старую операцию повторно после более нового переименования.

Ниже разобран имеющийся код, а не новый игровой прогон. Успешный путь предполагает активную загруженную сессию, разрешённый объект и право управления. Число 42 и ключ ship из тестов — тестовые значения; их нельзя использовать как ID настоящего корабля.

Держи рядом четыре файла#

Файл от корня проекта Поисковые имена
extension/ui/x4_online_object_names.lua SetComponentName=function, requestObjectRename, objectNamesTick
x4_online/object_names.py ObjectNameAuthority, request, pump, observe, stream
extension/md/x4_online_object_names.xml cue Rename, set_object_name, $Actual, $Records
tests/test_object_names.py test_rename_retries_immutable_intent_and_late_replay_does_not_undo_name

Переходы между ними можно найти так:

POWERSHELL
Set-Location D:\X4FoundationsOnline
rg -n 'OBJECT_RENAME|OBJECT_NAME_EXECUTE|OBJECT_NAME_RECORD|OBJECT_NAME_APPLIED' extension x4_online

Весь путь перед подробностями#

TEXT
Гость: штатное переименование
  → Lua-обёртка SetComponentName
  → requestObjectRename: неизменяемый запрос
  → OBJECT_RENAME через локальный мост
  → bridge.py: сетевой пакет type=object_rename
  → хост: Session.control → handle_object_names
  → ObjectNameAuthority.request: права, состояние, revision
  → pump: OBJECT_NAME_EXECUTE в X4 хоста
  → Lua хоста: событие rename в MD
  → MD: set_object_name → чтение $Object.name → запись исхода
  → OBJECT_NAME_STATE / OBJECT_NAME_RECORD обратно в Python
  → снимок участнику → Lua гостя
  → применение подтверждённого имени и OBJECT_NAME_APPLIED

OBJECT_NAME_RESULT сообщает исход запроса. OBJECT_NAME_STATE несёт текущее имя с ревизией. Они решают разные задачи: ответ «запрос завершён» не заменяет доставку актуального состояния.

1. Lua принимает обычное действие#

Мод сохраняет исходный SetComponentName, затем ставит обёртку. В активном сетевом режиме для кораблей и станций она находит постоянный ключ объекта и вызывает app.requestObjectRename(key, name).

Исходный setter в этой ветке сразу не вызывается. Иначе UI гостя успел бы изменить свой объект до решения хоста. Когда условие сетевого перехвата не подходит, сохраняется обычный путь.

Обрати внимание на два разных имени:

Имя Происхождение
SetComponentName Функция среды X4, которую наш код оборачивает
app.requestObjectRename Наша Lua-функция, определённая в том же файле

ConvertIDTo64Bit здесь участвует в конкретном пути преобразования UI ID. Это не совет преобразовывать любой ID таким вызовом: форма входа определяется вызывающей стороной.

2. Создаётся запрос#

requestObjectRename проверяет, что уже известно имя с ревизией, нет другого ожидающего переименования, сессия активна и сохранение находится в фазе running. Затем создаёт request_id, запоминает текущий контекст и готовую строку запроса.

Сокращённая форма содержимого:

TEXT
OBJECT_RENAME | request_id | object_key | expected_revision | name_hex | scope

Пробелы вокруг разделителей добавлены для чтения. В коде строка собирается через table.concat(..., '|') без них. scope отличает имя объекта (object) от имени флота (fleet).

name_hex — текст, представленный шестнадцатеричными байтами. Благодаря этому символ | в имени не ломает разбор полей. Hex не является шифрованием и не даёт права на действие.

objectNamesTick отправляет ожидающий запрос, а при отсутствии результата повторяет ту же строку через интервал. Повтор не должен генерировать новое намерение с другим ID. Новый контекст делает старый ожидающий запрос непригодным.

3. Python переводит строку в команду сессии#

В bridge.py найди ветку message.startswith(('OBJECT_RENAME|','OBJECT_NAME_APPLIED|')). Она разбирает строку, декодирует имя и создаёт пакет:

TEXT
type: object_rename
epoch: текущий контекст
connection: текущее соединение
intent: request_id, object, revision, name, при необходимости scope

Это схема данных, а не полный JSON для ручной отправки. Если действие пришло из X4 хоста, вызывается локальный путь допуска. Если из X4 гостя — пакет отправляется хосту по выбранному транспорту.

В session_commands.py регистрация object_rename ведёт к handle_object_names. Сначала _require_connection проверяет допустимость пакета и контекста, затем вызывается session.object_names.request(...).

4. Хост решает, можно ли начинать#

ObjectNameAuthority.request проверяет форму намерения, текст, предыдущий исход, готовность системы, фазу сохранения, занятость, право управления и ожидаемую ревизию имени.

Ситуация Ветка, которую стоит найти
Такой запрос уже завершался Вернуть сохранённый исход
Тот же ID пришёл с другим содержимым ProtocolError: коллизия запроса
Реестр ещё загружается loading
Идёт сохранение saving
Занята связанная операция busy
Нет права управления permission
Имя уже изменилось с тех пор stale
Всё подходит Сохранить pending, статус pending

pending означает «ожидаем выполнение», а не «успех». Обрати внимание: в этом методе нет set_object_name. Здесь только принимают запрос в работу.

5. Операция отправляется в настоящую X4 хоста#

Метод pump вызывается обслуживающим циклом. Он проверяет условия отправки, а до первой передачи дополнительно сверяет соединение и права. Затем посылает OBJECT_NAME_EXECUTE.

На стороне игры app.objectNamesReceive распознаёт сообщение. Проверка host(...) ограничивает исполнение активным хостом и текущим контекстом. Далее AddUITriggeredEvent('X4FO_ObjectNames', 'rename', ...) переводит данные в MD.

Именно на этом переходе появляется пара:

LUA
-- Сокращённая форма: payload должен быть собран и проверен адаптером.
AddUITriggeredEvent("X4FO_ObjectNames", "rename", payload)
XML
<event_ui_triggered screen="'X4FO_ObjectNames'" control="'rename'" />

Lua-фрагмент показан для чтения, не для вставки отдельно: переменная payload в нём намеренно не создаётся. Полный контракт находится в исходнике. Нельзя заменить его списком из одного нового имени.

6. MD меняет объект и читает результат#

Cue Rename получает список из event.param3, проверяет его форму, контекст и scope. Затем проверяет старый журнал. Если исход уже есть и содержимое совпадает, возвращает его.

Для новой операции MD находит объект хоста по постоянному ключу, проверяет существование, тип, владельца и ревизию. Важно, что права и реальное состояние проверяются также около операции движка: они могли поменяться после допуска в Python.

Ключевые строки ветки обычного имени:

XML
<set_object_name object="$Object" name="$D.{9}" />
XML
<set_value name="$Actual" exact="if $D.{11} == 'fleet' then $Object.fleet.name else $Object.name" />

Это выдержки из действий, а не самостоятельный MD-скрипт. $Object — разрешённый объект хоста; $D — проверенный список параметров. В данной версии элемент 9 содержит новое имя, элемент 11 — область object/fleet.

Далее код сравнивает $Actual с ожидаемым именем, обновляет состояние и сохраняет исход в Init.$Records. Пустое имя имеет особый смысл: сброс к генерируемому/стандартному имени, поэтому его ветку нельзя свести к обычному сравнению строк.

Между эффектом и записью исхода в этом участке нет ожиданий. Это устройство конкретной операции; оно само по себе не доказывает аварийную атомарность всех систем мода.

7. Результат возвращается, состояние доставляется#

MD выдаёт OBJECT_NAME_STATE и OBJECT_NAME_RECORD через Lua-события. Обработчик X4FO.ObjectNameWire ставит данные в очередь наблюдений. Дальше локальный мост доставляет их Python.

На хосте ветка OBJECT_NAME_ вызывает ObjectNameAuthority.observe. Она проверяет контекст, форму и соответствие записи принятому запросу. Для результата используется запись MD, а не предположение «раз команда отправилась, значит выполнена».

stream готовит имена, разрешённые данному участнику, и ответ на его запрос. session_snapshot.py включает их в снимок. В bridge.py найди names=(snapshot or{}).get('object_names',{}): здесь данные возвращаются в Lua как OBJECT_NAME_STATE и OBJECT_NAME_RESULT.

Lua гостя применяет имя к разрешённому местному представлению, снова читает его и только после совпадения отправляет OBJECT_NAME_APPLIED. Это подтверждение применения конкретного имени и ревизии. Оно не подтверждает всю игровую приёмку мода.

Зачем нужен журнал, если имя уже поменялось#

Представь порядок:

  1. Запрос A меняет «Элита» на «Север»; ответ теряется.
  2. Более новый запрос B меняет имя на «Восток».
  3. Из-за повтора снова приходит A.

Повтор A должен получить старый исход A и оставить «Восток» текущим именем. Для этого отдельно существуют ключ запроса, отпечаток его содержимого, исход и состояние с ревизией. В tests/test_object_names.py именно эту ситуацию проверяет тест с late_replay_does_not_undo_name.

Как это связано с сохранением#

MD хранит $States и $Records в контексте Init. При загрузке Python заново собирает набор через сообщения OBJECT_NAME_BEGIN, строки состояний/исходов и OBJECT_NAME_END. Готовность появляется после проверки полного количества строк.

Этот путь объясняет восстановление данного семейства. Полное единое сохранение, сбои и согласованность всех поколений относятся к отдельной приёмке этапа 13.

Упражнение после чтения#

Найди в исходниках ответы на четыре вопроса: где создаётся ID запроса; где проверяется can_manage; где реально вызывается set_object_name; где отправляется OBJECT_NAME_APPLIED. Затем прочитай Python без спешки и переходи к первой задаче.

Вернуться к маршруту.