Skip to content

Instantly share code, notes, and snippets.

@pckv
Created December 11, 2025 20:01
Show Gist options
  • Select an option

  • Save pckv/d939e86563e13dc4c47b64f8baab00f5 to your computer and use it in GitHub Desktop.

Select an option

Save pckv/d939e86563e13dc4c47b64f8baab00f5 to your computer and use it in GitHub Desktop.
source code for the lost-to-time tag.pckv.me runnable as a cmdline script
import re
NEW_COMBO = 4
SPINNER = 8
title_pattern = re.compile(r"Title:\s*(.+)")
artist_pattern = re.compile(r"Artist:\s*(.+)")
creator_pattern = re.compile(r"Creator:\s*(.+)")
version_pattern = re.compile(r"Version:\s*(.+)")
object_type_pattern = re.compile(r"(\d+,\d+,\d+,)(\d+)(.+)")
def generate_filename(content: str, version: str=None):
""" Generate a filename for the given content and current player number.
:param content: Content including the beatmap metadata
:param version: The difficulty name
:return: Filename as str without the extension
"""
title = title_pattern.search(content).group(1).strip("\n\r ")
artist = artist_pattern.search(content).group(1).strip("\n\r ")
creator = creator_pattern.search(content).group(1).strip("\n\r ")
if version:
return f"{artist} - {title} [{version}] ({creator})"
else:
return f"{artist} - {title} ({creator})"
def split_objects(hitobjects: str, player: int, num_players: int, by_object: bool=False):
""" Split every object in a string of .osu hitobject data
by combo for the given player
:param hitobjects: Contents under the [HitObjects] section of a .osu file
:param player: The highlighted player number
:param num_players: The number of players in the TAG map
:param by_object: Split by every object, instead of by combo
:yield: Each line that belongs to the player as str
"""
# Start on -1 since the first object is most likely a new combo
combo_count = -1
for match in object_type_pattern.finditer(hitobjects):
# Mark new combos
object_type = int(match.group(2))
if object_type & NEW_COMBO or by_object:
combo_count += 1
# Yield every combo that matches
if combo_count % num_players == player or object_type & SPINNER:
if by_object:
# Make every object a new combo
object_type = object_type | NEW_COMBO
yield object_type_pattern.sub(f'\\g<1>{object_type}\\g<3>', match.group())
else:
yield match.group()
def split_beatmap(content: str, num_players: int, by_object: bool=False):
""" Iterator that splits a beatmap into multiple difficulties by combo.
:param content: Content from a .osu file
:param num_players: The number of players in the TAG map
:param by_object: Split by every object, instead of by combo
:yield: A tuple of (content, filename) where content is the entire .osu
"""
top, bottom = content.split("[HitObjects]")
bottom = bottom.strip("\n\r ")
for player in range(num_players):
hitobjects = "\n".join(split_objects(bottom, player, num_players, by_object))
current_version = version_pattern.search(content).group(1).strip("\n\r ")
version = f"{current_version} Player {player + 1}"
new_top = top.replace(f"Version:{current_version}", f"Version:{version}")
filename = generate_filename(top, version)
new_content = f"{new_top}[HitObjects]\n{hitobjects}"
yield new_content, filename
def main():
import os
import argparse
parser = argparse.ArgumentParser(description="Split a TAG beatmap into multiple difficulties by combo.")
parser.add_argument("path", type=str, help="Path to the .osu file to split")
parser.add_argument("--players", "-p", type=int, help="Number of players in the TAG map")
parser.add_argument("--by-objects", action="store_true", help="Split by every object, instead of by combos")
args = parser.parse_args()
with open(args.path, encoding='utf-8') as f:
beatmap_content = f.read()
# Generate a directory for the new beatmap files
name = args.path.split(".")[0]
if not os.path.exists(name):
os.mkdir(name)
# Split and save all beatmap files
for content, filename in split_beatmap(beatmap_content, args.players, args.by_objects):
with open(os.path.join(name, filename + ".osu"), "w", encoding="utf-8") as f:
f.write(content)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment