package funkin.modding.events; import funkin.modding.IScriptedClass.IPlayStateScriptedClass; import funkin.modding.IScriptedClass; import funkin.modding.module.Module; /** * Utility functions to assist with handling scripted classes. */ @:nullSafety class ScriptEventDispatcher { /** * Invoke the given event hook on the given scripted class. * @param target The target class to call script hooks on. * @param event The event, which determines the script hook to call and provides parameters for it. */ public static function callEvent(target:Null, event:ScriptEvent):Void { if (target == null || event == null) return; target.onScriptEvent(event); // If one target says to stop propagation, stop. if (!event.shouldPropagate) { return; } // IScriptedClass switch (event.type) { case CREATE: target.onCreate(event); return; case STATE_CREATE: if (Std.isOfType(target, Module)) { var t:Module = cast(target, Module); t.onStateCreate(event); } return; case DESTROY: target.onDestroy(event); return; case UPDATE: target.onUpdate(cast event); return; default: // Continue; } if (Std.isOfType(target, IStateStageProp)) { var t:IStateStageProp = cast(target, IStateStageProp); switch (event.type) { case ADDED: t.onAdd(cast event); return; default: // Continue; } } else { // If the target doesn't support the event, stop trying to dispatch. if ([ScriptEventType.ADDED].contains(event.type)) return; } if (Std.isOfType(target, IDialogueScriptedClass)) { var t:IDialogueScriptedClass = cast(target, IDialogueScriptedClass); switch (event.type) { case DIALOGUE_START: t.onDialogueStart(cast event); return; case DIALOGUE_LINE: t.onDialogueLine(cast event); return; case DIALOGUE_COMPLETE_LINE: t.onDialogueCompleteLine(cast event); return; case DIALOGUE_SKIP: t.onDialogueSkip(cast event); return; case DIALOGUE_END: t.onDialogueEnd(cast event); return; default: // Continue; } } else { // If the target doesn't support the event, stop trying to dispatch. if ([ ScriptEventType.DIALOGUE_START, ScriptEventType.DIALOGUE_LINE, ScriptEventType.DIALOGUE_COMPLETE_LINE, ScriptEventType.DIALOGUE_SKIP, ScriptEventType.DIALOGUE_END ].contains(event.type)) return; } if (Std.isOfType(target, INoteScriptedClass)) { var t:INoteScriptedClass = cast(target, INoteScriptedClass); switch (event.type) { case NOTE_INCOMING: t.onNoteIncoming(cast event); return; case NOTE_HIT: t.onNoteHit(cast event); return; case NOTE_MISS: t.onNoteMiss(cast event); return; case NOTE_HOLD_DROP: t.onNoteHoldDrop(cast event); return; default: // Continue; } } else { // If the target doesn't support the event, stop trying to dispatch. if ([ ScriptEventType.NOTE_INCOMING, ScriptEventType.NOTE_HIT, ScriptEventType.NOTE_MISS, ScriptEventType.NOTE_HOLD_DROP ].contains(event.type)) return; } if (Std.isOfType(target, IBPMSyncedScriptedClass)) { var t:IBPMSyncedScriptedClass = cast(target, IBPMSyncedScriptedClass); switch (event.type) { case SONG_BEAT_HIT: t.onBeatHit(cast event); return; case SONG_STEP_HIT: t.onStepHit(cast event); return; default: // Continue; } } else { // If the target doesn't support the event, stop trying to dispatch. if ([ScriptEventType.SONG_BEAT_HIT, ScriptEventType.SONG_STEP_HIT].contains(event.type)) return; } if (Std.isOfType(target, IPlayStateScriptedClass)) { var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass); switch (event.type) { case NOTE_GHOST_MISS: t.onNoteGhostMiss(cast event); return; case SONG_START: t.onSongStart(event); return; case SONG_END: t.onSongEnd(event); return; case SONG_RETRY: t.onSongRetry(cast event); return; case GAME_OVER: t.onGameOver(event); return; case PAUSE: t.onPause(cast event); return; case RESUME: t.onResume(event); return; case SONG_EVENT: t.onSongEvent(cast event); return; case COUNTDOWN_START: t.onCountdownStart(cast event); return; case COUNTDOWN_STEP: t.onCountdownStep(cast event); return; case COUNTDOWN_END: t.onCountdownEnd(cast event); return; case SONG_LOADED: t.onSongLoaded(cast event); return; default: // Continue; } } else { // If the target doesn't support the event, stop trying to dispatch. if ([ ScriptEventType.NOTE_GHOST_MISS, ScriptEventType.SONG_START, ScriptEventType.SONG_END, ScriptEventType.SONG_RETRY, ScriptEventType.GAME_OVER, ScriptEventType.PAUSE, ScriptEventType.RESUME, ScriptEventType.SONG_EVENT, ScriptEventType.COUNTDOWN_START, ScriptEventType.COUNTDOWN_STEP, ScriptEventType.COUNTDOWN_END, ScriptEventType.SONG_LOADED ].contains(event.type)) return; } if (Std.isOfType(target, IStateChangingScriptedClass)) { var t = cast(target, IStateChangingScriptedClass); switch (event.type) { case STATE_CHANGE_BEGIN: t.onStateChangeBegin(cast event); return; case STATE_CHANGE_END: t.onStateChangeEnd(cast event); return; case SUBSTATE_OPEN_BEGIN: t.onSubStateOpenBegin(cast event); return; case SUBSTATE_OPEN_END: t.onSubStateOpenEnd(cast event); return; case SUBSTATE_CLOSE_BEGIN: t.onSubStateCloseBegin(cast event); return; case SUBSTATE_CLOSE_END: t.onSubStateCloseEnd(cast event); return; case FOCUS_LOST: t.onFocusLost(cast event); return; case FOCUS_GAINED: t.onFocusGained(cast event); return; default: // Continue; } } else { // If the target doesn't support the event, stop trying to dispatch. if ([ ScriptEventType.STATE_CHANGE_BEGIN, ScriptEventType.STATE_CHANGE_END, ScriptEventType.SUBSTATE_OPEN_BEGIN, ScriptEventType.SUBSTATE_OPEN_END, ScriptEventType.SUBSTATE_CLOSE_BEGIN, ScriptEventType.SUBSTATE_CLOSE_END, ScriptEventType.FOCUS_LOST, ScriptEventType.FOCUS_GAINED ].contains(event.type)) return; } if (Std.isOfType(target, IFreeplayScriptedClass)) { var t:IFreeplayScriptedClass = cast target; switch (event.type) { case CAPSULE_SELECTED: t.onCapsuleSelected(cast event); return; case DIFFICULTY_SWITCH: t.onDifficultySwitch(cast event); return; case SONG_SELECTED: t.onSongSelected(cast event); return; case FREEPLAY_INTRO: t.onFreeplayIntroDone(cast event); return; case FREEPLAY_OUTRO: t.onFreeplayOutro(cast event); return; case FREEPLAY_CLOSE: t.onFreeplayClose(cast event); return; default: // Continue; } } else { // If the target doesn't support the event, stop trying to dispatch. if ([ ScriptEventType.CAPSULE_SELECTED, ScriptEventType.DIFFICULTY_SWITCH, ScriptEventType.SONG_SELECTED, ScriptEventType.FREEPLAY_INTRO, ScriptEventType.FREEPLAY_OUTRO, ScriptEventType.FREEPLAY_CLOSE ].contains(event.type)) return; } if (Std.isOfType(target, ICharacterSelectScriptedClass)) { var t:ICharacterSelectScriptedClass = cast target; switch (event.type) { case CHARACTER_SELECTED: t.onCharacterSelect(cast event); return; case CHARACTER_DESELECTED: t.onCharacterDeselect(cast event); return; case CHARACTER_CONFIRMED: t.onCharacterConfirm(cast event); return; default: // Continue; } } else { // If the target doesn't support the event, stop trying to dispatch. if ([ ScriptEventType.CHARACTER_SELECTED, ScriptEventType.CHARACTER_DESELECTED, ScriptEventType.CHARACTER_CONFIRMED ].contains(event.type)) return; } // If we reach this line, it means a script event was dispatched while not being properly handled. // Throw an error so we know to add additional fallbacks. throw 'No corresponding function called for dispatched event type: ${event.type}'; } public static function callEventOnAllTargets(targets:Iterator, event:ScriptEvent):Void { if (targets == null || event == null) return; if (Std.isOfType(targets, Array)) { var t = cast(targets, Array); if (t.length == 0) return; } for (target in targets) { var t:IScriptedClass = cast target; if (t == null) continue; callEvent(t, event); // If one target says to stop propagation, stop. if (!event.shouldPropagate) { return; } } } }