The following is a guide on how to use Beat Saber Deno, referred as bsmap
for simpler term. If you
are familiar with basic JavaScript or new to TypeScript, this should be very familiar as it is about
what you expect it to do like a regular scripting. This does not cover everything but enough to get
good grasp at what it does.
You may refer to example script if you need more glimpse on how to use them. Alternatively, check out my mapping scripts to see how I would use them.
Create a .ts
script file anywhere, be it desktop, map folder, or any dedicated place, and simply
add the following on top of the script. No additional file or setup needed, it just works.
// be sure to check for latest version on 'bsmap@version'
import * as bsmap from 'https://deno.land/x/bsmap@1.4.1/mod.ts';
To first timer: Make sure to initialise Deno workspace before using the script. If you encounter
import error, you can ignore and run the (empty) script then it will automatically fetch the URL for
you. Alternatively, Alt+.
on the error message may reveal fix problem solution. If you are having
issue of not being able to retrieve module, then cache or reload the module to fix it. To reload or
cache the module, run deno cache --reload yourscriptpath.ts
and restart Deno server if necessary.
If it still does not work, change to a different workspace.
For rolling release, visit GitHub Repo and import raw
file directly from there (https://raw.githubusercontent.com/KivalEvan/BeatSaber-Deno/main/mod.ts
),
you may need to occasionally add --reload
tag for latest update.
Due to expansive library, namespace is used to separate functionality on their own area. Object
destructuring can be used to obtain certain variables and functions. Helpful tip, use CTRL+Space
to show list of available variables and functions.
import { load, save, utils, v3 } from 'https://deno.land/x/bsmap@1.4.1/mod.ts';
const { random, deepCopy } = utils;
List of available namespaces from root are load
, save
, v2
, v3
, utils
, globals
,
convert
, optimize
, logger
, and types
. Nested namespace is to be expected on an obscure area.
To load & save the beatmap, a function is used to parse, validate, and optimise the respective info and difficulty file.
const info = load.infoSync(); // not required
// undefined version, return base wrapper class
// can be either version 1, 2 or 3
// pass it to isV3 or similar function for type predicate
const data = load.difficultySync('HardStandard.dat');
// explicit version, return (and convert to) difficulty version
const data2 = await load.difficulty('ExpertStandard.dat', 2, {
directory: '/somewhere/else',
}); // advanced use
save.infoSync(info);
save.difficultySync(data);
await save.difficulty(data2, {
directory: '/somewhere/else',
filePath: 'overrideName.dat',
}); // advanced use
Difficulty file name is saved directly in difficulty class and can be changed.
data.filename = 'ExpertPlusStandard.dat';
If you happen to use the script outside of map folder, you can do the following before loading the source folder and saving to target the folder. You may change this anytime whenever necessary.
globals.directory = './YOUR/MAP/FOLDER/PATH/';
NOTE: Directory and file path will be overridden if explicitly provided in one of the following load and save functions.
To new coder: Windows typically uses \
instead of /
in path, this actually means escape
character in programming and would result in error. You may need to change the slash or escape
character.
All beatmap object is a class object as opposed to regular JSON object. This mean most array and object will only accept class object. This enables extensive method and functionality to be used directly from class object. Custom data is always available and require no checking if exist.
Each beatmap object including difficulty contain a static method create
which allows you to
instantiate one or more objects. Partial or no data can be used to instantiate an object and will
use default value to fill the empty spot. This method always return object(s) in an array with an
exception being object that is not placed in array such as difficulty and index filter.
Alternatively, if you prefer just a single object instantiation, you may use constructor method.
Object creation field can be mixed with either the schema field or the wrapped field, prioritises wrapped field when presented.
const bomb = v3.BombNote.create();
const event = new v3.BasicEvent();
const notes = v3.ColorNote.create(
{},
{ b: 1, x: 0, y: 1 },
{
time: 2,
posX: 1,
posY: 0,
},
{ b: 2, color: 1 },
);
data.colorNotes.push(...notes);
Difficulty class has a built-in method that allows instantiating of an object directly and insert into an array. This also allows insertion of an already instantiated object; preferrably clone the object before passing it here to avoid unnecessary mutation.
data.addBasicEvents(
{ et: 3 },
{ time: 2, type: 1, value: 3 },
{
b: 5,
type: 2,
value: 7,
f: 1,
},
{},
);
data.addBasicEvents(...events);
In modcharting, cloning is often used to create certain effect. This method can be used to clone an existing object without referencing the original.
const original = v3.ColorNotes.create()[0];
const cloned = original.clone(); // new object with same property as original without reference
One liner or method chaining can be proven powerful in certain case scenarios.
const clones = notes.map((n) =>
n
.clone()
.setTime(n.time + 4)
.setDirection(8)
.addCustomData({ color: [1, 1, 1] })
);
The library provide constant variables in form of PascalCase
or SCREAMING_SNAKE_CASE
that can be
used to make your script slightly more readable but it is not necessarily needed.
const note = v3.ColorNotes.create({
b: 24,
c: NoteColor.RED,
d: NoteDirection.ANY,
x: PositionX.MIDDLE_LEFT,
y: PositionY.BOTTOM,
})[0];
data.addBasicEvents({
time: 10,
type: EventType.BACK_LASERS,
value: EventLightValue.WHITE_FADE,
});
This module is not available directly from main import as it is heavy, unstable, and make use of third-party library. This provides plentiful of helpers that may be useful for modcharting and many other purposes.
import * as chroma from 'https://deno.land/x/bsmap@1.4.1/extensions/chroma/mod.ts';
import * as NE from 'https://deno.land/x/bsmap@1.4.1/extensions/NE/mod.ts';
import * as selector from 'https://deno.land/x/bsmap@1.4.1/extensions/selector/mod.ts';
If you wish to import all of them, do as following:
import * as ext from 'https://deno.land/x/bsmap@1.4.1/extensions/mod.ts';
This module is not included as it is very rarely used and unstable. It contains functions to attempt fix and alter beatmap objects that were potentially broken or contain incompatible data.
import * as patch from 'https://deno.land/x/bsmap@1.4.1/patch/mod.ts';
If you happen to work on multiple script files or has centralised folder for map scripting, a dependency file can be used. Similarly, you can break your script into multiple file for modularity purpose.
// deps.ts
export * from 'https://deno.land/x/bsmap@1.4.1/mod.ts';
export * as ext from 'https://deno.land/x/bsmap@1.4.1/extensions/mod.ts';
// map.ts
import * as bsmap from './deps.ts';
import { types, v3 } from './deps.ts';
Static type is an incredibly powerful tool that can ensure type correctness of an object. This is used extensively in the library and is encouraged to explore further into it by utilising type casting. This is an intermediate knowledge of TypeScript but should be relatively easy to grasp.
const event = [{ c: 2 }, { b: 0.25, s: 0, i: 1 }] as Partial<types.v3.LightColorBase>[];
data.addLightColorEventBoxGroup({ e: [{ e: event }] });
Contrary to popular belief, this is simply an output logging that can be controlled by level. This can show and hide logging based on level.
bsmap.logger.setLevel(5); // completely hidden logging
bsmap.load.difficultySync('Test.dat');
bsmap.logger.setLevel(0); // verbose mode logging
bsmap.load.difficultySync('Test.dat');
bsmap.logger.setLevel(2); // default info logging
If you prefer to script the old-fashioned way but would like to keep strong-typed schema, it is possible but you may lose the ability to use certain utilities built around it.
const difficulty = load.difficultySync('ExpertPlusStandard.dat').toJSON();
const difficultyJSON = JSON.parse(
Deno.readTextFileSync('ExpertPlusStandard.dat'),
) as types.v3.IDifficulty; // unsafe