On this page 26 sections
- Events
- blse.post_load
- blse.level_init
- blse.level_shutdown
- blse.level_change
- blse.game_frame
- blse.game_loaded
- blse.before_save
- blse.game_saved, blse.before_load, blse.load_failed, blse.after_load, blse.save_deleted
- blse.engine_restart
- Entity outputs
- Entities appearing and disappearing
- What the player is looking at
- blse.look_target_changed, blse.using_changed, blse.discipline_target_changed
- Gameplay events
- blse.attack
- blse.quest_changed
- blse.entity_input
- Combat, stealth and blocking inputs
- Each NPC's fight
- Animations
- Outfits
- Character creation
- blse.set_chargen_clan_rule
- Saving and loading
- Engine restart
Events are how a Python mod notices what happens in the game: a level starts, the player saves, an NPC dies, a door opens, the player looks at something, a character is made. You subscribe a function to an event's topic, and BLSE calls it every time the event happens. This page lists the events that are not about one special subject, and the functions that read the same things on demand (what the player looks at, whether a fight is on). Events about dialogue, items, combat and the other subjects are on their own pages (dialogue, items, combat, ...).
A minimal working example: log every level the player enters, and say whether it came from a save.
import blse
def on_level(topic, data):
map, old, landmark, loaded = data.split('\0')
if loaded == '1':
blse.log('mymod: loaded a save in %s' % map)
else:
blse.log('mymod: entered %s from %s' % (map, old or '-'))
blse.subscribe('blse.level_init', on_level)Important, before you start:
- Every handler gets
(topic, data):datais one string; events with several fields separate them with NUL characters, sodata.split('\0')gives a list. Numbers (handles, states) arrive as text: useint(...). - Many events are only sent on a recognized game build (
blse.build_id()is notNone). On other builds you are never called; nothing fails. - Most game events reach Python at the next safe point, the end of the current server frame, not at the very moment they happen. The entity may already be gone then: that is why payloads carry names and handles. Where an event is delivered at once, its entry says so.
- Keep handlers short, especially for frequent events (
blse.game_frameruns every frame,blse.entity_outputandblse.entity_inputmany times a second in busy places). Return early when the event is not one you care about. - Subscribe once, at the top of your script, not inside another handler: a subscription made in
blse.level_initis added again at every level. - After an engine restart Python's subscriptions are gone; your script runs again and subscribes again (see Engine restart).
How to read the entries: each gives the topic, when exactly it is sent, the payload's fields in order, and a handler. "Handle" is a number naming one entity while it exists (see Words used in these pages).
Events#
| Topic | data |
|---|---|
blse.post_load |
''. Sent once at startup after all plugins have loaded; in practice before most scripts can subscribe |
blse.level_init |
map + '\0' + old_map + '\0' + landmark + '\0' + loaded; loaded is '1' when the level was restored from a save. A level transition (door, area exit) has old_map and landmark set and loaded = '0' |
blse.level_shutdown |
''. Sent once per level_init, before the level's entities are destroyed |
blse.level_change |
(b0.5.48) map, landmark = data.split('\0'): a level change was asked for (a door or exit, a taxi, a script; landmark may be ''). It happens on a later frame, so this arrives before level_shutdown: a companion mod can decide who goes along (block_companion_map) |
blse.game_frame |
'1' if the game simulated this frame, '0' if paused. Sent every server frame: keep the handler cheap |
blse.game_loaded |
The map name. Sent once on the first server frame after a save game was loaded and restored |
blse.before_save |
The map name. Sent right before the game writes a save: a save game, an autosave, or the current level's state at a level transition. Messages still queued for Python (entity outputs, damage, ...) are delivered first, then this one, immediately, so attributes set on G in any of them are in the save (b0.5.9) |
blse.game_saved |
name + '\0' + ok: a save was written (ok '1') or failed ('0'). name is the save's name as the game passed it: 'autosave', 'quick', 'Vampire-011.sav' (b0.5.27) |
blse.before_load |
The save's name ('quick', 'Vampire-010.sav'): a load was asked for, from the menu, F12 or the load command. Sent before anything is loaded (b0.5.27) |
blse.load_failed |
name + '\0' + reason: the load asked for did not happen ('the save does not exist', 'the save could not be restored'). No after_load follows (b0.5.27) |
blse.after_load |
name + '\0' + map: the save is loaded and restored; sent right after blse.game_loaded (b0.5.27) |
blse.save_deleted |
The save's name without .sav: a save file was deleted (load menu) (b0.5.27) |
blse.look_target_changed |
classname + '\0' + name + '\0' + handle: the crosshair moved to another entity (the one the game names on the HUD); all three empty when it moved off. Checked every game frame (b0.5.27) |
blse.engine_restart |
A 4-byte little-endian restart number |
blse.entity_output |
output + '\0' + classname + '\0' + name + '\0' + activator_classname + '\0' + activator_name. Sent when a game entity fires an output (see below). Only on recognized game builds |
blse.post_load#
Sent once when the game starts, after all native plugins have loaded. data is ''. Scripts run around the same time, so most scripts are not subscribed yet when it is sent: to run code at startup, put it at the top of your script instead.
blse.level_init#
Sent after a level has been set up (the map's entities exist): at a new game, a level change, a loaded save, the map command, and for the main menu's background level. The place to set up your mod for this level.
Fields: map, old_map, landmark, loaded = data.split('\0')
map: the level's map name ('sm_hub_1').old_map,landmark: for a level transition (a door, an area exit) the map the player came from and the name of the landmark entity that links the two maps; both''for a loaded save or amapcommand.loaded:'1'if the level was restored from a save, else'0'.
The main-menu background level reports as sp_genesisdevice_1. Loading a save first shuts down the menu level, then reports the saved map with loaded = '1'. Save data written before a transition is still there after it. To read your saved values after a load, use blse.game_loaded instead: at level_init they may still be the old ones.
blse.level_shutdown#
Sent once per level_init, just before the level's entities are destroyed (quit to menu, load, level change). data is ''. Clean up per-level things here: HUD elements, windows, lists of handles (they are invalid in the next level).
blse.level_change#
(b0.5.48) A level change was asked for (a door or exit, a taxi, a script); it happens on a later frame, so this comes before level_shutdown. Fields: map, landmark = data.split('\0') (landmark may be ''). Reaches Python at the next safe point, still before the change. A companion mod decides here who goes along (block_companion_map).
blse.game_frame#
Sent after every server frame (many times a second). data is '1' if the game simulated this frame, '0' if it is paused (menus, the console with the game paused). Use it for work that must run continuously (a timer, checking a key); keep it cheap.
ticks = 0
def on_frame(topic, data):
global ticks
if data != '1': return # paused
ticks = ticks + 1
if ticks % 60 == 0: pass # every 60th simulated frame
blse.subscribe('blse.game_frame', on_frame)blse.game_loaded#
Sent once on the first server frame after a save game was loaded and restored. data: the map name. The right moment to read values your mod saved (G, values on entities, the shared database).
blse.before_save#
(b0.5.9) Sent right before the game writes a save: a save game, an autosave, or the current level's state at a level transition. data: the map name. Messages still queued for Python are delivered first, then this one, at once, so values set on G in any of them are in the save. Write here anything you keep only in your own variables.
import __main__
def before_save(topic, data):
__main__.G.MyMod_Kills = kills
blse.subscribe('blse.before_save', before_save)blse.game_saved, blse.before_load, blse.load_failed, blse.after_load, blse.save_deleted#
(b0.5.27) The save's name for every save, load and deletion, so a mod can keep its own files next to each save. See Saving and loading.
| Topic | When | Fields |
|---|---|---|
blse.game_saved |
A save was written, or writing it failed | name, ok: the save's name as the game passed it ('autosave', 'quick', 'Vampire-011.sav'), '1' written or '0' failed |
blse.before_load |
A load was asked for (menu, F12, the load command), before anything is loaded |
name ('quick', 'Vampire-010.sav') |
blse.load_failed |
The load did not happen; no after_load follows |
name, reason ('the save does not exist', 'the save could not be restored') |
blse.after_load |
The save is loaded and restored, right after blse.game_loaded |
name, map |
blse.save_deleted |
A save file was deleted (load menu) | the name without .sav |
blse.engine_restart#
Sent when the game restarts its engine without quitting. data: a 4-byte little-endian number counting the restarts (1, 2, ...). Python state from before the restart is gone; see Engine restart.
Entity outputs#
blse.entity_output reports the game's entity outputs: the same events map makers connect in Hammer, about 250 kinds. Examples: OnDeath from npc_VVampire guard_1 (activator player), OnOpen/OnFullyOpen from doors, OnTrigger from triggers and logic_relay, OnDialogBegin/OnDialogEnd from NPCs, OnBeginSequence/OnEndSequence from scripted sequences, OnGrappleBegin (feeding grapple). Others in the game include OnFedUponBegin, OnSkillFail, OnMasqueradeLevelChanged, OnPlayerTookDamage and the OnActivate<Discipline> outputs of the player events entity.
import blse
def on_output(topic, data):
output, cls, name, act_cls, act_name = data.split('\0')
if output == 'OnDeath' and act_cls == 'player':
blse.log('player killed %s (%s)' % (name or '?', cls))
blse.subscribe('blse.entity_output', on_output)Python subscribers receive these at the next safe point: at the end of the current server frame (or just before blse.level_init/blse.level_shutdown), in the order they were fired. Outputs fire from deep inside the game's code, sometimes while the game's own Python is running, and calling Python right there can stall scripted scenes; delivering slightly later avoids that. The entities may be gone by the time the message arrives, so the payload carries their names. At most 4096 messages are queued per frame; extra ones are dropped and logged. Native plugins receive outputs immediately. The output name is empty if BLSE could not determine it (the entity that fired is not the output's owner).
Fields, in order: the output's name ('OnDeath'), the classname and name of the entity that fired it, the classname and name of the activator (the entity that caused it, often the player; empty when there is none). Names are the entities' targetnames from the map and are often ''. Only on recognized game builds.
Example:
import blse
def on_level(topic, data):
map, old, landmark, loaded = data.split('\0')
blse.log('entered %s from %s (from save: %s)' % (map, old or '-', loaded))
blse.subscribe('blse.level_init', on_level)The main-menu background level reports as sp_genesisdevice_1. Loading a save first shuts down the menu level, then reports the saved map with loaded = '1'. Going through a door from sm_hub_1 into the pawnshop reports map=sm_pawnshop_1 old=sm_hub_1 landmark=haven1. Save data set before a transition is still there after it.
Entities appearing and disappearing#
On recognized game builds (b0.5.13):
| Topic | data |
|---|---|
blse.entity_spawned |
classname, name, handle = data.split('\0'): an entity was created while the level runs (an NPC arriving, an item given or dropped, a projectile, ...). Sent at the end of the frame it was created in, when its name and keyvalues are set |
blse.entity_removed |
Same fields: an entity is being removed (picked up, killed and cleaned up, a finished scripted sequence, a used trigger). The entity still exists while this is delivered to native subscribers |
handle identifies one entity (the same number in both events). An entity removed in the frame it was created in is reported neither spawned nor removed. Entities created while a level loads or removed while it shuts down are not reported; use blse.level_init and the game's FindEntitiesByClass to look at a level's entities.
import blse
def on_spawn(topic, data):
cls, name, handle = data.split('\0')
if cls == 'npc_VCop':
blse.log('a cop arrived: %s' % handle)
blse.subscribe('blse.entity_spawned', on_spawn)When to use them: keep a list of the NPCs of a kind in the level, react to a projectile, notice an item dropped. handle is text: int(handle) gives the number other BLSE functions take. BLSE's own blse.find_entities also lists a level's entities.
What the player is looking at#
Each returns (classname, name, handle) (the handle as in the entity events) or None; all are None outside a level and on unknown builds.
| Function | The entity |
|---|---|
blse.look_target() |
Under the crosshair: the one the game names on the HUD (an NPC, an item, a door or door knob, a computer). blse.look_target_changed reports changes |
blse.using_object() |
What the player is in the middle of using: a door knob being tried, a sign or report being read, a carried prop. None again when the interaction ends |
blse.discipline_target() |
The creature a targeted Discipline would hit right now, the game's own pick. It is recalculated every frame and can flicker at the edge of range |
blse.picker_target() |
(b0.5.34) The console's !picker (what ent_* commands act on): the entity straight ahead, else the one nearest the crosshair; any entity, also ones the game does not name on the HUD. None for walls and the level's fixed decorations (they are part of the world, b0.5.35) |
blse.using_changed and blse.discipline_target_changed (b0.5.34) report changes of using_object() and discipline_target() like blse.look_target_changed (cls, name, handle = data.split('\0'), all empty for nothing). The Discipline target is recalculated every frame: walking past NPCs gives a target, then nothing, within a fraction of a second.
In the tuples the functions return, handle is a number (an int); in the events' payloads it is text. (b0.5.27; picker_target b0.5.34.)
When to use them: a mod that shows extra information about the NPC under the crosshair, a key that acts on what the player looks at, a Discipline mod that checks its target.
def inspect(name, args):
t = blse.look_target()
if t is None:
blse.console_print('nothing under the crosshair')
else:
cls, ent_name, handle = t
blse.console_print('%s %s (handle %d)' % (cls, ent_name or '(no name)', handle))
blse.add_command('mymod_inspect', inspect)blse.look_target_changed, blse.using_changed, blse.discipline_target_changed#
Sent when the entity returned by look_target() (b0.5.27), using_object() or discipline_target() (b0.5.34) changes. Fields: cls, name, handle = data.split('\0'), all three '' when it changed to nothing. Checked every game frame.
Gameplay events#
On recognized game builds:
| Topic | data |
|---|---|
blse.attack |
kind, cls, name, handle, weapon, shots = data.split('\0'): a character attacked. kind is 'melee' (every character's swings and punches, once per swing) or 'ranged' (every gun, the player's and NPCs' since b0.5.29; weapon is its classname, shots the bullets fired, usually '1') |
blse.quest_changed |
quest, state = data.split('\0'): the player's quest was set ('Tutorial', '2'), from dialogue or a script. Sent every time it is set, also to the state it already had |
blse.entity_input |
input, cls, name, value, act_cls, act_name, caller_cls, caller_name, blocked = data.split('\0') (blocked since b0.5.29: '1' if a rule or filter stopped it): an entity received an input: from the map (a door's Use, a relay's Trigger, OpenWindow on a sign) or from a game script (pc.AwardExperience("Tutor01") is the input AwardExperience sent to the player with the value 'Tutor01'; activator and caller are then empty). Many per second in busy places: filter by input name early |
Delivered at the end of the frame. Experience, Masquerade and Humanity changes are stat changes: blse.stat_change with the names experience, masquerade (it counts violations: it rises with each one) and humanity.
import blse
def on_attack(topic, data):
kind, cls, name, handle, weapon, shots = data.split('\0')
if cls == 'player' and kind == 'ranged':
blse.log('the player fired %s' % weapon)
blse.subscribe('blse.attack', on_attack)blse.attack#
A character attacked: once per melee swing or punch, once per gunshot. Fields: kind ('melee' or 'ranged'), the attacker's classname, name and handle, the weapon's classname, and shots (bullets fired, usually '1'). Ranged attacks of every character since b0.5.29. Use it for noise systems ("a gunshot was heard"), ammunition counters, statistics.
blse.quest_changed#
The player's quest state was set, from dialogue or a script. Fields: quest, state ('Tutorial', '2'). Sent every time it is set, also to the state it already had: compare with your own record if you need real changes.
def on_quest(topic, data):
quest, state = data.split('\0')
if quest == 'Tutorial' and state == '2':
blse.give_item(1, 'item_g_bloodpack')
blse.subscribe('blse.quest_changed', on_quest)blse.entity_input#
An entity received an input (a command sent to it): from the map's wiring or from a game script. Fields, in order: the input's name, the receiving entity's classname and name, the value ('' if none), the activator's classname and name, the caller's classname and name (the entity whose output sent it), and blocked ('1' if a block or change rule or a plugin filter stopped it; b0.5.29). Frequent: compare the input name first and return.
Combat, stealth and blocking inputs#
blse.combat_state (event), blse.combat_state() |
'combat' while a living NPC has the player as its enemy and hates it (since b0.5.30: bystanders fleeing from the player also target it, but fear it, and do not count), 'alert' while nobody fights but the stealth meter shows the player noticed or detected, 'normal' otherwise. Checked ten times a second; calming down waits 2 seconds. combat_state() is '' until the first check in a level. When a level ends (quit, load, level change) during a fight or alert, the event says 'normal' |
blse.stealth_changed (event) |
old, new, meter = data.split('\0'): the stealth level changed: 0 unseen (green), 1 noticed (yellow), 2 detected (red, "!"); meter is the percentage the HUD shows at that moment |
blse.stealth_state() |
(level, meter) now (the meter changes constantly: read it when needed), or None outside a level |
blse.block_input(input[, target[, blocked]]) |
While blocked, entities ignore the input: any entity, or target by classname or name. blse.block_input('Use', 'func_door_rotating') locks hinged doors; doors opened through their knob need blse.block_input('Use', 'prop_doorknob') too. blocked=0 unblocks; blse.clear_input_blocks() removes all of them |
blse.change_input(input, target, value[, when]) |
(b0.5.33) Entities (any if target is '', else by classname or name) get the input with value instead of its own; with when, only when its own value is when. value=None removes the change; blse.clear_input_blocks() removes changes too, and so does a Python restart. Blocks win over changes. Only inputs that carry a value can be changed (pc.MoneyAdd(5), pc.AwardExperience('Tutor01')); value is converted to the input's type |
Native plugins can also change an input's value with code (add_input_filter, see plugins).
blse.combat_state#
blse.combat_state() # -> 'combat', 'alert', 'normal' or ''The event blse.combat_state sends the new state as data when it changes; the function returns the current one ('' until the first check in a level). Use it for combat music, a "you cannot rest now" rule, a HUD warning.
def on_combat(topic, data):
if data == 'combat': blse.show_text('You are under attack!', 'top')
blse.subscribe('blse.combat_state', on_combat)blse.stealth_changed, blse.stealth_state#
blse.stealth_state() # -> (level, meter) or Nonelevel: 0 unseen, 1 noticed, 2 detected. meter: 0 to 100, the percentage the HUD's stealth meter shows. The event blse.stealth_changed sends old, new, meter (as text) when the level changes.
blse.block_input, blse.clear_input_blocks#
blse.block_input(input[, target[, blocked]]) # -> 1
blse.clear_input_blocks()Makes entities ignore an input. When to use it: lock doors ('Use' on func_door_rotating and prop_doorknob), stop a script from giving experience, keep a button from working.
input: the input's name ('Use','AwardExperience'); must not be empty (ValueError).target: a classname or a name;Noneor left out: every entity.blocked: 1 (default) blocks, 0 removes this block.clear_input_blocks()removes every block and change made from Python. ReturnsNone.- A blocked input still appears in
blse.entity_inputwithblocked'1'.
blse.change_input#
blse.change_input(input, target, value[, when]) # -> 1(b0.5.33) Entities get the input with value instead of the value it carries. When to use it: give more experience than a quest script awards (change_input('AwardExperience', 'player', 'Tutor02', 'Tutor01')), change an amount of money.
input: the input's name.target: a classname or a name;''any entity.value: the new value as text, up to 255 characters;Noneremoves the change. Converted to the input's own type (a number, a string, a vector).when: only change it when the input's own value iswhen.- Blocks win over changes. Only inputs that carry a value can be changed. Errors:
ValueErrorfor an empty input or value, or a value over 255 characters.
Each NPC's fight#
blse.npc_combat (event) |
what, npc, cls, name, target, target_cls, target_name = data.split('\0'): what is 'start' (an NPC began to fight: alive, not idle, with an enemy it hates, the player or another character), 'target' (it turned to another enemy) or 'stop' (3 seconds after it stopped fighting; at once when it dies or is removed, and for every fight when the level ends). npc and target are entity handles (decimal) |
blse.hit (event) |
victim, vcls, vname, attacker, acls, aname, damage, kind = data.split('\0'): every hit, as blse.damage, with the victim's and attacker's handles (decimal; empty for none). Only built while anyone listens. See Companions |
blse.companion (event) |
what, npc, cls, name, map = data.split('\0'): a companion 'arrived' from map, 'stayed' on map, or was 'lost'. See Companions |
blse.combat_npcs() |
[(npc, cls, name, target, target_cls, target_name), ...] for the NPCs fighting now (handles as numbers) |
When to use them: a mod that reacts when a guard starts fighting, counts fights, or lets a companion help when another NPC attacks the player.
def on_npc_combat(topic, data):
what, npc, cls, name, target, tcls, tname = data.split('\0')
if what == 'start' and tcls == 'player':
blse.log('%s (%s) attacks the player' % (name or cls, npc))
blse.subscribe('blse.npc_combat', on_npc_combat)blse.combat_npcs() returns the same information for every NPC fighting now, as a list of tuples (handles as numbers); [] if none.
Animations#
(b0.5.49) blse.anim_started, blse.anim_ended and blse.anim_chosen: an entity's sequence started or ended, an NPC's AI picked a sequence for an activity. Payloads and examples in Animation.
Outfits#
blse.outfit_changed: what the player wears from BLSE changed (an outfit put on or taken off, or worn again after a load). now, before, model, why = data.split('\0'). See Outfits for every field and reason.
Character creation#
When the player makes a character for a new game (b0.5.47). All NUL-separated; delivered at once, Python too: the game is paused while the sheet is open.
| Event | data |
|---|---|
blse.chargen_started |
'1': the character sheet opened to make a character |
blse.chargen_step |
The questionnaire's screen being shown, by its name in vdata/system/charcreatewizard.txt in lower case: 'help_popup0' (the first choice: questionnaire, the sheet, or the Unofficial Patch's hunter), 'trait_male_vs_female', the questions, 'clan_chooser', ... |
blse.chargen_answer |
screen, button = data.split('\0'): a button was clicked (1-4 as shown), before the next screen |
blse.chargen_gender |
'male' or 'female' |
blse.chargen_clan |
number, name = data.split('\0'): the clan, set by the questionnaire or picked on the sheet (each step of the sheet's arrows), by the game's name for it: 'Player_Brujah', 'Player_Tremere', ... |
blse.chargen_history |
The history's number ('-1': none; the sheet sets that whenever the gender changes) |
blse.chargen_points |
how, stat, count = data.split('\0'): 'buy' or 'sell', a point spent or taken back on the sheet |
blse.chargen_autolevel |
The questionnaire spent the points by a template (its arguments) |
blse.chargen_finished |
clan, clan_name, gender, name, history, path, mode = data.split('\0'): Accept. path: 'questionnaire', 'manual' or 'hunter' (the Unofficial Patch's option) |
blse.chargen_cancelled |
'cancel' (back to the main menu) or 'level_shutdown' |
Clan rules while a character is made (b0.5.49): blse.set_chargen_clan_rule(clan, replacement) -> 1 or 0. Picking clan ('Brujah' or the game's 'Player_Brujah', any case) gives replacement instead; None blocks it (the current clan stays, and the sheet's arrows skip it, as they skip a clan a rule turns into the current one); '' removes the rule. Every way the clan is set goes through it: the questionnaire, the sheet's clan arrows, the console. The later rule for the same clan wins. Rules apply only while a character is made; set them any time (a script's first lines, or the main menu). blse.chargen_clan reports the clan applied after the rules. Native plugins also get filters that see every pick.
The game applies every choice to the player at once, so at chargen_finished the player already has the clan, stats and name. Change them there, give items or experience (the game sets experience to 0 just before), set G values: the level change to the game's start comes after it. The game is paused while the sheet is open, so these events reach Python at once rather than at the end of a frame.
def on_finished(topic, data):
clan, clan_name, gender, name, history, path, mode = data.split('\0')
blse.give_item(1, 'item_g_bluebloodpack')
blse.subscribe('blse.chargen_finished', on_finished)The questionnaire's screens, questions and answers come from vdata/system/charcreatewizard.txt: a mod that ships a changed copy changes them without code.
The fields of blse.chargen_finished, in order: the clan's number, its name ('Player_Tremere'), 'male' or 'female', the character's name, the history's number ('-1' none), how the character was made ('questionnaire', 'manual' or 'hunter'), and '1'. Recognized builds only.
blse.set_chargen_clan_rule#
blse.set_chargen_clan_rule(clan, replacement) # -> 1 or 0(b0.5.49) When to use it: a mod that adds a clan in place of another, or forbids a clan.
clan: the clan picked,'Brujah'or the game's'Player_Brujah', any case.replacement: the clan to give instead;Noneblocks the pick (the current clan stays);''removes the rule.- Returns 1, or 0 if the rule could not be set.
blse.set_chargen_clan_rule('Malkavian', None) # no Malkavians in this mod
blse.set_chargen_clan_rule('Ventrue', 'Toreador') # picking Ventrue gives ToreadorSaving and loading#
blse.game_saved, before_load, load_failed, after_load and save_deleted (see Events) give the save's name for every save, load and deletion, so a mod can keep its own files next to each save:
import blse
def on_saved(topic, data):
name, ok = data.split('\0')
if ok == '1':
blse.log('saved as ' + name)
blse.subscribe('blse.game_saved', on_saved)The order of events: saving sends blse.before_save (write your values now), then blse.game_saved. Loading sends blse.before_load, then blse.level_shutdown of the old level, blse.level_init of the saved one (loaded '1'), blse.game_loaded (read your values now) and blse.after_load; or blse.load_failed instead.
Engine restart#
When the game restarts its engine in-process, it clears all non-built-in modules and the __main__ namespace. BLSE then registers a fresh blse module, and all previous Python subscriptions are dropped. Scripts that run again after the restart simply subscribe again. Native plugins keep their subscriptions.