Skip to content

UHC events

The GDK fires a small set of UHC-specific events on top of vanilla Bukkit events. Modules, roles and listeners subscribe to them with the standard @EventHandler annotation. This page lists every event currently fired, when it fires, and what payload it carries. Every UHC event extends org.bukkit.event.Event and is dispatched on the main server thread.

Game start and finish are split into a before / after pair so subscribers can choose where to plug in.

Fired once the world is ready and start preconditions have all passed, but before the GDK spreads players, distributes kits, or starts the cyclic timers. Use it for last-mile setup that needs the start-of-game world but should observe the playing state being installed (composition snapshots, listener pre-registration). No payload.

Fired once the game session has fully transitioned into the playing phase. Timers are running, scenarios are enabled, the active module’s listeners are registered, and the active module’s onGameStart(context) callback has already fired. No payload.

@EventHandler
fun onGameStarted(@Suppress("UNUSED_PARAMETER") event: UHCGameStartedEvent) {
// Any "the game has begun" state setup that is not captured by the active
// module's onGameStart callback, e.g. cross-cutting analytics, per-server logs.
}

UHCGameStartingEvent fires before the active module’s onGameStart(context) callback runs; UHCGameStartedEvent fires after. Prefer the module callback for module-internal state, and reserve the events for cross-cutting consumers.

UHCGameFinishesEvent(victoryResult: VictoryResult)

Section titled “UHCGameFinishesEvent(victoryResult: VictoryResult)”

Fired when a game is about to end, before the framework unregisters scenario / module listeners and switches players to spectator. Carries the VictoryResult so subscribers see the outcome while the playing state is still live. Use this to snapshot rosters, persist game state, append a “game finished” log boundary, or run any role-specific reset that needs to fire while listeners are still bound.

@EventHandler
fun onGameFinishes(event: UHCGameFinishesEvent) {
when (val result = event.victoryResult) {
is VictoryResult.SingleWinner -> auditLog.append("winner=${result.player.name}")
is VictoryResult.MultipleWinners -> auditLog.append("winners=${result.players.map { it.name }}")
is VictoryResult.CustomAnnouncement -> auditLog.append("custom=${result.message}")
is VictoryResult.NoWinnerYet -> auditLog.append("forced-stop")
}
contracts.clear()
}

UHCGameFinishedEvent(victoryResult: VictoryResult)

Section titled “UHCGameFinishedEvent(victoryResult: VictoryResult)”

Fired after the post-game cleanup completes: scenarios and module listeners are unregistered, players are in spectator, the kills leaderboard has been broadcast. Use this when your subscriber needs to observe the world after the game is fully torn down (post-game UI, redirect to lobby, …).

A clean pattern is to have every role and per-game listener subscribe to UHCGameFinishesEvent to clear their own state. The module’s onGameFinish callback is then free to focus on module-level (not role-level) cleanup, which keeps role state co-located with the role’s other lifecycle hooks.

UHCDayStartEvent(dayNumber: Int, episode: Int)

Section titled “UHCDayStartEvent(dayNumber: Int, episode: Int)”

Fired at the start of each logical day, including the very first day at game start. dayNumber is the 1-based in-game day index and increments by one every full cycle. episode is the GDK episode index at the moment the event fires (an episode is the 20-min beat that paces the game; the first one is 10 min long). The two indices are independent: a single episode can span multiple day/night transitions.

UHCNightStartEvent(dayNumber: Int, episode: Int)

Section titled “UHCNightStartEvent(dayNumber: Int, episode: Int)”

Fired at the start of each logical night. Night N belongs to day N: the first night fires UHCNightStartEvent(1, episode) ten minutes into the game.

@EventHandler
fun onNight(event: UHCNightStartEvent) {
rolesManager.getPlayersOfRole(this).forEach { uuid ->
Bukkit.getPlayer(uuid)?.addPotionEffect(NIGHT_STRENGTH, true)
}
}

UHCDayEndsEvent(dayNumber: Int, episode: Int) and UHCNightEndsEvent(dayNumber: Int, episode: Int)

Section titled “UHCDayEndsEvent(dayNumber: Int, episode: Int) and UHCNightEndsEvent(dayNumber: Int, episode: Int)”

Fired immediately before the next half-cycle starts, so the (Ends → Start) pair always fires together at a transition. Use the End events to close anything you opened on the matching Start event (a wolf-only chat window, a “beware the night” boss bar, …) without double-bookkeeping which phase is currently active.

All four events are derived from the GDK’s logical cycle (10 min day 1, then alternating 10 min night / 10 min day) and are independent of Bukkit world time. See Scheduling & time for the cycle reference and the timewarp catch-up semantics (huge warps collapse to the final transition to avoid event floods).

Fired when an active UHC player dies. Distinct from UHCPlayerEliminationEvent: a player can die without being eliminated when the active module returns EliminationDecision.KeepAlive or Deferred.

UHCPlayerKilledByPlayerEvent(killer: Player, victim: Player)

Section titled “UHCPlayerKilledByPlayerEvent(killer: Player, victim: Player)”

Fired immediately before UHCPlayerDeathEvent when a killer can be resolved, either via Bukkit’s native killer or via the GDK’s last-damager tracker (the player that landed the most recent hit within the tracking window). Use this when your handler needs to read the killer / victim relationship while the team and role state still reflects “the moment of the kill”, before any other listener tears it down.

@EventHandler(priority = EventPriority.HIGH)
fun onKilledByPlayer(event: UHCPlayerKilledByPlayerEvent) {
val killer = event.killer
if (rolesManager.getCurrentRole(killer.uniqueId) !== this) return
// ...
}

Fired when the GDK permanently removes the player from the game (EliminationDecision.Eliminate returned, or a deferred decision resolved to Eliminate, or the player quit during a deferred limbo window). Subscribers observe a player who is about to be moved to spectator; the event fires before the spectator switch and the broadcast.

UHCPlayerResurrectionEvent(player: Player, location: Location)

Section titled “UHCPlayerResurrectionEvent(player: Player, location: Location)”

Fired when the framework brings a previously dead player back into the game (typically as the result of a deferred elimination resolving to KeepAlive, or a module-driven revivePlayer call). The location is where the player just respawned.

@EventHandler
fun onResurrection(event: UHCPlayerResurrectionEvent) {
event.player.sendMessage("${GDKMain.GDK_PREFIX_SUCCESS} You are back in the game.")
}

UHCPlayerEntersLimboEvent(player: Player, limboLocation: Location, delayTicks: Long)

Section titled “UHCPlayerEntersLimboEvent(player: Player, limboLocation: Location, delayTicks: Long)”

Fired when an EliminationDecision.Deferred opens a limbo window (the framework freezes the player at limboLocation for delayTicks ticks while waiting for the deferred decision to resolve). Use it to mark UI, open a one-shot prompt to another player (“the Witch can save them”), or schedule any countdown that should align with the limbo timeout. The decision still resolves automatically; this event is purely informational.

UHCGameRoleAssignedEvent(playerUuid: UUID, role: UHCGameRole, reason: Reason)

Section titled “UHCGameRoleAssignedEvent(playerUuid: UUID, role: UHCGameRole, reason: Reason)”

Fired by GDKGameRolesManager every time a role is bound to a player. Reasons:

ReasonTriggered by
ACTUALInitial attribution or any later assignActualRole call.
IMPERSONATIONA setImpersonatedRole call (e.g. a thief stealing).
SWAPReserved for future use by swapPlayersRoles flows.

The role is already registered as a Bukkit listener when the event fires, so the role’s own handler can subscribe with the standard “filter by this” idiom and react to its assignment without a side hook:

@EventHandler
fun onAssigned(event: UHCGameRoleAssignedEvent) {
if (event.role !== this) return
wolfChat.setEligibility(event.playerUuid, WolfChatEligibility.SEND_AND_RECEIVE)
}

UHCGameRoleUnassignedEvent(playerUuid: UUID, role: UHCGameRole, reason: Reason)

Section titled “UHCGameRoleUnassignedEvent(playerUuid: UUID, role: UHCGameRole, reason: Reason)”

Mirror of the assigned event, fired before the role’s listener is unregistered (so the role’s own handler still sees the event). Reasons match UHCGameRoleAssignedEvent.Reason. Pair the two handlers to keep per-carrier state co-located with the role.

UHCRolesAttributedEvent(rolesByPlayer: Map<UUID, GDKNamespacedKey>)

Section titled “UHCRolesAttributedEvent(rolesByPlayer: Map<UUID, GDKNamespacedKey>)”

Fired once when the GDK finishes the initial role dispatch (see GDKGameManager.attributeRoles). The map is immutable and uses each role’s GDKNamespacedKey id as the value. Subscribers use it to render a “the roles are out” UI, to push the assignment into an external observability backend, or to refresh a faction-private chat channel now that every faction member is known.

@EventHandler
fun onRolesAttributed(event: UHCRolesAttributedEvent) {
event.rolesByPlayer.forEach { (uuid, roleId) ->
analytics.recordAttribution(uuid, roleId.asString())
}
}

UHCRolesSwappedBetweenPlayersEvent(players: Pair<Player, Player>, reason: Reason)

Section titled “UHCRolesSwappedBetweenPlayersEvent(players: Pair<Player, Player>, reason: Reason)”

Fired when two players’ roles are swapped (a “thief”-style steal, a “trickster”-style switch, or any custom swap mechanic). The reason enum values currently include VOLEUR, TRUBLION, and OTHER; pick the one that best matches the mechanic, or OTHER for module-specific swaps that do not fit either label. Order within the pair mirrors the constructor arguments.

val swap = UHCRolesSwappedBetweenPlayersEvent(
victim,
thief,
UHCRolesSwappedBetweenPlayersEvent.Reason.OTHER,
)
Bukkit.getPluginManager().callEvent(swap)

Use this event for cross-cutting reactions (analytics, action logs, scoreboard refresh) rather than coupling your role mechanic to its caller.

UHCDynamicTeamDissolvedEvent(team: UHCGameTeam, reason: Reason)

Section titled “UHCDynamicTeamDissolvedEvent(team: UHCGameTeam, reason: Reason)”

Fired when a dynamic sub-team (a team created via GDKGameTeamsManager.createDynamicTeam, e.g. a Cupidon couple) is dissolved. Two reasons:

ReasonTriggered by
EXPLICITA direct dissolveTeam(team) call (for instance a role tearing the team down on its own conditions).
LAST_MEMBER_ELIMINATEDThe framework’s own auto-dissolve, fired after UHCPlayerEliminationEvent when no surviving member of the team remains.

The static teams returned from UHCGameModule.listGameTeams() are never auto-dissolved; only dynamic instances created at runtime are.

The most subtle ordering is around death. The GDK fires events in this sequence on a vanilla PlayerDeathEvent for an active player:

PlantUML Diagram

UHCPlayerKilledByPlayerEvent is fired before UHCPlayerDeathEvent so a role’s kill listener (e.g. “did the killer just hit their assigned target?”) can observe team / role state before another listener mutates it on the generic death event.

The repository’s roadmap includes first-class events for episode boundaries (UHCEpisodeStartEvent / UHCEpisodeFinishEvent) and additional player-facing events (UHCPlayerDamagesPlayerEvent, UHCPlayerDisplayRoleEvent, etc.). Until they ship, the recommended pattern for “I want a hook at episode start” is to schedule a one-shot via GDKTaskScheduler. The gdk-architecture reference page lists the planned event surface for context.