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を渡している。