이 편에서 다룰 내용은

WoW API : Event, timer
Lua : 반복 구조(iterator)
WA : Custom event trigger
입니다.

이벤트로 만든 커스텀 트리거의 예제를 무엇으로 할까 고민이 꽤 길었는데 결과적으로 오늘 사용한 두 개 예제는 모두 가끔 쪽지로 의뢰받는 질문에 답변드리며 만들었던 것들입니다.


예제 6-3-1 : 퇴마술 발동 알림

성기사의 퇴마술은 자동 공격시 20% 확률로 재사용 대기시간이 초기화되며 이 때 단축바의 아이콘에 테두리(glow overlay)가 표시됩니다.
그런데 비슷한 부류의 발동 효과들이 버프를 획득하며 발동되는 것과 달리(급살!, 실탄 장전 등) 이 효과가 발동될 때는 겉으로 보기에는 아무 일도 없이 쿨이 초기화되며 테두리가 뜨는 것이 전부입니다.

자, 이제 WA 표시기를 만들기 전에 꽤 긴 사전지식 설명을 시작하겠습니다.

WoW API - EVENT
(서버) - ( 클라이언트 (코어) - (인터페이스) ) - (유저)

와우의 인터페이스는 우리가 명령을 입력하게 해주고, 반대로 와우 코어에서 받은 정보를 우리에게 보여주는 역할을 합니다. 블리자드가 만든 인터페이스라도 외부 애드온과 대단히 다르게 동작하는 것이 아닙니다. 그러니까 스킬바에 테두리가 뜬다면 우리도 그걸 알 수 있을겁니다.

와우 코어가 인터페이스에게 ‘야 지금 이러이러한 일이 생겼다’고 알리는 메시지가 바로 이벤트EVENT 입니다(귀족의 정원 뭐 이런 이벤트 말고요).
인터페이스의 상당 부분은 OnUpdate(매 프레임)로 동작하는데(이것 또한 넓은 개념으로 보면 ‘지금 다음 프레임 됐어. 할거 해’라고 알려주는 이벤트라 할 수 있습니다) 또 나머지 상당 부분은 굳이 매 프레임 체크를 할 필요가 없는 기능들이며 여기에는 이벤트가 사용됩니다.
실탄 장전 발동시 폭발 사격에 테두리 띄우는 기능 같은 것이 예인데 이것 때문에 스킬바를 다루는 인터페이스가 매 프레임마다 실탄 장전 버프가 있는지 체크하지는 않습니다(물론 버프창을 다루는 인터페이스는 아이콘 띄워야 하니 매 프레임마다 체크합니다). 실탄 장전이 발동되면 와우 클라이언트에서 인터페이스 쪽으로 ‘폭발 사격 스킬창 만들어놓은 거 있으면 거기 테두리 띄워라. 아, 발동 되면서 쿨도 바뀌었어’ 이런 정보를 이벤트 형식으로 주고 해당 기능을 담당하는 인터페이스는 그제서야 일을 시작합니다.

와우 클라이언트가 쏘아주는(이벤트를 발생시킨다고 표현할 때 동사로 ‘fire’를 씁니다. 냅 fire) 이벤트의 목록은 다음 링크에 정리되어 있습니다.


몇 가지 예를 들자면
“PLAYER_REGEN_DISABLED” : 비전투중 리젠이 중단됨 = 전투 상태 시작
“CHAT_MSG_CHANNEL” : 대화채널에 메시지 등장. 글쓴이에 대한 정보와 메시지 내용 또한 이 이벤트 뒤에 첨부되어 같이 전달됩니다. 채팅창에 글씨가 나오는 것은 인터페이스가 이 이벤트에 포함된 메시지를 받고 그 내용을 띄우는 것입니다.
“COMBAT_LOG_EVENT_UNFILTERED” : 줄여서 CLEU. 거의 모든 전투행동(버프 획득, 피해 입힘, 치유 등등)의 내역을 이벤트 뒤에 붙는 메시지로 인터페이스에 전달합니다. 미터기 애드온들은 이걸 전부 분석해서 정리하는 역할을 합니다. 기본 채팅창에서 전투기록을 보는 것은 “COMBAT_LOG_EVENT” 이벤트의 목록이며 이것은 필터에 따라 표시할 내용에 대해서만 발생합니다.
“GROUP_ROSTER_UPDATE” : 파티나 공대에 참여할 때, 파티장이 바뀔 때, 다른 공대원이 파티에 들어오거나 나갈 때 파티 구성에 변화가 있다고 알려줍니다.
“UNIT_AURA” : 어떤 유닛이(이벤트 뒤에 메시지로 대상을 알려줍니다) 버프나 디버프를 새로 얻거나, 없어지거나, 지속시간이 변하거나 중첩이 바뀔 때 발생됩니다.
“UNIT_SPELLCAST_SUCCEEDED” : 어떤 유닛이 주문 시전을 완료할 때(시전시간이 있든, 즉시시전이든) 발생합니다.

큰 따옴표로 둘러쌌습니다. 이벤트 이름들은 모두 스트링값이기 때문입니다.
원래 이런 이벤트 정보를 전달받으려면 Frame이라고 하는 개체를 만든 다음 ‘이 프레임은 XXXX라는 이벤트를 주시오’라고 등록해야 합니다. 그러면 인터페이스에서 이벤트들마다 어디어디에 배달해야 하는지 목록을 작성해두었다가 해당 이벤트 발생시 등록된 프레임들 각각에 지정된 함수들을 실행합니다.
WA는 이런 절차를 배후에서 처리해줍니다.



WA - Custom trigger
WA의 트리거는(조건이라 번역되었으나 이 부분에서는 계속 트리거라 말하겠습니다)
특정한 조건이 충족될 때 표시기를 보여주고 불필요해지면 숨깁니다
진행바의 지속시간, 표시할 아이콘, 오라일 때에 중첩 회수 등의 데이터를 생성합니다.

1번 부분에서 조건을 어떻게 판단할 것인가를 두고 트리거가 크게 네 가지로 나뉩니다.
오라, 상태, 이벤트, 커스텀(개인추가로 번역되었습니다. 이 부분에서는 계속 커스텀이라 말하겠습니다)

오라는 버프/디버프의 상태를 추적하는 기능으로 가장 자주 쓰이기 때문에 따로 분류되어 있고 기능이 제법 강력합니다.
상태는 매 프레임마다 체크할 수 있는 조건을 추적합니다. 오라는 엄밀히 말해 상태에 속하는 것이나 위에서 말했듯 살림이 커서 분가했습니다.
이벤트는 와우 event 혹은 WA가 전용으로 생성하는 이벤트들에 기초해서 발생한 시점에 조건을 판단합니다. EAM에 쿨이 돌아온 아이콘이 잠깐 표시되는 기능을 구현하고 싶다면 상태 타입처럼 ‘지금 쿨이 있나 없나’만 체크하는 것으로는 불충분합니다. 정확히 쿨이 돌아온 시점에 애니메이션을 시작해야지요.
커스텀은 다시 상태와 이벤트로 나뉩니다. 표시할지 말지를 판단하는 코드를 직접 작성하는데 이 코드는 매 프레임 혹은, 등록한 이벤트가 발생할 때 수행됩니다.

우리는 커스텀, 구체적으로는 커스텀 - 이벤트 유형으로 갑니다.

WA 표시기를 보여주고 숨기는 기능은 custom trigger와 custom untrigger에서 담당합니다.
각각 function() - end 안에 코드가 쓰인 함수를 써넣습니다.
매 프레임, 혹은 등록된 이벤트가 발생할 때 우선 트리거 함수부터 수행합니다. 결과가 true라면 디스플레이를 표시하고 지속시간, 중첩 등등의 정보를 만드는 코드도 실행합니다.
표시된 상태에서 또 트리거 코드를 실행한 결과가 true가 아니면 이 때에 한해 untrigger를 수행합니다. 여기에서 결과가 true라면 디스플레이를 숨깁니다. 이쪽의 결과가 true가 아니면 아무 일도 일어나지 않습니다.
표시된 상태에서 트리거 코드가 또 수행됐는데 언트리거가 true가 아니어서 숨겨지지 않는다면 지속시간, 중첩 등등의 코드를 다시 수행합니다.



다시 본론으로 돌아와서, 이제 우리는 기본 스킬창에서 퇴마술 아이콘에 번쩍번쩍 테두리를 표시하라고 알려주는 이벤트가 발생하면 아이콘을 보여주고, 숨기라는 이벤트가 발생하면 아이콘을 숨기는 기능을 만들 것입니다.

아이콘, 텍스쳐 아무거나 발동되었을 때 표시할 개체를 만듭니다. 저는 아이콘을 선택했습니다.
조건(트리거) 탭으로 이동합니다.



유형은 개인추가(커스텀)
이벤트 유형은 이벤트 (이 부분이 이벤트와 상태로 분류된 것은 사실 큰 의미가 없다고 보는데 초기 버전의 동작 방식 때문에 분류되어 있는 것으로 압니다. 상태 타입이라도 트리거 체크를 수행할 조건이 매프레임 / 이벤트기반으로 다시 나뉩니다.)

이제 표시 여부를 판단할 이벤트를 등록해야합니다.

이 기능에 관련된 이벤트는 "SPELL_ACTIVATION_OVERLAY_GLOW_SHOW" 와 "SPELL_ACTIVATION_OVERLAY_GLOW_HIDE" 입니다.


각각 이벤트 뒤에 주문id가 따라붙습니다. 기본 인터페이스에서는 스킬 단축 아이콘 60개에 모두 저 이벤트가 등록되어서 이벤트가 발생했을 때 자기 자리에 있는 주문의 id와 같으면 그 때 테두리를 표시하는 방식으로 동작하는 겁니다.

입력칸에 두 가지를 모두 적어넣습니다. 쉼표로 구분해도 되고 단순히 스페이스 바로 구분해도 됩니다. 여기에는 큰 따옴표가 필요 없습니다.
둘 중 하나는 표시 트리거에 쓸거고 나머지는 숨김 트리거에 쓸건데 저렇게 해두면 둘 중 어떤 것이 발생하든 트리거부터 체크하고 true가 안나오면 같은 이벤트로 언트리거를 체크합니다.

개인추가 조건 텍스트 편집창에 다음 코드를 넣습니다.

function(event, arg1)
if event == "SPELL_ACTIVATION_OVERLAY_GLOW_SHOW" and arg1 == 879 then
return true
end
end

커스텀 트리거가 이벤트 기반으로 돌아가면 트리거 함수는 이벤트와 그 메시지를 입력 파라미터로 전달받습니다.
우리가 쓰는 두 개 이벤트들은 모두 뒤에 주문 아이디가 붙으니 이벤트와 주문아이디를 각각 event, arg1이라는 이름의 인수로 잡아내게 합니다. 노파심에 말씀드리지만 변수명은 아무렇게나 쓰셔도 위치만 맞으면 됩니다. 각각 E, A로 써도 되고 x, y로 써도 되고 아무튼 코드 안에서만 그 변수가 판단에 잘 사용되면 됩니다.

코드는 매우 간단하지요. ‘주문 활성화 오버레이 글로우 표시’ 이벤트가 발생했고, 그 때 주문 id가 879(퇴마술)이면 return true. 트리거에서 true를 반환하면 표시기가 보여집니다.
이벤트에 큰 따옴표를 씌워 스트링 형태로 비교한 것에 주의하세요.


숨김 조건은 숨길 때 적당한 이벤트가 발생하면 개인 추가(커스텀) / 그냥 정해진 시간만큼만 표시하려면 timed를 고릅니다. 여기에서는 XXX_HIDE 이벤트를 쓰니까 커스텀.
Custom Untrigger 편집창에 들어갈 코드는 다음과 같습니다.

function(event, arg1)
if event == "SPELL_ACTIVATION_OVERLAY_GLOW_HIDE" and arg1 == 879 then
return true
end
end

마찬가지입니다. 숨김 이벤트일 때 퇴마술 아이콘에 대한 거라면 return true.

표시기에 사용할 아이콘이 자동으로 설정되는 것은 프리셋에서나 그렇고 우리는 어떤 아이콘인지 정보를 아이콘 정보 코드에서 생성하거나 디스플레이 탭에서 수동으로 선택해야 합니다.
수동으로 선택하려면 excorcism을 검색해서 퇴마술 아이콘을 고르면 되고 트리거 탭에서 만들려면 아이콘 정보 편집창에 다음과 같이 입력합니다.

function()
return select(3, GetSpellInfo(879))
end

아이콘 정보 코드에서는 사용할 아이콘의 이미지 파일 경로를 반환해야 합니다. 주문 아이콘의 이미지 파일 경로는 GetSpellInfo()의 세 번째 반환값입니다.




자, 마지막으로 아이콘이 뜰 때 테두리 글로우도 같이 표시하고 싶습니다. 액션 탭에 해당 기능이 있습니다.



이 테두리 표시 애니메이션은 아이콘이 표시될 때만 표시하고 사라질 때 같이 숨기게 할겁니다(자원 절약).
보여짐 쪽에서 Button Glow - 보이기
숨겨짐 쪽에서 Button Glow - 숨김

각각 아래쪽에서 어떤 개체에 테두리를 표시할지 선택하게 되어있습니다.
(즉, 다른 WA 표시기에 테두리를 두르거나 WA랑 상관 없는, 예를 들어 미니맵에 테두리를 두를 수도 있습니다. 이렇게 만든 이유는 테두리 표시 기능이 표시기가 등장하고 사라질 때에만 On/Off할 수 있게 되어 있기 때문으로 냥꾼의 예를 들면 폭발 사격 쿨다운 추적기를 만들었다면 실탄 장전 때 테두리를 표시하려면 실탄 장전 버프를 추적하는 개체를 따로 만들어서 여기에서 폭발 사격 아이콘에 테두리를 띄우게 만드는 겁니다.)
아무튼, 선택 버튼 누르고 새로 만든 표시기를 눌러줍니다.

끝.



예제 6-3-2 : 죽음의 표적

블랙핸드가 죽음의 표적을 시전했을 때, 아이콘을 띄운 다음 대상자가 누구누구인지 표시하는 기능을 만들겠습니다. (DBM에도 뜨지만 왜인지 의뢰가 들어왔습니다.)

이 기능에서 대상자를 잡아내는 방법으로는 공대원 중 디버프가 걸려있는 사람을 찾아내는 것이 제일 확실합니다. 프리셋으로도 이렇게 만들 수 있고요.
하지만 이걸 하자고 매 프레임, 혹은 “UNIT_AURA” 이벤트 기반으로 전 공대원의 오라 상황을 체크하는건 구현하고자 하는 기능에 비해 자원 소모가 너무 큽니다.

반면 ‘블랙핸드가 죽음의 표적을 시전했을 때’라는 조건은 매우 경제적으로 만들 수 있습니다. “UNIT_SPELLCAST_SUCCEEDED”라는 이벤트가 누가 무슨 주문 시전에 성공했다는 정보를 주니 여기에서 시전자가 “boss1”이며 주문id가 죽표에 해당하는 것인 때를 체크하면 됩니다.
그런데 주문을 시전한 순간에는 대상자에게 디버프가 걸려있지 않다는 것이 문제입니다. 이 기능을 의뢰받고 WOL에서  전투로그를 뒤져본 결과 블랙핸드 주문 시전에서 디버프 획득 까지는 0.03~0.1초 정도 간격이 있었습니다. DBM의 블랙핸드쪽 기능을 살펴보니 0.5초 후에 공대원 오라 체크를 수행하게 되어있었기 때문에 저도 이 방법을 사용하겠습니다.

결과적으로는 가볍게 만들려고 구조는 복잡해지게 되어 좀 갑작스럽게 고급 기법을 다루게 되었습니다.

WoW API - C_Timer
많은 애드온들이 특정한 이벤트로부터 ‘xx.xx초 뒤에’ 어떤 기능을 수행하도록 짜여있었습니다. 이 예제와 같은 상황이라든가, 주문 시전 순간에는 쿨다운 정보가 즉시 생성되지 않는 문제 등등에 의한 것입니다.
이걸 하려면 매 프레임마다 시간만 체크해서 시간 되면 알려주는 부분을 따로 짜야 되는데 꽤 귀찮거든요. 그래서 이런 기능을 담당하는 라이브러리(애드온이 쓰는 애드온이라고 보시면 됩니다)인 Ace Timer라는 것이 널리 쓰였지요.
블리자드가 이제서야 개발자들을 긍휼히 여기사 6.02에서 이 기능을 와우에 넣었으니 이것이 C_Timer입니다. 

사용법은
C_Timer.After(duration, callback)
duration은 지금부터 x초 후 (number)
callback은 시간이 되면 수행할 함수입니다 (function)

일정 시간 간격으로 계속 반복하려면
Ticker = C_Timer.NewTicker(duration, callback, iterations)
Ticker는 시간을 잴 개체의 글로벌 이름입니다. 반복하는 기능이라면 중간에 중단해야할 필요가 생기기 때문에 반복형 타이머는 위 명령으로 개체를 만들고 그 개체가 기능을 수행하다가 원할 때 Ticker:Cancel() 명령으로 중단할 수 있습니다.
iteration은 반복할 회수. 값을 넣지 않으면 무한 반복. (number)


WA - WeakAuras.ScanEvents(“EVENT”)
이 부분은 연재글 #4 에서 다룬 것입니다.

WA에는 와우의 기본 이벤트와 유사하게 애드온내에서 사용되는 가짜 이벤트들을 만드는 기능이 있고 이런 이벤트는 트리거의 체크할 이벤트 목록에 등록해서 진짜 이벤트와 같이 사용할 수 있습니다.

이벤트를 fire 하려면
WeakAuras.ScanEvents(“MY_CUSTOM_EVENT” [, arg1, arg2, …])
와 같은 명령을 실행하면 됩니다.
이벤트 이름은 다른 이벤트와 겹치지 않게 적당히 지으시고 원한다면 메시지들도 1개 이상 넣을 수 있습니다.

자세한 내용은 연재글 4번을 참고하세요.


이제 시작합시다.



아이콘 표시기를 만듭니다.
조건 탭에서 개인추가 - 이벤트
이벤트 이름은 일단 UNIT_SPELLCAST_SUCCEEDED를 넣고 커스텀 이벤트를 쓸 것이니 적당한 이름으로 하나 더 넣습니다. 저는 WA_CHECK_MFD라고 넣었습니다.

개인추가 조건은 다음과 같이

function(event, ...)
if event == "UNIT_SPELLCAST_SUCCEEDED" then
local unit, _, _, _, spellid = ...
if unit == "boss1" and spellid == 156096 then
C_Timer.After(0.2, function()
WeakAuras.ScanEvents("WA_CHECK_MFD")
end)
end
elseif event == "WA_CHECK_MFD" then
return true
end
end


코드 설명하기 전에 작동하는 알고리즘부터 개괄적으로 설명드리겠습니다.

시작하는 이벤트는 “UNIT_SPELLCAST_SUCCEEDED”입니다.
이 때에는 “boss1”이 156096번 주문을 시전한거라면 0.2초 후에 “WA_CHECK_MFD”라는 이벤트를 fire합니다.
커스텀 이벤트가 발생하면 다시 트리거가 실행되고 이 이벤트라는 조건으로 분류되면 실제 아이콘을 표시합니다.
즉, 보스가 주문을 쓰면 0.2초 후에 아이콘이 표시되면서 다른 커스텀 함수쪽에서 누구누구한테 디버프가 있는지 체크하게 만든 것입니다.

첫 줄, 함수에서 사용할 인수를 보겠습니다.
이벤트는 아까도 썼던 것인데 두 번째 인수로 … 이라는 것이 들어가 있습니다.(마침표*3)
함수가 받는 입력 인수의 갯수가 고정되지 않고 유동적일 때 다루는 방법입니다.
event는 확실히 있을 것인데 이 때 이벤트가 “UNIT_SPELLCAST_SUCCEEDED”라면 5개의 메시지를 추가로 전달받습니다. “WA_CHECK_MFD”라면 메시지가 더는 없고요.

따라서 일단 두 번째 이후의 인수들은 … 이라는 저장공간에 넣어두었다가 이벤트가 “UNIT_SPELLCAST_SUCCEEDED”라면 있는만큼 꺼내 쓸겁니다.
세 번째 줄의
local unit, _, _, _, spellid = …
이런 식으로 사용하게 됩니다.


함수 안으로 들어가서 일단 이벤트부터 if ~ then ~ ifelse ~ end 조건문으로 분류합니다.


1) “UNIT_SPELLCAST_SUCCEEDED”
http://wowprogramming.com/docs/events/UNIT_SPELLCAST_SUCCEEDED
메시지는 ("unitID", "spell", "rank", lineID, spellID) 이며 그 중 unitID와 spellID를 사용합니다.

“boss1”이 156096번 주문을 시전한거라면 0.2초 후에 커스텀 이벤트를 fire합니다.

C_Timer.After(0.2, function()
WeakAuras.ScanEvents("WA_CHECK_MFD")
end)

C_Timer.After의 사용법은 C_Timer.After(duration, callback)이었습니다. callback 함수 부분에 즉석에서 이름 없는 함수를 하나 짜서 넣은 것입니다.

function()
WeakAuras.ScanEvents("WA_CHECK_MFD")
end
라는 함수가 C_Timer.After의 두 번째 입력 인수로 들어갔습니다.

한 줄로 쓰려면
C_Timer.After(0.2, function() WeakAuras.ScanEvents("WA_CHECK_MFD") end)
이리 하셔도 됩니다.

다른 프로그램 쪽은
C_Timer.After
(
    0.2,
    function()
        WeakAuras.ScanEvents("WA_CHECK_MFD")
    end
)
이런 방식을 권했을 것입니다만 와우 애드온 제작자들은 본문 코드의 방식을 더 많이 사용합니다.

아무튼, 0.2초 후에 이벤트 fire하라고 예약만 하고 실제 트리거 true는 반환하지 않습니다. 아이콘은 아직 표시되지 않습니다.


2) “WA_CHECK_MFD”
1번 부분에 의해 0.2초 후에 커스텀 이벤트가 발생하고 if 분류에 의해 이쪽으로 옵니다.
단순히 return true
이제 아이콘을 표시하고 실제 작업은 이름 정보 함수로 넘깁니다.



숨김 조건은 timed, 4.8초.
5초짜리 디버프이고 0.2초 대기했으니 4.8초를 썼습니다. 실제로는 시전하고 디버프 걸리는데 시간이 있으므로 조금 더 쓰셔도 됩니다.



이름 정보에서 실제 디버프가 걸린 사람들 목록을 작성할 것입니다.

function()
local aura = GetSpellInfo(156096)
local S = ""

for _, unit in ipairs(WeakAuras.raidUnits) do
if UnitDebuff(unit, aura) then
local name, class = UnitName(unit), select(2,UnitClass(unit))
S = S.."|c"..RAID_CLASS_COLORS[class].colorStr..name.."|r\n"
end
end

if S:len() > 1 then
S = S:sub(1, -2)
return S
end
end


보스가 주문을 시전하고 0.2초 후이니까 아마 공대원 중 2~3명에게 디버프가 걸렸을 것입니다.
이건 별 수 없이 한 명 한 명 다 체크해야 합니다.

local aura = GetSpellInfo(156096)
local S = ""

aura는 “죽음의 표적”이라는 디버프 이름입니다. WA 코드에는 가능하면 한글을 안쓰는 주의라(해보시면 아시겠지만 타자치기 귀찮습니다.) 주문ID에서 주문 정보를 얻는 GetSpellInfo의 첫 반환값을 사용했습니다.
S는 이름 문자열로 반환할 스트링입니다. “”라고 써서 일단 그냥 빈 스트링을 만듭니다.


for _, unit in ipairs(WeakAuras.raidUnits) do
if UnitDebuff(unit, aura) then
local name, class = UnitName(unit), select(2,UnitClass(unit))
S = S.."|c"..RAID_CLASS_COLORS[class].colorStr..name.."|r\n"
end
end

“raid1”, “raid2”, “raid3”, … 등에 대해 반복해서 UnitDebuff를 얻고, 값이 있다면 스트링을 변경할 것입니다.
for 반복문을 사용하는데 for 반복 구조에 대해서 상세한 내용은 이 글 뒤쪽의 Lua 강좌를 참조하세요.

일반적인 상황이라면 unitID를 생성하기 위해 다음과 같은 방법을 썼을 것입니다.
for i = 1, 30 do
local unit = “raid”..i
(이하 코드)
end

블랙핸드는 최대 30인 레이드이니 1부터 30까지 “raidX”라는 스트링을 unit이란 변수에 넣고 이걸로 UnitDebuff를 돌릴 수 있습니다.

그런데 마침 WeakAuras는 내부적으로 사용하려고 WeakAuras.raidUnits라는 테이블을 만들어두고 있습니다. 이 테이블은 아무것도 없이 그냥
{“raid1”, “raid2”, “raid3”, …, “raid40”}
이렇게 40개의 공대원 unitID가 들어있는 테이블입니다.
그래서 이것을 활용합시다. 테이블이 있으니 generic for 구문을 사용할 수 있습니다.


for _, unit in ipairs(WeakAuras.raidUnits) do
에 의해
local unit = “raid1”
local unit = “raid2”
이라는 지정을 한 것과 같이 unit의 값이 바뀌며 for 구문 안의 코드가 40회 수행됩니다.


if UnitDebuff(unit, aura) then
이번에 체크하는 공대원에게 aura ( = “죽음의 표적”) 디버프가 있다면

local name, class = UnitName(unit), select(2,UnitClass(unit))
공대원 이름을 name에
직업명을 class라는 변수에 저장합니다.

직업명은 글자색을 바꾸기 위함입니다.


S = S.."|c"..RAID_CLASS_COLORS[class].colorStr..name.."|r\n"
자, 봅시다.
S는 여기에서 반환할 스트링이었는데 S.. 했으니 뒤에 뭔가 덧붙입니다.


"|c"
와우에서 텍스트에 속성을 부여하려면 pipe character라고 불리는 “|”로 시작하는 코드를 사용합니다.

색상을 바꾸는 형식은 “|cAARRGGBB”
AA : 알파(불투명도)
RR, GG, BB : red, green, blue 각각의 색상
모두 1바이트 16진수입니다.(00~FF)

일단 |c로 시작한 다음 뒤에 색상 코드를 넣을 것입니다.


..RAID_CLASS_COLORS[class].colorStr
와우에서 사용할 직업 색상들이 저장된 테이블이 RAID_CLASS_COLORS입니다.
키값으로는 직업 이름을 대문자로 넣습니다. “MAGE”, “MONK”, “DEATHKNIGHT” 등등…

와우 채팅창에 /dump RAID_CLASS_COLORS.HUNTER 엔터 하시면
{b = 0.45, colorStr = “ffabd473”, g = 0.83, r = 0.67}
이라는 테이블이 나옵니다.
0~1 범위로 표시된 r, g, b 값과 컬러 스트링입니다. 컬러스트링을 사용할 건데요,

colorStr = “ffabd473”이 뒤에 붙으면 “|cffabd473”이라는, 냥꾼 색으로 텍스트를 바꾸는 이스케잎 시퀀스가 완성됩니다.


..name
UnitName으로 잡은 캐릭 이름

..”|r\n”
|r은 앞서 사용한 색상 변경 코드를 종료합니다.
n은 줄바꿈 문자입니다.

자, 그래서 
“raid4” 공대원인 냥꾼 “김밀렵”과
“raid22” 공대원인 전사 “냅닥돌”이 죽음의 징표에 걸렸으면

S = “”에서 시작해서

네 번째 반복을 거치면
S = “|cffabd473김밀렵|r\n”
이 되고

22번째 반복을 거치면 계속 뒤에 붙여서
S = “|cffabd473김밀렵|rn|cffc79c6e냅닥돌|r\n”
이 됩니다.
(이걸 채팅창에서 print 함수로 확인해보시려면 |를 \124로 고치셔야 잘 동작합니다.)



if S:len() > 1 then
S = S:sub(1, -2)
return S
end

만~약에 아무도 안걸렸으면 S는 그냥 처음 지정한 “” 그대로입니다. 하나라도 걸렸으면 뭔가 들어갔겠죠.
S:len()은 스트링의 길이를 반환합니다(바이트 단위)

그래서 if문의 조건은 그냥 ‘아무나 하나 이상 걸렸으면’해당하고요


S = S:sub(1, -2)
아까 사람마다 맨 뒤에 \n, 줄바꿈 문자를 입력했습니다. 그런데 맨 뒷사람은 굳이 줄바꿈 다시 할 필요가 없으니 저걸 지울겁니다.
string.sub(“string”, start, end) 혹은 string:sub(start, end) 는 스트링에서 원하는 부분만을 잘라내는 함수이고요, 1, -2라는 범위는 ‘첫 글자’부터 ~ ‘뒤에서 두 번째 글자’까지에 해당합니다.
즉, 맨 뒤 한 바이트만 잘라내는 것인데요, \n은 두 글자로 입력했지만 실제로는 줄바꿈 문자 1바이트거든요.


return S
지금껏 생성한 스트링을 이름 정보로 반환합니다.


이제 디스플레이 탭에서 %n으로 이름 정보에서 생성한 텍스트를 표시하되, 아이콘 오른쪽 바깥에 표시하게 합니다.

/dump GetSpellInfo(156096) 엔터 해보시면 아이콘 파일 이름이 marked4death입니다. 자동 아이콘 체크를 끄고 marked4death를 검색해서 아이콘을 지정합니다.





불러오기 조건에서 전투중, 지역으로 검은바위 용광로 넣으시면 적당할 것입니다.




스트링 함수들 다뤄야 하는데… 일단 링크로 대체합니다.


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

정리:

WoW API

이벤트는 와우 클라이언트가 인터페이스 부분에 상황을 알리는 메시지입니다.
대단히 많은 이벤트들이 있으며 http://wowprogramming.com/docs/events 링크를 참조.
텍스트를 쓸 때 |cAARRGGBB로 색바꿈을 시작, |r로 색바꿈을 끝냅니다. \n은 줄바꿈 문자


WeakAuras

커스텀 트리거는 매 프레임 / 혹은 이벤트 기반으로 트리거 함수를 실행해서 표시 여부를 결정합니다.
WeakAuras.ScanEvents("이벤트_이름" [, 메시지1, 메시지2, ...]) 를 사용해 이벤트를 생성할 수 있고 이 이벤트는 커스텀 트리거의 체크 조건 이벤트로 사용될 수 있습니다.





Lua 2 타입Types

2.5 표Tables

뒤늦게 나왔습니다. 미루고 미루다가 반복 구조를 하려니 나왔군요.

C의 array와 비슷합니다. 여러 타입의(표 안에 표가 다시 들어갈 수도 있습니다.) 값들을 하나 이상 넣어두고 나중에 접근할 수 있게 해주는 구조체입니다.
책 맨 앞에 목차가 있어서 원하는 내용이 몇 페이지에 있는지 알려주는 것처럼 표는 key 또는 index라는 방법으로 해당 데이터가 있는 구획field을 찾아갈 수 있습니다.
테이블의 생성은 { } 중괄호를 사용합니다.

기본적인 테이블의 생성과 인덱스 사용법을 봅시다.



> a = { 2, “WoW”, print }
> print(a[1], a[2], type(a[3]))
2 Wow function
중괄호 안에 콤마로 구분한 값들을 넣으면 각각의 값들이 저장된 field는 표이름[인덱스index]라는 방식으로 접근 가능합니다. 인덱스는 1로 시작하는 정수이며(강제로 0, 혹은 음수도 넣을 수 있습니다) a[1], a[2] 등의 표현은 해당 값이 지정된 변수처럼 사용할 수 있습니다.

이렇게 새 field를 생성하거나 기존 값을 바꿀 수도 있습니다.
> a[1], a[5] = 4, 3.1416
> print(a[1], a[5])
4 3.1416
4번 필드가 비었지만 괜찮습니다.

인덱스로 지정된 필드 생성은 다음처럼 할 수도 있습니다.
> b = { [2] = 3, [3] = 6 }
> print(b[1], b[2], b[3])
nil 3 6

섞어서도 가능하지만 주의하세요. 순서대로 덮어씁니다.
> c = { [2] = “a”, [5] = “b”, 11, 12, 13 }
> print(c[1], c[2], c[3], c[4], c[5])
11 12 13 nil b


다음, 키key로 접근하는 방법을 봅시다.



> b = { x = 3 }
> b[“y”] = 4
> c = “z”
> b[c] = 5
> print(b[“x”], b.y, b[“z”])
3 4 5
> print(b.x)
3
> print(b[x])
nil

앞에서 다룬 index는 숫자이며, 생성시 중괄호 안에 그냥 값을 넣으면 자동으로 부여되고 특정하려면 [index] 형식이어야 합니다.

이에 반해 key는 문자열이고, 생성시 key = value 형태로 넣으면 됩니다.

대괄호를 이용해 키로 지정된 필드를 특정하려면 대괄호 안에 문자열, 혹은 문자열이 지정된 변수를 넣으면 됩니다.

c = “z”인 상태에서 b[c]는 b[“z”]와 동일한 결과를 냅니다.

b[“y”]와 b.y는 동일합니다. 인덱스라면 a.1과 같이 쓸 수 없습니다.

위 구문chunk 어디에서도 x라는 변수를 지정한 곳이 없으므로 b[x]는 b[nil]이 됩니다. 즉, b.x에서 x는 “x”라는 문자열 값을, b[x]에서 x는 변수 이름을 의미합니다.

한 테이블 안에 인덱스와 키로 정의된 필드를 혼합해서 사용할 수 있습니다.

테이블 안에는 다시 테이블이 들어갈 수 있습니다.



> c = {}
> c[1] = {}
> c.d = {}
> c[1][“a”] = “c-1-a”
> c[“d”].a = “c-d-a”
> print(c[1].a, c.d[“a”])
c-1-a c-d-a

여기까지의 결과로 c라는 테이블은
c = { [1] = { a = “c-1-a” }, d = { a = “c-d-a” } }
로 생성한 것과 동일한 구조를 가집니다.

다층 구조 테이블을 만들 때는 한 단계씩 가야 합니다.

> c.e[1] = 3
error

c.e는 없는 상태에서 생성할 수 있지만 없는 것을 테이블인 것처럼 한 번에 c.e[1]이라는 인덱스 필드에는 접근할 수 없습니다.

테이블과 나머지 값이 약간 다른 점이 있는데 테이블은 고유한 개체이며 정확히 같은 구조를 가지는 두 테이블도 별개의 개체 점입니다.
예를 들어 3과 2 + 1은 그냥 같은 값이나 { 1 }과 { 1 }은 내용이 같으나 구분되는 두 테이블입니다.

그래서 다음과 같은 현상이 발생합니다.



> a = 3
> b = a
> b = b + 1
> print(a, b)
3 4
> c = {}
> c[1] = 3
> d = c
> d[1] = d[1] + 1
> print(c[1], d[1])
4 4
> e = { 3 }
> f = { 3 }
> f[1] = f[1] + 1
> print(e[1], f[1])
3 4

두 번째 사례의 d = c가 하는 일은 c를 정의할 때 생성한 바로 그 테이블에 d라는 변수도 연결하는 것입니다. c와 d는 같은 개체에 연결됩니다.
첫 사례에서 b = a를 할 때는 a와 b가 따로 같은 값을 갖게 되는 것과 좀 다릅니다.
b = a에서는 값이 복사되는 것이고 d = c를 할 때는 개체는 하나고 연결만 하나 더 생깁니다.

> c = nil
> print(d[1])
4

c=nil을 지정해서 c라는 변수를 지워도 연결되어있던 테이블은 그냥 남아있고 d와의 연결도 그대로이므로 d를 통해서는 접근할 수 있습니다.
만약 이 상태로 d=nil을 지정하면 이제 저 테이블에는 접근할 방법이 없습니다. 그러나 테이블은 남아있으며 collectgarbage 명령으로 쓸 일 없는 자료를 삭제하기 전에는 메모리를 차지합니다.

원래 lua.org에는 이쪽에 있지 않지만 #table이라는 표현을 보고 넘어가겠습니다.



위 주소에서 standalone interpreter를 웹기반으로 사용할 수 있습니다.

> t = {}
> t[1], t[2], t[5], t.a, t.b = 1, 2, 5, “A”, “B”
> print(#t)
2
> t[3] = 3; print(#t)
3
> t[4] = 4; print(#t)
5

#table은 테이블의 어레이 사이즈array size를 반환합니다. 일단 정수 인덱스가 아닌 키를 통해 생성된 필드는 무시됩니다.
그런데 인덱스가 중간에 비어있다면 결과를 예측할 수 없습니다.
처음에는 1, 2, 5번 인덱스에 자료가 있었고 2가 반환됩니다.
3을 채우면 3
4번 인덱스를 채우니 5까지 세어서 5가 반환됩니다.
#table은 정수 인덱스 필드만 세는데 중간이 비어있을 수 있다면 사용하지 않는 것이 바릅니다.



Lua 4 식 Statements

여기부터 while, repeat, for 등의 반복 구조iterator를 다루겠습니다.

4.3.2 while

첫 대상은 while입니다.
while (조건) do (반복할 코드) end 형태로 씁니다.
먼저 조건을 판단하고 true라면 내부의 코드를 수행한 뒤 다시 조건을 판단하러 갑니다. 내부 코드에 의해 조건이 만족하지 않게 바뀌면 더이상 수행하지 않습니다.



> sum, i = 0, 1
> while i <= 10 do
>> sum = sum + i
>> i = i + 1
>> end
> print(sum)
55

1부터 10까지 더하는 코드입니다.
Standalone interpreter(lua.exe)를 쓰니 글로벌 변수를 사용하지만 실제 상황에서는 대체로 로컬 변수를 사용할 것이며 while 구조는 블럭으로 내부에서 생성한 로컬 변수는 외부에서 접근할 수 없으니 반복구조 밖에서 미리 변수를 생성하시기 바랍니다.
sum과 i는 각각 총합, 현재 숫자입니다.
총합을 저장하는 변수에 현재 숫자를 더하고 현재 숫자를 1씩 증가시킵니다.
계속 하다가 10을 더한 다음 i가 11이 되면 i <= 10 조건을 만족하지 않기 때문에 종료됩니다.

> a = {1, 2, 3}
> while #a > 0 do
>> print(a[1])
>> table.remove(a, 1)
>> end
1
2
3

첫 예에서는 인덱스 변수(i)를 사용했는데 테이블을 사용하는 일도 자주 있습니다.
table.remove는 테이블에서 필드 하나를 삭제합니다.
table.remove(table [, index]) 형태로 사용하며 원하는 인덱스를 쓰지 않으면 맨 뒤의 값을 삭제합니다.
이 예에서는 반복할 때마다 테이블의 첫 번째 필드에 있는 값을 출력한 다음 해당 필드를 삭제합니다. table.remove로 삭제하면 뒤쪽 필드가 한 자리씩 앞으로 당겨져서 자리를 채우므로 처음에 {1, 2, 3}이었던 테이블은 한 번 수행한 뒤에는 {2, 3}이 되고 다시 {3}에 이어 마지막으로 {}가 되어 while의 조건을 만족하지 않아 종료됩니다.

반복 구조를 사용하실 때에는 내부 코드가 조건을 변화시켜서 언젠가는 탈출하도록 만들어졌음을 꼭 확인하시기 바랍니다. 무한히 반복할 가능성이 있습니다.


4.3.3 repeat

while과 거의 비슷합니다. 구조는 repeat (코드) until (조건)
‘처음에 일단 코드를 한 번 수행한 다음’ until 뒤의 조건을 확인해서 조건이 ‘false라면’ 다시 repeat 부분으로 돌아가 코드를 다시 수행하다가 조건이 true가 되면 종료합니다. until이라는 단어의 의미가 ‘~이기 전까지’임을 유념하세요.
while과의 차이는 일단 한 번은 수행된다는 점입니다.



> a, i = 1, 0
> repeat
>> a = ½ * (a + 2/a)
>> i = i + 1
>> until i > 3
> print(a, math.sqrt(2))
1.4142135623747 1.4142135623731

x’ = ½ * (x + 2/x) 라는 점화식은 2의 제곱근의 근사값을 구하는 뉴턴-랩슨 근사법의 식입니다.
루트 2가 대략 1.4xxx임을 알기 때문에 초기값으로 1을 넣고 반복합니다.
i가 0에서 시작해서 4가 되는 순간 반복구조 맨 뒤의 조건이 true가 되면서 탈출하니 4회 반복됩니다.
math.sqrt 함수는 제곱근을 구하는 함수인데 a의 첫 값은 1이었고 점화식 계산을 4회 반복한 뒤 5번째 항의 값이 소수점 아래 11번째 자리까지 맞음을 볼 수 있습니다. 제곱근의 근사값을 구하는 가장 강력한 방법입니다...만 계산기가 없으면 어짜피 피곤하기는 매한가지입니다.


4.3.4 Numeric for

정해진 회수만큼 코드를 반복하는데 가장 흔히 쓰이는 방법입니다.
구조는 for (인덱스 변수) = (시작값), (끝값) [,(간격)] do (수행할 코드) end

인덱스 변수 : 코드 안에서 지금 몇 회차 반복인지 접근할 수 있는 변수입니다. 첫 바퀴에는  시작값으로 주어지고 매번 반복할 때마다 변화된 값이 됩니다. for 구문에서 로컬 변수로 사용됩니다. 코드 안에서 이 변수의 값을 직접 변화시키는 것은 추천하지 않습니다.
특별한 취향이 없는 사람들은 i를 사용합니다.
시작값, 끝값, 간격 : 시작값부터 끝값까지, 간격이 지정되면 그 간격만큼 변화시킵니다. 예를 들어 i = 3, 11, 2 라고 지정했으면 처음에는 3, 다음에는 3+2 = 5, 그 다음에는 7, 9, 11까지 총 5번 코드가 수행됩니다. 간격을 지정하지 않으면 +1 단위로 수행됩니다.



> for i = 1, 10 do
>> sum = sum and sum + i or i
>> end
> print(sum)
55

1부터 10까지 더합니다. 원래 밖에서 sum을 지정하고 들어갔어야 하는데 그냥 들어간 바람에 이상한 코드가 되었습니다.
(sum) and (sum + i) or (i)
sum이 이미 있으면 sum에 i를 더하고, 없다면 그냥 i값을 지정
처음 i가 1일 때는 sum이 없으므로 sum = 1이 수행되고 이후로는 sum = sum + i가 수행됩니다.

> for i = 1, 5 do
>> if i == 5 then
>> unit = “player”
>> else
>> unit = “party”..i
>> end
>> print(unit)
>> end
party1
party2
party3
party4
player

5인 파티에서 파티원들의 unitID를 출력하는 코드입니다. 5인 파티에서는 자신은 player, 나머지 파티원들은 파티에 참여한 순서대로 party1, party2, party3, party4가 됩니다. 자신이 파티장이 아니라면 파티장이 party1에 들어갑니다.
최초 4바퀴 동안은 partyx 형태를, 마지막 5번째에는 player를 출력합니다.


4.3.5 Generic for

Numeric for는 정해진 회수만큼 반복하는 것이었고, generic for는 보통 테이블의 인덱스(혹은 key) - 필드 각각에 대해 수행됩니다.



> a = {}
> a[1], a[2], a[3], a[5], a.a, a.b = 1, 2, 3, 5, “A”, “B”
테이블을 하나 만듭니다. 인덱스와 키가 섞여있으며 인덱스는 1, 2, 3, 5로 중간이 비었습니다.

> for i, v in ipairs(a) do
>> print(v)
>> end
1
2
3

for의 반복 구간을 지정할 때 ipairs(a)로 지정했습니다. 이러면 인덱스에 대해서만, 1부터 차례로 증가하다가 없어지기 전까지 수행됩니다.
반복문 안에서는 해당 필드에 있는 값을 출력하는데 1, 2, 3까지만 반복된 것을 볼 수 있습니다.

> for i, v in pairs(a) do
>> print(v)
>> end
A
2
B
1
5
3

반면 반복 구간을 pairs(a)로 지정하면 테이블 a에 있는 모든 필드에 대해 반복문이 수행됩니다.
다만, 순서가 지 마음대로입니다.


4.4 break and return

break는 반복구조(for, while, repeat) 안에 쓰여서 break가 속한 반복구조 블럭 중 가장 안쪽 것을 종료하고 밖으로 나갑니다(반복구조 안에 다시 반복구조가 있어서 그 안에서 break를 사용하면 안쪽 것이 종료되며 바깥쪽 반복구조는 계속 수행됩니다).



> a = {“A”, “B”, “C”, “D”, “F”}
테이블 하나 만들고요,

> for i, v in ipairs(a) do
>> if v == “C” then
>> print(i)
>> break
>> end
>> end
3
테이블에 대해 ipairs로 반복수행하다가 필드의 값이 “C”라면 break합니다. ipairs였으니 1, 2, 3번째 필드를 반복하다가 3번째 필드에서 중단되었을 것입니다.

>while true do
>> if a[1]  == “C” then
>> break
>> end
>> table.remove(a, 1)
>> end
> print(a[1])
C

while문은 ‘조건이 참인 동안’ 반복되므로 조건에 true를 넣으면 정상적인 방법으로는 영원히 끝나지 않습니다.
테이블의 첫 번째 필드 값이 “C”가 아니라면 if문 뒤의 코드대로 첫 필드를 삭제하고 다시 반복합니다. a[1]이 “C”가 나오면 뒤쪽 코드가 수행되기 전에, 그 지점에서 즉시 반복문을 종료하기 때문에 밖에서 a[1]을 출력하면 C가 나온 것을 볼 수 있으며 이 테이블은 이제 {“C”, “D”, “F”}입니다.

함수에서 return은 함수의 반환값을 내어주는 명령으로 쓰였으나 단순히 함수를 중단하기위해 사용할 수도 있습니다. 반복문을 break로 중단하는 것처럼 특정한 조건에서 함수에 있는 뒤쪽 코드의 수행을 건너뛰기 위해 반환값 없이 return을 쓸 수 있습니다.


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