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
としなければけない。

2020年5月22日金曜日

findAll で文字化けする問題

findAll でオニオンソードを探すと、文字化けする。

//findall オニオンソード
Searching: オニオンメ[ド
↑のような感じ。「ソ」がいわゆるダメ文字。

エスケープ記号を使って

//findall オニオンソ\ード
Searching: オニオンメ[ド

とやってもやっぱりダメ。他のアドオンならこれでうまくいくのにfindAllはダメ。

findAll の場合、解決策としては、\を3つ書く。

//findall オニオンソ\\\ード
???/storage: オニオンソード
findAll は内部で自分自身を windower.send_command を使って
呼びなおしているため、エスケープ記号がその際に潰されてしまう。
下記がその該当コード。

handle_command = function(...)
    if first_pass then
        first_pass = false
        windower.send_ipc_message('findAll update')
        windower.send_command('wait 0.05;findall '..table.concat({...},' '))
ここの処理で消えないように\を3つ書くとうまくいく。4つでもOK。5つでもOK
6つでもOK
でも1つや2つじゃダメ。7つ以上でもダメ。



3つも\を書くのは面倒くさいので、1つでも大丈夫になるようにコードを修正してみる。

windower.send_command('wait 0.05;findall '..table.concat({...},' '):gsub('\\', '\\\\'))

これで「//findall オニオンソ\ード」で文字化けしなくなる。
でも、エスケープ記号を1つ使わないといけないのはしょうがないことだとして諦めよう。


2020年2月8日土曜日

GearSwap の add_to_chat は shift_jis への変換を自動的に行う

GearSwap のユーザーファイルの中では「Windower.」をつけずに add_to_chat() 関数が利用可能です。このadd_to_chat() 関数は、自動的にシフトJISへ文字コードを変換してくれる仕様となっています。

ユーザーファイルの中のLuaの環境は、以前の記事の通り、user_env 変数で定義されたものが利用可能です。user_env は refresh.lua で定義されていますが、その中には add_to_chat も含まれています。

具体的には、add_to_chat=add_to_chat_user と定義されていおり、add_to_chat を動作させると実際には add_to_char_user 関数が動きます。この関数は、user_functions.lua の中で記述されています。

function add_to_chat_user(num,str)
    local backup_str
    if type(num) == 'string' then
        -- It was passed a string as the first argument.
        str = not tonumber(str) and str or num
        num = 8
    elseif not num and str and type(str) == 'string' then
        -- It only needs the number.
        num=8
    end

    if language == 'japanese' then
        msg.add_to_chat(num,windower.to_shift_jis(str))
    else
        msg.add_to_chat(num,str)
    end
end
上記の通り、to_shift_jis しています。日本語文字を出力させる場合は注意が必要です。

2020年2月7日金曜日

GearSwapで/ma "magic-name" <me> <st>が正常動作しない件

「/ma ケアル <me> <st>」を実行すると、通常はユーザーの入力を待ってからケアル詠唱を行います。しかし、GearSwap を使っているとユーザーの入力を待たずに瞬時にケアル詠唱が行われてしまいます。

これは、GearSwap が3つ目までの入力引数しかチェックしないためです。なので4つ目の入力値(<st>)を確認するように GearSwap を修正することで、正常動作するようになります。

以下、修正方法です。

・flow.lua

(1) 78行目から85行目までをコメントアウト
--[[ if val2 then
        if type(val2) == 'table' and val2.type then logit(' : '..val2.type)
        else
            logit(' : Unknown type val2- '..tostring(val2))
        end
    else
        logit(' : nil-or-false')
    end]]

(2) 214行目:4つ目の引数を追加
return equip_sets_exit(swap_type,ts,val1,val2)

(3) 229行目:4つ目の引数を追加
function equip_sets_exit(swap_type,ts,val1,val2)

(4) 282行目:4つ目の引数を見て st_flag = true を動作させるように変更
if st_targs[val1.target.raw] or st_targs[val2] then
    -- st targets
    st_flag = true




・triggers.lua

(1) 56行目:変数 val2 を追加
local abil, temptarg, temp_mob_arr, val2

(2) 73行目 :4つ目の引数を val2 に代入する処理を追加
    temptarg, temp_mob_arr = valid_target(splitline[3])
    val2 = splitline[4]
elseif validabils[language][unified_prefix] then
    temptarg, temp_mob_arr = valid_target(splitline[2])
    val2 = splitline[3]
end
※ val2 = splitline[3] の方は、例えば「/ra <t> <st> 」のような場合

(3) 157行目:4つ目の引数を追加
return equip_sets('pretarget',-1,spell,val2)

上記の修正で<me> <st>が正常動作するようになると思います。




2020年2月6日木曜日

GearSwapの処理軽量化

モーグリやサクラなどのスフィアを展開するフェイスを出していると、棒立ちの状態でも息継ぎをするように十数秒おきに重くなると思います。当方の環境では定期的に29fpsから27fpsに落ちて、処理がカクつきます。いわゆるプチフリ状態です。このプチフリはGearSwapをアンロードすると起こらなくなります。
どの処理が重いのか調査したところ、packet_parsing.lua の 518 行目あたりの処理が関係していることがわかりました。518行目からの4行をコメントアウトすると処理落ちが軽減されました。下記の4行です。これはバフ情報更新のパケットが飛んでくると動く処理です。

if not gearswap_disabled then
    refresh_globals()
    equip_sets('buff_refresh',nil,buff_name,new)
end

上記の refresh_globals() 関数はけっこう重い処理で、プレイヤーの状態や、かばんの中身など、膨大な量のデータをすべて再精査する処理が行われています。たまに動く程度ならば問題ないのですが、スフィアを展開するフェイスを出していると3秒おきにバフ情報更新のパケットが飛んできてそのたびに処理が実行され、継続的に大量のデータが更新され続けることでLuaの内部でガーベジコレクションなどの処理が動き、プチフリみたいなことになっているのかなと考えています。

で、3秒おきに飛んでくるスフィアのバフ情報更新のパケットですが、そのバフの持続時間(duration)はだいたい5~8秒に設定されています。なので、フェイスをしまうと数秒後にバフが消えるという仕掛けです。

この5~8秒という数値を利用して、duration が10秒以上の場合にのみrefresh_globals() 関数を実行するように変更することで、3秒おきにくるスフィア用のパケットを無視できます。

if not gearswap_disabled and new.duration > 10 then
    refresh_globals()
    equip_sets('buff_refresh',nil,buff_name,new)
end

上記のように変更すると、プチフリが軽減されました。スフィアのバフはIDが通常のバフとは違っているため、duration ではなくバフのIDでフィルタリングしても良いのですが、Allied ManteletやMirthful Mandragoraのリフレシュスフィアのようにバフによっては通常のバフと同じIDを使用している場合もあるため、duration の方が確実だと思います。