diff --git a/.gitignore b/.gitignore index a16355f750..a0769c6849 100644 --- a/.gitignore +++ b/.gitignore @@ -199,3 +199,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# VSCode +.vs/ diff --git a/data/src/HexBug/data/special_handlers.py b/data/src/HexBug/data/special_handlers.py index 35defee05c..20b6883cf7 100644 --- a/data/src/HexBug/data/special_handlers.py +++ b/data/src/HexBug/data/special_handlers.py @@ -18,6 +18,7 @@ VALID_MASK_PATTERN = re.compile(r"[-v]+") VALID_HEXFLOW_COPY_MASK_PATTERN = re.compile(r"[-n]+") +VALID_SWIZZLE_PATTERN = re.compile(r"[xyz]{3}") class SpecialHandlerInfo(BaseModel): @@ -657,3 +658,88 @@ def parse_tail_depth(value: str): if all(c == "-" for c in value): return len(value) raise ValueError(f"Invalid tail depth (expected an integer or dashes): {value}") + + +class HextrapatsSwizzlingSpecialHandler(SpecialHandler[str]): + prefix = "eeeeqaawddea" + + @override + def try_match(self, registry: HexBugRegistry, pattern: HexPattern) -> str | None: + if pattern.signature.startswith(self.prefix): + flat_dir = pattern.direction.rotated_by(HexAngle.BACK) + + result = "" + side = 0 + + for direction in itertools.islice(pattern.iter_directions(), 13, None): + match direction.angle_from(flat_dir): + case HexAngle.FORWARD if side == 0: + result += "Y" + case HexAngle.RIGHT: + if side == 0: + side = 1 + elif side == -1: + side = 0 + result += "X" + case HexAngle.LEFT: + if side == 0: + side = -1 + elif side == 1: + side = 0 + result += "Z" + case _: + return None + + if side != 0 or len(result) != 3: + return None + + return result + + def generate_pattern( + self, + registry: HexBugRegistry, + value: str, + ) -> tuple[str, HexPattern]: + value = value.lower().strip() + if not VALID_SWIZZLE_PATTERN.fullmatch(value): + raise ValueError( + f"Invalid swizzle (expected exactly 3 of x, y, or z): {value}" + ) + + signature = self.prefix + current = HexDir.NORTH_EAST + + # Surely there's a better way to do this + for c in value: + match c: + case "x": + match current: + case HexDir.NORTH_EAST: + signature += "wd" + case HexDir.EAST: + signature += "qd" + case HexDir.SOUTH_EAST: + signature += "ad" + current = HexDir.SOUTH_EAST + case "y": + match current: + case HexDir.NORTH_EAST: + signature += "e" + case HexDir.EAST: + signature += "w" + case HexDir.SOUTH_EAST: + signature += "q" + current = HexDir.EAST + case "z": + match current: + case HexDir.NORTH_EAST: + signature += "da" + case HexDir.EAST: + signature += "ea" + case HexDir.SOUTH_EAST: + signature += "wa" + current = HexDir.NORTH_EAST + case _: + raise RuntimeError("unreachable") + + return value, HexPattern(HexDir.WEST, signature) diff --git a/data/test/data/test_special_handlers.py b/data/test/data/test_special_handlers.py index e75b97d003..0b9c96a864 100644 --- a/data/test/data/test_special_handlers.py +++ b/data/test/data/test_special_handlers.py @@ -7,6 +7,7 @@ from HexBug.data.registry import HexBugRegistry from HexBug.data.special_handlers import ( HexFlowCopyMaskSpecialHandler, + HextrapatsSwizzlingSpecialHandler, MaskSpecialHandler, ) @@ -130,3 +131,49 @@ def generate_pattern( registry = cast(HexBugRegistry, None) # lie _, pattern = special_handler.generate_pattern(registry, value) assert pattern.signature == want_signature + + +def describe_HextrapatsSwizzlingSpecialHandler(): + @pytest.fixture() + def special_handler() -> HextrapatsSwizzlingSpecialHandler: + return HextrapatsSwizzlingSpecialHandler( + id=ResourceLocation("hextrapats", "vec/swizzle"), + ) + + patterns = [ + ("eeeeqaawddeawdadad", "XXX"), + ("eeeeqaawddeaeww", "YYY"), + ("eeeeqaawddeadadada", "ZZZ"), + ("eeeeqaawddeawdqea", "XYZ"), + ("eeeeqaawddeaeeae", "YZY"), + ("eeeeqaawddeawdadwa", "XXZ"), + ] + + @pytest.mark.parametrize("direction", HexDir) + @pytest.mark.parametrize( + ["signature", "want_value"], + [*patterns, ("eeeeqaawddeadaww", None), ("eeeeqaawddeaeweawd", None)], + ids=lambda v: f"'{v}'", + ) + def try_match( + special_handler: HextrapatsSwizzlingSpecialHandler, + direction: HexDir, + signature: str, + want_value: str | None, + ): + registry = cast(HexBugRegistry, None) # lie + pattern = HexPattern(direction, signature) + assert special_handler.try_match(registry, pattern) == want_value + + @pytest.mark.parametrize( + ["value", "want_signature"], + [(value, signature) for (signature, value) in patterns], + ) + def generate_pattern( + special_handler: HextrapatsSwizzlingSpecialHandler, + value: str, + want_signature: str, + ): + registry = cast(HexBugRegistry, None) # lie + _, pattern = special_handler.generate_pattern(registry, value) + assert pattern.signature == want_signature