fork(2) download
  1. #!/usr/bin/python3
  2.  
  3. # Copyright (c) 2021-2024 Pixel Grass
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a copy
  6. # of this software and associated documentation files (the "Software"), to deal
  7. # in the Software without restriction, including without limitation the rights
  8. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. # copies of the Software, and to permit persons to whom the Software is
  10. # furnished to do so, subject to the following conditions:
  11. #
  12. # The above copyright notice and this permission notice shall be included in all
  13. # copies or substantial portions of the Software.
  14. #
  15. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. # SOFTWARE.
  22.  
  23. # Algorithm example for the 4:3 to 16:9/widescreen conversion case:
  24. # w - screen width (according to localcoord),
  25. # b1 - old camera's horizontal bound value (abs/radius), d1 - old 4:3 horizontal delta,
  26. # b2 - new camera's horizontal bound value (abs/radius), d2 - new 16:9 horizontal delta.
  27. #
  28. # Case diagram:
  29. #
  30. # d1 * b1
  31. # |-------------------|
  32. # d2 * b2 1/6w 1/2w
  33. # |------------|------|------------------|
  34. # off-screen ^ ^ ^
  35. # | +-- 4:3 edge +-- screen center
  36. # +-- 16:9 edge
  37. #
  38. # Bound/delta equations:
  39. #
  40. # 1) according to the diagram:
  41. # d1 * b1 - d2 * b2 = 1/6 * w
  42. # d2 * b2 = d1 * b1 - 1/6 * w
  43. # d2 = (d1 * b1 - 1/6 * w) / b2
  44. #
  45. # 2) substitute b2 with the following bound modification formula:
  46. # b2 = b1 - 1/6 * w
  47. #
  48. # 3) final deltas modification formula:
  49. # d2 = (d1 * b1 - 1/6 * w) / (b1 - 1/6 * w)
  50.  
  51.  
  52. import argparse
  53. import re
  54. import shutil
  55. import sys
  56.  
  57. from dataclasses import dataclass
  58. from decimal import Decimal, Context, ROUND_HALF_DOWN
  59. from math import floor, log10, isclose
  60. from pathlib import Path
  61. from typing import Any, Callable, Optional, Pattern
  62.  
  63. MODE_MUGEN = "mugen".casefold()
  64. MODE_IKEMEN = "ikemen".casefold()
  65. MODE_NONE = "none".casefold()
  66.  
  67. BOUNDS_MODE_VALUES = [MODE_MUGEN, MODE_IKEMEN, MODE_NONE]
  68. BOUNDS_MODES = " | ".join(BOUNDS_MODE_VALUES)
  69.  
  70. CMD_PARSER = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description="""
  71. A stage DEF file converter for M.U.G.E.N/Ikemen that allows e.g. to adjust a zoom-less 4:3 stage
  72. to support a widescreen aspect ratio, by adjusting zoom, horizontal deltas and other parameters.
  73. Alternatively, this tool can also adjust a zoom-less stage to support a target zoom level.
  74.  
  75. When using this tool to enable widescreen support, then the resulting stage is meant to
  76. be displayed with the `StageFit` parameter (from the `mugen.cfg` config file) set to `0`.
  77.  
  78. This converter works by setting the `zoomin` and `zoomout` values and then calculating and adjusting
  79. the `boundleft` and `boundright` parameters, as well as the horizontal `delta` values of backgrounds,
  80. including the old-style `parallax` type backgrounds using the `xscale` parameter.
  81.  
  82. The players' starting position values of `p1startx` and `p2startx` are also adjusted appropriately.
  83.  
  84. The camera's vertical bounding parameters are also tweaked, depending on the target engine.
  85. Unfortunately, this part is mostly guesswork, so manual follow-up adjustments will be necessary,
  86. however it should still be much easier than editing the whole stage completely by hand. To adjust
  87. the vertical camera bounds after conversion, use e.g. `boundhigh`, `cutlow` and similar parameters.
  88. It should also be mentioned, that Mugen's and Ikemen's vertical camera bounding algorithms work
  89. very differently, especially in the case of a wide zoom range.
  90.  
  91. The parallax floors using the `xscale` parameter should also receive automatic delta adjustments,
  92. however these may need to be tweaked after conversion.
  93.  
  94. Stages already having zoom are not supported. Highres stages using `highres` are also
  95. not supported. Highres stages using `localcoord` and/or `(x/y)scale` should theoretically
  96. be supported, however this has not been extensively tested.
  97. """, epilog="""
  98. examples:
  99. %(prog)s DEF_FILE
  100. modify the given zoom-less stage DEF file to support a 16:9 aspect ratio
  101. (this is the default behavior when no additional options are given)
  102. %(prog)s -z 0.85 DEF_FILE
  103. modify the given zoom-less stage DEF file to support a zoom of 0.85
  104. the stage will be set to always display in this exact zoom
  105. %(prog)s -z 0.8 -n 1.0 DEF_FILE
  106. modify the given zoom-less stage DEF file to support a zoom-out of 0.8
  107. set the zoom-in value to 1.0, making the effective zoom range [0.8, 1.0]
  108. %(prog)s -r 64x27 DEF_FILE
  109. modify the given zoom-less stage DEF file to support the ultra-wide 64:27 aspect ratio
  110. (this implies a large zoom-out value, therefore some artifacts may not be avoided)
  111.  
  112. Copyright (c) 2021-2023 Pixel Grass, License: MIT
  113. ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. **
  114. """)
  115.  
  116. CMD_PARSER.add_argument("stage_def_files", metavar="DEF_FILE", type=Path, nargs="+", help="""
  117. the stage DEF file (multiple files can be given)\nthe files will be modified in place!
  118. """.strip())
  119.  
  120. CMD_PARSER.add_argument("-r", "--target-aspect-ratio", metavar="RATIO", help="""
  121. adjust the stage to support the given widescreen aspect ratio (e.g. 16x9)
  122. mutually exclusive with the `--target-zoom` option: use only one or the other
  123. """.strip())
  124.  
  125. CMD_PARSER.add_argument("-z", "--target-zoom", metavar="ZOOM", type=float, help="""
  126. adjust the stage to support at least the given zoom value (e.g. 0.75)
  127. warning: setting this value too low may introduce artifacts
  128. mutually exclusive with the `--target-aspect-ratio` option: use only one or the other
  129. """.strip())
  130.  
  131. CMD_PARSER.add_argument("-n", "--set-zoom-in", metavar="ZOOM_IN", type=float, help="""
  132. set/override the `zoomin` camera parameter to the given value
  133. (default: calculated from the target aspect ratio or the same as target zoom)
  134. """.strip())
  135.  
  136. CMD_PARSER.add_argument("-t", "--set-zoom-out", metavar="ZOOM_OUT", type=float, help="""
  137. set/override the `zoomout` camera parameter to the given value
  138. warning: setting this value lower than the target zoom *will* introduce artifacts
  139. (default: calculated from the target aspect ratio or the same as target zoom)
  140. """.strip())
  141.  
  142. CMD_PARSER.add_argument("-m", "--bounds-mode", metavar="CB_MODE", help=f"""
  143. choose camera's bounding mode (values: {BOUNDS_MODES}, default: {MODE_MUGEN})
  144. * the `{MODE_MUGEN}` mode enables hacks for MUGEN's quirks, to possibly avoid artifacts
  145. * the `{MODE_IKEMEN}` mode disables stage height cutting
  146. * the `{MODE_NONE}` mode leaves the camera's vertical bounding parameters unchanged
  147. """.strip())
  148.  
  149. CMD_PARSER.add_argument("-p", "--no-parallax", default=False, action='store_true', help="""
  150. do not modify the parallax backgrounds (default: modify parallax backgrounds)
  151. """.strip())
  152.  
  153. CMD_PARSER.add_argument("-d", "--debug-bg", default=False, action='store_true', help="""
  154. set the `debugbg` property to `1` in the modified stage DEF file (default: do not set)
  155. """.strip())
  156.  
  157. CMD_PARSER.add_argument("-o", "--output-path", metavar="OUT_PATH", type=Path, help="""
  158. write the modified stage DEF file or files to the given output file or
  159. directory respectively, instead of modifying the given DEF files in-place
  160. """.strip())
  161.  
  162. CMD_PARSER.add_argument('--no-backup', default=False, action='store_true', help="""
  163. do not create a backup for the modified stage DEF files
  164. all of the file will be irreversibly modified in place!
  165. """.strip())
  166.  
  167. CMD_PARSER.add_argument('--version', action='version', version="%(prog)s 1.1.0")
  168.  
  169.  
  170. INFO = "Info".casefold()
  171. STAGE_INFO = "StageInfo".casefold()
  172. PLAYER_INFO = "PlayerInfo".casefold()
  173. CAMERA = "Camera".casefold()
  174. BG_DEF = "BGdef".casefold()
  175.  
  176. MUGEN_VERSION = "mugenversion".casefold()
  177. LOCAL_COORD = "localcoord".casefold()
  178. HIRES = "hires".casefold()
  179. BOUND_HIGH = "boundhigh".casefold()
  180. BOUND_LEFT = "boundleft".casefold()
  181. BOUND_RIGHT = "boundright".casefold()
  182. CUT_LOW = "cutlow".casefold()
  183. ZOOM_IN = "zoomin".casefold()
  184. ZOOM_OUT = "zoomout".casefold()
  185. VERTICAL_FOLLOW = "verticalfollow".casefold()
  186. P1_START_X = "p1startx".casefold()
  187. P2_START_X = "p2startx".casefold()
  188. TYPE = "type".casefold()
  189. DELTA = "delta".casefold()
  190. XSCALE = "xscale".casefold()
  191. WINDOW = "window".casefold()
  192. DEBUG_BG = "debugbg".casefold()
  193.  
  194. PARALLAX = "parallax".casefold()
  195.  
  196.  
  197. VALUES_COUNT = {
  198. MUGEN_VERSION: 1, LOCAL_COORD: 2, HIRES: 1,
  199. BOUND_HIGH: 1, BOUND_LEFT: 1, BOUND_RIGHT: 1, CUT_LOW: 1,
  200. ZOOM_IN: 1, ZOOM_OUT: 1, VERTICAL_FOLLOW: 1,
  201. P1_START_X: 1, P2_START_X: 1, DELTA: {1, 2}, WINDOW: 4,
  202. BG_DEF: 1, TYPE: 1
  203. }
  204.  
  205. DECIMAL_PLACES = 8
  206. WINDOW_TOLERANCE = 5
  207. VERTICAL_FOLLOW_MIN = 0.00000001
  208.  
  209. MARGIN_FACTOR = {MODE_MUGEN: 1.0 / 30.0, MODE_IKEMEN: 1.0 / 30.0}
  210. MARGIN_FACTOR_STATIC = {MODE_MUGEN: 0.1, MODE_IKEMEN: 1.0 / 30.0}
  211.  
  212. COMMENT_RE = re.compile(r";.*")
  213. SECTION_RE = re.compile(r"^\[([^]]+)]$")
  214. PARAMETER_RE = re.compile(r"^([^=]+)=([^=]*)$")
  215. LINE_BREAK_RE = re.compile("[\r\n]+$")
  216.  
  217. FIRST_VALUE_RE = re.compile(r"(^\s*[^=;]+\s*=\s*)([^=,;\s]+)(.*$)")
  218. SECOND_VALUE_RE = re.compile(r"(^\s*[^=;]+\s*=\s*[^=,;\s]+\s*,\s*)([^=,;\s]+)(.*$)")
  219.  
  220.  
  221. @dataclass(frozen=True)
  222. class Parameter:
  223. name: str
  224. values: list[str]
  225.  
  226.  
  227. @dataclass(frozen=True)
  228. class LocalCoord:
  229. width: float = 320.0
  230. height: float = 240.0
  231.  
  232.  
  233. @dataclass(frozen=True)
  234. class ParallaxInfo:
  235. bg: str
  236. xscale_index: int
  237.  
  238.  
  239. Parser = Callable[[str], float]
  240. Modifier = Callable[[float], float]
  241. Verifier = Callable[[float, float], None]
  242.  
  243.  
  244. def parse_ratio(ratio: str) -> float:
  245. match = re.search(r"^\s*([0-9]+)[^0-9]([0-9]+)\s*$", ratio)
  246. if match is None:
  247. CMD_PARSER.error(f"invalid aspect ratio definition: '{match}', "
  248. f"use e.g. '16x9', '21x9' or '16:9', '21:9' etc.")
  249.  
  250. width = int(match.group(1))
  251. height = int(match.group(2))
  252. return float(width) / height
  253.  
  254.  
  255. def parse_parameter(param_line: str, stage_name: str) -> Optional[Parameter]:
  256. match = PARAMETER_RE.search(param_line)
  257. if match is None:
  258. return None
  259.  
  260. param_name = match.group(1).strip().casefold()
  261. param_values = match.group(2).strip().split(",")
  262.  
  263. if param_name in VALUES_COUNT:
  264. actual_len = len(param_values)
  265. expected_len = VALUES_COUNT[param_name]
  266.  
  267. if actual_len not in (expected_len if isinstance(expected_len, set) else {expected_len}):
  268. CMD_PARSER.error(f"invalid parameter for stage '{stage_name}', line: '{param_line}'")
  269.  
  270. return Parameter(param_name, list(value.strip() for value in param_values))
  271.  
  272.  
  273. def parse_value(param_line: str, pattern: Pattern, name: str) -> float:
  274. try:
  275. match = pattern.search(param_line)
  276. return float(match.group(2))
  277. except:
  278. CMD_PARSER.error(f"failed to parse the {name} parameter value, line: '{param_line}'")
  279.  
  280.  
  281. def parse_first_value(param_line: str) -> float:
  282. return parse_value(param_line, FIRST_VALUE_RE, "first")
  283.  
  284.  
  285. def parse_second_value(param_line: str) -> float:
  286. return parse_value(param_line, SECOND_VALUE_RE, "second")
  287.  
  288.  
  289. def round_value(value: float) -> Decimal:
  290. precision = DECIMAL_PLACES + (0 if abs(value) < 1 else int(floor(log10(abs(value)) + 1.0)))
  291. return Decimal(value).normalize(Context(prec=precision, rounding=ROUND_HALF_DOWN))
  292.  
  293.  
  294. def modify_value(lines: list[str], index: int, pattern: Pattern,
  295. parse: Parser, modify: Modifier, verify: Optional[Verifier] = None) -> None:
  296. param_line = lines[index]
  297.  
  298. old_value = parse(param_line)
  299. new_value = modify(old_value)
  300. not verify or verify(old_value, new_value)
  301.  
  302. rounded_value = round_value(new_value)
  303. lines[index] = pattern.sub(f"\\g<1>{rounded_value:f}\\g<3>", param_line)
  304.  
  305.  
  306. def modify_first_value(lines: list[str], index: int, modify: Modifier, verify: Optional[Verifier] = None) -> None:
  307. return modify_value(lines, index, FIRST_VALUE_RE, parse_first_value, modify, verify)
  308.  
  309.  
  310. def modify_second_value(lines: list[str], index: int, modify: Modifier, verify: Optional[Verifier] = None) -> None:
  311. return modify_value(lines, index, SECOND_VALUE_RE, parse_second_value, modify, verify)
  312.  
  313.  
  314. def set_value(lines: list[str], index: int, value: float) -> None:
  315. param_line = lines[index]
  316. lines[index] = FIRST_VALUE_RE.sub(f"\\g<1>{round_value(value):f}\\g<3>", param_line)
  317.  
  318.  
  319. def insert_value(lines: list[tuple[int, str]], index: int, param_name: str, value: float, line_break_str: str) -> None:
  320. lines.append((index + 1, f"{param_name} = {round_value(value):f}{line_break_str}"))
  321.  
  322.  
  323. def set_or_insert_value(
  324. set_lines: list[str], set_index: int, insert_lines: list[tuple[int, str]], insert_index: int,
  325. param_name: str, value: float, line_break_str: str) -> None:
  326. if set_index is not None:
  327. set_value(set_lines, set_index, value)
  328. else:
  329. insert_value(insert_lines, insert_index, param_name, value, line_break_str)
  330.  
  331.  
  332. def get_width_extension_ratio(zoom: float) -> float:
  333. return ((1.0 / zoom) - 1.0) / 2.0
  334.  
  335.  
  336. def get_height_margin(bounds_mode: str, full_height: float, bound_high: float) -> Optional[float]:
  337. factor = MARGIN_FACTOR if bound_high else MARGIN_FACTOR_STATIC
  338. return full_height * factor[bounds_mode] if factor[bounds_mode] else None
  339.  
  340.  
  341. def debug(a: float, b: [float, None] = None) -> str:
  342. return f"({round_value(a)})" if b is None else f"({round_value(a)}, {round_value(b)})"
  343.  
  344.  
  345. def process_stage(args, stage_file: Path, target_ratio: float, bounds_mode: str) -> list[str]:
  346. with open(stage_file, newline="", encoding="iso8859_1") as stage_input:
  347. lines = stage_input.readlines()
  348.  
  349. stage_name = stage_file.name
  350.  
  351. values: dict[str, Any] = {LOCAL_COORD: LocalCoord(), BOUND_HIGH: -25.0, WINDOW: []}
  352. indexes: dict[str, Any] = {DELTA: [], WINDOW: []}
  353.  
  354. parallax_infos: list[ParallaxInfo] = []
  355. parallax_delta_indexes: dict[str, int] = {}
  356.  
  357. current_section = None
  358. current_bg_type = None
  359.  
  360. for line_index, line in enumerate(lines):
  361. clean_line = COMMENT_RE.sub("", line).strip()
  362. if not clean_line:
  363. continue
  364.  
  365. section_match = SECTION_RE.search(clean_line)
  366. if section_match is not None:
  367. current_section = section_match.group(1).strip().casefold()
  368. indexes[current_section] = line_index
  369. current_bg_type = None
  370. continue
  371.  
  372. if current_section is None:
  373. continue
  374.  
  375. parameter = parse_parameter(clean_line, stage_name)
  376. if parameter is None:
  377. continue
  378.  
  379. if parameter.name == TYPE:
  380. current_bg_type = parameter.values[0].strip().casefold()
  381. continue
  382.  
  383. elif parameter.name == DELTA:
  384. delta_value = float(parameter.values[0])
  385. if delta_value < 0.0:
  386. CMD_PARSER.error(f"for stage '{stage_name}', "
  387. f"negative delta values {debug(delta_value)} are not supported, aborting'")
  388.  
  389. if current_bg_type == PARALLAX:
  390. parallax_delta_indexes[current_section] = line_index
  391. else:
  392. indexes[DELTA].append(line_index)
  393. continue
  394.  
  395. elif parameter.name == XSCALE and current_bg_type == PARALLAX:
  396. parallax_infos.append(ParallaxInfo(current_section, line_index))
  397. continue
  398.  
  399. elif parameter.name == WINDOW:
  400. indexes[WINDOW].append(line_index)
  401. values[WINDOW].append(list(float(coord) for coord in parameter.values))
  402. continue
  403.  
  404. if current_section == INFO:
  405. if parameter.name == MUGEN_VERSION:
  406. indexes[MUGEN_VERSION] = line_index
  407.  
  408. elif current_section == STAGE_INFO:
  409. if parameter.name == LOCAL_COORD:
  410. values[LOCAL_COORD] = LocalCoord(float(parameter.values[0]), float(parameter.values[1]))
  411. elif parameter.name == HIRES:
  412. CMD_PARSER.error(f"the 'hires' parameter is not supported for stage '{stage_name}'")
  413.  
  414. elif current_section == PLAYER_INFO:
  415. if parameter.name in {P1_START_X, P2_START_X}:
  416. indexes[parameter.name] = line_index
  417.  
  418. elif current_section == CAMERA:
  419. if parameter.name in {ZOOM_IN, ZOOM_OUT}:
  420. indexes[parameter.name] = line_index
  421.  
  422. zoom_value = parse_first_value(clean_line)
  423. if zoom_value != 1.0:
  424. CMD_PARSER.error(f"only zoom-less stages are supported, "
  425. f"but stage '{stage_name}' has zoom values other than 1.0")
  426.  
  427. elif parameter.name in {BOUND_HIGH, BOUND_LEFT, BOUND_RIGHT, CUT_LOW, VERTICAL_FOLLOW}:
  428. indexes[parameter.name], values[parameter.name] = line_index, float(parameter.values[0])
  429.  
  430. elif current_section == BG_DEF:
  431. if parameter.name == DEBUG_BG:
  432. indexes[DEBUG_BG] = line_index
  433.  
  434. if not (BOUND_LEFT in indexes and BOUND_RIGHT in indexes):
  435. CMD_PARSER.error(f"camera bound parameters missing for stage '{stage_name}'")
  436.  
  437. local_width = values[LOCAL_COORD].width
  438. local_height = values[LOCAL_COORD].height
  439.  
  440. for window_index, line_index in enumerate(indexes[WINDOW]):
  441. window_coords = values[WINDOW][window_index]
  442. window_width = window_coords[2] - window_coords[0]
  443. window_height = window_coords[3] - window_coords[1]
  444.  
  445. # detect and remove full-screen windows
  446. if local_width - window_width <= WINDOW_TOLERANCE and local_height - window_height <= WINDOW_TOLERANCE:
  447. lines[line_index] = ";" + lines[line_index]
  448.  
  449. local_ratio = local_width / local_height
  450. target_zoom = args.target_zoom or 1.0 / (target_ratio / local_ratio)
  451. width_extension = local_width * get_width_extension_ratio(target_zoom)
  452.  
  453. modify_first_value(lines, indexes[BOUND_LEFT], lambda bound: bound + width_extension)
  454. modify_first_value(lines, indexes[BOUND_RIGHT], lambda bound: bound - width_extension)
  455.  
  456. bound_radius = (values[BOUND_RIGHT] - values[BOUND_LEFT]) / 2.0
  457.  
  458. def delta_modifier(delta: float) -> float:
  459. return max((delta * bound_radius - width_extension) / (bound_radius - width_extension), 0.0)
  460.  
  461. def delta_verifier(old_delta: float, new_delta: float):
  462. if old_delta > 0.0 and new_delta == 0.0:
  463. print(f"{CMD_PARSER.prog}: warning: for stage '{stage_name}', "
  464. f"capping a non-zero delta {debug(old_delta)} to zero; "
  465. f"this suggests that some artifacts may remain", file=sys.stderr)
  466.  
  467. for delta_index in indexes[DELTA]:
  468. modify_first_value(lines, delta_index, delta_modifier, delta_verifier)
  469.  
  470. for parallax_info in parallax_infos if not args.no_parallax else []:
  471. xscale_index = parallax_info.xscale_index
  472. top_xscale = parse_first_value(lines[xscale_index])
  473. bottom_xscale = parse_second_value(lines[xscale_index])
  474.  
  475. if isclose(top_xscale, 0.0) or isclose(bottom_xscale, 0.0):
  476. print(f"{CMD_PARSER.prog}: warning: for stage '{stage_name}', will not modify parallax "
  477. f"with xscale values {debug(top_xscale, bottom_xscale)} too close to zero")
  478. continue
  479.  
  480. bottom_delta_factor = bottom_xscale / top_xscale
  481.  
  482. delta_index = parallax_delta_indexes[parallax_info.bg]
  483. top_delta = parse_first_value(lines[delta_index])
  484. bottom_delta = top_delta * bottom_delta_factor
  485.  
  486. if isclose(top_delta, 0.0) or isclose(bottom_delta, 0.0):
  487. print(f"{CMD_PARSER.prog}: warning: for stage '{stage_name}', will not modify parallax "
  488. f"with effective delta values {debug(top_delta, bottom_delta)} too close to zero")
  489. continue
  490.  
  491. if isclose(bottom_delta_factor, 1.0) or isclose(bottom_delta, 1.0):
  492. modify_first_value(lines, delta_index, delta_modifier, delta_verifier)
  493. continue
  494.  
  495. if isclose(top_delta, 1.0):
  496. new_bottom_delta = delta_modifier(bottom_delta_factor)
  497. if isclose(new_bottom_delta, 0.0):
  498. print(f"{CMD_PARSER.prog}: warning: for stage '{stage_name}', will not modify parallax "
  499. f"with resulting bottom xscale value {debug(new_bottom_delta)} too close to zero")
  500. continue
  501.  
  502. modify_first_value(lines, xscale_index, lambda _: 1.0)
  503. modify_second_value(lines, xscale_index, lambda _: new_bottom_delta)
  504. continue
  505.  
  506. if top_delta < bottom_delta:
  507. new_top_delta = delta_modifier(top_delta)
  508. new_bottom_delta = (bottom_delta - 1.0) * (1.0 - new_top_delta) / (1.0 - top_delta) + 1.0
  509. else:
  510. new_bottom_delta = delta_modifier(bottom_delta)
  511. new_top_delta = (top_delta - 1.0) * (1.0 - new_bottom_delta) / (1.0 - bottom_delta) + 1.0
  512.  
  513. if isclose(new_top_delta, 0.0) or isclose(new_bottom_delta, 0.0):
  514. print(f"{CMD_PARSER.prog}: warning: for stage '{stage_name}', will not modify parallax "
  515. f"with resulting effective delta values {debug(new_top_delta, new_bottom_delta)} too close to zero")
  516. continue
  517.  
  518. modify_first_value(lines, delta_index, lambda _: new_top_delta)
  519.  
  520. modify_first_value(lines, xscale_index, lambda _: 1.0)
  521. modify_second_value(lines, xscale_index, lambda _: new_bottom_delta / new_top_delta)
  522.  
  523. zoom_in = args.set_zoom_in or target_zoom
  524. zoom_out = args.set_zoom_out or target_zoom
  525.  
  526. if zoom_out < target_zoom:
  527. print(f"{CMD_PARSER.prog}: warning: for stage '{stage_name}', setting the zoom-out ({zoom_out}) "
  528. f"lower than the target zoom ({target_zoom}) will introduce artifacts", file=sys.stderr)
  529.  
  530. start_x_ratio = get_width_extension_ratio(zoom_in) + 1.0
  531.  
  532. for start_index in [indexes[P1_START_X], indexes[P2_START_X]]:
  533. modify_first_value(lines, start_index, lambda start_x: start_x * start_x_ratio)
  534.  
  535. if values[VERTICAL_FOLLOW] == 0:
  536. assert VERTICAL_FOLLOW in indexes, "'verticalfollow' must exist in the DEF file to be non-zero"
  537. set_value(lines, indexes[VERTICAL_FOLLOW], VERTICAL_FOLLOW_MIN)
  538.  
  539. line_break = LINE_BREAK_RE.search(lines[indexes[INFO]]).group(0)
  540. insert_lines = []
  541.  
  542. def update(param: str, insert_index: int, value: float) -> None:
  543. set_or_insert_value(lines, indexes.get(param), insert_lines, insert_index, param.lower(), value, line_break)
  544.  
  545. if args.debug_bg:
  546. update(DEBUG_BG, indexes[BG_DEF], 1)
  547.  
  548. camera_index = indexes[CAMERA]
  549.  
  550. if not bounds_mode == MODE_NONE:
  551. is_static = values[BOUND_HIGH] == 0.0
  552. height_margin = round(get_height_margin(bounds_mode, local_height, values[BOUND_HIGH]))
  553.  
  554. if bounds_mode == MODE_MUGEN or (bounds_mode == MODE_IKEMEN and not is_static):
  555. update(BOUND_HIGH, camera_index, values[BOUND_HIGH] + height_margin)
  556. update(CUT_LOW, camera_index, height_margin)
  557.  
  558. else: # bounds_mode == MODE_IKEMEN and is_static:
  559. update(BOUND_HIGH, camera_index, 1)
  560. update(CUT_LOW, camera_index, height_margin + 1)
  561. update(VERTICAL_FOLLOW, camera_index, VERTICAL_FOLLOW_MIN)
  562.  
  563. update(ZOOM_OUT, camera_index, zoom_out)
  564. update(ZOOM_IN, camera_index, zoom_in)
  565.  
  566. update(MUGEN_VERSION, indexes[INFO], 1.1)
  567.  
  568. for insert_line in sorted(insert_lines, key = lambda it: it[0], reverse = True):
  569. lines.insert(insert_line[0], insert_line[1])
  570.  
  571. return lines
  572.  
  573.  
  574. def get_stage_out_file(stage_file: Path, out_path: Path) -> Path:
  575. if not out_path:
  576. out_file = stage_file
  577. elif out_path.is_dir():
  578. out_file = Path(out_path, stage_file.name)
  579. else:
  580. out_file = out_path
  581.  
  582. if out_file.exists() and not out_file.is_file():
  583. CMD_PARSER.error(f"invalid output file: '{out_file}'")
  584. return out_file
  585.  
  586.  
  587. def main():
  588. args = CMD_PARSER.parse_args()
  589. if len(args.stage_def_files) > 1 and args.output_path and not args.output_path.is_dir():
  590. CMD_PARSER.error(f"the output path needs to be a directory "
  591. f"if multiple input file are given: '{args.output_path}'")
  592.  
  593. if args.target_aspect_ratio and args.target_zoom:
  594. CMD_PARSER.error(f"the target aspect ratio (-r) and target zoom (-z) options are mutually exclusive")
  595.  
  596. bounds_mode = (args.bounds_mode or MODE_MUGEN).casefold()
  597. if bounds_mode not in BOUNDS_MODE_VALUES:
  598. CMD_PARSER.error(f"invalid bounds mode: '{bounds_mode}', allowed values: {BOUNDS_MODE_VALUES}")
  599.  
  600. target_ratio = parse_ratio(args.target_aspect_ratio or "16x9")
  601.  
  602. for stage_file in args.stage_def_files:
  603. if not stage_file.is_file():
  604. CMD_PARSER.error(f"the given stage DEF_FILE path: '{stage_file}' does not point to a regular file")
  605.  
  606. stage_out_file = get_stage_out_file(stage_file, args.output_path)
  607. stage_bak_file = stage_out_file.with_name(stage_out_file.name + ".bak")
  608.  
  609. make_backup = stage_out_file.exists() and not args.no_backup
  610. if make_backup and stage_bak_file.exists():
  611. CMD_PARSER.error(f"the stage backup file: '{stage_bak_file}' already exists, aborting")
  612.  
  613. lines = process_stage(args, stage_file, target_ratio, bounds_mode)
  614.  
  615. if make_backup:
  616. shutil.copyfile(stage_out_file, stage_bak_file)
  617.  
  618. stage_out_file.parent.mkdir(parents=True, exist_ok=True)
  619.  
  620. with open(stage_out_file, "w", newline="", encoding="iso8859_1") as stage_output:
  621. stage_output.writelines(lines)
  622.  
  623.  
  624. if __name__ == '__main__':
  625. main()
  626.  
Runtime error #stdin #stdout #stderr 0.2s 25068KB
stdin
Standard input is empty
stdout
Standard output is empty
stderr
Traceback (most recent call last):
  File "./prog.py", line 221, in <module>
    @dataclass(frozen=True)
  File "./prog.py", line 224, in Parameter
    values: list[str]
TypeError: 'type' object is not subscriptable