Skip to content

Player profile

The query functions read the active player’s profile. They do not describe an opponent, temporary fight equipment, or an item preview. Declare profile.read and call after profile loading, such as inside a UI or combat callback. Calling during mod loading or without an active profile raises an error.

Request the existing Eclipse switch on the story map, including its native button, map tint, replay battles and tutorial behavior.

Signature: sf2.profile.set_eclipse_mode(enabled)

Returns: A boolean: true if the requested mode is active and the story map is ready after the request (including when already in that mode). false if the map is unavailable, blocked, not in story mode, the native switch is absent, or its quest has not finished. A false result can follow a partial native transition; retry from a later user action instead of assuming the mode is unchanged.

When: From a user or story callback with an active profile on the ready map. Do not call from on_close cleanup, which rejects progression changes.

Requires: story.progression. enabled must be a boolean. Missing capability, wrong argument types or an unavailable host service raise errors.

-- Call from a map notification's button or on_back callback.
local function continue_in_normal_mode(view)
if sf2.profile.set_eclipse_mode(false) then
sf2.ui.close(view)
end
end

This request follows the native switch policy; it does not unlock Eclipse mode, bypass its tutorial, or write arbitrary profile variables. A refused request can leave your view open until the player can try again.

Signature: sf2.profile.level()

Returns: The active player’s level as an integer.

When: After a game profile has loaded.

Requires: profile.read.

-- Inside a UI callback; view contains a text widget named "level".
sf2.ui.set_text(view, "level", "Player level: " .. sf2.profile.level())

This is a fresh read of profile progression. It neither changes the level nor reports a mode’s generated opponent level.

Read saved progress for a fight without creating or changing its save record.

Signature: sf2.profile.fight(fight)

Returns: A fresh table with present (boolean), wins (integer) and losses (integer). A known fight with no saved record returns false, 0, 0.

When: After a profile loads, for example inside a battle_result callback. The host must have an active content catalog. An unavailable profile or unknown fight raises an error; it is not reported as an unwon fight.

Requires: profile.read. Pass a qualified fight ID string or a fight handle created in this script context. Other namespaces, including core, require a declared dependency. String queries do not require content.register.

local sf2 = require("sf2")
-- Also declare story.events and a dependency on core.
sf2.story.on("battle_result", function(event)
if event.outcome == "win" then
local progress = sf2.profile.fight("core:fights/zone_1/tournament/3")
if progress.wins >= 1 then
sf2.log.info("The first tournament prerequisite is complete.")
end
end
end)

These are the native WinCount/LossCount quest counters, stored as CompletedCount/LossCount. They do not select the separate eclipse counters based on the current mode, or count rounds. Query a distinct eclipse fight ID when that content has its own record. Native resets can change these counts; they are not an immutable lifetime history. Editing the returned table has no effect on saved progress. A new query reads the currently bound profile.

Ask whether the player has ever won a fight against an opponent.

Signature: sf2.profile.has_defeated(warrior)

Returns: true if the active profile has a recorded or proven win against warrior, otherwise false.

When: After a profile loads, for example in a UI, story or battle_result callback. Without an active profile the call raises an error.

Requires: profile.read. Pass one of:

  • a warrior handle from sf2.warriors.register, or a qualified ID such as "my_mod:warriors/storm_master";
  • a warrior template handle from sf2.warriors.get_template or sf2.warriors.register_template, or a qualified ID such as "core:warrior-templates/hermit_swords".

Other namespaces, including core, require a declared dependency. String queries do not require content.register.

local sf2 = require("sf2")
-- capabilities = ["profile.read"]; also declare the core dependency.
-- Call after profile loading, for example from a UI button callback.
local function hermit_beaten()
return sf2.profile.has_defeated("core:warrior-templates/hermit_swords")
end

What is recorded. Whenever the player wins a fight (story, tournaments, challengers, bosses, Underworld raids run by Eclipse, Sensei or any other fight that ends with the normal result screen), the beaten opponent is remembered permanently:

  • its warrior ID, if it is a mod warrior registered with sf2.warriors.register;
  • its warrior template ID, if the fight lists it with a template. This covers core opponents, which have no warrior IDs of their own; story bosses use their template, for example core:warrior-templates/lynx_claws, hermit_swords, butcher_backswords, wasp_naginata or shogun_katana. A mod warrior built on a template records both IDs.

A query matches either ID, so asking about a template is true after a win against any opponent built from that template. In a fight with several opponents in a row, only the opponent beaten at the end of the won fight is recorded. Losses, surrenders and draws record nothing. The records are kept in Eclipse’s own save data, which the original game never reads, and survive mod changes.

Earlier wins. Wins from before this record existed are proven by the game’s own fight records: a fight with at least one saved win counts as a win against an opponent when every warrior that fight lists has that warrior or template ID. Fights whose opponents are drawn from a random group, and fights listing different opponents, prove nothing. Most Underworld raid bosses share the generic core:warrior-templates/default template, so earlier raid wins do not identify a particular boss; only new wins against mod raid bosses with their own warrior or template ID can be told apart.

The answer never goes from true back to false, except when the player switches to another profile.

Signature: sf2.profile.item(item)

Returns: A new snapshot table with these fields:

Field Meaning
type Since 0.35: native item type, such as "Weapon" or "Consumable"; nil if the runtime item definition is unavailable.
subtype Since 0.35: native subtype, such as "Nunchaku"; an empty string means unspecified, nil means the runtime definition is unavailable.
present Boolean: an inventory record exists for this item.
owned Boolean: the record exists and its quantity is greater than zero.
count Integer native quantity; zero if no record exists.
equipped Boolean native equipped flag; false if no record exists.
upgrade Integer native upgrade index, or nil if no record exists. The native unspecified sentinel may be -1.

When: After a game profile has loaded.

Requires: profile.read and either an item handle obtained by this mod context or a qualified item ID string. Other namespaces, including core, require a declared dependency. String queries do not require content.register.

local sf2 = require("sf2")
-- Obtain handles during mod loading.
local nunchaku = sf2.items.get("core:items/weapon/weapon_nunchaku")
-- Call this function from an existing UI or gameplay callback.
local function describe_equipment()
local item = sf2.profile.item(nunchaku)
if item.equipped then
return "Nunchaku equipped"
elseif item.owned then
return "Nunchaku in inventory"
end
return "Nunchaku not owned"
end

An empty inventory record can have present = true and owned = false. Ownership is current state, not evidence of a past purchase. Type/subtype come from the runtime item catalog and are available for unowned items too. They preserve native spelling and case. They describe the resolved item, not a past purchase event or temporary combat state. Missing catalog metadata does not imply that an existing inventory record is unowned. Core item handles resolve to their native names; supported item redirects use the resolved item. Unknown or forged handles raise an error, rather than appearing to be unowned.

Editing the returned table does not change the inventory. Call again for current values; snapshots are not live handles. These functions expose no inventory, currency or progression mutations, and do not create event subscriptions.

Registration/binding and controlled native roster tests pass. Full-game profile switching and comparisons against the inventory UI remain acceptance checks.

Profile reset temporarily makes queries unavailable. Constructing an internal comparison roster does not change which player these queries read. A newly selected profile becomes readable after native activation and inventory preparation.

Read whether the active profile has learned a perk and its stored upgrade number. This queries the learned-perk list, not temporary combat effects, equipment enchantments or whether a trigger is currently active.

Signature: sf2.profile.perk(perk)

Returns: A fresh table with learned (boolean) and upgrade (integer when learned; nil otherwise). Upgrade zero is a valid stored value; it does not mean unlearned. The number is the native UpgradeLevel, not a count of purchases.

When: After the active game profile has loaded, for example in a UI or story callback. A profile switch or perk upgrade is reflected on the next query.

Requires: profile.read. Pass a perk handle acquired in the same script context, or a qualified perk ID string. Other namespaces, including core, require a declared dependency. Strings need no content.register capability. Malformed IDs, wrong categories, unavailable definitions, forged handles and queries without an active profile raise errors.

local sf2 = require("sf2")
local cobra = sf2.perks.get("core:perks/PERK_COBRA")
-- Call after profile loading, for example from your menu's button callback.
local function describe_cobra()
local perk = sf2.profile.perk(cobra)
if perk.learned then
return "Cobra upgrade: " .. perk.upgrade
end
return "Cobra has not been learned"
end

Changing this returned table changes no game state. Each call reads the active profile and returns detached values. Disabling/resetting a profile does not leave its old perk list available to subsequent queries.

Automated checks cover native host mapping/lifetime with controlled roster services, Lua capability/handle rejection and snapshot isolation. Full-game learning, upgrading, reset and save/reload acceptance remain pending.

With story.events, profile.read, and a core dependency, an observer can inspect core items discovered at runtime without obtaining handles at load time:

local sf2 = require("sf2")
sf2.story.on("item_acquired", function(event)
-- Other mods need their own declared dependencies before querying their items.
if event.item and event.item:match("^core:") then
local item = sf2.profile.item(event.item)
sf2.log.info(event.item .. " currently owned: " .. item.count)
end
end)

The query reads current inventory; nested grants can make it newer than the event’s count snapshot. Unknown event items are nil; skip them. A qualified but unavailable definition raises an error instead of reporting an unowned item.

Inspect the active profile’s equipped records without knowing their item IDs in advance.

Signature: sf2.profile.equipment()

Returns: A fresh contiguous array of snapshot tables. An empty array means no records are marked equipped. Each record contains:

Field Meaning
item Qualified definition ID, or nil when the API cannot identify the native item.
type Native item type, or nil when metadata is unavailable.
subtype Native subtype; empty string means unspecified, nil means metadata unavailable.
count Native inventory quantity.
owned Whether quantity is greater than zero.
upgrade Stored upgrade index, possibly the native unspecified sentinel -1; nil if unavailable.
enchantments Array of qualified perk ID strings for the item’s current enchantments, in native order. Always present; empty when the item has none. Perks the API cannot identify are left out.

When: After an active game profile has loaded, including UI and story callbacks.

Requires: profile.read. No item handles or content.register capability. An unavailable profile raises an error. Enumeration includes all native equipped records, including records from other mods. Passing a returned ID to a separate profile.item query still requires that namespace’s declared dependency.

local sf2 = require("sf2")
-- Call from a story condition or UI callback after profile loading.
local function wearing_katana()
for _, item in ipairs(sf2.profile.equipment()) do
if item.owned and item.type == "Weapon" and item.subtype == "Katana" then
return true
end
end
return false
end

Definition IDs are normalized to lower case, so compare in lower case, for example "core:perks/perk_hermitstorm". This example reports whether any worn item carries a given enchantment, such as a forged ability:

local sf2 = require("sf2")
-- Call from a story, UI or combat callback after profile loading.
local function wearing_enchantment(perk_id)
perk_id = perk_id:lower()
for _, item in ipairs(sf2.profile.equipment()) do
if item.owned then
for _, perk in ipairs(item.enchantments) do
if perk == perk_id then return true end
end
end
end
return false
end
local storm = wearing_enchantment("core:perks/PERK_HERMITSTORM")

This reads profile equipment, not a fight’s temporary rule-imposed loadout or the opponent. It includes every record marked equipped by the native inventory, rather than assuming exactly five slots. Do not use array positions as slot identifiers; inspect type and subtype. Records with zero quantity remain visible with owned = false. Unknown items retain their available metadata and quantity. Changing the array or its entries changes no game state; query again after an equipment change. This function does not equip or grant anything.

Production-query/Lua fixture checks cover identity, missing metadata, empty lists, snapshot isolation and profile unbinding using controlled inventory services. Full-game comparisons against the equipment UI and temporary battle loadouts remain acceptance checks.