Статья
Раздел:
Триггеры и объекты
Лирическое вступление
Пару недель назад, в свете того, что у меня появилось свободное время и на фоне больших ожиданий от анонсированного Warcraft direct(да-да, мамонты не вымерли), я решил вернутся к своему древнему хобби и реанимировать старую карту. Тут же я столкнулся с тем, что заставило меня ее забросить в прошлый раз, а именно тема данного поста - мерзопакостный баг с событиями фреймов. Но я был полон оптимизма, и подумал, что за всё это время лекарство должно было быть найдено, но изучив вопрос нашел только костыли, а стало быть лекарства не существует вовсе. И лучше способа убить время, чем пытаться решить проблему, которую не решили люди в разы умнее меня, я не нашел и решил изобрести свой костыль.
Что, собственно, за баг?
В 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 без задержки. Такую пару мгновенных событий я буду далее называть "фальшивыми" событиями.
Многие считают причиной фальшивых событий - смещение курсора мышки, но это не так, точнее не совсем так.

Так что там с мышкой?

При срабатывании фальшивых событий курсор мышки на мгновение смещается вправо и вверх на некоторое расстояние, может упрыгивать за пределы экрана. Если под координаты "прилёта" курсора положить фрейм с событием входа, то это событие не будет зарегистрировано. В лабораторных условиях удалось установить, что разница координат смещения курсора равна абсолютному положению 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, последним созданным фреймом станет чат-бокс, и курсор будет прыгать со смещением координат чат-бокса.
В полевых испытаниях были обнаружены другие странности поведения, которые я не могу адекватно задокументировать.

Это мой костыль. Таких костылей много, но этот мой.

Из плюсов:
  • Чинит мышку
  • Эффективно отлавливает фальшивые события.
Из минусов:
  • Событие выхода ловится с задержкой.
  • Мерцание подсветки не чинит.
  • Почти не тестировалось в мультиплеере.
  • Необходимо использовать псевдогеттеры.
  • Нативные события никуда не деваются, а так же лупят очередями.
Реализация в 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% настоящих выходов, но он не работает в мультиплеере. Потому я его унесу с собой в могилу.
Карта с примером прилагается.
`
ОЖИДАНИЕ РЕКЛАМЫ...
24
Прикольно. Попадалась на глаза инфа, что в 2.0 события теперь можно повесить на SIMPLEBUTTON, но симпл-фреймы это такая стрёмная штука, что даже проверять не хотелось. На 1.36.1 костылил полный отказ от нативных событий фреймов в пользу обмаза тултипами и проверки видимости этих тултипов, чтобы узнавать текущий фрейм под курсором.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.