2020年9月19日土曜日

構文図(precast, midcast, engaged, idle)[Mote-Include]

Mote-Includeのsets.precastなどの構文図のようなものを作ってみました。




・sets.precast

















・sets.midcast











・sets.engaged






・sets.idle

作って気が付きましたが、sets.engagedとsets.idleとでは、user_customize_*_set()とcustomize_*_set()の呼び出し順が逆なんですね。



2020年9月18日金曜日

Mote-Include: aftercastの流れを整理してみる

 Mote-Includeのaftercastにおける自動着替えの流れはちょっと複雑なので整理してみる。


全体の流れは、Mote-Include.luaのhandle_actions()で定義されており、
aftercast()
 ┗handle_actions()
   ┗filter_aftercast()      → EquipStopの処理
[C]   user_aftercast() [U]
[C][h] job_aftercast()  [U]アクション成功時(not spell.interrupted)の処理など
[C][h] default_aftercast()
     ┗handle_equipping_gear()
       ┗job_handle_equipping_gear() [U]check_range_lock()など
     [h']  equip_gear_by_status() → sets.engaged or sets.idleへ着替え
[C]   user_post_aftercast() [U]
[C]   job_post_aftercast()   [U]sets.Buffへの着替えなど
    cleanup_aftercast()
     ┗reset_transitory_classes() → classes.CustomClass and .JAMode= nil

上記の順番で関数が実行される。
[C]:eventArgs.cancel = trueのとき実行されない。
[h]:eventArgs.handled = trueのとき実行されない。
[U]:ユーザー関数

ユーザー関数[U]は5つもあるが、実際にはjob_aftercast()くらいしか使われていないようだ。希少な例だがTHF.luaでは他のユーザー関数も実装されている。

ここではBLM.luaのjob_aftercast()の例を挙げる。

function job_aftercast(spell, action, spellMap, eventArgs)
    -- Lock feet after using Mana Wall.
    if not spell.interrupted then
        if spell.english == 'Mana Wall' then
            enable('feet')
            equip(sets.buff['Mana Wall'])
            disable('feet')
        elseif spell.skill == 'Elemental Magic' then
            state.MagicBurst:reset()
        end
    end
end
マナウォールが成功した場合(not spell.interrupted)にマナウォール用の足装備に着替えて、足の部位を装備変更不可にしている。また、精霊魔法を成功させたときは、マジックバーストモードをリセットしている。

なお、equip()は装備をGearSwapに登録するだけの関数で、サーバーへのパケット送信はこのタイミングでは行われない。equip()はGearSwapのグローバル変数equip_listに値をセットしているだけである。パケット送信がされるのは、equip_sets()内のみ。equip_sets()をユーザーファイルから呼び出すことはできない。

ユーザー関数aftercast()は、通常、triggers.luaのアクションパケットのイベントハンドラから呼び出される。その際、equip_sets('aftercast',~)という感じでequip_sets()にラップされる形で呼び出される。equip_sets()はaftercast()を実行し終わった後、着替え用パケットを送信する。


2020年9月16日水曜日

Mote-Includeのstate.Buffとbuffactiveの違い


◇ state.Buffとbuffactiveはどう違う?

 Githubにあるmote-includeのシーフ用ユーザーファイルを見ると、

function job_setup()

    state.Buff['Sneak Attack'] = buffactive['sneak attack'] or false
    state.Buff['Trick Attack'] = buffactive['trick attack'] or false
    state.Buff['Feint'] = buffactive['feint'] or false

という記述がある。このstate.Buffは、単にbuffactiveの値をセットしているだけなので、パッと見て、buffactiveをそのまま使えばいいやん!と思ってしまう。state.Buffという変数領域を新規に割り当てる意味はどこにあるのかと。

結論から言うと、state.Buffとbuffactiveには違いがある。

簡単に言うと、
        state.Buff:precast時に同期的に値がtrueにセットされる。
        buffactive:midcast以降に非同期的に値がセットされる。
という違い。



・ state.Buffはprecast処理の中で値をtrueにセットされていることから、自動着替えを実行するGearSwapのスレッドの中で同期的に処理されており、ラグが発生しても想定した順番通り動く。

・ それに対して、buffactiveはサーバーからのバフ更新パケットを処理するスレッドの中で値がセットされるため、着替え用スレッドとは非同期で動くことになり、ラグが発生すると想定した順番通りには動かない可能性がある。

したがって、state.Buffを使わずにbuffactiveだけで処理をしてしまうと、不意打ちを実行したのに、ラグのせいでbuffactiveが更新されずに不意打ち用の自動着替えが行われないということも起こりえる。

これは想像だけれども、そういうミスを起こさないためにstate.Buffという変数を新規に作っているのだと思う。



◇ 具体的な処理の流れ


ここで、「不意打ち+通常攻撃」におけるMote-Includeの自動着替えの処理の流れを具体的に書いてみる。

(1) 抜刀中のユーザーが不意打ちを実行する。
  クライアントは不意打ち実行用のパケットを送信(outgoing text)しようとするが、
  GearSwapがそれをブロックし、自動着替え用の処理を開始する。

(2) Mote-Inlcludeが不意打ち用のprecast処理を実行し、
  state.Buff['Sneak Attack']をtrueに設定する。
  Mote-Includeのprecast処理終了後、GearSwapは着替え用パケットを送信し、
  続けてmidcast処理を開始する。

(3) Mote-Includeが不意打ち用のmidcast処理を実行し、その処理が完了後、
  GearSwapはブロックしていた不意打ち実行用のパケットを再送信し、
  さらに、着替え用パケットを送信(アビの場合は普通は無し)。

(4) 不意打ち実行用のパケットがサーバーに届き、サーバー内で不意打ちが実行され、
  クライアントへ、不意打ち処理完了のパケットと、
  バフ変更パケットの2種類のパケットが返ってくる。
  バフ変更パケットを受け取るとクライアントではrefresh_globals()が非同期的に動き、
  buffactive['sneak attack']が1になる。
  不意打ち処理完了パケットを受け取ると、GearSwapはaftercast処理を開始する。

(5) Mote-Includeが不意打ち用のaftercast処理を実行し、
  aftercast()の中でhandle_equipping_gear()が動き、
  その中で、job_handle_equipping_gear()が動き、
  さらにその中で、check_buff()が動き、
  sets.buff['Sneak Attack']を着替え装備として選択する。
  (通常のaftercast処理では、抜刀時にはsets.engagedが選択される)
  Mote-Includeのaftercast処理終了後、GearSwapは着替え用パケットを送信。

(6) 不意打ち効果付きの通常攻撃が実行され、不意打ちのバフが解除される。

(7) バフが解除されたことでbuff_change()が非同期的に動き、
  buffactive['sneak attack']がnilになり、
  state.Buff['Sneak Attack']もfalseになる。

(8) (7)のbuff_change()の中で、job_buff_change()が動き、
  その中で、handle_equipping_gear()が動き、
  (4)と同様に、check_buff()が動くが不意打ちバフがないため、
  sets.buff['Sneak Attack']は選択されず、
  通常通り、equip_gear_by_status()の中で、sets.engagedが選択され、
  GearSwapが着替え用パケットを送信する。



かなりややこしい処理の流れだが、重要なのはバフ用変数がセットされるタイミングの違い。state.Buffが(2)でtrueになり、buffactiveは(4)で1になる。

もしもbuffactiveだけを使いstate.Buffを使わなかったとしたら、仮にひどいラグが起こってバフ変更パケットだけ処理が遅れた場合、(4)の実行タイミングが後ろにずれてしまい、(5)でsets.buff['Sneak Attack']への着替えが起こらず、(6)の通常攻撃をsets.engaged(通常攻撃用の装備)で行ってしまうことになる。バフ変更と着替えの2つのスレッド処理が非同期で動いているため、ずれが発生すると正常に動作しなくなる。なので、そうならないためにもstate.Buffが必要。

もしもバフ変更パケットでなく、不意打ち処理完了パケットの方が遅れた場合、気づいた時には通常攻撃しちゃった後ってことになる。しかし、
sets.precast.JA['Sneak Attack'] = sets.buff['Sneak Attack']
と定義してあるため、(2)の段階で不意打ち装備への着替えは完了しているから、(5)の処理が完了する前に通常攻撃しちゃったとしても問題はない。



ところで、おそらく不意打ち完了のパケットはAction Category 06だと思われるが、もしかすると、その中に不意打ちバフのIDが含まれていて、不意打ちバフの更新も同期的に処理することができそうな気はする。でも、GearSwapはそういう風には実装されていない。あくまでバフは非同期的に処理されている。このあたりについては要検証。



なお、もしも不意打ちのアビが何らかの理由で発動できなかった場合(不意打ちバフが得られなかった場合)はaftercast()の中でstate.Buffをfalseに設定している。アビが失敗したときspell.interruptedがtrueになる。
function aftercast(spell)
    if state.Buff[spell.english] ~= nil then
        state.Buff[spell.english] = not spell.interrupted or buffactive[spell.english] or false
    end
    handle_actions(spell, 'aftercast')
end




◇ もっと簡単に実装できないの?


ややこしい話が続いたけれども、もっと単純に、不意打ちのprecast時に不意打ち用の装備に着替えて、その後、不意打ちのバフが解除されるまでずっと装備変更不可にすればいいんじゃねって考えたくなったりするけど、そう単純にはいかない。

不意打ちのバフは1分間有効なので事前に使っておいて敵に攻撃する前にちょっと空蝉とかを使ったりしたらファストキャスト装備に着替えて欲しくなるが装備変更不可ではそれができない。

なので、通常攻撃とWSのときだけ不意打ち用の装備にしたいわけで、そういうニーズに応えるためには処理が複雑になってしまう。

なお、不意打ち用WS装備は冒頭のURLのファイルの中では、例えば、
sets.precast.WS['Exenterator'].SA = set_combine(sets.precast.WS['Exenterator'].Mod, {ammo="Qirmiz Tathlum"})

と定義している。このSAは、以下の関数の中でsetsの末端の”枝”として選択されるようになっている。

function get_custom_wsmode(spell, spellMap, defaut_wsmode)
  local wsmode

  if state.Buff['Sneak Attack'] then
    wsmode = 'SA'
  end
  if state.Buff['Trick Attack'] then
    wsmode = (wsmode or '') .. 'TA'
  end

  return wsmode
end
このget_custom_wsmode()に関しての説明(英語)は、Mote-IncludeのGitHubにある。



◇ トレジャーハンター


実は、トレジャーハンター装備の着替えロジックが上記の「装備変更不可にすればいいんじゃね」になっている。トレハンつけてない敵をタゲって抜刀すると自動的にトレハン装備に着替えてくれるが、同時にGearSwap内で該当部位の装備スロットをロックするため、攻撃前に魔法を使ったりするとその部位はファストキャスト装備などに着替えてくれない。

なので、トレハンの着替えも上記の不意打ちみたいにうまく実装できないものかと考えてしまう。が、カロリーが高そうなのでなかなかできそうにない。




ところで、もしもトレハン8で十分な場合はトレハンをつけたら(Tag)もうそれ以降はトレハン装備に着替える必要がない。
Mote-Includeは敵ごとにトレハンフラグ(info.tagged_mobs[player.target.id])を管理していて、それを見て抜刀時にトレハン装備に着替えるかどうかの判断をしている。

しかし、この抜刀時の処理が並列的に、二重に動いているかもしれなくて、ちょっと危うい実装になっている。

具体的には、本家GearSwapのgearswap.luaで
windower.register_event('status change'しているのとは別に、
Mote-TreasureHunter.luaでもwindower.register_event('status change'している。

前者ではsets.engagedへの着替えを行い、後者はsets.TreasureHunterへの着替えを行う。
もしかすると、二つのイベントハンドラは厳密には同時並列実行はされてはいないのかもしれないので断定はできないが、もしも同時並列に動いているのならば危うい結果になることは想像できる。でも今のところ異常は発生したことがない。

イベントによっては引数にbool blockをとることがあり、その場合、仕様的に同じ種類のイベントハンドラを並列実行できないと思うから、(blockはないけれども)status changeも順番に1つ1つ実行しているような気もする。もちろん、種類が違うイベントは並列処理してもいいけどね。

まあ、だから、たぶん大丈夫なのかも。でも、やっぱ二重になっているのは一本化した方がよいな。Mote-TresasureHunter.luaでのregister_eventをやめて、そこでのトレハン処理をjob_status_changeに移行すれば抜刀時の着替え処理を一本化できる気がする。




しかしラグに関しては問題がある。

不意打ちの場合は上記(2)のprecastで着替えているので、アビより前に着替えが済んでおり、同期処理なので着替えは想定した順番通りに問題なく実行される。

それに対してトレハンの場合は抜刀した後に着替えが行われるため、もしも抜刀後すぐに通常攻撃が発生するような状況において、抜刀時にラグが発生すると、1発目の通常攻撃とトレハン着替えのどちらが先に実行されたのか分からなくなる。もしも着替えだけが遅れてしまった場合、トレハンをつけてないのにトレハンフラグ(tagged_mobs)だけは立ってしまうという不具合が発生する。

それでもモードがFulltimeならば抜刀後はトレハン装備に着替えたままなのでいつかはトレハンをつけることができるが、Tagのときはトレハンフラグが立っているとトレハン装備には着替えないからトレハンがつかない。

もちろんそういう時はモードをFulltimeに切り替えればよいがちょっとめんどい。

もしも抜刀開始の直前に動作するイベントハンドラがあれば問題は解決する。抜刀に関しては、SendAllTargetアドオンを見ると特定のoutgoingパケットを送信すれば実現できるので、そのパケットを検出したらすぐに着替えるようにすればよさそうだ。




◇ ディフュージョン


最後に、state.Buffの綺麗な使い方を紹介する。
青用ユーザーファイルのjob_post_midcast()ではstate.Buffに対応したsets.buffに自動着替えするコードが実装されている。非常にすっきりとしたコードであり、こういうことがしたかったんだろうなと実感できる。
function job_post_midcast(spell, action, spellMap, eventArgs)
  -- Add enhancement gear for Chain Affinity, etc.
  if spell.skill == 'Blue Magic' then
    for buff,active in pairs(state.Buff) do
      if active and sets.buff[buff] then
        equip(sets.buff[buff])
      end
    end
例えばディフュージョンでマイティを詠唱したとき、midcastでの着替えでMote-Includeはsets.buff.Diffusionを自動的に選択してくれる。init_gear_sets()に
sets.buff.Diffusion = {feet="LLチャルク+3"}
と書いておけばディフュージョン効果アップの恩恵を受けられる。(アビリティを実行するときではなく、該当する青魔法の着弾時、つまりmidcastのときにディフュージョン効果アップ装備を着る必要があることに注意)

ただし、複数のアビを同時実行した場合、装備部位がバッティングする可能性もあるから、個別な調整が必要になる。

なお、日本語環境の場合、上記のif spell.skill == 'Blue Magic' thenは
if spell.skill == '青魔法' then
もしくは
if spell.type == 'BlueMagic' then
としなければけない。