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
781 lines
29 KiB
Python
781 lines
29 KiB
Python
"""Library functions for patching."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
from enum import IntEnum, auto
|
|
from typing import TYPE_CHECKING, Any, List, Union
|
|
from functools import lru_cache
|
|
|
|
import js
|
|
import math
|
|
from randomizer.Enums.ScriptTypes import ScriptTypes
|
|
from randomizer.Patching.Patcher import ROM, LocalROM
|
|
from randomizer.Enums.Items import Items
|
|
from randomizer.Enums.Maps import Maps
|
|
from randomizer.Enums.Types import BarrierItems, Types
|
|
from randomizer.Enums.Settings import HardModeSelected, MiscChangesSelected, HelmDoorItem, IceTrapFrequency, ProgressiveHintItem
|
|
from randomizer.Patching.Library.Assets import getPointerLocation, TableNames
|
|
from randomizer.Patching.Library.DataTypes import short_to_ushort
|
|
|
|
if TYPE_CHECKING:
|
|
from randomizer.Lists.MapsAndExits import Maps
|
|
|
|
|
|
class MenuTextDim(IntEnum):
|
|
"""Definition of base size of image."""
|
|
|
|
size_w32_h32 = auto()
|
|
size_w32_h64 = auto()
|
|
size_w64_h32 = auto()
|
|
|
|
|
|
class MenuTexture:
|
|
"""Class to store information regarding a texture compatible with the main menu background."""
|
|
|
|
def __init__(self, name: str, dim: MenuTextDim, table: int = 25, weight: int = 100, is_color: bool = False):
|
|
"""Initialize with given parameters."""
|
|
self.name = name
|
|
self.dim = dim
|
|
self.table = table
|
|
self.weight = weight
|
|
self.is_color = is_color
|
|
|
|
|
|
class CustomActors(IntEnum):
|
|
"""Custom Actors Enum."""
|
|
|
|
NintendoCoin = 0x8000 # Starts at 0x8000
|
|
RarewareCoin = auto()
|
|
Null = auto()
|
|
PotionDK = auto()
|
|
PotionDiddy = auto()
|
|
PotionLanky = auto()
|
|
PotionTiny = auto()
|
|
PotionChunky = auto()
|
|
PotionAny = auto()
|
|
KongDK = auto()
|
|
KongDiddy = auto()
|
|
KongLanky = auto()
|
|
KongTiny = auto()
|
|
KongChunky = auto()
|
|
KongDisco = auto()
|
|
KongKrusha = auto()
|
|
Bean = auto()
|
|
Pearl = auto()
|
|
Fairy = auto()
|
|
IceTrapBubble = auto()
|
|
IceTrapReverse = auto()
|
|
IceTrapSlow = auto()
|
|
Medal = auto()
|
|
JetpacItemOverlay = auto()
|
|
CrankyItem = auto()
|
|
FunkyItem = auto()
|
|
CandyItem = auto()
|
|
SnideItem = auto()
|
|
ZingerFlamethrower = auto()
|
|
Scarab = auto()
|
|
HintItemDK = auto()
|
|
KopDummy = auto()
|
|
HintItemDiddy = auto()
|
|
HintItemLanky = auto()
|
|
HintItemTiny = auto()
|
|
HintItemChunky = auto()
|
|
ArchipelagoItem = auto()
|
|
|
|
|
|
compatible_background_textures = {
|
|
0x47A: MenuTexture("Gold Tower Stack", MenuTextDim.size_w32_h64),
|
|
0x9DD: MenuTexture("Book", MenuTextDim.size_w32_h64),
|
|
0x5C8: MenuTexture("Bricks", MenuTextDim.size_w32_h64),
|
|
0x76F: MenuTexture("Bricks", MenuTextDim.size_w32_h64),
|
|
0xAAF: MenuTexture("Floodlights", MenuTextDim.size_w32_h64),
|
|
0x33D: MenuTexture("Wooden Board", MenuTextDim.size_w32_h64),
|
|
0x79C: MenuTexture("Grassy Brick", MenuTextDim.size_w32_h64),
|
|
0x992: MenuTexture("Wooden Door", MenuTextDim.size_w32_h64),
|
|
0x39B: MenuTexture("C Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x39C: MenuTexture("G Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x39D: MenuTexture("9 Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x39F: MenuTexture("R Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x3A0: MenuTexture("S Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x3A1: MenuTexture("1 Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x3A2: MenuTexture("F Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x3A3: MenuTexture("8 Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x3A4: MenuTexture("7 Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x3A5: MenuTexture("B Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x3A6: MenuTexture("4 Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x3A7: MenuTexture("N Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x3A8: MenuTexture("D Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x3A9: MenuTexture("Q Block", MenuTextDim.size_w32_h32, 25, 7),
|
|
0x7B2: MenuTexture("Up Arrow", MenuTextDim.size_w32_h32, 25, 50),
|
|
0x7B3: MenuTexture("Down Arrow", MenuTextDim.size_w32_h32, 25, 50),
|
|
0xAC: MenuTexture("TNT", MenuTextDim.size_w32_h32),
|
|
0x7CD: MenuTexture("Night Sign", MenuTextDim.size_w32_h32),
|
|
0x3DE: MenuTexture("Color", MenuTextDim.size_w32_h32, 7, 50, True),
|
|
0xF7: MenuTexture("Grass", MenuTextDim.size_w32_h32),
|
|
0xA00: MenuTexture("Sand", MenuTextDim.size_w32_h32),
|
|
0xA84: MenuTexture("Sand", MenuTextDim.size_w32_h32),
|
|
0xB4D: MenuTexture("Leaf", MenuTextDim.size_w32_h32),
|
|
0xB19: MenuTexture("Boxes", MenuTextDim.size_w32_h32),
|
|
0xB24: MenuTexture("Pineapple Switch", MenuTextDim.size_w32_h32),
|
|
0xB25: MenuTexture("Coconut Switch", MenuTextDim.size_w32_h32),
|
|
0xB1E: MenuTexture("Peanut Switch", MenuTextDim.size_w32_h32),
|
|
0xC80: MenuTexture("Feather Switch", MenuTextDim.size_w32_h32),
|
|
0xC81: MenuTexture("Grape Switch", MenuTextDim.size_w32_h32),
|
|
# 0xB27: MenuTexture("Boxes", MenuTextDim.size_w32_h32),
|
|
0xCF1: MenuTexture("L Square", MenuTextDim.size_w32_h32),
|
|
0xCF4: MenuTexture("R Square", MenuTextDim.size_w32_h32),
|
|
0xE63: MenuTexture("Metallic Green", MenuTextDim.size_w32_h32),
|
|
0x9F3: MenuTexture("Watery Blue", MenuTextDim.size_w32_h32),
|
|
0x9F4: MenuTexture("Watery Yellow", MenuTextDim.size_w32_h32),
|
|
0x83: MenuTexture("Beige Strips", MenuTextDim.size_w32_h32),
|
|
0x788: MenuTexture("Beige Panels", MenuTextDim.size_w32_h32),
|
|
0x789: MenuTexture("Blue Panels", MenuTextDim.size_w32_h32),
|
|
0x792: MenuTexture("White Granite", MenuTextDim.size_w32_h32),
|
|
0x1258: MenuTexture("Horizontal Metal Green", MenuTextDim.size_w32_h32),
|
|
0x1260: MenuTexture("Red Light", MenuTextDim.size_w32_h32),
|
|
0x1343: MenuTexture("Fluid Red", MenuTextDim.size_w32_h32),
|
|
0x1344: MenuTexture("Fluid Green", MenuTextDim.size_w32_h32),
|
|
0x1347: MenuTexture("Fluid Orange", MenuTextDim.size_w32_h32),
|
|
0x1348: MenuTexture("Fluid Blue", MenuTextDim.size_w32_h32),
|
|
0xCC: MenuTexture("Bathroom Wall", MenuTextDim.size_w32_h64),
|
|
0xCD5: MenuTexture("Orange Barrel", MenuTextDim.size_w32_h64),
|
|
0xCD6: MenuTexture("Green Barrel", MenuTextDim.size_w32_h64),
|
|
0xCD7: MenuTexture("Purple Barrel", MenuTextDim.size_w32_h64),
|
|
0xCD8: MenuTexture("Yellow Barrel", MenuTextDim.size_w32_h64),
|
|
0xCD9: MenuTexture("Blue Barrel", MenuTextDim.size_w32_h64),
|
|
0xCDA: MenuTexture("Red Barrel", MenuTextDim.size_w32_h64),
|
|
0xE3A: MenuTexture("Light Fixing", MenuTextDim.size_w32_h64),
|
|
0x8F5: MenuTexture("Metal Pillars", MenuTextDim.size_w32_h64),
|
|
0x786: MenuTexture("Just Straight Dirt", MenuTextDim.size_w32_h64),
|
|
0x1257: MenuTexture("Copper", MenuTextDim.size_w32_h64),
|
|
# 0xC: MenuTexture("Shelf of Bananas", MenuTextDim.size_w64_h32),
|
|
# 0xD: MenuTexture("Shelf of Books", MenuTextDim.size_w64_h32),
|
|
# 0xE: MenuTexture("Shelf of Wine", MenuTextDim.size_w64_h32),
|
|
0xA8F: MenuTexture("Grime Panels", MenuTextDim.size_w64_h32),
|
|
0xA43: MenuTexture("Books", MenuTextDim.size_w64_h32),
|
|
0xA53: MenuTexture("Dolphins", MenuTextDim.size_w64_h32),
|
|
0xA70: MenuTexture("Way Out Sign", MenuTextDim.size_w64_h32),
|
|
0xA72: MenuTexture("Banana Hoard Sign", MenuTextDim.size_w64_h32),
|
|
0xA73: MenuTexture("Training Area Sign", MenuTextDim.size_w64_h32),
|
|
0xA74: MenuTexture("Cranky's Lab Sign", MenuTextDim.size_w64_h32),
|
|
# 0xA76: MenuTexture("DK's Sign", MenuTextDim.size_w64_h32),
|
|
0xC14: MenuTexture("No Admittance Sign", MenuTextDim.size_w64_h32),
|
|
0xC47: MenuTexture("Danger Sign", MenuTextDim.size_w64_h32),
|
|
0xC64: MenuTexture("Accept Sign", MenuTextDim.size_w64_h32),
|
|
0xCCF: MenuTexture("A Sign", MenuTextDim.size_w64_h32, 25, 7),
|
|
0xCD0: MenuTexture("B Sign", MenuTextDim.size_w64_h32, 25, 7),
|
|
0xCD1: MenuTexture("C Sign", MenuTextDim.size_w64_h32, 25, 7),
|
|
0xCD2: MenuTexture("D Sign", MenuTextDim.size_w64_h32, 25, 7),
|
|
0xCD3: MenuTexture("E Sign", MenuTextDim.size_w64_h32, 25, 7),
|
|
0xCD4: MenuTexture("F Sign", MenuTextDim.size_w64_h32, 25, 7),
|
|
0xD3C: MenuTexture("Blessed", MenuTextDim.size_w64_h32), # Beans
|
|
0x8DA: MenuTexture("Museum Sign", MenuTextDim.size_w64_h32),
|
|
0x8DB: MenuTexture("Ballroom Sign", MenuTextDim.size_w64_h32),
|
|
0x8DE: MenuTexture("Library Sign", MenuTextDim.size_w64_h32),
|
|
0x9DA: MenuTexture("Library Wall", MenuTextDim.size_w64_h32),
|
|
0x79A: MenuTexture("Minecart Tracks", MenuTextDim.size_w64_h32),
|
|
0x339: MenuTexture("Piano Keys", MenuTextDim.size_w64_h32),
|
|
0x398: MenuTexture("4 and B Blocks", MenuTextDim.size_w64_h32, 25, 7),
|
|
0x399: MenuTexture("C and Z Blocks", MenuTextDim.size_w64_h32, 25, 7),
|
|
0x902: MenuTexture("Carpet", MenuTextDim.size_w64_h32),
|
|
}
|
|
|
|
|
|
class HelmDoorRandomInfo:
|
|
"""Store information regarding helm door random boundaries."""
|
|
|
|
def __init__(self, min_bound: int, max_bound: int, selection_weight: float):
|
|
"""Initialize with given parameters."""
|
|
self.min_bound = min_bound
|
|
self.max_bound = max_bound
|
|
self.selection_weight = selection_weight
|
|
self.selected_amount = None
|
|
|
|
def chooseAmount(self, rando) -> int:
|
|
"""Choose amount for the helm door."""
|
|
raw_float = rando.triangular(self.min_bound, self.max_bound)
|
|
self.selected_amount = round(raw_float)
|
|
return self.selected_amount
|
|
|
|
|
|
class HelmDoorInfo:
|
|
"""Store information about helm door requirements."""
|
|
|
|
def __init__(
|
|
self,
|
|
absolute_max: int,
|
|
hard: HelmDoorRandomInfo = None,
|
|
medium: HelmDoorRandomInfo = None,
|
|
easy: HelmDoorRandomInfo = None,
|
|
):
|
|
"""Initialize with given parameters."""
|
|
self.absolute_max = absolute_max
|
|
self.hard = hard
|
|
self.medium = medium
|
|
self.easy = easy
|
|
|
|
def getDifficultyInfo(self, difficulty: int) -> HelmDoorRandomInfo:
|
|
"""Get the random info pertaining to the difficulty."""
|
|
if difficulty == 0:
|
|
return self.easy
|
|
if difficulty == 1:
|
|
return self.medium
|
|
if difficulty == 2:
|
|
return self.hard
|
|
return None
|
|
|
|
|
|
class PaletteFillType(IntEnum):
|
|
"""Palette Fill Type enum."""
|
|
|
|
block = auto()
|
|
patch = auto()
|
|
sparkle = auto()
|
|
checkered = auto()
|
|
radial = auto()
|
|
kong = auto()
|
|
|
|
|
|
class Overlay(IntEnum):
|
|
"""Overlay enum."""
|
|
|
|
Boot = 0
|
|
Static = 1
|
|
Menu = 2
|
|
Multiplayer = 3
|
|
Minecart = 4
|
|
Race = 5
|
|
Critter = 6
|
|
Boss = 7
|
|
Bonus = 8
|
|
Arcade = 9
|
|
Jetpac = 10
|
|
Custom = 11 # Fake overlay used for patching
|
|
|
|
|
|
def getNextFreeID(ROM_COPY: LocalROM, cont_map_id: Union[Maps, int], ignore: List[Union[Any, int]] = []) -> int:
|
|
"""Get next available Model 2 ID."""
|
|
setup_table = getPointerLocation(TableNames.Setups, cont_map_id)
|
|
ROM_COPY.seek(setup_table)
|
|
model2_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
|
|
vacant_ids = list(range(0, 600))
|
|
for item in range(model2_count):
|
|
item_start = setup_table + 4 + (item * 0x30)
|
|
ROM_COPY.seek(item_start + 0x2A)
|
|
item_id = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
if item_id in vacant_ids:
|
|
vacant_ids.remove(item_id)
|
|
for id in range(0x220, 0x225):
|
|
if id in vacant_ids:
|
|
vacant_ids.remove(id)
|
|
for id in ignore:
|
|
if id in vacant_ids:
|
|
vacant_ids.remove(id)
|
|
if len(vacant_ids) > 0:
|
|
return min(vacant_ids)
|
|
return 0 # Shouldn't ever hit this. This is a case if there's no vacant IDs in range [0,599]
|
|
|
|
|
|
def addNewScript(ROM_COPY: LocalROM, cont_map_id: Union[Maps, int], item_ids: List[int], type: ScriptTypes) -> None:
|
|
"""Append a new script to the script database. Has to be just 1 execution and 1 endblock."""
|
|
script_table = getPointerLocation(TableNames.InstanceScripts, cont_map_id)
|
|
ROM_COPY.seek(script_table)
|
|
script_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
good_scripts = []
|
|
# Construct good pre-existing scripts
|
|
file_offset = 2
|
|
for script_item in range(script_count):
|
|
ROM_COPY.seek(script_table + file_offset)
|
|
script_start = script_table + file_offset
|
|
script_id = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
block_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
file_offset += 6
|
|
for block_item in range(block_count):
|
|
ROM_COPY.seek(script_table + file_offset)
|
|
cond_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
file_offset += 2 + (8 * cond_count)
|
|
ROM_COPY.seek(script_table + file_offset)
|
|
exec_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
file_offset += 2 + (8 * exec_count)
|
|
script_end = script_table + file_offset
|
|
if script_id not in item_ids:
|
|
script_data = []
|
|
ROM_COPY.seek(script_start)
|
|
for x in range(int((script_end - script_start) / 2)):
|
|
script_data.append(int.from_bytes(ROM_COPY.readBytes(2), "big"))
|
|
good_scripts.append(script_data)
|
|
# Get new script data
|
|
subscript_type = -100
|
|
if type == ScriptTypes.Bananaport:
|
|
subscript_type = -1
|
|
elif type == ScriptTypes.Wrinkly:
|
|
subscript_type = -2
|
|
elif type == ScriptTypes.TnsPortal:
|
|
subscript_type = -3
|
|
elif type == ScriptTypes.TnsIndicator:
|
|
subscript_type = -4
|
|
elif type == ScriptTypes.CrownMain:
|
|
subscript_type = -5
|
|
elif type == ScriptTypes.CrownIsles2:
|
|
subscript_type = -6
|
|
elif type == ScriptTypes.MelonCrate:
|
|
subscript_type = -13
|
|
elif type == ScriptTypes.DeleteItem:
|
|
subscript_type = -16
|
|
for item_id in item_ids:
|
|
script_arr = [
|
|
item_id,
|
|
1, # Block Count
|
|
0, # Behav 9C, Not sure the purpose on this. 0 seems safe from prior knowledge
|
|
0, # Cond Count
|
|
1, # Exec Count
|
|
7, # Func Type (Run JALR)
|
|
125, # JALR Type (Points to our custom code)
|
|
short_to_ushort(subscript_type), # Subscript Type
|
|
item_id, # Item ID, for the purpose of our script to locate any required data
|
|
]
|
|
good_scripts.append(script_arr)
|
|
# Reconstruct File
|
|
ROM_COPY.seek(script_table)
|
|
ROM_COPY.writeMultipleBytes(len(good_scripts), 2)
|
|
for script in good_scripts:
|
|
for x in script:
|
|
ROM_COPY.writeMultipleBytes(x, 2)
|
|
|
|
|
|
def getObjectAddress(ROM_COPY: LocalROM, map: int, id: int, object_type: str) -> int:
|
|
"""Get address of object in setup."""
|
|
setup_start = getPointerLocation(TableNames.Setups, map)
|
|
ROM_COPY.seek(setup_start)
|
|
model_2_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
|
|
if object_type == "modeltwo":
|
|
for item in range(model_2_count):
|
|
item_start = setup_start + 4 + (item * 0x30)
|
|
ROM_COPY.seek(item_start + 0x2A)
|
|
if int.from_bytes(ROM_COPY.readBytes(2), "big") == id:
|
|
return item_start
|
|
mystery_start = setup_start + 4 + (0x30 * model_2_count)
|
|
ROM_COPY.seek(mystery_start)
|
|
mystery_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
|
|
actor_start = mystery_start + 4 + (0x24 * mystery_count)
|
|
ROM_COPY.seek(actor_start)
|
|
actor_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
|
|
if object_type == "actor":
|
|
for item in range(actor_count):
|
|
item_start = actor_start + 4 + (item * 0x38)
|
|
ROM_COPY.seek(item_start + 0x34)
|
|
if int.from_bytes(ROM_COPY.readBytes(2), "big") == id:
|
|
return item_start
|
|
return None
|
|
|
|
|
|
def getObjectAddressBrowser(ROM_COPY: ROM, map: int, id: int, object_type: str) -> int:
|
|
"""Get address of object in setup."""
|
|
setup_start = getPointerLocation(TableNames.Setups, map)
|
|
ROM_COPY.seek(setup_start)
|
|
model_2_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
|
|
if object_type == "modeltwo":
|
|
for item in range(model_2_count):
|
|
item_start = setup_start + 4 + (item * 0x30)
|
|
ROM_COPY.seek(item_start + 0x2A)
|
|
if int.from_bytes(ROM_COPY.readBytes(2), "big") == id:
|
|
return item_start
|
|
mystery_start = setup_start + 4 + (0x30 * model_2_count)
|
|
ROM_COPY.seek(mystery_start)
|
|
mystery_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
|
|
actor_start = mystery_start + 4 + (0x24 * mystery_count)
|
|
ROM_COPY.seek(actor_start)
|
|
actor_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
|
|
if object_type == "actor":
|
|
for item in range(actor_count):
|
|
item_start = actor_start + 4 + (item * 0x38)
|
|
ROM_COPY.seek(item_start + 0x34)
|
|
if int.from_bytes(ROM_COPY.readBytes(2), "big") == id:
|
|
return item_start
|
|
return None
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
def is_item_selected_cached(
|
|
bool_setting: bool,
|
|
multiselector_setting: tuple, # Change the type here
|
|
check: Union["HardModeSelected", "MiscChangesSelected"],
|
|
result_if_empty: bool,
|
|
) -> bool:
|
|
"""Determine whether a multiselector setting is enabled."""
|
|
if not bool_setting:
|
|
return False
|
|
if len(multiselector_setting) == 0:
|
|
return result_if_empty
|
|
return check in multiselector_setting
|
|
|
|
|
|
# Helper function to call the cached version
|
|
def IsItemSelected(
|
|
bool_setting: bool,
|
|
multiselector_setting: List[Union["MiscChangesSelected", Any]],
|
|
check: Union["HardModeSelected", "MiscChangesSelected"],
|
|
result_if_empty: bool = True,
|
|
) -> bool:
|
|
"""Determine whether a multiselector setting is enabled."""
|
|
# Convert the list to a tuple before passing it to the cached function
|
|
return is_item_selected_cached(bool_setting, tuple(multiselector_setting), check, result_if_empty)
|
|
|
|
|
|
class SpawnerChange:
|
|
"""Information regarding a spawner change."""
|
|
|
|
def __init__(self, map: Maps, spawner_id: int):
|
|
"""Initialize with given variables."""
|
|
self.map_target = map
|
|
self.spawner_target = spawner_id
|
|
self.new_enemy = None
|
|
self.new_scale = None
|
|
self.new_speed_0 = None
|
|
self.new_speed_1 = None
|
|
|
|
|
|
def applyCharacterSpawnerChanges(ROM_COPY: ROM, changes: list[SpawnerChange], fence_speed_factor: float = None):
|
|
"""Apply a series of changes to character spawners."""
|
|
formatted_changes = {}
|
|
id_changes_in_map = {}
|
|
for change in changes:
|
|
if change.map_target not in formatted_changes:
|
|
formatted_changes[change.map_target] = {}
|
|
id_changes_in_map[change.map_target] = []
|
|
formatted_changes[change.map_target][change.spawner_target] = change
|
|
id_changes_in_map[change.map_target].append(change.spawner_target)
|
|
for map_id in formatted_changes:
|
|
file_start = getPointerLocation(TableNames.Spawners, map_id)
|
|
ROM_COPY.seek(file_start)
|
|
fence_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
offset = 2
|
|
used_fence_ids = []
|
|
if fence_count > 0:
|
|
for x in range(fence_count):
|
|
fence = []
|
|
fence_start = file_start + offset
|
|
ROM_COPY.seek(file_start + offset)
|
|
point_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
offset += (point_count * 6) + 2
|
|
ROM_COPY.seek(file_start + offset)
|
|
point0_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
if fence_speed_factor is not None:
|
|
for y in range(point0_count):
|
|
ROM_COPY.seek(file_start + offset + 2 + (y * 10) + 8)
|
|
old_value = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
new_value = int(old_value * fence_speed_factor)
|
|
ROM_COPY.seek(file_start + offset + 2 + (y * 10) + 8)
|
|
ROM_COPY.write(new_value)
|
|
offset += (point0_count * 10) + 6
|
|
fence_finish = file_start + offset
|
|
fence_size = fence_finish - fence_start
|
|
ROM_COPY.seek(fence_finish - 4)
|
|
used_fence_ids.append(int.from_bytes(ROM_COPY.readBytes(2), "big"))
|
|
ROM_COPY.seek(fence_start)
|
|
for y in range(int(fence_size / 2)):
|
|
fence.append(int.from_bytes(ROM_COPY.readBytes(2), "big"))
|
|
ROM_COPY.seek(fence_finish)
|
|
spawner_count_location = file_start + offset
|
|
ROM_COPY.seek(spawner_count_location)
|
|
spawner_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
|
|
offset += 2
|
|
for x in range(spawner_count):
|
|
# Parse spawners
|
|
ROM_COPY.seek(file_start + offset + 0x13)
|
|
enemy_index = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
init_offset = offset
|
|
if enemy_index in id_changes_in_map[map_id]:
|
|
change_data = formatted_changes[map_id][enemy_index]
|
|
if change_data.new_enemy is not None:
|
|
ROM_COPY.seek(file_start + init_offset)
|
|
ROM_COPY.write(change_data.new_enemy)
|
|
if change_data.new_scale is not None:
|
|
ROM_COPY.seek(file_start + init_offset + 0xF)
|
|
ROM_COPY.write(change_data.new_scale)
|
|
if change_data.new_speed_0 is not None:
|
|
ROM_COPY.seek(file_start + init_offset + 0xC)
|
|
ROM_COPY.write(change_data.new_speed_0)
|
|
if change_data.new_speed_1 is not None:
|
|
ROM_COPY.seek(file_start + init_offset + 0xD)
|
|
ROM_COPY.write(change_data.new_speed_1)
|
|
ROM_COPY.seek(file_start + offset + 0x11)
|
|
extra_count = int.from_bytes(ROM_COPY.readBytes(1), "big")
|
|
offset += 0x16 + (extra_count * 2)
|
|
|
|
|
|
def camelCaseToWords(string: str):
|
|
"""Convert camel case string to separated words."""
|
|
words = [[string[0]]]
|
|
|
|
for c in string[1:]:
|
|
if words[-1][-1].islower() and c.isupper():
|
|
words.append(list(c))
|
|
else:
|
|
words[-1].append(c)
|
|
|
|
return " ".join(["".join(word) for word in words])
|
|
|
|
|
|
def getItemNumberString(count: int, item_type: Types) -> str:
|
|
"""Get a string which displays the number of items and the item name."""
|
|
names = {
|
|
Types.Banana: "Golden Banana",
|
|
Types.BlueprintBanana: "Golden Banana",
|
|
Types.Shop: "Move",
|
|
Types.Blueprint: "Blueprint",
|
|
Types.Fairy: "Fairy",
|
|
Types.Key: "Key",
|
|
Types.Crown: "Crown",
|
|
Types.Coin: "Company Coin",
|
|
Types.TrainingBarrel: "Move",
|
|
Types.Climbing: "Move",
|
|
Types.Kong: "Kong",
|
|
Types.Medal: "Medal",
|
|
Types.Shockwave: "Move",
|
|
Types.Bean: "Bean",
|
|
Types.Pearl: "Pearl",
|
|
Types.RainbowCoin: "Rainbow Coin",
|
|
Types.FakeItem: "Ice Trap",
|
|
Types.ToughBanana: "Golden Banana",
|
|
Types.JunkItem: "Junk Item",
|
|
Types.Hint: "Hint",
|
|
Types.PreGivenMove: "Move",
|
|
Types.Climbing: "Move",
|
|
Types.NintendoCoin: "Nintendo Coin",
|
|
Types.RarewareCoin: "Rareware Coin",
|
|
Types.Cranky: "Cranky",
|
|
Types.Funky: "Funky",
|
|
Types.Candy: "Candy",
|
|
Types.Snide: "Snide",
|
|
Types.IslesMedal: "Medal",
|
|
Types.ProgressiveHint: "Hint",
|
|
Types.ArchipelagoItem: "Archipelago Item",
|
|
}
|
|
name = names.get(item_type, item_type.name)
|
|
if count != 1:
|
|
name = f"{name}s"
|
|
if item_type == Types.Fairy:
|
|
name = "Fairies"
|
|
return f"{count} {name}"
|
|
|
|
|
|
def recalculatePointerJSON(ROM_COPY: ROM):
|
|
"""Recalculates the pointer tables."""
|
|
TABLE_COUNT = 32
|
|
POINTER_OFFSET = 0x101C50
|
|
new_data = [None] * TABLE_COUNT
|
|
for x in range(TABLE_COUNT):
|
|
ROM_COPY.seek(POINTER_OFFSET + ((TABLE_COUNT + x) << 2))
|
|
table_data = {"entries": []}
|
|
count = int.from_bytes(ROM_COPY.readBytes(4), "big")
|
|
ROM_COPY.seek(POINTER_OFFSET + (x << 2))
|
|
head = POINTER_OFFSET + int.from_bytes(ROM_COPY.readBytes(4), "big")
|
|
for y in range(count):
|
|
ROM_COPY.seek(head + (y << 2))
|
|
local_data = {
|
|
"index": y,
|
|
"pointing_to": POINTER_OFFSET + int.from_bytes(ROM_COPY.readBytes(4), "big"),
|
|
}
|
|
next_file = POINTER_OFFSET + int.from_bytes(ROM_COPY.readBytes(4), "big")
|
|
local_data["compressed_size"] = next_file - local_data["pointing_to"]
|
|
table_data["entries"].append(local_data)
|
|
new_data[x] = table_data
|
|
js.pointer_addresses = new_data
|
|
|
|
|
|
def setItemReferenceName(spoiler, item: Items, index: int, new_name: str):
|
|
"""Set new name for a location of an item."""
|
|
try:
|
|
if item == Items.CameraAndShockwave:
|
|
setItemReferenceName(spoiler, Items.Camera, index, new_name)
|
|
setItemReferenceName(spoiler, Items.Shockwave, index, new_name)
|
|
else:
|
|
for loc in spoiler.location_references:
|
|
if loc.item == item:
|
|
loc.setLocation(index, new_name)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def DoorItemToBarrierItem(item: HelmDoorItem, is_coin_door: bool = False, is_crown_door: bool = False) -> BarrierItems:
|
|
"""Convert helm door item enum to barrier item enum."""
|
|
if item == HelmDoorItem.vanilla:
|
|
if is_coin_door:
|
|
return BarrierItems.CompanyCoin
|
|
elif is_crown_door:
|
|
return BarrierItems.Crown
|
|
converter = {
|
|
HelmDoorItem.opened: BarrierItems.Nothing,
|
|
HelmDoorItem.req_bean: BarrierItems.Bean,
|
|
HelmDoorItem.req_bp: BarrierItems.Blueprint,
|
|
HelmDoorItem.req_companycoins: BarrierItems.CompanyCoin,
|
|
HelmDoorItem.req_crown: BarrierItems.Crown,
|
|
HelmDoorItem.req_fairy: BarrierItems.Fairy,
|
|
HelmDoorItem.req_gb: BarrierItems.GoldenBanana,
|
|
HelmDoorItem.req_key: BarrierItems.Key,
|
|
HelmDoorItem.req_medal: BarrierItems.Medal,
|
|
HelmDoorItem.req_pearl: BarrierItems.Pearl,
|
|
HelmDoorItem.req_rainbowcoin: BarrierItems.RainbowCoin,
|
|
}
|
|
return converter.get(item, BarrierItems.Nothing)
|
|
|
|
|
|
def getIceTrapCount(settings) -> int:
|
|
"""Get the amount of Ice Traps the game will attempt to place."""
|
|
if settings.archipelago:
|
|
return settings.ice_trap_count
|
|
|
|
ice_trap_freqs = {
|
|
IceTrapFrequency.rare: 4,
|
|
IceTrapFrequency.mild: 10,
|
|
IceTrapFrequency.common: 32,
|
|
IceTrapFrequency.frequent: 64,
|
|
IceTrapFrequency.pain: 100,
|
|
}
|
|
return ice_trap_freqs.get(settings.ice_trap_frequency, 16)
|
|
|
|
|
|
EXPONENT = 1.7
|
|
OFFSET_DIVISOR = 15
|
|
|
|
|
|
def getHintRequirement(slot: int, cap: int):
|
|
"""Get the hint requirement for a slot index."""
|
|
if slot == 34:
|
|
return cap
|
|
offset = cap / OFFSET_DIVISOR
|
|
hint_slot = slot & 0xFC
|
|
multiplier = cap - offset
|
|
final_offset = (cap + offset) / 2
|
|
exp_result = 1 + (math.pow(hint_slot, EXPONENT) / math.pow(34, EXPONENT))
|
|
z = math.pi * exp_result
|
|
required_gb_count = int(multiplier * 0.5 * math.cos(z) + final_offset)
|
|
if required_gb_count == 0:
|
|
return 1
|
|
return required_gb_count
|
|
|
|
|
|
def getHintRequirementBatch(batch: int, cap: int):
|
|
"""Get the hint requirement for a batch index."""
|
|
slot = 34
|
|
if batch < 9:
|
|
slot = batch * 4
|
|
return getHintRequirement(slot, cap)
|
|
|
|
|
|
def getProgHintBarrierItem(item: ProgressiveHintItem) -> BarrierItems:
|
|
"""Get the accompanying barrier item for the prog hint item."""
|
|
barrier_bijection = {
|
|
ProgressiveHintItem.req_gb: BarrierItems.GoldenBanana,
|
|
ProgressiveHintItem.req_bp: BarrierItems.Blueprint,
|
|
ProgressiveHintItem.req_key: BarrierItems.Key,
|
|
ProgressiveHintItem.req_medal: BarrierItems.Medal,
|
|
ProgressiveHintItem.req_crown: BarrierItems.Crown,
|
|
ProgressiveHintItem.req_fairy: BarrierItems.Fairy,
|
|
ProgressiveHintItem.req_rainbowcoin: BarrierItems.RainbowCoin,
|
|
ProgressiveHintItem.req_bean: BarrierItems.Bean,
|
|
ProgressiveHintItem.req_pearl: BarrierItems.Pearl,
|
|
ProgressiveHintItem.req_cb: BarrierItems.ColoredBanana,
|
|
}
|
|
return barrier_bijection[item]
|
|
|
|
|
|
def getValueFromByteArray(ba: bytearray, offset: int, size: int) -> int:
|
|
"""Get value from byte array given an offset and size."""
|
|
value = 0
|
|
for x in range(size):
|
|
local_value = ba[offset + x]
|
|
value <<= 8
|
|
value += local_value
|
|
return value
|
|
|
|
|
|
class Holidays(IntEnum):
|
|
"""Holiday Enum."""
|
|
|
|
no_holiday = 0
|
|
Christmas = auto()
|
|
Halloween = auto()
|
|
Anniv25 = auto()
|
|
|
|
|
|
def getHolidaySetting(settings):
|
|
"""Get the holiday setting."""
|
|
is_offseason = True
|
|
if is_offseason:
|
|
return settings.holiday_setting_offseason
|
|
return settings.holiday_setting
|
|
|
|
|
|
def getHoliday(settings):
|
|
"""Get the holiday experienced."""
|
|
if getHolidaySetting(settings):
|
|
return Holidays.Christmas
|
|
return Holidays.no_holiday
|
|
|
|
|
|
plando_colors = {
|
|
"\x04": [
|
|
"orange",
|
|
"woth",
|
|
"keys",
|
|
"donkey",
|
|
"aztec",
|
|
"freekongs",
|
|
"dogadon1",
|
|
],
|
|
"\x05": [
|
|
"red",
|
|
"foolish",
|
|
"diddy",
|
|
"helm",
|
|
],
|
|
"\x06": [
|
|
"blue",
|
|
"lanky",
|
|
"galleon",
|
|
"pufftoss",
|
|
],
|
|
"\x07": [
|
|
"purple",
|
|
"tiny",
|
|
"forest",
|
|
"fungi",
|
|
"dogadon2",
|
|
],
|
|
"\x08": [
|
|
"lightgreen",
|
|
"chunky",
|
|
"japes",
|
|
"dillo1",
|
|
],
|
|
"\x09": [
|
|
"magenta",
|
|
"castle",
|
|
"kutout",
|
|
],
|
|
"\x0a": [
|
|
"cyan",
|
|
"caves",
|
|
"fridge",
|
|
"dillo2",
|
|
],
|
|
"\x0b": [
|
|
"rust",
|
|
"isles",
|
|
"training",
|
|
],
|
|
"\x0c": [
|
|
"paleblue",
|
|
"allkongs",
|
|
"factory",
|
|
"madjack",
|
|
],
|
|
"\x0d": [
|
|
"green",
|
|
"jetpac",
|
|
],
|
|
}
|