Удалённые вызовы
Документация движка. Эта страница определяет переиспользуемый контракт удалённых вызовов FOnline. Каждый подключающий проект владеет конкретными вызовами, правилами авторизации, сгенерированным каталогом вызовов и политикой совместимости.
Назначение
Используйте удалённые вызовы для односторонних сообщений между аутентифицированным клиентским runtime и сервером либо от сервера клиенту подключённого игрока. Удалённый вызов состоит из четырёх частей:
- одного общего объявления
///@ RemoteCall; - исходящего метода, сгенерированного на отправляющей стороне;
- обработчика принимающей стороны с AngelScript
[[ServerRemoteCall]]/[[ClientRemoteCall]]либо Managed C#[ServerRemoteCall]/[ClientRemoteCall]; - запечённых метаданных, которые задают обоим runtime одинаковые имя, порядок аргументов, типы, nullable-признаки и подсказку имени исходного файла.
Удалённые вызовы не являются событиями движка, обычными вызовами функций или функциями «запрос-ответ». Они возвращают void; ответ оформляется как другой явно объявленный удалённый вызов.
Проверенные пути исходного кода
Source/Tools/MetadataBaker.cppSource/Common/EngineBase.cppSource/Common/MetadataRegistration.cppSource/Common/Settings.incSource/Common/NetBuffer.cppSource/Server/ClientDataValidation.hSource/Server/ClientDataValidation.cppSource/Server/Server.cppSource/Scripting/AngelScript/AngelScriptRemoteCalls.cppSource/Scripting/AngelScript/AngelScriptAttributes.cppSource/Scripting/Managed/ManagedScriptBackend.cppSource/Scripting/Managed/CoreScripts/RemoteCall.csSource/Scripting/Managed/CoreScripts/RemoteCallScriptFuncs.csSource/Tools/ManagedScriptBaker.cppSource/Tests/Test_MetadataBaker.cppSource/Tests/Test_AngelScriptAttributes.cppSource/Tests/Test_ManagedScriptBaker.cppSource/Scripting/Managed/Tests/BootstrapScenarios.csSource/Tests/Test_ClientDataValidation.cppSource/Tests/Test_NetBuffer.cppBuildTools/docs_metadata.pyBuildTools/tests/test_docs_metadata.pyExamples/MinimalProject/Scripts/Starter.fosExamples/MinimalProject/run_starter_smoke.py
Направление и runtime-интерфейс
Цель в объявлении называет принимающую сторону.
| Цель | Отправляющая сторона | Исходящий интерфейс | Принимающий обработчик |
|---|---|---|---|
Server |
клиент | player.ServerCall.Name(args...) |
[[ServerRemoteCall]] void Namespace::Name(Player player, args...) |
Client |
сервер | player.ClientCall.Name(args...) или cr.PlayerClientCall.Name(args...) |
[[ClientRemoteCall]] void Namespace::Name(args...) |
Для цели Server принимающий обработчик получает вызывающего Player первым аргументом. Этот аргумент предоставляет движок; в объявлении ///@ RemoteCall его не пишут. Обработчик цели Client получает только объявленные аргументы.
Player.ClientCall адресует вызов клиенту этого игрока. Critter.PlayerClientCall маршрутизирует его через игрока, связанного с криттером. Объект вызывающей стороны участвует в маршрутизации и не сериализуется как объявленный аргумент.
Mapper не поддерживает удалённые вызовы. [[AdminRemoteCall]] относится к отдельному пути административных команд: его не объявляют как ///@ RemoteCall Admin и не включают в проектные каталоги удалённых вызовов.
Transport contract общий, а authoring syntax и текущее покрытие типов различаются:
| Аспект | AngelScript | Managed C# |
|---|---|---|
| Inbound marker | [[ServerRemoteCall]] / [[ClientRemoteCall]] |
[ServerRemoteCall] / [ClientRemoteCall] |
| Outbound call | generated player.ServerCall.Name(...), player.ClientCall.Name(...) или cr.PlayerClientCall.Name(...) |
те же generated property и typed method names |
| Поиск handler | stem файла объявления выбирает совпадающий namespace | assembly discovery выбирает attributed static method по имени call; file-stem namespace rule отсутствует |
| Возврат handler | void; [[Async]] использует AngelScript suspension |
void, Task или Task<T>; completion/fault наблюдается, значение Task<T> игнорируется, потому что wire result отсутствует |
| Текущее покрытие collections | arrays, dictionaries и dictionaries of arrays поддерживаются AngelScript serializer | только scalars и arrays; dictionaries отклоняются до расширения Managed bridge |
Оба backend используют один baked RemoteCallDesc, native hostile-input validation, byte format, per-call limits, routing и authority boundary. Parity здесь означает единый network contract с явно описанными различиями возможностей, а не два несовместимых протокола.
Грамматика объявления
Объявите каждый контракт один раз в исходном скрипте, видимом при запекании метаданных:
///@ RemoteCall Server RequestRename(string name) MaxBytes 256
///@ RemoteCall Client RenameResult(bool accepted, string reason) MaxBytes 512
Грамматика:
///@ RemoteCall (Server|Client) Name([Type[?] argument [, ...]]) [MaxBytes N] [MaxCollectionSize N]
Правила, которые проверяет MetadataBaker:
- цель должна быть ровно
ServerилиClient; - после имени должны идти круглые скобки;
- тип каждого аргумента должен разрешаться через метаданные движка;
- каждый аргумент должен иметь имя;
- аргументы разделяются запятыми;
?записывается отдельно от разрешённого типа.- после закрывающей скобки допускаются только
MaxBytesиMaxCollectionSize: каждый option можно указать не более одного раза, за ним должно следовать неотрицательное целое число;0или отсутствие option означает отсутствие структурного per-call предела.
Имена регистрируются в картах входящих и исходящих вызовов и должны быть уникальны внутри каждого направления на одной стороне runtime. Считайте имя вызова, цель, порядок аргументов, типы и nullable-признаки единым сетевым контрактом.
Контракт файла и пространства имён AngelScript
Baker сохраняет в качестве подсказки подсистемы только имя исходного файла объявления, например AccountUi.fos. AngelScript binder удаляет расширение и ищет принимающую функцию в одноимённом пространстве имён:
// AccountUi.fos
namespace AccountUi
{
///@ RemoteCall Server RequestRename(string name)
///@ RemoteCall Client RenameResult(bool accepted, string reason)
#if SERVER
[[ServerRemoteCall]]
void RequestRename(Player player, string name)
{
// Authorize and validate before changing server state.
}
#endif
#if CLIENT
[[ClientRemoteCall]]
void RenameResult(bool accepted, string reason)
{
// Update client presentation only.
}
#endif
}
Для этого файла требуется пространство имён AccountUi. Переименование или перемещение скрипта без согласованного изменения основы имени файла, пространства имён, объявлений и обработчиков приводит к ошибке привязки. Если входящий обработчик отсутствует, runtime сообщает ожидаемую декларацию.
Обработчики должны использовать атрибут своей принимающей стороны. Валидатор отклоняет обработчик с атрибутом противоположной стороны, атрибутированную функцию без соответствующего входящего объявления и обычный прямой вызов обработчика remote call. Отправляйте вызов через сгенерированный интерфейс:
// Client to server.
player.ServerCall.RequestRename("Ranger");
// Server to one player's client.
player.ClientCall.RenameResult(true, "");
Контракт binding Managed C#
Разместите тот же declaration tag в .cs source, входящем в Managed resource pack. Managed baker создаёт typed method RemoteCaller для каждой outbound metadata record и добавляет соответствующее property ServerCall, ClientCall или PlayerClientCall generated entity types. Inbound methods обнаруживаются по всей project assembly; они должны быть static, иметь attribute принимающей стороны, точное имя и arguments call, а на server первым принимать предоставленный движком Player:
namespace Game.Scripts;
using System.Threading.Tasks;
using FOnline;
///@ RemoteCall Server RequestRename(string name) MaxBytes 256
///@ RemoteCall Client RenameResult(bool accepted, string reason) MaxBytes 512
public static class AccountUi
{
[ServerRemoteCall]
public static async Task RequestRename(Player player, string name)
{
// Authorize, validate, and reacquire cover after every await.
await Game.YieldAsync(1);
}
[ClientRemoteCall]
public static void RenameResult(bool accepted, string reason)
{
// Update client presentation only.
}
}
Отправляйте через generated typed surface:
player.ServerCall.RequestRename("Ranger");
player.ClientCall.RenameResult(true, "");
FOnline.RemoteCall.Send(...) остаётся низкоуровневым named entry point для generated surface и диагностики; gameplay code следует предпочитать generated method, чтобы C# compiler проверял список arguments. RemoteCall.Loopback(...) предназначен только для diagnostic/tests и не доказывает real peer, authentication или transport path.
Аргументы и сериализация
Общий wire format поддерживает разрешённые metadata primitives, integer-backed enums, string, hstring, зарегистрированные reference types, fixed-layout structs, arrays, dictionaries и dictionaries of arrays. AngelScript bridge покрывает весь этот список. Текущий Managed bridge принимает scalar values и arrays, а из ref types — только dynamic managed ref types; dictionaries, dictionary-of-array shapes и native-only ref wrappers отклоняются. Поэтому известность типа metadata ещё не является полноценным backend/transport test: скомпилируйте и запеките обе стороны, затем выполните call через каждый поставляемый scripting backend и целевой network path.
Правила T? описаны в Nullability.md. Объявление является авторитетным: принимающий обработчик должен точно повторять тип и nullable-признак каждого аргумента. Не используйте nullable-синтаксис вместо необязательного поля с предметным смыслом; определите этот смысл в протоколе явно.
Делайте payload ограниченным и предназначенным для одной задачи. В частности:
- предпочитайте компактные идентификаторы и заново разрешайте серверные сущности на принимающей стороне;
- задавайте проектные пределы длины строк и количества элементов коллекций;
- не передавайте большие снимки состояния, когда достаточно идентификатора и ревизии;
- добавляйте явный идентификатор запроса или корреляции, если одновременно могут ожидаться несколько ответов;
- вводите новый вызов или версионированный payload, если старые и новые клиенты не могут одинаково интерпретировать сигнатуру.
Для каждого недоверенного вызова клиент-сервер задавайте MaxBytes как максимальный корректный размер сериализованного тела, а MaxCollectionSize — как максимальный корректный размер любой объявленной коллекции. Предел коллекции отдельно применяется к массивам, словарям, внешнему словарю и вложенным массивам словаря массивов. Если предел не объявлен, в метаданных остаётся 0: это явное неограниченное структурное значение, а не рекомендуемый production default.
Авторитет и обработка ошибок
Удалённый вызов с клиента на сервер является запросом, а не доказательством разрешённости действия. Серверный обработчик должен получить личность вызывающего из предоставленного движком Player, а затем до изменения состояния проверить аутентификацию, владение, валидность сущностей, текущее игровое состояние, стоимость ресурсов, ограничения частоты и все проектные правила авторизации.
Не принимайте идентификатор игрока или владельца из payload как замену вызывающему Player. Разрешайте идентификаторы payload по обычному контракту синхронизации и доступа к сущностям, а после любого yield повторно проверяйте состояние перед использованием захваченных сущностей. Клиентские обработчики должны отвечать за представление; авторитетное состояние остаётся на сервере.
На сервере ServerEngine::Process_RemoteCall() отклоняет отрицательный размер payload или размер, превышающий остаток текущего frame, и разрешает объявленный вызов до allocation или копирования его тела. Поэтому неизвестный вызов не может принудить сервер выделить память под тело. Эффективный предел байтов — меньшее ненулевое значение MaxBytes этого вызова и общей настройки ServerNetwork.MaxRemoteCallPayloadSize (по умолчанию 1 МиБ). Per-call значение определяет допустимую форму протокола, а global value остаётся защитным пределом для враждебного input.
Затем сервер выполняет ValidateInboundRemoteCallData() до получения cover вызывающего Player и запуска handler. Validator проходит metadata shape, проверяет enum/hash/reference data, отклоняет отрицательное или превышающее предел число элементов collections, доказывает наличие минимально требуемых bytes до обхода collection и требует полного потребления payload. AngelScript decoder независимо применяет тот же предел collection до Reserve() или создания container, включая nested dictionary arrays. Managed decoder использует общий reader RemoteCallWire, повторяет проверки payload и array ceiling до создания managed List<T>, требует полного потребления и вызывает attributed handler внутри backend context. Эти transport checks не заменяют domain validation. Записывайте в log достаточно call/caller context для диагностики, не журналируя secrets или полный untrusted payload.
Мониторинг времени выполнения и отключение
Сервер наблюдает каждое соединение независимо через настройки ServerNetwork:
| Настройка | По умолчанию | Значение |
|---|---|---|
RemoteCallWarningTimeMs |
1000 |
Предупреждать, когда незавершённый входящий RPC или полный ожидающий входной фрейм достигает этого возраста. |
RemoteCallDisconnectTimeMs |
30000 |
Отключать этого клиента, когда любой из двух возрастов достигает порога. |
RemoteCallWarningIntervalMs |
10000 |
Минимальный интервал предупреждений на соединение; 0 означает одно предупреждение на непрерывный эпизод превышения. |
MaxPendingRemoteCalls |
256 |
Максимум принятых незавершённых обработчиков на соединение; следующий вызов отвергается с отключением до входа в обработчик. |
Ноль независимо отключает соответствующую реакцию по возрасту или предел количества. Оба нулевых порога возраста вместе с нулевым пределом отключают учёт. Отрицательные значения отвергаются при запуске сервера. Порог предупреждения не обязан быть ниже порога отключения; отключение имеет приоритет.
Независимый watchdog проверяет монотонное время примерно каждые 100 мс. Ему
не нужны entity cover или свободный script worker. После проверки фрейма и размера
payload учёт вызова начинается до проверки аргументов и получения cover вызывающего игрока, а заканчивается при
фактическом завершении обработчика: синхронно, после успеха, ошибки или отмены
Managed Task либо возврата приостановленного AngelScript-контекста.
Диагностические идентификаторы локальны для соединения и не заменяют прикладные
request ID. Завершение по-прежнему ничего не доказывает о доставке отдельного ответа.
Нативный мониторинг использует отдельный RpcRuntimeCallId для исполнения RPC
и существующий ident_t игрока для CallerId; эти ID нельзя неявно смешивать.
Нулевой ID исполнения означает отсутствие учитываемого вызова.
Возраст очереди отсчитывается с прихода последнего байта, делающего старейший ожидающий входной фрейм полным. Частичная доставка сама по себе не является готовой работой. Решение принадлежит читателю фреймов; наблюдение не потребляет вход. Учитываются все виды сообщений верхнего уровня, включая ожидающие за выполняющимся RPC. История прихода ограничена 4096 сегментами; при объединении сохраняется более новая отметка, поэтому сообщение может задержаться, но возраст не завышается.
Предупреждения содержат peer, известного вызывающего, имя/ID вызова, причину,
возраст, число pending, байты буфера, общие и пометодные счётчики начала/завершения.
Для очереди queued_call_hash обозначает ожидающий RPC, если он известен;
last_dispatch и stats_call относятся к уже запущенной работе, их нельзя
считать именем ожидающего метода. Аргументы и учётные данные не записываются.
При выборе порогов учитывайте легитимные долгие обработчики наряду со злоупотреблениями.
Отключение записывает DisconnectReason::RpcRuntimeLimit и закрывает транспорт.
Оно использует NetworkServerConnection::RequestDisconnect() без ожидания
send/receive callbacks, которые сами могут ждать блокировку игрового потока.
Обычные Disconnect() / DropAsyncCallbacks() по-прежнему дожидаются callbacks
до уничтожения владельца. Закрытие только транспорта может отложить уведомление
об отключении, в том числе для interthread; учёт watchdog останавливается сразу.
Оно не прерывает исполняемый код, не отменяет произвольные tasks и не откатывает
игровые изменения. Владельцы завершения удерживают отдельное состояние соединения
и слабую ссылку на транспорт; disconnect/teardown очищает pending как покинутые,
позднее завершение безопасно освобождает владельца. Новое соединение получает
новое состояние.
Запечённые метаданные
MetadataBaker создаёт по одному бинарному файлу метаданных для каждой цели. Запись remote call содержит:
name, source-file hint, In|Out, type, nullable marker, argument name, ..., Limits, max-bytes, max-collection-size
Для цели Server серверные метаданные записывают In, а клиентские — Out. Для цели Client направления меняются местами. Каждая запись заканчивается обязательным трёхтокенным trailer Limits, включая Limits 0 0, если в declaration нет обоих options; dynamic registration отклоняет прежнюю форму без trailer. Допустимые записи превращаются в RemoteCallDesc. AngelScript регистрирует caller methods и связывает file/namespace handlers; Managed baker генерирует typed caller methods, а Managed runtime связывает attributed assembly methods по имени.
Запечённый формат сохраняет имя исходного файла, но не путь относительно репозитория и не строку объявления. Поэтому документация, созданная из .fometa, должна показывать это поле как подсказку исходного файла, а не выдумывать полное происхождение.
Создание проектного каталога
Генератор движка читает файлы, которыми владеет parser. Он не разбирает .fos или .cs второй грамматикой:
python Engine/BuildTools/docs_metadata.py \
--root . \
--metadata Baking/Metadata/Metadata.fometa-server \
--metadata Baking/Metadata/Metadata.fometa-client \
--write
По умолчанию он создаёт принадлежащие проекту артефакты:
Docs/generated/project-remote-calls.jsonдля инструментов и AI-поиска;Docs/generated/project-remote-calls.mdдля GitHub Pages и чтения человеком.
Каждый вызов получает стабильный ID вида script.remote-call.<target>.<name> и статус project-owned. Этот статус определяет границу владения, а не обещает, что игра сохраняет обратную совместимость вызова.
Генератор строго декодирует бинарный контейнер, проверяет UTF-8 и обязательный limits trailer, отклоняет дублирующиеся входы и вызовы и требует совпадения сигнатур и структурных пределов на серверной и клиентской сторонах. JSON model и Markdown table показывают выбранный scripting backend, корректный для него синтаксис handler attribute, MaxBytes и MaxCollectionSize рядом с каждым вызовом. Managed handler показан неквалифицированной lookup signature, потому что baked metadata не сохраняет его declaring C# type и не сообщает, возвращает ли реализация void или Task; source hint остаётся указателем для проверки объявления. Генератор также записывает SHA-256 входных файлов. После запекания используйте те же входные пути с --check в CI проекта:
python Engine/BuildTools/docs_metadata.py \
--root . \
--metadata Baking/Metadata/Metadata.fometa-server \
--metadata Baking/Metadata/Metadata.fometa-client \
--check
Флаг --allow-unpaired предназначен для диагностики, когда доступна только одна сторона. Не используйте его для публикуемого production-каталога: одиночная запись не доказывает согласованность отправителя и получателя.
Совместимость и выпуск
Проектные remote calls являются запечёнными проектными метаданными и намеренно не входят в сгенерированную модель API движка. Игра должна хранить сгенерированный каталог в репозитории, сравнивать его при review и развёртывать согласованные клиентскую и серверную ревизии проекта.
Переименование вызова, изменение его цели, перестановка аргументов, изменение типа или nullable-признака аргумента либо изменение любого структурного предела является изменением сетевого контракта. Зафиксируйте его в release notes и политике совместимости подключающего проекта. Изменения транспорта, формы метаданных или регистрации remote calls на уровне движка также должны соблюдать правило версии совместимости из AGENTS.md.
Диагностика
Remote call function not found: в AngelScript проверьте stem файла и namespace, side guards, имя handler и точную signature; в Managed C# проверьте, что source попал в target assembly и найден matching attributed static method.- Ошибка проверки атрибута: используйте только marker принимающей стороны —
[[ServerRemoteCall]]/[[ClientRemoteCall]]либо[ServerRemoteCall]/[ClientRemoteCall]. - Managed rejection collection/ref type: сократите declaration до scalars, arrays и dynamic managed ref types либо оставьте этот call на AngelScript до расширения Managed bridge.
- Дублирующаяся регистрация: имена вызовов пересекаются внутри одного входящего или исходящего направления; переименуйте один контракт.
- Одиночная запись документации: серверные и клиентские метаданные получены из разных или неполных bake либо один файл устарел.
- Отсутствующий или несовпадающий limits record: заново запеките обе цели на одной revision Engine/project; каждая запись должна заканчиваться
Limits <max-bytes> <max-collection-size>, и обе стороны должны совпадать. - Нет ссылки на исходный код в сгенерированном результате:
.fometaхранит только подсказку имени файла; исследуйте инвентарь исходного кода проекта вместо угадывания пути. - Ошибка сериализации runtime: сократите контракт до заведомо поддерживаемых типов и добавьте узкий сквозной транспортный тест.
Контрольный список проверки
- Держите каждое объявление общим для входных метаданных обеих сторон.
- Реализуйте точную входящую сигнатуру на правильной стороне и с правильным атрибутом.
- Выполните обычное запекание ресурсов проекта; для каждого включённого backend требуются AngelScript compile без warnings и/или чистые Managed C# compile плюс analyzer pass.
- Создайте и проверьте парный JSON/Markdown-каталог из этого bake.
- Выполняйте
BuildTools/docs_metadata.py --checkс теми же входами в CI. - Проверьте авторизацию client-to-server и представление server-to-client через реальную сеть или интеграционный тест проекта.
- Проверьте отклонение прав, устаревших идентификаторов, некорректных предметных значений, global/per-call пределов payload, а также пределов внешних и вложенных коллекций.
- Для каждого несовместимого изменения вызова задайте явное решение по выпуску и совместимости.
Принадлежащий движку AngelScript минимальный проект доказывает разбор declarations, обе inbound binding, paired baked metadata, stable catalog IDs и server lifecycle startup. Test_ManagedScriptBaker вместе с Managed CoreScripts tests доказывают generated caller shape и managed handler registration/loopback. Ни один из этих уровней не заменяет реальные multiplayer transport/authorization tests игры.
См. также
- Скриптовый runtime — устройство скриптовой подсистемы и владение callback.
- Nullability.md — nullable-контракт скриптов и совпадение сигнатур remote call.
- GeneratedApiAndMetadata.md — граница владения codegen и запечённых метаданных.
- Сеть и авторитетность — модель транспорта и соединений.
- Baking Pipeline — пакеты ресурсов и выполнение baker.