Лирическое вступление
Пару недель назад, в свете того, что у меня появилось свободное время и на фоне больших ожиданий от анонсированного Warcraft direct(да-да, мамонты не вымерли), я решил вернутся к своему древнему хобби и реанимировать старую карту. Тут же я столкнулся с тем, что заставило меня ее забросить в прошлый раз, а именно тема данного поста - мерзопакостный баг с событиями фреймов. Но я был полон оптимизма, и подумал, что за всё это время лекарство должно было быть найдено, но изучив вопрос нашел только костыли, а стало быть лекарства не существует вовсе. И лучше способа убить время, чем пытаться решить проблему, которую не решили люди в разы умнее меня, я не нашел и решил изобрести свой костыль.
Что, собственно, за баг?
В Reforge, начиная с какого-то патча добавление событий (FRAMEEVENT_MOUSE_ENTER,FRAMEEVENT_MOUSE_LEAVE,FRAMEEVENT_CONTROL_CLICK) к фреймам, которые поддерживают данные события, приводит к неадекватному поведению игры при их вызове: мышка начинает скакать, вызов события зацикливается, кнопки с подсветкой начинают мигать.
А его можно обойти?
Как оказывается можно, использовав SIMPLEBUTTON. В актуальном патче, данный фрейм поддерживает эти события и не вызывает вышеуказанных проблем.
Тогда к чему это всё?
SIMPLEBUTTON не имеет такой гибкости в использовании как обычные фреймы и остальные SIMPLE-фреймы не поддерживают события.
В Reforge, начиная с какого-то патча добавление событий (FRAMEEVENT_MOUSE_ENTER,FRAMEEVENT_MOUSE_LEAVE,FRAMEEVENT_CONTROL_CLICK) к фреймам, которые поддерживают данные события, приводит к неадекватному поведению игры при их вызове: мышка начинает скакать, вызов события зацикливается, кнопки с подсветкой начинают мигать.
А его можно обойти?
Как оказывается можно, использовав SIMPLEBUTTON. В актуальном патче, данный фрейм поддерживает эти события и не вызывает вышеуказанных проблем.
Тогда к чему это всё?
SIMPLEBUTTON не имеет такой гибкости в использовании как обычные фреймы и остальные SIMPLE-фреймы не поддерживают события.
Занявшись изучением вопроса я собрал информацию, которой здесь поделюсь.
События
FRAMEEVENT_MOUSE_ENTER
Самый вредный из злополучной троицы. Создает цикл срабатываний при наведении на фрейм, со всеми вытекающими симптомами. При наличии на фрейме FRAMEEVENT_MOUSE_LEAVE зацикливает и его.
FRAMEEVENT_MOUSE_LEAVE
Собутыльник - безопасный в одиночку, так как срабатывает уже после покидания фрейма.
FRAMEEVENT_CONTROL_CLICK
Спусковой крючок - сам по себе не несет опасности, при срабатывании мышка разово смещается, кнопка может мигнуть и ничего страшного, но при наличии дополнительно FRAMEEVENT_MOUSE_LEAVE зациклит его.
Зацикливание событий
Для изучения механизма зацикливаний я использовал фрейм GLUETEXTBUTTON и свой допотопный персональный ПК. Потому не могу отвечать за подлинность результатов на другом оборудовании.
Замерять частоту будем таймером TimerStart(CreteTimer(),9999.,false,null) снимая с него TimerGetElapsed()
При 120+ fps, событие срабатывало каждые ~100ms, при падении fps срабатывание прыгало между ~100ms и ~200ms, во время сильных фризов могло достигать 300-600+ms.
При наличии обоих событий (ENTER и LEAVE) сначала срабатывало событие LEAVE и сразу за ним ENTER без задержки. Такую пару мгновенных событий я буду далее называть "фальшивыми" событиями.
Многие считают причиной фальшивых событий - смещение курсора мышки, но это не так, точнее не совсем так.
Замерять частоту будем таймером TimerStart(CreteTimer(),9999.,false,null) снимая с него TimerGetElapsed()
При 120+ fps, событие срабатывало каждые ~100ms, при падении fps срабатывание прыгало между ~100ms и ~200ms, во время сильных фризов могло достигать 300-600+ms.
При наличии обоих событий (ENTER и LEAVE) сначала срабатывало событие LEAVE и сразу за ним ENTER без задержки. Такую пару мгновенных событий я буду далее называть "фальшивыми" событиями.
Многие считают причиной фальшивых событий - смещение курсора мышки, но это не так, точнее не совсем так.
Так что там с мышкой?
При срабатывании фальшивых событий курсор мышки на мгновение смещается вправо и вверх на некоторое расстояние, может упрыгивать за пределы экрана. Если под координаты "прилёта" курсора положить фрейм с событием входа, то это событие не будет зарегистрировано. В лабораторных условиях удалось установить, что разница координат смещения курсора равна абсолютному положению FRAMEPOINT_BOTTOMLEFT последнего созданного фрейма. Тут понятнее с примерами.
set b1 = BlzCreateFrameByType("GLUETEXTBUTTON", "MyButton",BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "ScriptDialogButton", 0 )
set b2 = BlzCreateFrameByType("GLUETEXTBUTTON", "MyButton",BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "ScriptDialogButton", 0 )
call BlzFrameSetSize(b1,0.1,0.1)
call BlzFrameSetAbsPoint(b1,FRAMEPOINT_BOTTOMLEFT, 0.5,0.5)
call BlzFrameSetSize(b2,0.1,0.1)
call BlzFrameSetAbsPoint(b2,FRAMEPOINT_BOTTOMLEFT, 0.25,0.25)
При фальшивом событии мышка из фрейма b2 будет прыгать прямиком на b1 в те же внутренние координаты.
Или даже вот так.
Или даже вот так.
set b1 = BlzCreateFrameByType("GLUETEXTBUTTON", "MyButton",BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "ScriptDialogButton", 0 )
set b2 = BlzCreateFrameByType("GLUETEXTBUTTON", "MyButton",BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "ScriptDialogButton", 0 )
call BlzFrameSetSize(b2,0.5,0.1)
call BlzFrameSetPoint(b2,FRAMEPOINT_RIGHT,BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0),FRAMEPOINT_RIGHT, 0.,0.)
call BlzFrameSetSize(b1,0.1,0.1)
call BlzFrameSetAbsPoint(b1,FRAMEPOINT_BOTTOMLEFT, 0.,0.)
Мышь из нижнего левого угла кнопки b1(координат 0., 0.) будет прыгать в левый нижний угол b2.
Но если поменять местами порядок создания кнопок
Но если поменять местами порядок создания кнопок
set b2 = BlzCreateFrameByType("GLUETEXTBUTTON", "MyButton",BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "ScriptDialogButton", 0 )
set b1 = BlzCreateFrameByType("GLUETEXTBUTTON", "MyButton",BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "ScriptDialogButton", 0 )
То курсор перестанет вовсе прыгать и визуально останется на месте, но фальшивые события никуда не денутся и подсветка продолжит мигать.
И если наведя на фрейм с событием открыть окно чата нажав Enter, последним созданным фреймом станет чат-бокс, и курсор будет прыгать со смещением координат чат-бокса.
В полевых испытаниях были обнаружены другие странности поведения, которые я не могу адекватно задокументировать.
И если наведя на фрейм с событием открыть окно чата нажав Enter, последним созданным фреймом станет чат-бокс, и курсор будет прыгать со смещением координат чат-бокса.
В полевых испытаниях были обнаружены другие странности поведения, которые я не могу адекватно задокументировать.
Это мой костыль. Таких костылей много, но этот мой.
Из плюсов:
- Чинит мышку
- Эффективно отлавливает фальшивые события.
Из минусов:
- Событие выхода ловится с задержкой.
- Мерцание подсветки не чинит.
- Почти не тестировалось в мультиплеере.
- Необходимо использовать псевдогеттеры.
- Нативные события никуда не деваются, а так же лупят очередями.
Реализация в 2х словах. Для отлова входов на фрейм отслеживаем текущий фрейм под мышкой. Для отлова фальшивых событий следим за быстрым выходом/входом на тот же фрейм. Выходы отлавливаем таймером. Фикс мышки делается созданием огромного пустого спрайта в центре экрана. Как и почему это работает? Понятия не имею.
Никому не нужный код
/*==============================================================================================
API:
FixMouseTwitch()
- Must be used after last custon frame created to fix mouse.
AddMouseEnterAction(framehandle, code)
- Attaches code to framehandle for FRAMEEVENT_MOUSE_ENTER (only one action can be added)
AddMouseLeaveAction(framehandle, code)
- Attaches code to framehandle for FRAMEEVENT_MOUSE_LEAVE (only one action can be added)
RemoveMouseEnterAction(framehandle)
- Removes attached code from framehandle for FRAMEEVENT_MOUSE_ENTER
RemoveMouseLeaveAction(framehandle)
- Removes attached code from framehandle for FRAMEEVENT_MOUSE_LEAVE
StripFrame(framehandle)
- Removes all attached data, must be used before destroying frame to prevent leaks
GetEventFrame() => framehandle
- Must be used intead BlzGetTriggerFrame() inside attached code.
GetEventPlayer() => player
- Must be used instead GetTriggerPlayer() inside attached code.
GetFrameEvent() => frameeventtype
- Must be used instead BlzGetTriggerFrameEvent() inside attached code.
==============================================================================================*/
library FrameEventFix initializer init
globals
hashtable FEF_Hash
trigger FEF_EventListner
framehandle array FEF_ActiveFrame[25]
framehandle FEF_GetEventFrame = null
framehandle FEF_FixDummy = null
player FEF_GetEventPlayer = null
frameeventtype FEF_GetFrameEvent =null
constant real FEF_EVENT_TIMEOUT = 0.2
boolean FEF_DEBUG = false
constant integer DEBUG_RealEnter = 1
constant integer DEBUG_ForcedEnter = 2
constant integer DEBUG_RealLeave = 3
constant integer DEBUG_TimeoutLeave = 4
constant integer DEBUG_ForcedLeave = 5
constant integer DEBUG_EmptyTimer = 6
constant integer DEBUG_FakeEvenet = 0
constant integer OFFSET_EVENT_FRAME = 10
constant integer OFFSET_EVENT_PLAYER = 20
constant integer OFFSET_FRAME_REGISTRED = 30
constant integer OFFSET_ENTER_EVENT_ACTION = 40
constant integer OFFSET_LEAVE_EVENT_ACTION = 50
constant integer OFFSET_ENTER_EVENT_TRIGGER = 300
constant integer OFFSET_LEAVE_EVENT_TRIGGER = 400
constant integer OFFSET_DETECT_LEAVE_TIMER = 500
integer array DEBUG_counter[7]
endglobals
function FixMouseTwitch takes nothing returns nothing
if GetHandleId(FEF_FixDummy)>0 then
call BlzDestroyFrame(FEF_FixDummy)
endif
set FEF_FixDummy= BlzCreateFrameByType("SPRITE", "IAMYOURSAVIOUR", BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "WarCraftIIILogo", 22)
call BlzFrameClearAllPoints(FEF_FixDummy)
call BlzFrameSetPoint(FEF_FixDummy,FRAMEPOINT_CENTER,BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0),FRAMEPOINT_CENTER,0.0,0.0)
call BlzFrameSetSize(FEF_FixDummy, 2., 2.)
endfunction
function FixMouseTwitchTimer takes nothing returns nothing
call FixMouseTwitch()
call DestroyTimer(GetExpiredTimer())
endfunction
function GetEventFrame takes nothing returns framehandle
return FEF_GetEventFrame
endfunction
function GetEventPlayer takes nothing returns player
return FEF_GetEventPlayer
endfunction
function GetFrameEvent takes nothing returns frameeventtype
return FEF_GetFrameEvent
endfunction
function ResetDebugData takes nothing returns nothing
local integer i=0
loop
exitwhen i==7
set DEBUG_counter[i]=0
set i=i+1
endloop
endfunction
function DEBUG_register takes integer etype returns nothing
if not FEF_DEBUG then
return
endif
set DEBUG_counter[etype]=DEBUG_counter[etype]+1
call ClearTextMessages()
call BJDebugMsg("=========== Overall ============")
call BJDebugMsg("Enters registred = "+I2S(DEBUG_counter[DEBUG_RealEnter]+DEBUG_counter[DEBUG_ForcedEnter]))
call BJDebugMsg("Leaves registred = "+I2S(DEBUG_counter[DEBUG_RealLeave]+DEBUG_counter[DEBUG_TimeoutLeave]+DEBUG_counter[DEBUG_ForcedLeave]))
call BJDebugMsg("Fake events = "+I2S(DEBUG_counter[DEBUG_FakeEvenet]))
call BJDebugMsg("=========== Details ============")
call BJDebugMsg("Forced enters = "+I2S(DEBUG_counter[DEBUG_ForcedEnter]))
call BJDebugMsg("Forced leaves = "+I2S(DEBUG_counter[DEBUG_ForcedLeave]))
call BJDebugMsg("Timeout leaves = "+I2S(DEBUG_counter[DEBUG_TimeoutLeave]))
call BJDebugMsg("Empty timers = "+I2S(DEBUG_counter[DEBUG_EmptyTimer]))
endfunction
function FEF_ExecuteTrigger takes trigger trig, framehandle fh, player p, frameeventtype e returns nothing
if GetHandleId(trig)>0 then
set FEF_GetEventFrame = fh
set FEF_GetEventPlayer = p
set FEF_GetFrameEvent = e
call TriggerExecute(trig)
endif
endfunction
function DetecteLeaveTimeout takes nothing returns nothing
// Timer Expired = real leave
local timer tt=GetExpiredTimer()
local integer ttid=GetHandleId(tt)
local framehandle fh = LoadFrameHandle(FEF_Hash,ttid,OFFSET_EVENT_FRAME)
local player p
local trigger exectrig
if GetHandleId(fh)> 0 then // Sometimes destroyed timers still fire
set p = LoadPlayerHandle(FEF_Hash,ttid,OFFSET_EVENT_PLAYER)
set exectrig = LoadTriggerHandle(FEF_Hash,GetHandleId(fh),OFFSET_LEAVE_EVENT_TRIGGER)
set FEF_ActiveFrame[GetPlayerId(p)]= null
call FEF_ExecuteTrigger(exectrig,fh,p,FRAMEEVENT_MOUSE_LEAVE)
call DEBUG_register(DEBUG_TimeoutLeave)
else
call DEBUG_register(DEBUG_EmptyTimer)
endif
call FlushChildHashtable(FEF_Hash,ttid)
call DestroyTimer(tt)
set tt=null
set exectrig=null
endfunction
function RegisterMouseFrameEvent takes framehandle fh, integer offset, integer taoffset, code action, boolean add returns nothing
local integer fhid=GetHandleId(fh)
local trigger eventtrigger
local triggeraction trgact
if fhid==0 then
return
endif
if not LoadBoolean(FEF_Hash,fhid,OFFSET_FRAME_REGISTRED) then
call SaveBoolean(FEF_Hash,fhid,OFFSET_FRAME_REGISTRED,true)
call BlzTriggerRegisterFrameEvent(FEF_EventListner,fh,FRAMEEVENT_MOUSE_ENTER)
call BlzTriggerRegisterFrameEvent(FEF_EventListner,fh,FRAMEEVENT_MOUSE_LEAVE)
endif
set eventtrigger=LoadTriggerHandle(FEF_Hash,fhid,offset)
if GetHandleId(eventtrigger)>0 then
call TriggerRemoveAction(eventtrigger,LoadTriggerActionHandle(FEF_Hash,fhid,taoffset))
call DestroyTrigger(eventtrigger)
endif
if add then
set eventtrigger=CreateTrigger()
call SaveTriggerHandle(FEF_Hash,fhid,offset,eventtrigger)
set trgact = TriggerAddAction(eventtrigger,action)
call SaveTriggerActionHandle(FEF_Hash,fhid,taoffset,trgact)
endif
set eventtrigger = null
set trgact = null
endfunction
function AddMouseEnterAction takes framehandle fh, code action returns nothing
call RegisterMouseFrameEvent(fh,OFFSET_ENTER_EVENT_TRIGGER,OFFSET_ENTER_EVENT_ACTION,action,true)
endfunction
function AddMouseLeaveAction takes framehandle fh, code action returns nothing
call RegisterMouseFrameEvent(fh,OFFSET_LEAVE_EVENT_TRIGGER,OFFSET_LEAVE_EVENT_ACTION,action,true)
endfunction
function RemoveMouseEnterAction takes framehandle fh returns nothing
call RegisterMouseFrameEvent(fh,OFFSET_ENTER_EVENT_TRIGGER,OFFSET_ENTER_EVENT_ACTION,null,false)
endfunction
function RemoveMouseLeaveAction takes framehandle fh returns nothing
call RegisterMouseFrameEvent(fh,OFFSET_LEAVE_EVENT_TRIGGER,OFFSET_LEAVE_EVENT_ACTION,null,false)
endfunction
function StripFrame takes framehandle fh returns nothing
local trigger trg
local triggeraction trgact
local integer fhid=GetHandleId(fh)
local integer i=0
local timer tt
if fhid==0 then
return
endif
call RemoveMouseEnterAction(fh)
call RemoveMouseLeaveAction(fh)
loop
exitwhen i>24
if FEF_ActiveFrame[i]==fh then
set FEF_ActiveFrame[i]=null
endif
set tt=LoadTimerHandle(FEF_Hash,fhid,OFFSET_DETECT_LEAVE_TIMER+i)
if GetHandleId(tt) > 0 then
call FlushChildHashtable(FEF_Hash,GetHandleId(tt))
call DestroyTimer(tt)
endif
set i=i+1
endloop
call FlushChildHashtable(FEF_Hash,fhid)
set trg=null
set trgact=null
endfunction
function FrameEventListnerActions takes nothing returns nothing
local framehandle fh=BlzGetTriggerFrame()
local integer fhid = GetHandleId(fh)
local player p=GetTriggerPlayer()
local integer pid=GetPlayerId(p)
local trigger exectrig
local timer tt
if BlzGetTriggerFrameEvent()== FRAMEEVENT_MOUSE_ENTER then
if FEF_ActiveFrame[pid] == null then
// real Enter
call DEBUG_register(DEBUG_RealEnter)
set FEF_ActiveFrame[pid]=fh
set exectrig = LoadTriggerHandle(FEF_Hash,fhid,OFFSET_ENTER_EVENT_TRIGGER)
call FEF_ExecuteTrigger(exectrig,fh,p,FRAMEEVENT_MOUSE_ENTER)
elseif FEF_ActiveFrame[pid] == fh then
// False Enter
call DEBUG_register(DEBUG_FakeEvenet)
set tt=LoadTimerHandle(FEF_Hash,fhid,OFFSET_DETECT_LEAVE_TIMER+pid)
if GetHandleId(tt) > 0 then
call FlushChildHashtable(FEF_Hash,GetHandleId(tt))
call DestroyTimer(tt)
else
call BJDebugMsg("SOMETHING WRONG SHUT DOWN")
call DisableTrigger(GetTriggeringTrigger())
return
endif
elseif FEF_ActiveFrame[pid]!= fh then
// real enter with Missing leave event
set tt=LoadTimerHandle(FEF_Hash,GetHandleId(FEF_ActiveFrame[pid]),OFFSET_DETECT_LEAVE_TIMER+pid)
if GetHandleId(tt) > 0 then
call FlushChildHashtable(FEF_Hash,GetHandleId(tt))
call DestroyTimer(tt)
else
call BJDebugMsg("SOMETHING WRONG SHUT DOWN")
call DisableTrigger(GetTriggeringTrigger())
return
endif
set exectrig = LoadTriggerHandle(FEF_Hash,GetHandleId(FEF_ActiveFrame[pid]),OFFSET_LEAVE_EVENT_TRIGGER)
call FEF_ExecuteTrigger(exectrig,FEF_ActiveFrame[pid],p,FRAMEEVENT_MOUSE_LEAVE)
call DEBUG_register(DEBUG_ForcedLeave)
set FEF_ActiveFrame[pid]=fh
set exectrig = LoadTriggerHandle(FEF_Hash,fhid,OFFSET_ENTER_EVENT_TRIGGER)
call FEF_ExecuteTrigger(exectrig,fh,p,FRAMEEVENT_MOUSE_ENTER)
call DEBUG_register(DEBUG_ForcedEnter)
endif
elseif BlzGetTriggerFrameEvent()== FRAMEEVENT_MOUSE_LEAVE then
set tt = CreateTimer()
call SaveTimerHandle(FEF_Hash,fhid,OFFSET_DETECT_LEAVE_TIMER+pid,tt)
call SaveFrameHandle(FEF_Hash,GetHandleId(tt),OFFSET_EVENT_FRAME,fh)
call SavePlayerHandle(FEF_Hash,GetHandleId(tt),OFFSET_EVENT_PLAYER,p)
call TimerStart(tt,FEF_EVENT_TIMEOUT,false,function DetecteLeaveTimeout)
endif
set exectrig = null
set tt = null
endfunction
private function init takes nothing returns nothing
set FEF_Hash=InitHashtable()
set FEF_EventListner=CreateTrigger()
call TriggerAddAction(FEF_EventListner,function FrameEventListnerActions)
call TimerStart(CreateTimer(),0.2,false,function FixMouseTwitchTimer)
endfunction
endlibrary
В целом данное решение полностью покрывает мои потребности.
Существует способ мгновенного отлова 95% настоящих выходов, но он не работает в мультиплеере. Потому я его унесу с собой в могилу.
Карта с примером прилагается.