Добавлен , опубликован

Занимательные баги

Содержание:

Обнаружение

Как-то играя в версию 3.-1.14, я попытался улучшить Wall Tower, но у меня не хватало золота. Через некоторое время я всё же смог улучшить эту башню до Arcane Tower, но что-то было не так. Почему-то у этой башни способность и все характеристики были от Lightning Tower. Недолго думая, я продал башню, получив половину стоимости Lightning Tower, и построил с нуля Arcane, убедившись, что способность и характеристики действительно "те". После я вовсе стал думать, что мне показалось, но просмотр записи игры развеял все сомнения.

Cуть ошибки

Я сразу же понял, что нужно копать в сторону улучшения Wall. Быстрый тест показал, что Wall Tower "запоминает" первый выбор игрока, не важно, хватает ли на него Золота или улучшение затем было отменено. Происходит такая ситуация: игрок пытается улучшить башню, но у него не хватает Золота (или игрок затем отменяет данное улучшение), башня "запоминает" данный выбор, и от дальнейшего выбора зависела только конечная модель, тип башни оставался неизменным.
Причём все затраты были верными: для улучшения требовалось столько Золота, сколько требует улучшение в "запомненный" тип. Это хорошо показано в ролике: у игрока есть 100 Золота, но почему-то при попытке улучшить Wall Tower в Arrow Tower, что требует ровно 100 Золота, выводится ошибка.

Как происходит улучшение

Чтобы определить тип башни, в который улучшается Wall Tower, ловятся все приказы без цели. Если отданный приказ является равкодом башни, то значит, что Wall Tower был отдан приказ улучшиться в эту башню. По равкоду я узнаю тип башни и записываю его в специальный атрибут у Wall Tower uptype.
Сразу же после отдачи приказа срабатывает триггер UpgradeStart, который вызывает соответствующую функцию у структуры Wall, передавая лишь юнита, что вызвал триггер. Аналогично работают триггеры UpgradeCancel и UpgradeFinish.
Код Wall Tower
Показан лишь необходимый код, чтобы читатель не отвлекался.
struct Wall extends Tower    
    integer uptype
    
    static method upgradeStart takes unit u returns nothing
        local thistype this = GetUnitUserData(u)
        // GetUnitTypeId() returns type id of Wall.
        if owner.spendGold(TowerData[uptype].goldcost) then
            set isReady = false
        else
            call DisableTrigger(gg_trg_UpgradeCancel)
            call IssueImmediateOrderById(tower, Order_cancel)
            call EnableTrigger(gg_trg_UpgradeCancel)
        endif
    endmethod
    
    static method upgradeCancel takes unit u returns nothing
        local thistype this = GetUnitUserData(u)
        local integer temp = TowerData[uptype].goldcost
        set isReady = true
        call owner.addGold(temp)
        call owner.createGoldTextUnit(tower, temp)
    endmethod
    
    static method upgradeEnd takes unit u returns nothing
        local thistype this = GetUnitUserData(u)
        local CustomPlayer p = owner
        local real x = this.x
        local real y = this.y
        local integer T = uptype
        // Flush Wall
        call owner.addGold(goldcost)
        call owner.createGoldTextUnit(tower, goldcost)
        call delete()
        // Init new tower
        call Tower.construct(u, p, x, y, T)
        call Tower(GetUnitUserData(u)).makeReady()
    endmethod
endstruct
Код PlayerImdtOrders
Показан лишь необходимый код, чтобы читатель не отвлекался.
function TowerImdtOrders takes Tower caster, integer id returns nothing
    local TowerAbility abil = caster.abil
    // TowerUpgrade
    if IsTowerRawCode(id) then
        set Wall(caster).uptype = GetTowerIdByRawCode(id)
        return
    endif
endfunction

function Trig_PlayerImdtOrders_Actions takes nothing returns nothing
    local integer id = GetIssuedOrderId()
    local unit u = GetOrderedUnit()
    local integer caster = GetUnitUserData(u)
    local integer T = GetUnitTypeId(u)
    if T == BuilderId then
        call BuilderImdtOrders(u, id)
    elseif IsUnitTower(u) then
        call TowerImdtOrders(caster, id)
    endif
    set u = null
endfunction

Расследование

Быстрый дебаг показал, что методы upgradeStart и upgradeCancel работают исправно, а вот функция TowerImdtOrders срабатывала лишь 1 раз, а это могла значить одно: после отмены улучшения (игроком или из-за нехватки Золота) Wall Tower переставала считаться башней, то есть IsUnitTower возвращала false. Что же пошло не так...

Классификации, улучшения и морфы

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

Возникновение

Для однозначного определения, чем является юнит, я использую классификации. У башен это Механический (Mechanical). Потому функция IsUnitTower выглядит вот так.
globals
    constant unittype TowerClass = UNIT_TYPE_MECHANICAL
endglobals

function IsUnitTower takes unit u returns boolean
    return IsUnitType(u, TowerClass)
endfunction
Классификация устанавливалась в Редакторе объектов, но во время разработки версии 3.-1.14 я посчитал, что её будет удобнее и логичнее ставить при создании структуры типа Tower, а в Редакторе объектов установку классификации стоит убрать. Из-за этого и возникла ошибка, описанная выше.
  1. Строится Wall Tower, создаётся объект Wall, юниту даётся классификация.
  2. При улучшении юнит сменяется другим, у которого нет классификации.
  3. При отмене юнит вновь сменяется, хоть и на изначальный, но уже без классификации.
  4. Нет классификации -> не башня -> функция TowerImdtOrders не вызывается -> атрибут uptype не обновляется -> создаётся тот тип, что был запомнен до отмены.

Ранее этой ошибки не было, так как классификация у всех башен была изначально установлена.
  1. Строится Wall Tower сразу с классификацией, создаётся объект Wall.
  2. При улучшении юнит сменяется другим, у которого есть классификация.
  3. При отмене юнит вновь сменяется, на изначальный, у которого есть классификация.
  4. Есть классификация -> башня -> функция TowerImdtOrders вызывается -> атрибут uptype обновляется -> создаётся тот тип, который соответствует приказу-равкоду.

Исправление

Было 2 пути: либо при смене юнита добавлять классификацию, либо вернуть в изначальное состояние. Я выбрал второе, так как подобное изменение в v3.-1.14 я применил не только для башен, но и для миньонов, которые в будущем могут подвергаться морфам.

Мораль

Если делаете hotfix версию с небольшими изменениями баланса (численными), делайте hotfix версию с небольшими изменениями баланса. Не надо менять что-то устоявшееся, это можно сделать после, а затем хорошенько протестировать.

Содержание
`
ОЖИДАНИЕ РЕКЛАМЫ...