Unsafe to call TMulticastScriptDelegate::Broadcast from a timer callback?

I have an AIController which uses a timer callback to handle its update logic. When the controller starts up it grabs a pointer to a component in its pawn, then calls a function on the component from inside the timer callback.

The component function has a call to Broadcast in it. This will eventually cause the game to crash and visual studio shows the message “UE4Editor.exe has triggered a breakpoint”. Sometimes it happens on the first call to Broadcast, other times on the third or fourth call, but eventually, it always crashes. Is it unsafe to invoke a multicast script delegate (i.e. a BlueprintAssignable event) from a timer callback? VS shows it’s running on the main thread so I’d think it would be ok. All the pointers are valid right before the Broadcast call that fails. In the code below, the line containing OnGestureLocked.Broadcast() is the one that causes the break.

Going to try compiling the engine to get some PDBs. In the meantime thought I might check if anyone’s seen this. Thanks!

Here’s the callstack:

KernelBase.dll!000007fefd703ca2()	Unknown
UE4Editor-Core.dll!000007fee6f9e96b()	Unknown
UE4Editor-Core.dll!000007fee6e718ea()	Unknown
UE4Editor-Core.dll!000007fee6e5fea0()	Unknown
UE4Editor-CoreUObject.dll!000007fee8b31053()	Unknown
UE4Editor-CoreUObject.dll!000007fee8b2bba2()	Unknown
UE4Editor-CoreUObject.dll!000007fee8ad1615()	Unknown
UE4Editor-Engine.dll!000007fee4bc0e1a()	Unknown
UE4Editor-Engine.dll!000007fee5af5860()	Unknown
UE4Editor-CoreUObject.dll!000007fee8ba41fb()	Unknown
UE4Editor-CoreUObject.dll!000007fee8bb2272()	Unknown
UE4Editor-CoreUObject.dll!000007fee8bb35db()	Unknown
UE4Editor-CoreUObject.dll!000007fee8ba4708()	Unknown
UE4Editor-CoreUObject.dll!000007fee8bb35db()	Unknown
UE4Editor-CoreUObject.dll!000007fee8bb2e30()	Unknown
UE4Editor-Engine.dll!000007fee48fa03f()	Unknown
UE4Editor-SuperAction-Win64-DebugGame.dll!TScriptDelegate<FWeakObjectPtr>::ProcessDelegate<UObject>(void * Parameters) Line 200	C++
UE4Editor-SuperAction-Win64-DebugGame.dll!TMulticastScriptDelegate<FWeakObjectPtr>::ProcessMulticastDelegate<UObject>(void * Parameters) Line 395	C++
UE4Editor-SuperAction-Win64-DebugGame.dll!UWizardCasterCmp::GestureSubmit(UWizardCasterCmp::WizHand * mainHand, UWizardCasterCmp::WizHand * offHand, EWizardGesture gesture) Line 109	C++
UE4Editor-SuperAction-Win64-DebugGame.dll!UWizardCasterCmp::GestureSubmit(EWizardHandFlags hands, EWizardGesture gesture) Line 46	C++
UE4Editor-SuperAction-Win64-DebugGame.dll!AWizardAIController::AdvanceGesture() Line 136	C++
UE4Editor-SuperAction-Win64-DebugGame.dll!AWizardAIController::SetState(unsigned char state) Line 110	C++
UE4Editor-SuperAction-Win64-DebugGame.dll!AWizardAIController::AdvanceWait() Line 117	C++
UE4Editor-SuperAction-Win64-DebugGame.dll!TBaseUObjectMethodDelegateInstance_NoParams<AWizardAIController,void>::Execute() Line 521	C++
UE4Editor-Engine.dll!000007fee4fa44c6()	Unknown
UE4Editor-Engine.dll!000007fee4ff5add()	Unknown
UE4Editor-Engine.dll!000007fee4c25e06()	Unknown
UE4Editor-UnrealEd.dll!000007fee1083026()	Unknown
UE4Editor-UnrealEd.dll!000007fee145e956()	Unknown
UE4Editor.exe!FEngineLoop::Tick() Line 2084	C++
UE4Editor.exe!GuardedMain(const wchar_t * CmdLine, HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, int nCmdShow) Line 133	C++
UE4Editor.exe!WinMain(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * __formal, int nCmdShow) Line 196	C++
UE4Editor.exe!__tmainCRTStartup() Line 618	C
kernel32.dll!00000000777759ed()	Unknown
ntdll.dll!00000000778ac541()	Unknown

AIController timer delegate:

//==============================================================================
void AWizardAIController::AdvanceGesture () {

	uint8 packedGesture = m_curSpell->gestures[m_curGestureIdx];
	++m_curGestureIdx;
	if (m_curGestureIdx == m_curSpell->length)
		SetState(STATE_CASTING);

	EWizardGesture gesture = WizardGestureFromPacked(packedGesture);
	EWizardHandFlags handFlag;
	if ((packedGesture & WIZARD_GESTURE_FLAG_BOTH) != 0)
		handFlag = WIZARD_HAND_FLAG_BOTH;
	else
		handFlag = WIZARD_HAND_FLAG_RIGHT;

	m_casterCmp->GestureSubmit(handFlag, gesture);

	if (m_state == STATE_GESTURING) {
		float waitTime = FMath::FRandRange(.25f, 2.0f);
		GetWorldTimerManager().SetTimer(this, &AWizardAIController::AdvanceGesture, waitTime);
	}

}

Event definition and function that calls broadcast:

DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
	FWizardGestureLocked,
	UWizardCasterCmp *, sender,
	EWizardGesture, gesture,
	EWizardHandFlags, hand
);

//==============================================================================
void UWizardCasterCmp::GestureSubmit (EWizardHandFlags hands, EWizardGesture gesture) {

	if ((hands & WIZARD_HAND_FLAG_LEFT) != 0)
		GestureSubmit(&m_leftHand, &m_rightHand, gesture);
	if ((hands & WIZARD_HAND_FLAG_RIGHT) != 0)
		GestureSubmit(&m_rightHand, &m_leftHand, gesture);

}

//==============================================================================
void UWizardCasterCmp::GestureSubmit (WizHand * mainHand, WizHand * offHand, EWizardGesture gesture) {

	HandGesture handGesture;
	handGesture.gesture = WizardGesturePack(gesture, WIZARD_GESTURE_FLAG_NONE);
	handGesture.time = m_tickTime.Ticks;
	handGesture.sequence = m_gestureSequence++;

	// Check if this was a simultaneous gesture with the offhand.
	bool isSimultaneous = false;
	if (offHand->gestures.Num()) {
		HandGesture & offhandLast = offHand->gestures.Last();
		bool isInSequence = offhandLast.sequence == handGesture.sequence - 1;
		FTimespan timeOffset(handGesture.time - offhandLast.time);
		bool isInTime = timeOffset.GetMilliseconds() < 500;
		bool isAlreadyMatched = (offhandLast.gesture & WIZARD_GESTURE_FLAG_BOTH) != 0;
		EWizardGesture offhandGesture = WizardGestureFromPacked(offhandLast.gesture);
		if (isInSequence && isInTime && !isAlreadyMatched && offhandLast.gesture == gesture) {
			isSimultaneous = true;
			offhandLast.gesture |= WIZARD_GESTURE_FLAG_BOTH;
			handGesture.gesture |= WIZARD_GESTURE_FLAG_BOTH;
		}
	}

	// Double stab counts as nothing
	if (isSimultaneous && gesture == WIZARD_GESTURE_STAB)
		return;

	mainHand->gestures.Add(handGesture);
	OnGestureLocked.Broadcast(this, gesture, mainHand->handFlag);

	TArray<const WizardSpellDef *> & castSpells = ScratchSpellArray;
	GetCastSpells(*mainHand, &castSpells);

	if (castSpells.Num()) {
		SpellSortForCasting sorter;
		castSpells.Sort(sorter);

		const WizardSpellDef * spell = castSpells[0];
		if (!m_validator || m_validator->CanCastSpell(spell)) {
			EWizardHandFlags flags = spell->usesBothHands ? WIZARD_HAND_FLAG_BOTH : mainHand->handFlag;
			OnSpellCast.Broadcast(this, spell->name, flags);
		}
	}

}

I’ve updated the code to not use timers and the break still happens, so I can scratch timers off the list of possible causes.

Whew, found it. Figures it was because I did something silly.

The second parameter of FMath::RandomRange is actually inclusive, not exclusive. That’s what I get for assuming. This was causing an out-of-range enumeration value to be passed to the multicast delegate which is what was causing the breakpoint to be hit. Not sure if it’s possible to be able to generate a better error message in this case, but that’s what the issue was.