39918-vm/src/msd_converter.py
2026-05-06 17:53:23 +00:00

109 lines
2.9 KiB
Python

import json
import os
import shutil
import subprocess
import sys
BASE_DIR = os.path.dirname(__file__)
def _resolve_msd_command():
env_path = os.environ.get("MSD_BIN_PATH")
if env_path:
if os.name != "nt" and env_path.lower().endswith(".exe"):
wine = shutil.which("wine64") or shutil.which("wine")
if wine:
return [wine, env_path], env_path
return [env_path], env_path
windows_msd = os.path.join(BASE_DIR, "msd.exe")
native_msd = os.path.join(BASE_DIR, "msd")
if os.name == "nt":
return [windows_msd], windows_msd
if os.path.exists(native_msd):
return [native_msd], native_msd
if os.path.exists(windows_msd):
wine = shutil.which("wine64") or shutil.which("wine")
if wine:
return [wine, windows_msd], windows_msd
return [native_msd], native_msd
def parse_hitobjects(osu_file, mod="NM"):
hitobjects = []
in_section = False
with open(osu_file, "r", encoding="utf8") as f:
for line in f:
line = line.strip()
if line == "[HitObjects]":
in_section = True
continue
if not in_section or not line:
continue
parts = line.split(",")
x = int(parts[0])
time = int(parts[2])
obj_type = int(parts[3])
if mod == "DT":
time = int(time * 2 / 3)
elif mod == "HT":
time = int(time * 4 / 3)
hitobjects.append({"x": x, "time": time, "type": obj_type})
return hitobjects
def osu_to_etterna_rows(hitobjects, keycount=4):
rows = {}
column_width = 512 / keycount
for obj in hitobjects:
time = round(obj["time"] / 1000.0, 4)
column = int(obj["x"] // column_width)
rows[time] = rows.get(time, 0) | (1 << column)
# LN releases are intentionally ignored (obj_type & 128)
return [{"notes": rows[t], "time": t} for t in sorted(rows)]
def calculate_msd(notes):
cmd, msd_path = _resolve_msd_command()
if not os.path.exists(msd_path):
raise FileNotFoundError(
f"MSD binary not found at '{msd_path}'. Set MSD_BIN_PATH or add a compatible executable to src/."
)
if os.name != "nt" and msd_path.lower().endswith(".exe") and len(cmd) == 1:
raise RuntimeError(
"Found msd.exe on Linux/macOS, but Wine is not installed. Install Wine or provide a native msd via MSD_BIN_PATH."
)
popen_kwargs = {
"stdin": subprocess.PIPE,
"stdout": subprocess.PIPE,
"stderr": subprocess.PIPE,
"text": True,
}
if os.name == "nt":
popen_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
p = subprocess.Popen(cmd, **popen_kwargs)
output, err = p.communicate(json.dumps(notes))
if err:
print("MSD ERROR:", err, file=sys.stderr)
return json.loads(output)