forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
403 lines
16 KiB
Python
403 lines
16 KiB
Python
"""Encryption and Decryption of settings strings."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any, Dict, Tuple
|
|
|
|
from randomizer.Enums.Settings import (
|
|
LogicType,
|
|
SettingsStringDataType,
|
|
SettingsStringEnum,
|
|
SettingsStringIntRangeMap,
|
|
SettingsStringListTypeMap,
|
|
SettingsStringTypeMap,
|
|
SpoilerHints,
|
|
)
|
|
|
|
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
index_to_letter = {i: letters[i] for i in range(64)}
|
|
letter_to_index = {letters[i]: i for i in range(len(letters))}
|
|
|
|
|
|
def int_to_bin_string(num, bytesize):
|
|
"""Convert an integer to a binary representation.
|
|
|
|
This function is needed to handle negative numbers.
|
|
"""
|
|
return format(num if num >= 0 else (1 << bytesize) + num, f"0{bytesize}b").zfill(bytesize)
|
|
|
|
|
|
def bin_string_to_int(bin_str: str, bytesize: int) -> int:
|
|
"""Convert a binary string to an integer.
|
|
|
|
This function is needed to handle negative numbers.
|
|
"""
|
|
if bin_str[0] == "1":
|
|
return int(bin_str, 2) - (1 << bytesize)
|
|
else:
|
|
return int(bin_str, 2)
|
|
|
|
|
|
def get_var_int_encode_details(settingEnum: SettingsStringEnum) -> Tuple[int, bool]:
|
|
"""Return key information needed to encode/decode a given var_int setting.
|
|
|
|
Returns:
|
|
int - The bit length of the int.
|
|
bool - True if negative numbers are possible.
|
|
"""
|
|
range = SettingsStringIntRangeMap[settingEnum]
|
|
max_val = range["max"]
|
|
min_val = range["min"]
|
|
limiting_val = max_val
|
|
negatives_possible = min_val < 0
|
|
if negatives_possible and abs(min_val) > max_val:
|
|
# We subtract one, to handle the edge case where the absolute value is
|
|
# a negative power of 2.
|
|
limiting_val = abs(min_val) - 1
|
|
# Get the bit length of the limiting value.
|
|
bit_len = limiting_val.bit_length()
|
|
# If negatives are possible, add one to the bit length.
|
|
if negatives_possible:
|
|
bit_len += 1
|
|
return bit_len, negatives_possible
|
|
|
|
|
|
def encode_var_int(settingEnum, num):
|
|
"""Convert a variable-size integer to a binary representation."""
|
|
bit_len, _ = get_var_int_encode_details(settingEnum)
|
|
return int_to_bin_string(num, bit_len)
|
|
|
|
|
|
def decode_var_int(settingEnum: SettingsStringEnum, bin_str: str) -> int:
|
|
"""Convert a binary string to a variable-size integer."""
|
|
bit_len, negatives_possible = get_var_int_encode_details(settingEnum)
|
|
if negatives_possible:
|
|
return bin_string_to_int(bin_str, bit_len)
|
|
else:
|
|
return int(bin_str, 2)
|
|
|
|
|
|
# A map tying certain key settings to other settings that should be excluded
|
|
# from the string, if the key setting has a certain value.
|
|
settingsExclusionMap = {
|
|
"helm_hurry": {
|
|
False: [
|
|
"helmhurry_list_banana_medal",
|
|
"helmhurry_list_battle_crown",
|
|
"helmhurry_list_bean",
|
|
"helmhurry_list_blueprint",
|
|
"helmhurry_list_boss_key",
|
|
"helmhurry_list_colored_bananas",
|
|
"helmhurry_list_company_coins",
|
|
"helmhurry_list_fairies",
|
|
"helmhurry_list_golden_banana",
|
|
"helmhurry_list_ice_traps",
|
|
"helmhurry_list_kongs",
|
|
"helmhurry_list_move",
|
|
"helmhurry_list_pearl",
|
|
"helmhurry_list_rainbow_coin",
|
|
"helmhurry_list_starting_time",
|
|
]
|
|
},
|
|
"shuffle_items": {False: ["item_rando_list_selected"]},
|
|
"enemy_rando": {False: ["enemies_selected"]},
|
|
"bonus_barrel_rando": {False: ["minigames_list_selected", "disable_hard_minigames"]},
|
|
"cb_rando_enabled": {False: ["cb_rando_list_selected"]},
|
|
"logic_type": {LogicType.glitchless: ["glitches_selected"], LogicType.nologic: ["glitches_selected"]},
|
|
"quality_of_life": {False: ["misc_changes_selected"]},
|
|
"hard_mode": {False: ["hard_mode_selected"]},
|
|
"spoiler_hints": {
|
|
SpoilerHints.off: [
|
|
"points_list_kongs"
|
|
"points_list_keys"
|
|
"points_list_guns"
|
|
"points_list_instruments"
|
|
"points_list_training_moves"
|
|
"points_list_fairy_moves"
|
|
"points_list_important_shared"
|
|
"points_list_pad_moves"
|
|
"points_list_barrel_moves"
|
|
"points_list_active_moves"
|
|
"points_list_bean"
|
|
"points_list_shopkeepers"
|
|
],
|
|
SpoilerHints.vial_colors: [
|
|
"points_list_kongs"
|
|
"points_list_keys"
|
|
"points_list_guns"
|
|
"points_list_instruments"
|
|
"points_list_training_moves"
|
|
"points_list_fairy_moves"
|
|
"points_list_important_shared"
|
|
"points_list_pad_moves"
|
|
"points_list_barrel_moves"
|
|
"points_list_active_moves"
|
|
"points_list_bean"
|
|
"points_list_shopkeepers"
|
|
],
|
|
},
|
|
}
|
|
|
|
|
|
def prune_settings(settings_dict: dict):
|
|
"""Remove certain settings based on the values of other settings."""
|
|
settings_to_remove = ["plandomizer_data", "enable_song_select", "music_selections"]
|
|
# Remove settings based on the exclusion map above.
|
|
for keySetting, exclusions in settingsExclusionMap.items():
|
|
if keySetting in settings_dict.keys() and settings_dict[keySetting] in exclusions:
|
|
settings_to_remove.extend(exclusions[settings_dict[keySetting]])
|
|
# Remove any deprecated settings.
|
|
for pop in settings_to_remove:
|
|
if pop in settings_dict:
|
|
settings_dict.pop(pop)
|
|
return settings_dict
|
|
|
|
|
|
def encrypt_settings_string_enum(dict_data: dict):
|
|
"""Take a dictionary and return an enum-based encrypted string.
|
|
|
|
Args:
|
|
dict_data (dict): Posted JSON data from the form.
|
|
|
|
Returns:
|
|
str: Returns an encrypted string.
|
|
"""
|
|
for pop in [
|
|
"download_patch_file",
|
|
"load_patch_file",
|
|
"seed",
|
|
"settings_string",
|
|
"chunky_main_colors",
|
|
"chunky_main_custom_color",
|
|
"chunky_other_colors",
|
|
"chunky_other_custom_color",
|
|
"diddy_clothes_colors",
|
|
"diddy_clothes_custom_color",
|
|
"dk_fur_colors",
|
|
"dk_fur_custom_color",
|
|
"dk_tie_colors",
|
|
"dk_tie_custom_color",
|
|
"enguarde_skin_colors",
|
|
"enguarde_skin_custom_color",
|
|
"klaptrap_model",
|
|
"random_models",
|
|
"random_enemy_colors",
|
|
"misc_cosmetics",
|
|
"disco_chunky",
|
|
"dark_mode_textboxes",
|
|
"pause_hint_coloring",
|
|
"lanky_clothes_colors",
|
|
"lanky_fur_colors",
|
|
"lanky_clothes_custom_color",
|
|
"lanky_fur_custom_color",
|
|
"rambi_skin_colors",
|
|
"rambi_skin_custom_color",
|
|
"gb_colors",
|
|
"gb_custom_color",
|
|
"random_colors",
|
|
"random_music",
|
|
"music_bgm_randomized",
|
|
"music_events_randomized",
|
|
"music_majoritems_randomized",
|
|
"music_minoritems_randomized",
|
|
"music_vanilla_locations",
|
|
"tiny_hair_colors",
|
|
"tiny_clothes_colors",
|
|
"tiny_hair_custom_color",
|
|
"tiny_clothes_custom_color",
|
|
"override_cosmetics",
|
|
"remove_water_oscillation",
|
|
"head_balloons",
|
|
"colorblind_mode",
|
|
"big_head_mode",
|
|
"search",
|
|
"holiday_setting",
|
|
"holiday_setting_offseason",
|
|
"homebrew_header",
|
|
"dpad_display",
|
|
"camera_is_follow",
|
|
"sfx_volume",
|
|
"music_volume",
|
|
"true_widescreen",
|
|
"camera_is_not_inverted",
|
|
"sound_type",
|
|
"smoother_camera",
|
|
"songs_excluded",
|
|
"excluded_songs_selected",
|
|
"music_filtering",
|
|
"music_filtering_selected",
|
|
"troff_brighten",
|
|
"better_dirt_patch_cosmetic",
|
|
"crosshair_outline",
|
|
"custom_music_proportion",
|
|
"fill_with_custom_music",
|
|
"show_song_name",
|
|
"delayed_spoilerlog_release",
|
|
"shockwave_status", # Deprecated with starting move selector rework - this is now derived in the settings constructor
|
|
"music_disable_reverb",
|
|
"archipelago",
|
|
]:
|
|
if pop in dict_data:
|
|
dict_data.pop(pop)
|
|
dict_data = prune_settings(dict_data)
|
|
bitstring = ""
|
|
for sort_this in ["starting_moves_list_1", "starting_moves_list_2", "starting_moves_list_3", "starting_moves_list_4", "starting_moves_list_5"]:
|
|
if sort_this in dict_data.keys():
|
|
dict_data[sort_this].sort()
|
|
for key in dict_data:
|
|
value = dict_data[key]
|
|
# At this time, all strings represent ints, so just convert.
|
|
if isinstance(value, str):
|
|
value = int(value)
|
|
key_enum = SettingsStringEnum[key]
|
|
key_data_type = SettingsStringTypeMap[key_enum]
|
|
# Encode the key.
|
|
key_size = max([member.value for member in SettingsStringEnum]).bit_length()
|
|
bitstring += bin(key_enum)[2:].zfill(key_size)
|
|
if key_data_type == SettingsStringDataType.bool:
|
|
bitstring += "1" if value else "0"
|
|
elif key_data_type == SettingsStringDataType.int4:
|
|
bitstring += int_to_bin_string(value, 4)
|
|
elif key_data_type == SettingsStringDataType.int8:
|
|
bitstring += int_to_bin_string(value, 8)
|
|
elif key_data_type in (SettingsStringDataType.int16, SettingsStringDataType.u16):
|
|
bitstring += int_to_bin_string(value, 16)
|
|
elif key_data_type == SettingsStringDataType.var_int:
|
|
bitstring += encode_var_int(key_enum, value)
|
|
elif key_data_type == SettingsStringDataType.list:
|
|
bitstring += f"{len(value):08b}"
|
|
key_list_data_type = SettingsStringListTypeMap[key_enum]
|
|
for item in value:
|
|
if isinstance(item, str):
|
|
item = int(item)
|
|
if key_list_data_type == SettingsStringDataType.bool:
|
|
bitstring += "1" if item else "0"
|
|
elif key_list_data_type == SettingsStringDataType.int4:
|
|
bitstring += int_to_bin_string(item, 4)
|
|
elif key_list_data_type == SettingsStringDataType.int8:
|
|
bitstring += int_to_bin_string(item, 8)
|
|
elif key_list_data_type in (SettingsStringDataType.int16, SettingsStringDataType.u16):
|
|
bitstring += int_to_bin_string(item, 16)
|
|
elif key_list_data_type == SettingsStringDataType.var_int:
|
|
bitstring += encode_var_int(key_enum, item)
|
|
else:
|
|
# The value is an enum.
|
|
max_value = max([member.value for member in key_list_data_type])
|
|
bitstring += format(item, f"0{max_value.bit_length()}b")
|
|
else:
|
|
# The value is an enum.
|
|
max_value = max([member.value for member in key_data_type])
|
|
# If value is an int
|
|
if isinstance(value, int):
|
|
bitstring += format(value, f"0{max_value.bit_length()}b")
|
|
else:
|
|
bitstring += format(value.value, f"0{max_value.bit_length()}b")
|
|
|
|
# Pad the bitstring with zeroes until the length is divisible by 6.
|
|
remainder = len(bitstring) % 6
|
|
if remainder > 0:
|
|
for _ in range(0, 6 - remainder):
|
|
bitstring += "0"
|
|
|
|
# Split the bitstring into 6-bit chunks and look up the corresponding
|
|
# letters.
|
|
letter_string = ""
|
|
for i in range(0, len(bitstring), 6):
|
|
chunk = int(bitstring[i : i + 6], 2)
|
|
letter_string += letters[chunk]
|
|
return letter_string
|
|
|
|
|
|
def decrypt_settings_string_enum(encrypted_string: str) -> Dict[str, Any]:
|
|
"""Take an enum-based encrypted string and return a dictionary.
|
|
|
|
Args:
|
|
encrypted_string (str): Passed settings string.
|
|
|
|
Returns:
|
|
dict: Returns the decrypted set of data.
|
|
"""
|
|
# Take each letter of the encrypted_string and convert it to a 6-bit binary
|
|
# number, then use the embedded keys to get the value from the settings
|
|
# string.
|
|
bitstring = ""
|
|
for letter in encrypted_string:
|
|
index = letter_to_index[letter]
|
|
bitstring += f"{index:06b}"
|
|
bitstring_length = len(bitstring)
|
|
settings_dict = {}
|
|
bit_index = 0
|
|
key_size = max([member.value for member in SettingsStringEnum]).bit_length()
|
|
# If there are fewer than (key_size + 1) characters left in our bitstring,
|
|
# we have hit the padding. (key_size + 1 characters is the minimum needed
|
|
# for a key and a value.)
|
|
while bit_index < (bitstring_length - (key_size + 1)):
|
|
# Consume the next key.
|
|
key = int(bitstring[bit_index : bit_index + key_size], 2)
|
|
bit_index += key_size
|
|
key_enum = SettingsStringEnum(key)
|
|
key_name = key_enum.name
|
|
key_data_type = SettingsStringTypeMap[key_enum]
|
|
val = None
|
|
if key_data_type == SettingsStringDataType.bool:
|
|
val = True if bitstring[bit_index] == "1" else False
|
|
bit_index += 1
|
|
elif key_data_type == SettingsStringDataType.int4:
|
|
val = bin_string_to_int(bitstring[bit_index : bit_index + 4], 4)
|
|
bit_index += 4
|
|
elif key_data_type == SettingsStringDataType.int8:
|
|
val = bin_string_to_int(bitstring[bit_index : bit_index + 8], 8)
|
|
bit_index += 8
|
|
elif key_data_type in (SettingsStringDataType.int16, SettingsStringDataType.u16):
|
|
val = bin_string_to_int(bitstring[bit_index : bit_index + 16], 16)
|
|
if key_data_type == SettingsStringDataType.u16 and val < 0:
|
|
val += 65536
|
|
bit_index += 16
|
|
elif key_data_type == SettingsStringDataType.var_int:
|
|
bit_len, _ = get_var_int_encode_details(key_enum)
|
|
val = decode_var_int(key_enum, bitstring[bit_index : bit_index + bit_len])
|
|
bit_index += bit_len
|
|
elif key_data_type == SettingsStringDataType.list:
|
|
list_length = int(bitstring[bit_index : bit_index + 8], 2)
|
|
bit_index += 8
|
|
val = []
|
|
key_list_data_type = SettingsStringListTypeMap[key_enum]
|
|
for _ in range(list_length):
|
|
list_val = None
|
|
if key_list_data_type == SettingsStringDataType.bool:
|
|
list_val = True if bitstring[bit_index] == "1" else False
|
|
bit_index += 1
|
|
elif key_list_data_type == SettingsStringDataType.int4:
|
|
list_val = bin_string_to_int(bitstring[bit_index : bit_index + 4], 4)
|
|
bit_index += 4
|
|
elif key_list_data_type == SettingsStringDataType.int8:
|
|
list_val = bin_string_to_int(bitstring[bit_index : bit_index + 8], 8)
|
|
bit_index += 8
|
|
elif key_list_data_type in (SettingsStringDataType.int16, SettingsStringDataType.u16):
|
|
list_val = bin_string_to_int(bitstring[bit_index : bit_index + 16], 16)
|
|
if key_data_type == SettingsStringDataType.u16 and val < 0:
|
|
val += 65536
|
|
bit_index += 16
|
|
elif key_data_type == SettingsStringDataType.var_int:
|
|
bit_len, _ = get_var_int_encode_details(key_enum)
|
|
list_val = decode_var_int(key_enum, bitstring[bit_index : bit_index + bit_len])
|
|
bit_index += bit_len
|
|
else:
|
|
# The value is an enum.
|
|
max_value = max([member.value for member in key_list_data_type])
|
|
int_val = int(bitstring[bit_index : bit_index + max_value.bit_length()], 2)
|
|
list_val = key_list_data_type(int_val)
|
|
bit_index += max_value.bit_length()
|
|
val.append(list_val)
|
|
else:
|
|
# The value is an enum.
|
|
max_value = max([member.value for member in key_data_type])
|
|
int_val = int(bitstring[bit_index : bit_index + max_value.bit_length()], 2)
|
|
val = key_data_type(int_val)
|
|
bit_index += max_value.bit_length()
|
|
# If this setting is not deprecated, add it.
|
|
# The plando setting needs to be encoded in settings strings but not applied when decoding for logging purposes.
|
|
if key_enum != SettingsStringEnum.enable_plandomizer:
|
|
settings_dict[key_name] = val
|
|
return settings_dict
|