이 편에서 다룰 내용은

WoW API : "COMBAT_LOG_EVENT_UNFILTERED" 이벤트
WA : On Init 액션
입니다.


예제 6-4-1 : 소생의 안개 추적기

지난 시간에 잠깐 얘기가 나왔던 "COMBAT_LOG_EVENT_UNFILTERED"(이하 CLEU) 이벤트를 다루기 위한 예제입니다.

CLEU는 /전투기록 명령으로 기록되는 전투로그 내용을 전달하는 이벤트입니다. 내가 시전한 소생의 안개 오라의 획득 / 종료에 해당하는 메시지를 받으면 대상 공대원에게 걸린 소생의 안개 버프 지속시간을 목록에 기록해서 소생의 안개가 걸린 공대원의 숫자와 지속시간이 가장 짧은 오라의 지속시간을 표시하는(숫자가 언제 줄어들지 판단하도록) 표시기를 만들겠습니다.

WoW API - COMBAT_LOG_EVENT_UNFILTERED
CLEU는 데미지, 치유, 오라, 시전 등의 전투와 관계된 거의 모든 사건의 정보를 전달하는 이벤트입니다.
이벤트와 함께 전달되는 메시지는 최하 11개에서 최대 25개에 달합니다.
CLEU의 메시지는 "COMBAT_LOG_EVENT"와 정확히 같은 내용입니다. 단, "COMBAT_LOG_EVENT"가 전투기록 채팅창에서 설정한 필터에 맞는 이벤트에 대해서만(보통 내가 준 피해/치유 혹은 내가 받은 피해/치유) 발생하는 것과 달리 이런 필터링을 거치지 않은 모든 이벤트에 대한 것이라는 의미로 _UNFILTERED가 붙은 별개의 이벤트인 CLEU가 전달됩니다.


기본적인 11개의 메시지는 다음과 같습니다.
("COMBAT_LOG_EVENT_UNFILTERED",) timestamp, event, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags

timestamp : 이벤트의 발생 시각입니다. time() 형식이지만 time()으로는 1초 단위로만 시각을 얻을 수 있는 것과 달리 timestamp 메시지는 소수점 아래 3자리, 즉 밀리초 단위까지 시각이 표시됩니다.
event : CLEU 이벤트 중에서 어떤 분류에 해당하는지 알려줍니다. 진짜 이벤트와 혼동을 피하기 위해 message라고 부르기도 합니다. "SWING_DAMAGE"(자동 공격으로 피해 입힘), "SPELL_CAST_START"(주문 시전 시작), "SPELL_HEAL"(치유) 등등이 있습니다.
sourceGUID, sourceName, sourceFlags, sourceRaidFlags : 이 이벤트의 주체(공격, 치유, 주문을 시전한 유닛)에 대한 정보입니다.
destGUID, destName, destFlags, destRaidFlags : 이벤트의 대상에 대한 정보입니다.

GUID:
Global Unit ID. 서버에 존재하는 모든 유닛에 부여되는 고유 식별번호입니다. 모든 플레이어 캐릭터는 진영 변경을 하거나 서버 이전을 하기 전까지는 생성시 부여받은 GUID를 계속해서 사용합니다.
NPC 유닛은 서버 재시작 이후로 유닛이 생성될 때마다 +1씩 번호를 늘려가며 고유한 GUID를 부여받습니다.

/dump UnitGUID("target")
명령으로 주위 유닛들의 GUID를 확인할 수 있습니다.

자세한 내용은 생략하고, GUID는 동일한 유닛인지 확인하는 가장 강력한 방법입니다. 같은 이름을 가진 애드몹이 수백마리 있어도 GUID는 모두 다릅니다.

Flag:
NPC인지 플레이어인지, 파티원인지 아니면 공대원인지 등의 정보를 담은 식별용 16진수 코드입니다.
예를 들어 내 캐릭터의 Flag는 0x511(10진수로 1297)인데 이는 0x400 + 0x100 + 0x10 + 0x1이고 각각
'플레이어 유닛(NPC 아님)' , '플레이어가 조종' , '우호적' , '내가 조종' 을 의미합니다.

RaidFlag는 해골, 별 등의 징표 관련 정보를 담은 flag 코드입니다.


12번 이후의 메시지는 이벤트 유형에 따라 다릅니다.
주문 관련 이벤트라면 12 13 14번 메시지로 각각 주문id, 주문명, 주문속성이 전달되고 15번 이후로 자세한 정보가 더 이어집니다.
주문이 없는 이벤트라면 12번째부터 바로 추가 정보가 이어집니다.



WA - On Init Action
연재글 #5에서 소개한 바 있습니다.
커스텀 코드가 있는 표시기에서, 커스텀 코드에서 사용할 함수나 글로벌 변수 등을 미리 만드는 용도로 보통 사용합니다. 좀 심도있게 가면 트리거에 이벤트를 등록하는 대신 여기에 프레임을 만들어서 이벤트 핸들러를 생성할 수도 있습니다.

On Init 액션 코드는 function() - end 구조를 사용하지 않고 코드를 그냥 쓰면 됩니다.
이 코드는 그냥 대기상태로 있다가 표시기에 포함된 커스텀 함수가 실행되기 전에 미리 실행되며 한 번 실행되고 나면 커스텀 함수를 다시 실행할 때도 더 실행되지 않습니다.

다만, 표시기 설정을 변경하면 다시 실행되므로 이 점을 감안해야합니다. 예를 들어 이쪽에서 글로벌 변수를 선언하고 다른 코드에서 여기에 값을 저장하는 용도로 사용중이었다면, On Init 코드가 다시 실행되며 기존 값을 지우고 초기값으로 지정할 수도 있으므로 '이미 있다면 그 값을 그대로 사용'하라는 형식을 적절히 쓰길 권합니다. 이 때는 On Init 코드를 수정해도 '이미 존재하니' 안바뀔 수도 있으므로 '적절히' 사용해야합니다.


작업 구상은 이렇습니다.
- On Init 액션에서 자료를 저장할 테이블을 만들고 글로벌 경로를 지정하여 다른 커스텀 코드에서 접근할 수 있게 함
- 트리거에서 "GROUP_ROSTER_CHANGED" 이벤트 발생시 공대원 전체의 GUID 목록을 갱신
- 트리거에서 CLEU 이벤트 발생시 소생의 안개 버프 정보가 바뀌는 내용인지 판단하여 GUID로부터 어떤 공대원인지 정보를 가지고 해당 공대원의 버프 만료 시각을 다른 목록에 저장
- 지속시간 정보에서는 가장 빨리 끝나는 시간을 기준으로 지속시간 정보 생성
- 중첩 정보에서는 버프를 가진 공대원 숫자에 해당하는 정보 생성
목록을 갱신하는 기능은 On Init 코드에서 함수를 만들어두고 트리거쪽에서 이 함수를 실행


새로운 - 아이콘 표시기를 만들고 Actions 탭으로 갑니다.




On Init 항목의 개인추가 체크박스를 켜고 코드를 넣습니다.
WeakAuras.CustomValues = WeakAuras.CustomValues or {}
WeakAuras.CustomValues.RenewingMistTracker = WeakAuras.CustomValues.RenewingMistTracker or {}
local core = WeakAuras.CustomValues.RenewingMistTracker
core.Roster = core.Roster or {}
core.Auras = core.Auras or {}
core.Num = core.Num or 0
core.spellName = GetSpellInfo(115151)

function core.UpdateRoster()
wipe(core.Roster)
if IsInRaid() then
for i = 1, GetNumGroupMembers() do
local unit = "raid"..i
core.Roster[UnitGUID(unit)] = unit
end
for GUID, expires in pairs(core.Auras) do
if not core.Roster[GUID] then
core.Auras[GUID] = nil
end
end
else
wipe(core.Auras)
end
end

function core.UpdateUnit(GUID)
local unit = core.Roster[GUID]
if unit then
local _, _, _, _, _, _, expires = UnitBuff(unit, core.spellName, nil, "PLAYER")
core.Auras[GUID] = expires
end
end

function core.UpdateAuras()
local now, i, shortest = GetTime(), 0
for GUID, expires in pairs(core.Auras) do
if expires < now then
core.Auras[GUID] = nil
else
i = i + 1
shortest = shortest and min(shortest, expires) or expires
end
end
core.Num = i
core.Shortest = shortest
end

앞쪽은 데이터를 저장할 공간을 생성하는 내용입니다.
WeakAuras는 애드온에서 만드는 데이터 저장용 테이블인데 여기에 CustomValues라는 테이블을 다시 만들고 그 안에 RenewingMistTracker 테이블을 만듭니다. 이 테이블은 자주 접근할 것이니 core라는 로컬 변수를 연결합니다.
core 안에는 공대원들의 GUID목록인 Roster, 소생의 안개 버프의 지속시간 목록인 Auras 두 개의 테이블도 생성합니다.
"소생의 안개"라고 한글 타자를 치는 일을 피하기 위해 GetSpellInfo로 소생의 안개 주문명을 spellName이라는 키로 저장해둡니다.

UpdateRoster 함수는 공대원 명단에 변화가 생겼을 때 공대원 각각의 GUID를 얻어서 Roster 테이블에 저장합니다.
CLEU에서는 GUID와 이름만 알 수 있고 raid3인지 raid22인지는 알 수 없으므로 Roster[GUID] = unitID 형태로 접근하기 위한 테이블입니다.

UpdateUnit 함수는 전투로그의 GUID를 이용해서 해당 공대원의 UnitBuff 만료시각 결과를 Auras 테이블에 저장합니다.

UpdateAuras 함수는 Auras 테이블 전체를 뒤져서 몇 명에게 걸려있고, 가장 짧은 사람의 만료시각은 언제인지 정보를 얻어 각각 core.Num, core.Shortest라는 키로 저장해둡니다.

테이블을 만들 때는 WeakAuras.CustomValues = WeakAuras.CustomValues or {} 와 같이 써서 '이미 있다면 그것 그냥 사용, 없을 때만 {}' 으로 수행되게 하였는데 함수들은 전부 그냥 생성합니다. 이는 코드를 중간에 수정할 때 새로 지정한 코드가 덮어쓰게 하기 위해서입니다.



조건 탭으로 이동합니다.

개인추가 - 상태 - 이벤트로 설정하고
COMBAT_LOG_EVENT_UNFILTERED, GROUP_ROSTER_UPDATE, WA_RENEW_FORCEUPDATE 세 개의 이벤트를 등록합니다. 세 번째 이벤트는 WeakAuras.ScanEvents() 명령으로 생성할 커스텀 이벤트이고 CLEU가 놓친 것이 있을 때 강제로 갱신하기 위해 사용합니다.

개인추가 조건
function(event, ...)
local core = WeakAuras.CustomValues.RenewingMistTracker
if event == "COMBAT_LOG_EVENT_UNFILTERED" then
local timestamp, message, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags = ...
if sourceFlags == 1297 and (message == "SPELL_CAST_SUCCESS" or message == "SPELL_AURA_APPLIED" or message == "SPELL_AURA_REMOVED") then 
local spellID, spellName, spellSchool, auraType, amount = select(12, ...)
if spellName == core.spellName then
core.UpdateUnit(destGUID)
core.UpdateAuras()
end
end
elseif event == "GROUP_ROSTER_UPDATE" then
core.UpdateRoster()
core.UpdateAuras()
else
core.UpdateRoster()
core.UpdateAuras()
end
if core.Num ~= 0 then
return true
end
end

On Init에서 생성한 데이터 저장 테이블을 core라는 로컬 변수로 일단 연결해둡니다.
이벤트는 세 개가 있으며 CLEU에서 메시지 갯수가 가변적이기 때문에 함수의 입력인수는 event, ... 입니다.


if event == "COMBAT_LOG_EVENT_UNFILTERED" then
local timestamp, message, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags = ...

CLEU 이벤트라면 일단 최초 11개의 메시지부터 읽어옵니다. 만약 CLEU 이벤트 하나만을 등록했다면 첫 줄에서 아예
function(event, timestamp, message, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...)
로 11개의 메시지를 읽었을 것입니다.

저 중에 실제 사용할 메시지는 message(이벤트 유형), sourceFlags, destGUID 뿐이므로 실제는
local _, message, _, _, _, sourceFlags, _, destGUID = ...
이렇게 썼을 것이나 가이드 글이라 그냥 다 썼습니다.


앞서서 CLEU의 12번 이하 메시지는 이벤트 유형에 따라 달라진다고 했습니다. 그래서 일단 message로 필터링을 먼저 합니다.

if sourceFlags == 1297 and (message == "SPELL_CAST_SUCCESS" or message == "SPELL_AURA_APPLIED" or message == "SPELL_AURA_REMOVED") then 
local spellID, spellName, spellSchool, auraType, amount = select(12, ...)

나머지 필요한 메시지는 저렇게, 메시지로 필터링 한 후에 그에 맞도록 select를 이용해 12번 이후만 얻습니다.
(만약 첫 줄에서 function(event, timestamp,  이런 형태로 11개의 메시지를 얻은 다음 ...을 썼다면 select를 할 필요 없이 그냥 ...이 12번 메시지부터에 해당합니다.)

자, 이것은 가이드 글이므로 이런 교과서적 방법(기본 메시지 11개만 얻고 이벤트 유형으로 필터링해서 그에 맞는 나머지 메시지들을 얻는 방식)을 썼습니다. 뒤에서 제가 실제로 쓸 방법을 설명드리겠습니다.

sourceFlags == 1297이 '내 캐릭터가 시전자'라는 의미입니다(16진수 0x511 = 1297). "SPELL_CAST_SUCCESS"는 이미 소생이 있는 공대원에게 덮어쓸 때 사용하기 위해 포함되었고 "SPELL_AURA_APPLIED"와 "SPELL_AURA_REMOVED"가 각각 오라 획득/종료에 해당하는 CLEU 이벤트 유형입니다.

if spellName == core.spellName then

여기까지 필터링하면
- 내가 시전한 주문에 관련된 이벤트이고
- 주문시전 성공, 오라 획득, 오라 종료 중 하나이며
- 주문이나 오라 이름이 "소생의 안개"
입니다.
대상에게 오라가 생기든 덧씌우든 없어지든 했을 것이므로 
core.UpdateUnit(destGUID) 대상의 오라 정보를 갱신하고
core.UpdateAuras() 전체 공대원 오라 갯수와 최단시간을 새로 계산합니다


elseif event == "GROUP_ROSTER_UPDATE" then
core.UpdateRoster()
core.UpdateAuras()
공대원 목록에 변동이 생기면 공대 목록을 갱신하고, 그 과정에서 공대에서 빠진 사람의 오라 정보는 Auras테이블에서 삭제되므로 갯수와 최단시간을 새로 계산합니다.


else
core.UpdateRoster()
core.UpdateAuras()
나머지 이벤트에서는 강제 갱신합니다.
"WA_RENEW_FORCEUPDATE" 라는 커스텀 이벤트도 이쪽으로 옵니다만 트리거가 상태 유형이기 때문에 "PLAYER_ENTERING_WORLD" 이벤트가 발생할 때도 이쪽으로 오게 되며 WA 설정창을 닫을 때도 같은 이벤트가 발생한 것처럼 가정해서 이쪽으로 옵니다.

if core.Num ~= 0 then
return true
end
소생 걸린 사람 숫자가 0이 아니면 디스플레이를 표시



이 예제의 필터링 과정은
- event가 CLEU
  - sourceFlag and message
    - 주문명이 "소생의 안개"
의 과정을 거칩니다.

CLEU 이벤트를 사용할 때 신경쓸 점은 이게 무지하게 많이 발생하는 이벤트라는 점입니다.
많으면 초당 수백개씩 발생하기 때문에 까딱하면 매프레임 전 공대원 오라 현황을 체크하는 것보다 더 무겁게 될 수도 있습니다.
그래서 if문을 이용해 필터링할 때 최대한 경제적으로 필터링 되도록 순서를 고려해야 합니다.

이건 예제용이고 저는 CLEU를 안쓴 소생의 안개 추적기를 씁니다만 CLEU를 이용한다면 다음과 같이 구성했을 것입니다.
function(event, _, msg, _, _, _, srcFlag, _, destGUID, _, _, _, _, spellName)
local core = WeakAuras.CustomValues.RenewingMistTracker
if event == "COMBAT_LOG_EVENT_UNFILTERED" then
if spellName == core.spellName and
(message == "SPELL_CAST_SUCCESS" or message == "SPELL_AURA_APPLIED" or message == "SPELL_AURA_REMOVED") and
sourceFlags == 1297 then
-- 이하 실제 수행

이벤트가 CLEU가 아니면 나머지 메시지들이 없기 때문에 저 변수들은 전부 nil입니다.
CLEU라도 SPELL_ 계열의 메시지가 아니면 spellName은 다른 값이 됩니다.
그래도 == 비교하는데는 오류가 발생하지 않습니다(nil값을 가지고 사칙연산을 하거나 대소관계 비교를 하거나 문자열 축약을 해서는 안됩니다). 따라서 여기에서는 굳이 단계별로 인수를 받아올 필요가 없습니다.

if문은 (주문명) and (이벤트타입) and (시전자flag) 구조로 되어있습니다.
and 조건이면 맨 앞쪽 조건에서 최대한 많이 걸러질 수 있게 만들면 뒤쪽 조건은 비교할 필요가 없어집니다.
그래서 일단 "소생의 안개"가 아닌 것들을 걸러내고(여기에서 대부분 걸러집니다) 뒤에서 치유효과 때문에 발생하는 메시지들 걸러내고 마지막으로 내것이 아닌 것을 거릅니다.


Custom Untrigger에는
function()
local core = WeakAuras.CustomValues.RenewingMistTracker
if core.Num == 0 then
return true
end
end

숫자가 0이면 숨김



지속시간 정보
function()
local core = WeakAuras.CustomValues.RenewingMistTracker
return 20, core.Shortest or 0
end

지속시간 정보의 반환값은 '전체시간', '만료시각' 형식입니다.(연재글 #2 참고) 20초짜리 버프이고 최단시간인 것을 기준으로 지속시간 정보를 생성합니다.


중첩 정보
function()
local core = WeakAuras.CustomValues.RenewingMistTracker
return core.Num
end

소생 걸린 사람 숫자




디스플레이 탭으로 갑니다.



지속시간 정보 생성했으니 쿨다운 체크하면 시계방향 회전 애니메이션이 표시됩니다.
아이콘은 수동으로 지정하시고요, 텍스트 쪽에
   %c     %s
라고 넣습니다.

아이콘 표시기에는 텍스트를 하나만 넣을 수 있기 때문에 남은 시간과 중첩수를 다 표시하려면 좀 꼼수를 써야됩니다.
%c에서 남은 시간을 표시할 것이고 %s는 걸린 사람 수입니다.

텍스트는 아래 - 안쪽에 표시하게 하고

개인추가 텍스트는 매 프레임 업데이트(시간이 계속 가야되니까요)
글상자 function()
local core = WeakAuras.CustomValues.RenewingMistTracker
if core and core.Shortest then
if core.Shortest < GetTime() then
WeakAuras.ScanEvents("WA_RENEW_FORCEUPDATE")
else
return ("%.1f"):format(core.Shortest - GetTime()) .. "\n\n\n"
end
end
end

매 프레임 체크를 하는데 
if core.Shortest < GetTime() then
WeakAuras.ScanEvents("WA_RENEW_FORCEUPDATE")
이건 문제가 생길 때를 대비한 것입니다.

누가 멀리 나가서 오라가 없어져버리면 CLEU에서 이걸 못잡아내기 때문에 벌써 없어진 오라가 목록에는 계속 남아있을 수 있습니다. 그러므로 최단시각이 지났다면(즉, 벌써 없어졌어야 하는데 자료에만 남아있다면) 강제로 트리거를 실행해서 저걸 삭제하도록 합니다. UpdateAuras() 함수에서 삭제하게 됩니다.


Shortest 값이 정상적인 상황이면
else
return ("%.1f"):format(core.Shortest - GetTime()) .. "\n\n\n"

("%.1f"):format(core.Shortest - GetTime())
초단위 시간을 소수점 1자리 형식으로 표시합니다.
형식은 "%.1f" = 소수점 1자리, 표시할 값은 core.Shortest - GetTime() = 남은 시간
string.format("%.1f", core.Shortest - GetTime()) 이렇게 써도 됩니다.
[형식]:format([값]) 혹은 string.format([형식], [값])


.. "\n\n\n"
뒤에 줄바꿈을 세 개 붙입니다.


아까 텍스트 창에 '  %c       %s'라고 썼는데요, 이 결과로 다음과 같이 표시됩니다.



이런 귀찮은 짓을 해야하는 이유는 여러 줄을 쓸 때 가운데 정렬 형식으로 표시되기 때문입니다.
중첩수를 오른쪽 구석에 맞추려고 오른쪽 아래로 지정하면 남은 시간 길이가 더 길기 때문에 남은 시간의 오른쪽이 기준으로 잡혀서 이 변하면서 중첩수가 좌우로 흔들흔들합니다.

아무튼, 글자 크기와 줄바꿈 갯수, 텍스트 입력란의 공백 갯수를 잘 조절해서 원하는 위치에 표시되게 하시면 됩니다.

위 이미지에는 가운데에 9라는 숫자가 8~9초 사이 남았다고 표시하고 있는데 이건 Tukui에서 쿨다운 애니메이션에 숫자를 표시하는 기능 때문입니다. OmniCC를 쓰셔도 저렇게 될 것 같습니다.






예제 6-4-2 : 이동불가 상태 알림

전에 냥게에 어떤 분이 '이동 불가 상태가 되면 주인의 부름을 시전하라고 알려주는' 기능을 텔미웬으로 만들 수 있는가 물어보셨는데 그걸 보고 재미삼아 WA로 만들어두었던 것이 있어 제작 과정을 소개합니다.

알려진 모든 이동불가 효과 디버프 이름을 등록하고 그 중 하나 이상 걸리면 표시하게 만들 수도 있습니다. 그런데 좀 추천할 방법은 아니죠.

'이동 불가 상태'인지 여부를 즉시 확인하는 API 함수는 없습니다. 그러나 인터페이스 - 전투 - 제어불가 효과 정보 옵션을 켜면 내 캐릭터가 CC 효과를 얻을 때 화면 가운데에 디버프 아이콘과 제어불가 효과 종류 텍스트가 표시됩니다.
기본 인터페이스는 뭔가를 알고 있다는 의미죠.

지난번에 발동효과 테두리 알림 예제를 다룰 때도 '기본 인터페이스가 알고 있으니 우리도 알 수 있다'는 말씀을 드린 적이 있습니다.
이 예제에서 다루는 주된 내용은 기본 인터페이스의 기능을 역추적하는 과정입니다.


자, 우선
블리자드 프로그래머들이 한 땀 한 땀 작업한 인터페이스 Lua 소스 코드가 필요합니다.

WoW API - 기본 인터페이스 파일 추출하기
우리가 다운받아 설치하는 인터페이스는 '써드-파티 애드온'이라고도 부릅니다.
블리자드가 기본으로 제공하는 인터페이스도 써드파티 애드온과 마찬가지 원리로 제작되어 있는데요, 친절한 눈보라사는 원하는 유저들이 이걸 직접 확인할 수 있는 기능을 제공합니다.

와우를 종료하고 배틀넷 런처에서 설정 - 게임 설정 - 월드 오브 워크래프트로 이동합니다.


명령줄 인수 추가 체크박스를 켜고 -console 이라 입력한 다음 완료.
와우를 다시 실행합니다. (Cmd 창에서 와우 설치 폴더를 찾아간 다음 >wow -console 로 실행해도 됩니다.)

별다를 것이 없어보이나 캐릭터 선택창에서 esc키 아래, 1 옆의 ` 키를 누르면 콘솔 명령어 입력창이 열립니다.



>ExportInterfaceFiles code
대소문자 구분해서 입력하고 엔터.

와우 설치 폴더 아래에 BlizzardInterfaceCode 라는 폴더가 생성되고 여기에 기본 인터페이스 파일들이 저장됩니다.

여기에서는 쓰지 않을 것이나 기본 인터페이스에서 사용하는 이미지 파일들도 구경하려면
>ExportInterfaceFiles art
입력한 뒤 컴퓨터 건드리지 말고 5분 정도 기다리세요. 용량이 크고 시간이 오래 걸립니다.
이 이미지 파일들은 .blp 확장자인데 XNView라는 이미지 뷰어로 볼 수 있습니다.

(페이지 중간쯤에서 받으세요. Minimal이나 Standard 버전은 무료인 것을 확인했고 Extended는 모르겠네요.)

이제 와우를 끄고 명령줄 인수 추가 체크박스를 꺼도 됩니다.


위에서 말한대로 BlizzardInterfaceCode 폴더 안에 파일들이 생성되는데 양이 좀 됩니다. 원하는 것을 찾아가려면 어느 정도는 단서를 알아야 합니다.

매크로를 두 개 소개하겠습니다.

/run local name =GetMouseFocus():GetName() or GetMouseFocus():GetParent():GetName() ChatFrame1:AddMessage("frame name: " .. (name or "Unknown"))

/run local f=EnumerateFrames();while f do if ( f:IsVisible() and MouseIsOver(f) ) then print(f:GetName() or string.format("[Unnamed Frame: %s]", tostring(f))); end f=EnumerateFrames(f); end

마우스를 적당한 곳에 놓고 매크로 단축키를 누르면
- 위의 것은 현재 마우스와 상호작용하는(올리면 툴팁을 띄우거나 클릭이 가능한) 개체의 프레임 이름을 출력합니다.
- 아래 것은 마우스 상호작용하지 않는 개체에 대해서도 마우스 위치에 있는, 숨겨지지 않은 모든 개체의 이름을 출력합니다.

위쪽 매크로는 마우스 상호작용이라는 제한조건 외에도 여러 개체가 한 자리에 겹쳐 있을 때 가장 위쪽 것만 이름을 출력하고 아래쪽 매크로는 출력하는 갯수가 좀 많습니다.(제 기준으로 모든 프레임 갯수를 세면 1만개가 좀 넘고 숨겨지지 않은 것만 500개 정도입니다. 그러나 많은 수는 실제 위치가 지정되지 않은 프레임이라 마우스 위치 라는 조건으로 거르면10개 이내 정도가 출력됩니다.)


인터페이스 설정 - 전투로 가서 해당 기능을 켜고 끄는 체크박스에 마우스를 올리고 첫 매크로를 누릅니다.

저 체크박스 이름은 InterfaceOptionsCombatPanelLossOfControl 입니다.

제어불가 효과를 다루는 용어가 Loss of Control이군요. 분명히 저 체크박스가 연결된 부분에서 제어불가 효과 아이콘을 띄우는 기능을 담당할겁니다.
("제어 불가 효과 경보"라는 텍스트가 인터페이스 파일의 한글화 관련 부분에 있을 것이므로 저 텍스트를 추적해도 됩니다.)

다른 방법은 직접 제어불가 효과에 걸린 다음 표시되는 아이콘에서 단서를 얻는 것입니다. 이 때 화면에 표시되는 아이콘은 클릭이 불가능한 개체이므로 두 번째 매크로를 써야합니다.


아이콘이 떴을 때 매크로를 한 번 누르고 그럴듯한 프레임을 찾습니다. 모르겠으면 없어진 다음 다시 눌러서 비교합니다.

역시 LossOfControl이라는 용어를 발견할 수 있습니다.

NotePad++를 열고 Shift + Ctrl + F를 눌러 폴더에서 찾기를 켭니다.



lossofcontrol을 검색하는데 대상 폴더로 아까 추출한 BlizzardInterfaceCode를 선택합니다.

모두 찾기를 누르면



71군데에 등장하는데 그 중 상당수는 아까 본 설정창에 관련된 부분입니다. 실제 이 기능을 담당하는 것으로 보이는 파일은 FrameXML/ LossofControlFrame.lua
아무 라인이나 더블클릭하면 파일이 열리고 해당 위치로 이동합니다.

제어불가가 걸리면 표시하는데 이벤트가 관련되어있을 수 있으니 어떤 이벤트들을 사용하는지 찾아봅시다.
"LOSS_OF_CONTROL_UPDATE"와 "LOSS_OF_CONTROL_ADDED"가 등록되어 있습니다.
아래 있는 "CVAR_UPDATE"는 옵션창에서 설정값 바뀔 때 발생하는 이벤트입니다. 기능 사용/중지 하는데 쓰이겠지요.

"LOSS_OF_CONTROL_ADDED" 이벤트 아래에
local locType, spellID, text, iconTexture, startTime, timeRemaining, duration, lockoutSchool, priority, displayType = C_LossOfControl.GetEventInfo(ACTIVE_INDEX)
라는 코드가 보이고 _UPDATE 이벤트에서 LossOfControlFrame_UpdateDisplay라는 함수가 실행되는데 이걸 찾아가도 마찬가지입니다.

C_LossOfControl.GetEventInfo()가 제어불가 효과의 정보를 얻는 함수 같고 변수 이름을 보면 주문 아이디, 표시할 텍스트, 아이콘 경로, 지속시간 관련 정보 등을 얻을 수 있겠습니다.
ACTIVE_INDEX라는 값은 파일 처음에 1로 지정하고 계속 1인 상태로 사용됩니다.
여러 개의 제어불가 효과가 걸려있을 때 제일 중요한 것이 1번으로 정렬되니 그것만 표시되는 것 같습니다.

한 번 써봅시다.



고르그론드 돌망치 투기장 바로 북쪽 온천 지대에 얼회를 쓰는 몹이 있습니다.
저는 친구가 없어서 얘를 찾아갔으나 여러분은 친구 법사에게 깃 꽂으세요.

얼회 걸린 상태에서 /dump C_LossOfControl.GetEventInfo(1) 이라고 입력하고 엔터를 치니 위 이미지와 같습니다.
각각
1: 제어불가 효과 종류
2: 주문ID
3: 화면에 표시되는 텍스트
4: 아이콘 경로
5: 종료 시각
6: 남은 시간
7: 전체 지속시간
뭐 이렇겠네요.




그러니까, "LOSS_OF_CONTROL_ADDED" 이벤트가 발생하면 C_LossOfControl.GetEventInfo 목록을 뒤져서 "ROOT"가 있다면 이동불가 효과가 있는 것이니 표시
코드 뒤쪽에 보면 "LOSS_OF_CONTROL_UPDATE"에서 실행된 LossOfControlFrame_UpdateDisplay가 숨기는 기능도 담당하니 우리도 이 이벤트를 이용해서 찾아보고 뿌리묶기가 없으면 숨김.
이런 WA 표시기를 만들겠습니다.



새로운 - 아이콘

조건탭:
개인추가 - 이벤트
이벤트 : LOSS_OF_CONTROL_ADDED, LOSS_OF_CONTROL_UPDATE

개인추가 조건
function(event, ...)
if event == "LOSS_OF_CONTROL_ADDED" then
local index = ...
local locType = C_LossOfControl.GetEventInfo(index)
if locType and locType == "ROOT" then
return true
end
end
end

노가다를 통해 새 효과가 추가될 때 메시지로 몇 번째 인덱스로 추가되었는지 전달됨을 확인했습니다.
그래서 새로 추가된 제어불가 효과의 정보에서 표시되는 메시지가 "ROOT"이면 표시.


Custom Untrigger
function(event)
if event == "LOSS_OF_CONTROL_UPDATE" then
local num = C_LossOfControl.GetNumEvents()
while num > 0 do
local locType = C_LossOfControl.GetEventInfo(index)
if locType and locType == "ROOT" then
return false
end
num = num - 1
end
return true
end
end

while문을 통해 모든 제어불가 효과 정보에 대해 종류를 체크하는데 하나라도 "ROOT"이 걸리면 false를 반환, 즉 안숨깁니다.
while문이 끝날 때까지 return이 수행 안됐으면 ROOT이 없다는 뜻이니 true를 반환해서 숨김


지속시간 정보
function()
local num = C_LossOfControl.GetNumEvents()
while num > 0 do
local locType, _, text, icon, s, _, d = C_LossOfControl.GetEventInfo(num)
if locType and locType == "ROOT" then
return d, s+d
end
num = num - 1
end
end

뻔하구요, 이제와서 보니 잘못된 점은 뒤에서 앞으로 while문이 돌아가는데 앞에서 뒤로 돌렸어야합니다.


이름 정보
function()
local num = C_LossOfControl.GetNumEvents()
while num > 0 do
local locType, spellID = C_LossOfControl.GetEventInfo(num)
if locType and locType == "ROOT" then
local name = GetSpellInfo(spellID)
return name
end
num = num - 1
end
end

주문명입니다.


아이콘 정보
function()
local num = C_LossOfControl.GetNumEvents()
while num > 0 do
local locType, _, _, icon = C_LossOfControl.GetEventInfo(num)
if locType and locType == "ROOT" then
return icon
end
num = num - 1
end
end


디스플레이 탭에서 %n 표시하게 하시고요, 쿨다운 체크.

아이콘은 자동 아이콘 하면 디버프 아이콘이 뜰 것이고 저는 수동으로 주인의 부름으로 설정했습니다.
액션 탭에서 테두리 효과 추가.





=============================================================================

정리:


WoW API

COMBAT_LOG_EVENT_UNFILTERED는 전투 상황과 관련된 거의 모든 정보를 전달하는 이벤트입니다.
두 번째 메시지로 이벤트 유형이 전달되고 이에 따라 나머지 메시지들의 배치가 달라집니다.

콘솔 모드로 클라이언트를 실행해 블리자드 기본 인터페이스 코드를 추출할 수 있습니다.
기본 인터페이스의 기능을 참고하는 것이 도움이 될 때가 있습니다.


WeakAuras

On Init 액션에 대한 설명은 연재글 #5를 참고


=================================================================

예제에 소개하지 않은 소생의 안개 추적기:
davQwaGis4seLuJIqDkczveLKxHiuZIO6weLQDHs9levkdtb1XquwMkspJOuAAic5AiIyBki5BeLIXPIiCoevY6uqCpsQ9HiGdQIAHKeEifrMiIkUib1gvqQpsqgPkIQtsuSsI8svePzIiQBIiTtfPFssAOOewkbEksnvsPRsI(kfHZIisRfLK3suI5sru3fLO2lv(lf1GHdJKfRIWJvOjtHlR0MvuFwbgTI40uA1QiIEnIGMnkUnPy3O63Q0WvHLJWZPQPRQRtQ2oIQ(ofPXJsQZts06rjY7reO5JOs19vru2VSJmNwhTghTHJ2WP1rpQ7)RgTlRFh9SoF8TxUyrjfcHGDoyIZMfS81ulFaPw)ZkcIaf3iyIZMfS81ulFaPw)Zkcc2(jxIF9F4NS8KcHqiecbI91j4bXkK1SjBwHOHmFj0q9InXzZcw(AQLpGuR)zfbbB)KlXV(pi7bH2xRvZkwKOaB2bfMm3K5MmxrsHqiSCJKwUHJwN)e2bdwcNwh9OU)VAC0Z68X3E5IfLuiece7RtWdpH(M0YnCVJ2VmZS4A5VJwfQQvRQKsYcp0AD0pHDWGLWP1r)Q8yD0J6()QXrpQ7)RgZVkpwhT)PEDVJEj)YFVJMfK8zsQWcml8uTclqMPcz2eQkGKjhh9OU)VAihDcQ3E5o6zD(4BVCXIskecb7CWeNnly5RPw(asT(NveeHFYYtkecHqiei2xNGhoDy5btC2SGLVMA5di16FwrqW2p5s8R)tkecHLBK0YnC0J6()QzkLM1rpRZhF7LlwusHqiyNdM4SzblFn1YhqQ1)SIGi8twEsHqiecHaX(6e8GjoBwWYxtT8bKA9pRiiypE15FsHqiSCJKwUHJEu3)xnYmE5o6KsYrpozhjHoA9ddQFD0J6()QXrpRZhF7LlwusHqiyNdM4SzblFn1YhqQ1)SIGi8twEsHqiecHaX(6e8GjoBwWYxtT8bKA9pRiiypE15FiYD1HHtkecHLBK0YnC0J6()QX8eRX6Oh19)vJ7D0tw7GjVBkj5uhnZLYWP1rR7xZ8L8lH7eo6NIz5VtRJwJoZBDADV7D0(JD0P1r7T8bmRB6PU3rByNNTJ6mVkDAD0A0zERtR7DVJMGACDAD0A0zERtR7DVJw3VM9h7OtfoAIlZ606O1OZ8wNw37Eh9idL3706O9w(aM1rtQQtpPU3rR7xZJmuEVtfU3rpF5pTLLw3uYgkhTH1FWqPs6NAKe6D0HqqwpgcHqiiR5oAIDG9Y1QYJ1rBhVChnRvDE2VVB6Po6X7LX4Ak3PchTLBFZJxnhm7VgUPK5OnS(tsD(4Bj)6uHJMLUxnUPdZE4tD0u6)1DchT1Wrly5RPw(asT(NveeoAkdd7BVCkgZpHDWGLW706EhTPwJFIBkj5uhnf3QH6TxUtRJE2YT(joTo6XlZLqGBkzoA)ifZYA3uYC0umhtO0QYJ1rtXCmHIf6m(1rpEzUeQWnLmh94L5siKBkzoAkMJjudToF0rhcHaX(6e8WSoF8TxU4dI7aI17Lh8pfXlpySmpLOKcHqieyUJumbk(aMnOoi(G4oGy9(GSkCAqwf0q9tyFyffK9Gynu)e2h2a5w4urjfcHqiqSVobp4FkIpqIdIflwd1pHT3YftXhWSIcK4azIi3ovuqwfmwMNsusHqiSCJKcbhTUFntXCmHYPch9bX633rFIRoFCjNDKsLoAkMJjuUPd7OvPB6WoAw7MoSJE8YCjo7MsMJ2psXSQ0nLmhnX9P(1nDyhTHob1BVChT7DVJE8YCjCADtjrUHMKiBLnKjBgkse56KGCDQSLmYgEOCZYojr26EhnxxJwc7GblH3nLmhn14BVCVtRJ2YTVtRJ24AEu3)xnov4Oh19)vJJ2oh43p4xMD8fFQCLC7BsmuEVyfhmuQCjuisuqT6GcsvD6jvr4NS8KcHqGyFDcEsl3iPKyUJumbbKguhmXzZcw(AQLpGuR)zfbr4semjIL63HMGsZkwXqtqPzvipOWeNnly5RPw(asT(NveekeLKaszZq5FLhgu)kpiGu2JxD(lpiGu2(jxIF9V8GaszF5k9BqDyy5HHLh4wg5Hzkg)Musm3rkMGGLVMguheAF6JLHrg(8vmzKrUgkYitusm3rkMGq7R1QzLhuYTVk0NNLhKXldxaL1qEqO9Pp2rlfJLLOE7LlpOXYLhuYTVWS26FV8Gq7pvxJqex9dsxnNyj8b1jj0(ATAw5bLC7Rc95z5bz8YWfqznKheAF6JD0sXyzjQ3E5YdASC5bLC7lmRT(3lpi0(t11ieXv)G0vZjwcFsm3rkMWaIR(H8WFvEyq9BqDGe8KjpqcEYskjM7iftywNp(2lpOkx5Hb1VI9lZS8WYqD4xdrjfcHGFzMzZq5FLhgu)guh8lZmBgk)R8WG63ajoSmuh(1iPqiemDILzdI9lZmBgk)R8WG63ajqyy2KjkyCtkecHqieyUJumb(10G6Gq7R1QzflkPqiecHqWVmZShV68xEWVmZS9tUe)6)G6WWYdCltsHqiecHqsHqiecHW8LiOZTFWYdhuwcV4)Q8WG6xrbJBsHqiecHqiecm3rkMGz5MLBwUz5MLBwEyz9rqDqj3(QqFEwSo3(Ydcw(AQ8a3YipOGfK8zsQWcuikPqiecHqiecb7Cyz9r4NS8KcHqiecHqiecHqyaXv)qw052NLdQdlRpskecHqiecHqyz8BsHqiecHqiecHqimG4QFil6C7ZYb1bULjPqiecHqiecHLBKuiecHqiSCJKcHqiecHKcHqiecH5lrqNBF5HL1hblpCqzj8IhqC1pefmUjfcHqiecHqiyNdk52xywB9VxSo3(IcuCJWY6JajqGFnn8twEsHqiecHqiecHqi4xMz2JxD(huh8lZm7XRo)dK4azjfcHqiecHqiecHGFzMz7NCj(1)b1b)YmZ2p5s8R)duCJGglxSFzMz7NCj(1)YdlRpefUeHL1hjfcHqiecHqiSm(nPqiecHqiecHqiegqC1pKfDU9z5G6a3YKuiecHqiecHWYnskecHqiewUrsHqiecHqsHqiecHGP2Jv8FvEyq9ROKcHqiecb)YmZMHY)kpmO(nOoOXYfpmBYKh8lZmBgk)R8WG63GShgMnzIskecHLBK0YnskjbKoeb7aR)xcHj)YFXkesGQkzHzbQsRvybMvYcY5SwHvikjbKoeb7aR)xcHj)YFXkybjFMKkSaZ0SqytsMZKSmS6SwzuDQzt6KFEQqctokeLKDoiJxgUakRHyrbkUrqO9Pp2rlfJLLOE7LlwuqT6WPHFYYtkecH5lrWguhitEqO9NQRriIR(bPRMtSeEXIcg3KcHqiecbM7iftqNB)G6GcckRHc2STjfcHqiec)v5Hb1VYIo3(SCqD4j03KcHqy5gjfcHGashc9(0Je2JxScv5kpmO(vH8GQCLhgu)kkPqieeq6qeSdS(FjeM8l)fRq5uz0A(SsbNvikPqieeqk7lxPFdQdpH(M0YnskjbKoe69PhjShVyfQYfM8l)vipmRZhF7Ll2VmZYdl5x(lpWMnBrjfcHqiec25Ws(L)b1QdkesGQkzHzbQsRvybMvYcY5SwHveUeHL8l)dQvhuWcs(mjvybMPzHWMKmNjzzy1zTYO6uZM0j)8uHeMCue(jlpPqiecHqiecb7CqgVmCbuwdXIcuCJGq7tFSJwkgllr92lxSOGA1HtduCJGy(9d(LzM9LR0VIc)KLNuiecHqiecHqiecZxIGnOoqM8Gq7pvxJqex9dsxnNyj8IffmUjfcHqiecHqiecHqiec)v5Hb1VYIcckRHc2STSCqD4j03KcHqiecHqiecHqy5gjfcHqiecHqiecHGFzMhc9(0Je2JxScv5kpmO(vH8GQCLhgu)kkPqiecHqiecHqie8lZ8qeSdS(FjeM8l)fRq5uz0A(SsbNvikPqiecHqiecHqie8lZm7lxPFdQdpH(MuiecHqiecHqiec(LzMTFYL4x)huh4wMKcHqiecHqiewg)ANdId87helJxgUakRrGIBeeAF6JD0sXyzjQ3E5IffuRoCQOGOaf3i4xMz2xUs)g(jlpPqiecHqiecHqie8lZ8qO3NEKWE8IvOkx5Hb1VkKh4wgrjfcHqiecHqiecHGFzMhIsoXoW6)LqyYV8xScLtLrR5ZkfCwHOKcHqiecHqiecHqWVmZShV68pOomCsHqiecHqiecHqi4xMz2xUs)guhMPy8BsHqiecHqieclJFTZHL8l)dQvhuiKavvYcZcuLwRWcmRKfKZzTcRiqXnc(LzM9LR0VHFYYtkecHqiecHqiecH5lrWguhitEqO9NQRriIR(bPRMtSeEXIcg3KcHqiecHqiecHqiecH)Q8WG6xzrbbL1qbB2wwoOo8e6BsHqiecHqiecHqiSCJKcHqiecHqiecHqWu7XkEaXv)qusHqiecHqiecHqi4xMz2(jxIF9FqDGBzskecHqiecHqiecbv5kpmO(vSFzMLhgMnzIskecHqiecHqy5gjfcHqieclJFTZHL8l)dQvhuOCQmAnFwPGZkc)KLNuiecHqiecHaZDKIjOZTFqDGnB2HlrqHIKcHqiecHqieSZbDU9hYSLBiwbbL1qHOWpz5jfcHqiecHqiecHWFvEyq9RSOZTplhuhEc9nPqiecHqiecHLBKuiecHqiSCJKwUHi37EhTH1FWqPszgVChTm8FjMPgxtwMXl3BYuNWYyFvAwZLtcntS81ulFGgR)9oA)tnsc9S4A5VJwfQQvRQK6EhT3rFkBYypmBYKDYK9bZJKOthkYLSXrtE3uYirNsM7Doa