🔥 バルハラ推薦 🔥
本記事は実運用で効果を確認した内容のみ掲載しています
この記事はシリーズのSTEP外(自作アドオン解説編)です。
■ この記事でできること
- WSをトリガーに別キャラのWSを自動発動する
- 魔法をトリガーに後続の魔法やMBを自動発動する
- アビリティをトリガーに別キャラの行動をつなぐ
- 日本語名で設定しつつ、送信時は英語名へ変換する
👉 つまり、設定は日本語で書けて、実行はWindowerコマンド向けに英語名へ変換する構成です。
■ ws_linkとは?
ws_link は、WS・魔法・アビリティをトリガーにして、別キャラの行動を自動でつなぐ自作アドオンです。
たとえば、
- CharacterA が「花車」
- 3秒後に CharacterB が「サベッジブレード」
- さらに3秒後に CharacterC が「シャンデュシニュ」
- 最後に CharacterD が「サンダーV」
この流れを、自分で毎回手入力しなくても自動でつなげられます。
■ ここが重要:なぜ辞書Luaが必要なのか
ここがこのアドオンの肝です。
FF11では、設定ファイルに日本語名を書きたくても、実際に送信するコマンドは英語名の方が安定することがあります。
たとえば、設定ではこう書きたいです。
花車
サンダーV
震天動地の章
でも、送信コマンド側ではこうしたい場面があります。
/ws "Tachi: Kasha" <t>
/ma "Thunder V" <t>
/ja "Immanence" <me>
この差を吸収するのが dictionary.lua です。
つまり、日本語で設定して、実行時だけ英語名に変換するために辞書Luaが必要です。
■ ファイル構成
Windower/addons/ws_link/ws_link.lua
Windower/addons/ws_link/data/dictionary.lua
Windower/addons/ws_link/data/settings.lua
- ws_link.lua … 本体。トリガー判定、辞書変換、送信処理を担当
- dictionary.lua … 日本語名と英語名の対応表
- settings.lua … 実際のシーン設定
■ 導入手順
① フォルダを作る
Windower/addons/ws_link/
Windower/addons/ws_link/data/
② 3つのファイルを配置する
このあと載せる ws_link.lua、dictionary.lua、settings.lua をそれぞれ保存します。
③ 読み込む
//lua load ws_link
④ 設定変更後の再読み込み
//wsl reload
⑤ 状態確認
//wsl status
■ ws_link.lua(本体)
保存先:
Windower/addons/ws_link/ws_link.lua
_addon.name = 'ws_link'
_addon.author = 'sample'
_addon.version = '1.0.0'
_addon.commands = {'wsl', 'wslink'}
local windower = windower
local dict = nil
local scenes = nil
local state = {
enabled = true,
debug = false,
queue = {},
}
local action_category = {
spell_finish = 4,
ws_finish = 6,
ja_finish = 13,
}
local function log(msg)
windower.add_to_chat(207, ('[ws_link] %s'):format(msg))
end
local function dlog(msg)
if state.debug then
windower.add_to_chat(8, ('[ws_link:debug] %s'):format(msg))
end
end
local function safe_require_settings()
package.loaded['data.dictionary'] = nil
package.loaded['data.settings'] = nil
local ok1, res1 = pcall(require, 'data.dictionary')
if not ok1 then
log('dictionary.lua の読み込みに失敗しました')
log(tostring(res1))
return false
end
local ok2, res2 = pcall(require, 'data.settings')
if not ok2 then
log('settings.lua の読み込みに失敗しました')
log(tostring(res2))
return false
end
dict = res1 or {}
scenes = res2 or {}
if type(dict) ~= 'table' then
log('dictionary.lua がテーブルを返していません')
return false
end
if type(scenes) ~= 'table' then
log('settings.lua がテーブルを返していません')
return false
end
return true
end
local function normalize_text(s)
if not s then
return ''
end
s = tostring(s)
s = s:gsub('^%s+', ''):gsub('%s+$', '')
s = s:lower()
return s
end
local function build_alias_set(kind, names)
local result = {}
local function add_name(name)
if not name or name == '' then
return
end
result[normalize_text(name)] = true
end
if type(names) == 'string' then
add_name(names)
elseif type(names) == 'table' then
for _, name in ipairs(names) do
add_name(name)
end
end
local source_dict = {}
if kind == 'ws' then
source_dict = dict.ws or {}
elseif kind == 'ma' then
source_dict = dict.ma or {}
elseif kind == 'ja' then
source_dict = dict.ja or {}
end
local expanded = {}
for key, _ in pairs(result) do
expanded[key] = true
end
for jp_name, en_name in pairs(source_dict) do
local jp_norm = normalize_text(jp_name)
local en_norm = normalize_text(en_name)
if result[jp_norm] or result[en_norm] then
expanded[jp_norm] = true
expanded[en_norm] = true
end
end
return expanded
end
local function matches_name(kind, configured_names, actual_name)
if not actual_name or actual_name == '' then
return false
end
local aliases = build_alias_set(kind, configured_names)
return aliases[normalize_text(actual_name)] == true
end
local function find_scene_for_action(actor_name, act_kind, act_name)
if not scenes or not state.enabled then
return nil
end
for _, scene in ipairs(scenes) do
if scene.enabled ~= false and scene.trigger and type(scene.trigger) == 'table' then
local who_ok = true
if scene.trigger.who and scene.trigger.who ~= '' then
who_ok = normalize_text(scene.trigger.who) == normalize_text(actor_name)
end
if who_ok then
if act_kind == 'ws' and scene.trigger.ws then
if matches_name('ws', scene.trigger.ws, act_name) then
return scene
end
elseif act_kind == 'ma' and scene.trigger.ma then
if matches_name('ma', scene.trigger.ma, act_name) then
return scene
end
elseif act_kind == 'ja' and scene.trigger.ja then
if matches_name('ja', scene.trigger.ja, act_name) then
return scene
end
end
end
end
end
return nil
end
local function get_first_name(data)
if not data then
return nil
end
if type(data) == 'string' then
return data
end
if type(data) == 'table' then
for _, v in ipairs(data) do
if type(v) == 'string' and v ~= '' then
return v
end
end
end
return nil
end
local function resolve_command_name(kind, names)
local source_dict = {}
if kind == 'ws' then
source_dict = dict.ws or {}
elseif kind == 'ma' then
source_dict = dict.ma or {}
elseif kind == 'ja' then
source_dict = dict.ja or {}
end
local primary = get_first_name(names)
if not primary then
return nil
end
if source_dict[primary] then
return source_dict[primary]
end
for jp_name, en_name in pairs(source_dict) do
if normalize_text(primary) == normalize_text(en_name) then
return en_name
end
if normalize_text(primary) == normalize_text(jp_name) then
return en_name
end
end
return primary
end
local function build_send_command(step)
if type(step) ~= 'table' then
return nil, 'step が不正です'
end
local who = step.who
local kind = step.kind
local target = step.target or '<t>'
if not who or who == '' then
return nil, 'step.who がありません'
end
if not kind or kind == '' then
return nil, 'step.kind がありません'
end
if kind == 'ws' then
local name = resolve_command_name('ws', step.ws)
if not name then
return nil, 'WS名がありません'
end
return ('send %s /ws "%s" %s'):format(who, name, target)
elseif kind == 'ma' then
local name = resolve_command_name('ma', step.ma)
if not name then
return nil, '魔法名がありません'
end
return ('send %s /ma "%s" %s'):format(who, name, target)
elseif kind == 'ja' then
local name = resolve_command_name('ja', step.ja)
if not name then
return nil, 'アビ名がありません'
end
return ('send %s /ja "%s" %s'):format(who, name, target)
end
return nil, ('未対応 kind: %s'):format(tostring(kind))
end
local function schedule_step(scene_name, index, step, delay_base)
local delay = tonumber(step.delay or 0) or 0
local cmd, err = build_send_command(step)
if not cmd then
log(('step %d のコマンド生成失敗: %s'):format(index, err or 'unknown'))
return
end
local execute_at = os.clock() + delay_base + delay
table.insert(state.queue, {
execute_at = execute_at,
cmd = cmd,
scene_name = scene_name or 'unnamed',
step_index = index,
})
dlog(('queue: [%s] step=%d delay=%.2f cmd=%s'):format(scene_name or 'unnamed', index, delay_base + delay, cmd))
end
local function enqueue_scene(scene)
if not scene or type(scene.steps) ~= 'table' then
return
end
local accumulated = 0
for i, step in ipairs(scene.steps) do
local delay = tonumber(step.delay or 0) or 0
accumulated = accumulated + delay
schedule_step(scene.name, i, step, accumulated - delay)
end
log(('scene発動: %s'):format(scene.name or 'unnamed'))
end
local function process_queue()
if #state.queue == 0 then
return
end
local now = os.clock()
local remain = {}
for _, item in ipairs(state.queue) do
if now >= item.execute_at then
dlog(('exec: [%s] step=%d'):format(item.scene_name, item.step_index))
windower.send_command(item.cmd)
else
table.insert(remain, item)
end
end
state.queue = remain
end
local function get_action_name(act)
if not act or not act.param then
return nil, nil
end
local category = act.category
local param = act.param
if category == action_category.ws_finish then
local res = windower.ffxi.get_abilities().weapon_skills[param]
if res and res.en then
return 'ws', res.en
end
elseif category == action_category.spell_finish then
local res = windower.ffxi.get_spells()[param]
if res and res.en then
return 'ma', res.en
end
elseif category == action_category.ja_finish then
local res = windower.ffxi.get_abilities().job_abilities[param]
if res and res.en then
return 'ja', res.en
end
end
return nil, nil
end
windower.register_event('load', function()
if safe_require_settings() then
log('loaded')
else
log('読み込み失敗')
end
end)
windower.register_event('unload', function()
state.queue = {}
end)
windower.register_event('prerender', function()
process_queue()
end)
windower.register_event('action', function(act)
if not state.enabled then
return
end
local actor = windower.ffxi.get_mob_by_id(act.actor_id)
if not actor or not actor.name then
return
end
local act_kind, act_name = get_action_name(act)
if not act_kind or not act_name then
return
end
dlog(('action: actor=%s kind=%s name=%s'):format(actor.name, act_kind, act_name))
local scene = find_scene_for_action(actor.name, act_kind, act_name)
if scene then
enqueue_scene(scene)
end
end)
windower.register_event('addon command', function(...)
local args = {...}
local cmd = args[1] and args[1]:lower() or 'status'
if cmd == 'on' then
state.enabled = true
log('ON')
return
end
if cmd == 'off' then
state.enabled = false
log('OFF')
return
end
if cmd == 'toggle' then
state.enabled = not state.enabled
log(state.enabled and 'ON' or 'OFF')
return
end
if cmd == 'debug' then
local v = args[2] and args[2]:lower() or ''
if v == 'on' then
state.debug = true
log('debug ON')
elseif v == 'off' then
state.debug = false
log('debug OFF')
else
log('使い方: //wsl debug on|off')
end
return
end
if cmd == 'reload' or cmd == 'r' then
if safe_require_settings() then
state.queue = {}
log('dictionary.lua / settings.lua を再読み込みしました')
else
log('再読み込み失敗')
end
return
end
if cmd == 'status' then
log(('status: enabled=%s debug=%s queue=%d'):format(
tostring(state.enabled),
tostring(state.debug),
#state.queue
))
return
end
if cmd == 'clear' then
state.queue = {}
log('queueを消去しました')
return
end
log('コマンド一覧: //wsl on | off | toggle | reload | debug on/off | status | clear')
end)
■ dictionary.lua(日本語辞書 / 英語辞書)
保存先:
Windower/addons/ws_link/data/dictionary.lua
このファイルが、日本語名と英語名の対応表です。
local dict = {}
dict.ws = {
['花車'] = 'Tachi: Kasha',
['月光'] = 'Tachi: Gekko',
['照破'] = 'Tachi: Shoha',
['十二之太刀・照破'] = 'Tachi: Shoha',
['サベッジブレード'] = 'Savage Blade',
['シャンデュシニュ'] = 'Chant du Cygne',
['ラストスタンド'] = 'Last Stand',
}
dict.ma = {
['ストーン'] = 'Stone',
['ストーンII'] = 'Stone II',
['ストーンIII'] = 'Stone III',
['ストーンIV'] = 'Stone IV',
['ストーンV'] = 'Stone V',
['ストーンVI'] = 'Stone VI',
['ウォータ'] = 'Water',
['ウォータII'] = 'Water II',
['ウォータIII'] = 'Water III',
['ウォータIV'] = 'Water IV',
['ウォータV'] = 'Water V',
['ウォータVI'] = 'Water VI',
['ブリザド'] = 'Blizzard',
['ブリザドII'] = 'Blizzard II',
['ブリザドIII'] = 'Blizzard III',
['ブリザドIV'] = 'Blizzard IV',
['ブリザドV'] = 'Blizzard V',
['ブリザドVI'] = 'Blizzard VI',
['ファイア'] = 'Fire',
['ファイアII'] = 'Fire II',
['ファイアIII'] = 'Fire III',
['ファイアIV'] = 'Fire IV',
['ファイアV'] = 'Fire V',
['ファイアVI'] = 'Fire VI',
['サンダー'] = 'Thunder',
['サンダーII'] = 'Thunder II',
['サンダーIII'] = 'Thunder III',
['サンダーIV'] = 'Thunder IV',
['サンダーV'] = 'Thunder V',
['サンダーVI'] = 'Thunder VI',
['エアロ'] = 'Aero',
['エアロII'] = 'Aero II',
['エアロIII'] = 'Aero III',
['エアロIV'] = 'Aero IV',
['エアロV'] = 'Aero V',
['エアロVI'] = 'Aero VI',
['雷門の計'] = 'Ionohelix',
['雷門の計II'] = 'Ionohelix II',
['水門の計'] = 'Hydrohelix',
['水門の計II'] = 'Hydrohelix II',
['氷門の計'] = 'Cryohelix',
['氷門の計II'] = 'Cryohelix II',
['火門の計'] = 'Pyrohelix',
['火門の計II'] = 'Pyrohelix II',
['風門の計'] = 'Anemohelix',
['風門の計II'] = 'Anemohelix II',
['光門の計'] = 'Luminohelix',
['光門の計II'] = 'Luminohelix II',
['闇門の計'] = 'Noctohelix',
['闇門の計II'] = 'Noctohelix II',
}
dict.ja = {
['震天動地の章'] = 'Immanence',
['連環計'] = 'Tabula Rasa',
}
return dict
■ settings.lua(実際の設定)
保存先:
Windower/addons/ws_link/data/settings.lua
ここは実際のトリガーと後続行動を書く場所です。読者はここを自分用に書き換えます。
local SCENES = {
{
name = '基本連携テスト_WS→WS→WS→MB',
enabled = true,
trigger = {
who = 'CharacterA',
ws = { '花車' },
},
steps = {
{
who = 'CharacterB',
kind = 'ws',
ws = { 'サベッジブレード' },
delay = 3.0,
target = '<t>',
},
{
who = 'CharacterC',
kind = 'ws',
ws = { 'シャンデュシニュ' },
delay = 3.0,
target = '<t>',
},
{
who = 'CharacterD',
kind = 'ma',
ma = { 'サンダーV' },
delay = 3.0,
target = '<t>',
},
},
},
{
name = 'ヘリックス起点_MBサンプル',
enabled = true,
trigger = {
who = 'CharacterA',
ma = { '雷門の計' },
},
steps = {
{
who = 'CharacterB',
kind = 'ma',
ma = { 'サンダーV' },
delay = 3.0,
target = '<t>',
},
{
who = 'CharacterC',
kind = 'ma',
ma = { 'サンダーV' },
delay = 3.0,
target = '<t>',
},
{
who = 'CharacterD',
kind = 'ma',
ma = { 'サンダーVI' },
delay = 3.0,
target = '<t>',
},
},
},
{
name = 'アビ起点サンプル',
enabled = false,
trigger = {
who = 'CharacterA',
ja = { '震天動地の章' },
},
steps = {
{
who = 'CharacterA',
kind = 'ma',
ma = { 'ストーン' },
delay = 1.0,
target = '<t>',
},
{
who = 'CharacterB',
kind = 'ma',
ma = { 'サンダーV' },
delay = 3.0,
target = '<t>',
},
},
},
}
return SCENES
■ settings.lua は日本語で書ける
この構成の利点は、settings.lua 側を日本語中心で書けることです。
たとえば、トリガー側はこう書けます。
ws = { '花車' }
ma = { '雷門の計' }
ja = { '震天動地の章' }
そして送信時だけ本体が辞書を見て、英語名へ変換します。
つまり、読者は日本語で管理しやすく、アドオンは英語コマンドで安定動作させる、という形です。
■ 使い方
settings.luaのCharacterA〜CharacterDを自分のキャラ名に変える- 必要なWS・魔法・アビの名前を日本語で設定する
- 辞書にないものは
dictionary.luaに追加する //wsl reloadで再読み込みする
■ よくあるミス
1. 辞書に登録していない
日本語名で書いたのに辞書に対応がないと、送信時に英語名へ変換できません。
2. キャラ名が一致していない
who は送信先キャラ名と完全一致させる必要があります。
3. settings.lua に書いた名前と辞書の表記がズレている
たとえば「サンダー5」と「サンダーV」は別物です。表記は統一してください。
4. 本体だけ置いて辞書を置いていない
今回の ws_link は、dictionary.lua 前提の構成です。本体だけでは日本語設定は成立しません。
■ まとめ
- ws_link は WS・魔法・アビをトリガーに別キャラの行動を自動でつなぐアドオン
- 日本語で設定するためには辞書Luaが必要
- 送信時は辞書を使って英語名へ変換するのがポイント
- 本体・辞書・設定を分けることで管理しやすくなる
👉 つまりこのアドオンは、「日本語で書ける設定」と「英語で安定実行する送信」を分けて作るのが正解です。
コメント