109 lines
2.9 KiB
Python
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)
|