Skip to content

Events and fight sequences

A mode starts with an ordered roster of already registered fights. It controls which step is available, what happens after a win/loss, and whether entry costs a mod-owned item. Events and raids share this format. An optional Lua result callback can choose branches or finish a run.

The three registration functions below accept the same table:

Field Type Default Meaning
id String Required Local mode/event/raid ID.
fights Fight-handle array Required 1–100 distinct fights owned by this mod.
repeatable Boolean false Restart at the first step after completion.
reset_on_loss Boolean false Restart at the first step after a loss.
minimum_level Integer, 1–1000 1 Player entry requirement.
starts_at UTC Unix timestamp 0 Inclusive start time.
ends_at UTC Unix timestamp 0 Exclusive end time; zero means no end.
entry_item Consumable item handle Omitted Entry ticket owned by this mod.
entry_count Integer, 1–100000 Required with entry item Number consumed per fight attempt.
hard_mode Boolean false Raid-only Power Mode filter setting.
on_result Lua function Omitted Choose the next fight after native result settlement. See below.
on_prepare Lua function Omitted Generate an encounter or wait for a player choice before entry.

Timestamps are integer seconds since 1970-01-01 UTC, from 0 through 253402300799. A nonzero end must be after the start. Omit both for permanent availability. Supplying entry_count without entry_item is an error. Shared currencies cannot be entry tickets. Non-raid modes cannot set hard_mode=true.

A fight belongs to exactly one sequence step. Its battle must use the compatible category, and all referenced fights must exist. The examples below assume first and final are two fight handles registered by this mod; use only the function matching your intended content, not all three on the same fights.

Create a progression sequence, such as a repeatable trial.

Signature: sf2.modes.register(definition)

Requires: content.register.

When: Entrypoint, after registering its fights.

Returns: nil.

sf2.modes.register {
id = "training_trial",
fights = { first, final },
repeatable = true,
reset_on_loss = true,
}

Use the sequence fields above. Every owned map battle resolves to the sequence’s current fight. Without a callback override, a win advances it. A completed repeatable sequence starts again; a nonrepeatable one remains complete. A loss keeps or resets progress according to reset_on_loss.

Create a scheduled or permanently available event using the sequence format.

Signature: sf2.events.register(definition)

Requires: content.register.

When: Entrypoint, after its fights.

Returns: nil.

sf2.events.register {
id = "open_training",
fights = { first, final },
minimum_level = 6,
repeatable = true,
-- Add starts_at/ends_at only when a real UTC schedule is intended.
}

Use the same fields and limits as a mode. Eligibility is checked again when the player actually enters, not only when the map was first shown. Event registration does not add a general Lua event bus; combat callbacks are documented separately.

Register a local offline raid sequence.

Signature: sf2.raids.register(definition)

Requires: content.register.

When: Entrypoint, after its fights with battle type "raid".

Returns: nil.

-- boss_fight belongs to this mod and to a battle registered with type="raid".
sf2.raids.register {
id = "local_boss",
fights = { boss_fight },
repeatable = true,
reset_on_loss = true,
hard_mode = false,
}

A raid battle without a raid sequence registration is rejected. Raid victory requires defeating the boss; surviving a timeout does not award a win. This is an offline content API, not matchmaking, multiplayer, or server leaderboard access. See offline raids for boss health and rewards.

Signature: on_result = function(result) ... end

Returns: A fight handle from this mode’s fights roster, "complete", or nil. A fight handle selects that encounter for the next attempt, including retries, backward routes and skipped encounters. "complete" finishes the run and increments its saved completion count, even when the last fight was lost. Repeatable modes then restart at the first encounter. nil uses the normal win/loss progression described above. Other return values are errors.

When: Once when the native host settles an entered mode fight, after its fight outcome and reward handling. Also available on event and raid registrations. This synchronous callback cannot wait for a UI choice. It runs with a bounded instruction budget. Failure, invalid results or budget exhaustion log a warning and use default progression; the same native result cannot invoke it twice.

Requires: content.register to register the mode. There is no fighter or reward capability in this callback. Other APIs still require their own manifest capabilities. Returning a route does not change the native fight’s win/loss result or replace its reward calculation.

result is a fresh snapshot table. Changing it does not change the native result.

Field Type Meaning
won Boolean Native settled fight outcome.
step Integer One-based roster position of the fight just played.
total Integer Number of registered roster entries, not a route length.
completions Integer Saved completed runs before this result.
fight_id String Qualified definition ID of the fight just played.
-- first, middle and final are fight handles already registered by this mod.
sf2.modes.register {
id = "branching_trial", fights = { first, middle, final }, repeatable = true,
on_result = function(result)
if not result.won then return first end
if result.step == 3 then return "complete" end
if result.step == 1 and result.completions % 2 == 0 then return final end
return nil
end,
}

This alternates a short route with a full route. The selected encounter and run completion count persist through the existing mode save path. Local Lua variables are session memory; use the owned state API when additional state must persist. Callback side effects are not rolled back if it later fails, so validate decisions before changing owned state. Keep callbacks small and avoid opening a menu that implies settlement is waiting for its buttons.

Linear completion bricks and the linear count suffix are hidden for modes with this callback: jumping to roster entry three does not mean two fights were won. The map still resolves each owned battle to the saved selected encounter. See the complete Branching Trial example. Use on_prepare for generated encounters and pre-entry choices, and saved random streams for repeatable draws. A complete custom result/lobby lifecycle remains outside this contract.

Entry cost applies per fight attempt. Re-entering an interrupted reserved attempt does not charge again; a failed scene launch refunds a new reservation. Progress and reservations survive save/load and temporary mod removal.

Keep published sequence identities stable. A saved sequence that no longer matches its definition is rejected rather than silently reinterpreted. Test updates on a separate save before shipping a changed sequence.

Signature: on_prepare = function(request, event) ... end

Returns: An encounter plan to resolve immediately, or nil to leave the request pending. An empty plan {} accepts the current fight unchanged.

When: The player attempts entry into a mode whose current encounter has no saved plan. Runs before entry is charged or combat is launched. event contains one-based step, total, completions before this attempt, and the current blueprint’s qualified fight_id. Return a generated plan directly, or open a custom UI and call sf2.modes.resolve from its input callback later. Combat launch is deferred until the Lua callback has returned. Repeated Fight clicks while preparing do not create another request.

Requires: content.register on mode registration. UI choices also need ui.create; saved random draws need state.read and state.write. The callback is bounded to 200,000 instructions. Invalid results/errors cancel entry and report a diagnostic. It cannot yield a Lua coroutine. Owned state writes and random draws are not rolled back if the callback later fails or the player cancels. Draw only when committing a choice if cancellation should leave the stream unchanged, as in Generated Expedition.

sf2.modes.register {
id = "expedition", fights = { first, second, third }, repeatable = true,
on_prepare = function(_, event)
return {
warriors = { opponents[sf2.random.integer("encounter_rng", 1, #opponents)] },
level = event.step + 2,
rounds = 1, round_time = 60,
}
end,
}

This example assumes registered fight/warrior handles and a declared integer state field encounter_rng. Lua can calculate the roster, level and timing with normal loops, conditions and functions. It does not register new global fights.

An encounter plan is a detached description applied to the current registered fight blueprint. All fields are optional:

Field Meaning
warriors Dense array of 1–64 owned warrior handles. Order and repetitions are retained. Omit to inherit the blueprint roster.
player_character Owned warrior handle. Omit or use nil to inherit the blueprint’s player character, or its normal saved-player setup when none is declared.
level Integer 1–1000 applied to the encounter’s warriors. Omit to retain their declared levels.
rounds Integer 1–99; omit to inherit.
round_time Integer 1–3600 seconds; omit to inherit.
rules Dense array of 0–100 distinct owned rule handles. Omit to inherit the blueprint rule list; an explicit empty array removes all blueprint rules for this prepared encounter.
description String up to 1024 characters. Omit to inherit the blueprint description; an explicit empty string clears it for this prepared encounter.

The blueprint supplies location, music and rewards. Rules and description inherit from it unless the plan supplies replacements. Dynamic behavior rules in a plan run through the same active-encounter dispatch as behavior rules declared on the fight. Native reward settlement and entry tickets remain under host control. Only the generated instance changes; definitions and other fights keep their values. The host validates native construction before saving the plan and entering combat.

player_character lets a setup screen choose who the player controls. It uses that character’s declared body, skins, level, equipment and attributes for this encounter. Normal player controls and round rules apply, and saved equipment is not rewritten. The plan’s level field changes opponents only. Register the playable warrior in this mod before preparing the encounter; foreign handles, strings, booleans and unavailable characters are rejected. An explicit player choice affects only this generated instance, including its difficulty preview. The shared fight blueprint and other prepared instances keep their own setup.

-- In a setup view callback; hero is this mod's registered warrior handle.
sf2.modes.resolve(request, { player_character = hero })
sf2.ui.close(view)

Authored Fighter Lab provides a complete two-character chooser using these public callbacks.

Completed plans are stored with the mode’s step in the player save, including rule selections, the description override and any player-character choice. Reload and failed scene-launch retries reuse the same plan without running on_prepare or drawing random numbers again. Resolving a fight consumes its plan, including a loss that retries the same step. Pending UI/closures are not saved: leaving the scene, changing profile or disabling scripts cancels preparation. Press Fight again to recreate a pending choice. Plans with a player choice use save version 3. The host also reads version-1 plans and version-2 plans containing rules/description; omitting a player choice retains the existing version-1/2 save format. Unknown newer versions, changed rosters/rules or missing owned content are rejected with saved data preserved.

Random draws and other state writes made by Lua are not rolled back if a later callback or scene launch fails. Draw after the player commits a choice when canceling should not advance the random stream. Once the native save succeeds, the chosen plan and the saved stream travel together. This is deterministic encounter selection, not deterministic combat simulation.

Signature: sf2.modes.resolve(request, plan)

Returns: Nothing.

When: Complete a pending preparation, including from a UI callback. Reusing a resolved/canceled request or supplying an invalid plan is an error. It marks the request ready; native validation/save/entry occur after Lua returns.

Requires: The pending request supplied to this script’s on_prepare. Forged or foreign request tables are rejected. No extra capability.

-- Inside the setup view's on_click callback:
sf2.modes.resolve(request, { warriors = { selected_opponent }, level = 4 })
sf2.ui.close(view)

Signature: sf2.modes.cancel(request)

Returns: Nothing.

When: Cancel an unfinished setup, for example in the view’s on_close. No ticket is charged and no fight starts. Repeated cancellation is harmless. Cancellation after resolution is a no-op, so closing a successful choice view cannot undo the selected plan. Scene/script teardown can still prevent launch.

Requires: An owned request. No extra capability.

on_close = function() sf2.modes.cancel(request) end

Signature: sf2.modes.is_pending(request)

Returns: true while awaiting a result, otherwise false.

When: Guard buttons or late callbacks against stale requests.

Requires: An owned request. No extra capability.

if sf2.modes.is_pending(request) then
sf2.modes.resolve(request, {})
end

The Generated Expedition example combines a saved random stream, generated opponent roster and asynchronous game- styled setup UI. Its runtime fixture tests the shipped Lua, cancellation, deferred entry and save/reload. Full-game combat acceptance remains a playtest.