|
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() |