# type: ignore
# DOCUMENT
from builtins import iter as std_iter
from iters import iter
from gd.api.guidelines import Guidelines
from gd.api.hsv import HSV
from gd.api.recording import Recording, RecordingEntry
from gd.api.utils import get_dir, get_id
from gd.color import Color
from gd.converters import Password, Version
from gd.crypto import decode_base64_str, encode_base64_str, unzip_level_str, zip_level_str
from gd.decorators import cache_by
from gd.enums import (
Easing,
Enum,
Gamemode,
InstantCountComparison,
InternalType,
LevelLength,
LevelType,
PickupItemMode,
PlayerColor,
PortalType,
PulseMode,
PulseType,
Speed,
SpeedChange,
SpeedMagic,
TargetPosCoordinates,
TouchToggleMode,
ZLayer,
)
from gd.index_parser import IndexParser
from gd.iter_utils import is_iterable
from gd.model_backend import (
Base64Field,
BaseField,
BoolField,
EnumField,
FloatField,
IntField,
IterableField,
MappingField,
Model,
ModelField,
ModelIterField,
StrField,
partial,
)
from gd.text_utils import is_level_probably_decoded
from gd.typing import (
TYPE_CHECKING,
Callable,
Dict,
Iterable,
Iterator,
Mapping,
Optional,
Set,
Tuple,
Type,
TypeVar,
Union,
)
__all__ = (
"PORTAL_IDS",
"SPEED_IDS",
"SPEEDS",
"Object",
"ColorChannel",
"Channel",
"Header",
"LevelAPI",
"ColorCollection",
"DEFAULT_COLORS",
)
if TYPE_CHECKING:
from gd.api.editor import Editor
IntoColor = Union[Color, Tuple[int, int, int], str, int]
SPEEDS = {}
for speed in Speed:
name = speed.name.casefold()
magic = SpeedMagic.from_name(name)
speed_change = SpeedChange.from_name(name)
SPEEDS.update({speed.value: magic.value, speed_change.value: magic.value})
del speed, name, magic, speed_change
PORTAL_IDS = {portal.value for portal in PortalType}
SPEED_IDS = {speed.value for speed in SpeedChange}
SPEED_AND_PORTAL_IDS = PORTAL_IDS | SPEED_IDS
T = TypeVar("T")
KT = TypeVar("KT")
VT = TypeVar("VT")
KU = TypeVar("KU")
VU = TypeVar("VU")
def color_from(color: IntoColor) -> Color:
if isinstance(color, Color):
return color
elif isinstance(color, int):
return Color(color)
elif isinstance(color, str):
return Color.from_hex(color)
elif is_iterable(color):
return Color.from_rgb(*color)
else:
raise ValueError(
f"Do not know how to convert {color} to color. Known conversions: {IntoColor}."
)
def map_key_value(
mapping: Mapping[KT, VT], key_func: Callable[[KT], KU], value_func: Callable[[VT], VU]
) -> Mapping[KU, VU]:
return {key_func(key): value_func(value) for key, value in mapping.items()}
def enum_from_value(value: T, enum_type: Type[Enum]) -> Enum:
return enum_type.from_value(value)
def enum_to_value(enum: Enum) -> T:
return enum.value
[docs]class Object(Model):
PARSER = IndexParser(",", map_like=True)
id: int = IntField(index=1, default=0)
x: float = FloatField(index=2, default=0.0)
y: float = FloatField(index=3, default=0.0)
h_flipped: bool = BoolField(index=4)
v_flipped: bool = BoolField(index=5)
rotation: float = FloatField(index=6)
red: int = IntField(index=7, aliases=("r",))
green: int = IntField(index=8, aliases=("g",))
blue: int = IntField(index=9, aliases=("b",))
duration: float = FloatField(index=10)
touch_triggered: bool = BoolField(index=11)
secret_coin_id: int = IntField(index=12)
special_checked: bool = BoolField(index=13)
tint_ground: bool = BoolField(index=14) # deprecated
use_player_color_1: bool = BoolField(index=15)
use_player_color_2: bool = BoolField(index=16)
blending: bool = BoolField(index=17)
# index_18: ... = ?Field(index=18)
# index_19: ... = ?Field(index=19)
editor_layer_1: int = IntField(index=20)
color_1_id: int = IntField(index=21, aliases=("color_1",))
color_2_id: int = IntField(index=22, aliases=("color_2",))
target_color_id: int = IntField(index=23)
z_layer: ZLayer = EnumField(index=24, enum_type=ZLayer, from_field=IntField)
z_order: int = IntField(index=25)
# index_26: ... = ?Field(index=26)
# index_27: ... = ?Field(index=27)
move_x: float = FloatField(index=28)
move_y: float = FloatField(index=29)
easing: Easing = EnumField(index=30, enum_type=Easing, from_field=IntField)
text: str = Base64Field(index=31)
scale: float = FloatField(index=32)
# index_33: ... = ?Field(index=33)
group_parent: bool = BoolField(index=34)
opacity: float = FloatField(index=35)
trigger: bool = BoolField(index=36)
# index_37: ... = ?Field(index=37)
# index_38: ... = ?Field(index=38)
# index_39: ... = ?Field(index=39)
# index_40: ... = ?Field(index=40)
color_1_hsv_enabled: bool = BoolField(index=41)
color_2_hsv_enabled: bool = BoolField(index=42)
color_1_hsv: HSV = ModelField(index=43, model=HSV)
color_2_hsv: HSV = ModelField(index=44, model=HSV)
fade_in_time: float = FloatField(index=45)
hold_time: float = FloatField(index=46)
fade_out_time: float = FloatField(index=47)
pulse_mode: PulseMode = EnumField(index=48, enum_type=PulseMode, from_field=IntField)
copied_color_hsv: HSV = ModelField(index=49, model=HSV)
copied_color_id: int = IntField(index=50)
target_group_id: int = IntField(index=51)
pulse_type: PulseType = EnumField(index=52, enum_type=PulseType, from_field=IntField)
# index_53: ... = ?Field(index=53)
teleport_portal_distance: float = FloatField(index=54)
# index_55: ... = ?Field(index=53)
activate_group: bool = BoolField(index=56)
groups: Set[int] = IterableField(index=57, delim=".", transform=set, from_field=IntField)
lock_to_player_x: bool = BoolField(index=58)
lock_to_player_y: bool = BoolField(index=59)
copy_opacity: bool = BoolField(index=60)
editor_layer_2: int = IntField(index=61)
spawn_triggered: bool = BoolField(index=62)
spawn_duration: float = FloatField(index=63)
do_not_fade: bool = BoolField(index=64)
main_only: bool = BoolField(index=65)
detail_only: bool = BoolField(index=66)
do_not_enter: bool = BoolField(index=67)
degrees: int = IntField(index=68)
full_rotation_times: int = IntField(index=69)
lock_object_rotation: bool = BoolField(index=70)
other_id: int = IntField(
index=71, aliases=("follow_group_id", "target_pos_id", "center_id", "secondary_id"),
)
x_mod: float = FloatField(index=72)
y_mod: float = FloatField(index=73)
# index_74: ... = ?Field(index=74)
strength: float = FloatField(index=75)
animation_id: int = IntField(index=76)
count: int = IntField(index=77)
subtract_count: bool = BoolField(index=78)
pickup_item_mode: PickupItemMode = EnumField(
index=79, enum_type=PickupItemMode, from_field=IntField
)
item_or_block_id: int = IntField(index=80, aliases=("item_id", "block_id", "block_a_id"))
hold_mode: bool = BoolField(index=81)
touch_toggle_mode: TouchToggleMode = EnumField(
index=82, enum_type=TouchToggleMode, from_field=IntField
)
# index_83: ... = ?Field(index=83)
interval: float = FloatField(index=84)
easing_rate: float = FloatField(index=85)
exclusive: bool = BoolField(index=86)
multi_trigger: bool = BoolField(index=87)
comparison: InstantCountComparison = EnumField(
index=88, enum_type=InstantCountComparison, from_field=IntField
)
dual_mode: bool = BoolField(index=89)
speed: float = FloatField(index=90)
follow_y_delay: float = FloatField(index=91)
follow_y_offset: float = FloatField(index=92)
trigger_on_exit: bool = BoolField(index=93)
dynamic_block: bool = BoolField(index=94)
block_b_id: int = IntField(index=95)
disable_glow: bool = BoolField(index=96)
custom_rotation_speed: float = FloatField(index=97)
disable_rotation: float = FloatField(index=98)
multi_activate: bool = BoolField(index=99)
use_target: bool = BoolField(index=100)
target_pos_coordinates: TargetPosCoordinates = EnumField(
index=101, enum_type=TargetPosCoordinates, from_field=IntField
)
editor_disable: bool = BoolField(index=102)
high_detail: bool = BoolField(index=103)
# index_104: ... = ?Field(index=104)
follow_y_max_speed: float = FloatField(index=105)
randomize_start: bool = BoolField(index=106)
animation_speed: float = FloatField(index=107)
linked_group_id: int = IntField(index=108)
... # 2.2 future proofing fields will be added when it gets released
def h_flip(self) -> "Object":
self.h_flipped = not self.h_flipped
def v_flip(self) -> "Object":
self.v_flipped = not self.v_flipped
def set_id(self, directive: str) -> "Object":
self.id = get_id(directive)
return self
def set_z_layer(self, directive: str) -> "Object":
self.z_layer = get_id(get_dir(directive, "layer"), into_enum=True)
return self
def set_easing(self, directive: str) -> "Object":
self.easing = get_id(get_dir(directive, "easing"), into_enum=True)
return self
def add_groups(self, *groups: int) -> "Object":
if self.groups is None:
self.groups = set(groups)
else:
self.groups |= set(groups)
return self
def remove_groups(self, *groups: int) -> "Object":
if self.groups is not None:
self.groups -= set(groups)
return self
def get_pos(self) -> Tuple[float, float]:
return (self.x, self.y)
def set_pos(self, x: float, y: float) -> "Object":
self.x = x
self.y = y
return self
def move(self, x: float = 0.0, y: float = 0.0) -> "Object":
self.x += x
self.y += y
return self
def rotate(self, degrees: float = 0.0) -> "Object":
if self.rotation is None:
self.rotation = degrees
else:
self.rotation += degrees
return self
def is_checked(self) -> bool:
return self.special_checked
def is_portal(self) -> bool:
return self.id in PORTAL_IDS
def is_speed(self) -> bool:
return self.id in SPEED_IDS
def is_speed_or_portal(self) -> bool:
return self.id in SPEED_AND_PORTAL_IDS
[docs]class ColorChannel(Model):
PARSER = IndexParser("_", map_like=True)
red: int = IntField(index=1, default=255, aliases=("r",))
green: int = IntField(index=2, default=255, aliases=("g",))
blue: int = IntField(index=3, default=255, aliases=("b",))
player_color: PlayerColor = EnumField(
index=4, enum_type=PlayerColor, from_field=IntField, default=PlayerColor.NotUsed
)
blending: bool = BoolField(index=5, default=False)
id: int = IntField(index=6, default=0)
opacity: float = FloatField(index=7, default=1.0)
index_8: bool = BoolField(index=8, default=True)
copied_id: int = IntField(index=9)
hsv: HSV = ModelField(index=10, model=HSV)
unknown_red: int = IntField(index=11, default=255, aliases=("unknown_r",))
unknown_green: int = IntField(index=12, default=255, aliases=("unknown_g",))
unknown_blue: int = IntField(index=13, default=255, aliases=("unknown_b",))
index_15: bool = BoolField(index=15, default=True)
copy_opacity: bool = BoolField(index=17)
index_18: bool = BoolField(index=18, default=False)
def __init__(self, directive: Optional[str] = None, **kwargs) -> None:
super().__init__(**kwargs)
if directive is not None:
self.set_id(directive)
def set_id(self, directive: str) -> "ColorChannel":
self.id = get_id(get_dir(directive, "color"))
return self
def get_color(self) -> Color:
return Color.from_rgb(self.r, self.g, self.b)
def set_color(self, color: IntoColor) -> "ColorChannel":
new = color_from(color)
self.r = new.r
self.g = new.g
self.b = new.b
return self
color = property(get_color, set_color)
DEFAULT_COLORS = (
ColorChannel("BG").set_color(0x287DFF),
ColorChannel("G").set_color(0x0066FF),
ColorChannel("Line").set_color(0xFFFFFF),
ColorChannel("P1").set_color(0x7DFF00),
ColorChannel("P2").set_color(0x00FFFF),
ColorChannel("G2").set_color(0x0066FF),
)
Channel = ColorChannel
[docs]class ColorCollection(set):
def __init__(
self, iterable: Optional[Iterable[ColorChannel]] = None, use_default: bool = True,
) -> None:
if use_default:
super().__init__(DEFAULT_COLORS)
else:
super().__init__()
if iterable is not None:
super().update(iterable)
@classmethod
def new(cls, *args: ColorChannel, use_default: bool = True) -> "ColorCollection":
return cls(args, use_default=use_default)
[docs] def remove(self, directive_or_id: Union[int, str]) -> None:
self.discard(self.get(directive_or_id))
[docs] def copy(self) -> "ColorCollection":
return self.__class__(channel.copy() for channel in self)
def clone(self) -> "ColorCollection":
return self.__class__(channel.clone() for channel in self)
[docs] def difference(self, other: Iterable[ColorChannel]) -> "ColorCollection":
return self.__class__(super().difference(other))
[docs] def intersection(self, other: Iterable[ColorChannel]) -> "ColorCollection":
return self.__class__(super().intersection(other))
[docs] def symmetric_difference(self, other: Iterable[ColorChannel]) -> "ColorCollection":
return self.__class__(super().symmetric_difference(other))
[docs] def union(self, other: Iterable[ColorChannel]) -> "ColorCollection":
return self.__class__(super().union(other))
[docs] def update(self, other: Iterable[ColorChannel]) -> "ColorCollection":
super().update(other)
return self
def get(self, directive_or_id: Union[int, str]) -> Optional[ColorChannel]:
if isinstance(directive_or_id, str):
id = get_id(get_dir(directive_or_id, "color"))
else:
id = directive_or_id
return iter(self).get(id=id)
def __or__(self, other: Set[ColorChannel]) -> "ColorCollection":
return self.__class__(super().__or__(other))
def __xor__(self, other: Set[ColorChannel]) -> "ColorCollection":
return self.__class__(super().__xor__(other))
def __sub__(self, other: Set[ColorChannel]) -> "ColorCollection":
return self.__class__(super().__sub__(other))
def __and__(self, other: Set[ColorChannel]) -> "ColorCollection":
return self.__class__(super().__and__(other))
def __ror__(self, other: Set[ColorChannel]) -> "ColorCollection":
return self.__class__(super().__ror__(other))
def __rxor__(self, other: Set[ColorChannel]) -> "ColorCollection":
return self.__class__(super().__rxor__(other))
def __rsub__(self, other: Set[ColorChannel]) -> "ColorCollection":
return self.__class__(super().__rsub__(other))
def __rand__(self, other: Set[ColorChannel]) -> "ColorCollection":
return self.__class__(super().__rand__(other))
[docs]class LevelAPI(Model):
ENFORCE_STR = False
REPR_IGNORE = {"unprocessed_data", "recording_string"}
id: int = BaseField(index="k1", de=int, ser=int, default=0)
name: str = BaseField(index="k2", de=str, ser=str, default="Unnamed")
description: str = BaseField(index="k3", de=decode_base64_str, ser=encode_base64_str)
unprocessed_data: str = BaseField(index="k4", de=str, ser=str)
creator: str = BaseField(index="k5", de=str, ser=str)
track_id: int = BaseField(index="k8", de=int, ser=int)
downloads: int = BaseField(index="k11", de=int, ser=int)
index_k13: bool = BaseField(index="k13", de=bool, ser=bool, default=True)
verified: bool = BaseField(index="k14", de=bool, ser=bool)
uploaded: bool = BaseField(index="k15", de=bool, ser=bool)
version: int = BaseField(index="k16", de=int, ser=int, default=1)
attempts: int = BaseField(index="k18", de=int, ser=int)
normal_mode_percentage: int = BaseField(index="k19", de=int, ser=int)
practice_mode_percentage: int = BaseField(index="k20", de=int, ser=int)
level_type: LevelType = BaseField(
index="k21", de=partial(enum_from_value, enum_type=LevelType), ser=enum_to_value
)
likes: int = BaseField(index="k22", de=int, ser=int)
length: LevelLength = BaseField(
index="k23", de=partial(enum_from_value, enum_type=LevelLength), ser=enum_to_value,
)
stars: int = BaseField(index="k26", de=int, ser=int)
recording_string: str = BaseField(index="k34", de=str, ser=str)
jumps: int = BaseField(index="k36", de=int, ser=int)
password_field: Password = BaseField(
index="k41", de=Password.from_robtop_number, ser=Password.to_robtop_number
)
original_id: int = BaseField(index="k42", de=int, ser=int)
song_id: int = BaseField(index="k45", de=int, ser=int)
revision: int = BaseField(index="k46", de=int, ser=int)
index_k47: bool = BaseField(index="k47", de=bool, ser=bool, default=True)
object_count: int = BaseField(index="k48", de=int, ser=int)
binary_version: Version = BaseField(
index="k50", de=Version.from_number, ser=Version.to_number, default=Version(3, 5),
)
first_coint_acquired: bool = BaseField(index="k61", de=bool, ser=bool)
second_coin_acquired: bool = BaseField(index="k62", de=bool, ser=bool)
third_coin_acquired: bool = BaseField(index="k63", de=bool, ser=bool)
requested_stars: int = BaseField(index="k66", de=int, ser=int)
extra_string: str = BaseField(index="k67", de=str, ser=str)
timely_id: int = BaseField(index="k74", de=int, ser=int)
unlisted: bool = BaseField(index="k79", de=bool, ser=bool)
editor_seconds: int = BaseField(index="k80", de=int, ser=int)
copies_seconds: int = BaseField(index="k81", de=int, ser=int)
folder: int = BaseField(index="k84", de=int, ser=int)
x: float = BaseField(index="kI1", de=float, ser=float)
y: float = BaseField(index="kI2", de=float, ser=float)
zoom: float = BaseField(index="kI3", de=float, ser=float)
build_tab_page: int = BaseField(index="kI4", de=int, ser=int)
build_tab: int = BaseField(index="kI5", de=int, ser=int)
build_tab_pages_dict: Dict[int, int] = BaseField(
index="kI6",
de=partial(map_key_value, key_func=int, value_func=int),
ser=partial(map_key_value, key_func=str, value_func=str),
)
editor_layer: int = BaseField(index="kI7", de=int, ser=int)
internal_type: InternalType = BaseField(
index="kCEK",
de=partial(enum_from_value, enum_type=InternalType),
ser=enum_to_value,
default=InternalType.LEVEL,
)
def get_password(self) -> Optional[int]:
if self.password_field is None:
return None
return self.password_field.password
def set_password(self, password: Optional[int]) -> None:
if self.password_field is None:
self.password_field = Password(password)
else:
self.password_field.password = password
password = property(get_password, set_password)
def get_copyable(self) -> bool:
if self.password_field is None:
return False
return self.password_field.copyable
def set_copyable(self, copyable: bool) -> None:
if self.password_field is None:
self.password_field = Password(None, copyable)
else:
self.password_field.copyable = copyable
copyable = property(get_copyable, set_copyable)
@cache_by("unprocessed_data")
def get_data(self) -> str:
unprocessed_data = self.unprocessed_data
if unprocessed_data is None:
return ""
if is_level_probably_decoded(unprocessed_data):
return unprocessed_data
else:
return unzip_level_str(unprocessed_data)
def set_data(self, data: str) -> None:
if is_level_probably_decoded(data):
self.unprocessed_data = zip_level_str(data)
else:
self.unprocessed_data = data
data = property(get_data, set_data)
@cache_by("recording_string")
def get_recording(self) -> Recording:
if self.recording_string is None:
return Recording()
return Recording.from_string(unzip_level_str(self.recording_string))
def set_recording(self, recording: Iterable[RecordingEntry]) -> None:
self.recording_string = zip_level_str(Recording.collect_string(recording))
recording = property(get_recording, set_recording)
@cache_by("recording_string")
def iter_recording(self) -> Iterator[RecordingEntry]:
if self.recording_string is None:
return std_iter(())
return Recording.iter_string(unzip_level_str(self.recording_string))
def open_editor(self) -> "Editor":
from gd.api.editor import Editor
return Editor.load_from(self, "data")
def to_dict(self) -> Dict[str, T]:
result = super().to_dict()
result.update(password=self.password, copyable=self.copyable)
return result