
Security News
ECMAScript 2025 Finalized with Iterator Helpers, Set Methods, RegExp.escape, and More
ECMAScript 2025 introduces Iterator Helpers, Set methods, JSON modules, and more in its latest spec update approved by Ecma in June 2025.
⚠️ Warning: When loading beatmaps, bsmap will attempt to convert them to v3 if not already. Try to make all maps in v3 to avoid data loss!
The Beat Saber Mapping Framework is a comprehensive Python library designed to simplify the creation, manipulation, and analysis of Beat Saber maps. Built on Pydantic for strong type validation, the framework provides an intuitive and object-oriented approach to Beat Saber mapping, supporting all official characteristics and various customization options through mods like Chroma and Noodle Extensions.
pip install bsmap
git clone https://github.com/CodeSoftGit/bsmap.git
cd bsmap
pip install -e .
A Beat Saber map consists of:
[Disk: Input Files]
- Info.dat (JSON)
- <Difficulty>.dat (JSON, e.g., Expert.dat)
- template.json (JSON, for bsmap.templates)
|
| (File Read, JSON Parsing)
V
+-------------------------------------------------------------------------------------------------+
| bsmap.mapper.BeatSaberMapper |
|-------------------------------------------------------------------------------------------------|
| - load_info_dat(filePath) |
| - load_beatmap(filePath) |
| - load_map_folder(folderPath) |
| - create_empty_map(...) |
| (These methods parse JSON and instantiate Pydantic models from bsmap.models) |
+-------------------------------------------------------------------------------------------------+
|
| (Data converted to In-Memory Pydantic Model Instances)
V
+-------------------------------------------------------------------------------------------------+
| In-Memory Data Representation (Defined in bsmap.models & bsmap.custom_data) |
|-------------------------------------------------------------------------------------------------|
| 1. info_dat_object: bsmap.models.InfoDat |
| - Contains: _version, _songName, _bpm, _difficultyBeatmapSets (list of DifficultyBeatmapSet),|
| _customData (can hold bsmap.custom_data.Settings), etc. |
| |
| 2. beatmap_files_dict: Dict[str, bsmap.models.BeatmapFile] |
| - Key: beatmapFilename (e.g., "Expert.dat") |
| - Value: beatmap_object: bsmap.models.BeatmapFile |
| - Contains: version, bpmEvents, rotationEvents, waypoints, |
| colorNotes: List[bsmap.models.ColorNote] |
| bombNotes: List[bsmap.models.BombNote] |
| obstacles: List[bsmap.models.Obstacle] |
| sliders: List[bsmap.models.Slider] |
| basicBeatmapEvents: List[bsmap.models.BasicBeatmapEvent] |
| customData (can hold bsmap.custom_data.NoodleExtensions, |
| bsmap.custom_data.ChromaData, bsmap.custom_data.Animation), etc. |
+-------------------------------------------------------------------------------------------------+
| ^ | ^ | ^
| (Read) | (Modify/Create) | (Read) | (Modify) | (R/W) | (Create from/to file)
V | V | V |
+---------------------------+ +---------------------------------+ +--------------------------------+
| bsmap.analysis | | bsmap.autolights | | bsmap.templates |
| (MapAnalysis class) | | (LightingAutomation class) | | (TemplateManager class) |
|---------------------------| |---------------------------------| |--------------------------------|
| - get_note_statistics() |---->| (Reads BeatmapFile object) |<----| - save_template() |
| (Input: BeatmapFile) | | | | (Input: BeatmapFile section) |
| (Output: Dict stats) | | - generate_advanced_lighting() |--+ | (Output: template.json file) |
| | | (Input: BeatmapFile) | | | |
| - identify_map_issues() | | (Modifies BeatmapFile by | | | - (load_template()) |
| (Input: BeatmapFile) | | adding BasicBeatmapEvents) | | | (Input: template.json file) |
| (Output: List issues) | +---------------------------------+ | | (Output: BeatmapFile object) |
+---------------------------+ ^ |+--------------------------------+
| (Returns |
| modified |
| BeatmapFile |
| object) |
+----------------+
(bsmap.operations.MapOperations - also interacts here, modifying BeatmapFile objects)
(bsmap.utils - provides helper functions)
|
| (Processed/Modified In-Memory Pydantic Model Instances)
V
+-------------------------------------------------------------------------------------------------+
| bsmap.mapper.BeatSaberMapper |
|-------------------------------------------------------------------------------------------------|
| - save_info_dat(InfoDat, filePath) |
| - save_beatmap(BeatmapFile, filePath) |
| - save_map_folder(folderPath, InfoDat, beatmap_files_dict) |
| - validate_map(InfoDat, beatmap_files_dict) (Reads models, returns validation warnings) |
| (These methods serialize Pydantic models to JSON and write to files) |
+-------------------------------------------------------------------------------------------------+
|
| (File Write, Pydantic Model Serialization to JSON)
V
[Disk: Output Files]
- Info.dat (JSON)
- <Difficulty>.dat (JSON)
- (Potentially new/modified template.json files via TemplateManager)
from bsmap import BeatSaberMapper, Characteristic, Difficulty
# Create a new map
info, beatmaps = BeatSaberMapper.create_empty_map(
song_name="My First Map",
song_author="Artist Name",
level_author="Your Name",
bpm=120.0,
audio_filename="song.ogg",
cover_filename="cover.jpg",
characteristics=[Characteristic.STANDARD.value],
difficulties=[Difficulty.EASY.value, Difficulty.NORMAL.value]
)
# Save the map
BeatSaberMapper.save_map_folder("./my_first_map", info, beatmaps)
from bsmap import ColorNote, NoteDirection, NoteColor
# Get the Easy difficulty beatmap
easy_beatmap = beatmaps["Easy.dat"]
# Add a red note
easy_beatmap.colorNotes.append(
ColorNote(
b=1.0, # Beat time
x=1, # X position (0-3)
y=0, # Y position (0-2)
c=NoteColor.RED.value, # Color
d=NoteDirection.UP.value # Direction
)
)
# Add a blue note
easy_beatmap.colorNotes.append(
ColorNote(
b=2.0,
x=2,
y=0,
c=NoteColor.BLUE.value,
d=NoteDirection.UP.value
)
)
# Sort notes by beat time
easy_beatmap.sort_objects()
from bsmap.autolights import LightingAutomation
# Generate basic lighting
LightingAutomation.generate_basic_lighting(
beatmap=easy_beatmap,
beat_divisor=0.5, # Eighth notes
intensity=0.8
)
The models
module contains Pydantic models representing Beat Saber map structures:
InfoDat
: Represents the main Info.dat fileBeatmapFile
: Represents a beatmap file (e.g., Expert.dat)ColorNote
: Represents a red/blue noteBombNote
: Represents a bombObstacle
: Represents a wall/obstacleRotationEvent
: Represents a rotation event for 360/90 mapsDifficulty
, Characteristic
, NoteColor
, NoteDirection
The mapper
module provides utilities for working with Beat Saber maps:
BeatSaberMapper.create_empty_map()
: Create a new mapBeatSaberMapper.load_map_folder()
: Load a map from a folderBeatSaberMapper.save_map_folder()
: Save a map to a folderBeatSaberMapper.validate_map()
: Validate a map for issuesThe operations
module provides utilities for map operations:
MapOperations.mirror_map()
: Create a mirrored version of a mapMapOperations.copy_section()
: Copy a section of a mapMapOperations.paste_section()
: Paste a section into a mapMapOperations.shift_section()
: Shift a section of a mapMapOperations.adjust_difficulty()
: Adjust difficulty parametersThe analysis
module provides utilities for analyzing maps:
MapAnalysis.get_note_statistics()
: Calculate statistics about a mapMapAnalysis.identify_mapping_issues()
: Identify potential issuesMapAnalysis.compare_maps()
: Compare two maps and calculate differencesThe autolights
module provides utilities for lighting automation:
LightingAutomation.generate_basic_lighting()
: Generate basic lightingLightingAutomation.generate_note_sync_lighting()
: Generate note-synchronized lightingLightingAutomation.generate_advanced_lighting()
: Generate advanced lightingThe templates
module provides utilities for template management:
TemplateManager.save_template()
: Save a section as a templateTemplateManager.load_template()
: Load a templateTemplateManager.list_templates()
: List available templatesTemplateManager.search_templates()
: Search templatesfrom bsmap.custom_data import NoodleExtensions, ChromaData
# Add Noodle Extensions custom data
note.customData = {
"track": "example_track",
"coordinates": [1.5, 0.0],
"disableNoteLook": True
}
# Using the structured models
noodle_data = NoodleExtensions(
track="example_track",
coordinates=[1.5, 0.0],
disableNoteLook=True
)
note.customData = noodle_data.model_dump(exclude_none=True)
# Create a 360 degree map
info, beatmaps = BeatSaberMapper.create_empty_map(
# ... other parameters ...
characteristics=[Characteristic.DEGREE_360.value],
difficulties=[Difficulty.EXPERT.value]
)
# Get the Expert difficulty beatmap
expert_beatmap = beatmaps["Expert360Degree.dat"]
# Add rotation events
expert_beatmap.rotationEvents.append(
RotationEvent(
b=1.0, # Beat time
e=0, # Event type
r=90 # Rotation in degrees
)
)
from bsmap import BeatmapFile, ColorNote, NoteColor, NoteDirection
from bsmap.operations import MapOperations
from bsmap.templates import TemplateManager
# --- 1. Define the Pattern ---
# Create a temporary BeatmapFile to hold the pattern.
# Notes within this pattern should be timed starting from beat 0 relative to the pattern itself.
pattern_bm = BeatmapFile(version="3.3.0") # Or match your map's specific version
# Example: A short, fast stream of 4 alternating notes
# (e.g., Red on left, Blue on right, all down-cuts)
pattern_notes_data = [
ColorNote(b=0.0, x=1, y=1, c=NoteColor.RED.value, d=NoteDirection.DOWN.value), # Beat 0 of pattern
ColorNote(b=0.25, x=2, y=1, c=NoteColor.BLUE.value, d=NoteDirection.DOWN.value), # Beat 0.25 of pattern
ColorNote(b=0.5, x=1, y=1, c=NoteColor.RED.value, d=NoteDirection.UP.value), # Beat 0.5 of pattern
ColorNote(b=0.75, x=2, y=1, c=NoteColor.BLUE.value, d=NoteDirection.UP.value), # Beat 0.75 of pattern
]
pattern_bm.colorNotes.extend(pattern_notes_data)
pattern_bm.sort_objects() # Good practice after adding or modifying map objects
# --- 2. Save the Pattern as a Template ---
template_name = "MyShortFastStream"
TemplateManager.save_template(
beatmap_section=pattern_bm, # The BeatmapFile instance containing the pattern
name=template_name,
description="A short, fast stream of 4 alternating notes (R-B-R-B)",
tags=["stream", "fast", "short", "alternating"]
)
# You can confirm by checking if a file like "MyShortFastStream.json" (or similar)
# is created in your templates directory.
# --- 3. Load and Use the Template in an Existing Map ---
# Assume 'target_map_difficulty' is an existing BeatmapFile object you are working on
# (e.g., for an ExpertPlus difficulty).
# It might be loaded like this:
# info, beatmaps = BeatSaberMapper.load_map_folder("./my_map_project")
# target_map_difficulty = beatmaps["ExpertPlusStandard.dat"]
#
# For this example, we'll initialize a new BeatmapFile instance to demonstrate the functionality.
target_map_difficulty = BeatmapFile(version="3.3.0")
# Ensure it's sorted if it was empty or newly created
target_map_difficulty.sort_objects()
# Load the template you saved earlier
# - 'loaded_pattern_bm' will be a BeatmapFile containing the notes from the template.
# - 'metadata' will hold the template's name, description, tags, etc.
loaded_pattern_bm, metadata = TemplateManager.load_template(template_name)
# Specify the beat in your 'target_map_difficulty' where the pattern should start
paste_at_beat = 32.0
# Paste the loaded pattern into your target map.
# The notes from 'loaded_pattern_bm' (timed from beat 0 within that template)
# will be offset by 'paste_at_beat' and added to 'target_map_difficulty'.
MapOperations.paste_section(
target=target_map_difficulty, # Your main BeatmapFile object to modify
section=loaded_pattern_bm, # The BeatmapFile loaded from the template
target_beat=paste_at_beat # Beat in 'target' where the pattern begins
)
target_map_difficulty.sort_objects() # Re-sort after adding new objects from the pattern
# Now, 'target_map_difficulty' contains the notes from the pasted pattern.
beatmap.sort_objects()
after adding notesBeatSaberMapper.validate_map()
to check for issuesAttributeError: 'dict' object has no attribute 'x'
TypeError: Object of type x is not JSON serializable
ValueError: field required
Note Position Outside Grid
Same Hand Double Notes
Missing Rotation Events in 360/90 Maps
We welcome contributions to the Beat Saber Mapping Framework! Here's how you can help:
The Beat Saber Mapping Framework (bsmap) is released under the MIT License. See the LICENSE file for details.
FAQs
Beat Saber Mapping Framework
We found that bsmap demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
ECMAScript 2025 introduces Iterator Helpers, Set methods, JSON modules, and more in its latest spec update approved by Ecma in June 2025.
Security News
A new Node.js homepage button linking to paid support for EOL versions has sparked a heated discussion among contributors and the wider community.
Research
North Korean threat actors linked to the Contagious Interview campaign return with 35 new malicious npm packages using a stealthy multi-stage malware loader.