On this page 16 sections
(b0.5.48) A mod no longer has to replace the game's files. It is one folder in Bin/loader/blse/mods/, and BLSE makes the game find its files as if they were the game's own. Several mods can add to the same data file and change the same map without overwriting each other, and deleting the folder removes the mod. None of this needs programming: it is folders and text files.
New to modding? Start here walks through a first mod step by step. This page is the full reference.
A first example: a mod that makes the katana cheaper, with no code at all. Make these two files, start the game, and the katana costs 100 in every shop.
Bin/loader/blse/mods/cheap_katana/mod.ini
Bin/loader/blse/mods/cheap_katana/blse/merge/vdata/items/item_w_katana.txt; mod.ini
[mod]
name = Cheap katana
version = 1.0// blse/merge/vdata/items/item_w_katana.txt
WeaponData
{
"item_worth" "100"
}Read this first
- Mod folders are read when the game starts. Adding, removing or reordering a mod needs a game restart.
- Map patches only reach maps the player has not visited yet in that game (and new games): saves keep the entities they had. See Changing a map's entities.
- Removing a mod can break saves that use its items, models or quests; an item class the mod added crashes the game when such a save loads. See Removing a mod, and saves.
- The later mod in
loadorder.txtwins every conflict.blse.loglists each conflict. - Python file and map filters are for scripts that must compute a change; most mods only need files, merges and map patches.
Words used on this page#
- Game path: a file's path inside the game, with
/, as the game asks for it (vdata/items/item_w_katana.txt,models/character/...mdl). - Mod folder: one folder under
Bin/loader/blse/mods/, holding the mod's files in the game's layout. - Load order: the order of the mod folders in
Bin/loader/blse/config/loadorder.txt; a later mod wins. - Merge: a small file with only your changes to one of the game's data files; BLSE combines it with the game's file.
- KeyValues: the game's text format for data:
"key" "value"pairs in named{ }blocks. .dlgrow: one line of a conversation file, in{ }columns; the first column is the line's number.- Map patch: a text file that removes, adds or changes a map's entities (the objects placed in a map: characters, doors, triggers).
- Entity text: a map's list of entities as text, one
{ }block of"key" "value"pairs per entity. - Filter (Python): a function BLSE calls with a file's (or a map's) text; what it returns is used instead.
A mod folder#
Everything in a mod folder sits at the same path it has in the game. A game file sound/weapons/melee/katana/swish_1.wav is replaced by my_mod/sound/weapons/melee/katana/swish_1.wav; a new file at a new path is simply added. The blse/ folder inside holds the things that are not game files: merges, map changes, scripts, plugins and lists of new items. They cannot sit at the top of the mod folder, because every path there is a game path: the game has its own scripts folder (weapon and HUD scripts), so my_mod/scripts/ would change those instead of running Python. Keeping everything BLSE reads in blse/, plugins included, gives one rule for every mod.
Bin/loader/blse/mods/my_mod/
models/ materials/ sound/ maps/ vdata/ dlg/ resource/ scripts/ particles/ ...
models/my_mod/*.mdl models, and animation files (animation-only .mdl, listed in blse/animations.txt)
sound/character/dlg/... voice lines (.mp3, .wav), each with a .lip of the same name beside it for the mouth
python/ the game's Python modules
blse/scripts/*.py the mod's BLSE scripts
blse/plugins/*.dll the mod's native plugins
blse/merge/<game path> additions merged into a game file
blse/maps/<map>.txt changes to a map's entities
blse/animations.txt animation files added to models (see [Animation](animation.md#new-animations-for-models))
blse/items.txt new item classes (see [Items](items.md#new-items-and-weapons))
blse/outfits.txt outfits for the player (see [Outfits](appearance.md#outfits-clothes-for-the-player))
mod.ini its name, version and what it needs (optional)
readme.txt files at the top of the folder are ignored- Game files: everything in the game's own layout. A file with the same path as the game's (or the Unofficial Patch's, or one inside the
.vpkpacks) replaces it; a new path adds a file. Nothing is copied into the game. Ship loose files:.vpkpacks in a mod folder are only used for files that exist nowhere else. python/: put first onsys.path, with its sub-folders (map scripts live inpython/<hub>/<hub>.py). The game's Python imports from there before its own folders.blse/scripts/: the mod's BLSE scripts (see Getting started). They run at game start, mod by mod in load order, after the plugins have loaded. Name them after your mod: script names share one namespace across all mods.blse/plugins/: the mod's native plugins, 32-bit.dllfiles (see Plugins). The plugins of all loaded mods load as one set at game start: dependencies first, otherwise by file name. If two mods ship a DLL with the same file name, only the one from the mod later inloadorder.txtis loaded, and the log names the other as not loaded. Log lines name the mod:plugin my_mod: my_mod.dll: loaded (...). Only install plugins from people you trust: they can do anything a program can.- A mod that is switched off, or whose
mod.inirequirements are not met, runs none of its scripts and loads none of its plugins. save/,cfg/config.cfgand files at the top of the folder are never used as game files, and merge files for them are ignored (b0.5.49: logged).
Load order: the later mod wins#
Bin/loader/blse/config/loadorder.txt lists the folders, one per line. # starts a comment, -name switches a mod off. BLSE adds new folders to the end of the file (alphabetically, but after the folders they need), so players can see and change the order.
One rule decides every conflict: the mod further down in loadorder.txt wins. Everything that can be combined is combined; only what two mods change in the same place is decided by the order:
| Two mods ... | Result |
|---|---|
| ship the same file | the later mod's file is used |
| merge into the same data file | both merges apply, in load order: added entries from both, and for a key both set, the later mod's value |
change the same .dlg line (line merge) |
the later mod's line; their other lines both apply |
| patch the same map | both patches apply, in load order: for an entity both change, the later mod's change |
ship the same python/ module |
the later mod's (its folder comes first on sys.path) |
| ship a plugin DLL with the same file name | the later mod's DLL is loaded; the other is logged as not loaded |
register the same conversation or line from a script (add_dialog, extend_dialog) |
the later registration: scripts run in load order |
add replies under the same NPC line (add_dialog_reply) |
both replies are offered (the game shows up to 4) |
Merges always apply on top of whichever file wins, also a full file another mod ships: a mod that adds a quest by a merge keeps working beside an old mod that replaces the whole quest file.
blse.log lists the load order and every conflict with the winner last: mods: conflict: vdata/items/x.txt: modA < modB (a file), ...nurse.dlg: line 11: modA < modB, ...mapnames_localized.txt: key mapnames/sm_apartment_1: modA < modB (merges: logged when the game first reads that file). blse.mod_conflicts() returns them, and the console command blse shows the order.
mod.ini#
Optional, at the top of the mod folder:
[mod]
name = Guard Talk
version = 1.0
author = Me
description = A guard with a conversation and a quest.
requires_blse = b0.5.48
requires = blse_sample >= 1.0, other_modrequiresnames other mod folders, optionally with the lowest version of theirs (from theirmod.ini).- A mod whose requirements are not met is switched off for that start, and the reason is logged and shown by the console command
blse: BLSE older thanrequires_blse, a required folder missing or switched off, too old, or off itself, or a brokenmod.ini. The game then runs as if the folder were not there. - A required mod that comes later in
loadorder.txtonly gets a warning: its files then win over yours. Normally a mod comes after the mods it builds on. blse.mod_info(folder)returns it all (name,version,author,description,requires,requires_blse,positionin the load order,loaded,reason), also for a mod that is off (itsmod.inifields are empty only when the file is missing or broken).
Adding to a data file: merges#
Most of the game's data files are KeyValues text (vdata/system/*.txt, vdata/items/*.txt, scripts/*.txt, resource/*.res): pairs of "key" "value" in quotes, grouped in named blocks between { and }. Instead of shipping the whole file, put only your changes in blse/merge/<the same path>. Every mod's merge is applied, in load order, on top of the game's file (or on top of the file a mod replaced).
The smallest merge changes one value. The game's katana file (vdata/items/item_w_katana.txt) has "item_worth" "215" (its price) in its WeaponData block. This merge makes it 100 and leaves the rest of the file alone:
// blse/merge/vdata/items/item_w_katana.txt
WeaponData
{
"item_worth" "100"
}Repeat the block names down to what you change. A + in front of a name adds a new entry, a - removes one, and @match picks one of several blocks with the same name. Here all of them at once, in a quest file:
// blse/merge/vdata/system/quests_santamonica.txt
QuestTable
{
+Quest // a new quest
{
"Title" "My_Quest"
"DisplayName" "My quest"
CompletionState { "ID" "1" "Description" "Find the thing." "Type" "incomplete" }
}
Quest // change one quest
{
"@match" "Title=Sm_Pawnshop"
"DisplayName" "Pawnshop (my version)"
}
-Quest { "@match" "Title=Sm_Other" } // remove one
}| In a merge file | Does |
|---|---|
"key" "value" |
sets the first key in that block (adds it if there is none) |
"+key" "value" |
adds another key |
"-key" "" |
removes every key there; "-key" "value" only those with that value |
Name { ... } |
merges into the first block Name (adds it if there is none) |
+Name { ... } |
adds a new block |
"@match" "Key=Value" |
inside a block: which block Name is meant ("Sub/Key=Value" looks inside sub-blocks; several must all match) |
Keys and @match values compare without case. The merged file is valid KeyValues without the comments.
Dialogue lines (.dlg): blse/merge/dlg/<area>/<npc>.dlg holds only the rows you change, in the game's own row format. Each row replaces the game's row with the same number, or is added if the number is new; the rest of the game's file stays. A row with an empty text removes that line (the game skips such rows). Two mods changing the same line: the later one wins. The game removes text between [ and ] from dialogue lines (its own files use brackets for notes), so do not use them in what players should see.
// blse/merge/dlg/santa monica/nurse.dlg: the nurse's first line, its texts changed
{ 11 }{ Please, take a seat. }{ Please, take a seat. }{ # }{ npc.SetDisposition("Neutral", 1) }{ G.Nurse_Talk = 1 }{ }{ }{ }{ }{ }{ }{ }The easy way to write one: open the character's file in the game's dlg folder (Unofficial_Patch/dlg/...), copy the whole row you want to change into your merge file and edit only its text: the first column is the line's number, the second and third are the text (as said to a male and to a female player character). Keep the other columns as they are.
A reply under one of the game's NPC lines needs a number between that line and the next NPC line, which the game's files rarely have free: add those with blse.add_dialog_reply (dialogue).
Other files that are not KeyValues (.lst lists, experience_table.txt, masquerade.txt): blse/merge/<path>.append adds its text at the end of the file, <path>.prepend at the start. One mod with several of these for the same file: its merge applies first, then .append, then .prepend (b0.5.49: always in this order).
A merge that does not parse is skipped and logged; so are single entries it cannot apply (+ or - without a name, an ill-formed @match), named in the log with the file and the mod. Merged files are built once per run into Bin/loader/blse/cache/files/, which you can open to see the result.
vdata/system/stats.txt keeps its sizes (b0.5.49): the game keeps each stat group in a fixed array inside every character: 35 Attributes, 13 Abilities, 17 Disciplines, 17 ActiveDisciplines. A different count only prints a developer warning, and an extra entry then overwrites other character data. So BLSE checks the file after every mod's merge, replacement or script filter: with other counts it is not used, the game reads its own, and the log names the group (Disciplines has 18 Stat entries, the game has room for exactly 17). Renaming or changing existing stats works. A new Discipline cannot be added as data.
Changing a map's entities#
blse/maps/<map>.txt changes the map's entities without a new .bsp, in the format of Source's Stripper:Source:
// blse/maps/sm_pawnshop_1.txt
filter: // remove the entities that match
{
"targetname" "bum_1"
}
add: // add an entity
{
"classname" "prop_dynamic"
"model" "models/my_mod/box.mdl"
"origin" "100 200 32"
"targetname" "my_box"
}
modify: // change the entities that match
{
match: { "classname" "func_door" "targetname" "door_*" }
replace: { "spawnflags" "0" } // keys the entity has
delete: { "OnOpen" "relay_old*" } // pairs removed
insert: { "OnOpen" "my_relay,Trigger,,0,-1" }
}* and ? are wildcards; values compare without case; remove: is the same as filter:; lines starting with //, ; or # are comments. Patches of all mods apply in load order.
When: the server reads a map's entities only when the map is loaded fresh: a new game, the map command, and the first time the player enters a map. A loaded save and a map visited before in the same game keep the entities they saved. So a patch shows up in a running game only in maps the player has not been to yet; entities a patch added stay in saves made with it. To change an already visited map, use level_init and blse.spawn_entity / blse.fire_input instead.
Python functions#
| Function | Description |
|---|---|
blse.mods() |
[(name, folder), ...] in load order (the last one wins) |
blse.mod_file(path) |
the name of the mod that provides a game file, '(merged)', '(provided)', or None |
blse.mod_conflicts() |
['path: modA < modB', ...]: files, .dlg lines and merge keys two mods change (the later one wins) |
blse.mod_info(folder) |
its mod.ini and state: {'name', 'version', 'author', 'description', 'requires', 'requires_blse', 'position', 'loaded', 'reason', 'folder'}, or None |
blse.provide_file(path, data) |
the game reads data as this file from now on, over the game's and every mod's file (merges still apply); None removes it |
blse.add_file_filter(path, function) |
function(path, data) returns the changed data or None; path may use * and ? |
blse.add_map_filter(function) |
function(map, entities) returns the changed entity text or None. It is called at every map start, also when a save is loaded; the game uses the text only on fresh map loads (see above) |
The game keeps many files after reading them once, so provide_file and filters reach only files it reads later: register them when your script loads. Files in mod folders and merge files apply from the start.
Keep a filter's path narrow: every file it matches is copied into Bin/loader/blse/cache/files/ when the game opens it ('*' would copy every model and sound the game reads). A filter runs on the game's main thread only; a matching file the game first opens on another thread is built without it (logged).
import blse
# Every fresh load of the map sm_apartment_1: add a marker entity (entity text is the map's own format).
def add_marker(map_name, entities):
if map_name != 'sm_apartment_1':
return None
return entities + '{\n"classname" "info_target"\n"targetname" "my_marker"\n"origin" "0 0 0"\n}\n'
blse.add_map_filter(add_marker)blse.mods#
blse.mods() # -> [(name, folder), ...]The mods in use, in load order (the last one wins conflicts): name is the mod folder's name ('cheap_katana'), folder its full path on disk. Mods that are switched off are not listed (see mod_info).
for name, folder in blse.mods():
blse.log(name)blse.mod_file#
blse.mod_file(path) # -> str or NoneWho provides a game file now: the name of the mod folder whose file is used, '(merged)' (BLSE builds it from merges), '(provided)' (a script gave it with provide_file), or None (the game's own file, or a path that is not a game path). Use it to find out why a file is not the one you expect.
blse.log(str(blse.mod_file('vdata/items/item_w_katana.txt')))blse.mod_conflicts#
blse.mod_conflicts() # -> ['path: modA < modB', ...]Every place two mods change the same thing (files, .dlg lines, merge keys), as text, with the winner last. The same lines are written to blse.log. Merge conflicts appear once the game has read that file.
blse.mod_info#
blse.mod_info(folder) # -> dict or NoneA mod's mod.ini and state, also for a mod that is switched off. Use it to check that a mod your script builds on is there and loaded.
folder(str): the mod folder's name, case ignored.- Returns a dict with:
'folder'(its name),'name','version','author','description','requires','requires_blse'(texts frommod.ini,''when missing),'position'(int, 1 for the first in the load order, 0 if it is off),'loaded'(int, 1 or 0),'reason'(why it is off,''if loaded).Noneif there is no such folder.
info = blse.mod_info('blse_sample')
if not info or not info['loaded']:
blse.log('blse_sample is missing or off')blse.provide_file#
blse.provide_file(path, data) # -> 1From now on the game reads data as the file at path, over the game's and every mod's file (merges still apply on top). Use it for a file your script builds, such as a generated list.
path(str): a game path (relative, no.., not undersave/orpython/).data(str): the whole file's bytes;Noneremoves your file again. Another type raisesTypeError.- Returns
1. RaisesRuntimeErrorfor a path that is not allowed, or if this game version is not recognized.
The game keeps many files after reading them once: call it when your script loads, before the game reads the file.
blse.provide_file('vdata/system/my_list.txt', 'one\ntwo\n')blse.add_file_filter#
blse.add_file_filter(path, function) # -> 1Calls function(path, data) each time the game opens a matching file; the text it returns is used instead (None: unchanged).
path(str): a game path;*and?are wildcards. Keep it narrow (see above).function: a function taking two strings. An error inside it prints its traceback and changes nothing; a return value that is not a string orNoneis ignored (and logged). A non-callable raisesTypeError.- Returns
1. RaisesRuntimeErrorfor a bad path, or if this game version is not recognized.
A filter cannot be removed; it stays until Python starts again with the engine.
def show_size(path, data):
blse.log('%s: %d bytes' % (path, len(data)))
return None # the file stays as it is
blse.add_file_filter('vdata/items/item_w_*.txt', show_size)blse.add_map_filter#
blse.add_map_filter(function) # -> 1Calls function(map, entities) at every map start with the map's name and entity text; the text it returns is used instead (None: unchanged). The game uses the text only on fresh map loads (see When). Errors and wrong return values are handled as for add_file_filter; a non-callable raises TypeError. Returns 1. See the example above.
Console command blse#
Typed in the game's console: shows BLSE's version, the number of plugins, whether Python runs, the log's path, and the mod folders in load order with their versions, the mods switched off with the reason, and the number of conflicts.
Parsing entity text into lists, and wrapping functions of the game's Python modules (by wrapping __builtin__.__import__), need no game hook, so BLSE leaves them to scripts and libraries.
Removing a mod, and saves#
Delete its folder: the game reads its own files again. Nothing of the file layer is stored in saves, but a save can refer to things by name: an item from the mod in the inventory, a model on an entity, a quest the mod added, entities a map patch added, an NPC given one of the mod's conversations, objects of the mod's Python modules kept in G. Those are missing when the mod is gone, and an item of a class the mod added makes the game crash when it loads that save (Items). Tell players to keep the mod while they play a game they started with it.
An example mod folder is in examples/mods/blse_sample (in the release: SDK/examples/mods/blse_sample).