BLSE0.6

Python mods

Events

On this page 26 sections

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.

Python
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): data is one string; events with several fields separate them with NUL characters, so data.split('\0') gives a list. Numbers (handles, states) arrive as text: use int(...).
  • Many events are only sent on a recognized game build (blse.build_id() is not None). 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_frame runs every frame, blse.entity_output and blse.entity_input many 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_init is 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 a map command.
  • 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.

Python
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.

Python
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.

Python
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:

Python
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.

Python
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.

Python
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.

Python
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.

Python
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#

Python
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.

Python
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#

Python
blse.stealth_state()  # -> (level, meter) or None

level: 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#

Python
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; None or left out: every entity.
  • blocked: 1 (default) blocks, 0 removes this block.
  • clear_input_blocks() removes every block and change made from Python. Returns None.
  • A blocked input still appears in blse.entity_input with blocked '1'.

blse.change_input#

Python
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; None removes 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 is when.
  • Blocks win over changes. Only inputs that carry a value can be changed. Errors: ValueError for 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.

Python
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.

Python
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#

Python
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; None blocks the pick (the current clan stays); '' removes the rule.
  • Returns 1, or 0 if the rule could not be set.
Python
blse.set_chargen_clan_rule('Malkavian', None)        # no Malkavians in this mod
blse.set_chargen_clan_rule('Ventrue', 'Toreador')    # picking Ventrue gives Toreador

Saving 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:

Python
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.