Перейти к содержимому

Первый реальный мод

В этом руководстве пройдём все этапы создания реального Python‑мода. В качестве примера будет повторён мод Быстрый демонтаж оборудования 2.0. Этот мод позволяет быстро демонтировать оборудование с танка, находясь в меню установки оборудования на другой танк.

Разберём не только необходимые для разработки шаги, но и принцип, как до них догадаться.

hero

Мод является крайне полезным и не очень сложным в разработке, что делает его отличным примером для изучения.

Философия

Основной подход к разработке модов заключается в подмене существующих методов на свои реализации.

Благодаря языку 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, который крайне тяжело модифицируется.

Как понять на чём сделан интерфейс

Отличить 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

Поэтому наиболее простой способ отображения интерфейса выбора танка для демонтажа — модифицировать существующее контекстное меню.

Как найти контекстное меню

Контекстное меню в игре вызывается через Python и обычно реализуется контролирующим классом, наследующимся от AbstractContextMenuHandler.

Попробуем найти в исходном коде игры контекстное меню для оборудования в интерфейсе выбора. Скорее всего, в названии контролирующего класса есть слово ContextMenu, и можно предположить, что там будет Equipment, так как это меню оборудования.

Воспользуемся поиском в VSCode по регулярному выражению class .*Equipment.*ContextMenu, здесь .* означает любое число произвольных символов.

Результат поиска по регулярному выражению

search-result

Совет

Учтите, что поиск по регулярным выражениям нужно включить кнопкой .* рядом с полем ввода.

Если используете Git и у вас есть .gitignore, то по умолчанию поиск не проходит по игнорируемым файлам. Чтобы искать везде, нажмите ... рядом с полем ввода в files to exclude и отключите Use Exclude Settings and Ignore Files.

Первый результат поиска — класс BaseEquipmentItemContextMenu по пути .../tank_setup/context_menu/base_equipment.py, где папка tank_setup подтверждает, что это именно нужное нам меню в интерфейсе настройки танка.

Префикс Base в названии говорит, что это базовый класс. Посмотрим, кто его наследует. Введём в поиск class .*\(BaseEquipmentItemContextMenu.

Результат поиска наследников BaseEquipmentItemContextMenu

search-child-cm

Нашлось три класса:

  • BattleAbilityItemContextMenu – что‑то связанное с боевыми умениями
  • ConsumableItemContextMenu – что‑то связанное с расходниками
  • OptDeviceItemContextMenu – то, что нам нужно

Как модифицировать контекстное меню

Класс OptDeviceItemContextMenu реализует меню декларативно (подробнее — как создать контекстное меню) через наследование от 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:', <unbound method OptDeviceItemContextMenu._generateOptions>)

Важно

После выполнения кода обязательно сотрите или закомментируйте сохранение, чтобы случайно не перезаписать 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

Определение действий по нажатию на пункты меню

Первый аргумент 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

Получение списка танков с оборудованием

Теперь нужно научиться получать список танков с установленным оборудованием.

Для начала нужно понять, на каком оборудовании вызвано контекстное меню (чтоб понять что именно надо демонтировать). Скорее всего, эта информация уже есть в 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

Среди атрибутов есть метод _getItem. Выведем его в консоль print(last_OptDeviceItemContextMenu._getItem()) и получим OptionalDevice:

OptionalDevice<intCD:23801, type:optionalDevice, nation:15>

Дальнейшее проставление символа . после 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()) 
...
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()))

Совет

Как работать с dependency и IItemsCache можно почитать в статье Как работать с Dependency Injections

В результате в консоли появится множество (set) танков с установленным оборудованием.

set([
    Vehicle<id:548, intCD:62241, nation:2, lock:(0, 0)>,
    ...
    Vehicle<id:516, intCD:6977, nation:4, lock:(0, 0)>
])

Преобразуем его в массив (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

Вызов демонтажа оборудования

Остался последний шаг, нужно научиться программно демонтировать оборудование с танка, который сейчас не выбран.

В игре демонтаж происходит через GF‑кнопку. Найти, что она вызывает, непросто. Можно попробовать поиск по demount, однако он даёт много лишнего.

Поиск демонтажа по контекстному меню

Можно вспомнить, что демонтаж доступен из контекстного меню в ангаре: demount-from-hangar

Можно искать другие ...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)
  ...

Совет

Как работать с adisp_process можно почитать в статье Асинхронное программирование

Как видно из кода, нужно вызвать действие 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)

Совет

Подробнее исследовать что есть в 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) # подставьте свои значения
Результат

demount-frozen-screen

Получили зависший экран с размытием. Такое бывает. Поможет ESC -> Сменить сервер (что приведёт к перезагрузке интерфейса ангара). Иногда может потребоваться полная перезагрузка игры.

Если код верный, но не работает, проблема может быть в способе запуска. Возможно, в PjOrion код выполняется не в главном потоке, из-за чего интерфейс не может инициализироваться.

Можно применить трюк с отложенным вызовом. С помощью метода движка BigWorld.callback(time, callback), который откладывает выполнение функции callback на указанное время. Спустя это время функция будет вызвана в основном потоке от имени движка. Отложим на 0 секунд, чтобы вызвать в следующий кадр.

PjOrion
python
BigWorld.callback(0, lambda: demount(4737, 25593, 1))
Результат

demount-succes-screen

Теперь всё работает, даже если выбрать другой танк.

Получение индекса слота

Остаётся автоматически определять 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()
Откуда взялся 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 окружения. Весь функционал разобьём на разные файлы, чтобы было проще ориентироваться в коде.

Логика демонтажа

В отдельный файл 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

Улучшения

В результате получился рабочий мод, позволяющий демонтировать оборудование с других танков прямо из меню установки. Параллельно вы научились работать с исходным кодом игры и поняли процесс разработки.

Мод можно улучшить, например:

  • Добавить опцию докупки оборудования как в оригинальном моде (искать по ActionsFactory.BUY_MODULE).
  • Если список танков длинный, то его можно разбить на подменю по уровню техники.
  • Добавить настройку мода, которая позволит автоматически демонтировать оборудование без показа диалогового окна (ActionsFactory.doAction(ActionsFactory.REMOVE_OPT_DEVICE, vehicle, item, slotId, skipConfirm = True).

Авторы

История изменений