Skip to content

Instantly share code, notes, and snippets.

@object-Object
Created July 17, 2022 00:48
Show Gist options
  • Save object-Object/8b35f0367dea533e12eb59386c30c1ef to your computer and use it in GitHub Desktop.
Save object-Object/8b35f0367dea533e12eb59386c30c1ef to your computer and use it in GitHub Desktop.
A parser to display pattern names for Hex Casting hexes.
from __future__ import annotations
import regex
from typing import Literal
from enum import Enum
# working as of https://github.com/gamma-delta/HexMod/blob/c00815b7b9d90593dc33e3a7539ce87c2ece4fc9/Common/src/main/java/at/petrak/hexcasting/common/casting/RegisterPatterns.java
# setup:
# - run "pip install regex"
# - download this script somewhere
# - go to https://github.com/gamma-delta/HexMod/blob/main/Common/src/main/java/at/petrak/hexcasting/common/casting/RegisterPatterns.java
# - right click the "Raw" button, click "Save Link As", and save it with the name "RegisterPatterns.java" to the same folder as this script
# - note: you'll have to do this again after each update, if new patterns are added or old patterns are changed
# - (optional) disable highlighting by changing the value on following line from False to True
NO_HIGHLIGHTING = False
# usage:
# - put your hex on the stack
# - cast Reveal
# - open <game folder>/logs/latest.log, scroll to the bottom, and copy your hex
# - paste your hex inside the quotes on the following line (it should look like this: data = "[HexPattern(...), HexPattern(...)]")
data = ""
# - save and run the script
regexes = {
"registry": regex.compile(r"PatternRegistry\s*\.\s*mapPattern\s*\(\s*HexPattern\s*\.\s*fromAngles\s*\(\s*\"([aqwed]+)\"\s*,\s*HexDir\s*\.\s*\w+\s*\)\s*,\s*modLoc\s*\(\s*\"([\w/]+)\"\s*\)", regex.M),
"list_iota": regex.compile(r"\[(.*)\]"),
"list_iota_item": regex.compile(r"(\[(?>[^\[\]]+|(?R))*\]|[^,]+)(?:, )?"),
"pattern_iota": regex.compile(r"HexPattern\((\w+)(?: ([aqwed]+))?\)"),
"number_constant": regex.compile(r"\d+(?:\.\d+)?"),
}
class Angle(Enum):
FORWARD = 0
w = 0
RIGHT = 1
e = 1
RIGHT_BACK = 2
d = 2
BACK = 3
LEFT_BACK = 4
a = 4
LEFT = 5
q = 5
class Direction(Enum): # numbers increase clockwise
NORTH_EAST = 0
EAST = 1
SOUTH_EAST = 2
SOUTH_WEST = 3
WEST = 4
NORTH_WEST = 5
def angle_from(self, other: Direction) -> Angle:
return Angle((self.value - other.value) % len(Angle))
def rotated(self, angle: str | Angle) -> Direction:
offset = Angle[angle].value if type(angle) is str else angle.value
return Direction((self.value + offset) % len(Direction))
# parse the pattern definitions
pattern_lookup = {}
with open("./RegisterPatterns.java", "r") as file:
for match in regexes["registry"].finditer(file.read()):
(pattern, name) = match.groups()
pattern_lookup[pattern] = name
def print_indented(value: str, level: int, value_type: Literal["pattern", "number_constant", "unknown", "none"]):
indent = " " * level
if NO_HIGHLIGHTING:
print(indent + value)
return
match value_type:
case "pattern":
highlight = "\033[93m" # yellow
case "number_constant":
highlight = "\033[92m" # light green
case "unknown":
highlight = "\033[91m" # light red
case "none" | None:
highlight = ""
print(indent + highlight + value + "\033[00m")
def get_pattern_directions(starting_direction: Direction, pattern: str) -> list[Direction]:
directions = [starting_direction]
for c in pattern:
directions.append(directions[-1].rotated(c))
return directions
def parse_mask(starting_direction: Direction, pattern: str) -> str | None:
if not pattern:
return "⁻"
directions = get_pattern_directions(starting_direction, pattern)
flat_direction = starting_direction.rotated(Angle.LEFT) if pattern[0] == "a" else starting_direction
mask = ""
skip = False
for index, direction in enumerate(directions):
if skip:
skip = False
continue
angle = direction.angle_from(flat_direction)
if angle == Angle.FORWARD:
mask += "⁻"
continue
if index >= len(directions) - 1:
return None
angle2 = directions[index + 1].angle_from(flat_direction)
if angle == Angle.RIGHT and angle2 == Angle.LEFT:
mask += "v"
skip = True
continue
return None
return mask
def parse_iota(iota: str, level: int = 0) -> int:
list_contents_match = regexes["list_iota"].fullmatch(iota)
if list_contents_match:
list_contents = list_contents_match.group(1)
print_indented("[", level, "none")
for item_match in regexes["list_iota_item"].finditer(list_contents):
level += parse_iota(item_match.group(1), level + 1)
print_indented("]", level, "none")
else:
pattern_match = regexes["pattern_iota"].fullmatch(iota)
if pattern_match:
direction = Direction[pattern_match.group(1)]
pattern = pattern_match.group(2) or ""
name = pattern_lookup.get(pattern)
mask = parse_mask(direction, pattern)
if name:
if name == "open_paren":
print_indented("{", level, "pattern")
return 1
elif name == "close_paren":
print_indented("}", level - 1, "pattern")
return -1
else:
print_indented(name, level, "pattern")
elif mask:
print_indented(f"mask {mask}", level, "pattern")
elif pattern.startswith(("aqaa", "dedd")):
negate = pattern.startswith("dedd")
accumulator = 0.
for c in pattern[4:]:
match c:
case "w":
accumulator += 1
case "q":
accumulator += 5
case "e":
accumulator += 10
case "a":
accumulator *= 2
case "d":
accumulator /= 2
if negate:
accumulator = -accumulator
print_indented(f"number {str(accumulator).rstrip('0').rstrip('.')}", level, "pattern")
else:
print_indented(pattern, level, "unknown")
else:
if regexes["number_constant"].fullmatch(iota):
print_indented(iota, level, "number_constant")
else:
print_indented(iota, level, "none")
return 0
parse_iota(data)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment