ラベル Lua の投稿を表示しています。 すべての投稿を表示
ラベル Lua の投稿を表示しています。 すべての投稿を表示

2023年5月18日木曜日

mote-include:ヘイストやインスニをしても、強化魔法スキル装備に着替えない件

結構いまさら感ある内容ですが、mote-includeの話を書いてみます。

mote-include を使った強化魔法での自動着替えで注意が必要になるというお話です。


ヘイストとかインスニとか、バフの効果時間の延長を狙って強化魔法の効果時間延長装備に着替えたいわけですが、 強化魔法用に sets.midcast['強化魔法'] を定義してもデフォルトでは着替えてくれません。これはバグではなくて仕様です。

でも、ヘイストIIの場合はちゃんと着替えてくれますし、ストンスキンとかアクアベールでも着替えてくれます。ちょっと謎な仕様ではあります。

結論から申し上げますと、Mote-Mappings.lua で以下のように定義されていて、これが悪さ?をしています。

no_skill_spells_list = S{'Haste', 'Refresh', 'Regen', 'Protect', 'Protectra', 'Shell', 'Shellra','Raise', 'Reraise', 'Sneak', 'Invisible', 'Deodorize'}

この no_skill_spells_list に含まれている魔法は、midcast のときに着替えない仕様となっています。より正確には、sets.midcast['強化魔法'] の対象外となるようにプログラムされています。

そのことは、Mote-Include.lua にもちゃんと書かれておりました。

-- List of spells and spell maps that don't benefit from greater skill (though
-- they may benefit from spell-specific augments, such as improved regen or refresh).
-- Spells that fall under this category will be skipped when searching for
-- spell.skill sets.
classes.NoSkillSpells = no_skill_spells_list

強化魔法スキルの恩恵を受けないから、着替えないと書かれております。でも、スキルの恩恵はなくても強化魔法の効果時間延長のためには着替えて欲しいわけで、なかなか微妙なところではあります。

応急処置としては、ユーザーlua ファイルの get_sets() の中で classes.NoSkillSpells を上書きしてしまうのが一番楽ちんだと思います。

-- Initialization function for this job file.
function get_sets()
    mote_include_version = 2
    -- Load and initialize the include file.
    include('Mote-Include.lua')
    classes.NoSkillSpells = S{'Protect', 'Protectra', 'Shell', 'Shellra', 'Raise', 'Reraise' }
    set_language('japanese')
end


あるいは、個別に

sets.midcast.Haste = sets.midcast['強化魔法']

とかでも良いですが、classes.NoSkillSpells を上書きしちゃう方が楽かな。


2022年6月7日火曜日

menu IDって何のID?

 NPCと会話するとincoming 0x034でmenu idが送られてきますが、このmenu idって何のテーブルのIDなのか良くわからなかったので、WEB検索して少し調べてみました。

https://www.reddit.com/r/FFXIPrivateServers/comments/tcxycb/xievents_ffxis_event_vm_fully_reversed/

https://github.com/atom0s/XiEvents

英語ですが上記のリンクが参考になりました。そして、結論から言うと、Zone Events DATファイルの中のイベントスクリプトのID(EvectExecNum)でした。かなりややこしいデータ構造なので、備忘録代わりに簡単にさらっと書いてみます。なお、変数名は上記リンクのものをそのまま使用しています。

まず、例えばルルデの庭のDATファイル群は以下のようになっていて

zone id: 243
エリア名: Ru'Lude Gardens
Zone Entities:  ROM/27/52.DAT
Zone Events:   ROM/21/52.DAT
Zone Dialogs/Strings:  ROM/25/52.DAT(English)

一番下のZone Dialogs/StringsのDATファイルの中にはNPCがしゃべる実際のテキストデータが格納されています。たとえば、シグネットをかけてやろう、などの文とかです。なお、このテキストデータのIDは、incoming 0x02AのMessage IDとして使われています。エリチェンする度にハッピーパワーのログが表示されたりしますが、あれは0x02Aで送られてきており、そのとき指定されるのがこのテキストデータのIDとなります。(ただし、Message IDの最上位ビットをクリアする必要があります。それとバージョンアップの度にMessage IDはコロコロと値が変わるので扱いづらいです)

windower の lib/dialog.lua の function dialog.get_entry(dat, id) を使うと、datファイルとMessage IDを引数として渡すことで、対応するエリアのDATファイルからIDで指定されたテキストデータを取得することができます。

なお、引数である dat には function dialog.open_dat_by_zone_id(zone_id, language) の返り値をそのまま代入します。(処理が終わったらクローズ処理が必要です)

ここまでの話はそんなに難しくないのでよいのですが、ここから難しくなります。


Zone Events DATファイルの話をします。

まず、上記のリンクに書かれていることによると、NPCとおしゃべりをするとクライアントプログラムは裏でEventVM(仮想マシン)を動かしているとのことです。例えば、シグネットをかけてやろう、においては、文字を表示したり、ユーザーの選択を待ったり、NPCに口パクをさせたり、ユーザーにエフェクトをかけたりなど、いろいろな処理が行われますが、そういった処理が、どの順番でどういうタイミングで実行されるのかをスクリプトのようにくみ上げ、それをバイトコード列としてあらかじめDATファイルに記録しておき、実行時にそれを動的に解釈して処理を実施しています。また、カットシーンのNPCの動きなども仮想マシンで処理されていおり、汎用性の高いスクリプトのようです。

どういう順番でどういう処理をするのかという指示データはバイトコード列(EventData)としてZone Events DATファイルに格納されていて、そのバイトコード列のIDが menu id です。つまり、イベントスクリプトのIDです。なお、これは、かなりざっくりした説明で、実際にはもっと細かくデータ構造がくみ上げられています。

バイトコード列はマシン語のようにオペコードとオペランドの列になっていて、たとえばオペコード0x1Dはオペランドで指定した文字列を表示する命令になっています。

例えば、0x1D 0x04 0x80というバイト列の場合、0x04が表示する文字列の番号になっていて、これはImidData[0x04]で指定されているテキストデータを表示するという意味になります。ImidDataもZone Events DATファイルに記録されているデータで、Message IDの配列となっているようです。なのでオペランドは間接参照のようですね。ですが、これは0x80の場合のみで、0x80以外の場合はワークメモリ(Work_Zone)を参照したりします。

上記のリンクのすべてを読みこなせているわけではないので、これ以上の細かい部分の説明はできませんが大枠ではこんなところです。

で、一番肝心なことですが、menu idの意味が分かったとしてそれが何の役にたつのかという質問の答えは、現状はたいして役には立たないかも^^)vという回答になりそうです。


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 の方が確実だと思います。


2019年12月19日木曜日

GearSwapのユーザーファイルとsetfenv


GearSwap を利用する場合、あらかじめWAR.lua などのユーザーファイルを利用者が作成しておくことになるが、作成したファイルは自動的にGearSwapによって読み込まれて自動着替えの際に内容が参照される。
このユーザーファイルの読み込み処理は refresh.lua の load_user_files() にて行われる。
function load_user_files(job_id,user_file)

  local path,base_dir,filename
  path,base_dir,filename = pathsearch({user_file})
  if not path then
    local long_job = res.jobs[job_id].english
    local short_job = res.jobs[job_id].english_short
    local tab = {player.name..'_'..short_job..'.lua',player.name..'-'..short_job..'.lua',
      player.name..'_'..long_job..'.lua',player.name..'-'..long_job..'.lua',
      player.name..'.lua',short_job..'.lua',long_job..'.lua','default.lua'
}
    path,base_dir,filename = pathsearch(tab)
  end

  -- Try to load data/<name>_<main job>.lua
  local funct, err = loadfile(path)

  setfenv(funct, user_env)

上記のコードにて loadfile によってユーザーファイルが読み込まれるが、その後、読み込んだ内容(funct)を setfenv している。これによりユーザーファイルに記述されたユーザー関数を実行するときの環境が user_env にセットされる。下記は user_env の内容である。ユーザー関数内ではこの user_env の中に含まれている変数や関数を利用できる。逆に言うと、グローバル変数であっても user_env に含まれていなければ利用することができない。
user_env = {gearswap = _G, _global = _global, _settings = _settings,_addon=_addon,
  -- Player functions
  equip = equip, cancel_spell=cancel_spell, change_target=change_target, 
  cast_delay=cast_delay,
  print_set=print_set,set_combine=set_combine,disable=disable,enable=user_enable,
  send_command=send_cmd_user,windower=user_windower,include=include_user,

  midaction=user_midaction,pet_midaction=user_pet_midaction,
  set_language=set_language,
  show_swaps = show_swaps,debug_mode=debug_mode,
  include_path=user_include_path,
  register_unhandled_command=user_unhandled_command,
  move_spell_target=move_spell_target,
  language=language,

  -- Library functions
  string=string,math=math,table=table,set=set,list=list,T=T,S=S,L=L,
  pack=pack,functions=functions,
  os=os,texts=texts,bit=bit,type=type,tostring=tostring,tonumber=tonumber,pairs=pairs,
  ipairs=ipairs, print=print, add_to_chat=add_to_chat_user,unpack=unpack,next=next,
  select=select,lua_base_path=windower.addon_path,empty=empty,file=file,
  loadstring=loadstring,assert=assert,error=error,pcall=pcall,io=io,dofile=dofile,

  debug=debug,coroutine=coroutine,setmetatable=setmetatable,
  getmetatable=getmetatable,
  rawset=rawset,rawget=rawget,require=include_user,
  _libs=_libs,

  -- Player environment things
  buffactive=buffactive,
  player=player,
  world=world,
  pet=pet,
  fellow=fellow,
  alliance=alliance,
  party=alliance[1],
  sets={naked = {main=empty,sub=empty,range=empty,ammo=empty,
    head=empty,neck=empty,ear1=empty,ear2=empty,
    body=empty,hands=empty,ring1=empty,ring2=empty,
    back=empty,waist=empty,legs=empty,feet=empty}}
  }

user_env['_G'] = user_env
requireはinclude_userに置き換えられている。inlcude_user() 関数は user_functions.lua で定義された関数であり、引数で指定されたファイルを loadfile() で読み込み、その環境を user_env に setfenv する処理になっている。

Mote-inlcude を使う場合、”決まり文句”として include('Mote-Include.lua') をするが、この include は user_env に定義されている通り、include_user() 関数のことである。
-- Initialization function for this job file.
function get_sets()
    mote_include_version = 2

    -- Load and initialize the include file.
    include('Mote-Include.lua')
end


2019年12月18日水曜日

ステータス(buff)の残り時間の取得方法


ヘイストやリフレシュなどの buff があと何分もつかは、公式の機能 /statustimer により画面上部のステータスアイコン上に数値で表示される。しかし windower本体はその数値を管理してはいない。Timersプラグインにはデータがあると思われるが、アドオンからはプラグインの内部データにアクセスすることはできないのでbuffが切れる直前に何かを実行させたいと思っても簡単にはできない。

"windower duration timer"でググるとこんな投稿があった
If you're working outside of gearswap, or if you absolutely need the duration, the incoming packet which contains that information is 0x063. The packet id is reused for different types of information, but will contain the timestamps that indicate when buffs will wear off when the fifth byte has the value 9. This change was made to the packet in the update that introduced the timer display.

GearSwapはbuff情報を自前で管理している。buffactiveにリアルタイムのbuff情報が格納されている。そのbuffactiveよりも詳細な情報が player.buff_details に格納されていて、buff がいつ切れるかの時間も入っている。以下は、packet_parsing.lua の該当部分のコード。
parse.i[0x063] = function (data)
  if data:byte(0x05) == 0x09 then
    local newbuffs = {}
    for i=1,32 do
        local buff_id = data:unpack('H',i*2+7)
        if buff_id ~= 255 and buff_id ~= 0 then -- 255 is used for "no buff"
          local t = data:unpack('I',i*4+0x45)/60+501079520+1009810800
          newbuffs[i] = setmetatable({
            name=res.buffs[buff_id].name,
            buff=copy_entry(res.buffs[buff_id]),
            id = buff_id,
            time=t,
            date=os.date('*t',t),
         },
         {__index=function(t,k)
           if k and k=='duration' then
              return rawget(t,'time')-os.time()

           else
              return rawget(t,k)
           end
        end})
     end
  end
        table.reassign(_ExtraData.player.buff_details,newbuffs)

GearSwap のユーザーlua ファイルの中であれば player.buff_details[i].duration で buff の残り時間が参照できると思われる。

そして、ユーザーイベント関数 buff_change(name,gain,buff_details) の引数として渡される buff_details に player.buff_details の値が格納されているので、それを利用することも可能。




windower本体がこのbuff情報を正式に管理してくれるとありがたいのだが。

2019年12月15日日曜日

GearSwapを一時的に使用したくない場合


たとえば、ラーニング用の手装備を外したくない場合、
//gs disable hands
を実行すると、両手装備はGearSwapで変更されなくなる。

ラーニングが終わってGearSwapの動作を元に戻したい場合は、
//gs enable hands
を実行する。

両手を disable しているときにGearSwapが着替えを実行した場合、内部的にはnot_sent_out_equip['hands']という変数に本来着替えるべき装備アイテム名を記録し、enable したときにその記録した値で両手の着替えを瞬時に実行するように作られている。

全身の着替えを無効化したい場合は、
//gs disable all
とコマンド入力する。

部位を表す文字は以下のものが使用できる。statics.lua の slot_map で定義されている。
main, sub, range, ranged, ammo, head, neck, body, hands, back, waist, legs, feet,
ear1, ear2, lear, rear, left_ear, right_ear, learring, rearring,
lring, rring, left_ring, right_ring, ring1, ring2

なお、ドット付きの名称 L.ear とか R.ring とかも使用可能である。内部的には、
ltab:append(v:gsub('[^%a_%d]',''):lower())
という命令文によって、英数アンダーバー以外の文字を消した後、小文字化している。


装備部位を指定せずに
//gs disable
と実行した場合は、GearSwap 全体の動作がストップする。
//gs enable
を実行すると、動作が再開する。
disable を実行すると内部的には gearswap_disabled という変数に true が設定され、この変数の値を見て着替え関連の動作を中断するようにできている。


Mote-Includeの装備セット選びの基本アルゴリズム


Mote-IncludeはWSや魔法、アビリティごとに装備セットを簡単に設定できてとても便利。
GitHubにあるジョブごとのサンプルテンプレートを見ると、たとえば、

sets.precast.JA['Mana Wall'] = {feet="Goetia Sabots +2"}
sets.precast.JA.Manafont = {body="Sorcerer's Coat +2"}
sets.precast.FC = {ammo="Impatiens", head="Nahtirah Hat",
                             ear2="Loquacious Earring",
                             body="Vanir Cotehardie",ring1="Prolix Ring",
                             back="Swith Cape +1",waist="Witful Belt",
                             legs="Orvail Pants +1",feet="Chelona Boots +1"}
sets.precast.FC.Cure = set_combine(sets.precast.FC,
                             {body="Heka's Kalasiris", back="Pahtli Cape"})

こんな風に各アクションごとに装備するセット内容を指定できる。Mote-Include はユーザーが定義した装備セットの中からその状況にあう最適なものを自動的に選択してくれる。
ちなみに Manafont は魔力の泉のこと。

でも、どういうアルゴリズムでどの装備セットが選ばれるのか良く分からない。sets.precast.FC と sets.precast.FC.Cure の両方が存在していると競合しないのかとか気になる。なので調べてみた。なお、language = english とする。



1.基本となるsets

基本となるsetsは以下のものとなる。Mote-Include.luaの108行目で定義されている。
-- Sub-tables within the sets table that we expect to exist, and are annoying to have to
-- define within each individual job file. We can define them here to make sure we don't
-- have to check for existence. The job file should be including this before defining
-- any sets, so any changes it makes will override these anyway.

sets.precast = {}
sets.precast.FC = {}
sets.precast.JA = {}
sets.precast.WS = {}
sets.precast.RA = {}
sets.midcast = {}
sets.midcast.RA = {}
sets.midcast.Pet = {}
sets.idle = {}
sets.resting = {}
sets.engaged = {}
sets.defense = {}
sets.buff = {}

上記の sets の下に新しく”枝”を伸ばして装備セットを定義していく。
これ以外にも、Mote-TreasureHunter.lua では sets.TreasureHunter が使われている。



2.precastでの選択

Mote-Include.lua の get_precast_set()関数の中で装備セットの選択が行われる。
下記のコードの通り、プレイヤーが実行しようとしているアクションが魔法であれば FC の枝が選ばれ、WSであれば WS の枝が選ばれ、それ以外のアビリティであれば JA の枝が選ばれる。
local cat

if spell.action_type == 'Magic' then
    cat = 'FC'  -- sets.precast.FC
elseif spell.action_type == 'Ability' then
    if spell.type == 'WeaponSkill' then
        cat = 'WS' -- sets.precast.WS
    elseif spell.type == 'JobAbility' then
        cat = 'JA' -- sets.precast.JA
    else
        -- Allow fallback to .JA table if spell.type isn't found, for all non-weaponskill abilities.
        cat = (sets.precast[spell.type] and spell.type) or 'JA' -- ex. sets.precast.PetCommand
    end
end


下から3行目のコードは、ペットコマンドなど特殊なアビリティの場合の処理である。その場合、sets.precast.PetCommand が定義されていればそれが装備セットとして選ばれる。
以下は、res\job_abilities.lua から抜き出したものである。type で設定されている文字列を sets.precast の下の枝の名前として使うことができる。つまり、sets.precast.PetCommand だけでなく sets.precast.Samba とか sets.precast.Rune とかも名前として有効。

    [16] = {id=16,en="Mighty Strikes",ja="マイティストライク",prefix="/jobability",type="JobAbility"},
    [68] = {id=68,en="Super Jump",ja="スーパージャンプ",prefix="/jobability",type="JobAbility"},
    [69] = {id=69,en="Fight",ja="たたかえ",prefix="/pet",type="PetCommand"},
    [98] = {id=98,en="Fighter's Roll",ja="ファイターズロール",prefix="/jobability",type="CorsairRoll"},

    [131] = {id=131,en="Light Shot",ja="ライトショット",prefix="/jobability",type="CorsairShot"},
    [184] = {id=184,en="Drain Samba",ja="ドレインサンバ",prefix="/jobability",type="Samba"},
    [190] = {id=190,en="Curing Waltz",ja="ケアルワルツ",prefix="/jobability",type="Waltz"},
    [196] = {id=196,en="Spectral Jig",ja="スペクトラルジグ",prefix="/jobability",type="Jig"},
    [201] = {id=201,en="Quickstep",ja="クイックステップ",prefix="/jobability",type="Step"},
    [205] = {id=205,en="Desperate Flourish",ja="D.フラリッシュ",prefix="/jobability",type="Flourish1"},
    [206] = {id=206,en="Reverse Flourish",ja="R.フラリッシュ",prefix="/jobability",type="Flourish2"},
    [215] = {id=215,en="Penury",ja="簡素清貧の章",prefix="/jobability",type="Scholar"},
    [358] = {id=358,en="Ignis",ja="イグニス",prefix="/jobability",type="Rune"},
    [371] = {id=371,en="Valiance",ja="ヴァリエンス",prefix="/jobability",type="Ward"},
    [372] = {id=372,en="Gambit",ja="ガンビット",prefix="/jobability",type="Effusion"},
    [514] = {id=514,en="Shining Ruby",ja="ルビーの輝き",prefix="/pet",type="BloodPactWard"},
    [516] = {id=516,en="Meteorite",ja="プチメテオ",prefix="/pet",type="BloodPactRage"},
    [676] = {id=676,en="Dream Flower",ja="夢想花",prefix="/pet",type="Monster"},



3.パンくずリスト

たとえば、sets.precast.FC を装備セットとして選んだ時、Mote-Include はパンくずリスト(breadcrumbs)を残す。
mote_vars.set_breadcrumbs:append('sets')
mote_vars.set_breadcrumbs:append('precast')

mote_vars.set_breadcrumbs:append(cat)



4.さらに下の枝

ユーザーがケアル魔法を唱えようとしたとき、デフォルトでは sets.precast.FC が選ばれるが、もしも sets.precast.FC.Cure が定義されているならば sets.precast.FC.Cure が選ばれる。Cure は spell_maps で定義されている。

    spell_maps = {     ['Cure']='Cure',['Cure II']='Cure',['Cure III']='Cure'

上記のような感じで Mote-Mappings.lua の中で定義されている。ケアル以外にも
['Poisona']='StatusRemoval',['Paralyna']='StatusRemoval'
['Barfire']='BarElement',['Barstone']='BarElement'
['Protect']='Protect',['Protect II']='Protect',['Protect III']='Protect'

こんな感じでいろいろ spellMap が定義されている。
(新魔法が実装された場合、Mote-Mappings.lua を直接修正するか、あるいは、ユーザー関数 job_get_spell_map() で対応する必要がある)

例えば、プロテスII を唱えようとしたとき sets.precast.FC.Protect が定義されていればそれが選ばれる。



spellMap が選ばれるコードは以下のもの。
-- Simple utility function to handle a portion of the equipment set determination.
-- It attempts to select a sub-table of the provided equipment set based on the
-- standard search order of custom class, spell name, and spell map.
-- If no such set is found, it returns the original base set (equipSet) provided.

function get_named_set(equipSet, spell, spellMap)
    if equipSet then
        if classes.CustomClass and equipSet[classes.CustomClass] then
            mote_vars.set_breadcrumbs:append(classes.CustomClass)
            return equipSet[classes.CustomClass]
        elseif equipSet[spell.english] then -- ex. sets.precast.FC['Cure V']
            mote_vars.set_breadcrumbs:append(spell.english)
            return equipSet[spell.english]
        elseif spellMap and equipSet[spellMap] then
            mote_vars.set_breadcrumbs:append(spellMap)
            return equipSet[spellMap]
        else
            return equipSet
        end
    end
end
もしも、sets.precast.FC['Cure V'] が定義されている場合、sets.precast.FC.Cure よりも優先して選ばれる。それは上記のコードの通りである。英語フォーラムに投稿されていたものであるが、獣使いの場合、
sets.precast.PetCommand['Spur'] = {feet="Ferine Ocreae +2"}
というゆう風にもできる。Spurは「きばれ」。

個別の魔法名やアビリティ名よりも優先されるのが CustomClassである。これはユーザーが自分で設定する変数値。 英語フォーラムに例があった
function job_post_precast(spell, action, spellMap, eventArgs)
    if player.hpp < 51 then
        classes.CustomClass = "Breath"
    end
end
こんな風にして、ヒールブレス用の装備セットを優先させている。
ユーザーがディアを唱えたとき、sets.precast.FC.Breath が定義されていれば選ばれる。



5.別の選択肢

もしも、sets.precast.FC.Cure も sets.precast.FC['Cure V'] も sets.precast.FC.Breath もユーザーが定義していない場合、その他の選択肢が候補となる。

spell.skill("Healing Magic"、"Enhancing Magic"、"Ninjutsu"など魔法スキルのこと)
spell.type("WhiteMagic"、"BlackMagic"、"SummonerPact"、"Ninjutsu"、
                    "BardSong"、"BlueMagic"、"Trust"など魔法の種類)
の2つが候補。たとえば、
                     sets.precast.FC["Healing Magic"]  -- spell.skill
                     sets.precast.FC["WhiteMagic"] -- spell.type
など。ただし language が japanese の場合は spell.skill の方は日本語で指定する必要がある。

spell.skill と spell.type を選択するコードは以下。注目すべきは最後に get_named_set() を再度呼び出していること。つまり、sets.precast.FC["Healing Magic"].Cure というのも装備セットの名前としては有効。


-- Select the equipment set to equip from a given starting table, based on standard
-- selection order: custom class, spell name, spell map, spell skill, and spell type.
-- Spell skill and spell type may further refine their selections based on

-- custom class, spell name and spell map.
function select_specific_set(equipSet, spell, spellMap)
    -- Take the determined base equipment set and try to get the simple naming extensions that
    -- may apply to it (class, spell name, spell map).

    local namedSet = get_named_set(equipSet, spell, spellMap)
   
    -- If no simple naming sub-tables were found, and we simply got back the original equip set,
    -- check for spell.skill and spell.type, then check the simple naming extensions again.

    if namedSet == equipSet then
        if spell.skill and equipSet[spell.skill] and not classes.SkipSkillCheck then
            namedSet = equipSet[spell.skill]
            mote_vars.set_breadcrumbs:append(spell.skill)
        elseif spell.type and equipSet[spell.type] then
            namedSet = equipSet[spell.type]
            mote_vars.set_breadcrumbs:append(spell.type)
        else
            return equipSet
        end
       
        namedSet = get_named_set(namedSet, spell, spellMap)
    end
    return namedSet or equipSet
end



6.モード

Mote-Include ではモードが定義されている。
モード変更用のキーバインド設定がMote-Globals.luaにある。
-- Function to bind GearSwap binds when loading a GS script.
function global_on_load()
send_command('bind f9 gs c cycle OffenseMode')
send_command('bind ^f9 gs c cycle HybridMode')
send_command('bind !f9 gs c cycle RangedMode')
send_command('bind @f9 gs c cycle WeaponskillMode')
send_command('bind f10 gs c set DefenseMode Physical')
send_command('bind ^f10 gs c cycle PhysicalDefenseMode')
send_command('bind !f10 gs c toggle Kiting')
send_command('bind f11 gs c set DefenseMode Magical')
send_command('bind ^f11 gs c cycle CastingMode')
send_command('bind f12 gs c update user')
send_command('bind ^f12 gs c cycle IdleMode')
send_command('bind !f12 gs c reset DefenseMode')

send_command('bind ^- gs c toggle selectnpctargets')
send_command('bind ^= gs c cycle pctargetmode')
end
各モードの内容は user_setup()関数でユーザー自身が定義する。
以下は BLM.lua の例。
function user_setup()
    state.OffenseMode:options('None', 'Normal')
    state.CastingMode:options('Normal', 'Resistant', 'Proc')
    state.IdleMode:options('Normal', 'PDT') -- Physical Damage taken
Resistant は魔命特化、Proc はアビセアなどでの弱点用。PDT は物理防御。
プレイ中に Ctrl-F11 で CastingMode を変えることができる。
sets.midcast['Elemental Magic'].Resistant
sets.midcast['Elemental Magic'].Proc
上記のように定義しておけば、そのときのモードに合わせて自動的に選択される。









2019年12月14日土曜日

Mote-Includeは日本語に対応していない


Mote-includeはGearSwapをさらに使いやすくするためのライブラリであり、現在ではGearSwapにはMote-Includeが標準で含まれている。しかし、GearSwapは日本語対応しているのにたいしてMote-Includeは日本語非対応となっている。完全に動かないというわけではないが、正常動作しない部分がある。

たとえば、Mote-Includeではsets.idle.Townという装備セットを登録することで、街中に入ると自動で着替えるようになっているが、現在のエリアが街中かどうかを判断するコードが英語環境専用になっていて日本語環境の場合(set_language('japanese'))では正常には動作しない。

Mote-Mappings.luaの中で”街のエリア名”を格納する変数 areas.Cities が定義されているが、そこには英語のエリア名しか登録されていない。一方で、プレイヤーキャラが現在いるエリアの名前は日本語名で取得してしまう。言語の不一致で正常に動作しない。

また、「if buffactive.weakness then」というコードがあり、デバフ名を英語でハードコーディングしているため、衰弱時の自動着替え sets.idle.Weak も正常には動作しない。

加えて、「if spell.english == 'Spectral Jig' and buffactive.sneak then」というコードでは、スニークのバフ名を英語でハードコーディングしている。Mote-Includeでは”スペクトラルジグ”を実行するときにスニーク状態であれば、ジグを実行する前に自動的にスニークをcancelする処理が実装されている。日本語環境ではその処理が実行されない。

さらに、曜日や天候の属性(element)を処理する以下のコード(Mote-Utility.lua)でも、日本語と英語が混在することで動作不良を起こす。
    local world_elements = S{world.day_element}
    if world.weather_element ~= 'None' then
        world_elements:add(world.weather_element)
    end
world変数は本家GearSwapが提供する変数であり、日本語環境の場合は日本語名が格納されている。ところが、Mote-Includeは英語名を前提にして作られているため言語の不一致により正常には動作しない。









buffactiveのindexを英語でハードコーディングしている問題


GearSwapはbuffactive変数(table)によってプレイヤーのbuff内容をリアルタイムに管理している。たとえば、マーチが1曲かかっているときは、buffactive[214] == 1となり、2曲の時は値が2になる。214はマーチのIDである。このとき、インデックスは214だけでなく、buffactive.march == 2と書くこともできる。

ところが、buffactive.marchに値が設定されるのは、languageがenglishの場合のみである。GearSwapは
                            local buff = res.buffs[id][language]:lower()
上記の命令文によってインデックス名を指定しているため、languageがjapaneseの場合は、buffactive['マーチ'] == 2と書く必要があり、buffactive.marchには値が設定されない。

gearswapのluaファイルの中身を検索すると、buffactive.charmとかbuffactive.Pianissimo とかbuffactive.Entrustとか見つかる。あるいは「if T{'terror', 'sleep', 'stun', 'petrification', 'charm', 'weakness'}:contains(buff_name:lower()) then」のように状態異常名を直書きしているコードもある。インデックス名が英語で書かれていてはjapaneseの場合は正常に動作しない。

したがって正常に動作させるためには該当部分を日本語に修正するか、IDに修正する必要がある。buffactive['魅了']、buffactive['ピアニッシモ']、buffactive['エントラスト']への修正。
buffactive[14]、buffactive[17]、buffactive[409]、buffactive[584]への修正。
あるいは一切修正せず language を english にして magicReplace を使う方法もある。



ところで、上記lower()関数はビルトインのものではなくオーバーロードされた関数であることに注意。GearSwap\helper_functions.luaの35行目で下記の通りに再定義されている。
function string.lower(message)
    if message and type(message) == 'string' and language == 'english' then
        return __raw.lower(message)
    elseif message and type(message) == 'string' then
        return message:gsub('[A-Z]',function (letter) return string.char(letter:byte(1)+32) end)
    else
        return message
    end
end

さらにbuffactiveにはメタテーブルが設定されていて__indexがオーバーロードされている。

◆helper_functions.lua
function user_key_filter(val)
    return type(val) == 'string' and string.lower(val) or val
end
function make_user_table()
    return setmetatable({}, user_data_table)
end

◆statics.lua
user_data_table = {
    __newindex = function(tab, key, val)
        rawset(tab, user_key_filter(key), val)
    end,
    __index = function(tab, key)
        return rawget(tab, user_key_filter(key))
    end
    }
buffactive = make_user_table()

このオーバーロードによって buffactive.Pianissimo とか buffactive.Entrust のように先頭文字が大文字であってもlower()関数によって小文字化されるため問題なく動作するようになっている。






2019年12月13日金曜日

table:filter(-'')について

GearSwapのtriggers.luaの43行目には以下のコードがある。

windower.register_event('outgoing text',
                                         function(original,modified,blocked,ffxi,extra_stuff,extra2)
    windower.debug('outgoing text')
    if gearswap_disabled then return modified end
   
    local splitline = windower.from_shift_jis(windower.convert_auto_trans(modified))
                              :gsub(' <wait %d+>',''):gsub('"(.-)"',
            function(str) return str:gsub(' ',string.char(7))
            end):split(' '):filter(-'')

最後の文「filter(-'')」は謎めいている。

なお、括弧の中の記号はマイナス、シングルクォーテーションx2である。
そして、関数filterはaddons\libs\functions.luaの406行目で定義されている関数で、引数で与えられた値に該当する要素だけを抜き出したテーブルを返す関数となっている。その名の通り一般的にフィルタ処理と呼ばれるものに該当する。

上記のコードはマイナス記号がなければsplitが返したテーブルから空文字だけを抜き出す処理になるが、コードの意図をくみ取れば本来はその逆であり、マイナス記号は「空文字はいらないよ」という宣言になっていると思われる。

このマイナス記号は結論から言うとaddons\libs\strings.luaの19行目にて定義されている。
debug.setmetatable('', {
    __index = function(str, k)
                    return string[k] or type(k) == 'number' and string.sub(str, k, k) or (_raw and _raw.error or error)('"%s" is not defined for strings':format(tostring(k)), 2)
                    end,
    __unm = functions.negate .. functions.equals,
    __unp = functions.equals,
})

上記のとおり文字列クラスに単項マイナス(__unm)をオーバーロードしている。__unmは「等しいかどうか(equals)」を実行した後、さらにその結果を「反転する(negate)」を実行する関数として定義されている。つまり、文字列に単項マイナス記号を付けると、文字列から一転して関数になってしまうのだ。

関数filterは引数として関数も受け取れるようにできている。すなわち、-''を引数として渡すと空文字の要素を排除して空文字ではない要素だけを抜き出したテーブルが返ってくる。

なお、関数の連結(..)は functions.lua の 236行目で __concat を定義して演算子をオーバーロードすることで実装している。
debug.setmetatable(functions.empty, {
    __index = index,
    __add = add,
    __sub = sub,
    __concat = functions.pipe,
    __unm = functions.negate,
    __class = 'Function'
})

2019年12月9日月曜日

windower.send_command のアットマークは非同期実行の意味

じょーやさんのPositionsというアドオンに

windower.send_command('@wait 0.5;pos turn')

というコードがあった。このアットマークはどんな意味?
waitなので予想としては非同期実行かな?



"windower.send_command Async"でググると答えがあった。

It executes the command asynchronously, meaning the command handler returns before the command is fully processed.

ようするに、アットマークを付けない場合はコマンド内容が完了するまで次のコードに進まないのに対して、アットマークを付けるとすぐに次のコードに制御が移るという感じ。

consoleに渡される文にwaitが入っていると、完了するまで時間がかかってしまう。それを待ってしまうとアドオンの処理が止まってしまう。それを防ぐためにアットマークを付けている。

なお、pos turnについては、このアドオンの実行名がposで、自分自身を実行している。
そのときの引数としてturnを渡している。

2018年12月25日火曜日

日本時間でのリセット設定


diggerアドオンを見ていたら以下のコードがあった。
local today = os.date('!%Y-%m-%d', os.time() + 32400)

32400秒は9時間であるためUTC+9つまり日本標準時JSTを指定している。なお、Luaのos.dateで!を使うとUTCで書式化される決まり。それを9時間ずらしてどのタイムゾーンでも日本時間の日付を取得できる。

たとえば、日本時間1月1日AM8時は、ニューヨークだと前日のPM6時。14時間の時差があり日付が異なる状態となる。でも、ニューヨークで稼働しているWindowerも新年1月1日の日付がtodayに格納される。


diggerアドオンは、このtodayの値をsettings(config)を使ってファイルに保存し、地球時間0時またぎのリセットが発生したかどうかを検出している。

res.itemsはnameエントリで言語差を吸収している


サメーさんのShopperアドオンの中身を見ていたら
function validate_item(npc_item)
 for i,v in pairs(res.items) do
  if v.name:lower() == npc_item then
   return v.id
  end
 end
end
こんなコードがあった。res.itemsはres\items.luaの中身が入っている。でも、その中にはnameというエントリは含まれていない。アイテムの名前はenやjaで登録されている。

v.nameとはいったい何だろうか。

addons\lib\resources.luaには以下のコードがある。
local language_string = _addon and _addon.language and _addon.language:lower() or windower.ffxi.get_info().language:lower()

local redict = {
 name = language_string,
 name_log = language_string_log,
 name_short = language_string_short,
 english = 'en',
 japanese = 'ja',
 english_log = 'enl',
 japanese_log = 'ja',
 english_short = 'ens',
 japanese_short = 'jas',
}

-- The metatable for a single resource item (an entry in a sub table of the root resource table)
local resource_entry_mt = {__index = function()
 return function(t, k)
  return redict[k] and t[redict[k]] or table[k]
 end
end()}

このコードから予想できるのは、windowerの言語設定がjapaneseである場合、nameはjaのエイリアスとして機能するということ。

つまり、
item['name'] = item[redict['name']] = item['japanese'] = item[redict['japanese']] = item['ja']


そして、Windowerのwikiにもそう書かれていた。
The general structure of the resources can be gleaned by checking out the respective Lua file in the resources directory. However, there are some deviations. For example, the Lua files use the ISO 639-1 language code for English (en), Japanese (ja), German (de), French (fr), however, in the parsed resources they will appear under their full english names (english, japanese, german and french). Also, a new field name will be added, which is an alias to whatever the addon's language is, or, if none is provided for the addon, what the user's POL language is.


2018年12月13日木曜日

true:fn()について

Windower4\libs\functions.luaの178行目に
function functions.loop(fn, interval, cond)
 ~
 cond = cond or true:fn()
 ~
というコードがある。「true:fn()」はその前後の文脈からすると、常にtrueを返す関数だと思われる。ここでfnは関数loopの引数のことではなく、23行目で定義されている関数となっている。

functions.luaの23行目
for _, t in pairs({functions, boolean, math, string, table}) do
    t.fn = function(val)
        return function()
            return val
        end
    end
end