Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/python3
- # Copyright (c) 2024 Pixel Grass
- #
- # Permission is hereby granted, free of charge, to any person obtaining a copy
- # of this software and associated documentation files (the "Software"), to deal
- # in the Software without restriction, including without limitation the rights
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- # copies of the Software, and to permit persons to whom the Software is
- # furnished to do so, subject to the following conditions:
- #
- # The above copyright notice and this permission notice shall be included in all
- # copies or substantial portions of the Software.
- #
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- # SOFTWARE.
- import argparse
- import re
- import shutil
- import sys
- from dataclasses import dataclass
- from decimal import Decimal, Context, ROUND_HALF_DOWN
- from math import ceil, floor, log10, isclose
- from pathlib import Path
- from typing import Any, Callable, Optional, Pattern
- CMD_PARSER = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description="""
- A stage DEF file converter for M.U.G.E.N/Ikemen that allows to resize the whole stage by using
- the `xscale` and `yscale` parameters. The horizontal deltas, vertical offsets of backgrounds
- and the left, right and upper camera bounds are also adjusted accordingly.
- The vertical camera bounds may still need manual adjustments after conversion.
- Stages that already have the `xscale` or `yscale` parameters set to values other than 1.0
- are not supported. Highres stages using the `highres` parameter are also not supported.
- """, epilog="""
- examples:
- %(prog)s -s 1.5 DEF_FILE
- modify the given stage DEF file to resize the stage to 150%%
- %(prog)s -y 1.25 DEF_FILE
- modify the given stage DEF file to resize the stage vertically to 125%%
- the horizontal dimensions of the stage will remain unchanged
- Copyright (c) 2024 Pixel Grass, License: MIT
- ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. **
- """)
- CMD_PARSER.add_argument("stage_def_files", metavar="DEF_FILE", type=Path, nargs="+", help="""
- the stage DEF file (multiple files can be given)\nthe files will be modified in-place!
- """.strip())
- CMD_PARSER.add_argument("-s", "--scale", metavar="SCALE", type=float, help="""
- use the `xscale` and `yscale` stage parameters to resize the stage by the given factor
- adjust the horizontal deltas, vertical offsets of backgrounds and the camera bounds
- """.strip())
- CMD_PARSER.add_argument("-x", "--xscale", metavar="XSCALE", type=float, help=f"""
- use the `xscale` stage parameter to resize the stage horizontally by the given factor
- adjust the horizontal deltas of backgrounds and the left and right camera bounds
- """.strip())
- CMD_PARSER.add_argument("-y", "--yscale", metavar="YSCALE", type=float, help=f"""
- use the `yscale` stage parameter to resize the stage vertically by the given factor
- adjust the vertical offsets of backgrounds and the upper camera bound
- """.strip())
- CMD_PARSER.add_argument("-p", "--no-parallax", default=False, action='store_true', help="""
- do not modify the parallax backgrounds (default: modify parallax backgrounds)
- """.strip())
- CMD_PARSER.add_argument("-d", "--debug-bg", default=False, action='store_true', help="""
- set the `debugbg` property to `1` in the modified stage DEF file (default: do not set)
- """.strip())
- CMD_PARSER.add_argument("-o", "--output-path", metavar="OUT_PATH", type=Path, help="""
- write the modified stage DEF file or files to the given output file or
- directory respectively, instead of modifying the given DEF files in-place
- """.strip())
- CMD_PARSER.add_argument('--no-backup', default=False, action='store_true', help="""
- do not create a backup for the modified stage DEF files
- all of the file will be irreversibly modified in place!
- """.strip())
- CMD_PARSER.add_argument('--version', action='version', version="%(prog)s 1.0.0")
- INFO = "Info".casefold()
- STAGE_INFO = "StageInfo".casefold()
- CAMERA = "Camera".casefold()
- BG_DEF = "BGdef".casefold()
- MUGEN_VERSION = "mugenversion".casefold()
- LOCAL_COORD = "localcoord".casefold()
- Z_OFFSET = "zoffset".casefold()
- HIRES = "hires".casefold()
- X_SCALE = "xscale".casefold()
- Y_SCALE = "yscale".casefold()
- BOUND_HIGH = "boundhigh".casefold()
- BOUND_LEFT = "boundleft".casefold()
- BOUND_RIGHT = "boundright".casefold()
- TYPE = "type".casefold()
- START = "start".casefold()
- DELTA = "delta".casefold()
- XSCALE = "xscale".casefold()
- WINDOW = "window".casefold()
- DEBUG_BG = "debugbg".casefold()
- PARALLAX = "parallax".casefold()
- VALUES_COUNT = {
- MUGEN_VERSION: 1,
- LOCAL_COORD: 2, HIRES: 1,
- X_SCALE: {1, 2}, Y_SCALE: 1,
- BOUND_HIGH: 1, BOUND_LEFT: 1, BOUND_RIGHT: 1,
- START: 2, DELTA: {1, 2}, WINDOW: 4,
- BG_DEF: 1, TYPE: 1
- }
- DECIMAL_PLACES = 8
- WINDOW_TOLERANCE = 5
- COMMENT_RE = re.compile(r";.*")
- SECTION_RE = re.compile(r"^\[([^]]+)]$")
- PARAMETER_RE = re.compile(r"^([^=]+)=([^=]*)$")
- LINE_BREAK_RE = re.compile("[\r\n]+$")
- FIRST_VALUE_RE = re.compile(r"(^\s*[^=;]+\s*=\s*)([^=,;\s]+)(.*$)")
- SECOND_VALUE_RE = re.compile(r"(^\s*[^=;]+\s*=\s*[^=,;\s]+\s*,\s*)([^=,;\s]+)(.*$)")
- @dataclass(frozen=True)
- class Parameter:
- name: str
- values: list[str]
- @dataclass(frozen=True)
- class LocalCoord:
- width: float = 320.0
- height: float = 240.0
- @dataclass(frozen=True)
- class ParallaxInfo:
- bg: str
- xscale_index: int
- Parser = Callable[[str], float]
- Modifier = Callable[[float], float]
- Verifier = Callable[[float, float], None]
- def parse_parameter(param_line: str, stage_name: str) -> Optional[Parameter]:
- match = PARAMETER_RE.search(param_line)
- if match is None:
- return None
- param_name = match.group(1).strip().casefold()
- param_values = match.group(2).strip().split(",")
- if param_name in VALUES_COUNT:
- actual_len = len(param_values)
- expected_len = VALUES_COUNT[param_name]
- if actual_len not in (expected_len if isinstance(expected_len, set) else {expected_len}):
- CMD_PARSER.error(f"invalid parameter for stage '{stage_name}', line: '{param_line}'")
- return Parameter(param_name, list(value.strip() for value in param_values))
- def parse_value(param_line: str, pattern: Pattern, name: str) -> float:
- try:
- match = pattern.search(param_line)
- return float(match.group(2))
- except:
- CMD_PARSER.error(f"failed to parse the {name} parameter value, line: '{param_line}'")
- def parse_first_value(param_line: str) -> float:
- return parse_value(param_line, FIRST_VALUE_RE, "first")
- def parse_second_value(param_line: str) -> float:
- return parse_value(param_line, SECOND_VALUE_RE, "second")
- def round_value(value: float) -> Decimal:
- precision = DECIMAL_PLACES + (0 if abs(value) < 1 else int(floor(log10(abs(value)) + 1.0)))
- return Decimal(value).normalize(Context(prec=precision, rounding=ROUND_HALF_DOWN))
- def modify_value(lines: list[str], index: int, pattern: Pattern,
- parse: Parser, modify: Modifier, verify: Optional[Verifier] = None) -> None:
- param_line = lines[index]
- old_value = parse(param_line)
- new_value = modify(old_value)
- not verify or verify(old_value, new_value)
- rounded_value = round_value(new_value)
- lines[index] = pattern.sub(f"\\g<1>{rounded_value:f}\\g<3>", param_line)
- def modify_first_value(lines: list[str], index: int, modify: Modifier, verify: Optional[Verifier] = None) -> None:
- return modify_value(lines, index, FIRST_VALUE_RE, parse_first_value, modify, verify)
- def modify_second_value(lines: list[str], index: int, modify: Modifier, verify: Optional[Verifier] = None) -> None:
- return modify_value(lines, index, SECOND_VALUE_RE, parse_second_value, modify, verify)
- def set_value(lines: list[str], index: int, value: float) -> None:
- param_line = lines[index]
- lines[index] = FIRST_VALUE_RE.sub(f"\\g<1>{round_value(value):f}\\g<3>", param_line)
- def insert_value(lines: list[tuple[int, str]], index: int, param_name: str, value: float, line_break_str: str) -> None:
- lines.append((index + 1, f"{param_name} = {round_value(value):f}{line_break_str}"))
- def set_or_insert_value(
- set_lines: list[str], set_index: int, insert_lines: list[tuple[int, str]], insert_index: int,
- param_name: str, value: float, line_break_str: str) -> None:
- if set_index is not None:
- set_value(set_lines, set_index, value)
- else:
- insert_value(insert_lines, insert_index, param_name, value, line_break_str)
- def debug(a: float, b: [float, None] = None) -> str:
- return f"({round_value(a)})" if b is None else f"({round_value(a)}, {round_value(b)})"
- def process_stage(args, stage_file: Path, x_scale: float, y_scale: float) -> list[str]:
- with open(stage_file, newline="", encoding="iso8859_1") as stage_input:
- lines = stage_input.readlines()
- stage_name = stage_file.name
- values: dict[str, Any] = {LOCAL_COORD: LocalCoord(), BOUND_HIGH: -25.0, WINDOW: []}
- indexes: dict[str, Any] = {START: [], DELTA: [], WINDOW: []}
- parallax_infos: list[ParallaxInfo] = []
- parallax_delta_indexes: dict[str, int] = {}
- current_section = None
- current_bg_type = None
- for line_index, line in enumerate(lines):
- clean_line = COMMENT_RE.sub("", line).strip()
- if not clean_line:
- continue
- section_match = SECTION_RE.search(clean_line)
- if section_match is not None:
- current_section = section_match.group(1).strip().casefold()
- indexes[current_section] = line_index
- current_bg_type = None
- continue
- if current_section is None:
- continue
- parameter = parse_parameter(clean_line, stage_name)
- if parameter is None:
- continue
- if parameter.name == TYPE:
- current_bg_type = parameter.values[0].strip().casefold()
- continue
- elif parameter.name == START:
- indexes[START].append(line_index)
- continue
- elif parameter.name == DELTA:
- delta_value = float(parameter.values[0])
- if delta_value < 0.0:
- CMD_PARSER.error(f"for stage '{stage_name}', "
- f"negative delta values {debug(delta_value)} are not supported, aborting'")
- if current_bg_type == PARALLAX:
- parallax_delta_indexes[current_section] = line_index
- else:
- indexes[DELTA].append(line_index)
- continue
- elif parameter.name == XSCALE and current_bg_type == PARALLAX:
- parallax_infos.append(ParallaxInfo(current_section, line_index))
- continue
- elif parameter.name == WINDOW:
- indexes[WINDOW].append(line_index)
- values[WINDOW].append(list(float(coord) for coord in parameter.values))
- continue
- if current_section == INFO:
- if parameter.name == MUGEN_VERSION:
- indexes[MUGEN_VERSION] = line_index
- elif current_section == STAGE_INFO:
- if parameter.name == LOCAL_COORD:
- values[LOCAL_COORD] = LocalCoord(float(parameter.values[0]), float(parameter.values[1]))
- elif parameter.name == Z_OFFSET:
- indexes[parameter.name], values[parameter.name] = line_index, float(parameter.values[0])
- elif parameter.name == HIRES:
- CMD_PARSER.error(f"the 'hires' parameter is not supported for stage '{stage_name}'")
- elif parameter.name in {X_SCALE, Y_SCALE}:
- indexes[parameter.name] = line_index
- scale_value = float(parameter.values[0])
- if scale_value != 1.0:
- if (parameter.name == X_SCALE and x_scale != 1.0) or (parameter.name == Y_SCALE and y_scale != 1.0):
- CMD_PARSER.error(f"re-scaling already scaled stages is not supported, but stage '{stage_name}' "
- f"has '{parameter.name.lower()}' value other than 1.0")
- elif current_section == CAMERA:
- if parameter.name in {BOUND_HIGH, BOUND_LEFT, BOUND_RIGHT}:
- indexes[parameter.name], values[parameter.name] = line_index, float(parameter.values[0])
- elif current_section == BG_DEF:
- if parameter.name == DEBUG_BG:
- indexes[DEBUG_BG] = line_index
- if not (BOUND_LEFT in indexes and BOUND_RIGHT in indexes):
- CMD_PARSER.error(f"camera bound parameters missing for stage '{stage_name}'")
- local_width = values[LOCAL_COORD].width
- local_height = values[LOCAL_COORD].height
- for window_index, line_index in enumerate(indexes[WINDOW]):
- window_coords = values[WINDOW][window_index]
- window_width = window_coords[2] - window_coords[0]
- window_height = window_coords[3] - window_coords[1]
- # detect and remove full-screen windows
- if local_width - window_width <= WINDOW_TOLERANCE and local_height - window_height <= WINDOW_TOLERANCE:
- lines[line_index] = ";" + lines[line_index]
- bounds_diameter = values[BOUND_RIGHT] - values[BOUND_LEFT]
- stage_diameter = bounds_diameter + local_width
- bounds_radius = bounds_diameter / 2.0
- bounds_radius_scaled = (stage_diameter * x_scale - local_width) / 2.0
- bounds_extension = bounds_radius_scaled - bounds_radius
- modify_first_value(lines, indexes[BOUND_LEFT], lambda bound: bound - bounds_extension)
- modify_first_value(lines, indexes[BOUND_RIGHT], lambda bound: bound + bounds_extension)
- def delta_modifier(delta: float) -> float:
- bg_diameter = bounds_diameter * delta + local_width
- return max((bg_diameter * x_scale - local_width) / (2.0 * bounds_radius_scaled), 0.0)
- def delta_verifier(old_delta: float, new_delta: float):
- if old_delta > 0.0 and new_delta == 0.0:
- print(f"{CMD_PARSER.prog}: warning: for stage '{stage_name}', "
- f"capping a non-zero delta {debug(old_delta)} to zero; "
- f"this suggests that some artifacts may remain", file=sys.stderr)
- for delta_index in indexes[DELTA]:
- modify_first_value(lines, delta_index, delta_modifier, delta_verifier)
- height_extension = local_height * (y_scale - 1.0)
- y_offset = -ceil(height_extension / y_scale) if height_extension > 0.0 else -floor(height_extension / y_scale)
- for start_index in indexes[START]:
- modify_second_value(lines, start_index, lambda start_y: start_y + y_offset)
- for parallax_info in parallax_infos if not args.no_parallax else []:
- xscale_index = parallax_info.xscale_index
- top_xscale = parse_first_value(lines[xscale_index])
- bottom_xscale = parse_second_value(lines[xscale_index])
- if isclose(top_xscale, 0.0) or isclose(bottom_xscale, 0.0):
- print(f"{CMD_PARSER.prog}: warning: for stage '{stage_name}', will not modify parallax "
- f"with xscale values {debug(top_xscale, bottom_xscale)} too close to zero")
- continue
- bottom_delta_factor = bottom_xscale / top_xscale
- delta_index = parallax_delta_indexes[parallax_info.bg]
- top_delta = parse_first_value(lines[delta_index])
- bottom_delta = top_delta * bottom_delta_factor
- if isclose(top_delta, 0.0) or isclose(bottom_delta, 0.0):
- print(f"{CMD_PARSER.prog}: warning: for stage '{stage_name}', will not modify parallax "
- f"with effective delta values {debug(top_delta, bottom_delta)} too close to zero")
- continue
- if isclose(bottom_delta_factor, 1.0) or isclose(bottom_delta, 1.0):
- modify_first_value(lines, delta_index, delta_modifier, delta_verifier)
- continue
- if isclose(top_delta, 1.0):
- new_bottom_delta = delta_modifier(bottom_delta_factor)
- if isclose(new_bottom_delta, 0.0):
- print(f"{CMD_PARSER.prog}: warning: for stage '{stage_name}', will not modify parallax "
- f"with resulting bottom xscale value {debug(new_bottom_delta)} too close to zero")
- continue
- modify_first_value(lines, xscale_index, lambda _: 1.0)
- modify_second_value(lines, xscale_index, lambda _: new_bottom_delta)
- continue
- if top_delta < bottom_delta:
- new_top_delta = delta_modifier(top_delta)
- new_bottom_delta = (bottom_delta - 1.0) * (1.0 - new_top_delta) / (1.0 - top_delta) + 1.0
- else:
- new_bottom_delta = delta_modifier(bottom_delta)
- new_top_delta = (top_delta - 1.0) * (1.0 - new_bottom_delta) / (1.0 - bottom_delta) + 1.0
- if isclose(new_top_delta, 0.0) or isclose(new_bottom_delta, 0.0):
- print(f"{CMD_PARSER.prog}: warning: for stage '{stage_name}', will not modify parallax "
- f"with resulting effective delta values {debug(new_top_delta, new_bottom_delta)} too close to zero")
- continue
- modify_first_value(lines, delta_index, lambda _: new_top_delta)
- modify_first_value(lines, xscale_index, lambda _: 1.0)
- modify_second_value(lines, xscale_index, lambda _: new_bottom_delta / new_top_delta)
- z_complement = local_height - values[Z_OFFSET]
- z_reduction = values[Z_OFFSET] - round(local_height - z_complement * y_scale)
- modify_first_value(lines, indexes[Z_OFFSET], lambda z_offset: z_offset - z_reduction)
- line_break = LINE_BREAK_RE.search(lines[indexes[INFO]]).group(0)
- insert_lines = []
- def update(param: str, insert_index: int, value: float) -> None:
- set_or_insert_value(lines, indexes.get(param), insert_lines, insert_index, param.lower(), value, line_break)
- if args.debug_bg:
- update(DEBUG_BG, indexes[BG_DEF], 1)
- update(Y_SCALE, indexes[STAGE_INFO], y_scale)
- update(X_SCALE, indexes[STAGE_INFO], x_scale)
- update(BOUND_HIGH, indexes[CAMERA], min(floor((values[BOUND_HIGH] + y_offset) * y_scale), 0))
- update(MUGEN_VERSION, indexes[INFO], 1.1)
- for insert_line in sorted(insert_lines, key = lambda it: it[0], reverse = True):
- lines.insert(insert_line[0], insert_line[1])
- return lines
- def get_stage_out_file(stage_file: Path, out_path: Path) -> Path:
- if not out_path:
- out_file = stage_file
- elif out_path.is_dir():
- out_file = Path(out_path, stage_file.name)
- else:
- out_file = out_path
- if out_file.exists() and not out_file.is_file():
- CMD_PARSER.error(f"invalid output file: '{out_file}'")
- return out_file
- def main():
- args = CMD_PARSER.parse_args()
- if len(args.stage_def_files) > 1 and args.output_path and not args.output_path.is_dir():
- CMD_PARSER.error(f"the output path needs to be a directory "
- f"if multiple input file are given: '{args.output_path}'")
- if not (args.scale or args.xscale or args.yscale):
- CMD_PARSER.error(f"unspecified scale: please specify using -s, -x or -y")
- x_scale = args.xscale or args.scale or 1.0
- y_scale = args.yscale or args.scale or 1.0
- for stage_file in args.stage_def_files:
- if not stage_file.is_file():
- CMD_PARSER.error(f"the given stage DEF_FILE path: '{stage_file}' does not point to a regular file")
- stage_out_file = get_stage_out_file(stage_file, args.output_path)
- stage_bak_file = stage_out_file.with_name(stage_out_file.name + ".bak")
- make_backup = stage_out_file.exists() and not args.no_backup
- if make_backup and stage_bak_file.exists():
- CMD_PARSER.error(f"the stage backup file: '{stage_bak_file}' already exists, aborting")
- lines = process_stage(args, stage_file, x_scale, y_scale)
- if make_backup:
- shutil.copyfile(stage_out_file, stage_bak_file)
- stage_out_file.parent.mkdir(parents=True, exist_ok=True)
- with open(stage_out_file, "w", newline="", encoding="iso8859_1") as stage_output:
- stage_output.writelines(lines)
- if __name__ == '__main__':
- main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement