⚠️ 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!
- Introduction
- Installation
- Core Concepts
- Getting Started
- Module Reference
- Advanced Features
- Best Practices
- Troubleshooting
- Contributing
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.
- Complete Data Model: Full support for all Beat Saber map components with type safety
- Multi-Characteristic Support: Support for all official characteristics (Standard, OneSaber, 360Degree, etc.)
- Pattern Generation: Built-in utilities for common note patterns
- Map Analysis: Automated difficulty estimation and issue detection
- Lighting Automation: Tools to generate synchronized lighting effects
- Template Management: Save and reuse mapping patterns
- Map Operations: Mirror, copy, shift, and manipulate map sections
- Command-Line Interface: Scriptable operations for automation
- Python 3.9 or higher
- Pydantic 2.0 or higher
pip install bsmapgit clone https://github.com/CodeSoftGit/bsmap.git
cd bsmap
pip install -e .A Beat Saber map consists of:
- Info.dat: Contains metadata about the song and available difficulties
- Beatmap Files: Contains notes, obstacles, events, and other gameplay elements
- Audio File: The song audio file
- Cover Image: The map's cover art
- Notes: The red and blue cubes that players hit
- Bombs: Black spheres with points that must be avoided
- Walls: Obstacles that players must dodge
- Events: Lighting and environment effects
- Characteristics: Different play modes (Standard, OneSaber, etc.)
- Difficulties: Different difficulty levels (Easy, Normal, Hard, etc.)
[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 maps- Enums:
Difficulty,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 issues
The 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 parameters
The 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 differences
The 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 lighting
The 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 templates
from 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.- Consistent Spacing: Use consistent spacing between notes for readability
- Pattern Recognition: Create recognizable patterns that flow well
- Visual Clarity: Avoid vision blocks and excessive density
- Map Progression: Gradually increase difficulty throughout the map
- Sync to Music: Ensure notes match the rhythm and energy of the song
- Sort Objects: Always call
beatmap.sort_objects()after adding notes - Use Enums: Use provided enums instead of raw values
- Validate Maps: Run
BeatSaberMapper.validate_map()to check for issues - Model Validation: Ensure all objects have required fields properly set
- Clear Custom Data: Use the provided models for custom data
- Batch Processing: Process notes in batches rather than one at a time
- Reuse Templates: Use templates for repeated patterns
- Efficient Lighting: Limit the number of lighting events
- Memory Management: Clear large collections when no longer needed
- Object Pooling: Reuse objects when processing large maps
-
AttributeError: 'dict' object has no attribute 'x'
- Cause: Using a dictionary instead of a proper model object
- Solution: Use the appropriate model class instead of raw dictionaries
-
TypeError: Object of type x is not JSON serializable
- Cause: Custom Python objects in customData
- Solution: Ensure all data is JSON serializable
-
ValueError: field required
- Cause: Missing required field when creating a model
- Solution: Check model definition to see which fields are required
-
Note Position Outside Grid
- Cause: Invalid x or y position for notes
- Solution: Ensure x is 0-3 and y is 0-2
-
Same Hand Double Notes
- Cause: Multiple notes of the same color at the same time
- Solution: Adjust timing or use different colors
-
Missing Rotation Events in 360/90 Maps
- Cause: 360/90 degree map without rotation events
- Solution: Add appropriate rotation events
We welcome contributions to the Beat Saber Mapping Framework! Here's how you can help:
- Report Issues: Submit bug reports and feature requests
- Submit Pull Requests: Contribute code improvements and new features (i work for free, help is greatly appreciated!)
- Improve Documentation: Help clarify and expand the documentation so others can get started easier
- Share Templates: Contribute reusable templates and patterns
- Spread the Word: Tell others about the framework
The Beat Saber Mapping Framework (bsmap) is released under the MIT License. See the LICENSE file for details.