今回はWindowerアドオンからNPC会話をしてみたいと思います。
NPCとの会話を開始するには、outgoingのIDが0x01Aのパケットを使うようです。
("???"を調べる等も同じようです)
outgoing 0x01Aのパケットはlibs/packets/fields.luaを見ると以下のようになっています。
-- libs/packets/fields.lua enums['action'] = { [0x00] = 'NPC Interaction', [0x02] = 'Engage monster', [0x03] = 'Magic cast', [0x04] = 'Disengage', [0x05] = 'Call for Help', [0x07] = 'Weaponskill usage', [0x09] = 'Job ability usage', [0x0C] = 'Assist', [0x0D] = 'Reraise dialogue', [0x0E] = 'Cast Fishing Rod', [0x0F] = 'Switch target', [0x10] = 'Ranged attack', [0x12] = 'Dismount Chocobo', [0x14] = 'Zoning/Appear', -- I think, the resource for this is ambiguous. [0x19] = 'Monsterskill', [0x1A] = 'Mount', } -- Action fields.outgoing[0x01A] = L{ {ctype='unsigned int', label='Target', fn=id}, -- 04 {ctype='unsigned short', label='Target Index', fn=index}, -- 08 {ctype='unsigned short', label='Category', fn=e+{'action'}}, -- 0A {ctype='unsigned short', label='Param'}, -- 0C {ctype='unsigned short', label='_unknown1', const=0}, -- 0E {ctype='float', label='X Offset'}, -- 10 -- non-zero values only observed for geo spells cast using a repositioned subtarget {ctype='float', label='Z Offset'}, -- 14 {ctype='float', label='Y Offset'}, -- 18 }
Targetに対象NPCのID, Target Indexに対象NPCのIndexを入れ、CategoryにNPC Interaction(0x00)を指定してパケットを作ります。
Paramなどその他のパラメータは0となるようです。
packets = require('packets') local npc = {name = "hoge"} local npc_info = windower.ffxi.get_mob_by_name(npc.name) if npc_info then local p = packets.new('outgoing', 0x01A, { ["Target"] = npc_info.id, ["Target Index"] = npc_info.index, ["Category"] = 0x00, }) packets.inject(p) -- パケットを送信 end
会話を開始するとダイアログが表示されるNPCはincomingのIDが0x032(NPC Interaction Type 1)や0x034(NPC Interaction Type 2)のパケットがやってくるので、そのパケットを見て対応するoutgoingパケット(ID=0x05B:Dialogue optionsなど)を送ります。
(アドオンを作成する前に会話したいNPCがどのような挙動になっているのか確認すると良いと思います。)
なにかつくってみる
イオニスを付与してくれるNPCと会話して、イオニスを付けてもらいます。
処理ロジックは上位ミッションBFのトリガー交換アドオン"htmb"を参考にしました。
フロー
- outgoing 0x01Aのパケットで会話を開始する
- ダイアログが表示される会話であるincoming 0x034(NPC Interaction Type 2)のパケットがやってくる
- setkeyでEscキーを入力してダイアログをキャンセルする
- ダイアログをキャンセルした際に送信させるoutgoint 0x05Bのパケットの代わりにイオニス付与時のパケットを送信する
- イオニスが付与される
require('strings') require('logger') packets = require('packets') ionis_npcs = { [256] = {name = 'Fleuricette'}, -- 西アドゥリン [257] = {name = 'Quiri-Aliri'}, -- 東アドゥリン } local is_ionis_npc_busy = false function get_ionis_npc() local zone = windower.ffxi.get_info().zone local ionis_npc = ionis_npcs[zone] local npc = nil if ionis_npc then npc = windower.ffxi.get_mob_by_name(ionis_npc.name) else log('No ionis npc found!') return nil end if npc and math.sqrt(npc.distance) < 6 then return npc else log(ionis_npc.name..' found, but too far!') return nil end end function start_ionis() local npc = get_ionis_npc() if npc then local p = packets.new('outgoing', 0x01A, { ["Target"] = npc.id, ["Target Index"] = npc.index, ["Category"] = 0 }) packets.inject(p) is_ionis_npc_busy = true end end function incoming_ionis(id, data, modified, injected, blocked) if id == 0x034 then if is_ionis_npc_busy then local in_p = packets.parse('incoming', data) local npc = get_ionis_npc() if npc and npc.id == in_p["NPC"] then windower.send_command('wait 3;setkey escape;wait 0.5;setkey escape up;') end end end end function outgoing_ionis(id, data, modified, injected, blocked) if id == 0x05B then if is_ionis_npc_busy then local out_p = packets.parse('outgoing', data) local npc = get_ionis_npc() if npc and npc.id == out_p["Target"] then out_p["Option Index"] = 1 out_p["_unknown1"] = 0 is_ionis_npc_busy = false return packets.build(out_p) end end end end windower.register_event('addon command', function(...) local args = {...} if args[1] == 'ionis' then start_ionis() end end) windower.register_event('incoming chunk', incoming_ionis) windower.register_event('outgoing chunk', outgoing_ionis)
この他にも、NPCとの対話はギアスフェットのテンポラリ交換アドオン"temps"も参考になります。
今回の記事は10月頃に書いていてアップしようと思っていたのですが、丁度その時に"勲章"や"パルス管"のデュプリケイトの話題があり、本記事はデュプリケイトとは関係ないですがパケット関連の記事はタイミングが悪いかなと思い、公開せずそのままでした。
今年の記事は今年のうちにということで、公開してみます。良いお年を。