Работа с объектом метки
Какой класс использовать
| Класс | Когда им пользоваться |
|---|---|
LXWM_ProtectedServerMarker | Сервер, 4_World или 5_Mission. Используйте для точек, которыми управляет сервер. Помимо общих методов здесь можно задать получателей, выбрать хранение до рестарта или на диске и обновлять положение движущейся метки через SetPositionDynamic(). |
LXWM_ProtectedClientMarker | Клиент, 5_Mission. Используйте для личных меток игрока, а также для чтения серверных меток, полученных этим игроком. Класс один и тот же, но личную метку можно редактировать и удалять, а серверная для прямой записи закрыта. |
LXWM_ProtectedTemporaryMarker | Клиент, 5_Mission. Используйте для локальных событий и интеграций. Метка существует только в памяти до явного удаления или отключения от сервера. Доступны общие методы оформления, свой ID и SetPositionDynamic(), но нет сохранения в базу или сетевой передачи. Создание и жизненный цикл описаны в клиентском API. |
«Защищённый» не означает «только для чтения». Это означает, что Ваш мод работает с меткой через её методы, не получая саму запись базы или виджет карты. CreateServerMarker(), CreatePersonalMarker() и CreateTemporaryMarker() возвращают черновик: у него уже есть ID, но игроки его ещё не видят. Настройте объект и вызовите Apply(). Чтобы изменить уже существующую метку, получите её через GetServerMarker(), GetPersonalMarker() или GetTemporaryMarker(); повторный Create создаст другую метку.
Сервер: создать метку события и удалить её
Допустим, Ваш мод сбрасывает груз. Когда выбрали место сброса, вызовите MarkLandingSite() у объекта события и передайте мировые координаты. Пример оставляет метку в меню карты, но выключает её показ в мире. Третий аргумент false в CreateServerMarker означает, что метка временная; если передать true, она будет храниться на диске. Серверному коду в активной миссии не нужно ждать открытия карты игроком.
class NorthSupplyDropEvent
{
private ref LXWM_ProtectedServerMarker LandingSiteMarker;
string MarkLandingSite(vector LandingPosition)
{
if (LandingSiteMarker && LandingSiteMarker.IsValid())
{
return LandingSiteMarker.GetMarkerID();
}
LXWM_ServerOpenAPI MapAPI = LXWM_GetServerOpenAPI();
LXWM_ProtectedServerMarker NewSite = MapAPI.CreateServerMarker("Место сброса груза", LandingPosition, false);
if (!NewSite)
{
Print(MapAPI.GetLastError());
return string.Empty;
}
if (!NewSite.SetAllowShowMarker3D(false) || !NewSite.Apply())
{
Print(NewSite.GetLastError());
return string.Empty;
}
LandingSiteMarker = NewSite;
return LandingSiteMarker.GetMarkerID();
}
bool RemoveLandingSite()
{
if (!LandingSiteMarker)
{
return true;
}
if (!LandingSiteMarker.Delete())
{
Print(LandingSiteMarker.GetLastError());
return false;
}
LandingSiteMarker = NULL;
return true;
}
}
Здесь LandingSiteMarker является полем с ref: событие хранит сам объект и может обратиться к нему позже. Повторный вызов у того же объекта события вернёт ID существующей метки. Когда груз забрали, вызовите RemoveLandingSite(). Именно Delete() удаляет метку с карты, без последующего Apply(). Если просто присвоить полю NULL или уничтожить объект события, метка останется.
Не обязательно хранить объект до конца события. Если у Вас записан только ID, на сервере объявите LXWM_ProtectedServerMarker SavedSite = LXWM_GetServerOpenAPI().GetServerMarker(SavedSiteID);. Если SavedSite не равен NULL, с ним можно работать точно так же. Это удобно после рестарта для метки, которую Вы создали с сохранением на диск. Все методы этого типа описаны в серверном публичном API.
Клиент: создать и удалить личную метку
Если игрок хочет отметить собственный лагерь, серверная метка не нужна. Поместите эти два метода в клиентский класс Вашего мода. Первый создаёт личную точку и возвращает её ID. Сохраните эту строку у себя: второй метод сможет найти лагерь и удалить его, даже если переменной CampMarker из первого метода уже не существует.
string MarkMyCamp(vector CampPosition)
{
LXWM_ClientOpenAPI MapAPI = LXWM_GetClientOpenAPI();
if (!MapAPI.IsReady() || !MapAPI.CanWritePersonalMarkers())
{
return string.Empty;
}
LXWM_ProtectedClientMarker CampMarker = MapAPI.CreatePersonalMarker("Мой лагерь", CampPosition);
if (!CampMarker)
{
Print(MapAPI.GetLastError());
return string.Empty;
}
if (!CampMarker.SetShowInWorld(false) || !CampMarker.Apply())
{
Print(CampMarker.GetLastError());
return string.Empty;
}
return CampMarker.GetMarkerID();
}
Для удаления передайте сохранённый ID, а не надпись «Мой лагерь». У двух личных меток может быть одинаковое название. После успешного удаления уберите ID из данных своего мода. Если операция не удалась, сначала проверьте причину: например, при подключении личное хранилище может быть ещё не готово.
bool RemoveMyCamp(string SavedCampID)
{
LXWM_ClientOpenAPI MapAPI = LXWM_GetClientOpenAPI();
if (!MapAPI.IsReady() || !MapAPI.CanWritePersonalMarkers())
{
return false;
}
LXWM_ProtectedClientMarker CampMarker = MapAPI.GetPersonalMarker(SavedCampID);
if (!CampMarker)
{
Print(MapAPI.GetLastError());
return false;
}
if (!CampMarker.Delete())
{
Print(CampMarker.GetLastError());
return false;
}
return true;
}
Обратите внимание, где читается ошибка: если API не вернул объект, вызываем MapAPI.GetLastError(). Если объект получен, но он не смог применить изменение или удалить метку, вызываем CampMarker.GetLastError(). Для отмены неопубликованного черновика также подходит Delete(): на карте при этом ничего не появится и не исчезнет.
Клиент: прочитать серверную метку
Оба API имеют метод GetServerMarker(), но возвращают разные классы. На сервере Вы получаете LXWM_ProtectedServerMarker с возможностью записи. На клиенте по тому же ID Вы получаете LXWM_ProtectedClientMarker с информацией, которую сервер передал этому игроку. Например, Ваш интерфейс может показать название места сброса:
bool TryReadLandingSiteName(string ServerMarkerID, out string DisplayName)
{
DisplayName = string.Empty;
LXWM_ClientOpenAPI MapAPI = LXWM_GetClientOpenAPI();
if (!MapAPI.IsReady() || !MapAPI.HasServerMarkerSnapshot())
{
return false;
}
LXWM_ProtectedClientMarker ServerPoint = MapAPI.GetServerMarker(ServerMarkerID);
if (!ServerPoint)
{
return false;
}
DisplayName = Widget.TranslateString(ServerPoint.GetMarkerName());
return true;
}
У такого объекта ServerPoint.IsReadOnly() вернёт true, даже если игрок администратор. Вызов ServerPoint.Delete() не удалит общую метку. Чтобы изменить её из скрипта, работайте с серверным объектом на сервере. Чтобы предложить администратору редактирование в игре, при открытой карте проверьте ServerPoint.GetCanEdit() и вызовите ServerPoint.OpenEditor(). Подробнее в клиентском публичном API.
В следующем примере функция MoveMeetingPoint принимает именно базовый LXWM_ProtectedMarker. Поэтому из серверного кода можно вызвать MoveMeetingPoint(SavedSite, NewPosition), а из клиентского для личной метки MoveMeetingPoint(CampMarker, NewPosition). В обоих случаях функция изменит переданную ей метку, не создавая копию. Если вместо личной передать клиентское представление серверной метки, смена типа параметра не даст прав на запись.
Как изменить метку
Допустим, у события изменилась точка сбора. Получите существующую метку по её ID, вызовите SetWorldPosition и SetMarkerName, а затем один раз вызовите Apply(). До сохранения новые значения видны через этот объект, но не через другие объекты той же метки. Для ещё не опубликованного черновика порядок такой же.
bool MoveMeetingPoint(LXWM_ProtectedMarker MeetingMarker, vector MeetingPosition)
{
if (!MeetingMarker || !MeetingMarker.SetWorldPosition(MeetingPosition))
{
return false;
}
if (!MeetingMarker.SetMarkerName("Новая точка сбора") || !MeetingMarker.Apply())
{
Print(MeetingMarker.GetLastError());
return false;
}
return true;
}
Изменяются только поля, которые Вы задали. Если другой мод в это время поменял иконку, она сохранится. Если оба мода поменяли одно и то же поле, останется значение от последнего успешного Apply(). После ошибки изменения не публикуются и остаются у Вас: можно исправить значение и снова вызвать Apply().
Состояние, ID и ошибки
Личная метка получает ID автоматически. У серверной и временной клиентской можно оставить выданный ID или задать свой через SetMarkerID() и Apply(). Во втором случае добавляйте префикс своего мода и обозначение конкретной точки: например, NORTHMARKET_NWAF_TRADER, а не просто TRADER. При проверке уникальности регистр букв не различается. ID не обязан совпадать с названием, которое видят игроки.
| Сигнатура | Использование |
|---|---|
bool IsValid() |
Проверяет, что объект ещё относится к действующей метке или черновику текущей миссии. Это не проверка прав или видимости. |
bool IsDraft()bool HasPendingChanges() |
Первая проверка отличает новый черновик от опубликованной метки. Вторая вернёт true для черновика, для изменённых полей и при неисправленной ошибке поля. |
string GetMarkerID() |
Возвращает публичный ID, а не отображаемое название. У черновика уже есть ID. После подготовки переименования возвращается новый ID, но искать метку по нему можно лишь после успешного применения. |
bool Apply()bool Refresh() |
Apply проверяет всю итоговую запись и сохраняет подготовленные поля. Refresh читает актуальную запись и отбрасывает свои правки и ошибки. Обновить неопубликованный черновик нельзя. |
bool Delete() |
Удаляет опубликованную запись или отменяет черновик. Освобождение переменной не удаляет метку. Старый объект не станет действительным, даже если затем создать другую метку с тем же ID. |
string GetLastError() |
Код последней ошибки объекта. Читайте сразу после неудачного вызова. Если метод создания или поиска вернул NULL, ошибку нужно запросить у класса API, который Вы вызвали. |
Не сохраняйте объекты меток в JSON своего мода. Сохраняйте ID. При смене миссии или повторном подключении получите объект заново. Временные клиентские метки при отключении удаляются: старый ID не восстановит их, интеграция должна создать их снова по актуальным данным события. У недействительного объекта методы чтения могут вернуть последние известные значения: наличие названия в таком объекте не доказывает, что запись ещё существует.
Пример: отменить собственный предпросмотр. Для опубликованной метки нужно отказаться только от своих подготовленных правок, а не удалить запись у всех игроков. Если это черновик, публиковать нечего, его можно отменить через Delete().
bool CancelMarkerPreview(LXWM_ProtectedMarker PreviewMarker)
{
if (!PreviewMarker || !PreviewMarker.IsValid())
{
return false;
}
if (PreviewMarker.IsDraft())
{
return PreviewMarker.Delete();
}
if (PreviewMarker.HasPendingChanges() && !PreviewMarker.Refresh())
{
return false;
}
Print(string.Format("%1: %2", PreviewMarker.GetMarkerID(), Widget.TranslateString(PreviewMarker.GetMarkerName())));
return true;
}
Название, иконка и положение
Название можно передать обычным текстом на любом языке: например, SetMarkerName("Северный рынок"). Эту строку увидят игроки, которым доступна метка. Если название должно меняться вместе с языком игры, добавьте в stringtable.csv своего клиентского мода ключ NORTHMARKET_NORTH_DEPOT с нужными переводами, а в SetMarkerName передайте "#NORTHMARKET_NORTH_DEPOT". В таблице ключ пишется без #, в названии метки с #. Те же два варианта работают в первом аргументе CreateServerMarker, CreatePersonalMarker и CreateTemporaryMarker. Не переводите ключ на сервере: перевод нужен на стороне игрока. GetMarkerName() вернёт исходный текст или сам ключ.
| Сигнатура | Использование |
|---|---|
bool SetMarkerName(string NewValue)string GetMarkerName() |
Непустое название (до 256 по LengthUtf8). Например, SetMarkerName("Точка обмена"). Можно передать ключ перевода, доступный игрокам. |
bool SetMarkerIconID(string NewValue)string GetMarkerIconID() |
ID из каталога иконок, например SetMarkerIconID("map-pin"). При выборе ID прежний путь к собственной картинке сбрасывается. На клиенте список возвращает LXWM_GetClientOpenAPI().GetIconIDs(). |
bool SetMarkerIconPath(string NewValue)string GetMarkerIconPath() |
Первый метод задаёт путь к собственной текстуре вместо иконки Lucide. Второй возвращает этот путь, а если используется иконка из каталога, то пустую строку. GetMarkerIconID() по-прежнему возвращает ID Lucide, а не путь. |
bool SetWorldPosition(vector NewValue)vector GetWorldPosition() |
Игровой vector в метрах: X, высота Y, Z. Например, SetWorldPosition("6450 18.5 3200"). Если метка ставится на объект, передайте его GetPosition(), не координаты курсора в пикселях. |
bool SetColorRGB(array<int> NewValue)array<int> GetColorRGB() |
Массив из трёх целых чисел от 0 до 255: красный, зелёный, синий. Влияет на метку и её эффекты. Цвета зоны задаются отдельно. |
bool SetShowIconPanel(bool NewValue)bool GetShowIconPanel() |
false скрывает панель с иконкой. Это не выключает зону вокруг метки. |
bool SetShowInformation(bool NewValue)bool GetShowInformation() |
false скрывает подпись и расстояние. Чтобы на карте была только территория, включите зону, а у иконки и информации задайте false. |
Как поставить свою иконку
Добавьте текстуру .paa или .edds в клиентский мод и передайте путь от корня аддона, например NorthMarket/Assets/Icons/Trader.paa. Путь на диске P:, ссылка на сайт и путь через ../ не подойдут. Сервер передаёт игрокам только путь, не сам файл: даже для серверной метки картинка должна быть установлена у игроков. Метод проверяет формат пути, но не наличие файла у клиента.
Пример подходит для серверной, личной и временной клиентской метки, которую Ваш мод может редактировать. Сначала назначаем ID из каталога, затем собственную картинку и только после этого вызываем Apply():
bool SetTraderIcon(LXWM_ProtectedMarker TraderMarker)
{
if (!TraderMarker || !TraderMarker.SetMarkerIconID("map-pin"))
{
return false;
}
if (!TraderMarker.SetMarkerIconPath("NorthMarket/Assets/Icons/Trader.paa"))
{
Print(TraderMarker.GetLastError());
return false;
}
return TraderMarker.Apply();
}
Чтобы вернуть map-pin, вызовите TraderMarker.SetMarkerIconPath(""), затем TraderMarker.Apply(). Если нужна другая иконка из каталога, можно сразу вызвать TraderMarker.SetMarkerIconID("truck") и Apply(): отдельно очищать путь не нужно.
Как скопировать оформление
Например, для второй точки эвакуации можно взять оформление первой, но назначить своё название и положение. Цвет, полученный через GetColorRGB(), можно безопасно передать второй метке. Вызывайте для доступной на запись метки, у которой нет чужих незавершённых правок:
bool StyleEvacuationPoint(LXWM_ProtectedMarker TemplateMarker, LXWM_ProtectedMarker ExitMarker, vector ExitPosition)
{
if (!TemplateMarker || !TemplateMarker.IsValid() || !ExitMarker || !ExitMarker.IsValid())
{
return false;
}
ExitMarker.SetMarkerName("#NORTHMARKET_NORTH_DEPOT");
ExitMarker.SetWorldPosition(ExitPosition);
ExitMarker.SetMarkerIconID(TemplateMarker.GetMarkerIconID());
if (!ExitMarker.SetMarkerIconPath(TemplateMarker.GetMarkerIconPath()))
{
return false;
}
ExitMarker.SetColorRGB(TemplateMarker.GetColorRGB());
ExitMarker.SetShowIconPanel(TemplateMarker.GetShowIconPanel());
ExitMarker.SetShowInformation(TemplateMarker.GetShowInformation());
return ExitMarker.Apply();
}
Зона
| Сигнатура | Использование |
|---|---|
bool SetZoneEnabled(bool NewValue)bool GetZoneEnabled() |
Включить или выключить отрисовку зоны вокруг центра метки. Само по себе это не ограничивает получателей и не меняет правила игры внутри зоны. |
bool SetZoneShape(RZCO_MapZoneShape NewValue)int GetZoneShape() |
RZCO_MapZoneShape.CIRCLE или RZCO_MapZoneShape.RECTANGLE. Метод записи принимает enum, чтение возвращает его числовое значение. |
bool SetZoneRadiusMeters(float NewValue)float GetZoneRadiusMeters() |
Для круга: расстояние от центра до края, в метрах, не диаметр. |
bool SetZoneWidthMeters(float NewValue)float GetZoneWidthMeters() |
Для прямоугольника: полный размер с запада на восток, в метрах. |
bool SetZoneHeightMeters(float NewValue)float GetZoneHeightMeters() |
Для прямоугольника: полный размер с юга на север, в метрах. Не путайте с высотой Y. |
bool SetZoneBorderColorRGB(array<int> NewValue)array<int> GetZoneBorderColorRGB() |
Цвет обводки. Формат тот же, что у SetColorRGB. Штрихи, если игрок их включил, используют цвет обводки. |
bool SetZoneFillColorRGB(array<int> NewValue)array<int> GetZoneFillColorRGB() |
Цвет заливки. Массив цвета копируется, поэтому после вызова изменение исходного массива не перекрасит зону. |
bool SetZoneMergeSameColor(bool NewValue)bool GetZoneMergeSameColor() |
Объединять соприкасающиеся или пересекающиеся зоны. Включите у обеих меток и задайте одинаковые цвета и обводки, и заливки. Записи и их ID останутся разными. |
Допустимые радиус, ширина и высота: от 10 до 15000 метров. Не обнуляйте неиспользуемые размеры: при сохранении проверяется и радиус, и стороны прямоугольника. Чтобы выключить зону, передайте false в SetZoneEnabled. Чтение цветов возвращает копии массивов; для записи цвета передайте массив обратно соответствующему методу.
Пример: продлить территорию порта ещё на один причал. Передайте исходную прямоугольную зону и новый черновик. В примере обе зоны одинаковы по размеру и цветам, а центр второй смещён на одну полную ширину к востоку. Если у первой не включено соединение, метод оставит её как есть и вернёт false. Для круга скопируйте радиус через DockDraft.SetZoneRadiusMeters(PortZone.GetZoneRadiusMeters()) и задайте RZCO_MapZoneShape.CIRCLE.
bool ExtendPortZone(LXWM_ProtectedMarker PortZone, LXWM_ProtectedMarker DockDraft)
{
if (!PortZone || !PortZone.IsValid() || !PortZone.GetZoneEnabled() || !PortZone.GetZoneMergeSameColor())
{
return false;
}
if (PortZone.GetZoneShape() != RZCO_MapZoneShape.RECTANGLE || !DockDraft || !DockDraft.IsDraft())
{
return false;
}
vector DockPosition = PortZone.GetWorldPosition();
DockPosition[0] = DockPosition[0] + PortZone.GetZoneWidthMeters();
DockDraft.SetWorldPosition(DockPosition);
DockDraft.SetZoneEnabled(true);
DockDraft.SetZoneShape(RZCO_MapZoneShape.RECTANGLE);
DockDraft.SetZoneWidthMeters(PortZone.GetZoneWidthMeters());
DockDraft.SetZoneHeightMeters(PortZone.GetZoneHeightMeters());
DockDraft.SetZoneBorderColorRGB(PortZone.GetZoneBorderColorRGB());
DockDraft.SetZoneFillColorRGB(PortZone.GetZoneFillColorRGB());
DockDraft.SetZoneMergeSameColor(true);
return DockDraft.Apply();
}
Анимации
| Константа для вызова | Строковый ID | Как выглядит при скорости 1 |
|---|---|---|
RZCO_MarkerAnimationIDs.PING |
Ping |
Круг расходится от центра метки и постепенно исчезает. После небольшой паузы повторяется. |
RZCO_MarkerAnimationIDs.BREATH |
Breath |
Свечение плавно расширяется и становится ярче, затем сжимается и гаснет. |
RZCO_MarkerAnimationIDs.ROUND_DOTTED_LINE_SPIN |
RoundDottedLineSpin |
Вокруг метки медленно вращается пунктирное кольцо. Один оборот занимает 16 секунд. |
Это все встроенные эффекты. Передавайте в методы константу, а не русское название, имя класса или путь к картинке. Каждый эффект использует цвет метки.
| Сигнатура | Использование |
|---|---|
bool SetAnimationEnabled(string AnimationID, bool Enabled = true)bool IsAnimationEnabled(string AnimationID) |
Включить, выключить или проверить один эффект по ID. Включение уже активного эффекта не сбрасывает его скорость. |
bool SetAnimationSpeed(string AnimationID, float Speed)float GetAnimationSpeed(string AnimationID) |
Сначала включите эффект, затем задайте скорость от 0.05 до 10. 1 обозначает обычную скорость. Для выключенного эффекта метод чтения также возвращает 1, поэтому для проверки используйте IsAnimationEnabled. |
map<string, float> GetAnimations()bool SetAnimations(map<string, float> EnabledAnimations) |
Копия набора: ключом является ID включённого эффекта, значением его скорость. SetAnimations заменяет весь набор; пустой набор или NULL очищает его. |
bool DisableAllAnimations() |
Подготавливает отключение всех эффектов метки. Для применения вызовите Apply(). Оформление остальных меток не меняется. |
Например, так можно оформить место события: сделать круг радиусом 150 м, задать одинаковые цвета обводки и заливки и добавить два эффекта. Этот метод принимает уже полученную доступную для записи метку. Его можно применять как на сервере, так и к личной метке на клиенте, если разрешены зоны:
bool StyleEventMarker(LXWM_ProtectedMarker EventMarker)
{
if (!EventMarker || !EventMarker.IsValid())
{
return false;
}
array<int> EventColor = {230, 80, 65};
EventMarker.SetColorRGB(EventColor);
EventMarker.SetZoneEnabled(true);
EventMarker.SetZoneShape(RZCO_MapZoneShape.CIRCLE);
EventMarker.SetZoneRadiusMeters(150);
EventMarker.SetZoneBorderColorRGB(EventColor);
EventMarker.SetZoneFillColorRGB(EventColor);
EventMarker.SetZoneMergeSameColor(true);
EventMarker.SetAnimationEnabled(RZCO_MarkerAnimationIDs.BREATH);
EventMarker.SetAnimationEnabled(RZCO_MarkerAnimationIDs.ROUND_DOTTED_LINE_SPIN);
EventMarker.SetAnimationSpeed(RZCO_MarkerAnimationIDs.ROUND_DOTTED_LINE_SPIN, 0.75);
if (!EventMarker.Apply())
{
Print(EventMarker.GetLastError());
return false;
}
return true;
}
Пример: сменить сигналы у этапа события. Пока точка активна, оставим её исходные эффекты и добавим пинг с двойной скоростью. Когда этап закончится, отключим все эффекты одним вызовом. Изменение копии из GetAnimations() само по себе карту не обновляет:
bool SetEventSignals(LXWM_ProtectedMarker EventMarker, bool StageActive)
{
if (!EventMarker || !EventMarker.IsValid())
{
return false;
}
if (StageActive)
{
if (EventMarker.IsAnimationEnabled(RZCO_MarkerAnimationIDs.PING) && EventMarker.GetAnimationSpeed(RZCO_MarkerAnimationIDs.PING) == 2.0)
{
return !EventMarker.HasPendingChanges() || EventMarker.Apply();
}
map<string, float> StageEffects = EventMarker.GetAnimations();
StageEffects.Set(RZCO_MarkerAnimationIDs.PING, 2.0);
if (!EventMarker.SetAnimations(StageEffects))
{
return false;
}
}
else if (!EventMarker.DisableAllAnimations())
{
return false;
}
return EventMarker.Apply();
}
Эффекты можно сочетать. Включённые эффекты и их скорости сохраняются как одно поле, поэтому при одновременном изменении набора из двух объектов останется последний успешно применённый набор. Игрок может выключить воспроизведение у себя в мире или в меню карты, не меняя саму метку.