# Полная документация по разработке модов > Собрано из канонических Markdown-исходников. --- # Асинхронное программирование > Canonical URL: https://docs.wotstat.info/articles/adisp/ > Markdown URL: https://docs.wotstat.info/articles/adisp/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/articles/adisp/index.md Игра написана на `Python 2.7`, в котором отсутствует `async/await`, поэтому для асинхронного программирования используется библиотека `adisp`, позволяющая писать код в стиле `async/await`, используя корутины (генераторы). ## `adisp_process` Декоратор `@adisp_process` позволяет объявить процедуру как корутину, которая может использовать `yield` для ожидания завершения других асинхронных функций. Такая процедура **не может возвращать значения**. ```python from adisp import adisp_process @adisp_process def myAsyncFunction(): print('Before yield') result = yield someAsyncFunction() print('Result is:', result) myAsyncFunction() ``` ## `adisp_async` Декоратор `@adisp_async` позволяет объявить функцию, которую можно ожидать в `@adisp_process`, и которая **может вернуть результат** асинхронной операции посредством вызова `callback`. ```python from adisp import adisp_process, adisp_async @adisp_async def someAsyncFunction(callback): BigWorld.callback(1, lambda: callback(42)) # Симуляция асинхронной операции с задержкой @adisp_process def myAsyncFunction(): print('Before yield') result = yield someAsyncFunction() print('Result is:', result) myAsyncFunction() ``` ## Совмещение `adisp_process` и `adisp_async` Декораторы `@adisp_process` и `@adisp_async` можно совмещать для создания сложных асинхронных цепочек. ```python from adisp import adisp_process, adisp_async @adisp_async def someAsyncFunction(callback): BigWorld.callback(1, lambda: callback(42)) # Симуляция асинхронной операции с задержкой @adisp_async @adisp_process def anotherAsyncFunction(callback): print('Before yield in anotherAsyncFunction') result = yield someAsyncFunction() print('Result in anotherAsyncFunction is:', result) callback(result * 2) @adisp_process def myAsyncFunction(): print('Before yield in myAsyncFunction') result = yield anotherAsyncFunction() print('Result in myAsyncFunction is:', result) myAsyncFunction() ``` --- # Как создать контекстное меню > Canonical URL: https://docs.wotstat.info/articles/how-to-create-context-menu/ > Markdown URL: https://docs.wotstat.info/articles/how-to-create-context-menu/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/articles/how-to-create-context-menu/index.md ![demo](https://docs.wotstat.info/articles/how-to-create-context-menu/assets/demo.png) Контекстное меню — это всплывающее меню, которое появляется при нажатии правой кнопки мыши на элемент интерфейса. ## Императивный подход 1. Наследуем класс от `AbstractContextMenuHandler`. 2. Инициализируем хендлеры функций по схеме ИмяКнопки: имя_функции. 3. Реализуем `_generateOptions` (используем `self._makeItem` и `self._makeSeparator` для удобства). 4. Регистрируем наш хендлер по уникальному названию. 5. Вызываем по названию из Python или Flash. Кнопке можно передать произвольный текст, его цвет, состояние включенности. Но иконку можно установить только одну из доступных в клиенте; свою нельзя (в глубине реализации они берутся по номеру кадра из одной большой анимации). ```python from gui.Scaleform.framework.managers import context_menu from gui.Scaleform.framework.managers.context_menu import AbstractContextMenuHandler DEMO_CONTEXT_MENU = 'DEMO_CONTEXT_MENU' class BUTTONS(object): TEST1 = 'TEST1' TEST2 = 'TEST2' class DemoContextMenuHandler(AbstractContextMenuHandler): def __init__(self, cmProxy, ctx=None): super(WidgetContextMenuHandler, self).__init__(cmProxy, ctx, { BUTTONS.TEST1: 'onClickTest1', BUTTONS.TEST2: 'onClickTest2' }) @staticmethod def register(): context_menu.registerHandlers(*[(DEMO_CONTEXT_MENU, DemoContextMenuHandler)]) def _generateOptions(self, ctx=None): options = [] options.append(self._makeItem(BUTTONS.TEST1, 'Test 1 label')) options.append(self._makeSeparator()) options.append(self._makeItem(BUTTONS.TEST2, 'Test 2 label', { 'textColor': 13347959, 'iconType': 'addToSquad', 'enabled': True })) return options def onClickTest1(self): print('onClickTest1') def onClickTest2(self): print('onClickTest2') DemoContextMenuHandler.register() ``` ### Вызываем из питона ```python from helpers import dependency from skeletons.gui.app_loader import IAppLoader appLoader = dependency.instance(IAppLoader) # type: IAppLoader app = appLoader.getApp() if app: app.contextMenuManager.show('DEMO_CONTEXT_MENU', None) ``` ### Вызываем из флеша ```actionscript-3 App.contextMenuMgr.show('DEMO_CONTEXT_MENU'); ``` ## Декларативный подход Наследоваться от `ContextMenu` и использовать декоратор `@option(order, 'Label')` для методов-обработчиков кнопок. ```python from gui.Scaleform.daapi.view.lobby.shared.cm_handlers import ContextMenu, option class DemoContextMenuHandler(ContextMenu): @option(1, 'Test1') def onClickTest1(self): print('onClickTest1') @option(2, 'Test2') def onClickTest2(self): print('onClickTest2') ``` > **TIP — TODO** Этот способ можно расписать подробнее, с примерами вызова и определением дополнительных параметров, если это возможно. --- # Как создать виджет для стримов с удалённым управлением > Canonical URL: https://docs.wotstat.info/articles/how-to-create-remote-control-widget/ > Markdown URL: https://docs.wotstat.info/articles/how-to-create-remote-control-widget/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/articles/how-to-create-remote-control-widget/index.md В этой статье мы рассмотрим как **по шагам** создать виджет для стримов с удалённым управлением. Вы сможете повторить эту статью и получить рабочий виджет. Video: [open media](https://docs.wotstat.info/articles/how-to-create-remote-control-widget/assets/hero-widget.mp4) ## Подготовка Все виджеты являются обычными веб-сайтами, которые работают во встроенном браузере `OBS Studio`. Поэтому для создания виджета вам понадобится стандартный набор инструментов для веб-разработки. Для упрощения статьи мы не будет использовать никакие веб-фреймворки и реализуем всё на чистом `HTML`, `CSS` и `JavaScript`. Однако, всё ещё будет использовать сборку с помощью `Vite` (это такая программа, которая упрощает разработку и сборку проектов для последующей публикации). ### BunJS Установите `BunJS` - современный менеджер пакетов и среду выполнения `JavaScript`. Инструкции по установке можно найти на [официальном сайте](https://bun.sh/). **Windows** ```powershell powershell -c "irm bun.sh/install.ps1 | iex" ``` **Linux/MacOS** ```bash curl -fsSL https://bun.sh/install | bash ``` ### Visual Studio Code Так же вам понадобится `VSCode` - программа для редактирования кода, в которой мы будет разрабатывать, скачать можно на [официальном сайте](https://code.visualstudio.com/). ### Создание проекта Создайте новую папку для вашего проекта и откройте её в `VSCode`. Затем откройте терминал в `VSCode` (меню `Terminal` → `New Terminal`) и выполните команду: ```bash bun create vite . --template vanilla ``` После чего несколько раз нажмите `Enter`, чтобы принять значения по умолчанию. **Details — Вывод терминала после выполнения команды** ![vite-create-terminal-output](https://docs.wotstat.info/articles/how-to-create-remote-control-widget/assets/vite-create-terminal-output.png) У вас будет настроен проект, установленные зависимости и запущен локальный сервер для разработки (обычно по адресу `http://localhost:5174/`). Откройте этот адрес в браузере, вы должны увидеть стартовую страницу `Hello Vite!`. Очистить стартовую страницу, для этого, в `VSCode` удалим папку `public` со всем её содержимым, а так же файлы `src/counter.js` и `src/javascript.svg`. Отредактируем файл `src/main.js`, удалив из него всё содержимое кроме подключения стилей: **src/main.js** ```javascript import './style.css' ``` А так же, полностью очистим файл `src/style.css`, оставив его пустым. Готово! Каждое изменение в файлах будет автоматически отображаться в браузере благодаря `Vite`. Теперь мы видим пустую белую страницу. ## Создание виджета Разработка виджета будет состоять из двух этапов: - Создадим визуальное оформление виджета - Добавим функционал удалённого управления ### Визуальное оформление виджета Оформление виджета максимально индивидуально и зависит от вашей задачи, процесс совершенно ничем не отличается от создания обычного веб-сайта. В этой статье мы создадим простой счётчик ЛБЗ. Он состоит их трёх блоков, в каждом из которых есть прогресс-бар и число выполненных ЛБЗ. Единственное отличие от обычного веб-сайта - это размеры виджета. Мы будем делать автоматическую подгонку под ширину экрана, это позволит стримеру использовать виджет на на любых разрешениях. Для этого мы установим размер шрифта равным `1%` от ширины экрана, а все стили будем указывать в `em`. **src/style.css** ```css :root { /* Прозрачный фон */ color-scheme: dark; background: transparent; } body { /* Устанавливаем размер шрифта в зависимости от ширины окна */ font-size: 1vw; /* Убираем отступ и скрываем скролл */ margin: 0; overflow: hidden; } ``` Теперь создадим общую структуру виджета в файле `index.html`: **index.html** ```html ...

VI - VII уровень

18 / 25

... ``` И дважды продублируем блок `div.item`, изменив текст заголовка `h2`, на последний `div.item` добавим класс `completed`. Для оформления нам понадобятся некоторые аасеты из игры, найти их можно в пакете `gui.pkg` или в репозитории [Kurzdor/wot.assets](https://github.com/Kurzdor/wot.assets/tree/Lesta) - Шрифт можно найти по пути [`gui/gameface/fonts/Warhelios-Regular.ttf`](https://github.com/Kurzdor/wot.assets/tree/Lesta/gui/gameface/fonts/Warhelios-Regular.ttf) - Детали проггресс бара по пути [`/gui/maps/icons/components/progress_bar`](https://github.com/Kurzdor/wot.assets/tree/Lesta/gui/maps/icons/components/progress_bar) - Галочка о выполненном этапе по пути [`/gui/maps/icons/personalMissions3/QuestsView/complete.png`](https://github.com/Kurzdor/wot.assets/tree/Lesta/gui/maps/icons/personalMissions3/QuestsView/complete.png) Скопируйте нужные ассеты в папку `src/assets` вашего проекта. Настройте стили в файле `style.css`, чтобы получить следующий результат: **Details — Полный код файла `style.css`** **src/style.css** ````css :root { /* Прозрачный фон */ color-scheme: dark; background: transparent; } @font-face { font-family: Warhelios; src: url(./assets/Warhelios-Regular.ttf) format("truetype"); } body { /* Устанавливаем размер шрифта в зависимости от ширины окна */ font-size: 1vw; /* Убираем отступ и скрываем скролл */ margin: 0; overflow: hidden; font-family: Warhelios, sans-serif; } #app { display: flex; gap: 1em; padding: 1em; } .item { border-radius: 1.3em; padding: 1.2em; border: 0.25em solid rgba(255, 173, 65, 0.4); background: linear-gradient(0deg, rgba(195, 90, 4, 0.1), rgba(255, 255, 255, 0) 50%), radial-gradient(farthest-corner at 50% 200%, rgba(251, 166, 20, 0.4), rgba(218, 139, 1, 0.3) 90%); display: flex; flex-direction: column; flex: 1; max-width: 30%; box-shadow: 0 0 1em rgba(0, 0, 0, 0.2); } .completed { border: 0.25em solid rgba(106, 255, 65, 0.3); background: linear-gradient(0deg, rgba(80, 255, 27, 0.3), rgba(26, 255, 0, 0) 50%), radial-gradient(farthest-corner at 50% 200%, rgba(4, 102, 5, 0.4), rgba(13, 114, 0, 0.3) 90%); } header { font-size: 1em; display: flex; } header h2 { flex: 1; } header h2, header p { font-size: 2.7em; margin: 0; font-weight: normal; } .current { color: #ffd28f; } img { width: 9em; height: 9em; margin: -3em; display: none; } /* Прогресс бар */ .progress-bar { height: 0.3em; width: 100%; margin-top: 1em; position: relative; } /* Фоновая часть прогресс бара (серые прямоугольники) */ .progress-bar-background { background-image: url(./assets/pattern_grey.png); background-repeat: repeat; background-position: 0 50%; background-size: 0.4em 1em; position: absolute; width: 100%; height: 100%; box-shadow: 0 0 2em rgba(0, 0, 0, 0.5); } /* Текущее значение прогресса (блик) */ .progress-bar-blink { height: 5em; width: 5em; background-image: url(./assets//glow_small.png); background-size: contain; mix-blend-mode: lighten; position: absolute; transform: translate(-49%, -50%); top: 50%; left: var(--progress, 60%); transition: left 0.2s ease; } /* Заполненная часть прогресс бара */ .progress-bar-pattern { background-image: url(./assets/pattern_orange.png); background-repeat: repeat; background-position: 0 50%; background-size: 0.4em 1em; position: absolute; height: 100%; width: var(--progress, 60%); transition: width 0.2s ease; } /* Градиентная подсветка прогресс бара */ .progress-bar-pattern::after { content: ""; position: absolute; right: 0; top: 0; width: 100%; height: 100%; background-image: linear-gradient(90deg, rgba(0, 0, 0, 0.6), rgba(255, 206, 122, 0.5)); mix-blend-mode: overlay; } /* Переопределяет для завершенных этапов */ .completed .progress-bar-pattern { background-image: url(./assets/pattern_green.png); width: 100%; } .completed .progress-bar-pattern::after { background-image: linear-gradient(90deg, rgba(0, 0, 0, 0.6), rgba(187, 255, 78, 0.519), rgba(0, 0, 0, 0.6)); } .completed .progress-bar-blink, .completed header p { display: none; } .completed img { display: block; } ```` **Details — Полный код файла `index.html`** **src/index.html** ````html article-pm-widget

VI - VII уровень

18 / 25

VII - IX уровень

18 / 25

X - XI уровень

18 / 25

```` В результате получаем следующий виджет: ![widget-prepare-result.png](https://docs.wotstat.info/articles/how-to-create-remote-control-widget/assets/widget-prepare-result.png) ### Функционал удалённого управления Теперь добавим функционал удалённого управления. Для этого установим библиотеку [`wotstat-widgets-sdk`](https://www.npmjs.com/package/wotstat-widgets-sdk), и объявим в ней удалённые параметры нашего виджета. В терминале выполните команду: **Windows** ```powershell bun add wotstat-widgets-sdk ``` **Linux/MacOS** ```bash bun add wotstat-widgets-sdk ``` В файле `src/main.js` подключим библиотеку и объявим параметры виджета: **src/main.js** ```javascript import './style.css' import { WidgetsRemote } from 'wotstat-widgets-sdk'; const remote = new WidgetsRemote(); remote.defineState('VI - VII уровень', 0, { elementHelper: '.item-1' }); remote.defineState('VII - IX уровень', 0, { elementHelper: '.item-2' }); remote.defineState('X - XI уровень', 0, { elementHelper: '.item-3' }); ``` Теперь можем открыть нашу ссылку в панели управления виджетами на сайте [ru.widgets.wotstat.info/remote-control](https://ru.widgets.wotstat.info/remote-control) и убедиться, что параметры виджета появились в списке, а при наведение мышки обводится нужный блок виджета: ![widget-remote-control-params.png](https://docs.wotstat.info/articles/how-to-create-remote-control-widget/assets/widget-remote-control-params.png) Обводка работает благодаря свойству `elementHelper`, в котором мы указали CSS-селектор нужного блока. Далее нам нужно добавить обработку изменения параметров. Для создадим функцию `updateState`, и подпишемся на изменения каждого параметра с помощью метода `watch`: **src/main.js** ```javascript ... function updateState(selector, value) { const element = document.querySelector(selector); // Устанавливаем значение счетчика element.querySelector('.current').textContent = value; // Переключаем класс завершенного этапа if (value >= 25) element.classList.add('completed'); else element.classList.remove('completed'); // Добавляем CSS-переменную для прогресс-бара element.style.setProperty('--progress', `${100 * value / 25}%`); } remote.defineState('VI - VII уровень', 0, { elementHelper: '.item-1' }) .watch((v) => updateState('.item-1', v)); remote.defineState('VII - IX уровень', 0, { elementHelper: '.item-2' }) .watch((v) => updateState('.item-2', v)); remote.defineState('X - XI уровень', 0, { elementHelper: '.item-3' }) .watch((v) => updateState('.item-3', v)); ``` Готово! Теперь при изменении параметров в панели управления, виджет будет обновляться автоматически. ## Публикация виджета Остался последний шаг - публикация виджета, что бы он был доступен для использования через интернет. Для начала нужно скомпилировать проект. В терминале выполните команду: **Windows** ```powershell bun run build ``` **Linux/MacOS** ```bash bun run build ``` В результате чего будет создана папка `dist`, в которой будет находиться готовый к публикации сайт. ![terminal-build-output](https://docs.wotstat.info/articles/how-to-create-remote-control-widget/assets/terminal-build-output.png) Этот сайт можно разместить на любом хостинге статических сайтов, например: - [wasmer.io](https://wasmer.io/) бесплатно после регистрации (можно через Google аккаунт) - [Яндекс Облако](https://cloud.yandex.ru/) в режиме S3 хостига, бесплатно, но регистрация сложная - [GitHub Pages](https://docs.github.com/en/pages/quickstart/) бесплатно, но требует регистрации и создания репозитория на GitHub ### Добавление в OBS После публикации, вставьте ссылку на виджет в [панель удалённого управления](https://ru.widgets.wotstat.info/remote-control), сгенерируйте ключ доступа и добавьте виджет в `OBS Studio` как `Browser Source`, указав сгенерированную ссылку. > Не забудьте выбрать `Blending Method` -> `SRGB off`, подробнее можно изучить [руководстве](https://docs.wotstat.info/guide/widgets/stream/index.md#widget-adding) ## Исходный код Весь исходный код виджета доступен в репозитории на [GitHub](https://github.com/SoprachevAK/mt-pm3-progress-widget), а результат опубликован в GitHub Pages по ссылке: [soprachevak.github.io/mt-pm3-progress-widget](https://soprachevak.github.io/mt-pm3-progress-widget/) и в панели управления [ru.widgets.wotstat.info/remote-control](https://ru.widgets.wotstat.info/remote-control?widget-url=aHR0cHM6Ly9zb3ByYWNoZXZhay5naXRodWIuaW8vbXQtcG0zLXByb2dyZXNzLXdpZGdldC8/cmVtb3RlLWtleT1oNXpvOVhFZ0NT) --- # Как работать с Dependency Injections > Canonical URL: https://docs.wotstat.info/articles/how-to-work-with-di/ > Markdown URL: https://docs.wotstat.info/articles/how-to-work-with-di/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/articles/how-to-work-with-di/index.md Dependency Injection (DI) — это шаблон проектирования, который позволяет упростить управление зависимостями между компонентами программы. Часто бывает, что есть некоторые служебные классы, которые необходимы в большом количестве других мест. Самый простой способ реализации — создать глобальный экземпляр, который будет доступен отовсюду. Например, такой подход используется с `g_eventBus`: ```python from gui.shared import g_eventBus ``` Однако этот подход имеет множество недостатков: жёсткая связь между классами, сложность тестирования и трудности в управлении жизненным циклом объектов. Чтобы решить эти проблемы был придуман шаблон Dependency Injection. ## Инъекция зависимостей Чтобы получить ссылку на класс, в игре используется модуль `helpers.dependency`. Классы, которые можно получить через DI, определяются их интерфейсами, что позволяет легко заменять реализации без изменения кода, который их использует. Одним из таких интерфейсов является распространённый `IItemsCache`, который предоставляет доступ к данным об игроке, его танках, экипаже и т. д. Получить ссылку на реализацию интерфейса можно несколькими способами: - `@dependency.replace_none_kwargs` – заменяет параметры функции - `dependency.instance` – получить экземпляр здесь и сейчас - `dependency.descriptor` – определить зависимость как дескриптор класса Если у вас настроен VSCode с подсказками для игры, необходимо ставить аннотации типов, чтобы редактор понимал, какой тип будет возвращён. Аннотации ставятся через комментарий `# type: <тип>`. ### @dependency.replace_none_kwargs Позволяет автоматически внедрять зависимости в параметры функции или метода, если они не были переданы явно. ```python from helpers import dependency from skeletons.gui.shared import IItemsCache @dependency.replace_none_kwargs(itemsCache=IItemsCache) def demo(foo, bar, itemsCache=None): # type: (str, str, IItemsCache) -> None print(itemsCache) demo('foo', 'bar') # itemsCache будет автоматически внедрён ``` ### dependency.instance Позволяет получить экземпляр здесь и сейчас. ```python from helpers import dependency from skeletons.gui.shared import IItemsCache itemsCache = dependency.instance(IItemsCache) # type: IItemsCache print(itemsCache) ``` ### dependency.descriptor Позволяет определить зависимость как дескриптор класса. Зависимость будет автоматически внедрена при создании экземпляра класса. ```python from helpers import dependency from skeletons.gui.shared import IItemsCache class Demo: itemsCache = dependency.descriptor(IItemsCache) # type: IItemsCache def showItemsCache(self): print(self.itemsCache) demo = Demo() ``` ## Полезные интерфейсы Вот список некоторых полезных интерфейсов, которые могут пригодиться при разработке модов > **TIP — TODO** Расписать подробнее ### `IItemsCache` Предоставляет доступ к данным об игроке, его танках, экипаже, бейджах, достижениях и т.д. ```python from helpers import dependency from skeletons.gui.shared import IItemsCache itemsCache = dependency.instance(IItemsCache) # type: IItemsCache print(itemsCache.items.stats) # Доступ к статистике игрока print(itemsCache.items.getVehicles()) # Доступ к танкам игрока ``` ### `IHangarSpace` Предоставляет доступ к пространству ангара, позволяет подписываться на события создания и уничтожения пространства. ```python from helpers import dependency from skeletons.gui.shared.utils import IHangarSpace hangarSpace = dependency.instance(IHangarSpace) # type: IHangarSpace hangarSpace.onSpaceCreate += lambda: print("Ангар создан") hangarSpace.onSpaceDestroy += lambda: print("Ангар уничтожен") hangarSpace.onVehicleChanged += lambda: print("Танк в ангаре изменён") ``` ### `IBattleSessionProvider` Предоставляет доступ к данным о бое, позволяет подписываться на события начала и окончания боя. ```python from helpers import dependency from skeletons.gui.battle_session import IBattleSessionProvider sessionProvider = dependency.instance(IBattleSessionProvider) # type: IBattleSessionProvider sessionProvider.onBattleSessionStart += lambda: print("Бой начался") sessionProvider.onBattleSessionStop += lambda: print("Бой закончился") print(sessionProvider.isReplayPlaying) # Проверка, находится ли в данный момент воспроизведение реплея ``` ### `IEventsCache` Предоставляет доступ к данным о событиях, таких как кланы, рейтинговые бои. ```python from helpers import dependency from gui.server_events import IEventsCache eventsCache = dependency.instance(IEventsCache) # type: IEventsCache print(eventsCache.getAllQuests()) # Доступ ко всем задачам print(eventsCache.getPersonalMissions()) # Доступ к ЛБЗ ``` ### `ILobbyContext` ??? ```python from helpers import dependency from skeletons.gui.lobby_context import ILobbyContext lobbyContext = dependency.instance(ILobbyContext) # type: ILobbyContext ``` --- # Мультизапуск > Canonical URL: https://docs.wotstat.info/articles/multilaunch/ > Markdown URL: https://docs.wotstat.info/articles/multilaunch/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/articles/multilaunch/index.md > **DANGER — Внимание!** Мультизапуск может быть воспринят как запрещённая модификация. Используйте его **ИСКЛЮЧИТЕЛЬНО** для тестирования модов в тренировочных комнатах или на тестовых серверах. При разработке боевых модов их нужно где‑то тестировать. Есть несколько вариантов: - Режим "Топография". Доступен только на WG: позволяет войти в бой с ботами, которые будут стоять на месте и стрелять в вас при прямой видимости. - Режим "Полигон". Формируются реальные бои с игроками против ботов. Боты управляются искусственным интеллектом, но статистика таких боёв не учитывается в общей статистике аккаунта, и союзники не сильно обижаются, если вы во время тестирования никак не участвуете в бою. - Тренировочные комнаты. Необходим мультизапуск, так как для запуска тренировочной комнаты требуется минимум два аккаунта. ## WGC Multilaunch Самый простой способ запустить несколько копий игры — использовать программу `wgc.multilaunch`: она сбрасывает состояние лаунчера после запуска клиента игры. 1. Скачайте актуальную версию `wgc_multilaunch.exe` из [официального репозитория](https://gitlab.com/openwg/wgc.multilaunch/-/releases). 2. Запустите клиент игры любым способом (через лаунчер, PjOrion и т. д.) 3. Дождитесь входа в ангар. 4. Запустите `wgc_multilaunch.exe` — программа сбросит состояние лаунчера и тут же закроется (никаких окон не появится). 5. Снова запустите клиент игры, любым способом. 6. Повторяйте шаги 4–5 для запуска нужного количества копий. ## Песочница Windows Для запуска нескольких копий игры можно использовать встроенный компонент «Песочница Windows». 1. Откройте меню «Пуск» и введите «Включение или отключение компонентов Windows». 2. В открывшемся списке найдите и отметьте галочкой пункт «Песочница Windows». 3. Нажмите «ОК» и дождитесь завершения установки компонента. Возможно, потребуется перезагрузка компьютера. Теперь вы можете запустить игру через песочницу: > **TIP — TODO** Если вы знаете, как через песочницу запускать игру (в идеале с соединением к PjOrion), опишите, пожалуйста, этот процесс и отправьте PR. Лучше всего с картинками. --- # Получаем статистические данные среднего урона на технике внутри клиента > Canonical URL: https://docs.wotstat.info/articles/vehicle-moe/ > Markdown URL: https://docs.wotstat.info/articles/vehicle-moe/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/articles/vehicle-moe/index.md Ниже показано, как из клиента запросить у сервера распределение среднего урона по технике. ## Коротко об идее Для отправки запросов на сервер используются методы `doCmd()`. В нашем случае — метод `doCmdInt()` (он принимает один целочисленный аргумент). Вообще это семейство методов: - `doCmdStr()` — принимает строковый аргумент; - `doCmdInt2()` — принимает два целочисленных аргумента; - и т.д. На клиенте аналогичные методы реализованы в классе `PlayerAccount` — сущности аккаунта игрока (также существуют `PlayerLogin`, `PlayerAvatar`). Конкретная сущность возвращается в зависимости от пространства, из которого вызывается `BigWorld.player()`. Чтобы получить сущность аккаунта игрока, вызывать `BigWorld.player()` нужно, находясь в лобби (ангаре). ## Пример кода запроса ```python import BigWorld from gui.shared.personality import ServicesLocator from skeletons.gui.app_loader import GuiGlobalSpaceID # Имя команды для получения статистических данных from AccountCommands import CMD_GET_VEHICLE_DAMAGE_DISTRIBUTION # Идентификатор (числовой компакт-дескриптор) для ИС-7 intCompactDescr = 7169 def callback(requestID, responseID, errorStr, ext=None): print('response is {}'.format(ext)) def onGUISpaceEntered(spaceID): if spaceID == GuiGlobalSpaceID.LOBBY: playerAccount = BigWorld.player() playerAccount._doCmdInt( CMD_GET_VEHICLE_DAMAGE_DISTRIBUTION, intCompactDescr, callback ) ServicesLocator.appLoader.onGUISpaceEntered += onGUISpaceEntered ``` ## Вспомогательные методы `PlayerAccount` ```python class PlayerAccount(...): ... # Вызывает self.base.doCmdStr. # Callback вызывается при получении ответа: callback(requestID, resultID). def _doCmdStr(self, cmd, s, callback): return self.__doCmd('doCmdStr', cmd, callback, s) # Вызывает self.base.doCmdIntStr. # Callback вызывается при получении ответа: callback(requestID, resultID). def _doCmdIntStr(self, cmd, int1, s, callback): return self.__doCmd('doCmdIntStr', cmd, callback, int1, s) # Вызывает self.base.doCmdInt. # Callback вызывается при получении ответа: callback(requestID, resultID). def _doCmdInt(self, cmd, int_, callback): return self.__doCmd('doCmdInt', cmd, callback, int_) # Вызывает self.base.doCmdInt2. # Callback вызывается при получении ответа: callback(requestID, resultID). def _doCmdInt2(self, cmd, int1, int2, callback): return self.__doCmd('doCmdInt2', cmd, callback, int1, int2) ... ``` Все эти методы вызывают приватный метод `__doCmd()`, в который передаются название серверного метода, код команды, необходимые аргументы и `callback` для серверного ответа. Таким образом, помимо аргумента, соответствующего типу `doCmd()`-команды, при обращении через `PlayerAccount` метод должен принимать: - ID команды (в нашем случае `AccountCommands.CMD_GET_VEHICLE_DAMAGE_DISTRIBUTION`); - аргумент для команды; - колбэк. ## Формат ответа В случае **успешного** ответа от сервера в колбэк приходит 4 аргумента: - `requestID` — ID запроса; - `resultID` — код состояния ответа; - `errorStr` — строка ошибки (при успешном ответе — пустая строка `''`); - `ext` *(extended information)* — распакованный ответ от сервера. В случае **отрицательного** ответа передаются 3 аргумента (без `ext`). > Ограничение: не более **1 запроса в секунду**. Если лимит превышен, сервер вернёт `resultID = -5`, а `errorStr = "COOLDOWN"`. ## Пример успешного ответа Для ИС-7 (`intCompactDescr = 7169`) структура ответа (`ext`) имеет вид: ```python { 'battleCount': 1299652, 'maxDamage': 4974, 'distForMarkOnGun': (2429, 3472, 4304), 'fullDamageDist': [ ((0, 20), (0, 613)), ((20, 40), (613, 1498)), ((40, 55), (1498, 2046)), ((55, 65), (2046, 2429)), ((65, 75), (2429, 2891)), ((75, 85), (2891, 3472)), ((85, 95), (3472, 4304)), ((95, 100), (4304, 4974)) ], 'damageBetterThanNPercent': (0, 613, 1498, 2046, 2429, 2891, 3472, 4304, 4974) } ``` ### Пояснения к полям - `maxDamage` — максимальный средний урон на технике, соответствующий 100%. - `distForMarkOnGun` — значения среднего урона для отметок на стволе: 1-й (65%), 2-й (85%), 3-й (95%). - `fullDamageDist` — список кортежей; каждый соответствует интервалу среднего урона. Пример: `((20, 40), (613, 1498))`, где: - первый вложенный кортеж — это процентили (20% и 40%), - второй — значения среднего урона для этих процентов (613 для 20% и 1498 для 40%). --- # Как работают прицелы > Canonical URL: https://docs.wotstat.info/guide/crosshair/how-it-works/ > Markdown URL: https://docs.wotstat.info/guide/crosshair/how-it-works/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/crosshair/how-it-works/index.md Прицелы в Мир Танков разделены на три SWF-файла: - снайперский - аркадный - артиллерийский У прицела есть чётко определённая иерархическая структура, заменяя элементы которой можно изменять внешний вид прицела, при этом не потеряв его функциональность. Прицелы заменяются путём подмены соответствующих SWF-файлов, что позволяет разрабатывать их без применения Python. Вариации прицела (которые можно изменить в настройках игры) реализуются путём изменения структуры на разных кадрах (`keyframe`) внутри одного SWF-файла. > **TIP — TODO** Подробно расписать структуру прицелов, как с ней работать и как её изменять. С картинками. --- # Создание собственного модпака > Canonical URL: https://docs.wotstat.info/guide/distribution/create-modpack/ > Markdown URL: https://docs.wotstat.info/guide/distribution/create-modpack/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/distribution/create-modpack/index.md Один из способов распространения своих модов — создание модпака: приложения, содержащего несколько модов, которые автоматически устанавливаются в клиент игры. Создать базовый модпак не очень сложно, потому что для этого есть готовые инструменты, самый распространённый из которых — Inno Setup. ![modpack-example](https://docs.wotstat.info/guide/distribution/create-modpack/assets/modpack-example.png) > **TIP — TODO** Я ничего не знаю про Inno Setup, поэтому если вы знаете, как с его помощью создать модпак, то можете написать инструкцию сюда. Ещё есть вот такая штука чтоб находить куда установлен клиент https://github.com/Kurzdor/wot.clientdetection --- # Форум > Canonical URL: https://docs.wotstat.info/guide/distribution/forum/ > Markdown URL: https://docs.wotstat.info/guide/distribution/forum/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/distribution/forum/index.md Официальным местом для размещения ваших модификаций является [форум «Мир Танков»](http://forum.tanki.su/index.php?/forum/173-%D0%BC%D0%BE%D0%B4%D1%8B-%D0%B8-%D1%81%D0%BE%D1%84%D1%82/), раздел с модами. Здесь вы можете найти существующие актуальные моды, задавать вопросы их разработчикам и обсудить с другими игроками. Кроме того, вы можете свободно публиковать свои моды, делиться ими с сообществом и получать отзывы. Перед публикацией убедитесь, что мод соответствует [правилам форума](http://forum.tanki.su/index.php?/forum/969-%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D0%B0-%D1%80%D0%B0%D0%B7%D0%B4%D0%B5%D0%BB%D0%B0%D0%B8%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%86%D0%B8%D1%8F/). Если вы разместите мод в подходящем разделе и будете актуализировать его по мере выхода новых версий игры, вы можете получить специальную роль `Мододел`, которая даст вам некоторые преимущества (возможно, в будущем; пока никаких). ## МОСТ МОСТ – это [официальный модпак](http://forum.tanki.su/index.php?/topic/2205036-all-%D0%BC%D0%BE%D1%81%D1%82/) от разработчиков игры, его особенностью является автоматическое обновление модов без необходимости скачивать новую версию модпака. Если ваш мод окажется востребованным в сообществе, то вы можете претендовать на включение его в МОСТ. Это откроет вам доступ к широкой аудитории игроков и повысит популярность вашего мода. ![screenshot](https://docs.wotstat.info/guide/distribution/forum/assets/most.png) --- # Модпаки > Canonical URL: https://docs.wotstat.info/guide/distribution/modpacks/ > Markdown URL: https://docs.wotstat.info/guide/distribution/modpacks/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/distribution/modpacks/index.md Один из самых простых способов распространения модов на большую аудиторию — заинтересовать владельцев модпаков включить ваш мод в их сборку. Для этого мод должен быть качественным, полезным и не конфликтовать с другими популярными модами. Также важно, чтобы мод был совместим с последней версией игры и регулярно обновлялся. Сейчас существуют следующие популярные модпаки: - [МОСТ](http://forum.tanki.su/index.php?/topic/2205036-all-%D0%BC%D0%BE%D1%81%D1%82/) — официальный модпак Lesta. Принимают моды размещённые в [разделе форума](http://forum.tanki.su/index.php?/forum/851-%D0%BC%D0%BE%D0%B4%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D0%B8-%D0%BA%D0%BB%D0%B8%D0%B5%D0%BD%D1%82%D0%B0/). - [Jove's Mod Pack](https://joves-modpack.ru/) — легко идут на контакт и готовы оперативно добавлять вариативные **полезные** моды. - [ProTanki](https://protanki.tv/ru/) — один из самых популярных модпаков; многие моды разрабатываются командой, поэтому сложно предложить свой. Кроме того, многие крупные блогеры имеют собственные небольшие сборки модов, которые рекомендуют подписчикам. Зачастую моды в этих сборках минималистичны и сделаны под заказ, однако если ваш мод действительно полезен, блогеры могут заинтересоваться его добавлением. - [Nearyou modpack](https://nearyou.team/modpack/) — сборка от Nearyou Team. - [LeBwa modpack](https://lebwa.tv/hub/modpack-lebwa) — сборка от LeBwa. - [NIDIN modpack](https://nidin.ru/mods) — сборка от \_\_NIDIN\_\_. --- # Автоматизация сборки модов > Canonical URL: https://docs.wotstat.info/guide/first-steps/automatization/ > Markdown URL: https://docs.wotstat.info/guide/first-steps/automatization/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/first-steps/automatization/index.md Автоматизация сборки позволяет избавить вас от рутинных задач и значительно упростить процесс дистрибуции модов. Для автоматизации нужно создать репозиторий с исходным кодом и настроить CI/CD‑пайплайн, который будет компилировать вашу модификацию при установке тега в системе контроля версий. ## Необходимые инструменты - Настроенное окружение для `Python`‑модов (см. [инструкцию по настройке окружения для Python](https://docs.wotstat.info/guide/first-steps/environment/python/index.md)) - [Git](https://git-scm.com/) — система контроля версий, с помощью которой будет загружен исходный код игры. Весь процесс полностью бесплатен. ## Создание репозитория системы контроля версий В качестве хранилища исходного кода можно использовать любой популярный сервис, однако наиболее распространён — [GitHub](https://github.com/). На его примере рассмотрим процесс автоматизации сборки. Если у вас ещё нет аккаунта на GitHub — создайте его. GitHub позволяет бесплатно создавать как публичные, так и приватные репозитории с исходным кодом и отслеживанием изменений. 1. На главной странице GitHub нажмите на свой профиль и выберите `Repositories`. **Details — Меню профиля** ![Repositories](https://docs.wotstat.info/guide/first-steps/automatization/assets/go-to-repos.png) 2. Нажмите на кнопку `New` для создания нового репозитория. - Придумайте название репозитория, например `my-first-mod`. - Выберите `Public` или `Private` в зависимости от того, хотите ли вы, чтобы ваш мод был доступен всем или только вам. - Не меняйте остальные настройки и нажмите на кнопку `Create repository`. ### Связывание локального проекта с репозиторием После создания репозитория вам будет показана страница с инструкцией по связыванию локального проекта с удалённым репозиторием. Но перед этим необходимо подготовить локальный проект. В корне вашего проекта создайте файл `.gitignore` со следующим содержимым: **.gitignore** ``` *.pyc *.wotmod *.mtmod wot-src/ as3/bin ``` Этот файл указывает Git, какие файлы и папки не нужно отслеживать. Мы исключаем скомпилированные файлы, архивы модов и папки с исходным кодом игры. После этого откройте терминал в VSCode (`` Ctrl+` `` или `Terminal -> New Terminal`) и выполните команды, чтобы связать локальный проект с удалённым репозиторием. Инициализируйте локальный репозиторий: ```powershell git init ``` Задайте имя основной ветки: ```powershell git branch -M main ``` Привяжите удалённый репозиторий, заменив `` и `` на свои значения: ```powershell git remote add origin https://github.com//.git ``` > Ссылку на репозиторий можно найти на странице репозитория в GitHub, она будет выглядеть примерно так: `https://github.com/SoprachevAK/my-first-mod.git`. ### Отправка первого коммита Коммит (commit) — зафиксированное изменение в проекте. Коммиты позволяют отслеживать историю, возвращаться к предыдущим версиям и работать над проектом совместно. Чтобы отправить первый коммит, откройте в VSCode вкладку `Source Control` и в подразделе `Changes` у вас будут отображаться все файлы, которые были изменены или добавлены в вашем проекте. ![Source Control](https://docs.wotstat.info/guide/first-steps/automatization/assets/source-control.png) Наведите курсор на `Changes` и нажмите на иконку `+`, чтобы отметить все файлы как готовые к коммиту (Staged Changes). Введите сообщение, например `Initial commit`, и нажмите `Commit`. ![Initial commit](https://docs.wotstat.info/guide/first-steps/automatization/assets/initial-commit.png) **Details — Если вылезла ошибка** Если вы раньше не настраивали имя пользователя и email в Git, при попытке сделать коммит может появиться ошибка. ![Name and email error](https://docs.wotstat.info/guide/first-steps/automatization/assets/name-error.png) В этом случае откройте терминал в VSCode (`` Ctrl+` `` или `Terminal -> New Terminal`) и выполните следующие команды, заменив `` и `` на ваши значения: ```powershell git config --global user.name "" git config --global user.email "" ``` Это установит имя и email для всех будущих коммитов. Затем повторите попытку. Если не хотите раскрывать email, используйте служебный адрес GitHub: перейдите в [Настройки профиля GitHub -> Emails](https://github.com/settings/emails), включите `Keep my email addresses private` и используйте адрес вида `@users.noreply.github.com`. После того, как вы сделали коммит, вам нужно отправить его в удалённый репозиторий. Для этого в том же месте нажмите на кнопку `Publish branch`. > Если это первый пуш в репозиторий, потребуется авторизация через GitHub‑аккаунт. Готово! Вы успешно отправили первый коммит в удалённый репозиторий. Теперь вы можете обновить страницу вашего репозитория на GitHub и увидеть там ваши файлы. ![GitHub repo](https://docs.wotstat.info/guide/first-steps/automatization/assets/gh-repo.png) ## Настройка автоматической сборки Автоматизация в GitHub осуществляется с помощью GitHub Actions. Это инструмент, который предоставляет виртуальную машину для выполнения скриптов. На ней мы запускаем `build`‑скрипт, а результат (архив с модом) загружается в релизы репозитория. Для настройки автоматической сборки создайте папку `.github/workflows`, скачайте [файл release.yaml](https://docs.wotstat.info/download/mod-build/release.yaml) и поместите его туда. Так как в GitHub Actions используется `Ubuntu`, а наш скрипт сборки написан для `Windows`, нужно создать аналогичный `build.sh` для `Linux`. Скачайте [файл build.sh](https://docs.wotstat.info/download/mod-build/build.sh) и поместите его в корень вашего проекта (рядом с `build.bat`). На первых строках есть раздел с настройками, которые вам нужно изменить под ваш мод: **build.sh** ```bash # Настройки MOD_NAME="my.first-mod" MOD_ENTRY=mod_myFirstMod.py ``` ### Если у вас есть `AS3` часть Если в моде есть `AS3`‑часть, создайте `as3/build.sh` по аналогии с `build.bat` для сборки на `Linux`. Если нет — пропустите шаг. **as3/build.sh** ```sh mxmlc -load-config+=build-config.xml --output=bin/my.first_mod.HelloWorldWindow.swf src/my/first_mod/HelloWorldWindow.as ``` Команды в файле должны соответствовать тем, что в `as3/build.bat` для сборки `AS3`‑части. Если несколько `SWF`‑файлов — добавьте команды `mxmlc` для каждого. ### Отправка изменений в репозиторий Отправьте изменения в репозиторий: - На вкладке `Source Control` нажмите `+`, чтобы отметить все файлы как готовые (Staged Changes). - Введите сообщение, например `Setup CI/CD`, и нажмите `Commit`. - Нажмите `Sync Changes`, чтобы отправить изменения. ![cicd-commit](https://docs.wotstat.info/guide/first-steps/automatization/assets/cicd-commit.png) ### Запуск сборки Чтобы запустить сборку, создайте тег. Тег — метка, указывающая на конкретный коммит. Он используется как версия вашего мода. 1. Откройте терминал в VSCode (`` Ctrl+` `` или `Terminal -> New Terminal`) и выполните команду, заменив `1.0.0` на версию вашего мода: ```powershell git tag 1.0.0 ``` 2. Отправьте тег в удалённый репозиторий: ```powershell git push --tags ``` ![tag-push](https://docs.wotstat.info/guide/first-steps/automatization/assets/tag-push.png) > **TIP — Совет** Если отправили неправильный тег и хотите удалить: ```powershell git tag -d 1.0.0 git push origin :refs/tags/1.0.0 ``` #### Проверка `Actions` Откройте вкладку `Actions`. Вы увидите запущенный workflow `release`. Нажмите, чтобы посмотреть детали. Дождитесь завершения. При успехе — зелёная галочка. ![build-success](https://docs.wotstat.info/guide/first-steps/automatization/assets/build-success.png) Если ошибка — красный крестик. Нажмите, чтобы посмотреть логи и причину. #### Информация о релизе После успешной сборки перейдите во вкладку `Releases`. Там появится релиз с версией тега и вложением `.mtmod`. ![release](https://docs.wotstat.info/guide/first-steps/automatization/assets/release.png) Нажмите иконку карандаша, чтобы добавить описание. ![release-info](https://docs.wotstat.info/guide/first-steps/automatization/assets/release-info.png) #### Редактирование релиза Добавьте описание изменений и уберите галочку `This is a pre-release`, если хотите сделать релиз актуальным. Затем нажмите `Update release`. ![release-edit](https://docs.wotstat.info/guide/first-steps/automatization/assets/release-edit.png) ## Результат Поздравляем! Автоматическая сборка настроена. Каждый раз при создании тега сборка запускается автоматически, а результат появляется в релизах. Другие пользователи смогут скачать актуальную версию мода во вкладке `Releases` вашего репозитория. ![result](https://docs.wotstat.info/guide/first-steps/automatization/assets/result.png) --- # Знакомство с Chrome DevTools > Canonical URL: https://docs.wotstat.info/guide/first-steps/devtools/ > Markdown URL: https://docs.wotstat.info/guide/first-steps/devtools/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/first-steps/devtools/index.md `Chrome DevTools` — это набор инструментов разработчика, встроенных в браузер `Google Chrome` для отладки веб-приложений. Они предоставляют возможность в реальном времени просматривать и изменять HTML, CSS на веб-страницах и выполнять отладку `JavaScript`-кода. Новая система интерфейса игры «Мир Танков» построена с использованием [`Coherent Gameface`](https://docs.coherent-labs.com/cpp-gameface/) на основе веб-технологий (`HTML`, `CSS`, `JavaScript`). Благодаря этому, к интерфейсу игры можно подключить `Chrome DevTools`. Video: [open media](https://docs.wotstat.info/guide/first-steps/devtools/assets/hero-wg.mp4) ## Подключение DevTools к игре Официальная поддержка `DevTools` в игре отключена, но можно воспользоваться модификацией [`wotstat.chrome-devtools-protocol`](https://github.com/wotstat/wotstat-chrome-devtools-protocol), которая реализует протокол удалённой отладки `Chrome DevTools`. ### Установка модификации 1. Скачайте и установите мод [wotstat.chrome-devtools-protocol](https://github.com/wotstat/wotstat-chrome-devtools-protocol/releases/latest) 2. Скачайте и установите мод `net.openwg.gameface`, он необходим для работы всех `Gameface`-модов: - Для `WG` из официального [репозитория](https://gitlab.com/openwg/wot.gameface/-/releases) - Для `Lesta` нужна пропатченная версия из [репозитория wotstat](https://github.com/wotstat/wotstat-chrome-devtools-protocol/releases/latest) 3. Запустите игру (при первом запуске игра один раз перезагрузится для инициализации модов, [подробнее...](https://docs.wotstat.info/guide/scripting/gameface-theory/index.md)) ### Подключение DevTools 1. Запустите `Google Chrome` 2. В адресной строке введите `chrome://inspect` и нажмите `Enter` 3. Убедитесь, что у вас стоит галочка `Discover network targets` 4. Нажмите кнопку `Configure...` и убедитесь, что в списке указан адрес `localhost:9222` (если нет, добавьте его) **Details — Настройки DevTools** ![devices](https://docs.wotstat.info/guide/first-steps/devtools/assets/devices.jpg) 5. Спустя несколько секунд в разделе `Remote Target` появятся все активные вкладки игры > На одном экране игры может быть сразу несколько окон (вкладок), каждое из которых независимое и ведёт себя как отдельная веб-страница 6. Нажмите кнопку `inspect` напротив нужной вкладки, чтобы открыть `DevTools` для этой вкладки ## Обзор возможностей DevTools Модификация не полностью реализует протокол DevTools, но основные возможности для отладки интерфейса игры доступны: - Вкладка `Elements` — просмотр HTML и CSS страницы - Поддерживается функционал `Overlay`, который обводит в игре выбранный в DevTools элемент, а так же позволяет выбирать элементы на странице кликом по ним в игре - Вкладка `Console` — просмотр логов и выполнение JavaScript-кода в среде страницы - Вкладка `Sources` (частично) – предоставляет доступ к редактированию специального `override.css` файла для внесения изменений в стили страницы Учтите, что для подключения доступны только те страницы, которые фактически присутствуют на экране игры, если вы перейдёте в другое окно интерфейса, то существующие вкладки будут уничтожены, а новые появятся в списке `Remote Target`. > **WARNING — Внимание!** Функционал DevTools используется только для просмотра и отладки интерфейса, он не предоставляет возможности "сохранить" сделанные изменения или сгенерировать мод автоматически. Все изменения, внесённые через DevTools, будут потеряны после перезагрузки страницы или игры. --- # Настройка окружения для AS3-мода через Adobe Animate > Canonical URL: https://docs.wotstat.info/guide/first-steps/environment/animate/ > Markdown URL: https://docs.wotstat.info/guide/first-steps/environment/animate/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/first-steps/environment/animate/index.md Компилировать `SWF`‑моды можно с помощью [Adobe Animate](https://www.adobe.com/products/animate.html). Основное преимущество этого способа — визуальная разработка интерфейса модов с помощью встроенных инструментов: вы сможете создавать интерфейсные окна в визуальном редакторе. > **TIP — TODO** Если вы знаете, как разрабатывать модификации с Animate, можете написать этот раздел руководства. Я как программист предпочитаю создавать интерфейсы кодом, это более строго, явно и удобно для контроля версий. --- # Настройка окружения для AS3-мода (с графической частью) > Canonical URL: https://docs.wotstat.info/guide/first-steps/environment/as3/ > Markdown URL: https://docs.wotstat.info/guide/first-steps/environment/as3/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/first-steps/environment/as3/index.md Моды с графической частью пишутся на `AS3` (ActionScript 3) и компилируются в единый `SWF`‑файл, в одном таком файле может находиться несколько скриптов, картинки, анимации и шрифты. У одного мода может быть несколько `SWF`‑файлов. Например, один `SWF` отвечает за всплывающее окно, а другой — за индикатор на экране. Чисто `AS3`‑моды возможны, однако в большинстве случаев вам понадобится вспомогательный `Python`‑скрипт, который будет взаимодействовать с игрой и управлять графической частью мода. Поэтому подразумевается, что у вас уже настроено окружение для `Python`‑модов. Если это не так, сначала настройте его, следуя [инструкции по настройке окружения для Python](https://docs.wotstat.info/guide/first-steps/environment/python/index.md). ## Необходимые инструменты - Полностью настроенное окружение для `Python`‑модов (см. [инструкцию по настройке окружения для Python](https://docs.wotstat.info/guide/first-steps/environment/python/index.md)) - [ActionScript & MXML](https://marketplace.visualstudio.com/items?itemName=bowlerhatllc.vscode-as3mxml) – расширение для VSCode, которое добавляет поддержку ActionScript - [Java JDK 11+](https://www.oracle.com/java/technologies/downloads/#jdk24-windows) – последняя версия `JDK`, необходима для работы расширения - `AIRSDK` – набор инструментов для компиляции `AS3`, необходим для работы расширения. Устанавливается через [AIR SDK Manager](https://airsdk.harman.com/download) ![AIR SDK Manager](https://docs.wotstat.info/guide/first-steps/environment/as3/assets/air-sdk-manager.png) - [Apache Royale](https://royale.apache.org/download/) – компилятор `AS3` в `SWF`. Нужно скачать архив `APACHE ROYALE JS/SWF` с [официального сайта](https://royale.apache.org/download/) и распаковать его в удобное место на диске, например `C:\apache-royale`. > **WARNING — Важно** При загрузке `Apache Royale` не перепутайте версии `JS-ONLY` и `JS/SWF`, необходима именно версия с `JS/SWF`. ## Организация проекта Мы расширим структуру проекта, описанную в [инструкции по настройке окружения для Python](https://docs.wotstat.info/guide/first-steps/environment/python/index.md). `AS3`‑часть мода полностью независима от `Python`‑части, поэтому её исходный код не нужно помещать в папку `res`. Создайте в корне проекта папку `as3`, в которой разместим папки: - `src/my/first_mod` – корневая папка вашего `AS3`‑мода, в которой будет находиться весь исходный код. - `libs` – здесь будут находиться сторонние библиотеки, которые понадобятся для компиляции - `bin` – здесь будет находиться скомпилированный файл `SWF` Также скачайте конфигурационный файл [`asconfig.json`](https://docs.wotstat.info/download/mod-build/asconfig.json), в котором указаны настройки для расширения, и [`build-config.xml`](https://docs.wotstat.info/download/mod-build/build-config.xml) с настройками компиляции `SWF`. Создайте пустой файл `build.bat`, который будет запускать компиляцию, его мы заполним позже. ``` my-first-mod/ └── as3/ ├── bin/ ├── libs/ │ └── ... (сторонние библиотеки) ├── src/ │ └── my/ │ └── first_mod/ │ └── ... (исходный код AS3 мода) ├── build-config.xml ├── asconfig.json └── build.bat ``` ### Библиотеки игры SWC Для того, чтобы мод мог взаимодействовать с компонентами игры, необходимо подключить к проекту внешние библиотеки игры в формате `SWC`. Эти библиотеки поставляются вместе с игрой и находятся в пакетах `res/packages/gui-part.pkg/gui/flash/swc` относительно корня игры. Формат `.pkg` нужно открыть с помощью архиватора, например `7-Zip`. > **WARNING — Важно** Пакет `gui-part` разбит на два отдельных архива: `gui-part1.pkg` и `gui-part2.pkg`; часть нужных библиотек находится в первом, часть — во втором. Перенесите эти `SWC`‑файлы в папку с модом `as3/libs/`. Должно получиться 11 файлов: - `base_app-1.0-SNAPSHOT.swc` - `battle.swc` - `common_i18n_library-1.0-SNAPSHOT.swc` - `common-1.0-SNAPSHOT.swc` - `gui_base-1.0-SNAPSHOT.swc` - `gui_battle-1.0-SNAPSHOT.swc` - `gui_lobby-1.0-SNAPSHOT.swc` - `lobby.swc` - `damageIndicator.swc` (на самом деле не используется, можно не добавлять) - `directionIndicator.swc` (на самом деле не используется, можно не добавлять) - `predictionIndicator.swc` (на самом деле не используется, можно не добавлять) В дополнение к игровым библиотекам вам необходима ещё основная библиотека `playerglobal.swc`. [Скачайте](https://docs.wotstat.info/download/playerglobal.swc) её и поместите в папку `as3/libs/`. > Если вы добавляете `damageIndicator.swc`, `directionIndicator.swc` или `predictionIndicator.swc`, не забудьте добавить их в конфигурационные файлы `build-config.xml` и `asconfig.json` по аналогии с другими библиотеками. ### Скрипт сборки Заполните файл `as3/build.bat` следующим содержимым: **build.bat** ```bat @echo off rem ==== настройки ==== set "MXML_PATH=C:\apache-royale" rem ==== компиляция ==== set "MXMLC=%MXML_PATH%\royale-asjs\bin\mxmlc" call "%MXMLC%" -load-config+=build-config.xml --output=bin/my.first_mod.HelloWorldWindow.swf src/my/first_mod/HelloWorldWindow.as ``` На 4-й строке укажите путь к папке, в которую распаковали `Apache Royale`. На 9-й строке команда, с помощью которой мы компилируем `SWF`: - `-load-config+=build-config.xml` – указывает файл с настройками компиляции - `--output=bin/my.first_mod.swf` – путь к выходному `SWF`‑файлу - `src/my/first_mod/HelloWorldWindow.as` – файл с исходным кодом, с которого начинается компиляция (точка входа) Если вам в модификации понадобится несколько `SWF`‑файлов, просто добавьте в `build.bat` ещё одну команду для компиляции с другими параметрами. #### Обновление основного скрипта сборки Обновление основного скрипта не требуется, если вы настроили по инструкции [окружение для Python](https://docs.wotstat.info/guide/first-steps/environment/python/index.md), то можете убедиться, что там есть блок, который запускает `as3/build.bat`, если он присутствует в проекте: ```bat:line-numbers=44 if exist ".\as3\build.bat" ( pushd ".\as3" del /Q /F ".\bin\*.swf" call build.bat xcopy ".\bin\*.swf" "..\build\res\gui\flash\" /Y /I >nul popd ) ``` ## Подготовка тестового мода Для проверки работоспособности отобразим в ангаре игровое окно. Создадим в папке `as3/src/my/first_mod/` файл `HelloWorldWindow.as` со следующим содержимым: **HelloWorldWindow.as** ```actionscript-3 package my.first_mod { import net.wg.infrastructure.base.AbstractWindowView; import flash.text.TextField; public class HelloWorldWindow extends AbstractWindowView { public function HelloWorldWindow() { super(); } override protected function onPopulate():void { super.onPopulate(); width = 400; height = 100; window.title = 'My First Mod Window'; window.useBottomBtns = false; var text:TextField = new TextField(); text.width = 384; text.height = 84; text.x = 8; text.y = 8; text.htmlText = "Мод работает! Ура!"; addChild(text); } } } ``` Аналогично с `Python` у вас должны работать всплывающие подсказки по наведению мыши: ![hint](https://docs.wotstat.info/guide/first-steps/environment/as3/assets/hint.png) И автодополнение по нажатию `.` (точка): ![suggestion](https://docs.wotstat.info/guide/first-steps/environment/as3/assets/suggestion.png) > Если расширение `ActionScript & MXML` не работает и вызывает ошибку `as3mxml.java.path in settings does not point to a valid executable`, то необходимо проверить правильность установки `Java JDK`, и убедиться, что он указан в `PATH`. ### Отображение окна из Python Чтобы окно появилось в игре, нужно из `Python`‑скрипта добавить его на экран. Для этого создадим управляющий `Python`‑класс (подробнее в [теории AS3](https://docs.wotstat.info/guide/scripting/as3-theory/index.md)). Создайте файл `res/scripts/client/gui/mods/my_first_mod/HelloWorldWindow.py` со следующим содержимым: **HelloWorldWindow.py** ```python from frameworks.wulf.gui_constants import WindowLayer from gui.Scaleform.framework.entities.abstract.AbstractWindowView import AbstractWindowView from gui.Scaleform.framework import g_entitiesFactories, ScopeTemplates, ViewSettings from gui.Scaleform.framework.managers.loaders import SFViewLoadParams from helpers import dependency from skeletons.gui.app_loader import IAppLoader from gui.Scaleform.framework.application import AppEntry class HelloWorldWindow(AbstractWindowView): def onWindowClose(self): self.destroy() HELLO_WORLD_WINDOW = "MY_MOD_HELLO_WORLD_WINDOW" def setup(): settingsViewSettings = ViewSettings( HELLO_WORLD_WINDOW, HelloWorldWindow, "my.first_mod.HelloWorldWindow.swf", WindowLayer.TOP_WINDOW, None, ScopeTemplates.VIEW_SCOPE, ) g_entitiesFactories.addSettings(settingsViewSettings) def show(): appLoader = dependency.instance(IAppLoader) # type: IAppLoader app = appLoader.getApp() # type: AppEntry app.loadView(SFViewLoadParams(HELLO_WORLD_WINDOW)) ``` Функция `setup()` регистрирует `SWF`‑файл в системе, а функция `show()` отображает окно в интерфейсе. В основном файле вашего мода (`res/scripts/client/gui/mods/mod_myFirstMod.py`) добавьте следующий код: **mod_myFirstMod.py** ```python from gui import SystemMessages from helpers import dependency from skeletons.gui.shared.utils import IHangarSpace from .my_first_mod.HelloWorldWindow import setup, show # [!code ++] MOD_VERSION = '{{VERSION}}' # получаем ссылку на IHangarSpace hangarSpace = dependency.instance(IHangarSpace) # type: IHangarSpace # Мод загрузился def init(): print("[MY_FIRST_MOD] Hello, World! Mod version is %s" % MOD_VERSION) # Регистрируем SWF файл setup() # [!code ++] # Подписываемся на загрузку ангара hangarSpace.onSpaceCreate += onHangarSpaceCreate def onHangarSpaceCreate(): # Отписываемся от загрузки ангара hangarSpace.onSpaceCreate -= onHangarSpaceCreate # Выводим уведомление в ангаре SystemMessages.pushMessage( text='Привет мир! Версия мода: %s' % MOD_VERSION, type=SystemMessages.SM_TYPE.InformationHeader, messageData={ 'header': 'MY_FIRST_MOD' } ) # Отображаем окно в интерфейсе show() # [!code ++] ``` ## Проверочный запуск Скомпилируйте мод с помощью `build.bat` из корня проекта: ```cmd build.bat -v 1.1.0 ``` > **TIP — Обратите внимание** Должна использоваться оболочка `CMD`. Если у вас используется `PowerShell`, переключитесь на `CMD`, нажав на стрелочку рядом с кнопкой `+` в окне терминала и выбрав `Command Prompt`. В случае успешной компиляции вы увидите следующий вывод: ![build-success](https://docs.wotstat.info/guide/first-steps/environment/as3/assets/build-success.png) А так же файл мода `my.first-mod_1.1.0.mtmod`, перенесите его в папку с игрой `/mods/<актуальная версия игры>/`. И запустите игру. После загрузки ангара вы увидите своё первое графическое окно :tada: ![result](https://docs.wotstat.info/guide/first-steps/environment/as3/assets/result.png) ## Итоговая структура проекта В результате, после выполнения всех шагов, структура вашего проекта должна выглядеть так: ``` my-first-mod/ ├── .vscode │ └── settings.json ├── wot-src │ └── ... (исходный код игры) ├── build.bat ├── meta.xml ├── as3/ │ ├── bin/ │ ├── libs/ │ │ └── ... (.swc библиотеки игры 11 штук + playerglobal.swc) │ ├── src/ │ │ └── my/ │ │ └── first_mod/ │ │ └── HelloWorldWindow.as │ ├── build-config.xml │ ├── asconfig.json │ └── build.bat └── res └── scripts └── client └── gui └── mods ├── mod_myFirstMod.py └── my_first_mod ├── __init__.py └── HelloWorldWindow.py ``` Теперь на основе этого тестового проекта вы можете создавать свои моды как с графической частью, так и без. ## Советы - У ActionScript есть автоформатирование, рекомендуется включить в настройках VSCode опцию `Editor: Format On Save` (Форматировать при сохранении) - Собранный `SWF` можно тестировать без переупаковки мода и перезапуска игры: поместите его в `res_mods`, а после сборки пересоздавайте окно на следующем кадре. Подробнее — в разделе [«Тестирование SWF без перезапуска игры»](https://docs.wotstat.info/guide/scripting/as3-theory/index.md#hot-reload). --- # How It Works > Canonical URL: https://docs.wotstat.info/guide/first-steps/environment/python/how-it-works > Markdown URL: https://docs.wotstat.info/guide/first-steps/environment/python/how-it-works.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/first-steps/environment/python/how-it-works.md **Разбор аргументов** В этом блоке скрипт присваивает значение переменной `v` из аргумента командной строки `-v `, который указывает версию мода. ```bat:line-numbers=10 set "v=" :parse if "%~1"=="" goto after_parse if /I "%~1"=="-v" ( if "%~2"=="" (echo [ERROR] Missing value for -v & exit /b 1) set "v=%~2" shift & shift & goto parse ) echo Usage: %~nx0 -v ^ exit /b 1 :after_parse if not defined v ( echo [ERROR] Version is required. Use -v ^. exit /b 1 ) ``` --- **Очистка и подготовка build** Процесс компиляции происходит в папке `build`, которая создаётся заново при каждом запуске скрипта. В этой папке создаётся структура, аналогичная той, что используется в игре, и в неё копируются все ресурсы из папки `res` вашего проекта. ```bat:line-numbers=28 if exist ".\build" rmdir /S /Q ".\build" mkdir ".\build" xcopy ".\res" ".\build\res" /E /I /Y >nul ``` --- **Проставить версию** В этом блоке скрипт ищет в точке входа мода маркер `{{VERSION}}` и заменяет его на значение переменной `v`, которая была установлена из аргумента командной строки. ```bat:line-numbers=33 set "configPath=.\build\res\scripts\client\gui\mods\%MOD_ENTRY%" if exist "%configPath%" ( powershell -NoProfile -Command ^ "(Get-Content '%configPath%' -Raw -Encoding utf8) " ^ "-replace '\{\{VERSION\}\}','%v%' | " ^ "Set-Content '%configPath%' -Encoding utf8" ) else ( echo [WARN] %configPath% not found. ) ``` --- **Байткод Python 2** Происходит компиляция всех `.py`‑файлов в папке `build` в байткод `.pyc`, который используется игрой. ```bat:line-numbers=44 python -m compileall ".\build" ``` --- **Компиляция в SWF** Запускается скрипт `build.bat` из папки `as3`, если такая папка есть в корне проекта. Этот скрипт должен скомпилировать все `ActionScript`‑файлы в `SWF`‑файлы и поместить их в папку `./as3/bin`. После этого все `SWF`‑файлы копируются в папку `build/res/gui/flash`, откуда попадают в файл мода. ```bat:line-numbers=47 if exist ".\as3\build.bat" ( pushd ".\as3" del /Q /F ".\bin\*.swf" call build.bat xcopy ".\bin\*.swf" "..\build\res\gui\flash\" /Y /I >nul popd ) ``` --- **meta.xml с версией** Аналогично точке входа в `meta.xml` проставляется версия мода. ```bat:line-numbers=56 if exist ".\meta.xml" ( powershell -NoProfile -Command ^ "$m = Get-Content '.\meta.xml' -Raw -Encoding utf8; " ^ "$m = $m -replace '\{\{VERSION\}\}','%v%'; " ^ "Set-Content '.\build\meta.xml' $m -Encoding utf8" ) else ( echo [ERROR] meta.xml not found. exit /b 1 ) ``` --- **Упаковка в .mtmod (7-Zip)** Происходит упаковка необходимых файлов в архив `.mtmod` с помощью 7-Zip. Упаковываются только файлы с расширениями `.pyc`, `.swf`, `.png` и `meta.xml`. ```bat:line-numbers=67 pushd ".\build" set "folder=%MOD_NAME%_%v%.mtmod" if exist "%folder%" del /Q "%folder%" "%SEVENZIP%" a -tzip -mx=0 "%folder%" ".\*.pyc" -r >nul "%SEVENZIP%" a -tzip -mx=0 "%folder%" ".\*.swf" -r >nul "%SEVENZIP%" a -tzip -mx=0 "%folder%" ".\meta.xml" >nul "%SEVENZIP%" a -tzip -mx=0 "%folder%" ".\*.png" -r >nul popd ``` --- # Настройка окружения для Python-мода (без графической части) > Canonical URL: https://docs.wotstat.info/guide/first-steps/environment/python/ > Markdown URL: https://docs.wotstat.info/guide/first-steps/environment/python/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/first-steps/environment/python/index.md Python-моды наиболее просты в разработке и являются хорошей отправной точкой для новичков. В этом руководстве мы рассмотрим, как настроить окружение для **комфортной** разработки Python-модов. ## Необходимые инструменты Перед началом разработки убедитесь, что у вас установлены следующие инструменты: - [Python 2.7](https://www.python.org/downloads/release/python-2716/) — интерпретатор Python, который будет использоваться для компиляции скриптов. - [7-Zip](https://www.7-zip.org/) — архиватор, который будет использоваться для упаковки модов в формат `.mtmod`. - [Git](https://git-scm.com/) — система контроля версий, с помощью которой будет загружен исходный код игры. - [VSCode](https://code.visualstudio.com/) — редактор кода, который будет использоваться для написания кода. - [Python extension for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-python.python) — расширение для VSCode, которое добавляет поддержку Python. > **WARNING — Важно** Исполняемый файл Python должен быть доступен из командной строки. Для этого при установке Python необходимо включить `Add python.exe to Path`. Установите значение `Will be installed on local hard drive`, по умолчанию оно отключено. ![Add python.exe to Path](https://docs.wotstat.info/guide/first-steps/environment/python/assets/install-python.png) Если вы его уже установили, то можете переустановить или поискать в интернете, как добавить Python в PATH вручную. После добавления перезапустите VSCode. ## Организация проекта Создайте папку для вашего проекта (один проект = один мод) и откройте её в `VSCode`. ### Исходный код игры Для подсветки синтаксиса, автодополнений, переходов по коду и удобного поиска по исходному коду игры в папке с проектом необходимо иметь копию исходного кода. Скачаем её из [неофициального репозитория GitHub](https://github.com/izeberg/wot-src). Для этого откройте терминал в VSCode (`` Ctrl+` `` или `Terminal -> New Terminal`) и выполните команду: ```shell git clone https://github.com/izeberg/wot-src.git ``` В корне вашего проекта появится папка `wot-src` с исходным кодом игры. Перейдите во вкладку `Source Control` (или нажмите `Ctrl+Shift+G`), нажмите на кнопку `...` и поставьте галочку на `Repositories` ![Source Control](https://docs.wotstat.info/guide/first-steps/environment/python/assets/enable-repo.png) После этого выберите репозиторий `wot-src` в списке, нажмите на кнопку с веткой `EU` и во всплывающем списке выберите ветку `origin/RU` — это переключит исходный код на версию для «Мир Танков». ![Switch Branch](https://docs.wotstat.info/guide/first-steps/environment/python/assets/checkout.png) > **TIP — Обратите внимание** Репозиторий большой — скачивание и переключение веток может занять некоторое время, наберитесь терпения. #### Настройка VSCode Чтобы `VSCode` мог работать с исходным кодом игры, установите расширение [Python extension for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-python.python). После установки необходимо указать, где именно находятся скрипты игры. Для этого создайте в корне проекта папку `.vscode`, а в ней файл `settings.json` со следующим содержимым: **settings.json** ```json { "python.autoComplete.extraPaths": [ "wot-src/sources/res/scripts/client", "wot-src/sources/res/scripts/common", "wot-src/sources/res/scripts/client_common", ], "python.analysis.extraPaths": [ "wot-src/sources/res/scripts/client", "wot-src/sources/res/scripts/common", "wot-src/sources/res/scripts/client_common", ], "python.analysis.userFileIndexingLimit": 20000, } ``` ### Файл метаданных `meta.xml` В корне вашего проекта создайте файл `meta.xml` со следующим содержимым: **meta.xml** ```xml my.first-mod My First Mod Description {{VERSION}} Your Name ``` Обратите внимание на `{{VERSION}}` на 4-й строке — это специальный маркер, который будет заменён на актуальную версию мода при упаковке. ### Точка входа Поскольку игра автоматически запускает скрипты с префиксом `mod_` из папки `res/scripts/client/gui/mods/`, такую структуру папок и следует использовать в вашем проекте. Создайте файл вашего мода `mod_myFirstMod.py` по пути `res/scripts/client/gui/mods/mod_myFirstMod.py`. > **TIP — Совет** Вы можете нажать в VSCode кнопку создания нового файла, вписать туда полный путь до этого файла `res/scripts/client/gui/mods/mod_myFirstMod.py`, и VSCode сам создаст все необходимые подпапки. Напишите в этом файле следующий код: **mod_myFirstMod.py** ```python from gui import SystemMessages from helpers import dependency from skeletons.gui.shared.utils import IHangarSpace MOD_VERSION = '{{VERSION}}' # Получаем ссылку на IHangarSpace hangarSpace = dependency.instance(IHangarSpace) # type: IHangarSpace # Мод загрузился def init(): print("[MY_FIRST_MOD] Hello, World! Mod version is %s" % MOD_VERSION) # Подписываемся на загрузку ангара hangarSpace.onSpaceCreate += onHangarSpaceCreate def onHangarSpaceCreate(): # Отписываемся от загрузки ангара hangarSpace.onSpaceCreate -= onHangarSpaceCreate # Выводим уведомление в ангаре SystemMessages.pushMessage( text='Привет мир! Версия мода: %s' % MOD_VERSION, type=SystemMessages.SM_TYPE.InformationHeader, messageData={ 'header': 'MY_FIRST_MOD' } ) ``` Обратите внимание, что у вас в редакторе должна быть подсветка синтаксиса. Наведите мышку на `SystemMessages.pushMessage` и увидите всплывающую подсказку с описанием функции и её параметров. ![tooltip](https://docs.wotstat.info/guide/first-steps/environment/python/assets/hint.png) А если вы начнёте печатать, после символа `.` (точка) появятся подсказки с атрибутами и методами объекта. ![suggestion](https://docs.wotstat.info/guide/first-steps/environment/python/assets/suggestion.png) Если вы видите в окошке `Loading...`, нужно немного подождать, пока `VSCode` проиндексирует исходный код игры. ### Скрипты и ресурсы Кроме файла точки входа в вашем моде могут быть и другие скрипты и ресурсы (изображения, файлы конфигурации и т. д.). Хороший подход — создание корневой папки вашего мода рядом с точкой входа, например `my_first_mod`, и размещение всех дополнительных файлов там. Это минимизирует конфликты имён файлов с другими модами. По пути `res/scripts/client/gui/mods/` создайте папку `my_first_mod` и добавьте туда пустой файл `__init__.py`. ### Компиляция скриптов `build.bat` Скачайте [файл build.bat](https://docs.wotstat.info/download/mod-build/build.bat) и поместите его в корень вашего проекта. #### Блок настроек в начале файла ```bat rem ==== настройки ==== set "SEVENZIP=C:\Program Files\7-Zip\7z.exe" set "MOD_NAME=my.first-mod" set "MOD_ENTRY=mod_myFirstMod.py" ``` - `SEVENZIP` — путь до исполняемого файла 7-Zip. Если вы установили 7-Zip в другое место, измените этот путь. - `MOD_NAME` — имя вашего мода. Используйте формат `автор.название`; он будет автоматически подставлен при упаковке `автор.название_версия.mtmod`. - `MOD_ENTRY` — имя файла точки входа вашего мода. Обычно это `mod_<имя_вашего_мода>.py`. **Details — Как работает build.bat** Если вам интересно, как работает этот скрипт, ниже приведено подробное описание каждого блока. --- **Разбор аргументов** В этом блоке скрипт присваивает значение переменной `v` из аргумента командной строки `-v `, который указывает версию мода. ```bat:line-numbers=10 set "v=" :parse if "%~1"=="" goto after_parse if /I "%~1"=="-v" ( if "%~2"=="" (echo [ERROR] Missing value for -v & exit /b 1) set "v=%~2" shift & shift & goto parse ) echo Usage: %~nx0 -v ^ exit /b 1 :after_parse if not defined v ( echo [ERROR] Version is required. Use -v ^. exit /b 1 ) ``` --- **Очистка и подготовка build** Процесс компиляции происходит в папке `build`, которая создаётся заново при каждом запуске скрипта. В этой папке создаётся структура, аналогичная той, что используется в игре, и в неё копируются все ресурсы из папки `res` вашего проекта. ```bat:line-numbers=28 if exist ".\build" rmdir /S /Q ".\build" mkdir ".\build" xcopy ".\res" ".\build\res" /E /I /Y >nul ``` --- **Проставить версию** В этом блоке скрипт ищет в точке входа мода маркер `{{VERSION}}` и заменяет его на значение переменной `v`, которая была установлена из аргумента командной строки. ```bat:line-numbers=33 set "configPath=.\build\res\scripts\client\gui\mods\%MOD_ENTRY%" if exist "%configPath%" ( powershell -NoProfile -Command ^ "(Get-Content '%configPath%' -Raw -Encoding utf8) " ^ "-replace '\{\{VERSION\}\}','%v%' | " ^ "Set-Content '%configPath%' -Encoding utf8" ) else ( echo [WARN] %configPath% not found. ) ``` --- **Байткод Python 2** Происходит компиляция всех `.py`‑файлов в папке `build` в байткод `.pyc`, который используется игрой. ```bat:line-numbers=44 python -m compileall ".\build" ``` --- **Компиляция в SWF** Запускается скрипт `build.bat` из папки `as3`, если такая папка есть в корне проекта. Этот скрипт должен скомпилировать все `ActionScript`‑файлы в `SWF`‑файлы и поместить их в папку `./as3/bin`. После этого все `SWF`‑файлы копируются в папку `build/res/gui/flash`, откуда попадают в файл мода. ```bat:line-numbers=47 if exist ".\as3\build.bat" ( pushd ".\as3" del /Q /F ".\bin\*.swf" call build.bat xcopy ".\bin\*.swf" "..\build\res\gui\flash\" /Y /I >nul popd ) ``` --- **meta.xml с версией** Аналогично точке входа в `meta.xml` проставляется версия мода. ```bat:line-numbers=56 if exist ".\meta.xml" ( powershell -NoProfile -Command ^ "$m = Get-Content '.\meta.xml' -Raw -Encoding utf8; " ^ "$m = $m -replace '\{\{VERSION\}\}','%v%'; " ^ "Set-Content '.\build\meta.xml' $m -Encoding utf8" ) else ( echo [ERROR] meta.xml not found. exit /b 1 ) ``` --- **Упаковка в .mtmod (7-Zip)** Происходит упаковка необходимых файлов в архив `.mtmod` с помощью 7-Zip. Упаковываются только файлы с расширениями `.pyc`, `.swf`, `.png` и `meta.xml`. ```bat:line-numbers=67 pushd ".\build" set "folder=%MOD_NAME%_%v%.mtmod" if exist "%folder%" del /Q "%folder%" "%SEVENZIP%" a -tzip -mx=0 "%folder%" ".\*.pyc" -r >nul "%SEVENZIP%" a -tzip -mx=0 "%folder%" ".\*.swf" -r >nul "%SEVENZIP%" a -tzip -mx=0 "%folder%" ".\meta.xml" >nul "%SEVENZIP%" a -tzip -mx=0 "%folder%" ".\*.png" -r >nul popd ``` ### Итоговая структура проекта В итоге у вас должна получиться следующая структура проекта: ``` my-first-mod/ ├── .vscode │ └── settings.json ├── wot-src │ └── ... (исходный код игры) ├── build.bat ├── meta.xml └── res └── scripts └── client └── gui └── mods ├── mod_myFirstMod.py └── my_first_mod ├── __init__.py └── ... (другие файлы вашего мода) ``` ## Компиляция и упаковка Для запуска сборки откройте терминал в VSCode (`` Ctrl+` `` или `Terminal -> New Terminal`) > **TIP — Обратите внимание** Должна использоваться оболочка `CMD`. Если у вас используется `PowerShell`, переключитесь на `CMD`, нажав на стрелочку рядом с кнопкой `+` в окне терминала и выбрав `Command Prompt`. И выполните команду: ```cmd build.bat -v 1.0.0 ``` **Details — Вывод сборки** ![terminal](https://docs.wotstat.info/guide/first-steps/environment/python/assets/terminal-output.png) В корне проекта появится файл `my.first-mod_1.0.0.mtmod` — это ваш упакованный мод :tada:. > **WARNING — Важно** В `build.bat` используется команда `python -m compileall` — ожидается, что у вас в `PATH` доступен `Python 2.7`. Проверить, какая версия Python используется по умолчанию, можно командой в этом же терминале: ```bat python --version ``` Вывод должен быть таким: `Python 2.7.16`. Если версия отличается, замените строку в `build.bat` на путь до вашего `python.exe`, например: ```bat:line-numbers=32 # Компиляция Python C:\Python27\python.exe -m compileall ".\build" ``` ## Проверочный запуск В корневой папке игры очистите файл `python.log` (откройте любым текстовым редактором, удалите всё его содержимое и сохраните). Перенесите файл `my.first-mod_1.0.0.mtmod` в папку с игрой `/mods/<версия_игры>/`, запустите игру и дождитесь входа в ангар. В центре уведомлений должно появиться сообщение от вашего мода: ![notification](https://docs.wotstat.info/guide/first-steps/environment/python/assets/notification.png) Откройте файл `python.log` и убедитесь, что там есть вывод вашего мода: **python.log** ```log /------------------------------------------------------------------------------------------\ Tanki(x64) Build: 1.37.0.10 #2189918 starting on Mon Sep 8 04:43:39 2025 ... INFO: [PY_DEBUG] Mod package 'e:/tanki/mods/1.37.0.0/my.first-mod_1.0.0.mtmod' loaded ... INFO: [MY_FIRST_MOD] Hello, World! Mod version is 1.0.0 ... ``` Логов будет много, воспользуйтесь поиском по файлу (`Ctrl+F`) и найдите `MY_FIRST_MOD`, чтобы убедиться, что мод успешно загрузился и выполнился. --- # Первый реальный мод > Canonical URL: https://docs.wotstat.info/guide/first-steps/first-mod/ > Markdown URL: https://docs.wotstat.info/guide/first-steps/first-mod/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/first-steps/first-mod/index.md В этом руководстве пройдём все этапы создания реального Python‑мода. В качестве примера будет повторён мод [Быстрый демонтаж оборудования 2.0](http://forum.tanki.su/index.php?/topic/2204705-13700-quick-demount-20-%D0%B1%D1%8B%D1%81%D1%82%D1%80%D1%8B%D0%B9-%D0%B4%D0%B5%D0%BC%D0%BE%D0%BD%D1%82%D0%B0%D0%B6-%D0%BE%D0%B1%D0%BE%D1%80%D1%83%D0%B4%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F-20/). Этот мод позволяет быстро демонтировать оборудование с танка, находясь в меню установки оборудования на другой танк. Разберём не только необходимые для разработки шаги, но и принцип, как до них догадаться. ![hero](https://docs.wotstat.info/guide/first-steps/first-mod/assets/hero.png) Мод является крайне полезным и не очень сложным в разработке, что делает его отличным примером для изучения. ## Философия Основной подход к разработке модов заключается в подмене существующих методов на свои реализации. Благодаря языку Python и его динамическим возможностям мы можем в рантайме (во время работы программы) заменить любой метод на свой, добавив нужную логику. Например: ```python # сохраним оригинальный метод original_method = SomeClass.some_method # определим свою реализацию def my_method(self, *a, **k): # тут может быть любая наша логика ... # вызовем оригинальный метод return original_method(self, *a, **k) # заменим метод на наш SomeClass.some_method = my_method ``` После замены, когда игра вызовет `SomeClass().some_method()`, фактически будет выполнена наша реализация `my_method`, в которой мы можем делать всё что угодно и при необходимости вызвать оригинал. ## Идея мода Перед разработкой мода нужно чётко понимать, что именно он должен делать. В нашем случае, мод должен из меню установки оборудования на танк позволять демонтировать оборудование с других танков. ## Шаги для реализации Перед полноценной разработкой имеет смысл разбить задачу на мелкие шаги и попробовать каждый через `PjOrion`. Необходимые шаги: - Понять, как отобразить интерфейс для выбора танка, с которого нужно демонтировать оборудование - Понять, как получить список танков с установленным оборудованием - Научиться вызывать демонтирование оборудования программно, с танка, который не выбран в данный момент ## Интерфейс цели демонтажа Первое, что приходит в голову, — добавить кнопку «Демонтаж» рядом с «Установить» в окне установки оборудования. Однако интерфейс выбора оборудования сделан на `GF`, который крайне тяжело модифицируется. **Details — Как понять на чём сделан интерфейс** Отличить GF‑интерфейс можно просто: в файле `res/packages/gui-part.pkg/gui/gameface/styles/default.css` добавьте в конце стиль: **default.css** ```css * { border: 1px solid rgba(30, 247, 70, 0.4); } ``` Он добавит зелёную рамку ко всем элементам GF‑интерфейса, что позволит увидеть, какие элементы к нему относятся. Учтите, что пакет `gui-part.pkg` может быть разбит на несколько архивов, нужный файл `default.css` может находиться в любом из них. В результате видно, что весь интерфейс демонтажа — это GF. ![gf-border](https://docs.wotstat.info/guide/first-steps/first-mod/assets/gf-interface.webp) Поэтому наиболее простой способ отображения интерфейса выбора танка для демонтажа — модифицировать существующее контекстное меню. ### Как найти контекстное меню Контекстное меню в игре вызывается через `Python` и обычно реализуется контролирующим классом, наследующимся от `AbstractContextMenuHandler`. Попробуем найти в исходном коде игры контекстное меню для оборудования в интерфейсе выбора. Скорее всего, в названии контролирующего класса есть слово `ContextMenu`, и можно предположить, что там будет `Equipment`, так как это меню оборудования. Воспользуемся поиском в VSCode по регулярному выражению `class .*Equipment.*ContextMenu`, здесь `.*` означает любое число произвольных символов. **Details — Результат поиска по регулярному выражению** ![search-result](https://docs.wotstat.info/guide/first-steps/first-mod/assets/search-result.png) > **TIP — Совет** Учтите, что поиск по регулярным выражениям нужно включить кнопкой `.*` рядом с полем ввода. Если используете `Git` и у вас есть `.gitignore`, то по умолчанию поиск не проходит по игнорируемым файлам. Чтобы искать везде, нажмите `...` рядом с полем ввода в `files to exclude` и отключите `Use Exclude Settings and Ignore Files`. Первый результат поиска — класс `BaseEquipmentItemContextMenu` по пути `.../tank_setup/context_menu/base_equipment.py`, где папка `tank_setup` подтверждает, что это именно нужное нам меню в интерфейсе настройки танка. Префикс `Base` в названии говорит, что это базовый класс. Посмотрим, кто его наследует. Введём в поиск `class .*\(BaseEquipmentItemContextMenu`. **Details — Результат поиска наследников `BaseEquipmentItemContextMenu`** ![search-child-cm](https://docs.wotstat.info/guide/first-steps/first-mod/assets/search-child-cm.png) Нашлось три класса: - `BattleAbilityItemContextMenu` – что‑то связанное с боевыми умениями - `ConsumableItemContextMenu` – что‑то связанное с расходниками - `OptDeviceItemContextMenu` – то, что нам нужно ### Как модифицировать контекстное меню Класс `OptDeviceItemContextMenu` реализует меню декларативно (подробнее — [как создать контекстное меню](https://docs.wotstat.info/articles/how-to-create-context-menu/index.md#high-level-way)) через наследование от `ContextMenu` и декоратор `@option`. Такой способ удобен для фиксированного числа кнопок, но у нас число танков меняется, поэтому нужен императивный подход через переопределение базовой функции `_generateOptions`. Эта функция определена в цепочке предков: `OptDeviceItemContextMenu` -> `BaseEquipmentItemContextMenu` -> `BaseItemContextMenu` -> `BaseTankSetupContextMenu` -> `ContextMenu` -> `AbstractContextMenuHandler`. Проверим так ли это через `PjOrion`. Для начала сохраним оригинальный метод: **PjOrion** ```python from gui.Scaleform.daapi.view.lobby.tank_setup.context_menu.opt_device import OptDeviceItemContextMenu orig_generateOptions = OptDeviceItemContextMenu._generateOptions print('Original _generateOptions:', orig_generateOptions) ``` После выполнения кода в консоли `PjOrion` увидим, что оригинальный метод найден. ``` *** ('Original _generateOptions:', ) ``` > **WARNING — Важно** После выполнения кода обязательно сотрите или закомментируйте сохранение, чтобы случайно не перезаписать `orig_generateOptions` в дальнейшем. Теперь можно переопределить `OptDeviceItemContextMenu._generateOptions` своей реализацией. Для начала просто выведем сообщение. Внутри новой функции нужно вернуть результат `orig_generateOptions`, чтобы не сломать логику. **PjOrion** ```python from gui.Scaleform.daapi.view.lobby.tank_setup.context_menu.opt_device import OptDeviceItemContextMenu #orig_generateOptions = OptDeviceItemContextMenu._generateOptions def new_generateOptions(obj, *a, **k): print('new generate options') return orig_generateOptions(obj, *a, **k) OptDeviceItemContextMenu._generateOptions = new_generateOptions ``` После выполнения кода, перейдите в игру и вызовите это контекстное меню, в консоли PjOrion появится сообщение `new generate options`, что подтверждает успешное переопределение. #### Добавление своих пунктов меню Теперь посмотрим, что возвращает оригинальный метод, для этого сохраним результат оригинального метода и выведем его в консоль. **PjOrion** ```python from gui.Scaleform.daapi.view.lobby.tank_setup.context_menu.opt_device import OptDeviceItemContextMenu #orig_generateOptions = OptDeviceItemContextMenu._generateOptions def new_generateOptions(obj, *a, **k): orig_result = orig_generateOptions(obj, *a, **k) print('new generate options', orig_result) return orig_result OptDeviceItemContextMenu._generateOptions = new_generateOptions ``` После вызова контекстного меню получим массив элементов: ```python [ {'submenu': None, 'linkage': None, ..., label: 'Информация' }, {'submenu': None, 'linkage': None, ..., label: 'Поместить в слот 1' }, .... ] ``` Для нашей реализации нужно в конец списка добавить подменю `Демонтировать с другого танка` с перечислением танков. Пока добавим три тестовых. **PjOrion** ```python from gui.Scaleform.daapi.view.lobby.tank_setup.context_menu.opt_device import OptDeviceItemContextMenu #orig_generateOptions = OptDeviceItemContextMenu._generateOptions def new_generateOptions(obj, *a, **k): orig_result = orig_generateOptions(obj, *a, **k) submenuItems = [ obj._makeItem('demountFrom:veh_1', 'Tank 1'), obj._makeItem('demountFrom:veh_2', 'Tank 2'), obj._makeItem('demountFrom:veh_3', 'Tank 3') ] orig_result.append(obj._makeSeparator()) # разделитель orig_result.append(obj._makeItem('demount', 'Demount from:', optSubMenu=submenuItems)) return orig_result OptDeviceItemContextMenu._generateOptions = new_generateOptions ``` Готово. Теперь в контекстном меню есть пункт `Demount from:` с подменю из трёх танков. ![demount-ui](https://docs.wotstat.info/guide/first-steps/first-mod/assets/demount-ui.png) #### Определение действий по нажатию на пункты меню Первый аргумент `obj._makeItem` — уникальный идентификатор пункта (`optionId`), по нему можно идентифицировать нажатие. Для этого в базовом классе `AbstractContextMenuHandler` есть метод `onOptionSelect(optionId)`, вызываемый при нажатии на любой пункт меню. Сохраним его известным способом, затем закомментируем сохранение, чтобы не перезаписать. **PjOrion** ```python from gui.Scaleform.daapi.view.lobby.tank_setup.context_menu.opt_device import OptDeviceItemContextMenu orig_onOptionSelect = OptDeviceItemContextMenu.onOptionSelect ``` Теперь определим свою реализацию, в которой мы будем обрабатывать наши пункты меню. **PjOrion** ```python from gui.Scaleform.daapi.view.lobby.tank_setup.context_menu.opt_device import OptDeviceItemContextMenu #orig_onOptionSelect = OptDeviceItemContextMenu.onOptionSelect def new_onOptionSelect(obj, optionId): if optionId.startswith('demountFrom:'): veh_id = optionId.split(':')[1] print('Demount from vehicle:', veh_id) # тут будет логика демонтажа с танка veh_id return return orig_onOptionSelect(obj, optionId) OptDeviceItemContextMenu.onOptionSelect = new_onOptionSelect ``` Готово. Теперь при нажатии на `Tank 1`, `Tank 2`, `Tank 3` в консоли выводится соответствующий идентификатор. ![demount-log](https://docs.wotstat.info/guide/first-steps/first-mod/assets/demount-log.png) ## Получение списка танков с оборудованием Теперь нужно научиться получать список танков с установленным оборудованием. Для начала нужно понять, на каком оборудовании вызвано контекстное меню (чтоб понять что именно надо демонтировать). Скорее всего, эта информация уже есть в `OptDeviceItemContextMenu`, нужно найти её. ### Исследуем OptDeviceItemContextMenu Для этого модифицируем `new_generateOptions`, чтобы записать объект `obj` (экземпляр `OptDeviceItemContextMenu`) в глобальную область PjOrion и исследовать его. **PjOrion** ```python def new_generateOptionsSaveObj(obj, *a, **k): global last_OptDeviceItemContextMenu last_OptDeviceItemContextMenu = obj return orig_generateOptions(obj, *a, **k) OptDeviceItemContextMenu._generateOptions = new_generateOptionsSaveObj ``` После вызова контекстного меню, в `PjOrion` появится глобальная переменная `last_OptDeviceItemContextMenu`, которая содержит объект `OptDeviceItemContextMenu`. Теперь можно исследовать его: по нажатию ПКМ после `last_OptDeviceItemContextMenu.` выбрать `Show attributes`. ![last-opt-attributes](https://docs.wotstat.info/guide/first-steps/first-mod/assets/last-opt-attributes.png) Среди атрибутов есть метод `_getItem`. Выведем его в консоль `print(last_OptDeviceItemContextMenu._getItem())` и получим `OptionalDevice`: ``` OptionalDevice ``` Дальнейшее проставление символа `.` после `last_OptDeviceItemContextMenu._getItem()` и выбор `Show attributes` покажет все атрибуты объекта `OptionalDevice`. Одним из которых будет `getInstalledVehicles`, который по названию явно говорит о том, что он возвращает список танков с установленным оборудованием. Попробуем вызвать `last_OptDeviceItemContextMenu._getItem().getInstalledVehicles()` и получим ошибку о том, что методу нужны **два аргумента**, первый это `self`, а второй необходимо передавать. ### Понимание getInstalledVehicles Воспользовавшись поиском по `getInstalledVehicles` можно найти множество примеров, где аргументом передают `vehicles` (массив танков). Например в `InventoryBlockConstructor`: **.../gui/shared/tooltips/module.py** ```python ... def _getInstalledVehicles(self, module, inventoryVehicles): return module.getInstalledVehicles(inventoryVehicles.itervalues()) # [!code highlight] ... items = self.itemsCache.items inventoryVehicles = items.getVehicles(REQ_CRITERIA.INVENTORY) installedVehicles = self._getInstalledVehicles(module, inventoryVehicles) ... ``` Сделаем точно так же, получим список всех танков, которые есть в ангаре через `itemsCache` и `REQ_CRITERIA.INVENTORY` и передадим их в `getInstalledVehicles`. **PjOrion** ```python from helpers import dependency from skeletons.gui.shared import IItemsCache from gui.shared.utils.requesters import REQ_CRITERIA itemsCache = dependency.instance(IItemsCache) # type: IItemsCache inventoryVehicles = itemsCache.items.getVehicles(REQ_CRITERIA.INVENTORY) print(last_OptDeviceItemContextMenu._getItem().getInstalledVehicles(inventoryVehicles.itervalues())) ``` > **TIP — Совет** Как работать с `dependency` и `IItemsCache` можно почитать в статье [Как работать с Dependency Injections](https://docs.wotstat.info/articles/how-to-work-with-di/index.md) В результате в консоли появится множество (`set`) танков с установленным оборудованием. ``` set([ Vehicle, ... Vehicle ]) ``` Преобразуем его в массив (`intCD`, `shortUserName`) и выведем. **PjOrion** ```python from helpers import dependency from skeletons.gui.shared import IItemsCache from gui.shared.utils.requesters import REQ_CRITERIA itemsCache = dependency.instance(IItemsCache) # type: IItemsCache inventoryVehicles = itemsCache.items.getVehicles(REQ_CRITERIA.INVENTORY) installedVehicles = last_OptDeviceItemContextMenu._getItem().getInstalledVehicles(inventoryVehicles.itervalues()) print([(v.intCD, v.userName) for v in installedVehicles]) ``` В игре `intCD` это уникальный идентификатор танка, а `shortUserName` это короткое название танка. > В консоли `PjOrion` русские буквы отображаются как их Unicode коды, но в игре всё будет работать нормально. ### Обновление контекстного меню Обновим контекстное меню, чтобы показывать реальные танки. **PjOrion** ```python itemsCache = dependency.instance(IItemsCache) # type: IItemsCache inventoryVehicles = itemsCache.items.getVehicles(REQ_CRITERIA.INVENTORY) def new_generateOptionsRealVehicles(obj, *a, **k): original_result = orig_generateOptions(obj, *a, **k) installedVehicles = obj._getItem().getInstalledVehicles(inventoryVehicles.itervalues()) submenuItems = [ obj._makeItem('demountFrom:%d' % v.intCD, v.userName) for v in installedVehicles ] original_result.append(obj._makeSeparator()) original_result.append(obj._makeItem('demount', 'Demount from:', optSubMenu=submenuItems)) return original_result OptDeviceItemContextMenu._generateOptions = new_generateOptionsRealVehicles ``` ![demount-real-log](https://docs.wotstat.info/guide/first-steps/first-mod/assets/demount-real-log.png) ## Вызов демонтажа оборудования Остался последний шаг, нужно научиться программно демонтировать оборудование с танка, который сейчас не выбран. В игре демонтаж происходит через `GF`‑кнопку. Найти, что она вызывает, непросто. Можно попробовать поиск по `demount`, однако он даёт много лишнего. ### Поиск демонтажа по контекстному меню Можно вспомнить, что демонтаж доступен из контекстного меню в ангаре: ![demount-from-hangar](https://docs.wotstat.info/guide/first-steps/first-mod/assets/demount-from-hangar.png) Можно искать другие `...OptDevice...ContextMenu`, но нам повезло, и `HangarOptDeviceSlotContextMenu` находится рядом в том же файле который мы уже находили `.../tank_setup/context_menu/opt_device.py`. В этом классе находим опцию `demountFromSetup`, которая вызывает `_demountProcess`. **opt_device.py** ```python from gui.shared.gui_items.items_actions import factory as ActionsFactory @option(_sqGen.next(), TankSetupCMLabel.DEMOUNT_FROM_SETUP) def demountFromSetup(self): self._demountProcess(isDestroy=False, everywhere=False) @adisp_process def _demountProcess(self, isDestroy=False, everywhere=True): item = self._itemsCache.items.getItemByCD(self._intCD) action = ActionsFactory.getAction( ActionsFactory.REMOVE_OPT_DEVICE, self._getVehicle(), item, self._installedSlotId, isDestroy, forFitting=False, everywhere=everywhere ) result = yield ActionsFactory.asyncDoAction(action) ... ``` > **TIP — Совет** Как работать с `adisp_process` можно почитать в статье [Асинхронное программирование](https://docs.wotstat.info/articles/adisp/index.md) Как видно из кода, нужно вызвать действие `REMOVE_OPT_DEVICE` через `ActionsFactory`, передав: - танк, с которого нужно демонтировать оборудование (объект `self._getVehicle()`) - оборудование, которое нужно демонтировать (объект `self._itemsCache.items.getItemByCD(self._intCD)`) - ID слота, с которого нужно демонтировать оборудование (`self._installedSlotId`) Как получить танк и оборудование, мы знаем, осталось получить ID слота. ### Понимание ID слота Что это за ID — неясно. Переопределим `_demountProcess` и выведем параметры. Сохраняем оригинальный метод **PjOrion** ```python from gui.Scaleform.daapi.view.lobby.tank_setup.context_menu.opt_device import HangarOptDeviceSlotContextMenu orig_demountProcess = HangarOptDeviceSlotContextMenu._demountProcess ``` Переопределяем его **PjOrion** ```python from gui.Scaleform.daapi.view.lobby.tank_setup.context_menu.opt_device import HangarOptDeviceSlotContextMenu #orig_demountProcess = HangarOptDeviceSlotContextMenu._demountProcess def new_demountProcess(obj, *a, **k): item = obj._itemsCache.items.getItemByCD(obj._intCD) print('OnDemount:', obj._getVehicle(), item, obj._installedSlotId) return orig_demountProcess(obj, *a, **k) HangarOptDeviceSlotContextMenu._demountProcess = new_demountProcess ``` После этого можно поэкспериментировать с демонтажом из разных комплектов и понять, что именно передаётся в `self._installedSlotId`. По экспериментам видно, что это порядковый номер слота, начиная с нуля, в том числе и для дополнительного комплекта. ### Пробуем демонтировать Теперь можно реализовать демонтаж оборудования с выбранного танка. Нужны `intCD` танка и `intCD` оборудования. Возьмём текущую технику (`g_currentVehicle`), получим её `intCD` и оборудование из например второго слота: **PjOrion** ```python from CurrentVehicle import g_currentVehicle print(g_currentVehicle.intCD) print(g_currentVehicle.item.optDevices.installed[1].intCD) ``` > **TIP — Совет** Подробнее исследовать что есть в `g_currentVehicle` можно через `PjOrion` изучая подсказки по нажатию `.`. В моём случае: - `intCD` техники `4737` (Strv 103B) - `intCD` оборудования `25593` (Турбонагнетатель) - `id` слота `1` (второй слот, так как нумерация с нуля) Вызовем демонтаж, как в примере из `HangarOptDeviceSlotContextMenu._demountProcess`, но с нашими параметрами. **PjOrion** ```python from gui.shared.gui_items.items_actions import factory as ActionsFactory from helpers import dependency from skeletons.gui.shared import IItemsCache from adisp import adisp_process @adisp_process def demount(vehicleCD, deviceCD, slotId): itemsCache = dependency.instance(IItemsCache) # type: IItemsCache item = itemsCache.items.getItemByCD(deviceCD) vehicle = itemsCache.items.getItemByCD(vehicleCD) action = ActionsFactory.getAction( ActionsFactory.REMOVE_OPT_DEVICE, vehicle, item, slotId, False, forFitting=False, everywhere=True ) result = yield ActionsFactory.asyncDoAction(action) demount(4737, 25593, 1) # подставьте свои значения ``` **Details — Результат** ![demount-frozen-screen](https://docs.wotstat.info/guide/first-steps/first-mod/assets/demount-frozen-screen.png) Получили зависший экран с размытием. Такое бывает. Поможет `ESC` -> `Сменить сервер` (что приведёт к перезагрузке интерфейса ангара). Иногда может потребоваться полная перезагрузка игры. Если код верный, но не работает, проблема может быть в способе запуска. Возможно, в `PjOrion` код выполняется не в главном потоке, из-за чего интерфейс не может инициализироваться. Можно применить трюк с отложенным вызовом. С помощью метода движка `BigWorld.callback(time, callback)`, который откладывает выполнение функции `callback` на указанное время. Спустя это время функция будет вызвана в основном потоке от имени движка. Отложим на `0` секунд, чтобы вызвать в следующий кадр. **PjOrion** ```python BigWorld.callback(0, lambda: demount(4737, 25593, 1)) ``` **Details — Результат** ![demount-succes-screen](https://docs.wotstat.info/guide/first-steps/first-mod/assets/demount-succes-screen.png) Теперь всё работает, даже если выбрать другой танк. ### Получение индекса слота Остаётся автоматически определять `slotId` по оборудованию и танку. Проще всего перебрать `optDevices.installed`, как делали выше для `g_currentVehicle`: **PjOrion** ```python from helpers import dependency from skeletons.gui.shared import IItemsCache def getInstalledSlotIdx(vehicleCD, moduleIntCD): itemsCache = dependency.instance(IItemsCache) # type: IItemsCache # Получаем танк по intCD vehicle = itemsCache.items.getItemByCD(vehicleCD) # Перебираем установленное оборудование for idx, op in enumerate(vehicle.optDevices.installed): if op is not None and moduleIntCD == op.intCD: return idx return -1 print(getInstalledSlotIdx(4737, 25593)) # подставьте свои значения ``` Этот способ работает, но если оборудование в дополнительном комплекте, то оно не будет найдено (`vehicle.optDevices.installed` только текущий комплект). Список из дополнительного комплекта нельзя получить без переключения комплектов. У `ActionsFactory` которую мы используем для демонтажа, есть и другие действия, среди которых можно найти `CHANGE_SETUP_EQUIPMENTS_INDEX`, название говорит само за себя. Теперь можно выполнить поиск по проекту и найти где такая команда используется. Например, в `LoadoutPresenter` (`.../lobby/hangar/presenters/loadout_presenter.py`) есть метод `__doChangeSetupIndex`, вызывающий это действие. **loadout_presenter.py** ```python @adisp.adisp_process def __doChangeSetupIndex(self, groupId, currentIndex): action = ActionsFactory.getAction( ActionsFactory.CHANGE_SETUP_EQUIPMENTS_INDEX, self.__getVehicle(), groupId, currentIndex demountFromSetup, который вызывает) ... ``` Применим это к нашей функции `getInstalledSlotIdx`, чтобы находить оборудование в любом комплекте. Проверяем текущий, если ничего не нашли, то переключаем на противоположный и проверяем снова. > Для переключения необходимо использовать асинхронность: `@adisp_process` позволяет ждать результат (`yield`), а `@adisp_async` позволяет вернуть асинхронный результат через `callback`. **PjOrion** ```python from adisp import adisp_process, adisp_async from helpers import dependency from skeletons.gui.shared import IItemsCache from post_progression_common import TankSetupGroupsId from gui.shared.gui_items.items_actions import factory as ActionsFactory @adisp_async @adisp_process def getInstalledSlotIdx(vehicleCD, moduleIntCD, callback): itemsCache = dependency.instance(IItemsCache) # type: IItemsCache # Функция проверки текущего комплекта, если наши, вызываем callback и возвращаем True def checkDevices(): vehicle = itemsCache.items.getItemByCD(vehicleCD) for idx, op in enumerate(vehicle.optDevices.installed): if op is not None and moduleIntCD == op.intCD: callback(idx) return True return False # Проверяем текущий комплект, если True, значит нашли и можно выйти из фукнции if checkDevices(): return # Меняем комплект на противоположный vehicle = itemsCache.items.getItemByCD(vehicleCD) targetIndex = 1 if vehicle.optDevices.setupLayouts.layoutIndex == 0 else 0 action = ActionsFactory.getAction( ActionsFactory.CHANGE_SETUP_EQUIPMENTS_INDEX, vehicle, TankSetupGroupsId.OPTIONAL_DEVICES_AND_BOOSTERS, targetIndex ) # Дожидаемся смены result = yield ActionsFactory.asyncDoAction(action) # Проверяем новый текущий комплект if checkDevices(): return # Ничего не нашли callback(-1) @adisp_process def getSlot(): res = yield getInstalledSlotIdx(4737, 25593) # подставьте свои значения print(res) getSlot() ``` **Details — Откуда взялся `TankSetupGroupsId`** Поиск по проекту позволяет изучить примеры использования `CHANGE_SETUP_EQUIPMENTS_INDEX`, в которых он получают аргументом `groupId`. На практике это `2` для комплекта оборудования и `1` для снарядов. В том же самом файле `loadout_presenter.py` есть функция `def _getEquipmentsPairs(self, groupID))` которая принимает `groupID` и обрабатывает его значения. **loadout_presenter.py** ```python from post_progression_common import TankSetupGroupsId ... def _getEquipmentsPairs(self, groupID): ... if groupID == TankSetupGroupsId.EQUIPMENT_AND_SHELLS: elif groupID == TankSetupGroupsId.OPTIONAL_DEVICES_AND_BOOSTERS: ... ``` Отсюда берём `TankSetupGroupsId.OPTIONAL_DEVICES_AND_BOOSTERS` (равно `2`). Теперь можно проверить `getInstalledSlotIdx`, он должен находить слот в любом комплекте. **PjOrion** ```python from adisp import adisp_process @adisp_process def test(): res = yield getInstalledSlotIdx(4737, 25593) # подставьте свои значения print(res) test() ``` ## Реализация мода Теперь, когда мы научились делать каждый шаг по отдельности, можно собрать всё вместе и сделать полноценный мод. Сделаем на основе `my.first_mod` из обучения по [настройке Python окружения](https://docs.wotstat.info/guide/first-steps/environment/python/index.md). Весь функционал разобьём на разные файлы, чтобы было проще ориентироваться в коде. ### Логика демонтажа В отдельный файл `my_first_mod/demount.py` вынесем логику демонтажа. Вынесем `itemsCache` на уровень модуля. В `def demount` получаем `slotId` через `getInstalledSlotIdx`, затем вызываем демонтаж. **my_first_mod/demount.py** ```python from adisp import adisp_process, adisp_async from helpers import dependency from skeletons.gui.shared import IItemsCache from post_progression_common import TankSetupGroupsId from gui.shared.gui_items.items_actions import factory as ActionsFactory itemsCache = dependency.instance(IItemsCache) # type: IItemsCache @adisp_async @adisp_process def getInstalledSlotIdx(vehicleCD, moduleIntCD, callback): def checkDevices(): vehicle = itemsCache.items.getItemByCD(vehicleCD) for idx, op in enumerate(vehicle.optDevices.installed): if op is not None and moduleIntCD == op.intCD: callback(idx) return True return False # Проверяем текущий комплект if checkDevices(): return # Меняем комплект на противоположный vehicle = itemsCache.items.getItemByCD(vehicleCD) targetIndex = 1 if vehicle.optDevices.setupLayouts.layoutIndex == 0 else 0 action = ActionsFactory.getAction( ActionsFactory.CHANGE_SETUP_EQUIPMENTS_INDEX, vehicle, TankSetupGroupsId.OPTIONAL_DEVICES_AND_BOOSTERS, targetIndex ) # Дожидаемся смены result = yield ActionsFactory.asyncDoAction(action) # Проверяем новый текущий комплект if checkDevices(): return # Ничего не нашли callback(-1) @adisp_process def demount(vehicleCD, deviceCD): item = itemsCache.items.getItemByCD(deviceCD) vehicle = itemsCache.items.getItemByCD(vehicleCD) slotId = yield getInstalledSlotIdx(vehicleCD, deviceCD) if slotId == -1: print('Device not found on vehicle') return action = ActionsFactory.getAction( ActionsFactory.REMOVE_OPT_DEVICE, vehicle, item, slotId, False, forFitting=False, everywhere=True ) result = yield ActionsFactory.asyncDoAction(action) ``` ### Контекстное меню Логику контекстного меню вынесем в `my_first_mod/contextMenuOverride.py`, переопределив `_generateOptions` и `onOptionSelect`. Из `onOptionSelect` вызываем `demount`. **my_first_mod/contextMenuOverride.py** ```python from helpers import dependency from skeletons.gui.shared import IItemsCache from gui.Scaleform.daapi.view.lobby.tank_setup.context_menu.opt_device import OptDeviceItemContextMenu from .demount import demount itemsCache = dependency.instance(IItemsCache) # type: IItemsCache # ==== Переопределение _generateOptions ==== orig_generateOptions = OptDeviceItemContextMenu._generateOptions def new_generateOptionsRealVehicles(obj, *a, **k): original_result = orig_generateOptions(obj, *a, **k) inventoryVehicles = itemsCache.items.getVehicles(REQ_CRITERIA.INVENTORY) installedVehicles = obj._getItem().getInstalledVehicles(inventoryVehicles.itervalues()) submenuItems = [ obj._makeItem('demountFrom:%d' % v.intCD, v.userName) for v in installedVehicles ] if len(submenuItems) == 0: return original_result original_result.append(obj._makeSeparator()) original_result.append(obj._makeItem('demount', 'Демонтировать с танка:', optSubMenu=submenuItems)) return original_result OptDeviceItemContextMenu._generateOptions = new_generateOptionsRealVehicles # ==== Переопределение onOptionSelect ==== orig_onOptionSelect = OptDeviceItemContextMenu.onOptionSelect def new_onOptionSelect(obj, optionId): if optionId.startswith('demountFrom:'): veh_id = optionId.split(':')[1] demount(int(veh_id), obj._getItem().intCD) # вызов демонтажа return return orig_onOptionSelect(obj, optionId) OptDeviceItemContextMenu.onOptionSelect = new_onOptionSelect ``` ### Инициализация мода Осталось инициализировать мод, для этого в `mod_myFirstMod.py` добавим импорт `contextMenuOverride.new_onOptionSelect`, просто чтобы инициализировать переопределение методов. **mod_myFirstMod.py** ```python from .my_first_mod.contextMenuOverride import new_onOptionSelect ... ``` ## Результат Готово. Теперь можно скомпилировать мод и протестировать в игре. ![final-result](https://docs.wotstat.info/guide/first-steps/first-mod/assets/final-result.png) ## Улучшения В результате получился рабочий мод, позволяющий демонтировать оборудование с других танков прямо из меню установки. Параллельно вы научились работать с исходным кодом игры и поняли процесс разработки. Мод можно улучшить, например: - Добавить опцию докупки оборудования как в оригинальном моде (искать по `ActionsFactory.BUY_MODULE`). - Если список танков длинный, то его можно разбить на подменю по уровню техники. - Добавить настройку мода, которая позволит автоматически демонтировать оборудование без показа диалогового окна (`ActionsFactory.doAction(ActionsFactory.REMOVE_OPT_DEVICE, vehicle, item, slotId, skipConfirm = True`). --- # Первый графический мод > Canonical URL: https://docs.wotstat.info/guide/first-steps/first-ui-mod/ > Markdown URL: https://docs.wotstat.info/guide/first-steps/first-ui-mod/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/first-steps/first-ui-mod/index.md В этом руководстве мы пройдём все этапы создания реального AS3-мода — в качестве примера будет повторён мод **калькулятор бронепробития**. Этот мод повышает информативность игрового "светофора", отображая в прицеле информацию о текущем бронепробитии с учётом расстояния до цели, а также о приведённой броне танка в точке прицеливания. ![hero](https://docs.wotstat.info/guide/first-steps/first-ui-mod/assets/hero.png) Мод является крайне полезным и не очень сложным в разработке, что делает его отличным примером для изучения. ## Идея мода Мод должен выводить в прицеле информацию о текущем бронепробитии с учётом расстояния до цели, а также о приведённой броне танка в точке, в которую вы целитесь. ## Шаги для реализации - Добавить на экран интерфейс вывода информации (текстовое поле в прицеле) - В момент перемещения прицела определять, на какую цель он наведён и сколько брони в этой точке ## Вычисление брони и пробития Для тестирования функции вычисления брони вам потребуется запустить тренировочную комнату. Позовите друга или воспользуйтесь [мультизапуском](https://docs.wotstat.info/articles/multilaunch/index.md). В «Мир Танков» уже существует механизм вычисления брони в точке прицеливания (цветовая индикация в прицеле о вероятности пробития). Нам нужно воспользоваться этим же механизмом. Реализовано это посредством класса `_CrosshairShotResults`. В нём присутствуют функции: - `_shouldRicochet` — проверяет, отрикошетит ли снаряд от брони - `getShotResult` - вычисляет о вероятности пробития (низкая, средняя, высокая). В процессе определения вычисляется и искомая фактическая толщина брони. - `__shotResultModernHE` – для осколочно-фугасных снарядов - `__shotResultDefault` – для всех остальных типов снарядов Нам потребуется реализовать похожий механизм, но в качестве результата вернуть значение приведённой брони, а не вероятность пробития. За цветовую индикацию в прицеле отвечает класс `ShotResultIndicatorPlugin`, в нём можно подсмотреть в какой момент происходит перерасчёт маркера. **ShotResultIndicatorPlugin.py** ```python from skeletons.gui.battle_session import IBattleSessionProvider from helpers import dependency ... sessionProvider = dependency.descriptor(IBattleSessionProvider) def start(self): ... ctrl = self.sessionProvider.shared.crosshair ctrl.onGunMarkerStateChanged += self.__onGunMarkerStateChanged # [!code highlight] def __onGunMarkerStateChanged(self, markerType, position, direction, collision): ... ``` Отсюда мы видим, что необходимо подписаться на событие `onGunMarkerStateChanged` из `IBattleSessionProvider.shared.crosshair`. ### Тестирование через PjOrion Во время тестов мы будем часто менять код обработчика события, если каждый раз подписываться заново, то будет накапливаться всё больше и больше подписок, что приведёт к множественному срабатыванию обработчика. Если просто заменять функцию без повторной подписки, то вызываться будет старая функция, а не новая, это можно обойти создав обёртку (`wrapper`): **PjOrion** ```python from skeletons.gui.battle_session import IBattleSessionProvider from helpers import dependency sessionProvider = dependency.instance(IBattleSessionProvider) # type: IBattleSessionProvider def onGunMarkerStateChanged(markerType, hitPoint, direction, collision): print("onGunMarkerStateChanged", markerType, hitPoint, direction, collision) def wrapper(*a, **k): onGunMarkerStateChanged(*a, **k) sessionProvider.shared.crosshair.onGunMarkerStateChanged += wrapper ``` Запустите тренировочную комнату и в `PjOrion` выполните этот код. После этого закомментируйте строку с подпиской. **PjOrion** ```python # sessionProvider.shared.crosshair.onGunMarkerStateChanged += wrapper ``` Теперь вы можете изменить функцию `onGunMarkerStateChanged` и выполнять её в `PjOrion`, не боясь, что будет накапливаться количество подписок. **PjOrion** ```python def onGunMarkerStateChanged(markerType, hitPoint, direction, collision): print("onGunMarkerStateChanged_new", markerType, hitPoint, direction, collision) ``` Вместо старой функции теперь будет вызываться новая. ### Реализация вычислений Создадим функцию `computeResult(hitPoint, direction, collision)`, в которой будут выполняться все вычисления. **PjOrion** ```python def computeResult(hitPoint, direction, collision): print("computeResult", hitPoint, direction, collision) def onGunMarkerStateChanged(markerType, hitPoint, direction, collision): print("onGunMarkerStateChanged_new", markerType, hitPoint, direction, collision) # [!code --] computeResult(hitPoint, direction, collision) # [!code ++] ``` Будем работать только с ней. Добавим базовые проверки и получим информацию о снаряде и игроке. **PjOrion** ```python from Vehicle import Vehicle as VehicleEntity from DestructibleEntity import DestructibleEntity def computeResult(hitPoint, direction, collision): if not collision: return entity = collision.entity if not isinstance(entity, (VehicleEntity, DestructibleEntity)): return player = BigWorld.player() if player is None: return vDesc = player.getVehicleDescriptor() shell = vDesc.shot.shell shellKind = shell.kind ppDesc = vDesc.shot.piercingPower maxDist = vDesc.shot.maxDistance piercingPowerRandomization = shell.piercingPowerRandomization dist = (hitPoint - player.getOwnVehiclePosition()).length ``` Получим ссылку на `_CrosshairShotResults` и вызовем его методы для вычисления брони. **PjOrion** ```python from AvatarInputHandler import gun_marker_ctrl shotResultResolver = gun_marker_ctrl.createShotResultResolver() def computeResult(hitPoint, direction, collision): ... # Актуальное пробитие на дистанции distPiercingPower = shotResultResolver._computePiercingPowerAtDist(ppDesc, dist, maxDist, 1) # Список всех столкновений с колиженом танка collisionsDetails = shotResultResolver._getAllCollisionDetails(hitPoint, direction, entity) if collisionsDetails is None: return ``` `collisionsDetails` это список всех `EntityCollisionData` на пути снаряда. В каждом из них есть информация о дистанции от референсной `hitPoint`, косинус угла попадания, типе брони и её толщине. ![collisionsDetails](https://docs.wotstat.info/guide/first-steps/first-ui-mod/assets/collisions-details.webp) Причём в этом списке будет и входное, и выходное столкновение с бронёй танка, а так же все экраны на пути снаряда. Сделаем функцию, которая будет вычислять суммарную приведённую броню до первого столкновения с танком (`vehicleDamageFactor == 1`). Дополнительно, на каждом шаге будет проверяться, не отрикошетит ли снаряд от брони. Также не забудем учесть потери кумулятивной струи в воздухе (`jetLoss`). > После выхода из первого слоя брони, кумулятивная струя начинает терять 50% пробития за каждый метр Кроме того, некоторые типы брони, учитываются только один раз, они помечены флагом `collideOnceOnly`, будем сохранять их в `ignoredMaterials`. **PjOrion** ```python def computeTotalEffectiveArmor(hitPoint, collision, direction, shell): # type: (Math.Vector3, typing.Optional[EntityCollisionData], Math.Vector3, Shell) -> (float, Boolean) if collision is None: return (0.0, False, False, 0.0) entity = collision.entity collisionsDetails = shotResultResolver._getAllCollisionDetails(hitPoint, direction, entity) # type: typing.List[SegmentCollisionResultExt] if not collisionsDetails: return (0.0, False, False, 0.0) totalArmor = 0.0 # суммарная приведённая броня (в мм) ignoredMaterials = set() isRicochet = False # был ли рикошет hitArmor = False # было ли попадание в основную броню jetStartDist = None # дистанция начала потерь кумулятивной струи jetLoss = 0.0 # потери кумулятивной струи jetLossPPByDist = shotResultResolver._SHELL_EXTRA_DATA[shell.kind].jetLossPPByDist # сколько теряет кумулятивная струя в воздухе на метр for c in collisionsDetails: if not shotResultResolver._CrosshairShotResults__isDestructibleComponent(entity, c.compName): break material = c.matInfo # type: MaterialInfo if material is None or material.armor is None: continue key = (c.compName, material.kind) if key in ignoredMaterials: continue hitAngleCos = c.hitAngleCos if material.useHitAngle else 1.0 totalArmor += shotResultResolver._computePenetrationArmor(shell, hitAngleCos, material) isRicochet |= shotResultResolver._shouldRicochet(shell, hitAngleCos, material) if material.collideOnceOnly: ignoredMaterials.add(key) if material.vehicleDamageFactor: # вычисляем потери кумулятивной струи в воздухе ПЕРЕД основным слоем if jetStartDist: jetLoss = (c.dist - jetStartDist) * jetLossPPByDist hitArmor = True break if jetStartDist is None and jetLossPPByDist > 0.0: jetStartDist = c.dist + material.armor * 0.001 # дистанция в метрах, а броня в мм – переводим return (float(totalArmor), isRicochet, hitArmor, jetLoss) ``` Вызовем эту функцию из `computeResult` и выведем результат в консоль. **PjOrion** ```python def computeResult(hitPoint, direction, collision): ... totalArmor, isRicochet, hitArmor, jetLoss = computeTotalEffectiveArmor(hitPoint, collision, direction, shell) print("Result: dist=%.1f distPP=%.1f armor=%.1f ricochet=%s hitArmor=%s jetLoss=%.2f" % ( dist, distPiercingPower, totalArmor, isRicochet, hitArmor, jetLoss )) ``` **Details — Весь код целиком** **PjOrion** ```python from AvatarInputHandler import gun_marker_ctrl from Vehicle import Vehicle as VehicleEntity from DestructibleEntity import DestructibleEntity from skeletons.gui.battle_session import IBattleSessionProvider from helpers import dependency sessionProvider = dependency.instance(IBattleSessionProvider) # type: IBattleSessionProvider shotResultResolver = gun_marker_ctrl.createShotResultResolver() def computeTotalEffectiveArmor(hitPoint, collision, direction, shell): # type: (Math.Vector3, typing.Optional[EntityCollisionData], Math.Vector3, Shell) -> (float, Boolean) if collision is None: return (0.0, False) entity = collision.entity collisionsDetails = shotResultResolver._getAllCollisionDetails(hitPoint, direction, entity) # type: typing.List[SegmentCollisionResultExt] if not collisionsDetails: return (0.0, False) totalArmor = 0.0 ignoredMaterials = set() isRicochet = False hitArmor = False jetStartDist = None jetLoss = 0.0 jetLossPPByDist = shotResultResolver._SHELL_EXTRA_DATA[shell.kind].jetLossPPByDist # сколько теряет кумулятивная струя в воздухе на метр for c in collisionsDetails: if not shotResultResolver._CrosshairShotResults__isDestructibleComponent(entity, c.compName): break material = c.matInfo # type: MaterialInfo if material is None or material.armor is None: continue key = (c.compName, material.kind) if key in ignoredMaterials: continue hitAngleCos = c.hitAngleCos if material.useHitAngle else 1.0 totalArmor += shotResultResolver._computePenetrationArmor(shell, hitAngleCos, material) isRicochet |= shotResultResolver._shouldRicochet(shell, hitAngleCos, material) hitArmor |= material.vehicleDamageFactor > 0 if material.collideOnceOnly: ignoredMaterials.add(key) if material.vehicleDamageFactor: # вычисляем потери кумулятивной струи в воздухе ПЕРЕД основным слоем if jetStartDist: jetLoss = (c.dist - jetStartDist) * jetLossPPByDist break if jetStartDist is None and jetLossPPByDist > 0.0: jetStartDist = c.dist + material.armor * 0.001 # точка старта за бронёй return (float(totalArmor), isRicochet, hitArmor, jetLoss) def computeResult(hitPoint, direction, collision): if not collision: return entity = collision.entity if not isinstance(entity, (VehicleEntity, DestructibleEntity)): return player = BigWorld.player() if player is None: return vDesc = player.getVehicleDescriptor() shell = vDesc.shot.shell shellKind = shell.kind ppDesc = vDesc.shot.piercingPower maxDist = vDesc.shot.maxDistance piercingPowerRandomization = shell.piercingPowerRandomization dist = (hitPoint - player.getOwnVehiclePosition()).length # Актуальное пробитие на дистанции distPiercingPower = shotResultResolver._computePiercingPowerAtDist(ppDesc, dist, maxDist, 1) # Список всех столкновений с колиженом танка collisionsDetails = shotResultResolver._getAllCollisionDetails(hitPoint, direction, entity) if collisionsDetails is None: return (distPiercingPower, None, None, None, None, None) totalArmor, isRicochet, hitArmor, jetLoss = computeTotalEffectiveArmor(hitPoint, collision, direction, shell) print("Result: dist=%.1f distPP=%.1f armor=%.1f ricochet=%s hitArmor=%s jetLoss=%.2f" % ( dist, distPiercingPower, totalArmor, isRicochet, hitArmor, jetLoss )) def onGunMarkerStateChanged(markerType, hitPoint, direction, collision): computeResult(hitPoint, direction, collision) def wrapper(*a, **k): onGunMarkerStateChanged(*a, **k) sessionProvider.shared.crosshair.onGunMarkerStateChanged += wrapper ``` После запуска наведите прицел на танк и в консоли вы увидите вывод о пробитии, эффективной броне, рикошете, попадании по основной броне и потере кумулятивной струи. ![console-output](https://docs.wotstat.info/guide/first-steps/first-ui-mod/assets/console-output.webp) ## Добавление интерфейса Теперь, когда у нас есть вычисления, нужно вывести результат на экран. Будем делать на основе `my.first_mod` из обучения по [настройке AS3-окружения](https://docs.wotstat.info/guide/first-steps/environment/as3/index.md). Основная идея состоит в том, чтобы подключиться к игре в момент начала боя, найти в иерархии интерфейса `BaseBattlePage` и добавить туда наше `View`, которое будет отображать информацию. Для начала просто добавим на экран полупрозрачный прямоугольник, чтобы понять, что всё работает. Для этого создайте в вашем проекте файл `as3/src/my/first_mod/PiercingMainView.as` **PiercingMainView.as** ```actionscript-3 package my.first_mod { import flash.display.Sprite; import flash.display.DisplayObject; import net.wg.infrastructure.base.AbstractView; import net.wg.data.constants.generated.LAYER_NAMES; import net.wg.gui.components.containers.MainViewContainer; import net.wg.gui.battle.views.BaseBattlePage; import net.wg.infrastructure.interfaces.IView; public class PiercingMainView extends AbstractView { private var infoBox:Sprite = new Sprite(); public function PiercingMainView() { super(); // Закрашиваем прямоугольник 150x20 полупрозрачным зеленым цветом infoBox.graphics.beginFill(0x00FF00, 0.5); infoBox.graphics.drawRect(0, 0, 150, 20); infoBox.graphics.endFill(); // Двигаем на центр экрана infoBox.x = App.appWidth * 0.5 - infoBox.width / 2; infoBox.y = App.appHeight * 0.55; } override protected function configUI():void { super.configUI(); // Получаем основной контейнер игры var viewContainer:MainViewContainer = App.containerMgr.getContainer( LAYER_NAMES.LAYER_ORDER.indexOf(LAYER_NAMES.VIEWS) ) as MainViewContainer; // Перебираем все дочерние компоненты и ищем BaseBattlePage for (var i:int = 0; i < viewContainer.numChildren; i++) { var child:DisplayObject = viewContainer.getChildAt(i); if (child is BaseBattlePage) { // Нашли BaseBattlePage, добавляем в него наш прямоугольник (child as IView).addChild(infoBox); } } } } } ``` > **WARNING — Внимание!** Не забудьте отредактировать `as3/build.bat`, добавив строку для компиляции вашего нового файла `PiercingMainView.as` в `my.first_mod.PiercingMainView.swf`: **as3/build.bat** ```bat @echo off rem ==== настройки ==== set "MXML_PATH=C:\apache-royale" rem ==== компиляция ==== set "MXMLC=%MXML_PATH%\royale-asjs\bin\mxmlc" call "%MXMLC%" -load-config+=build-config.xml --output=bin/my.first_mod.HelloWorldWindow.swf src/my/first_mod/HelloWorldWindow.as call "%MXMLC%" -load-config+=build-config.xml --output=bin/my.first_mod.PiercingMainView.swf src/my/first_mod/PiercingMainView.as ``` После компиляции в `as3/bin` появится файл `my.first_mod.PiercingMainView.swf`. Теперь создадим контролирующий Python-скрипт `my_first_mod/PiercingMainView.py`, который будет связан с `SWF`. **my_first_mod/PiercingMainView.py** ```python from frameworks.wulf import WindowLayer from gui.Scaleform.framework.entities.View import View from gui.Scaleform.framework import g_entitiesFactories, ScopeTemplates, ViewSettings from gui.shared import events, EVENT_BUS_SCOPE, g_eventBus MY_FIRST_MOD_PIERCING_MAIN_VIEW = "MY_FIRST_MOD_PIERCING_MAIN_VIEW" class PiercingMainView(View): def __init__(self, *args, **kwargs): super(PiercingMainView, self).__init__(*args, **kwargs) def setup(): settingsViewSettings = ViewSettings( MY_FIRST_MOD_PIERCING_MAIN_VIEW, PiercingMainView, "my.first_mod.PiercingMainView.swf", WindowLayer.TOP_WINDOW, None, ScopeTemplates.VIEW_SCOPE, ) g_entitiesFactories.addSettings(settingsViewSettings) def onAppInitialized(event): if event.ns == APP_NAME_SPACE.SF_BATTLE: app = ServicesLocator.appLoader.getApp(event.ns) # type: AppEntry app.loadView(SFViewLoadParams(MY_FIRST_MOD_PIERCING_MAIN_VIEW)) g_eventBus.addListener(events.AppLifeCycleEvent.INITIALIZED, onAppInitialized, EVENT_BUS_SCOPE.GLOBAL) ``` В функции `setup()` мы подписываемся на событие инициализации Scaleform‑приложения, проверяем, что загруженное приложение — боевой интерфейс (`SF_BATTLE`), и загружаем наш `PiercingMainView.swf`. Теперь в точке входа мода `mod_myFirstMod.py` нужно вызвать `setup()`: **mod_myFirstMod.py** ```python from .my_first_mod.PiercingMainView import setup as setupPiercingMainView MOD_VERSION = '{{VERSION}}' def init(): setupPiercingMainView() ``` Теперь можно скомпилировать мод и запустить игру. > **DANGER — Внимание!** Проверяйте моды только в тренировочных комнатах, на тестовых серверах или в режиме "Полигон". Ошибки в модах могут привести к сбоям игры. **Details — Результат** ![green-line](https://docs.wotstat.info/guide/first-steps/first-ui-mod/assets/green-line.png) ### Вывод пробития в интерфейс Теперь, когда у нас есть свой контейнер (зелёный прямоугольник) в общем интерфейсе боя, можно дочерним элементом добавить текстовое поле и выводить туда информацию о броне и пробитии. Будем выводить в формате `{актуальное пробитие}/{фактическая броня}`. Цвет текста будет зависеть от вероятности пробития: - Серый – на пути снаряда нет брони - Фиолетовый – рикошет - Ярко-красный – гарантированное непробитие - Ярко-зелёный – гарантированное пробитие - Градиент от мягко красного к мягко зелёному – вероятность пробития от 0% до 100% Значение текстового поля и его цвет будут передаваться из контролирующего Python-скрипта в AS3 `PiercingMainView`. #### Изменение AS3 кода В `PiercingMainView.as` добавим инициализацию текстового поля и метод для обновления текста и его цвета. Для текстового поля используем `flash.text.TextField`. Шрифт, размер, цвет и выравнивание настраиваются через `TextFormat`, а тень – через `DropShadowFilter`. **PiercingMainView.as** ```actionscript-3 package my.first_mod { import flash.display.Sprite; import flash.display.DisplayObject; import net.wg.infrastructure.base.AbstractView; import net.wg.data.constants.generated.LAYER_NAMES; import net.wg.gui.components.containers.MainViewContainer; import net.wg.gui.battle.views.BaseBattlePage; import net.wg.infrastructure.interfaces.IView; import flash.text.TextField; import flash.text.TextFormat; import flash.filters.DropShadowFilter; import flash.text.TextFormatAlign; import flash.text.AntiAliasType; public class PiercingMainView extends AbstractView { private var infoBox:Sprite = new Sprite(); private var infoText:TextField = new TextField(); public function PiercingMainView() { super(); // Закрашиваем прямоугольник (контейнер) 150x20 прозрачным цветом infoBox.graphics.beginFill(0, 0); infoBox.graphics.drawRect(0, 0, 150, 20); infoBox.graphics.endFill(); // Двигаем на центр экрана infoBox.x = App.appWidth * 0.5 - infoBox.width / 2; infoBox.y = App.appHeight * 0.55; // Настраиваем текст var format:TextFormat = new TextFormat(); format.size = 18; format.color = 0xFFFFFF; format.font = "$FieldFont"; format.align = TextFormatAlign.CENTER; var filter:DropShadowFilter = new DropShadowFilter(); filter.distance = 0; filter.angle = 0; filter.color = 0x000000; filter.alpha = 0.9; filter.blurX = filter.blurY = 2; filter.strength = 2; filter.quality = 10; infoText.selectable = false; infoText.mouseEnabled = false; infoText.antiAliasType = flash.text.AntiAliasType.ADVANCED; infoText.defaultTextFormat = format; infoText.setTextFormat(format); infoText.filters = [filter]; // Добавляем текстовое поле в контейнер, и настраиваем его размер и позицию infoBox.addChild(infoText); infoText.width = infoBox.width; infoText.height = infoBox.height; infoText.x = 0; infoText.y = 0; } } } ``` Если во время боя игрок изменит разрешение экрана, то наш прямоугольник и текст останутся на старых координатах. Чтобы вернуть их на центр экрана, нужно подписаться на событие изменения размера окна и обновлять позицию контейнера. **PiercingMainView.as** ```actionscript-3 package my.first_mod { ... import flash.events.Event; // [!code ++] public class PiercingMainView extends AbstractView { public function PiercingMainView() { super(); ... App.stage.addEventListener(Event.RESIZE, onAppResize); // [!code ++] } private function onAppResize(event:Event):void { // [!code ++] infoBox.x = App.appWidth * 0.5 - infoBox.width / 2; // [!code ++] infoBox.y = App.appHeight * 0.55; // [!code ++] } // [!code ++] } } ``` Далее нужно объявить метод, который будет вызываться из Python-кода для обновления текста и его цвета. **PiercingMainView.as** ```actionscript-3 package my.first_mod { ... public class PiercingMainView extends AbstractView { ... public function as_setText(value:String, color:uint):void { // [!code ++] infoText.text = value; // [!code ++] infoText.textColor = color; // [!code ++] } // [!code ++] } } ``` **Details — Весь код `PiercingMainView.as` целиком** ```actionscript-3 package my.first_mod { import flash.display.Sprite; import flash.display.DisplayObject; import net.wg.infrastructure.base.AbstractView; import net.wg.data.constants.generated.LAYER_NAMES; import net.wg.gui.components.containers.MainViewContainer; import net.wg.gui.battle.views.BaseBattlePage; import net.wg.infrastructure.interfaces.IView; import flash.text.TextField; import flash.text.TextFormat; import flash.filters.DropShadowFilter; import flash.text.TextFormatAlign; import flash.text.AntiAliasType; import flash.events.Event; public class PiercingMainView extends AbstractView { private var infoBox:Sprite = new Sprite(); private var infoText:TextField = new TextField(); public function PiercingMainView() { super(); // Закрашиваем прямоугольник 150x20 полупрозрачным зеленым цветом infoBox.graphics.beginFill(0, 0); infoBox.graphics.drawRect(0, 0, 150, 20); infoBox.graphics.endFill(); // Двигаем на центр экрана infoBox.x = App.appWidth * 0.5 - infoBox.width / 2; infoBox.y = App.appHeight * 0.55; // Настраиваем текст var format:TextFormat = new TextFormat(); format.size = 18; format.color = 0xFFFFFF; format.font = "$FieldFont"; format.align = TextFormatAlign.CENTER; var filter:DropShadowFilter = new DropShadowFilter(); filter.distance = 0; filter.angle = 0; filter.color = 0x000000; filter.alpha = 0.9; filter.blurX = filter.blurY = 2; filter.strength = 2; filter.quality = 10; infoText.selectable = false; infoText.mouseEnabled = false; infoText.antiAliasType = flash.text.AntiAliasType.ADVANCED; infoText.defaultTextFormat = format; infoText.setTextFormat(format); infoText.filters = [filter]; infoBox.addChild(infoText); infoText.width = infoBox.width; infoText.height = infoBox.height; infoText.x = 0; infoText.y = 0; App.instance.addEventListener(Event.RESIZE, onAppResize); } override protected function configUI():void { super.configUI(); // Получаем основной контейнер игры var viewContainer:MainViewContainer = App.containerMgr.getContainer( LAYER_NAMES.LAYER_ORDER.indexOf(LAYER_NAMES.VIEWS) ) as MainViewContainer; // Перебираем все дочерние компоненты и ищем BaseBattlePage for (var i:int = 0; i < viewContainer.numChildren; i++) { var child:DisplayObject = viewContainer.getChildAt(i); if (child is BaseBattlePage) { // Нашли BaseBattlePage, добавляем в него наш прямоугольник (child as IView).addChild(infoBox); } } } public function as_setText(value:String, color:uint):void { infoText.text = value; infoText.textColor = color; } private function onAppResize(event:Event):void { infoBox.x = App.appWidth * 0.5 - infoBox.width / 2; infoBox.y = App.appHeight * 0.55; } } } ``` #### Изменение Python кода В `my_first_mod/PiercingMainView.py` добавим функцию обёртку над вызовом AS3 метода `as_setText`. **my_first_mod/PiercingMainView.py** ```python class PiercingMainView(View): ... def as_setText(self, text, color): # [!code ++] self.flashObject.as_setText(text, color) # [!code ++] ``` Объявим функцию для вычисления вероятности пробития (с учётом Гауссового распределения рандомизации пробития при `σ=3`) > Можете не вникать в код, это стандартная формула для нормального распределения с учётом ограниченного диапазона. **my_first_mod/PiercingMainView.py** ```python def penetrationProbability(piercing, totalArmor, piercingPowerRandomization): P0 = float(piercing) A = float(totalArmor) x = float(piercingPowerRandomization) # Допустимый диапазон пробития L = P0 * (1.0 - x) U = P0 * (1.0 + x) k = 3 sigma = (x * P0) / k mu = P0 # Быстрые случаи if A <= L: return 1.0 if A >= U: return 0.0 # Стандартная нормальная CDF через erf def Phi(z): return 0.5 * (1.0 + math.erf(z / math.sqrt(2.0))) zL = (L - mu) / sigma zU = (U - mu) / sigma zA = (A - mu) / sigma denom = Phi(zU) - Phi(zL) if denom <= 1e-15: return 1.0 if A <= mu else 0.0 p = (Phi(zU) - Phi(zA)) / denom # Численная обрезка if p < 0.0: p = 0.0 if p > 1.0: p = 1.0 return p ``` Объявим константы цветов **my_first_mod/PiercingMainView.py** ```python RICOCHET_COLOR = 0xa85dfc NOT_ARMOR_COLOR = 0xbbbbbb GREEN_COLOR = 0x00FF00 RED_COLOR = 0xFF0000 SOFT_GREEN_COLOR = 0x48f32c SOFT_RED_COLOR = 0xf32c2c ``` Перенесём в этот класс функции `computeTotalEffectiveArmor`, `computeResult` и `onGunMarkerStateChanged` из наших тестов в `PjOrion`. **my_first_mod/PiercingMainView.py** ```python ... class PiercingMainView(View): ... sessionProvider = dependency.descriptor(IBattleSessionProvider) # type: IBattleSessionProvider def __init__(self, *args, **kwargs): super(PiercingMainView, self).__init__(*args, **kwargs) self.shotResultResolver = gun_marker_ctrl.createShotResultResolver() # type: gun_marker_ctrl._CrosshairShotResults def computeTotalEffectiveArmor(self, hitPoint, collision, direction, shell): ... def computeResult(self, hitPoint, direction, collision): ... print("Result: dist=%.1f distPP=%.1f armor=%.1f ricochet=%s hitArmor=%s jetLoss=%.2f" % ( # [!code --] dist, distPiercingPower, totalArmor, isRicochet, hitArmor, jetLoss # [!code --] )) # [!code --] return (distPiercingPower, totalArmor, isRicochet, hitArmor, jetLoss, piercingPowerRandomization) # [!code ++] ``` Подпишемся на событие `onGunMarkerStateChanged` в `__init__` и будем обновлять текст в этом методе **my_first_mod/PiercingMainView.py** ```python ... class PiercingMainView(View): ... def __init__(self, *args, **kwargs): ... self.sessionProvider.shared.crosshair.onGunMarkerStateChanged += self.onGunMarkerStateChanged def onGunMarkerStateChanged(self, markerType, hitPoint, direction, collision): result = self.computeResult(hitPoint, direction, collision) if result is None: return self.as_setText('', 0) piercing, totalArmor, isRicochet, hitArmor, jetLoss, piercingPowerRandomization = result if piercing is None: return self.as_setText('', 0) if totalArmor is None: return self.as_setText('%d/-' % round(piercing), GREEN_COLOR) # Уменьшаем пробитие на потери кумулятивной струи realPiercing = piercing * max(0, (1 - jetLoss)) targetColor = GREEN_COLOR if isRicochet: targetColor = RICOCHET_COLOR elif not hitArmor: targetColor = NOT_ARMOR_COLOR elif realPiercing <= 0.0: targetColor = RED_COLOR elif totalArmor <= 0.0: targetColor = GREEN_COLOR else: prob = penetrationProbability(realPiercing, totalArmor, piercingPowerRandomization) if prob <= 0: targetColor = RED_COLOR elif prob >= 1: targetColor = GREEN_COLOR # Преобразование цвета от SOFT_RED_COLOR к SOFT_GREEN_COLOR в зависимости от вероятности пробития else: targetColor = lerpColor( SOFT_RED_COLOR, SOFT_GREEN_COLOR, prob, mid_L_shift=+10.0, mid_C_boost=1.15 ) self.as_setText('%d/%d' % (round(realPiercing), round(totalArmor)), targetColor) ``` Для работы плавного перехода цвета необходима функция `lerpColor`. Скачайте [файл `color.py`](https://docs.wotstat.info/download/first-ui-mod/color.py) и поместите его в папку `my_first_mod`. В этом файле объявлены функции для работы с цветами. Импортируйте его в `PiercingMainView.py`: **my_first_mod/PiercingMainView.py** ```python from .color import lerpColor ... ``` **Details — Весь код `my_first_mod/PiercingMainView.py` целиком** **my_first_mod/PiercingMainView.py** ```python import typing import math from ProjectileMover import EntityCollisionData from frameworks.wulf import WindowLayer from gui.Scaleform.framework.entities.View import View from gui.Scaleform.framework import g_entitiesFactories, ScopeTemplates, ViewSettings from gui.shared import events, EVENT_BUS_SCOPE, g_eventBus from gui.app_loader.settings import APP_NAME_SPACE from gui.Scaleform.framework.application import AppEntry from gui.Scaleform.framework.managers.loaders import SFViewLoadParams from gui.shared.personality import ServicesLocator from items.components.shared_components import MaterialInfo from AvatarInputHandler import gun_marker_ctrl from Vehicle import Vehicle as VehicleEntity from DestructibleEntity import DestructibleEntity from skeletons.gui.battle_session import IBattleSessionProvider from helpers import dependency from .color import lerpColor import BigWorld MY_FIRST_MOD_PIERCING_MAIN_VIEW = "MY_FIRST_MOD_PIERCING_MAIN_VIEW" RICOCHET_COLOR = 0xa85dfc NOT_ARMOR_COLOR = 0xbbbbbb GREEN_COLOR = 0x00FF00 SOFT_GREEN_COLOR = 0x48f32c RED_COLOR = 0xFF0000 SOFT_RED_COLOR = 0xf32c2c def penetrationProbability(piercing, totalArmor, piercingPowerRandomization): P0 = float(piercing) A = float(totalArmor) x = float(piercingPowerRandomization) # Границы клампа L = P0 * (1.0 - x) U = P0 * (1.0 + x) k = 3 sigma = (x * P0) / k mu = P0 # Быстрые случаи if A <= L: return 1.0 if A >= U: return 0.0 # Стандартная нормальная CDF через erf def Phi(z): return 0.5 * (1.0 + math.erf(z / math.sqrt(2.0))) zL = (L - mu) / sigma zU = (U - mu) / sigma zA = (A - mu) / sigma denom = Phi(zU) - Phi(zL) if denom <= 1e-15: return 1.0 if A <= mu else 0.0 p = (Phi(zU) - Phi(zA)) / denom # Численная обрезка if p < 0.0: p = 0.0 if p > 1.0: p = 1.0 return p class PiercingMainView(View): sessionProvider = dependency.descriptor(IBattleSessionProvider) # type: IBattleSessionProvider def __init__(self, *args, **kwargs): super(PiercingMainView, self).__init__(*args, **kwargs) self.shotResultResolver = gun_marker_ctrl.createShotResultResolver() # type: gun_marker_ctrl._CrosshairShotResults self.sessionProvider.shared.crosshair.onGunMarkerStateChanged += self.onGunMarkerStateChanged def computeTotalEffectiveArmor(self, hitPoint, collision, direction, shell): # type: (Math.Vector3, typing.Optional[EntityCollisionData], Math.Vector3, Shell) -> (float, Boolean) if collision is None: return (0.0, False) entity = collision.entity collisionsDetails = self.shotResultResolver._getAllCollisionDetails(hitPoint, direction, entity) # type: typing.List[SegmentCollisionResultExt] if not collisionsDetails: return (0.0, False) totalArmor = 0.0 ignoredMaterials = set() isRicochet = False hitArmor = False jetStartDist = None jetLoss = 0.0 jetLossPPByDist = self.shotResultResolver._SHELL_EXTRA_DATA[shell.kind].jetLossPPByDist # сколько теряет кумулятивная струя в воздухе на метр for c in collisionsDetails: if not self.shotResultResolver._CrosshairShotResults__isDestructibleComponent(entity, c.compName): break material = c.matInfo # type: MaterialInfo if material is None or material.armor is None: continue key = (c.compName, material.kind) if key in ignoredMaterials: continue hitAngleCos = c.hitAngleCos if material.useHitAngle else 1.0 totalArmor += self.shotResultResolver._computePenetrationArmor(shell, hitAngleCos, material) isRicochet |= self.shotResultResolver._shouldRicochet(shell, hitAngleCos, material) hitArmor |= material.vehicleDamageFactor > 0 if material.collideOnceOnly: ignoredMaterials.add(key) if material.vehicleDamageFactor: # вычисляем потери кумулятивной струи в воздухе ПЕРЕД основным слоем if jetStartDist: jetLoss = (c.dist - jetStartDist) * jetLossPPByDist break if jetStartDist is None and jetLossPPByDist > 0.0: jetStartDist = c.dist + material.armor * 0.001 # точка старта за бронёй return (float(totalArmor), isRicochet, hitArmor, jetLoss) def computeResult(self, hitPoint, direction, collision): if not collision: return None entity = collision.entity if not isinstance(entity, (VehicleEntity, DestructibleEntity)): return None player = BigWorld.player() if player is None: return None vDesc = player.getVehicleDescriptor() shell = vDesc.shot.shell ppDesc = vDesc.shot.piercingPower maxDist = vDesc.shot.maxDistance piercingPowerRandomization = shell.piercingPowerRandomization dist = (hitPoint - player.getOwnVehiclePosition()).length # Актуальное пробитие на дистанции distPiercingPower = self.shotResultResolver._computePiercingPowerAtDist(ppDesc, dist, maxDist, 1) # Список всех столкновений с колиженом танка collisionsDetails = self.shotResultResolver._getAllCollisionDetails(hitPoint, direction, entity) if collisionsDetails is None: return (distPiercingPower, None, None, None, None, None) totalArmor, isRicochet, hitArmor, jetLoss = self.computeTotalEffectiveArmor(hitPoint, collision, direction, shell) return (distPiercingPower, totalArmor, isRicochet, hitArmor, jetLoss, piercingPowerRandomization) def onGunMarkerStateChanged(self, markerType, hitPoint, direction, collision): result = self.computeResult(hitPoint, direction, collision) if result is None: return self.as_setText('', 0) piercing, totalArmor, isRicochet, hitArmor, jetLoss, piercingPowerRandomization = result if piercing is None: return self.as_setText('', 0) if totalArmor is None: return self.as_setText('%d/-' % round(piercing), GREEN_COLOR) realPiercing = piercing * max(0, (1 - jetLoss)) targetColor = GREEN_COLOR if isRicochet: targetColor = RICOCHET_COLOR elif not hitArmor: targetColor = NOT_ARMOR_COLOR elif realPiercing <= 0.0: targetColor = RED_COLOR elif totalArmor <= 0.0: targetColor = GREEN_COLOR else: prob = penetrationProbability(realPiercing, totalArmor, piercingPowerRandomization) if prob <= 0: targetColor = RED_COLOR elif prob >= 1: targetColor = GREEN_COLOR else: targetColor = lerpColor(SOFT_RED_COLOR, SOFT_GREEN_COLOR, prob, mid_L_shift=+10.0, mid_C_boost=1.15) self.as_setText('%d/%d' % (round(realPiercing), round(totalArmor)), targetColor) def as_setText(self, text, color): self.flashObject.as_setText(text, color) def setup(): settingsViewSettings = ViewSettings( MY_FIRST_MOD_PIERCING_MAIN_VIEW, PiercingMainView, "my.first_mod.PiercingMainView.swf", WindowLayer.WINDOW, None, ScopeTemplates.DEFAULT_SCOPE, ) g_entitiesFactories.addSettings(settingsViewSettings) def onAppInitialized(event): if event.ns == APP_NAME_SPACE.SF_BATTLE: app = ServicesLocator.appLoader.getApp(event.ns) # type: AppEntry app.loadView(SFViewLoadParams(MY_FIRST_MOD_PIERCING_MAIN_VIEW)) g_eventBus.addListener(events.AppLifeCycleEvent.INITIALIZED, onAppInitialized, EVENT_BUS_SCOPE.GLOBAL) ``` ## Результат Готово — теперь можно скомпилировать мод и проверить его в игре. Video: [open media](https://docs.wotstat.info/guide/first-steps/first-ui-mod/assets/result.webm) ## Улучшения Этот мод можно улучшить следующими способами: - Отключить отображение пробития при наведении на союзников и трупы (проверять по `isinstance(entity, Vehicle)`, `entity.isAlive()`, `entity.health`) - Для БОПСов отображать "глубину" танка (с помощью `collisionsDetails[x].dist`) - Сделать отображение в виде процента вероятности пробить (`prob = penetrationProbability...`) - Сделать перетаскивание по экрану --- # Введение > Canonical URL: https://docs.wotstat.info/guide/first-steps/introduction/ > Markdown URL: https://docs.wotstat.info/guide/first-steps/introduction/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/first-steps/introduction/index.md Разработка модификаций подразумевает изменение каких-либо аспектов игры — будь то графика, интерфейс или игровой процесс. Игра состоит из клиентской и серверной частей. Расчёт всей игровой логики происходит на сервере и недоступен для какого-либо изменения со стороны клиента. Любые пользовательские модификации могут влиять исключительно на клиентскую часть игры. > **WARNING — Запрещённые модификации** Перед разработкой модификаций убедитесь, что вы ознакомились со [списком запрещённых модификаций](https://tanki.su/ru/content/guide/ban/nonusefulmods). Основная философия запрета заключается в том, что модификации не должны давать игроку преимущества в бою, которое не может быть получено без них. ## Компоненты игры Игра «Мир Танков» состоит из следующих основных компонентов: - **Движок игры** Core (`BigWorld`) — написан на C++. Отвечает за сетевое взаимодействие и графический конвейер. - **Игровые скрипты** — написаны на `Python 2.7`. Отвечают за логику игры и взаимодействие с пользователем. - **Пользовательский интерфейс** - Старые окна интерфейса — написаны на `Scaleform` (ActionScript 3.0, Flash). Весь боевой интерфейс и часть ангара. - Новый интерфейс — написан на `Coherent Gameface` (JavaScript, CSS, HTML). - Полноэкранные браузерные окна — в игре присутствуют окна, которые реализованы с помощью встроенного браузера и представляют собой обычные веб-сайты (например, сборочный цех). - **Ресурсы игры** — модели, текстуры, звуки и прочее — хранятся в виде файлов в `pkg`-архивах на диске и могут быть легко заменены. | Ресурс | Язык | Возможность модифицировать | | ---------------- | ---------------- | -------------------------- | | Движок игры | C++ | Почти невозможно | | Игровые скрипты | Python 2.7 | Легко | | Старый интерфейс | ActionScript 3.0 | С ограничениями | | Новый интерфейс | JavaScript | Легко, с ограничениями | | Браузерные окна | - | Невозможно | | Ресурсы игры | - | Легко | ## Виртуальная файловая система Игра использует виртуальную файловую систему (`VFS`), которая позволяет загружать ресурсы из нескольких источников и объединять их в единое целое. Официальные ресурсы игры хранятся в `pkg`-архивах в папке `/res/packages` внутри папки с игрой. Архивы `.pkg` являются обычными `zip`-архивами без сжатия и могут быть открыты с помощью любого архиватора, например, [`7-Zip`](https://www.7-zip.org/). По мере необходимости игра загружает ресурсы из этих архивов в общую виртуальную файловую систему. > Например, при запуске режима «Натиск» игра загружает `comp7.pkg`, который содержит все ресурсы, скрипты и интерфейсные элементы, необходимые для этого режима. ### Загрузка пользовательских модификаций Игра предусматривает автоматическую загрузку пользовательских модификаций в VFS из следующих папок: - `/mods/<версия_игры>/*.mtmod` — используется для распространения готовых модов. - `/res_mods/<версия_игры>/*` — устаревший вариант, иногда используется во время разработки модов. ### Формат `.mtmod` Формат `.mtmod` используется для распространения готовых модификаций. Файл `.mtmod` является обычным `zip`-архивом с **нулевым сжатием** и содержит структуру папок, которая будет воспроизведена в виртуальной файловой системе игры на этапе её загрузки. Принято использовать следующую структуру названия пакетов модов: ``` <автор>.<название_мода>_<версия_мода>.mtmod ``` > Например: `wotstat.analytics_1.0.0.mtmod`, `me.poliroid.gunmark_2.1.3.mtmod` #### Файл `meta.xml` Внутри архива `.mtmod` рекомендуется размещать файл `meta.xml`, который содержит описание мода в следующем формате: **meta.xml** ```xml Уникальное название мода Версия мода Отображаемое имя мода Описание мода ``` **Details — Пример `meta.xml`** **meta.xml** ```xml panikaxa.quick_demount 2.1.0 Quick demount Мод предоставляет возможность при нажатии правой кнопки мыши на слоте оборудования выбрать из контекстного меню танк, на котором установлено данное оборудование и автоматически демонтировать его с выбранного танка. ``` ### Точка входа модов Точка входа – это место, откуда начинается выполнение кода модификации. Игра автоматически импортирует `.pyc`-скрипты, которые начинаются с префикса `mod_` из папки `res/scripts/client/gui/mods` виртуальной файловой системы. В таком скрипте может быть определена функция `init()`, которая будет вызвана автоматически после загрузки игры и `fini()` перед выходом из игры. **Details — Пример файловой системы мода и `mod_` файла** ``` .mtmod ├── meta.xml └── res/ └── scripts/ └── client/ └── gui/ └── mods/ ├── mod_demo.pyc └── mod_demo/ ├── __init__.pyc ├── feature1.pyc └── feature2.pyc ``` **mod_demo.py** ```python import BigWorld def init(): print("Demo mod initialized") def fini(): print("Demo mod finalized") ``` > **WARNING — Внимание!** Если включён режим разработки (`constants.IS_DEVELOPMENT = True`) клиент будет импортировать `.py`-скрипты. Для включения этого режима необходимо перезаписать файл `res/scripts/common/constants.py`, указав `IS_DEVELOPMENT = True`. ## Файл с логами В корневой папке с игрой находится текстовый файл `python.log`, в который игра пишет свои логи, в том числе вывод оператора `print`. Этот файл можно открыть любым текстовым редактором. Просмотр этого файла позволяет отлаживать модификации и находить в них ошибки. > **TIP — Обратите внимание** У вас в системе могут быть отключены расширения файлов, поэтому файл может отображаться просто как `python` без `.log`. Вы можете включить отображение расширений в настройках проводника. --- # Знакомство с PjOrion > Canonical URL: https://docs.wotstat.info/guide/first-steps/pjorion/ > Markdown URL: https://docs.wotstat.info/guide/first-steps/pjorion/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/first-steps/pjorion/index.md Программа `PjOrion` — *Project "ORION"* — это инструмент, упрощающий разработку модификаций для игры «Мир Танков». Наиболее полезная функция `PjOrion` — `REPL` (Read-Eval-Print Loop) для `Python`‑скриптов игры. Она позволяет подключиться к процессу игры и выполнять код в его контексте без перезапуска клиента. ## Установка 1. Скачайте последнюю версию `PjOrion` из официальной [темы на форуме](https://koreanrandom.com/forum/topic/15280-pjorion-%D1%80%D0%B5%D0%B4%D0%B0%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%BA%D0%BE%D0%BC%D0%BF%D0%B8%D0%BB%D1%8F%D1%86%D0%B8%D1%8F-%D0%B4%D0%B5%D0%BA%D0%BE%D0%BC%D0%BF%D0%B8%D0%BB%D1%8F%D1%86%D0%B8%D1%8F-%D0%BE%D0%B1%D1%84%D1%83%D1%81%D0%BA%D0%B0%D1%86%D0%B8%D1%8F-%D0%BC%D0%BE%D0%B4%D0%BE%D0%B2-%D0%B2%D0%B5%D1%80%D1%81%D0%B8%D1%8F-135-%D0%B4%D0%B0%D1%82%D0%B0-11082019/). Нас интересует архив `PjOrion_1.3.5_11.08.2019.zip (архив с DLL)`. 2. Распакуйте архив в любую папку. **Details — Распакованный архив** ![unpacked](https://docs.wotstat.info/guide/first-steps/pjorion/assets/unpacked.png) 3. Запустите `PjOrion.exe`. **Details — Окно программы** ![main](https://docs.wotstat.info/guide/first-steps/pjorion/assets/main-window.png) 4. В папке игры переименуйте `/win64/Tanki.exe` в `/win64/WorldOfTanks.exe`. Это нужно для того, чтобы `PjOrion` мог найти исполняемый файл игры. **Details — Переименованный файл** ![main](https://docs.wotstat.info/guide/first-steps/pjorion/assets/wotexe.png) 5. Создайте две символические ссылки (`symlink`) из корня игры в папку `/win64` для файлов `paths.xml` и `version.xml`. Это нужно для того, чтобы `PjOrion` мог найти конфигурационные файлы игры. Что бы создать такие ссылки, запустите `cmd.exe` (командную строку) **от имени администратора** и выполните следующие команды: ```cmd mklink C:\Games\Tanki\win64\paths.xml C:\Games\Tanki\paths.xml mklink C:\Games\Tanki\win64\version.xml C:\Games\Tanki\version.xml ``` Замените `C:\Games\Tanki` на путь до вашей папки с игрой. **Details — Пример создания символических ссылок** ![mklink-result](https://docs.wotstat.info/guide/first-steps/pjorion/assets/mklink-result.png) 6. В `PjOrion` нажмите `WOT-Transmission -> Options...`, во всплывающем окне уберите галочку с `Automatically search a WOT`, после чего выберите путь до папки с игрой и подпапки `win64`: **Details — Окно настроек** ![options](https://docs.wotstat.info/guide/first-steps/pjorion/assets/options.png) 7. Запустите игру через `PjOrion`, нажав `WOT-Transmission -> Run WOT-Client -> WorldOfTanks`. Игра запустится, а в консоли `PjOrion` должен появиться лог игры: **Details — Консоль `PjOrion`** ![game-launched](https://docs.wotstat.info/guide/first-steps/pjorion/assets/game-launched.png) ## Настройка подсказок кода В `PjOrion` есть функция подсказок кода непосредственно для объектов игры. Для её активации: 1. Нажмите `ПКМ` (правую кнопку мыши) по окну ввода скриптов 2. Выберите раздел `Select the attributes source` 3. Выберите пункт `WOT` **Details — Меню выбора источника подсказок** ![suggestions-source](https://docs.wotstat.info/guide/first-steps/pjorion/assets/wot-suggestions.png) После этого в окне ввода скриптов после символа `.` (точка) будут появляться подсказки с атрибутами и методами объектов игры. **Details — Пример подсказок** ![suggestions](https://docs.wotstat.info/guide/first-steps/pjorion/assets/suggestions.png) ## Использование Если у вас получилось запустить игру через `PjOrion` и лог игры успешно подхватился, вы готовы к использованию `REPL` — Read-Eval-Print Loop (цикл чтения‑выполнения‑вывода), который позволяет выполнять `Python`‑код в контексте игры. В нижней части окна `PjOrion` есть поле ввода, в котором можно писать `Python`‑код и выполнять его **внутри запущенной игры**. В консоли вы можете импортировать любые модули игры, например `BigWorld`, в котором находятся основные функции движка. Для выполнения кода нажмите `Shift+F5` или `WOT-Transmission -> Exec script in client (for ANSI)`. ### Пример 1: Вывод версии игры Напечатайте в консоли следующий код и выполните его: **PjOrion** ```python from helpers import getShortClientVersion print(getShortClientVersion()) ``` **Details — Результат выполнения кода** ![example-1](https://docs.wotstat.info/guide/first-steps/pjorion/assets/example-1.png) ### Пример 2: Вывод никнейма игрока Функция `BigWorld.player()` возвращает объект игрока, который в **ангаре** является экземпляром класса [`PlayerAccount`](https://github.com/izeberg/wot-src/blob/709a8c2b9ede8a7515b45b92bdc2d3eacf14f784/sources/res/scripts/client/Account.py#L130). У этого объекта есть атрибут `name`, в котором хранится никнейм игрока. **PjOrion** ```python import BigWorld print(BigWorld.player().name) ``` **Details — Результат выполнения кода** ![example-2](https://docs.wotstat.info/guide/first-steps/pjorion/assets/example-2.png) --- # ModsList – Список модов > Canonical URL: https://docs.wotstat.info/guide/integrations/mods-list/ > Markdown URL: https://docs.wotstat.info/guide/integrations/mods-list/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/integrations/mods-list/index.md [ModsList](https://gitlab.com/wot-public-mods/mods-list) – это мод, который добавляет в ангар список функциональных кнопок для разных модов. Из меню можно вызывать функции других модов, например, открывать окно настроек или перейти в просмотр попаданий. ![Пример работы ModsList](https://docs.wotstat.info/guide/integrations/mods-list/assets/preview-lobby.png) ## Использование Импортируйте модуль `g_modsListApi` в вашем коде и используйте метод `addModification`, чтобы зарегистрировать ваш мод в списке. ```python try: from gui.modsListApi import g_modsListApi def callback(): print('On mod button click') g_modsListApi.addModification( id="mod_id", name='Название мода', description='Описание мода', icon='gui/maps/my.first_mod/modsListApi.png', enabled=True, login=False, lobby=True, callback=callback ) except: print_log('g_modsListApi not found') ``` --- # ModsSettings API – Настройки модов > Canonical URL: https://docs.wotstat.info/guide/integrations/mods-settings/ > Markdown URL: https://docs.wotstat.info/guide/integrations/mods-settings/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/integrations/mods-settings/index.md Если вам нужно добавить настройки для вашего мода, вы можете использовать готовый мод [ModsSettings API](https://github.com/izeberg/modssettingsapi). Этот мод предоставляет удобный API для создания и управления настройками модов прямо в клиенте игры. ![Пример настроек модов](https://docs.wotstat.info/guide/integrations/mods-settings/assets/preview-window.png) Основные этапы работы модификации заключается в следующем: 1. При загрузке клиента игры `modsSettingsApi` загружает файл сохраненных настроек сторонних модификаций. Если файл отсутствует - будет создан новый. 2. Затем `modsSettingsApi` ожидает подключения к себе сторонних модификаций посредством программного интерфейса. 3. Для генерации меню настроек, сторонняя модификация должна отправить в `modsSettingsApi` шаблон, содержащий в себе описание необходимых для отображения графических элементов. 4. Кроме шаблона сторонняя модификация должна отправить ссылку на функцию, которая будет вызываться при любом изменении настроек стороннего мода. При необходимости обработки нажатия дополнительных кнопок модификация должна отправить ссылку на функцию, которая будет вызывается при нажатии на кнопку с передачей текущего значения. 5. При изменении настроек `modsSettingsApi` будет вызывать переданный модификацией метод с новыми параметрами. При использовании кнопок, `modSettingsAPI` будет вызывать переданный модификацией метод с текущим параметром и его значением. ## Использование ```python # Текстовый идентификатор мода. # ! ДОЛЖЕН быть уникальным для каждого мода modLinkage = 'myOwnMod' # Словарь для хранения настроек мода settings = { ... } # Словарь с шаблоном стандартных настроек template = { ... } # Функция, которая будет вызываться настройщиком при изменении настроек мода. # linkage - текстовый идентификатор мода, # settings - словарь с новыми настройками def onSettingsChanged(linkage, settings): if linkage != modLinkage: return # ваш обработчик настроек # Функция, которая будет вызываться настройщиком при клике по доп кнопкам. # linkage - текстовый идентификатор мода # varName - имя параметра # value текущее значение параметра выбранное в интерфейсе def onButtonClicked(linkage, varName, value): if linkage != modLinkage: return # ваш обработчик настроек try: # Попытка импорта общей точки входа настройщика from gui.modsSettingsApi import g_modsSettingsApi # сначала необходимо запросить у настройщика сохраненные настройки savedSettings = g_modsSettingsApi.getModSettings((modLinkage, ), template) # если настройки имеются if savedSettings: settings = savedSettings # Применим их # Зарегистрируем функцию-обработчик новых настроек, и функцию-обработчик нажатий (если таковая имеется) g_modsSettingsApi.registerCallback((modLinkage, ), onSettingsChanged, onButtonClicked) else: # Отправим в настройщик шаблон стандартных настроек, функцию-обработчик новых настроек, и функцию-обработчик нажатий (если таковая имеется) settings = g_modsSettingsApi.setModTemplate((modLinkage, ), template, onSettingsChanged, onButtonClicked) except: # Если попытка импорта не удалась # Используем стандартные настройки мода или загружаем их из самописного конфига pass ``` При изменении настроек в функцию-обработчик новых настроек отправляются текстовый идентификатор мода и его новые настройки в виде словаря. Названия переменных `(varName)` указываются в шаблоне. ```python def onSettingsChanged(linkage, settings) ``` При использовании дополнительного обработчика нажатий в функцию-обработчик будет отправляться текстовый идентификатор мода, название переменной и ее текущее значение. Названия переменных `(varName)` указываются в шаблоне. ```python def onButtonClicked(linkage, varName, value) ``` ## Общие положения при создании шаблона Каждый шаблон – это словарь с обязательными полями. В чистом виде он выглядит так: ```python template = { # Отображаемое имя модификации 'modDisplayName': 'Название модификации', # Версия шаблона. При любых изменениях нуждается в изменении 'settingsVersion': 0, # Статус модификации. Активирована она в настройщике или нет 'enabled': True, # Первая колонка отображаемых элементов. # Отрисовка графических элементов начинается с X = 0 'column1': [ ], # Вторая колонка отображаемых элементов. # Отрисовка графических элементов начинается с X = ШИРИНА_ОКНА / 2 'column2': [ ] } ``` Для добавления полей для отображения в списки `'column1'` и `'column2'` добавляются словари, содержащие в себе описания элементов для отображения. Для добавления динамических кнопок к элементам используются настройки внутри параметра `"button"`. Динамические кнопки можно добавлять только к элементам с пометкой "Возможность добавления кнопки с действием". Для добавления всплывающих подсказок к элементам используется параметр `"tooltip"`. Всплывающие подсказки можно добавлять только к элементам с пометкой "Возможность добавления подсказки". ## Ручное изменение настроек Объект `g_modsSettingsApi` имеет метод для ручного обновления настроек (например для возможности модом считывать настройки из своего конфига и хранить их в настройщике). После вызова данного метода новые настройки через callback `(onModSettingsChanged)` вернутся в мод. ```python g_modsSettingsApi.updateModSettings((modLinkage, ), newSettings) ``` ## Templates API Для создания шаблонов настроек можно использовать готовый API `templates`, который содержит в себе функции для создания всех необходимых элементов. ```python from gui.modsSettingsApi import g_modsSettingsApi, templates template = { 'modDisplayName': 'Mod Name', 'enabled': True, 'column1': [ templates.createCheckbox( 'Demo CheckBox', 'var-name-checkbox', True, tooltip='{HEADER}Tooltip{/HEADER}{BODY}text{/BODY}' ), templates.createDropdown( 'Demo Dropdown', 'var-name-dropdown', ['Variant 1', 'Variant 2', 'Variant 3'], 0, tooltip='{HEADER}Tooltip{/HEADER}{BODY}text{/BODY}', button=templates.createButton( width=30, height=23, offsetTop=0, offsetLeft=0, icon='../maps/icons/buttons/sound.png', iconOffsetTop=0, iconOffsetLeft=1 ), width=200 ), templates.createSlider( 'Demo Slider', 'var-name-slider', 5, 1, 15, 1 ), templates.createNumericStepper( 'Demo NumericStepper', 'var-name-stepper', 5, 1, 15, 0.1 ), templates.createRangeSlider( 'Demo range slider', 'var-name-slider', [20, 50], 0, 100, 1, 50, 10, 50, '' ) ] } ``` ## С помощью словаря Шаблон можно создать и вручную, используя словари. ```python from gui.modsSettingsApi import g_modsSettingsApi, templates template = { 'modDisplayName': 'Mod Name', 'enabled': True, 'column1': [ { 'type': 'CheckBox', 'text': 'Demo CheckBox', 'varName': 'var-name-checkbox', 'value': True, 'tooltip': '{HEADER}Tooltip{/HEADER}{BODY}text{/BODY}', }, { 'type': 'Dropdown', 'text': 'Demo Dropdown', 'varName': 'var-name-dropdown', 'options': [ { 'label': 'Variant 1' }, { 'label': 'Variant 2' }, { 'label': 'Variant 3' } ], 'value': 0, 'tooltip': '{HEADER}Tooltip{/HEADER}{BODY}text{/BODY}', 'button': { 'width': 30, 'height': 23, 'offsetTop': 0, 'offsetLeft': 0, 'iconSource': '../maps/icons/buttons/sound.png', 'iconOffsetTop': 0, 'iconOffsetLeft': 1, }, 'width': 200 }, { 'type': 'Slider', 'text': 'Demo Slider', 'varName': 'var-name-slider', 'value': 5, 'minimum': 1, 'maximum': 15, 'snapInterval': 1, 'format': '{{value}}', }, { 'type': 'NumericStepper', 'text': 'Demo NumericStepper', 'varName': 'var-name-stepper', 'value': 5, 'minimum': 1, 'maximum': 15, 'snapInterval': 0.1, }, { 'type': 'RangeSlider', 'text': 'Demo range slider', 'varName': 'var-name-slider', 'value': [20, 50], 'minimum': 0, 'maximum': 100, 'snapInterval': 1, 'divisionLabelStep': 50, 'minRangeDistance': 10, 'divisionStep': 50, 'divisionLabelPostfix': '', }, ] } ``` ## Список элементов для отображения ### Надпись - Возможность добавления подсказки ```python { 'type': 'Label', # Тип элемента 'text': 'My Label' # Отображаемый текст } ``` ### Флажок - Возможность добавления подсказки - Возможность добавления кнопки с действием ```python { 'type': 'CheckBox', # Тип элемента 'text': 'My CheckBox', # Текст, отображаемый рядом с чекбоксом 'value': True, # Стандартное значение. True - галочка стоит, False - отсутствует 'varName': 'CheckBox1' # Имя переменной, соответствующей значению данного элемента } ``` ### Горячая клавиша - Возможность добавления подсказки ```python { 'type': 'HotKey', # Тип элемента 'text': 'My HotKey', # Текст, отображаемый слева от элемента ввода 'value': [Keys.KEY_Q], # Стандартное значение. Число обозначающее кнопку (смотрите дополнение с кнопками) 'varName': 'HotKey1' # Имя переменной, соответствующей значению данного элемента } ``` ### Ползунок - Возможность добавления подсказки - Возможность добавления кнопки с действием ```python { 'type': 'Slider', # Тип элемента 'text': 'My Slider', # Заголовок, отображаемый над слайдером 'minimum': 1, # Минимальное значение слайдера 'maximum': 15, # Максимальное значение слайдера 'snapInterval': 1, # Шаг слайдера 'value': 5, # Стандартное значение. Minimum - ползунок слайдера вначале, Maximum - ползунок слайдера в конце 'format': '{{{1}}}', # Формат строки, отображаемой рядом со слайдером и показывающей его значение. {{{1}}} - заменяется на значение слайдера 'varName': 'Slider1' # Имя переменной, соответствующей значению данного элемента } ``` ### Выпадающий список - Возможность добавления подсказки - Возможность добавления кнопки с действием ```python { 'type': 'Dropdown', # Тип элемента 'text': 'My Dropdown', # Заголовок, отображаемый над выпадающим списком # Пункты меню 'options': [ { 'label': 'Dropdown 1' }, # Пункт меню с индексом 0 { 'label': 'Dropdown 2' } # Пункт меню с индексом 1 ], 'width': 200, # Ширина выпадающего списка 'value': 0, # Индекс выбранного элемента. 0 - выбран первый элемент списка 'varName': 'Dropdown1' # Имя переменной, соответствующей значению данного элемента } ``` ### Группа радио-кнопок - Возможность добавления подсказки - Возможность добавления кнопки с действием ```python { 'type': 'RadioButtonGroup', # Тип элемента 'text': 'My RadioButtonGroup', # Заголовок, отображаемый над группой элементов # Список кнопок 'options': [ { 'label': 'RadioButton 1' }, # Пункт группы с индексом 0 { 'label': 'RadioButton 2' } # Пункт группы с индексом 1 ], 'value': 0, # Стандартное значение. 0 - выбран первый элемент списка кнопок 'varName': 'RadioButtonGroup1' # Имя переменной, соответствующей значению данного элемента } ``` ### Поле ввода текста - Возможность добавления подсказки ```python { 'type': 'TextInput', # Тип элемента 'text': 'My TextInput', # Заголовок, отображаемый над полем ввода 'width': 200, # Ширина поля ввода 'value': 'any text', # Стандартное значение. Любой текст 'varName': 'TextInput1' # Имя переменной, соответствующей значению данного элемента } ``` ### Пустой элемент - Необходим для логического разделения блоков элементов - Высота 20 пикселей ```python { 'type': 'Empty' # Тип элемента } ``` ### Динамическая кнопка - Прикрепляется к элементам - По нажатию передает в Python текущее значение элемента - Может быть как текстовой так и с иконкой - Поддерживает изменение положения и размера ```python { # Параметры вашего элемента ... # Динамическая кнопка 'button': { 'width': 60, # Ширина кнопки. Можно не указывать. Стандартно 60 пикселей 'height': 24, # Высота кнопки. Можно не указывать. Стандартно 24 пикселей 'offsetTop': 0, # Смещение положения по вертикали. Можно не указывать 'offsetLeft': 0, # Смещение положения по горизонтали. Можно не указывать 'text': 'Button Text', # Текст кнопки. Если используете иконку оставьте пустым 'iconSource': '../maps/icons/buttons/sound.png', # Путь к иконке. Если используете текст оставьте пустым 'iconOffsetTop': 0, # Смещение иконки по вертикали. Можно не указывать 'iconOffsetLeft': 1 # Смещение иконки по горизонтали. Можно не указывать } } ``` ### Всплывающая подсказка - Прикрепляется к элементам - По наведению отображает подсказку ```python { # Параметры вашего элемента ... # Подсказка (при наведении на иконку) 'tooltip': '{HEADER}Tooltip header{/HEADER}{BODY}Tooltip body{/BODY}' } ``` Для корректного отображения всплывающих подсказок `"tooltip"` поля с их текстом должны иметь следующий формат: `{HEADER}Заголовок подсказки{/HEADER}{BODY}Текст подсказки{/BODY}` Для отображения подсказки без заголовка достаточно просто не указывать тег `{HEADER}`, т.е текст подсказки должен выглядеть следующим образом: `{BODY}Текст подсказки{/BODY}` --- # Агрегатор стилей – интеграция кастомных стилей > Canonical URL: https://docs.wotstat.info/guide/integrations/user-customization/ > Markdown URL: https://docs.wotstat.info/guide/integrations/user-customization/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/integrations/user-customization/index.md > **WARNING — Внимание!** Этот раздел документации не завершён. Если вы готовы помочь — сообщите об этом [главному разработчику мода](https://t.me/lrvval). Хотите интегрировать свой камуфляж в игру, не меняя при этом игровые стили? Используйте Агрегатор стилей (далее — **Агрегатор**)! Пример интегрированного стиля: ![Пример интегрированного стиля (Hellraisers)](https://docs.wotstat.info/guide/integrations/user-customization/assets/style-example.jpg) Основные этапы работы модификации заключаются в следующем: 1. При загрузке клиента мод читает элементы кастомизации из JSON-файлов в папке `valberton/user_customization/json`. В случае ошибки чтения клиент намеренно крашится с записью в файл `python.log` в корневой директории игры. 2. В момент подключения игрока к серверу собираются данные о всех стилях, кроме интегрированных, с целью оптимизации работы с экраном Агрегатора. 3. При нажатии на кнопку `Внешний вид`: — Если у игрока не был применён стиль на стороне сервера — идёт переход к стандартному экрану внешнего вида с соответствующим уведомлением; — Если у игрока был применён стиль на стороне сервера — показывается оверлей, позволяющий выбрать, к какому окну кастомизации перейти: стандартному или Агрегатору. 4. При переходе к окну Агрегатора игрок выбирает категорию стиля (по умолчанию: Пользовательский 2D-стиль) и сам камуфляж. Для применения игрок нажимает соответствующую кнопку. Для стилей с отличительными особенностями предусмотрена более детальная настройка.bbbbbb ## Интеграция стиля Для того чтобы встроить свой камуфляж в игру, нам потребуются: 1. [Текстуры](#style-textures) — камуфляж, надписи, эмблемы и т. д. в формате `.dds`. 2. [Конфиг](#style-config) — описание элементов стиля (стилей) в формате `.json` (далее — **JSON-конфиг**). > **WARNING — Внимание!** Перед началом разработки стиля крайне рекомендуется ознакомиться с принципом работы формата **JSON**, так как он является основным форматом передачи данных мода в игру. Также стоит учесть, что в этой статье указание путей к файлам происходит от папки `res` в директории игры. То есть, `scripts/client/path/to/file.py` означает, что искать этот файл нужно по пути `<папка_с_игрой>/res/scripts/client/path/to/file.py` относительно [виртуальной файловой системы](https://docs.wotstat.info/guide/first-steps/introduction/index.md#vfs). Для 3D-стилей ещё необходимы: 4. [Модели](#style-models) (изменённые модели танка в форматах `.model`, `.primitives`, `.visual`). 5. *При необходимости!* [Префабы](#style-prefabs) (эффекты и двигающиеся части танка в формате `.prefab`). ## Текстуры > **WARNING — Внимание!** Данный раздел не завершён. Автор `UotsonDesign` допишет его, как разберётся с делами IRL. Здесь будет использоваться следующая терминология: 1. `Паттерн (Pattern)` — зацикленный узор. В камуфляжах существует несколько возможных типов текстур: 1. `AM (AlbedoMap)` — паттерн камуфляжа, выполняется в цвете. 2. `GMM (GlossMetallicMap)` — текстура металлических отражений, выполняется по определённому алгоритму. 3. `EmissionMap` — карта свечения, на ней указываются элементы, которые будут излучать свет. 4. `PatternMap` — карта анимации, эта карта показывает игре, какую область «подсвечивать» на `EmissionMap`. Работает циклично. Скорость анимации `PatternMap` и яркость `EmissionMap` регулируются в [секции камуфляжа в JSON-конфиге](#json-camouflages) следующими параметрами: **style.json** ```json { "camouflages": { "camouflage_name": { "forwardEmissionBrightness" = 7, "deferredEmissionBrightness" = 7, "emissionAnimationSpeed" = 0.3 } } } ``` ### Рисование камуфляжа #### Подготовка рабочей среды Для начала работы вам потребуются следующие программы: * [Мир Танков](#mt) (обязательно). * [Photoshop](#photoshop) или [Paint.NET](#paint-net). ##### Мир Танков _- Текст пока не готов. -_ ##### Photoshop 1. После скачивания Photoshop вам потребуется `DDS-расширение` для создания текстурных файлов с расширением `.dds`. Скачать расширение вы можете [здесь](https://disk.yandex.ru/d/4PXKJhSfBxL3kg). Для установки запустите инсталлер, соответствующий разрядности вашей ОС. 2. После установки расширения в `папке установки Photoshop` у вас должна появиться папка `NVTT`: ![Папка установки Photoshop](https://docs.wotstat.info/guide/integrations/user-customization/assets/instal-plugin.png) 3. Теперь запускаем или перезапускаем (если он был запущен) `Photoshop` и создаём файл с размерами, имеющими соотношение сторон `1:1`. > **TIP — Примеры размеров текстуры для «Мира Танков»** 1. 512x512 2. 1024x1024 3. 2048x2048 4. 4096x4096 (по желанию; трудно рисовать, весят много и могут уменьшать быстродействие игры и вашего компьютера) ##### Paint.NET _- Текст пока не готов. —_ *** #### AM — текстура Создание камуфляжа начинается с рисования основного `паттерна` — AM-текстуры. Для создания текстуры можно использовать `Photoshop` или `Paint.NET`. Ниже описаны действия для каждого приложения. В рисовании узора есть несколько техник: 1. `Прямоугольный узор` — его легко зациклить, не надо прибегать к каким-то сложным методам. 2. `Узор не касается краёв` — соответственно, не надо зацикливать, но нужно учесть, что если у вас какая-нибудь голова дракона, `то её надо рисовать только на одной половине текстуры` — нижней или верхней — и зеркалить по месту. 3. `Узор по диагонали` — его я и буду рассматривать ниже. **Details — Photoshop** 1. Создаём основу узора. ![Создаём основу узора](https://docs.wotstat.info/guide/integrations/user-customization/assets/before.png) > [!IMPORTANT] > Размер узора должен совпадать с размером квадрата под ним. Как пользоваться инструментом `выделение`, можете найти в интернете. ![alert-text](https://docs.wotstat.info/guide/integrations/user-customization/assets/alert-text.gif) 2. Заходим в контекстное меню, выбираем `Фильтр` -> `Другое` -> `Сдвиг`. Дальше выставляем параметры сдвига, равные половине от ширины и половине от высоты изображения, и ставим галочку на `Сделать прозрачным`. ![Сдвиг](https://docs.wotstat.info/guide/integrations/user-customization/assets/How-to-create-texture.png) > [!TIP] > У вас файл 1024x1024 => смещение будет равняться 1024x0,5 × 1024x0,5 = 512 × 512. 3. У вас должен получиться кусок узора, равный 1/4 от всего холста. Теперь мы его копируем (`CTRL+J`). 4. Нажимаем ПКМ по копии, выбираем `Отразить по горизонтали` (*или по вертикали, это не принципиально*) и тащим отражённую копию в противоположный угол с зажатым `SHIFT`. ![куда жмать](https://docs.wotstat.info/guide/integrations/user-customization/assets/How-to-create-texture1.png) 5. Повторяем пункт 4, но теперь выбираем два кусочка, нажимаем `CTRL+J` и теперь `отражаем копию по вертикали` (*или по горизонтали, смотря какой вариант вы выбрали в 4 пункте*). Результат должен получиться таким: ![Создаём основу узора](https://docs.wotstat.info/guide/integrations/user-customization/assets/result.png) 6. Сохраняем получившийся файл в папку `camouflages` по алгоритму: `Файл` -> `Сохранить как...` -> `DDS`. ![HowToSave](https://docs.wotstat.info/guide/integrations/user-customization/assets/How-to-save.png) ## Модели ### Анимация модели _- Тексты пока не готовы. -_ ## Префабы _- Текст пока не готов. -_ ## JSON-конфиг Описание элементов стиля содержится в формате `.json`. > **TIP — Как было раньше** До этого в моде использовались файлы формата `.xml`, в которых находилось много лишних записей, которые были важны игре, но путали пользователей и разработчиков стилей. Сейчас же подобные проблемы устранены. В одном конфиге может быть описано множество элементов (например, стиль, камуфляж, надписи и эмблемы к нему). Также поддерживается описание нескольких стилей в одном файле для простоты раздачи контента. JSON — это древовидный формат обмена данными, поэтому когда будут расписываться секции, имейте в виду, что запись типа `x.y.z` в файле будет выглядеть так: ```json { "x": { "y": { "z": "значение" } } } ``` Ниже приведены «пустышки» для всех импортируемых элементов кастомизации. Обязательные к указанию параметры будут подсвечены. *** ### Камуфляж **template.json** ```json { "camouflages": { "template_camo": { "texture": "valberton/user_customization/camouflages/template.dds", "tilingSettings": { "type": "relative", "factor": [2.574355, 2.582175], "offset": [0, 0] }, "scales": [1.0, 1.3, 0.5], "glossMetallicMap": "valberton/user_customization/camouflages/glossMetallicMap.dds", "metallic": [0.23, 0.23, 0.23, 0.23], "gloss": [0.509, 0.509, 0.509, 0.509], "emissionMap": "valberton/user_customization/camouflages/emissionMap.dds", "emissionPatternMap": "valberton/user_customization/camouflages/emissionPatternMap.dds", "emissionAnimationSpeed": 1.0, "forwardEmissionBrightness": 1.0, "deferredEmissionBrightness": 1.0, "normalMap": "valberton/user_customization/camouflages/normalMap.dds", "normalMaxLod": 1, "normalMapFactor": 1.0, "rotation": { "hull": 0.0, "turret": 0.0, "gun": 0.0 }, "palettes": [ [255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [0, 0, 0, 255] ] } } } ``` > **WARNING — Внимание!** В данном случае `template_camo` — это строковый идентификатор камуфляжа, называться он может как угодно. Используйте его для обозначения используемого элемента кастомизации в [стиле](#json-styles). **Обязательные параметры:** > * `camouflages.template_camo.texture` — путь до текстуры. > * `camouflages.template_camo.tilingSettings` — настройки тайлинга. > > ::: details Про тайлинг > Это неисследованная тема, рекомендуется пока не указывать этот параметр. > ::: > > * `camouflages.template_camo.scales` — масштаб для разных размеров паттернов. > ::: details Про масштаб > В игре есть три степени масштабирования камуфляжа/стиля: 1x, 2x и 3x. > > ![Пример с масштабированием стиля в 1x, 2x и 3x](https://docs.wotstat.info/guide/integrations/user-customization/assets/scale-example.png) > > Этот параметр позволяет выбрать, какой будет размер камуфляжа при определённом множителе масштабирования. > ::: **Необязательные параметры:** > Для эффекта металла и бликов: > > * `camouflages.template_camo.glossMetallicMap` — путь к текстуре `glossMetallicMap` (см. [Текстуры](#style-textures)). > * Его необязательно указывать, если нужно просто увеличить эффект. > > * `camouflages.template_camo.gloss` — величина блеска в формате RGBA. > * `camouflages.template_camo.metallic` — величина эффекта металла в формате RGBA. > > Для эффекта анимированного стиля: > > * `camouflages.template_camo.emissionMap` — путь к текстуре маски свечения (см. [Текстуры](#style-textures)). > * `camouflages.template_camo.emissionPatternMap` — путь к текстуре паттерна свечения (см. [Текстуры](#style-textures)). > * `camouflages.template_camo.forwardEmissionBrightness` — интенсивность свечения в стандартном рендере. > * `camouflages.template_camo.deferredEmissionBrightness` — интенсивность свечения в улучшенном рендере. > * `camouflages.template_camo.emissionAnimationSpeed` — скорость анимации свечения. > > Дополнительные параметры: > > * `camouflages.template_camo.palettes` — палитры камуфляжа. > * `camouflages.template_camo.rotation` — угол поворота текстуры. *** ### Декали: эмблемы и надписи Эмблемы и надписи являются одним типом элемента — декалями. **Не путайте их с проекционными декалями!** **template.json** ```json { "decals": { "template_emblem": { "type": "EMBLEM", "texture": "valberton/user_customization/decals/template_emblem.dds", "mirror": false }, "template_inscription": { "type": "INSCRIPTION", "texture": "valberton/user_customization/decals/template_inscription.dds", "mirror": false } } } ``` > **WARNING — Внимание!** В данном случае `template_emblem` и `template_inscription` — это строковый идентификатор эмблемы и надписи соответственно, называться они могут как угодно. Для простоты объяснения приведём их к единому названию — `template_decal`. Используйте их для обозначения используемых элементов кастомизации в [стиле](#json-styles). **Обязательные параметры:** > * `decals.template_decal.type` — тип декали. `EMBLEM` — эмблема, `INSCRIPTION` — надпись. > * `decals.template_decal.texture` — путь до текстуры. **Необязательные параметры:** > * `decals.template_decal.mirror` — булево значение (истина, ложь), означающее возможность отразить по горизонтали декаль. *** ### Присоединяемые элементы > **WARNING — Внимание!** Полная поддержка этого типа элемента не подтверждена, при попытке его использования могут возникнуть проблемы. Просим сообщить о состоянии поддержки [разработчику](https://t.me/lrvval). **template.json** ```json { "attachments": { "template_attachment": { "modelName": "valberton/user_customization/prefabs/template.prefab", "hangarModelName": "valberton/user_customization/models/template_hangar.prefab", "sequenceId": "template_sequence", "attachmentLogic": "prefab" } } } ``` > **WARNING — Внимание!** В данном случае `template_attachment` — это строковый идентификатор присоединяемого объекта, называться он может как угодно. Используйте его для обозначения используемого элемента кастомизации в [стиле](#json-styles). **Обязательные параметры:** > * `attachments.template_attachment.modelName` — путь до модели/префаба (см. [Модели](#style-models)). **Необязательные параметры** > * `attachments.template_attachment.hangarModelName` — путь до отдельной модели/префаба для отображения в ангаре. > * `attachments.template_attachment.sequenceId` — путь до файла анимации модели (см. [Анимация модели](#style-sequences)). > * `attachments.template_attachment.attachmentLogic` — схема логики элемента. > > На данный момент известно 3 схемы: > * `flagPart` — флаг. > * `flagAnimation` — анимация флага. > * `prefab` — префаб (см. [Префабы](#style-prefabs)). *** ### Отметки на стволе **template.json** ```json { "insignias": { "template_insignia": { "atlas": "valberton/user_customization/insignias/template_insignia_atlas.dds", "alphabet": "valberton/user_customization/insignias/template_insignia_alphabet.xml", "texture": "valberton/user_customization/insignias/template_insignia_single.dds", "emissionMap": "valberton/user_customization/camouflages/emissionMap.dds", "emissionPatternMap": "valberton/user_customization/camouflages/emissionPatternMap.dds", "emissionAnimationSpeed": 1.0, "forwardEmissionBrightness": 1.0, "deferredEmissionBrightness": 1.0, "mirror": false } } } ``` > **WARNING — Внимание!** В данном случае `template_insignia` — это строковый идентификатор отметок на стволе, называться он может как угодно. Используйте его для обозначения используемого элемента кастомизации в [стиле](#json-styles). **Обязательные параметры:** > * `insignias.template_insignia.atlas` — путь к атласу (см. `<ссылка_на_объяснение_отметок>`). > * `insignias.template_insignia.alphabet` — путь к «алфавиту». > > ::: details Пример «алфавита» > «Алфавит» описывает используемые координаты отображения отметок. > На первое время используйте этот пример: > > ```xml > > > > * > 0.0 0.0 > 1.0 1.0 > > > ``` > > где > * ` * ` означает, что эти координаты будут использоваться для всех символов/отметок. > * `begin` и `end` — начальные и конечные координаты используемой области относительно левого верхнего угла изображения в процентах (1.0 = 100%). > ::: > > * `insignias.template_insignia.texture` — путь к текстуре **одной** отметки. **Необязательные параметры** > Для эффекта анимированных отметок: > > * `insignias.template_insignia.emissionMap` — путь к текстуре маски свечения (см. [Текстуры](#style-textures)). > * `insignias.template_insignia.emissionPatternMap` — путь к текстуре паттерна свечения (см. [Текстуры](#style-textures)). > * `insignias.template_insignia.forwardEmissionBrightness` — интенсивность свечения в стандартном рендере. > * `insignias.template_insignia.deferredEmissionBrightness` — интенсивность свечения в улучшенном рендере. > * `insignias.template_insignia.emissionAnimationSpeed` — скорость анимации свечения. > > Дополнительные параметры: > * `insignias.template_insignia.mirror` — булево значение (истина, ложь), означающее возможность отразить по горизонтали декаль. *** ### Потёртости > **WARNING — Внимание!** От кастомизации этого типа элемента решено было отказаться в виду низкой заинтересованности. Используйте заранее определённый разработчиками [перечень пресетов](https://github.com/izeberg/wot-src/blob/RU/sources/res/scripts/item_defs/customization/modifications/list.xml). Строки по типу `1` - это идентификатор, который можно использовать в [стиле](#json-styles). *** ### Краски **template.json** ```json { "paints": { "template_paint": { "texture": "valberton/user_customization/paints/template_paint_icon.png", "color": [255, 255, 255, 255], "gloss": 0.509, "metallic": 0.23 } } } ``` > **WARNING — Внимание!** В данном случае `template_paint` — это строковый идентификатор краски, называться он может как угодно. Используйте его для обозначения используемого элемента кастомизации в [стиле](#json-styles). **Обязательные параметры:** > * `paints.template_paint.texture` — путь к иконке в интерфейсе. > * `paints.template_paint.color` — цвет в формате RGBA. **Необязательные параметры:** > Для эффекта металла и бликов: > > * `paints.template_paint.gloss` — величина блеска. > * `paints.template_paint.metallic` — величина эффекта металла. *** ### Персональный номер > **WARNING — Внимание!** Полная поддержка этого типа элемента не подтверждена, при попытке его использования могут возникнуть проблемы. Просим сообщить о состоянии поддержки [разработчику](https://t.me/lrvval). > **WARNING — Внимание!** В случае если вы хотите встроить свой шрифт для персонального номера, вам потребуется сделать это через отдельный элемент — [Шрифт](#json-fonts). **template.json** ```json { "personal_numbers": { "template_personal_number": { "texture": "valberton/user_customization/personal_numbers/template_font.png", "fontID": "template_font", "digitsCount": 3 } } } ``` > **WARNING — Внимание!** В данном случае `template_personal_number` — это строковый идентификатор персонального номера, называться он может как угодно. Используйте его для обозначения используемого элемента кастомизации в [стиле](#json-styles). **Обязательные параметры:** > * `personal_numbers.template_personal_number.texture` — путь к иконке в интерфейсе. > * `personal_numbers.template_personal_number.fontId` — уникальный числовой/строковый идентификатор шрифта. **Необязательные параметры:** > * `personal_numbers.template_personal_number.digitsCount` — количество символов в номере (по умолчанию — 3). *** ### Шрифт Этот элемент нужен в случае, если для персонального номера нужен вид символов, отличный от того, что есть в игре. > **WARNING — Внимание!** Полная поддержка этого типа элемента не подтверждена, при попытке его использования могут возникнуть проблемы. Просим сообщить о состоянии поддержки [разработчику](https://t.me/lrvval). **template.json** ```json { "fonts": { "template_font": { "texture": "valberton/user_customization/fonts/template_font.dds", "alphabet": "valberton/user_customization/fonts/template_font.xml", "mask": "" } } } ``` > **WARNING — Внимание!** В данном случае `template_font` — это строковый идентификатор шрифта, называться он может как угодно. Используйте его для обозначения используемого элемента кастомизации в [персональном номере](#json-personal-numbers). **Обязательные параметры:** > > * `fonts.template_font.texture` — путь к текстуре. > * `fonts.template_font.alphabet` — путь к «алфавиту». > > ::: details Пример «алфавита» > В каждом «алфавите» шрифта прописывается: > > * `root.glyph.name` — к какой цифре (или букве?) относится символ. > * `root.glyph.begin` — начальные координаты символа. > * `root.glyph.end` — конечные координаты символа. > > Начальные и конечные координаты указываются в процентах (1.0 = 100%). > > ```xml > > > 0 > 0.000000 0.000000 > 0.121 0.5 > > > 1 > 0.121 0.000000 > 0.2089 0.5 > > > 2 > 0.2089 0.000000 > 0.33 0.5 > > > 3 > 0.33 0.000000 > 0.4492 0.5 > > > 4 > 0.4492 0.000000 > 0.5742 0.5 > > > 5 > 0.5742 0.000000 > 0.6933 0.5 > > > 6 > 0.6933 0.000000 > 0.8144 0.5 > > > 7 > 0.8144 0.000000 > 0.9257 0.5 > > > 8 > 0.000000 0.5 > 0.1152 1 > > > 9 > 0.1152 0.5 > 0.2363 1 > > > ``` > ::: **Необязательные параметры:** > * `fonts.template_font.mask` — пока неизвестный параметр, так как разработчиками нигде не используется. *** ### Проекционные декали > **WARNING — Внимание!** На момент публичного тестирования мода крайне не рекомендуется использовать этот элемент ввиду его существенной нестабильности. **template.json** ```json { "projection_decals": { "template_projection_decal": { "texture": "valberton/user_customization/projection_decals/template_projection_decal.dds", "glossTexture": "valberton/user_customization/projection_decals/template_projection_decal_gloss.dds", "scaleFactorId": 3, "mirror": false, "emissionMap": "valberton/user_customization/camouflages/emissionMap.dds", "emissionPatternMap": "valberton/user_customization/camouflages/emissionPatternMap.dds", "emissionAnimationSpeed": 1.0, "forwardEmissionBrightness": 1.0, "deferredEmissionBrightness": 1.0 } } } ``` > **WARNING — Внимание!** В данном случае `template_projection_decal` — это строковый идентификатор проекционной декали, называться он может как угодно. Используйте его для обозначения используемого элемента кастомизации в [стиле](#json-styles). **Обязательные параметры:** > * `projection_decals.template_projection_decal.texture` — путь к текстуре. **Необязательные параметры:** > Для эффекта бликов: > > * `projection_decals.template_projection_decal.glossTexture` — путь к текстуре карты блеска. Если не указано, то используется стандартная логика бликов. > > Для эффекта анимированного стиля: > > * `projection_decals.template_projection_decal.emissionMap` — путь к текстуре маски свечения (см. [Текстуры](#style-textures)). > * `projection_decals.template_projection_decal.emissionPatternMap` — путь к текстуре паттерна свечения (см. [Текстуры](#style-textures)). > * `projection_decals.template_projection_decal.forwardEmissionBrightness` — интенсивность свечения в стандартном рендере. > * `projection_decals.template_projection_decal.deferredEmissionBrightness` — интенсивность свечения в улучшенном рендере. > * `projection_decals.template_projection_decal.emissionAnimationSpeed` — скорость анимации свечения. > > Дополнительные параметры: > > * `projection_decals.template_projection_decal.mirror` — булево значение (истина, ложь), означающее возможность отразить по горизонтали декаль. *** ### Стиль Все предыдущие элементы необходимо описывать только для финального действия — соединить их в стиль. **template.json** ```json { "styles": { "template": { "styleIcon": "gui/maps/vehicles/styles/template.png", "styleName": "Пример для подражания", "outfits": [{ "season": "ALL", "camouflages": [{ "id": "template_camo1", "appliedTo": 4368 }], "decals": [{ "id": "template_emblem1", "appliedTo": 13104 }, { "id": "template_inscription1", "appliedTo": 52416 }] }], "is3D": false, "modelsSet": "template_3Dst", "styleDescription": "Стиль-шаблон для мода \"Агрегатор стилей\"", "isWithSerialNumber": false, "vehicleFilter": { "exclude": { "vehicles": ["china:Ch01_Type59_Gold"] } }, "alternateItems": { "camouflage": [ "template_camo1", "template_camo2" ], "decal": [ "template_emblem1", "template_emblem2", "template_inscription1", "template_inscription2" ] } } } } ``` **Обязательные параметры:** > > * `styles.template.styleIcon` — путь к иконке в интерфейсе. > * `styles.template.styleName` — название стиля в интерфейсе. > * `styles.template.outfit` — описание стиля в интерфейсе. > > Для 3D-стилей (если стиль двухмерный - **не указывайте их**): > * `styles.template.is3D` — булево значение (истина, ложь), обозначающее, является ли стиль трёхмерным. Для этого типа целей всегда ставьте `true`. > * `styles.template.modelsSet` — строковый идентификатор пресета моделей для стиля из папки `valberton/user_customization/3Dst_json`. > > * `styles.template.outfits` — варианты внешнего вида стиля, которых может находиться несколько. При необходимости отображения разного внешнего вида для определённых типов карт - лучше описывать все стандартные: Летний, Зимний и Песчаный типы. > > > Теперь перечислим основные элементы стиля, доступные для использования в вышеуказанном параметре. > > ::: details Основные элементы стиля > > **Общие положения** > > Каждый такой элемент в конфиге стиля представляет из себя список с перечисленными параметрами используемых элементов кастомизации. Для простоты объяснения назовём один элемент этого списка как `item`. > Почти у всех есть следующие значения: > * `id` — > строковый или числовой идентификатор элемента стиля. В JSON-конфиге можно использовать одновременно элементы с обоими типами идентификаторов. > * `appliedTo` — область применения элемента. Каждая такая область имеет своё числовое значение. > Чтобы нанести элемент на несколько областей - просто сложите необходимые числа. > > *** > > **Камуфляж** > > ```json > { > "camouflages": [{ > "id": "template", > "appliedTo": 4638, > "patternSize": 1, > "palette": 0 > }] > } > ``` > > * Области применения камуфляжа: > * Орудие — `4096` > * Башня — `256` > * Корпус — `16` > * `camouflages.item.patternSize` — порядковый номер размера паттерна от 0 до 5. > * `camouflages.item.palette` — порядковый номер палитры, который указан в конфиге камуфляжа. Если он всего один в списке - используйте `0`. > > *** > > **Декали** > > ```json > { > "decals": [{ > "id": "template_emblem", > "appliedTo": 13104 > }, { > "id": "template_inscription", > "appliedTo": 52416 > }] > } > ``` > > Области применения эмблем: > > * Орудие — `4096`, `8192` > * Башня — `256`, `512` > * Корпус — `16`, `32` > > Области применения надписей: > > * Орудие — `16384`, `32768` > * Башня — `1024`, `2048` > * Корпус — `64`, `128` > > *** > > **Присоединяемые объекты** > > ```json > { > "attachments": [{ > "id": "template_attachment", > "slotId": 18000, > "position": [0.0, 0.0, 0.0], > "rotation": [0.0, 0.0, 0.0] > }] > } > ``` > > * `attachments.item.slotId` — идентификатор слота объекта танка, которые прописываются внутри скрипта танка (`item_defs/vehicles/<нация>/<имя_танка>.xml`). Может быть такое, что данного слота нет на танке. Предположительно, это в будущем можно будет обойти. > * (Необязательно) `attachments.item.position` — позиция объекта по осям XYZ относительно позиции слота. > * (Необязательно) `attachments.item.rotation` — поворот объекта по параметрам YPR относительно поворота слота. > > *** > > **Отметки на стволе** > > ```json > { > "insignias": [{ > "id": "template_insignia", > "appliedTo": 4096 > }] > } > ``` > > Область применения отметок всегда находится на орудии и ровна `4096`. > > *** > > **Потёртости** > > ```json > { > "modification": 5 > } > ``` > > Значение для этого элемента следуется брать из заранее сделаного разработчиками [перечня](https://github.com/izeberg/wot-src/blob/RU/sources/res/scripts/item_defs/customization/modifications/list.xml), где строка типа `5` означает, что у определённого пресета потёртостей именно этот идентификатор. Его и пишем. > > *** > > **Краски** > > ```json > { > "paints": [{ > "id": "template_paint", > "appliedTo": 30576 > }] > } > ``` > > Области применения красок: > > * Орудие — `4096`, `8192`, `16384` > * Башня — `256`, `512`, `1024` > * Корпус — `16`, `32`, `64` > * Гусеницы — `1`, `2`, `4` > > *** > > **Персональный номер** > > ```json > { > "personal_numbers": [{ > "id": "template_personal_number", > "number": "483", > "appliedTo": 4096 > }] > } > ``` > > * `personal_numbers.item.number` — строка с самим персональным номером, то есть то, какое число будет отображаться в игре. Можно оставить пустым. > > > Области применения персонального номера такие же, как и у надписей. > > *** > > **Проекционная декаль** > > ```json > { > "projection_decals": [{ > "id": "template_projection_decal", > "scaleFactor": 2, > "tags": ["formfactor_square", "safe", "left"] > }] > } > ``` > > * `projection_decals.item.scaleFactorId` — идентификатор фактора масштабирования от 0 до 3. > * В `projection_decals.item.tags` самым важным тегом является положение декали на танке (`right`, `front`, `left`). > ::: > **Необязательные параметры:** > > * `styles.template.styleDescription` — описание стиля во всплывающей подсказке. > * `styles.template.isWithSerialNumber` — булево значение (истина, ложь), обозначающее, имеет ли стиль серийный номер с табло. > * `styles.template.vehicleFilter` — фильтр по технике: > > ::: details Фильтр по технике > С помощью этой секции вы можете обозначить, на какой танк/нацию можно нанести стиль. > > Нанести только на определённый танк: > ```json > { > "vehicleFilter": { > "include": { > "vehicles": ["germany:G98_Waffentrager_E100"] > } > } > } > ``` > или **не** наносить на него: > ```json > { > "vehicleFilter": { > "exclude": { > "vehicles": ["china:Ch01_Type59_Gold"] > } > } > } > ``` > > Если нужно обозначить всю нацию, замените `vehicleFilter.include.vehicles` или `vehicleFilter.exclude.vehicles` на `vehicleFilter.include.nations` или `vehicleFilter.exclude.nations` соответственно. > > Таким же образом можно сделать и по уровням техники - `levels`. > ::: > > * `styles.template.alternateItems` — дополнительные элементы стиля. > > ::: details Дополнительные элементы стиля > В моде есть возможность прописать альтернативные элементы стиля для более гибкой кастомизации. > > ```json > { > "alternateItems": { > "camouflage": [ > "template_camo1", > "template_camo2" > ], > "decal": [ > "template_emblem1", > "template_emblem2", > "template_inscription1", > "template_inscription2" > ] > } > } > ``` > > * `alternateItems.<тип_элемента>` — перечень альтернативных элементов, где `тип_элемента` — тип элемента так, как вы его называли в JSON-конфиге, но в единственном числе. Например, не `paints`, а `paint`. Не `personal_numbers`, а `personal_number`. > Значениями в этих перечнях выступают строковые и числовые идентификаторы элементов. Можно одновременно использовать оба типа идентификаторов. > ::: --- # Взаимодействие с игрой > Canonical URL: https://docs.wotstat.info/guide/integrations/wotstat-widgets/data-provider/ > Markdown URL: https://docs.wotstat.info/guide/integrations/wotstat-widgets/data-provider/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/integrations/wotstat-widgets/data-provider/index.md Для взаимодействия с игрой используется мод `data-provider`, он запускает локальный `WebSocket` сервер, к которому могут подключаться виджеты для получения данных об игре. В целях безопасности, `data-provider` односторонний, он предаёт данные виджетам, но не позволяет виджетам отправлять команды в игру. ## Основная концепция Data Provider оперирует двумя сущностями для передачи данных: - `State` – состояние, которое хранит актуальное значение и уведомляет об изменениях этого значения. *Например текущий танк игрока.* - `Trigger` – мгновенное событие, которое не имеет постоянного значения, и просто уведомляет о том, что событие произошло (может передавать данные). *Например получение результатов боя.* ## Работа c `wotstat-widgets-sdk` Все виджеты это обычные веб-страницы, что бы добавить поддержку Data Provider, необходимо подключить [`wotstat-widgets-sdk`](https://www.npmjs.com/package/wotstat-widgets-sdk) и подписаться на необходимые состояния и триггеры. ### Установка **npm** ```sh $ npm add -D wotstat-widgets-sdk ``` **bun** ```sh $ bun add -D wotstat-widgets-sdk ``` **index.html** ```html ``` ### WidgetSDK Для начала вам необходимо инициализировать SDK, так же вы можете подписаться на изменение статуса подключения к игре. ```js import { WidgetSDK } from 'wotstat-widgets-sdk' // инициализация SDK const sdk = new WidgetSDK() // подписка на изменение статуса (ожидание открытия игры) sdk.onStatusChange(status => console.log(status)) ``` У объекта `sdk` есть поле `data`, которое содержит все доступные состояния и триггеры. Состояния определяются классом `State`, у которого есть поле `value` с текущим значением и метод `watch`, который позволяет подписаться на изменение значения. У триггеров есть только метод `watch`, который позволяет подписаться на событие. У объекта `sdk` есть полная `TypeScript` типизация, благодаря которой у вас будут подсказки по доступным состояниям и триггерам. Изучить доступные состояния и триггеры вы можете в [исходном коде библиотеки](https://github.com/wotstat/wotstat-widgets-sdk/blob/main/lib/sdk/dataTypes/index.ts). ```js // получение текущего танка const currentTank = sdk.data.hangar.vehicle.info.value console.log('Current tank:', currentTank) // подписка на изменение танка sdk.data.hangar.vehicle.info.watch((newValue, oldValue) => { console.log('New tank:', newValue) console.log('Old tank:', oldValue) }) // подписка на получение результата боя sdk.data.battle.onBattleResult.watch(result => { console.log('Battle result:', result) }) ``` Виджет может получать информацию от кнопок управления в игре (если виджет добавлен в игру через мод `Wotstat Widgets`). Например, вы можете подписаться на событие очистки данных виджета через `sdk.commands.onClearData`: ```js // подписка на действие очистки данных const { setReadyToClearData } = sdk.commands.onClearData(() => console.log('Clear data')) // готовность к очистке данных (если передать false, то кнопки очистки данных не будет) setReadyToClearData(true) ``` ### WidgetMetaTags Для определения того, как мод `Wotstat Widgets` должен обрабатывать ваш виджет, вы можете использовать специальные мета-теги в `HTML` вашего виджета с префиксом `wotstat-widget:`. Пример `meta` тега в `HTML`: ```html ``` Поддерживаемые теги: | Мета-тег | Описание | | --------------------- | ------------------------------------------------------------------------------------ | | `auto-height` | Автоматическое изменение высоты виджета по размеру `body` | | `hangar-only` | Виджет доступен только в ангаре | | `ready-to-clear-data` | Готовность к очистке данных (если передать false, то кнопки очистки данных не будет) | | `use-sniper-mode` | Позиция виджета должна отключаться в аркадном и снайперском прицелах | | `preferred-top-layer` | Виджет должен быть в верхнем слое (ручная настройка по ПКМ более приоритета) | | `unlimited-size` | Убрать ограничение на размер виджета | | `insets` | Установить отрицательный отступ, чтоб виджет мог выходить за границы рамки | Для удобной работы с мета-тегами, вы можете использовать класс `WidgetMetaTags` из `wotstat-widgets-sdk`. ```js import { WidgetMetaTags } from 'wotstat-widgets-sdk' // включить автоматическое изменение высоты виджета, если оно было отключено WidgetMetaTags.setAutoHeight(true) // сделать виджет доступным только в ангаре WidgetMetaTags.setHangarOnly(true) // разрешить очистку данных (пкм -> очистить данные) WidgetMetaTags.setReadyToClearData(true) // указать, что позиция виджета должна различаться в аркадном и снайперском прицелах (ручная настройка по ПКМ более приоритета) WidgetMetaTags.setUseSniperMode(true) // указать, что виджет должен быть в верхнем слое (ручная настройка по ПКМ более приоритета) WidgetMetaTags.setPreferredTopLayer(true) // убрать ограничение на размер виджета WidgetMetaTags.setUnlimitedSize(true) // установить отрицательный отступ, чтоб виджет мог выходить за границы рамки WidgetMetaTags.setInsets({ top: 10, right: 10, bottom: 10, left: 10 }) ``` ### WidgetRelay Используется для создания pear-to-pear взаимодействия между виджетами. Позволяет определить состояние, которое будет синхронизироваться между всеми виджетами, использующими этот `WidgetsRelay`. Состояния не хранятся на сервере, а передаются напрямую между виджетами. ```js import { WidgetsRelay } from 'wotstat-widgets-sdk' const relay = new WidgetsRelay() const simple = relay.createState('simple', 0) const complex = relay.createState('complex', { foo: { bar: 0, 'long/deep': 0 }, baz: 0 }) ``` Для установки значения состояния используется обычная запись в свойство `value`, при изменении комплексных значений, необходимо вызвать метод `trigger()`, чтоб сообщить системе, что значение изменилось. ```js // Слежение за изменением, вызывается и при изменении своего значения и при синхронизации simple.watch(v => { console.log('Simple value changed:', v) console.log('Current value:', simple.value) console.log('All users values:', simple.all) }, { immediate: true }) // Изменение простого состояния simple.value = 5 // Изменение комплексного состояния counter.value.baz = 10 counter.trigger() ``` Синхронизация производится внутри канала по ключу состояния, ключ канала задаётся в URL, например: `?channel-key=demo` или в параметрах `new WidgetsRelay({ channel: 'demo' })`. ### Стили SDK предоставляет некоторые стандартные стили для удобства разработки виджетов. Доступ к стилям можно получить двумя способами: - Через использование длинных классов (например, `wotstat-background`, `wotstat-accent`) - Через использование родительского класса `widgets-sdk-styles` и дочерних коротких классов (например, `background`, `accent`) Стили будут доступны после инициализации SDK или вы можете проинициализировать их самостоятельно: ```js import { injectStylesheet, setupStyles } from 'wotstat-widgets-sdk' // Вставляет код CSS в head документа injectStylesheet() // Вставляет код CSS в head документа и добавляет обработчики на обновление стилей от query параметров. Вызов injectStylesheet не требуется. setupStyles() ``` > Цвета `background` и `accent` автоматически изменяются в зависимости от query параметров в URL. > `background` и `accent` соответственно, например: `?background=292929&accent=4ee100`. Поддерживается прозрачность. ```html

Widget

Widget

``` Более подробная информация в [документации о стилях](https://github.com/wotstat/wotstat-widgets-sdk/blob/HEAD/docs/styles.md). ## Моды-расширения Если вам недостаточно предоставляемых данных, вы можете написать мод-расширение для `data-provider`, которое будет передавать дополнительные данные в виджеты. ### Проверка наличия `dataProvider` ```python def checkDataProvider(): if not hasattr(BigWorld, 'wotstat_dataProvider'): return False return BigWorld.wotstat_dataProvider.version ``` ### Регистрация расширения ```python demo_extension = BigWorld.wotstat_dataProvider.registerExtension('exampleExtension') ``` ### Состояние `State` ```python # регистрация состояния state = demo_extension.createState(['demo', 'state'], 'Hello World!') # получить текущее значение value = state.getValue() # установить новое значение state.setValue('Hello World, Again!') ``` ### Триггер `Trigger` ```python # регистрация триггера state = demo_extension.createTrigger(['demo', 'trigger']) # вызов триггера c данными trigger.trigger('Hello World from Trigger!') ``` ### Использование расширений Доступ к данным расширений осуществляется по пути `data.extensions`, список зарегистрированных расширений можно получить по пути `data.registeredExtensions`. Вы можете подписаться на изменения данных расширения даже если оно не зарегистрировано в SDK. В этом случае значение по любому пути будет `undefined` до тех пор, пока расширение не будет зарегистрировано. Рекомендуется типизировать используемые расширения с помощью `d.ts` файлов. > Объявление типов расширений необходимо исключительно для удобства разработки и не влияет на работу SDK. **ExampleExtension.d.ts** ```ts import { State, Trigger, WidgetSDK } from "wotstat-widgets-sdk" declare global { interface WidgetsSdkExtensions { exampleExtension: { demo: { state: State trigger: Trigger } } } } ``` Использование расширения в коде виджета: ```ts const sdk = new WidgetSDK() sdk.data.extensions.exampleExtension.demo.state.onChange(<...>) sdk.data.extensions.exampleExtension.demo.trigger.onTrigger(<...>) ``` --- # WotStat Виджеты > Canonical URL: https://docs.wotstat.info/guide/integrations/wotstat-widgets/introduction/ > Markdown URL: https://docs.wotstat.info/guide/integrations/wotstat-widgets/introduction/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/integrations/wotstat-widgets/introduction/index.md WotStat Виджеты — это набор инструментов для создания и отображения виджетов связанных с игрой Мир Танков. ![architecture](https://docs.wotstat.info/guide/integrations/wotstat-widgets/introduction/assets/wotstat-widgets.png) - `wotstat-widgets` — мод для игры, который позволяет добавлять веб-виджеты прямо в игру. - `data-provider` — мод для игры, который безопасно передаёт в веб-виджеты данные об игре. Включен в состав `WotStat Widgets`, но может быть установлен и отдельно. - Поддерживает моды-расширения, которые могут могут добавлять новые данные для передачи в `widgets-sdk`. - `widgets-sdk` – библиотека JavaScript, которая облегчает создание виджетов, взаимодействующих с игрой через `Data Provider`. - `widgets-relay` — сервис, который позволяет передавать данные между виджетами (например для взводной синхронизации). - `widgets-remote` — сервис, который позволяет удалённо управлять виджетами (в основном для стримов) - `remote-control` – веб сайт для удалённого управления виджетами через `widgets-remote`. --- # Виджеты с удалённым управлением > Canonical URL: https://docs.wotstat.info/guide/integrations/wotstat-widgets/remote-control/ > Markdown URL: https://docs.wotstat.info/guide/integrations/wotstat-widgets/remote-control/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/integrations/wotstat-widgets/remote-control/index.md Виджеты с удалённым управлением позволяют изменять состояние виджета через веб-интерфейс. При этом управлять таким виджетом может другой человек удалённо. В первую очередь это полезно для стримеров, которые могут передать управление виджетами своим модераторам. Такие виджеты разрабатывать намного проще чем автоматические, так как они не требуют подключения к игре, а значит не ограничиваются возможностями `data-provider`. Вместо этого они используют сервис `widgets-remote`, который позволяет определить произвольный набор параметров изменяемых вручную. Например виджет показа прогресса по ЛБЗ, `data-provider` не предоставляет текущий прогресс, и делать мод-расширение для такой задачи слишком трудоёмко. Вместо этого можно сделать виджет с удалённым управлением, в котором модератор или сам стример будет вручную указывать текущий прогресс. > Одним и тем же виджетом может управлять сразу несколько человек, изменения синхронизируются не только для виджета, но и для всех подключённых панелей управления. ![widget-example](https://docs.wotstat.info/guide/integrations/wotstat-widgets/remote-control/assets/widget-example.png) ## Как это работает У каждого виджета есть связанная пара ключей: - `ключ доступа` – с помощью него виджет получает информацию - `ключ управления` – с его помощью можно изменять состояние Сервер `widgets-remote` хранит состояния для всех пар ключей, когда виджет загружается, он запрашивает текущее состояние по `ключу доступа` и применяет его. С помощью веб-интерфейса можно изменять состояние по `ключу управления`, и сервер уведомит все подключённые виджеты об изменении. Для сокращения задержки при обновлении состояния, виджеты используют `Web-Socket` соединение с сервером `widgets-remote`, что позволяет получать обновления практически мгновенно. Чтобы панель управления узнала, какие именно параметры доступны виджету, в неё необходимо вставить ссылку на этот виджет, после чего, сам виджет сообщает панели управления о своих параметрах. Благодаря этому, одна и та же панель управления может работать с разными в том числе и сторонними виджетами. Панель управления доступна по адресу: [widgets.wotstat.info/remote-control](https://widgets.wotstat.info/remote-control) и полностью бесплатна для использования. > Один и тот же виджет может иметь много разных состояний (например для разных стримеров), для этого нужно просто использовать разные пары ключей. Ключей может быть сколько угодно, они не привязаны к аккаунту или каким-либо ограничениям. ## Как пользоваться 1. Откройте панель управления по адресу: [widgets.wotstat.info/remote-control](https://widgets.wotstat.info/remote-control) 2. Вставьте ссылку на ваш виджет в поле `Enter widget URL` > В качестве примера можно использовать `https://widgets.wotstat.info/personal-missions/progress-remote` ### Окно предпросмотра В ссылку автоматически допишется параметр `?remote-key=`, а на экране появится предпросмотр виджета ![preview](https://docs.wotstat.info/guide/integrations/wotstat-widgets/remote-control/assets/preview.png) - Вы можете изменить размер окна предпросмотра, перетаскивая правый нижний угол или задать точное разрешение в элементе управления под предпросмотром - Вы можете изменить масштаб отображения виджета, с помощью элемента управления под предпросмотром - Над предпросмотром отображается текущий `URL` и кнопка `Скопировать` ### Вкладка инспектора В левой половине экрана отображается панель `Inspector` ![inspector](https://docs.wotstat.info/guide/integrations/wotstat-widgets/remote-control/assets/inspector.png) Во вкладке `Remote` отображаются все параметры доступные для управления, учтите, что иногда виджеты регистрируют параметры с задержкой, например, после отображения элемента на экране. Когда вы наводите курсор на параметр, в предпросмотре подсвечивается область, которая связана с этим параметром *(если виджет такое поддерживает)*. ### Параметры отправки В правом верхнем углу находятся настройки отправки параметров ![control](https://docs.wotstat.info/guide/integrations/wotstat-widgets/remote-control/assets/control.png) - `Зелёная точка` – статус соединения с сервером `widgets-remote`, если точка зелёная, значит соединение установлено и параметры будут синхронизироваться мгновенно - `Remote Key` – ключ управления, по умолчанию генерируется случайный ключ, но вы можете ввести свой или сгенерировать случайный новый. При изменение `ключа управления`, будет автоматически изменён и `ключ доступа` в ссылке предпросмотра - `Auto Send` – если включено, то при изменении любого параметра, панель управления автоматически отправит новое значение на сервер - `Send` – отправка текущих значений всех параметров на сервер вручную Если у вас есть необходимость сделать комплексное изменение нескольких параметров сразу, то лучше отключить `Auto Send`, внести все изменения и нажать кнопку `Send` для отправки всех изменений одновременно, это сделает обновление виджета более профессиональным. Так же, например, при вводе текста, можно отключить `Auto Send`, чтобы не отправлять обновление при каждом нажатии клавиши. ## Разработка Все виджеты это обычные веб-страницы, что бы добавить поддержку удалённого управления, необходимо подключить [`wotstat-widgets-sdk`](https://www.npmjs.com/package/wotstat-widgets-sdk) и зарегистрировать параметры, которые будут доступны для управления. ### Установка **npm** ```sh $ npm add -D wotstat-widgets-sdk ``` **bun** ```sh $ bun add -D wotstat-widgets-sdk ``` **index.html** ```html ``` ### Регистрация параметров После подключения `widgets-sdk`, создайте экземпляр `WidgetsRemote` ```js import { WidgetsRemote } from 'wotstat-widgets-sdk' const remote = new WidgetsRemote() ``` Ключ доступа берётся из параметра `?remote-key=demo` в URL, или вы можете указать его явно: ```js const remote = new WidgetsRemote({ channel: 'demo' }) ``` После чего, вы можете определять параметры для управления с помощью метода `defineState(key: string, defaultValue: T, meta?: object)` Поддерживается пять типов состояний: - `number` - число - `string` - строка - `color` - цвет (формат `#RRGGBB`) - `boolean` - логическое значение - `select` - выбор из списка (варианты задаются в виде массива) Для удобства управления, ключи могут быть разделены на группы с помощью символа `/`, например: `simple/number`, `helper/query`. В этом случае, в интерфейсе удалённого управления будет создана иерархия состояний. В параметре `meta` можно указать дополнительные настройки состояния: - `type` – тип состояния - `element` – элемент к которому будет привязано состояние, может быть `HTMLElement`, функция возвращающая `HTMLElement` или `CSS` селектор. Для текстовых, числовых и булевых состояний, оно будет записываться в `element.innerText`. Для всех типов значение будет записываться в `attribute` и `CSS переменную` элемента с префиксом `remote-`. - `elementHelper` – элемент, который будет обводиться рамкой при наведении курсора на параметр в панели управления, может быть `HTMLElement`, функция возвращающая `HTMLElement` или `CSS` селектор. С помощью `defineElementHelper(key: string, element: ElementDefinition)` можно определить элемент, который будет обводиться рамкой для группы состояний. ```js import { WidgetsRemote } from 'wotstat-widgets-sdk' const remote = new WidgetsRemote() const number = remote.defineState('simple/number', 0) const string = remote.defineState('simple/string', 'default') const color = remote.defineState('simple/color', '#4c8cff', { type: 'color' }) const boolean = remote.defineState('simple/boolean', false) const select = remote.defineState('simple/select', 'foo', { type: { type: 'select', variants: ['foo', 'bar', 'baz'] } }) const selectWithLabels = remote.defineState('simple/select with labels', 'foo', { type: { type: 'select', variants: [{value: 'foo', label: 'Имя foo'}, {value: 'bar', label: 'Имя bar'}] } }) const helperQuery = remote.defineState('helper/query', 0, { element: '#bbox' }) const helperElement = remote.defineState('helper/element', 0, { element: window.bbox }) const helperGetter = remote.defineState('helper/getter', 0, { element: () => window.bbox }) remote.defineElementHelper('simple', '#simple-states') ``` ### Отслеживание изменений В большинстве случаев, достаточно просто передать в `defineState` элемент, в который будет записываться значение состояния. Но иногда требуется выполнить дополнительные действия при изменении состояния. Для этого, у каждого состояния есть метод `watch`, который позволяет подписаться на изменения значения. ```js const number = remote.defineState('simple/number', 0) // Подписка на изменение состояния const unwatch = number.watch((newValue) => { console.log('simple/number changed to', newValue) }) // Получение текущего значения состояния console.log('Current value of simple/number:', number.value) // Отписка от изменения состояния unwatch() ``` Вторым опциональным параметром `watch` можно передать `{ immediate: true }` для немедленного вызова функции с текущим значением состояния. Это полезно для инициализации виджета до первых изменений. ```js number.watch((newValue) => { console.log('Установить значение:', newValue) }, { immediate: true }) ``` ## Комплексный пример Кроме простых виджетов, с помощью удалённого управления можно создавать и более сложные сцены. Например оверлей жеребьёвки турнира. Примет такого оверлея можно посмотреть [здесь](https://widgets.wotstat.info/remote-control?widget-url=aHR0cHM6Ly9tZXJmaS1kcmF3LnNvcHJhY2hldi5jb20vP3JlbW90ZS1rZXk9MUk5S0xUSElFUg==&height=1080&width=1920&scale=0.75) ![merfi-example](https://docs.wotstat.info/guide/integrations/wotstat-widgets/remote-control/assets/merfi-example.png) Поддерживается комплексная настройка композиции, всплывающие карточки, переключения между списком корзин. Все переходы анимированы. У каждого состояния есть управляемый элемент, который подсвечивается при наведении курсора на параметр в панели управления. --- # Моделирование в Blender > Canonical URL: https://docs.wotstat.info/guide/modelling/blender/ > Markdown URL: https://docs.wotstat.info/guide/modelling/blender/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/modelling/blender/index.md [Blender3D](https://www.blender.org/) — бесплатный и открытый редактор 3D‑графики. Поддерживает весь цикл создания 3D‑контента: моделирование, риггинг, анимация, симуляция, рендеринг, композитинг и отслеживание движения, видеоредактирование и создание игр. В «Мир Танков» с помощью пользовательского плагина для Blender можно импортировать модели танков из игры для последующего редактирования и рендеринга с высоким качеством. ![screenshot](https://docs.wotstat.info/guide/modelling/blender/main-window.png) > **TIP — TODO** Дописать инструкцию как пользоваться https://bitbucket.org/SkepticalFox/bigworld-blender-tools-wot-wowp-wows/src/master/ --- # Ремоделинг > Canonical URL: https://docs.wotstat.info/guide/modelling/introduction/ > Markdown URL: https://docs.wotstat.info/guide/modelling/introduction/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/modelling/introduction/index.md В игре можно достаточно легко заменить внешний вид техники, карт, текстур. Основной подход состоит в том, чтобы распаковать файл игры, отредактировать его, запаковать обратно и поместить в архив `.mtmod` по нужному пути, чтобы ваш файл после включения в VFS перезаписал оригинальный файл игры. --- # Знакомство c Unified Editor > Canonical URL: https://docs.wotstat.info/guide/modelling/unified-editor/first-steps/ > Markdown URL: https://docs.wotstat.info/guide/modelling/unified-editor/first-steps/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/modelling/unified-editor/first-steps/index.md Unified Editor — специальный инструмент для модификации 3D‑игровых ассетов, официально распространяемый Lesta для создания модов. С его помощью можно создавать свои ангары, редактировать существующие карты и визуальные эффекты. ![hero-screen](https://docs.wotstat.info/guide/modelling/unified-editor/first-steps/assets/hero-screen.jpg) ## Официальная тема на форуме ## Установка ## Проверочный запуск > **TIP — TODO** Тут надо описать какой‑нибудь минимальный сценарий добавления чего‑нибудь в игру. Например: добавить кубик в дефолтный ангар (через загрузчики или замену файла) и запустить игру с этим модом. --- # Интерфейс программы > Canonical URL: https://docs.wotstat.info/guide/modelling/unified-editor/interface/ > Markdown URL: https://docs.wotstat.info/guide/modelling/unified-editor/interface/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/modelling/unified-editor/interface/index.md # Интерфейс программы --- # Инструкция по редактированию документации > Canonical URL: https://docs.wotstat.info/guide/other/edit-docs/ > Markdown URL: https://docs.wotstat.info/guide/other/edit-docs/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/other/edit-docs/index.md Эта документация написана с помощью [VitePress](https://vitepress.dev/), и хранится в репозитории на GitHub: [wotstat-25/mods-development-docs](https://github.com/wotstat-25/mods-development-docs) Для внесения изменений вам необходим аккаунт на GitHub, если у вас его нет, то [зарегистрируйтесь](https://github.com/join), это бесплатно и не займёт много времени. Есть два способа внести изменения в документацию: 1. Через кнопку `Редактировать страницу` внизу каждой страницы. Это быстрый и простой способ внести небольшие правки, например исправить опечатку или добавить пару строк. 2. Через `fork` репозитория и создание pull request. Этот способ более сложный, но он позволяет вносить более серьёзные изменения, например добавлять новые страницы и статьи. ## Подготовка Чтобы ваш аккаунт отображался в качестве автора изменений внизу страницы, необходимо использовать служебный email‑адрес GitHub. Для этого: 1. Перейдите в настройки вашего аккаунта на GitHub: [https://github.com/settings/emails](https://github.com/settings/emails) 2. Активируйте опцию `Keep my email address private` ## Редактирование через кнопку `Редактировать страницу` 1. Перейдите на страницу, которую хотите отредактировать. 2. Нажмите на кнопку `Редактировать страницу` внизу страницы. 3. Нажмите на кнопку `Fork this repository`, чтобы создать локальную копию репозитория. 4. После этого в открывшемся интерфейсе вы можете изменить страницу. **Details — Пример интерфейса редактирования** ![edit-page](https://docs.wotstat.info/guide/other/edit-docs/assets/web-edit-page.png) 5. После внесения изменений в верхнем правом углу нажмите на кнопку `Commit changes`. 6. В открывшемся окне введите краткое описание ваших изменений. **Details — Пример окна коммита** ![commit-changes](https://docs.wotstat.info/guide/other/edit-docs/assets/web-commit-changes.png) 7. Нажмите кнопку `Create pull request`. **Details — Пример окна создания `Pull Request`** ![create-pr](https://docs.wotstat.info/guide/other/edit-docs/assets/create-pr.png) После этого ваши изменения будут отправлены на рассмотрение, и после проверки, они будут приняты в основную документацию. ## Редактирование через `fork` и `Pull Request` Этот способ является более удобным, потому что позволит вносить изменения через удобный редактор и с возможностью проверки изменений перед отправкой. ### Подготовка Вам потребуется установить `Git`, `VSCode` и `Bun`, если у вас их нет, то: 1. [Установите Git](https://git-scm.com/downloads) — это система контроля версий, которая позволит вам работать с репозиторием документации. 2. [Установите VSCode](https://code.visualstudio.com/download) — это редактор кода, который мы будем использовать для редактирования документации. 3. [Установите Bun](https://bun.com/docs/installation) — это среда выполнения JavaScript и менеджер пакетов, необходимые для запуска VitePress локально. Нужна версия 1.4 или новее; Bun доступен для Windows, macOS и Linux. ### Создание `fork` 1. Перейдите на страницу репозитория документации: [wotstat-25/mods-development-docs](https://github.com/wotstat-25/mods-development-docs) 2. Нажмите на кнопку `Fork` в правом верхнем углу страницы. **Details — Интерфейс создания `fork`** ![fork](https://docs.wotstat.info/guide/other/edit-docs/assets/create-fork.png) 3. Выберите свой аккаунт, в который хотите создать `fork`. И нажмите на кнопку `Create fork`. 4. Вас переадресует в вашу личную копию репозитория, в котором вы можете его как угодно редактировать. ### Создайте локальную копию 1. Склонируйте ваш `fork` себе на компьютер: - На странице вашего `fork` нажмите на кнопку `Code` и скопируйте ссылку. **Details — Интерфейс клонирования репозитория** ![clone](https://docs.wotstat.info/guide/other/edit-docs/assets/clone-fork-url.png) - Откройте `VSCode`, и в меню `Source Control` нажмите на кнопку `Clone Repository`. **Details — Интерфейс клонирования репозитория** ![vsc-clone](https://docs.wotstat.info/guide/other/edit-docs/assets/vsc-clone.png) - Вставьте скопированную ссылку и нажмите `Enter`. Выберите папку, в которую хотите склонировать репозиторий. - После клонирования, `VSCode` предложит вам открыть склонированный репозиторий, нажмите `Open`. 2. Откройте встроенный терминал VSCode (`` Ctrl+` `` или `Terminal -> New Terminal`) и установите зависимости: ```shell bun install ``` 3. Запустите локальный сервер для предпросмотра документации: ```shell bun run dev ``` После запуска в терминале появится ссылка на локальный сервер (обычно [http://localhost:5173/](http://localhost:5173/)). Перейдите по этой ссылке в браузере. ### Настройка связанного с GitHub адреса email Чтобы ваш профиль отображался внизу страницы как автор изменений, необходимо настроить email‑адрес, связанный с вашим аккаунтом на GitHub. Для этого: 1. Перейдите в настройки вашего аккаунта на GitHub: [https://github.com/settings/emails](https://github.com/settings/emails) 2. Активируйте опцию `Keep my email address private` 3. Скопируйте служебный email адрес, который выглядит как `@users.noreply.github.com` 4. В `VSCode`, внутри проекта, откройте терминал (`` Ctrl+` `` или `Terminal -> New Terminal`) и выполните команду: ```shell git config user.email "" ``` Замените `` на скопированный email адрес. ### Внесение изменений Теперь вы можете вносить изменения в документацию. Все страницы находятся в папке `docs`, а файлы страниц имеют расширение `.md` (Markdown). После каждого сохранения файла, в браузере с локальным сервером предпросмотра, страница автоматически обновится и вы сможете увидеть ваши изменения. Когда вы достигнете определённого прогресса и захотите сохранить ваши изменения в репозиторий, выполните следующие шаги: 1. В `VSCode`, в меню `Source Control` вы увидите список изменённых файлов. 2. Наведите курсор на подраздел `Changes` и нажмите на кнопку `+`, чтобы отметить все изменения. 3. Введите краткое описание ваших изменений в поле ввода сверху. **Details — Интерфейс коммита изменений** ![commit](https://docs.wotstat.info/guide/other/edit-docs/assets/commit.png) 4. Нажмите на кнопку `Commit`, чтобы зафиксировать изменения. 5. Нажмите на кнопку `Sync Changes`, чтобы отправить ваши изменения в GitHub. ### Создание `Pull Request` 1. Перейдите на страницу вашего `fork` на GitHub. 2. Нажмите на кнопку `Contribute` и выберите `Open pull request`. **Details — Интерфейс создания `Pull Request`** ![create-pr](https://docs.wotstat.info/guide/other/edit-docs/assets/open-pr.png) 3. В интерфейсе создания `Pull Request` введите заголовок и описание ваших изменений. **Details — Интерфейс описания `Pull Request`** ![pr-details](https://docs.wotstat.info/guide/other/edit-docs/assets/pr-details.png) 4. Если вы уже готовы, нажмите на кнопку `Create pull request`, чтобы отправить ваши изменения на рассмотрение. - Если вы хотите продолжить вносить изменения, но уже готовы обсудить текущие, выберите `Create draft pull request`, чтобы создать черновик `Pull Request`. **Details — Смена статуса на черновик** ![pr-draft](https://docs.wotstat.info/guide/other/edit-docs/assets/draft-pr.png) После открытия `Pull Request`, ваши изменения будут рассмотрены, прокомментированы и в случае одобрения, приняты в основную документацию. При открытом `Pull Request` вы можете продолжать вносить изменения в ваш `fork`, и они автоматически будут добавлены в открытый `Pull Request`. Не забывайте делать `Commit` и `Push` ваших изменений. ## Assets locality В этой документации принято хранить все ассеты, связанные с конкретной страницей, максимально близко к этой странице. Обычно это папка `assets`, расположенная рядом с файлом страницы. ``` .../edit-docs/ ├─ index.md └─ assets/ ├── image.png └── code.py ``` ## Как работать с Markdown Документация написана с помощью [VitePress](https://vitepress.dev/), который использует расширенный синтаксис Markdown, все возможности которого описаны в [официальной документации](https://vitepress.dev/guide/markdown). Из основных возможностей, которые могут пригодиться: ### Заголовки ```md # Заголовок 1 уровня ## Заголовок 2 уровня ### Заголовок 3 уровня ``` ### Якоря для заголовков ```md ## Заголовок 2 уровня {#header-2} ``` ### Параграфы ```md Это первый параграф. Это второй параграф. ``` ### Выделение текста ```md **Жирный текст** *Курсивный текст* ~~Зачёркнутый текст~~ ``` **Жирный текст** *Курсивный текст* ~~Зачёркнутый текст~~ ### Ссылки ```md [Текст ссылки](https://example.com) ``` [Текст ссылки](https://example.com/) ### Изображения ```md ![Название картинки](./assets/image.png) ``` Изображение должно находиться в папке `assets`, которая лежит рядом с файлом страницы. #### Размер изображения ```md ![Название картинки](./assets/image.png){width=400} ``` ### Блоки кода ````md ```python print("Hello, World!") ``` ```` ```python print("Hello, World!") ``` ### Вставки `кода` ```md Вставки `кода` ``` ### Блоки с подсветкой ```md ::: tip СОВЕТ Это блок с подсветкой для советов. ::: ::: warning ВНИМАНИЕ Это блок с подсветкой для предупреждений. ::: ::: details ПОДРОБНЕЕ Это блок с возможностью сворачивания. ::: ``` > **TIP — СОВЕТ** Это блок с подсветкой для советов. > **WARNING — ВНИМАНИЕ** Это блок с подсветкой для предупреждений. **Details — ПОДРОБНЕЕ** Это блок с возможностью сворачивания. ### Списки ```md - Пункт списка 1 - Пункт списка 2 - Вложенный пункт списка ``` - Пункт списка 1 - Пункт списка 2 - Вложенный пункт списка ### Нумерованные списки ```md 1. Первый пункт 2. Второй пункт ``` 1. Первый пункт 2. Второй пункт ### Таблицы ```md | Заголовок 1 | Заголовок 2 | | ----------- | ----------- | | Ячейка 1 | Ячейка 2 | ``` | Заголовок 1 | Заголовок 2 | | ----------- | ----------- | | Ячейка 1 | Ячейка 2 | --- # FFDec – декомпилятор Flash > Canonical URL: https://docs.wotstat.info/guide/programs/ffdec/ > Markdown URL: https://docs.wotstat.info/guide/programs/ffdec/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/programs/ffdec/index.md Программа позволяет просматривать и извлекать ресурсы из SWF-файлов, а также декомпилировать ActionScript 3.0 в читаемый текстовый формат. Удобно для изучения других модов. ## Использование [Скачайте](https://github.com/jindrapetrik/jpexs-decompiler/releases/latest) актуальную версию FFDec для вашей операционной системы. Запустите программу и претащите в окно SWF-файл мода, который хотите изучить. ![Главное окно FFDec](https://docs.wotstat.info/guide/programs/ffdec/assets/ffdec-main-window.png) В левой части окна отображается структура SWF-файла. Вы можете раскрывать папки, чтобы просматривать содержащиеся в них ресурсы. В центральной части окна отображается содержимое выбранного ресурса. --- # Теория AS3 модов > Canonical URL: https://docs.wotstat.info/guide/scripting/as3-theory/ > Markdown URL: https://docs.wotstat.info/guide/scripting/as3-theory/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/scripting/as3-theory/index.md Пользовательский интерфейс игры состоит из совокупности окон (`Windows`), которыми управляет движок `wulf`. Сами окна могут быть реализованы на: - `GF` – Coherent GameFace, это `HTML` + `JavaScript` + `CSS` - `Unbound` – собственный фреймворк Лесты - `Scaleform` – это `Flash`, в котором используется язык программирования `ActionScript 3` (AS3) Наиболее распространённым способом создания графических модов является использование `Scaleform`. ## Scaleform `Scaleform` — это технология, которая позволяет запускать `Flash`‑приложения внутри игры, аналогично тому, как они раньше запускались в браузере с помощью плагина `Adobe Flash Player`. `Flash`‑приложения пишутся на языке `ActionScript 3` (AS3) и компилируются в файл с расширением `.swf`, который содержит скрипты, картинки, анимации и другие ресурсы, необходимые для работы приложения. Игра умеет работать с такими файлами и отображать их в интерфейсе. У `SWF`‑файлов всегда есть точка входа — главный класс, экземпляр которого создаётся при загрузке. ## DAAPI Для взаимодействия `AS3`‑кода с `Python`‑скриптами используется механизм `DAAPI` (Direct Access API). Он позволяет связать `AS3`‑класс с `Python`‑классом, чтобы они могли обмениваться данными, вызывая методы друг друга. Связь образуется между указанным `Python`‑классом и основным классом `AS3`, указанным в точке входа `SWF`‑файла. ### Пример классов **HelloWorldWindow.as** ```actionscript-3 public class HelloWorldWindow extends AbstractWindowView { public var py_DemoFunction:Function; public function as_DemoFunction(message:String):void { trace("Message from Python: " + message); } public function sendMessageToPython(message:String):void { py_DemoFunction(message); } } ``` **HelloWorldWindow.py** ```python class HelloWorldWindow(AbstractWindowView): def py_DemoFunction(self, message): print("Message from AS3: {}".format(message)) def as_DemoFunction(self, message): self.flashObject.as_DemoFunction(message) ``` Префиксы `as_` и `py_` не являются обязательными, но они помогают отличать методы, которые вызываются из другого языка. Если создать связку между этими двумя классами, то можно будет в `Python`‑коде обратиться к `self.flashObject` и вызывать на нём метод, объявленный в `AS3`. И наоборот, в `AS3`‑коде можно объявить переменную типа `Function`, которая будет ассоциирована с методом из `Python`‑класса. ### Создание связки Чтобы связать `AS3`‑класс с `Python`‑классом, необходимо в `g_entitiesFactories` зарегистрировать `ViewSettings`, указав класс `Python` и путь к `SWF`‑файлу. ```python from frameworks.wulf.gui_constants import WindowLayer from gui.Scaleform.framework import g_entitiesFactories, ScopeTemplates, ViewSettings viewSettings = ViewSettings( 'MY_MOD_HELLO_WORLD_WINDOW', # уникальный ID окна HelloWorldWindow, # управляющий класс Python "HelloWorldWindow.swf", # путь к SWF файлу WindowLayer.TOP_WINDOW, None, ScopeTemplates.VIEW_SCOPE, ) g_entitiesFactories.addSettings(viewSettings) ``` ### Добавление окна в интерфейс После регистрации `SWF`‑файла его можно открыть в интерфейсе, вызвав метод `loadView` на текущем Scaleform‑приложении. В качестве аргумента передаётся `ViewLoadParams`, в котором указывается уникальный ID окна, указанный при регистрации. ```python from helpers import dependency from skeletons.gui.app_loader import IAppLoader from gui.Scaleform.framework.managers.loaders import SFViewLoadParams appLoader = dependency.instance(IAppLoader) app = appLoader.getApp() app.loadView(SFViewLoadParams('MY_MOD_HELLO_WORLD_WINDOW')) ``` ## Тестирование SWF без перезапуска игры Во время разработки `SWF` не обязательно каждый раз упаковывать в `.mtmod` и перезапускать игру. После сборки копируйте файл в `res_mods`, сохраняя его путь относительно каталога ресурсов (`gui\flash\...`): ```text \res_mods\\gui\flash\reload-exmaple\HelloWorldWindow.swf ``` На время разработки зарегистрируйте окно с путём к этому файлу относительно каталога `gui/flash`: ```python DEV_SWF = 'reload-exmaple/HelloWorldWindow.swf' viewSettings = ViewSettings( 'MY_MOD_HELLO_WORLD_WINDOW', HelloWorldWindow, DEV_SWF, WindowLayer.TOP_WINDOW, None, ScopeTemplates.VIEW_SCOPE, ) g_entitiesFactories.addSettings(viewSettings) ``` Файл не обязательно размещать в корне `gui/flash`: каталог `reload-exmaple` в примере нужен, чтобы получить отдельный виртуальный путь для разработки. Вы можете выбрать другое имя, главное — чтобы итоговый путь не совпадал с существующими ресурсами игры и с путём `SWF` внутри вашего `.mtmod`. Регистрировать окно заново после каждой сборки не нужно. Чтобы загрузить обновлённый `SWF`, уничтожьте старое окно, дождитесь следующего кадра и только затем создайте его снова: ```python import BigWorld from gui.Scaleform.framework.managers.loaders import SFViewLoadParams from helpers import dependency from skeletons.gui.app_loader import IAppLoader VIEW_ALIAS = 'MY_MOD_HELLO_WORLD_WINDOW' def loadMyView(): app = dependency.instance(IAppLoader).getApp() app.loadView(SFViewLoadParams(VIEW_ALIAS)) def reload(): app = dependency.instance(IAppLoader).getApp() app.containerManager.destroyViews(VIEW_ALIAS) BigWorld.callback(0, loadMyView) ``` После замены файла вызывайте `reload()` через [WotREPL](https://docs.wotstat.info/guide/first-steps/pjorion/index.md) или по кнопке из самого мода. Важно уничтожить все открытые окна, использующие этот `SWF`: пока хотя бы одно из них существует, Scaleform может продолжить использовать предыдущую версию файла. Вызов `loadMyView()` в том же кадре также загрузит старую версию, поэтому `BigWorld.callback(0, ...)` здесь обязателен. --- # Теория Gameface модов > Canonical URL: https://docs.wotstat.info/guide/scripting/gameface-theory/ > Markdown URL: https://docs.wotstat.info/guide/scripting/gameface-theory/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/scripting/gameface-theory/index.md Пользовательский интерфейс игры состоит из совокупности окон (`Windows`), которыми управляет движок `wulf`. Сами окна могут быть реализованы на: - `GF` – Coherent GameFace, это `HTML` + `JavaScript` + `CSS` - `Unbound` – собственный фреймворк Лесты - `Scaleform` – это `Flash`, в котором используется язык программирования `ActionScript 3` (AS3) ## Coherent Gameface `Coherent Gameface` (GF) — это технология, которая позволяет создавать интерфейсные окна с использованием веб-технологий: `HTML`, `CSS` и `JavaScript`. Такие окна отображаются внутри игры. Движок для отображения этих окон похож на браузер, однако оптимизирован для работы в игровом окружении. Для выполнения `JavaScript`-кода в окнах GF используется движок `V8`, который также применяется в браузере `Google Chrome`. Ознакомиться с документацией по `Coherent Gameface` можно на официальном сайте: [https://docs.coherent-labs.com/cpp-gameface/](https://docs.coherent-labs.com/cpp-gameface/), учтите, что игра использует не самую свежую версию `GF`, поэтому некоторые функции могут быть недоступны. Важными отличиями `GF` от обычного браузера являются: - Задержка между установкой `CSS` стилей и их применением к элементам страницы (2 кадра) - Работа с анимациями `CSS` - `GF` предоставляет оптимизированную структуру для выполнения анимаций, прямое манипулирование стилями ухудшает производительность - Масштабирование реализовано путём установки `font-size: 2px` на корневой элемент страницы, поэтому все размеры на странице должны быть заданы в `em` или `rem` единицах - В качестве лейаута страницы по умолчанию используется `flexbox` - Доступны фильтры для взаимодействия с фоном игры, например блюр ## Окна GF в игре Как браузер состоит из разных вкладок, внутри каждой из которых "живёт" своя веб-страница, так и в игре разные компоненты интерфейса реализованы в виде отдельных окон `GF`, каждое из которых загружает свою страницу. Отличием от браузера является то, что на одном экране может находиться сразу несколько окон `GF`. Например ангар на `Lesta` состоит из смеси `GF`-окон и `Scaleform`-окон. ## Обмен данными между Python и JavaScript Для взаимодействия `JavaScript`-кода страницы с `Python`-скриптами игры использутся реактивная ViewModel система. Окно определяется классом `View(ViewImpl)`, который инициализирует модель данных `Model(ViewModel)`, в которой определяются `значения (properties)` и `команды (commands)`. - Команды (`JS -> Python`) — это события, которые могут быть вызваны из `JavaScript`-кода страницы и обработаны в `Python`-скриптах. - Значения (`Python -> JS`) — это реактивные данные, которые могут быть изменены из `Python`-скриптов и прочитаны в `JavaScript`-коде страницы. Когда значение изменяется в `Python`, об этом автоматически уведомляется `JavaScript`-код страницы. **example_view.py** ```python from frameworks.wulf import ViewModel from gui.impl.pub import ViewImpl class ExampleModel(ViewModel): def __init__(self, properties=3, commands=1): super(ExampleModel, self).__init__(properties=properties, commands=commands) def _initialize(self): super(ExampleModel, self)._initialize() self._addStringProperty('exampleString', '') self._addNumberProperty('exampleNumber', 0) self._addBoolProperty('exampleBool', False) self.exampleCommand = self._addCommand('exampleCommand') class ExampleView(ViewImpl): viewLayoutID = ModDynAccessor('RES_JSON_KEY') def __init__(self, server, pageName='', pageId=''): settings = ViewSettings(ExampleView.viewLayoutID(), flags=ViewFlags.VIEW, model=CDPModel()) super(ExampleView, self).__init__(settings) @property def viewModel(self): return super(ExampleView, self).getViewModel() ``` ### Получение значений в JavaScript Значения из модели данных доступны в `JavaScript`-коде страницы через глобальную переменную `window.model`. Модель реактивная, за изменениями можно следить подписавшись на событие `viewEnv.onDataChanged`. **example.js** ```javascript function onModelChanged() { console.warn(`exampleString changed to: ${window.model.exampleString}, exampleNumber changed to: ${window.model.exampleNumber}, exampleBool changed to: ${window.model.exampleBool}`) } engine.whenReady.then(() => { engine.on('viewEnv.onDataChanged', onModelChanged) }) ``` Если у вас есть вложенные модели, то необходимо явно указать, что надо следить за изменениями в этих моделях, для этого используется метод `addDataChangedCallback`: ```javascript engine.whenReady.then(() => { viewEnv.addDataChangedCallback('model.child', 0, true) }) ``` После чего, изменение в `child` модели также будет вызывать событие `viewEnv.onDataChanged`. ### Вызов команд из JavaScript Команды из модели данных вызываются в `JavaScript`-коде страницы как методы на объекте `window.model`. Аргументы команды передаются в виде словаря. **example.js** ```javascript function sendCommandToPython() { window.model.exampleCommand({ arg1: 'value1', arg2: 42 }); } ``` В `Python`-коде команды обрабатываются посредством подписки на событие команды: **example_model.py** ```python class ExampleModel(ViewModel): ... def _initialize(self): ... self.exampleCommand += self._onExampleCommand def _onExampleCommand(self, args): print("exampleCommand called with args: {}".format(args)) ``` ## Ресурсы игры (res_map.json) Для использования в Python-скриптах различных ресурсов известных на этапе компиляции игры, Мир Танков использует механизм `DynAccessor` и файл `res_map.json`, на который эти `DynAccessor` ссылаются. **res_map.json** ```json { ... "f4": { "type": "Layout", "path": "coui://gui/gameface/_dist/production/lobby/crew/CrewHeaderTooltipView/CrewHeaderTooltipView.html", "parameters": { "entrance": "CrewHeaderTooltipView", "extension": "", "impl": "gameface" } }, "10db6": { "type": "Image", "path": "img://gui/maps/icons/crewWidget/buttonsBar/background.png", "parameters": { "extension": "", } }, ... } ``` Для взаимодействия с GF-окнами необходимо зарегистрировать ресурсы в `res_map.json`. ## OpenWG.Gameface [`OpenWG.Gameface`](https://gitlab.com/openwg/wot.gameface) — это специальный мод, который позволяет упростить регистрацию своих ресурсов в `res_map.json` и предоставляет дополнительные возможности для работы с окнами `GF`. Процесс регистрации состоит из перезаписи (путём помещения в папку `res_mods`) файла `res_map.json`, в котором дописываются новые ресурсы и последующим автоматическим перезапуском игры, который нужен только если файл был изменён (например установлен новый мод). ### Пример регистрации ресурсов В пакете мода необходимо создать файл `mods/configs/res_map/*.json` в котором описать используемые ресурсы. **mods/configs/res_map/my_mod.json** ```json [ { "itemID": "mods/testDialog/title", "type": "String", "parameters": { "key": "Dialog window made with Unbound (WULF)", "textdomain": "dialogs", "extension": "" } }, { "itemID": "mods/testTooltip/layoutID", "type": "Layout", "path": "coui://gui/gameface/mods/testTooltip/TestTooltip.html", "parameters": { "extension": "", "entrance": "TestTooltip", "impl": "gameface" } } ] ``` После чего, в `Python`-коде можно получить доступ к этим ресурсам через `ModDynAccessor`: **my_mod.py** ```python from openwg_gameface import ModDynAccessor title = ModDynAccessor('mods/testDialog/title') ``` Или из `JavaScript`-кода окна `GF`: **my_mod.js** ```javascript const title = R.dialogs.title; ``` > TODO: Проверить такой ли путь ## Пример добавления своего окна ### Регистрация в res_map.json Для регистрации в `res_map` необходим мод [`OpenWG.Gameface`](https://gitlab.com/openwg/wot.gameface). **mods/configs/res_map/example_mod.json** ```json [ { "itemID": "EXAMPLE_MOD_WINDOW", "type": "Layout", "path": "coui://gui/gameface/mods/example_mod/index.html", "parameters": { "extension": "", "entrance": "ExampleMod", "impl": "gameface" } } ] ``` В качестве `itemID` указываем уникальный идентификатор окна, который будет использоваться в `Python`-коде для его создания. В `path` указываем путь к `HTML`-файлу окна (внутри вашего мода). В качестве `type` указываем `Layout`. ### Код окна Создадим простое `Hello World` окно `GF`. **mods/gui/gameface/mods/example_mod/index.html** ```html

Hello, World!

``` Для управления окном используется `JavaScript`-код, который можно разместить в файле `index.js`. А для стилизации окна используется файл `index.css`. ### Отображение окна в игре Для создания окна необходимо создать класс окна `WindowImpl`, класс представления `ViewImpl` и модель данных `ViewModel`. #### Модель данных Используется для определения значений, которые будут переданы в `JavaScript`-код окна, а также команд, которые могут быть вызваны из `JavaScript`. ```python from gui.impl.pub import ViewModel class ExampleViewModel(ViewModel): def __init__(self, properties=1, commands=1): # type: (int, int) -> None super(ExampleViewModel, self).__init__(properties=properties, commands=commands) def _initialize(self): # type: () -> None super(ExampleViewModel, self)._initialize() self._addStringProperty('exampleString', 'Hello, World!') self._exampleCommand = self._addCommand('exampleCommand') self._exampleCommand += self._onExampleCommand def _onExampleCommand(self, args): # type: (dict) -> None print("Example command executed with args: {}".format(args)) @property def exampleString(self): # type: () -> str return self._getString(0) @exampleString.setter def exampleString(self, value): # type: (str) -> None self._setString(0, value) ``` #### Представление окна Используется для инициализации модели данных и управления логикой окна. ```python from gui.impl.pub import ViewImpl from frameworks.wulf import ViewSettings, ViewFlags from openwg_gameface import ModDynAccessor RES_MAP_ITEM_ID = 'EXAMPLE_MOD_WINDOW' class ExampleView(ViewImpl): viewLayoutID = ModDynAccessor(RES_MAP_ITEM_ID) def __init__(self): settings = ViewSettings(ExampleView.viewLayoutID(), flags=ViewFlags.VIEW, model=ExampleViewModel()) super(ExampleView, self).__init__(settings) @property def viewModel(self): # type: () -> ExampleViewModel return super(ExampleView, self).getViewModel() ``` #### Класс окна Используется для создания экземпляра окна и его отображения на экране. ```python from gui.impl.pub import WindowImpl from frameworks.wulf import WindowFlags, ViewSettings, ViewFlags class ExampleWindow(WindowImpl): def __init__(self): super(ExampleWindow, self).__init__(wndFlags=WindowFlags.WINDOW, content=ExampleView()) ``` ### Создание и показ окна ```python from skeletons.gui.impl import IGuiLoader from helpers import dependency from openwg_gameface import res_id_by_key RES_MAP_ITEM_ID = 'EXAMPLE_MOD_WINDOW' uiLoader = dependency.instance(IGuiLoader) # type: IGuiLoader view = uiLoader.windowsManager.getViewByLayoutID(res_id_by_key(RES_MAP_ITEM_ID)) ExampleWindow().load() ``` ## Пример модификации существующего окна Для модификации существующего окна `GF` необходимо с помощью мода `OpenWG.Gameface` инжектировать свой `JavaScript`-код в страницу окна. Для этого можно добавить своё `ViewImpl` в качестве дочернего к существующему окну. При этом новая `HTML`-страница не будет создана, а подключить свой `JavaScript`-код можно будет через `gf_mod_inject`. ```python from openwg_gameface import ModDynAccessor, gf_mod_inject from gui.impl.pub import ViewImpl, ViewSettings, ViewFlags from gui.impl.lobby.crew.hangar_crew_widget import HangarCrewWidget RES_MAP_ITEM_ID = 'EXAMPLE_MOD_SUBVIEW' class MyModel(ViewModel): def _initialize(self): ... gf_mod_inject(self, RES_MAP_ITEM_ID, styles=['coui://gui/gameface/mods/example/index.css'], modules=['coui://gui/gameface/mods/example/index.js'] ) class MyView(ViewImpl): viewLayoutID = ModDynAccessor(RES_MAP_ITEM_ID) def __init__(self, server, pageName='', pageId=''): settings = ViewSettings(MyView.viewLayoutID(), flags=ViewFlags.VIEW, model=MyView()) super(MyView, self).__init__(settings) orig_load = HangarCrewWidget._onLoading def new_load(self, *args, **kwargs): orig_load(self, *args, **kwargs) self.setChildView(MyView.viewLayoutID(), MyView()) HangarCrewWidget._onLoading = new_load ``` В `res_map.json` необходимо зарегистрировать ваш ресурс, однако в качестве `HTML` файла можно оставить пустую заглушку **mods/configs/res_map/example_mod.json** ```json [ { "itemID": "EXAMPLE_MOD_SUBVIEW", "type": "Layout", "path": "coui://gui/gameface/mods/example/index.html", "parameters": { "extension": "", "entrance": "example", "impl": "gameface" } } ] ``` ## Управление окном из JavaScript Для работы подсказок кода вы можете установить библиотеку с типами [`wot-gameface-types`](https://www.npmjs.com/package/wot-gameface-types). После чего подключите референсы типов в ваш `JavaScript`-код: **index.js** ```javascript /// ``` После чего у вас появится автодополнение для всех доступных в `Gameface` API. ![gameface-types-autocomplete.png](https://docs.wotstat.info/guide/scripting/gameface-theory/assets/gameface-types-autocomplete.png) ## JavaScript API Тут описаны некоторые из доступных API. ### `engine.whenReady` Большинство API доступны только после инициализации движка `engine`, которая происходит после загрузки страницы. Для того чтобы дождаться инициализации, используйте promise `engine.whenReady`: ```javascript engine.whenReady.then(() => { console.warn("Engine is ready!"); }) ``` > `console.warn` используется вместо `console.log`, так как warning-сообщения видны в логах игры, а обычные сообщения нет. ### `engine.on` На некоторые события можно подписаться с помощью метода `on`: ```javascript engine.on('clientResized', () => console.warn("Client resized")) engine.on('self.onScaleUpdated', () => console.warn("Scale updated")) ``` ### `viewEnv` Изменить размер окна можно с помощью метода `resizeViewPx` на объекте `viewEnv`: ```javascript const { height, width } = viewEnv.getClientSizePx() viewEnv.resizeViewPx(width, height) ``` Установить интерактивную область окна можно с помощью метода `setHitAreaPaddingsRem` на объекте `viewEnv`: ```javascript const remTop = viewEnv.pxToRem(top) const remBottom = viewEnv.pxToRem(bottom) const remLeft = viewEnv.pxToRem(left) const remRight = viewEnv.pxToRem(right) viewEnv.setHitAreaPaddingsRem(remTop, remRight, remBottom, remLeft, 15) ``` --- # Исходный код игры > Canonical URL: https://docs.wotstat.info/guide/scripting/sources/ > Markdown URL: https://docs.wotstat.info/guide/scripting/sources/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/scripting/sources/index.md При написании модификаций полезно иметь исходный код игры, которую вы модифицируете. Вы можете самостоятельно декомпилировать исходный код клиента игры или воспользоваться уже готовым результатом декомпиляции. ## Ручная декомпиляция > **TIP — TODO** Написать инструкцию по декомпиляции ## Готовый репозиторий с исходным кодом Исходный код клиента доступен в репозитории [github.com/izeberg/wot-src](https://github.com/izeberg/wot-src), который автоматически обновляется при выходе новых версий игры. В репозитории есть исходный код как **Мир Танков**, так и **World Of Tanks** с каждого региона. Сменить регион можно переключением ветки. ## Готовый репозиторий с картинками `gui.pkg` Ресурсы пользовательского интерфейса из архива `gui.pkg` можно найти в репозитории [github.com/Kurzdor/wot.assets](https://github.com/Kurzdor/wot.assets). По веткам можно переключаться между `WG` и `Lesta`. --- # Виджеты для стримов > Canonical URL: https://docs.wotstat.info/guide/widgets/stream/ > Markdown URL: https://docs.wotstat.info/guide/widgets/stream/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/guide/widgets/stream/index.md Виджеты на стримах являются обычными веб-сайтами с прозрачным фоном, разработка таких виджетов практически не отличается от разработки обычных веб-сайтов. ## Добавление виджетов на стрим Добавляются такие виджеты в [OBS](https://obsproject.com/) через функцию **Браузерный источник** (Browser Source). **Details — Добавление браузерного источника в OBS** ![add](https://docs.wotstat.info/guide/widgets/stream/assets/add.png) После чего, необходимо указать URL виджета, а так же размеры страницы в пикселях. **Details — Настройки браузерного источника** ![add-setup](https://docs.wotstat.info/guide/widgets/stream/assets/add-setup.png) > **TIP — Рекомендация** Старайтесь использовать такое разрешение страницы, которое вам потом не придётся масштабировать в OBS, любое масштабирование будет приводить к потере качества изображения. - Если указать разрешение **ниже** целевого и **увеличить** размер, то изображение будет размытым. - Если указать разрешение **выше** целевого и **уменьшить** размер, то появятся артефакты на границах элементов. (пропадёт сглаживание) **Details — Пример потери качества при масштабировании** ![compare](https://docs.wotstat.info/guide/widgets/stream/assets/compare-resolution.png) В методе смешивания включите `SRGB off`, это связано с некорректным отображением полупрозрачных цветов в OBS при включённом `SRGB`, отслеживать баг можно в [issue#469](https://github.com/obsproject/obs-browser/issues/469). **Details — Изменение метода смешивания** ![blending-method](https://docs.wotstat.info/guide/widgets/stream/assets/blending-method.png) ## Разработка виджетов По умолчанию, OBS автоматически применяет стиль к `body`, который задаёт прозрачный фон, делает нулевые отступы и скрывает появляющиеся полосы прокрутки. Однако, рекумендуется явно указать эти стили в вашем CSS, чтобы избежать возможных проблем с несовместимостью версий OBS и плагина браузерного источника. ```css body { background-color: rgba(0, 0, 0, 0); margin: 0px auto; overflow: hidden; } ``` Хорошей практикой будет автоматическое масштабирование виджета под ширину окна браузерного источника, для этого можно использовать следующий CSS: ```css body { font-size: 1vw; /* Устанавливаем размер шрифта в зависимости от ширины окна */ } ``` После чего, все размеры на странице должны быть заданы в `em` или `rem` единицах, чтобы они масштабировались вместе с размером шрифта, а значит и размером страницы. ```css .container { width: 30em; /* Ширина контейнера будет зависеть от ширины окна */ height: 10em; /* Высота контейнера будет зависеть от ширины окна */ } ``` Подробнее о разработке именно танковых виджетов можно прочитать в разделе [Интеграции -> WotStat виджеты](https://docs.wotstat.info/guide/integrations/wotstat-widgets/introduction/index.md). --- # Разработка модов для игры «Мир танков» > Canonical URL: https://docs.wotstat.info/ > Markdown URL: https://docs.wotstat.info/index.md > Source: https://github.com/wotstat/mods-development-docs/blob/main/docs/ru/index.md ## Мотивация Мир Танков – это игра, которая крайне хорошо подходит для создания модов. Однако, порог входа в разработку достаточно высок, официальной документации нет, а пользовательские руководства неполные и зачастую устаревшие. Этот сайт призван помочь как новичкам, так и опытным программистам быстро и эффективно настроить всё нужное для разработки модов, а также найти ответы на возникающие вопросы. В первых шагах вы **с нуля** настроите окружение с подсветкой синтаксиса, подсказками кода и автоматической компиляцией, а затем по шагам повторите разработку двух популярных модов: `быстрый демонтаж оборудования` и `калькулятор бронепробития`.