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

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

В этом руководстве пройдём все этапы создания реального 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).

Авторы

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