Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 72 additions & 16 deletions scripts/mod_loader/bootstrap/event.lua
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,25 @@ end

function Subscription:notify(args)
if not self.listenerFn then
error("Subscription is closed")
end

return pcall(function()
return self.listenerFn(unpack2(args))
end)
error("Subscription is closed")
end

local ok, result = xpcall(
function()
return self.listenerFn(unpack2(args))
end,
function(e)
-- Capture and return the stack trace of the xpcall
-- 2 makes it start a frame higher so it doesn't include
-- this error handling fn
return debug.traceback(tostring(e), 2)
end
)

return ok, result
end


--- Unsubscribes this Subscription the next time the event passed in argument
--- is triggered.
function Subscription:openUntil(event)
Expand Down Expand Up @@ -117,6 +128,9 @@ function Event:new(options)
if options[Event.SHORTCIRCUIT] then
self.options[Event.SHORTCIRCUIT] = true
end
if options.eventName then
self.eventName = options.eventName
end
end
end

Expand Down Expand Up @@ -209,19 +223,66 @@ function Event:unsubscribeAll()
end
end

local function isStackOverflowError(err)
function Event.isStackOverflowError(err)
return string.find(err, "C stack overflow")
end

local function buildErrorMessage(headerMessage, subscriptionCaller, dispatchCaller)
function Event.splitTrace(trace)
local firstLine, rest = trace:match("([^\n]+)\n?(.*)")
return firstLine, rest
end

function Event.stripAfterXpcall(trace)
local out = {}
for line in trace:gmatch("[^\n]+") do
if line:find("%[C%]: in function 'xpcall'") then
break
end
out[#out+1] = line
end
return table.concat(out, "\n")
end

function Event.tryExtractFireXxxxHooks(line)
-- captures: fire + anything + Hooks
return line:match("in function '(fire.*Hooks)'")
end

function Event.findFireXxxxHooks(trace)
for line in trace:gmatch("[^\n]+") do
local fn = Event.tryExtractFireXxxxHooks(line)
if fn then
return fn
end
end
return nil
end

function Event.buildErrorMessage(headerMessage, errorOrResult, fireFnName, subscriptionCaller, dispatchCaller)
fireFnName = fireFnName or Event.findFireXxxxHooks(dispatchCaller) or "<unable to determine>"
local firstLine, rest = Event.splitTrace(errorOrResult)
return string.format(
"%s\n- Subscribed at: %s\n- Dispatched at: %s",
"%s%s\n- Event\\Hook: %s\n- Call trace: \n %s\n- Subscribed at: %s\n- Dispatched at: %s",
headerMessage,
firstLine,
fireFnName,
string.gsub(Event.stripAfterXpcall(rest), "\n", "\n "),
string.gsub(subscriptionCaller, "\n", "\n "),
string.gsub(dispatchCaller, "\n", "\n ")
)
end

function Event:handleFailure(errorOrResult, creator, caller)
errorOrResult = errorOrResult or "<unspecified error>"
local message = self.buildErrorMessage("An event callback failed: ", errorOrResult,
self.eventName, creator, caller)
if Event.isStackOverflowError(errorOrResult) then
error(message)
else
LOG(message)
end
end

--- Fires this event, notifying all subscribers and passing all arguments
--- that have been passed to this function to them.
--- Arguments are passed as-is without any cloning or protection, so if you
Expand All @@ -235,13 +296,8 @@ function Event:dispatch(...)
for _, sub in ipairs(snapshot) do
local ok, errorOrResult = sub:notify(args)

if not ok and errorOrResult then
local message = buildErrorMessage("An event callback failed: " .. errorOrResult, sub.creator, caller)
if isStackOverflowError(errorOrResult) then
error(message)
else
LOG(message)
end
if not ok then
self:handleFailure(errorOrResult, sub.creator, caller)
elseif ok then
if errorOrResult and self.options[Event.SHORTCIRCUIT] then
return true
Expand Down
263 changes: 138 additions & 125 deletions scripts/mod_loader/bootstrap/modApi.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,132 +16,145 @@ modApi.events = {}
setmetatable(modApi.events, events_mt)

local t = modApi.events
t.onModMetadataDone = Event()
t.onModsMetadataDone = Event()
t.onModInitialized = Event()
t.onModsInitialized = Event()
t.onModLoaded = Event()
t.onModsLoaded = Event()
t.onModsFirstLoaded = Event()
t.onInitialLoadingFinished = Event()
t.onFtldatFinalized = Event()
t.onModContentReset = Event()
t.onTestsuitesCreated = Event()

t.onBoardClassInitialized = Event()
t.onPawnClassInitialized = Event()
t.onGameClassInitialized = Event()

t.onUiRootCreating = Event()
t.onUiRootCreated = Event()
t.onGameWindowResized = Event()
t.onConsoleToggled = Event()
t.onFrameDrawStart = Event()
t.onFrameDrawn = Event()
t.onWindowVisible = Event()

t.onMainMenuEntered = Event()
t.onMainMenuExited = Event()
t.onMainMenuLeaving = Event()
t.onHangarEntered = Event()
t.onHangarExited = Event()
t.onHangarLeaving = Event()
t.onHangarMechsSelected = Event()
t.onHangarMechsCleared = Event()
t.onHangarSquadSelected = Event()
t.onHangarSquadCleared = Event()
t.onGameEntered = Event()
t.onGameExited = Event()
t.onNewGameClicked = Event()
t.onContinueClicked = Event()
t.onSettingsChanged = Event()
t.onSettingsInitialized = Event()
t.onProfileChanged = Event()
t.onProfileCreated = Event()
t.onProfileDeleted = Event()
t.onDifficultyChanged = Event()
t.onLanguageChanged = Event()

t.onHangarUiShown = Event()
t.onHangarUiHidden = Event()
t.onEscapeMenuWindowShown = Event()
t.onEscapeMenuWindowHidden = Event()
t.onSquadSelectionWindowShown = Event()
modApi.eventNames = {
"onModMetadataDone",
"onModsMetadataDone",
"onModInitialized",
"onModsInitialized",
"onModLoaded",
"onModsLoaded",
"onModsFirstLoaded",
"onInitialLoadingFinished",
"onFtldatFinalized",
"onModContentReset",
"onTestsuitesCreated",

"onBoardClassInitialized",
"onPawnClassInitialized",
"onGameClassInitialized",

"onUiRootCreating",
"onUiRootCreated",
"onGameWindowResized",
"onConsoleToggled",
"onFrameDrawStart",
"onFrameDrawn",
"onWindowVisible",

"onMainMenuEntered",
"onMainMenuExited",
"onMainMenuLeaving",
"onHangarEntered",
"onHangarExited",
"onHangarLeaving",
"onHangarMechsSelected",
"onHangarMechsCleared",
"onHangarSquadSelected",
"onHangarSquadCleared",
"onGameEntered",
"onGameExited",
"onNewGameClicked",
"onContinueClicked",
"onSettingsChanged",
"onSettingsInitialized",
"onProfileChanged",
"onProfileCreated",
"onProfileDeleted",
"onDifficultyChanged",
"onLanguageChanged",

"onHangarUiShown",
"onHangarUiHidden",
"onEscapeMenuWindowShown",
"onEscapeMenuWindowHidden",
"onSquadSelectionWindowShown",
-- arguments: newPage, lastPage, wasOpen
t.onSquadSelectionPageChanged = Event()
t.onSquadSelectionWindowHidden = Event()
t.onCustomizeSquadWindowShown = Event()
t.onCustomizeSquadWindowHidden = Event()
t.onPilotSelectionWindowShown = Event()
t.onPilotSelectionWindowHidden = Event()
t.onDifficultySettingsWindowShown = Event()
t.onDifficultySettingsWindowHidden = Event()
t.onMechColorWindowShown = Event()
t.onMechColorWindowHidden = Event()
t.onAchievementsWindowShown = Event()
t.onAchievementsWindowHidden = Event()
t.onOptionsWindowShown = Event()
t.onOptionsWindowHidden = Event()
t.onLanguageSelectionWindowShown = Event()
t.onLanguageSelectionWindowHidden = Event()
t.onHotkeyConfigurationWindowShown = Event()
t.onHotkeyConfigurationWindowHidden = Event()
t.onProfileSelectionWindowShown = Event()
t.onProfileSelectionWindowHidden = Event()
t.onCreateProfileConfirmationWindowShown = Event()
t.onCreateProfileConfirmationWindowHidden = Event()
t.onDeleteProfileConfirmationWindowShown = Event()
t.onDeleteProfileConfirmationWindowHidden = Event()
t.onStatisticsWindowShown = Event()
t.onStatisticsWindowHidden = Event()
t.onNewGameWindowShown = Event()
t.onNewGameWindowHidden = Event()
t.onAbandonTimelineWindowShown = Event()
t.onAbandonTimelineWindowHidden = Event()
t.onStatusTooltipWindowShown = Event()
t.onStatusTooltipWindowHidden = Event()
t.onMapEditorTestEntered = Event()
t.onMapEditorTestExited = Event()
t.onPodWindowShown = Event()
t.onPodWindowHidden = Event()
t.onPerfectIslandWindowShown = Event()
t.onPerfectIslandWindowHidden = Event()
t.onWindowShown = Event()
t.onWindowHidden = Event()

t.onMissionChanged = Event()
t.onMissionDismissed = Event()
t.onIslandLeft = Event()
t.onGameStateChanged = Event()
t.onGameVictory = Event()
t.onSquadEnteredGame = Event()
t.onSquadExitedGame = Event()
t.onTilesetChanged = Event()
t.onFinalIslandHighlighted = Event()
t.onFinalIslandUnhighlighted = Event()
t.onFinalIslandSelected = Event()
t.onFinalIslandDeselected = Event()

t.onDeploymentPhaseStart = Event()
t.onLandingPhaseStart = Event()
t.onDeploymentPhaseEnd = Event()
t.onPawnUnselectedForDeployment = Event()
t.onPawnSelectedForDeployment = Event()
t.onPawnDeployed = Event()
t.onPawnUndeployed = Event()
t.onPawnLanding = Event()
t.onPawnLanded = Event()

t.onBoardAddEffect = Event()
t.onBoardDamageSpace = Event()

t.onShiftToggled = Event()
t.onAltToggled = Event()
t.onCtrlToggled = Event()
t.onKeyPressing = Event({ [Event.SHORTCIRCUIT] = true })
t.onKeyPressed = Event({ [Event.SHORTCIRCUIT] = true })
t.onKeyReleasing = Event({ [Event.SHORTCIRCUIT] = true })
t.onKeyReleased = Event({ [Event.SHORTCIRCUIT] = true })
"onSquadSelectionPageChanged",
"onSquadSelectionWindowHidden",
"onCustomizeSquadWindowShown",
"onCustomizeSquadWindowHidden",
"onPilotSelectionWindowShown",
"onPilotSelectionWindowHidden",
"onDifficultySettingsWindowShown",
"onDifficultySettingsWindowHidden",
"onMechColorWindowShown",
"onMechColorWindowHidden",
"onAchievementsWindowShown",
"onAchievementsWindowHidden",
"onOptionsWindowShown",
"onOptionsWindowHidden",
"onLanguageSelectionWindowShown",
"onLanguageSelectionWindowHidden",
"onHotkeyConfigurationWindowShown",
"onHotkeyConfigurationWindowHidden",
"onProfileSelectionWindowShown",
"onProfileSelectionWindowHidden",
"onCreateProfileConfirmationWindowShown",
"onCreateProfileConfirmationWindowHidden",
"onDeleteProfileConfirmationWindowShown",
"onDeleteProfileConfirmationWindowHidden",
"onStatisticsWindowShown",
"onStatisticsWindowHidden",
"onNewGameWindowShown",
"onNewGameWindowHidden",
"onAbandonTimelineWindowShown",
"onAbandonTimelineWindowHidden",
"onStatusTooltipWindowShown",
"onStatusTooltipWindowHidden",
"onMapEditorTestEntered",
"onMapEditorTestExited",
"onPodWindowShown",
"onPodWindowHidden",
"onPerfectIslandWindowShown",
"onPerfectIslandWindowHidden",
"onWindowShown",
"onWindowHidden",

"onMissionChanged",
"onMissionDismissed",
"onIslandLeft",
"onGameStateChanged",
"onGameVictory",
"onSquadEnteredGame",
"onSquadExitedGame",
"onTilesetChanged",
"onFinalIslandHighlighted",
"onFinalIslandUnhighlighted",
"onFinalIslandSelected",
"onFinalIslandDeselected",

"onDeploymentPhaseStart",
"onLandingPhaseStart",
"onDeploymentPhaseEnd",
"onPawnUnselectedForDeployment",
"onPawnSelectedForDeployment",
"onPawnDeployed",
"onPawnUndeployed",
"onPawnLanding",
"onPawnLanded",

"onBoardAddEffect",
"onBoardDamageSpace",

"onShiftToggled",
"onAltToggled",
"onCtrlToggled",
}

for _, event in ipairs(modApi.eventNames) do
t[event] = Event({ eventName = event })
end

modApi.shortcutEventNames = {
"onKeyPressing",
"onKeyPressed",
"onKeyReleasing",
"onKeyReleased",
}

for _, event in ipairs(modApi.shortcutEventNames) do
t[event] = Event({ eventName = event, [Event.SHORTCIRCUIT] = true })
end

-- //////////////////////////////////////////////////////////////////////////////
-- Hooks
Expand Down