Files
dockipelago/worlds/dk64/randomizer/Patching/MiscSetupChanges.py
Jonathan Tinney 7971961166
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
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

829 lines
42 KiB
Python

"""Apply misc setup changes."""
import math
from randomizer.Enums.Enemies import Enemies
from randomizer.Enums.Kongs import Kongs
from randomizer.Enums.Levels import Levels
from randomizer.Enums.SwitchTypes import SwitchType
from randomizer.Enums.Switches import Switches
from randomizer.Enums.Settings import (
DamageAmount,
PuzzleRando,
MiscChangesSelected,
FasterChecksSelected,
RemovedBarriersSelected,
KongModels,
SlamRequirement,
HardBossesSelected,
WinConditionComplex,
)
from randomizer.Lists.CustomLocations import CustomLocations
from randomizer.Enums.Maps import Maps
from randomizer.Lists.MapsAndExits import LevelMapTable
from randomizer.Patching.Library.Generic import IsItemSelected
from randomizer.Patching.Library.DataTypes import float_to_hex
from randomizer.Patching.Library.Assets import getPointerLocation, TableNames
from randomizer.Patching.Patcher import LocalROM
def pickRandomPositionCircle(random, center_x, center_z, min_radius, max_radius):
"""Pick a random position within a torus where the center and radius boundaries are specified."""
radius = min_radius + (math.sqrt(random.random()) * (max_radius - min_radius))
angle = random.uniform(0, math.pi * 2)
if angle == math.pi * 2:
angle = 0
item_dx = radius * math.sin(angle)
item_dz = radius * math.cos(angle)
item_x = center_x + item_dx
item_z = center_z + item_dz
return [item_x, item_z]
def pickRandomPositionsMult(random, center_x: float, center_z: float, min_radius: float, max_radius: float, count: int, min_dist: float, exclusions: list = []):
"""Pick multiple points within a torus where the center and radius boundaries are defined. There is a failsafe to make sure 2 points aren't within a certain specified distance away from each other."""
picked = []
check_list = exclusions.copy()
for item in range(count):
good_place = False
while not good_place:
selected = pickRandomPositionCircle(random, center_x, center_z, min_radius, max_radius)
if len(picked) == 0:
good_place = True
else:
good_place = True
for picked_item in check_list:
dx = picked_item[0] - selected[0]
dz = picked_item[1] - selected[1]
delta = math.sqrt((dx * dx) + (dz * dz))
if delta < min_dist:
good_place = False
if good_place:
picked.append(selected)
check_list.append(selected)
return {"picked": picked.copy(), "index": 0}
def pickChunkyCabinPadPositions(random):
"""Pick 3 points within a torus in Chunky's 5-door cabin where the center and radius boundaries are defined. There are failsafes to make sure 2 points are far enough apart and all points are easy enough to reach for casual game play purposes."""
picked_pads = []
# lamp_halfway_points are the center of the moving light circles when they are in their halfway points along their routes
lamp_halfway_points = [[169.53, 205.91], [435.219, 483.118]]
center_of_room = [294.594, 337.22]
lamp_radius = 70 # lamp radius is 65-70 but safe to use 70
for count in range(3):
good_pad = False
while not good_pad:
pad = pickRandomPositionCircle(random, center_of_room[0], center_of_room[1], 70, 180)
# check if pad is in a difficult spot to clear and if so, get the pad out of the difficult spot
for lamp in lamp_halfway_points:
# check if pad is in a lamp's radius when said lamp is on its halfway point
dx = pad[0] - lamp[0]
dz = pad[1] - lamp[1]
delta = math.sqrt((dx * dx) + (dz * dz))
# pad is in the radius mentioned in the comment above. Move the pad out of this radius
if delta < lamp_radius:
suggested_x = pad[0]
if lamp[0] < center_of_room[0]:
suggested_x = suggested_x + 70
else:
suggested_x = suggested_x - 70
suggested_z = pad[1]
if lamp[1] < center_of_room[1]:
suggested_z = suggested_z + 70
else:
suggested_z = suggested_z - 70
pad = random.choice([[suggested_x, pad[1]], [pad[0], suggested_z]])
# check if the pad is far inside and near the lamp radius (not in it, as that's what we fixed above)
# top right has a Low X and Low Z coordinate, bottom left has a high X and High Z coordinate
is_far_inside_top_right = lamp_halfway_points[0][0] < pad[0] < center_of_room[0] and lamp_halfway_points[0][1] < pad[1] < center_of_room[1]
is_far_inside_bottom_left = center_of_room[0] < pad[0] < lamp_halfway_points[1][0] and center_of_room[1] < pad[1] < lamp_halfway_points[1][1]
if is_far_inside_top_right or is_far_inside_bottom_left:
# flip the coordinates horizontally, this effectively moves the pad one quadrant clockwise
mirror_line = 294.594
difference = pad[0] - mirror_line
pad[0] = mirror_line - difference
# check if any pads overlap
if len(picked_pads) == 0:
good_pad = True
else:
good_pad = True
for previously_picked_item in picked_pads:
dx = previously_picked_item[0] - pad[0]
dz = previously_picked_item[1] - pad[1]
delta = math.sqrt((dx * dx) + (dz * dz))
if delta < 70:
good_pad = False
if good_pad:
picked_pads.append(pad)
return {"picked": picked_pads.copy(), "index": 0}
def SpeedUpFungiRabbit(ROM_COPY: LocalROM, factor: float = 1.0):
"""Change the speed of the Fungi Rabbit."""
file_start = getPointerLocation(TableNames.Spawners, Maps.FungiForest)
ROM_COPY.seek(file_start)
fence_count = int.from_bytes(ROM_COPY.readBytes(2), "big")
offset = 2
fence_bytes = []
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")
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"))
fence_bytes.append(fence)
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
ROM_COPY.seek(file_start + offset + 0x11)
extra_count = int.from_bytes(ROM_COPY.readBytes(1), "big")
offset += 0x16 + (extra_count * 2)
if enemy_index == 2:
# If enemy is the rabbit, adjust stats
speed_buff = 0.7 * factor
ROM_COPY.seek(file_start + init_offset + 0xD)
ROM_COPY.write(int(136 * speed_buff))
def getRandomGalleonStarLocation(random) -> tuple:
"""Get location for the DK Star which opens the treasure room."""
STAR_MAX_Y = 1657 # Star Y in vanilla game
boxes = [
[(1136, 1469, 1704), (1370, STAR_MAX_Y, 2207)],
[(2010, 1374, 1671), (2912, STAR_MAX_Y, 2216)],
[(2980, 663, 766), (2983, STAR_MAX_Y, 1311)],
[(3388, 594, 1834), (3441, STAR_MAX_Y, 2044)],
[(3731, 515, 1514), (3769, STAR_MAX_Y, 1833)],
]
bound = random.choice(boxes)
coord = [0, 0, 0]
for x in range(3):
coord[x] = random.randint(bound[0][x], bound[1][x])
return tuple(coord)
def randomize_setup(spoiler, ROM_COPY: LocalROM):
"""Randomize setup."""
if not spoiler.settings.disable_racing_patches:
SpeedUpFungiRabbit(ROM_COPY)
pickup_weights = [
{"item": "orange", "type": 0x56, "weight": 3},
{"item": "film", "type": 0x98, "weight": 1},
{"item": "crystals", "type": 0x8E, "weight": 4},
{"item": "standard_crate", "type": 0x8F, "weight": 4},
{"item": "homing_crate", "type": 0x11, "weight": 2},
# {
# "item": "feather_single",
# "type": 0x15D,
# "weight": 3,
# },
# {
# "item": "grape_single",
# "type": 0x15E,
# "weight": 3,
# },
# {
# "item": "pineapple_single",
# "type": 0x15F,
# "weight": 3,
# },
# {
# "item": "coconut_single",
# "type": 0x160,
# "weight": 3,
# },
# {
# "item": "peanut_single",
# "type": 0x91,
# "weight": 3,
# },
]
pickup_list = []
for pickup in pickup_weights:
if pickup["item"] == "film" and spoiler.settings.win_condition_item == WinConditionComplex.krem_kapture:
# Kremling Kapture requires a lot more film
pickup["weight"] = 5
for _ in range(pickup["weight"]):
pickup_list.append(pickup["type"])
arcade_r1_shortened = IsItemSelected(
spoiler.settings.faster_checks_enabled,
spoiler.settings.faster_checks_selected,
FasterChecksSelected.factory_arcade_round_1,
)
lighthouse_on = IsItemSelected(
spoiler.settings.remove_barriers_enabled,
spoiler.settings.remove_barriers_selected,
RemovedBarriersSelected.galleon_seasick_ship,
)
swap_list = [
{"map": Maps.AztecLlamaTemple, "item_list": [0xBC, 0x22B, 0x229, 0x22A]},
{"map": Maps.AztecTinyTemple, "item_list": [0xA7, 0xA6, 0xA5, 0xA4]},
{"map": Maps.FranticFactory, "item_list": [0x14D, 0x14C, 0x14B, 0x14A]},
{"map": Maps.CastleCrypt, "item_list": [0x247, 0x248, 0x249, 0x24A]},
]
if not spoiler.settings.perma_death and spoiler.settings.damage_amount not in (
DamageAmount.quad,
DamageAmount.ohko,
):
swap_list.append({"map": Maps.CastleMuseum, "item_list": [0x17]})
number_gb_data = [
{
"subtype": "corner",
"numbers": [
{"number": 12, "rot": 0},
{"number": 3, "rot": 1},
{"number": 5, "rot": 2},
{"number": 6, "rot": 3},
],
},
{
"subtype": "edge",
"numbers": [
{"number": 8, "rot": 0},
{"number": 10, "rot": 0},
{"number": 7, "rot": 1},
{"number": 16, "rot": 1},
{"number": 14, "rot": 2},
{"number": 9, "rot": 2},
{"number": 4, "rot": 3},
{"number": 1, "rot": 3},
],
},
{
"subtype": "center",
"numbers": [
{"number": 13, "rot": 0},
{"number": 15, "rot": 0},
{"number": 11, "rot": 0},
{"number": 2, "rot": 0},
],
},
]
vase_puzzle_positions = [
# [365.533, 138.167, 717.282], # Exclude center to force it to be a vase
[212.543, 120.5, 963.536],
[100.017, 120.5, 569.51],
[497.464, 120.5, 458.709],
[401.557, 138.167, 754.136],
[318.119, 138.167, 752.011],
[311.555, 138.167, 666.162],
[398.472, 138.167, 668.426],
]
diddy_5di_pads = pickRandomPositionsMult(spoiler.settings.random, 287.94, 312.119, 0, 140, 6, 40)
lanky_fungi_mush = pickRandomPositionsMult(
spoiler.settings.random,
274.9,
316.505,
40,
160,
5,
40,
[
[111.8, 238.5],
],
)
chunky_5dc_pads = pickChunkyCabinPadPositions(spoiler.settings.random)
spoiler.settings.random.shuffle(vase_puzzle_positions)
vase_puzzle_rando_progress = 0
raise_patch = IsItemSelected(
spoiler.settings.quality_of_life,
spoiler.settings.misc_changes_selected,
MiscChangesSelected.raise_fungi_dirt_patch,
)
move_cabin_barrel = IsItemSelected(
spoiler.settings.quality_of_life,
spoiler.settings.misc_changes_selected,
MiscChangesSelected.move_spring_cabin_rocketbarrel,
)
random_pufftoss_stars = IsItemSelected(spoiler.settings.hard_bosses, spoiler.settings.hard_bosses_selected, HardBossesSelected.pufftoss_star_rando, False)
higher_pufftoss_stars = IsItemSelected(spoiler.settings.hard_bosses, spoiler.settings.hard_bosses_selected, HardBossesSelected.pufftoss_star_raised, False)
removed_crypt_doors = IsItemSelected(
spoiler.settings.remove_barriers_enabled,
spoiler.settings.remove_barriers_selected,
RemovedBarriersSelected.castle_crypt_doors,
)
for cont_map_id in range(216):
cont_map_setup_address = getPointerLocation(TableNames.Setups, cont_map_id)
ROM_COPY.seek(cont_map_setup_address)
model2_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
# Puzzle Stuff
offsets = []
positions = []
if cont_map_id == Maps.FranticFactory:
number_replacement_data = {
"corner": {"offsets": [], "positions": []},
"edge": {"offsets": [], "positions": []},
"center": {"offsets": [], "positions": []},
}
for model2_item in range(model2_count):
item_start = cont_map_setup_address + 4 + (model2_item * 0x30)
ROM_COPY.seek(item_start + 0x28)
item_type = int.from_bytes(ROM_COPY.readBytes(2), "big")
ROM_COPY.seek(item_start + 0x2A)
item_id = int.from_bytes(ROM_COPY.readBytes(2), "big")
is_swap = False
for swap in swap_list:
if swap["map"] == cont_map_id and item_type in swap["item_list"]:
is_swap = True
if item_type == 0x196 and arcade_r1_shortened and cont_map_id == Maps.FactoryBaboonBlast:
ROM_COPY.seek(item_start + 0x28)
ROM_COPY.writeMultipleBytes(0x74, 2)
ROM_COPY.seek(item_start + 0xC)
ROM_COPY.writeMultipleBytes(0x3F000000, 4) # Scale: 0.5
elif item_type in pickup_list and spoiler.settings.randomize_pickups:
if cont_map_id != Maps.OrangeBarrel:
ROM_COPY.seek(item_start + 0x28)
ROM_COPY.writeMultipleBytes(spoiler.settings.random.choice(pickup_list), 2)
elif is_swap:
if spoiler.settings.puzzle_rando_difficulty != PuzzleRando.off:
offsets.append(item_start)
ROM_COPY.seek(item_start)
x = int.from_bytes(ROM_COPY.readBytes(4), "big")
y = int.from_bytes(ROM_COPY.readBytes(4), "big")
z = int.from_bytes(ROM_COPY.readBytes(4), "big")
ROM_COPY.seek(item_start + 0x1C)
ry = int.from_bytes(ROM_COPY.readBytes(4), "big")
positions.append([x, y, z, ry])
if item_type == 0x235:
if (cont_map_id == Maps.GalleonBoss and random_pufftoss_stars) or (cont_map_id == Maps.HideoutHelm and spoiler.settings.puzzle_rando_difficulty != PuzzleRando.off):
if cont_map_id == Maps.HideoutHelm:
y_position = spoiler.settings.random.uniform(-131, 500)
star_donut_center = [1055.704, 3446.966]
if y_position < 0:
star_donut_boundaries = [230, 300.971]
else:
star_donut_boundaries = [123.128, 235.971]
star_height_boundaries = [y_position, y_position]
elif cont_map_id == Maps.GalleonBoss:
star_donut_center = [1216, 1478]
star_donut_boundaries = [200, 460]
star_height_boundaries = []
star_pos = pickRandomPositionCircle(spoiler.settings.random, star_donut_center[0], star_donut_center[1], star_donut_boundaries[0], star_donut_boundaries[1])
star_a = spoiler.settings.random.uniform(0, 360)
if star_a == 360:
star_a = 0
star_x = star_pos[0]
star_z = star_pos[1]
ROM_COPY.seek(item_start)
ROM_COPY.writeMultipleBytes(int(float_to_hex(star_x), 16), 4)
ROM_COPY.seek(item_start + 8)
ROM_COPY.writeMultipleBytes(int(float_to_hex(star_z), 16), 4)
ROM_COPY.seek(item_start + 0x1C)
ROM_COPY.writeMultipleBytes(int(float_to_hex(star_a), 16), 4)
if len(star_height_boundaries) > 0:
star_y = spoiler.settings.random.uniform(star_height_boundaries[0], star_height_boundaries[1])
ROM_COPY.seek(item_start + 4)
ROM_COPY.writeMultipleBytes(int(float_to_hex(star_y), 16), 4)
if item_type == 0x74 and cont_map_id == Maps.GalleonLighthouse and lighthouse_on:
new_gb_coords = [407.107, 720, 501.02]
for coord_i, coord in enumerate(new_gb_coords):
ROM_COPY.seek(item_start + (coord_i * 4))
ROM_COPY.writeMultipleBytes(int(float_to_hex(coord), 16), 4)
elif cont_map_id == Maps.FranticFactory and spoiler.settings.puzzle_rando_difficulty != PuzzleRando.off and item_type >= 0xF4 and item_type <= 0x103:
for subtype_item in number_gb_data:
for num_item in subtype_item["numbers"]:
if num_item["number"] == (item_type - 0xF3):
subtype_name = subtype_item["subtype"]
ROM_COPY.seek(item_start)
x = int.from_bytes(ROM_COPY.readBytes(4), "big")
y = int.from_bytes(ROM_COPY.readBytes(4), "big")
z = int.from_bytes(ROM_COPY.readBytes(4), "big")
number_replacement_data[subtype_name]["offsets"].append({"offset": item_start, "rotation": num_item["rot"], "number": item_type - 0xF3})
number_replacement_data[subtype_name]["positions"].append({"coords": [x, y, z], "rotation": num_item["rot"]})
elif cont_map_id == Maps.ForestLankyMushroomsRoom and spoiler.settings.puzzle_rando_difficulty != PuzzleRando.off:
if item_type >= 0x1BA and item_type <= 0x1BE: # Mushrooms
spawner_pos = lanky_fungi_mush["picked"][lanky_fungi_mush["index"]]
ROM_COPY.seek(item_start)
ROM_COPY.writeMultipleBytes(int(float_to_hex(spawner_pos[0]), 16), 4)
ROM_COPY.seek(item_start + 8)
ROM_COPY.writeMultipleBytes(int(float_to_hex(spawner_pos[1]), 16), 4)
lanky_fungi_mush["index"] += 1
elif item_type == 0x205: # Lanky Bunch
spawner_pos = lanky_fungi_mush["picked"][0]
ROM_COPY.seek(item_start)
ROM_COPY.writeMultipleBytes(int(float_to_hex(spawner_pos[0]), 16), 4)
ROM_COPY.seek(item_start + 8)
ROM_COPY.writeMultipleBytes(int(float_to_hex(spawner_pos[1]), 16), 4)
elif cont_map_id == Maps.AngryAztec and spoiler.settings.puzzle_rando_difficulty != PuzzleRando.off and (item_type == 0x121 or (item_type >= 0x226 and item_type <= 0x228)):
# Is Vase Pad
ROM_COPY.seek(item_start)
for coord in range(3):
ROM_COPY.writeMultipleBytes(int(float_to_hex(vase_puzzle_positions[vase_puzzle_rando_progress][coord]), 16), 4)
vase_puzzle_rando_progress += 1
elif cont_map_id == Maps.CavesChunkyCabin and spoiler.settings.puzzle_rando_difficulty != PuzzleRando.off and item_type == 0x203:
spawner_pos = chunky_5dc_pads["picked"][chunky_5dc_pads["index"]]
ROM_COPY.seek(item_start)
ROM_COPY.writeMultipleBytes(int(float_to_hex(spawner_pos[0]), 16), 4)
ROM_COPY.seek(item_start + 8)
ROM_COPY.writeMultipleBytes(int(float_to_hex(spawner_pos[1]), 16), 4)
chunky_5dc_pads["index"] += 1
elif cont_map_id == Maps.GloomyGalleon and item_id == 0xC and spoiler.settings.puzzle_rando_difficulty in (PuzzleRando.hard, PuzzleRando.chaos):
coords = list(getRandomGalleonStarLocation(spoiler.settings.random))
ROM_COPY.seek(item_start)
for x in coords:
ROM_COPY.writeMultipleBytes(int(float_to_hex(x), 16), 4)
# Regular if because it can be combined with regular hard bosses
if item_type == 0x235 and cont_map_id == Maps.GalleonBoss and higher_pufftoss_stars:
ROM_COPY.seek(item_start + 4)
ROM_COPY.writeMultipleBytes(int(float_to_hex(345), 16), 4)
elif item_type == 0xCE and cont_map_id == Maps.HelmBarrelLankyMaze and spoiler.settings.sprint_barrel_requires_sprint:
ROM_COPY.seek(item_start + 0x28)
ROM_COPY.writeMultipleBytes(611, 2) # Overwrite type of obj to custom "Sprint Switch"
if spoiler.settings.chunky_phase_slam_req_internal and cont_map_id == Maps.KroolChunkyPhase and item_type == 0x16A:
slam_pads = {
SlamRequirement.green: 0x92,
SlamRequirement.blue: 0x16A,
SlamRequirement.red: 0x165,
}
ROM_COPY.seek(item_start + 0x28)
ROM_COPY.writeMultipleBytes(slam_pads[spoiler.settings.chunky_phase_slam_req_internal], 2)
# Delete crypt doors
if removed_crypt_doors:
size_down = False
if cont_map_id == Maps.CastleLowerCave:
size_down = item_id in (0x9, 0x6, 0x5, 0x7, 0x8, 0x4, 0x3)
elif cont_map_id == Maps.CastleCrypt:
size_down = item_id in (0xF, 0xE, 0xD)
if size_down:
ROM_COPY.seek(item_start + 0xC)
ROM_COPY.writeMultipleBytes(0, 4)
if spoiler.settings.puzzle_rando_difficulty != PuzzleRando.off:
if len(positions) > 0 and len(offsets) > 0:
spoiler.settings.random.shuffle(positions)
for index, offset in enumerate(offsets):
ROM_COPY.seek(offset)
for coord in range(3):
ROM_COPY.writeMultipleBytes(positions[index][coord], 4)
ROM_COPY.seek(offset + 0x1C)
ROM_COPY.writeMultipleBytes(positions[index][3], 4)
if cont_map_id == Maps.FranticFactory:
rotation_hexes = ["0x00000000", "0x42B40000", "0x43340000", "0x43870000"] # 0 # 90 # 180 # 270
for subtype in number_replacement_data:
subtype_name = subtype
subtype = number_replacement_data[subtype]
spoiler.settings.random.shuffle(subtype["positions"])
for index, offset in enumerate(subtype["offsets"]):
ROM_COPY.seek(offset["offset"])
base_rot = offset["rotation"]
for coord in range(3):
coord_val = subtype["positions"][index]["coords"][coord]
if coord == 1:
coord_val = int(float_to_hex(1002), 16)
ROM_COPY.writeMultipleBytes(coord_val, 4)
new_rot = subtype["positions"][index]["rotation"]
rot_diff = ((base_rot - new_rot) + 4) % 4
if subtype_name == "center":
rot_diff = spoiler.settings.random.randint(0, 3)
ROM_COPY.seek(offset["offset"] + 0x1C)
new_rot = (2 + rot_diff) % 4
ROM_COPY.writeMultipleBytes(int(rotation_hexes[new_rot], 16), 4)
# Mystery
ROM_COPY.seek(cont_map_setup_address + 4 + (model2_count * 0x30))
mystery_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
actor_block_start = cont_map_setup_address + 4 + (model2_count * 0x30) + 4 + (mystery_count * 0x24)
ROM_COPY.seek(cont_map_setup_address + 4 + (model2_count * 0x30) + 4 + (mystery_count * 0x24))
actor_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
actor_bytes = []
used_actor_ids = []
for actor_item in range(actor_count):
actor_start = actor_block_start + 4 + (actor_item * 0x38)
ROM_COPY.seek(actor_start + 0x32)
actor_type = int.from_bytes(ROM_COPY.readBytes(2), "big") + 0x10
if spoiler.settings.random_patches:
if not actor_type == 139:
byte_list = []
ROM_COPY.seek(actor_start + 0x34)
used_actor_ids.append(int.from_bytes(ROM_COPY.readBytes(2), "big"))
ROM_COPY.seek(actor_start)
for x in range(int(0x38 / 4)):
byte_list.append(int.from_bytes(ROM_COPY.readBytes(4), "big"))
actor_bytes.append(byte_list.copy())
if spoiler.settings.random_patches:
new_actor_id = 0x20
for dirt_item in spoiler.dirt_patch_placement:
for patch in CustomLocations[dirt_item["level"]]:
if patch.map == cont_map_id and patch.name == dirt_item["name"]:
patch_scale = min(patch.max_size / 64, 1)
if new_actor_id in used_actor_ids:
while new_actor_id in used_actor_ids:
new_actor_id += 1
dirt_bytes = []
dirt_bytes.append(int(float_to_hex(patch.coords[0]), 16))
if patch.is_fungi_hidden_patch and raise_patch:
dirt_bytes.append(int(float_to_hex(155), 16))
else:
dirt_bytes.append(int(float_to_hex(patch.coords[1]), 16))
dirt_bytes.append(int(float_to_hex(patch.coords[2]), 16))
dirt_bytes.append(int(float_to_hex(patch_scale), 16))
for x in range(8):
dirt_bytes.append(0)
rot_type_hex = hex(patch.rot_y) + "007B"
dirt_bytes.append(int(rot_type_hex, 16))
id_something_hex = hex(new_actor_id) + "46D0"
used_actor_ids.append(new_actor_id)
new_actor_id += 1
dirt_bytes.append(int(id_something_hex, 16))
actor_bytes.append(dirt_bytes)
ROM_COPY.seek(actor_block_start)
ROM_COPY.writeMultipleBytes(len(actor_bytes), 4)
for actor in actor_bytes:
for byte_list in actor:
ROM_COPY.writeMultipleBytes(byte_list, 4)
# Re-run through actor stuff for changes
ROM_COPY.seek(cont_map_setup_address + 4 + (model2_count * 0x30) + 4 + (mystery_count * 0x24))
actor_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
for actor_item in range(actor_count):
actor_start = actor_block_start + 4 + (actor_item * 0x38)
ROM_COPY.seek(actor_start + 0x32)
actor_type = int.from_bytes(ROM_COPY.readBytes(2), "big") + 0x10
ROM_COPY.seek(actor_start + 0x34)
actor_id = int.from_bytes(ROM_COPY.readBytes(2), "big")
if actor_type >= 100 and actor_type <= 105 and spoiler.settings.puzzle_rando_difficulty != PuzzleRando.off and cont_map_id == Maps.CavesDiddyIgloo: # 5DI Spawner
spawner_pos = diddy_5di_pads["picked"][diddy_5di_pads["index"]]
ROM_COPY.seek(actor_start)
ROM_COPY.writeMultipleBytes(int(float_to_hex(spawner_pos[0]), 16), 4)
ROM_COPY.seek(actor_start + 8)
ROM_COPY.writeMultipleBytes(int(float_to_hex(spawner_pos[1]), 16), 4)
diddy_5di_pads["index"] += 1
elif actor_type >= 64 and actor_type <= 66 and spoiler.settings.puzzle_rando_difficulty != PuzzleRando.off and cont_map_id == Maps.AngryAztec: # Exclude O Vase to force it to be vanilla
# Vase
ROM_COPY.seek(actor_start)
for coord in range(3):
ROM_COPY.writeMultipleBytes(int(float_to_hex(vase_puzzle_positions[vase_puzzle_rando_progress][coord]), 16), 4)
vase_puzzle_rando_progress += 1
elif actor_type == 0x1C and actor_id == 16 and spoiler.settings.fix_lanky_tiny_prod and cont_map_id == Maps.FranticFactory:
ROM_COPY.seek(actor_start + 0x14)
ROM_COPY.writeMultipleBytes(1, 4)
elif actor_type == 139 and raise_patch and not spoiler.settings.random_patches:
if cont_map_id == Maps.FungiForest and actor_id == 47:
ROM_COPY.seek(actor_start + 4)
ROM_COPY.writeMultipleBytes(int(float_to_hex(155), 16), 4)
elif actor_type == 49 and move_cabin_barrel and cont_map_id == Maps.CavesDiddyUpperCabin and actor_id == 0:
ROM_COPY.seek(actor_start)
ROM_COPY.writeMultipleBytes(int(float_to_hex(300), 16), 4)
ROM_COPY.writeMultipleBytes(int(float_to_hex(110), 16), 4)
ROM_COPY.writeMultipleBytes(int(float_to_hex(380), 16), 4)
def updateRandomSwitches(spoiler, ROM_COPY: LocalROM):
"""Update setup to account for random switch placement."""
if spoiler.settings.alter_switch_allocation:
switches = {
Kongs.donkey: [0x94, 0x16C, 0x167],
Kongs.diddy: [0x93, 0x16B, 0x166],
Kongs.lanky: [0x95, 0x16D, 0x168],
Kongs.tiny: [0x96, 0x16E, 0x169],
Kongs.chunky: [0xB8, 0x16A, 0x165],
}
all_switches = []
for kong in switches:
all_switches.extend(switches[kong])
for level in LevelMapTable:
if level not in (Levels.DKIsles, Levels.HideoutHelm):
switch_level = spoiler.settings.switch_allocation[level]
if switch_level > 0:
switch_level -= 1
acceptable_maps = LevelMapTable[level].copy()
if level == Levels.GloomyGalleon:
acceptable_maps.append(Maps.GloomyGalleonLobby) # Galleon lobby internally in the game is galleon, but isn't in rando files. Quick fix for this
for map in acceptable_maps:
file_start = getPointerLocation(TableNames.Setups, map)
ROM_COPY.seek(file_start)
model2_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
for model2_item in range(model2_count):
item_start = file_start + 4 + (model2_item * 0x30)
ROM_COPY.seek(item_start + 0x28)
item_type = int.from_bytes(ROM_COPY.readBytes(2), "big")
if item_type in all_switches:
for kong in switches:
if item_type in switches[kong]:
ROM_COPY.seek(item_start + 0x28)
ROM_COPY.writeMultipleBytes(switches[kong][switch_level], 2)
def updateSwitchsanity(spoiler, ROM_COPY: LocalROM):
"""Update setup to account for switchsanity."""
if spoiler.settings.switchsanity:
switches = {
SwitchType.SlamSwitch: [
0x94,
0x16C,
0x167, # DK
0x93,
0x16B,
0x166, # Diddy
0x95,
0x16D,
0x168, # Lanky
0x96,
0x16E,
0x169, # Tiny
0xB8,
0x16A,
0x165, # Chunky
],
SwitchType.GunSwitch: [0x129, 0x126, 0x128, 0x127, 0x125],
SwitchType.InstrumentPad: [0xA8, 0xA9, 0xAC, 0xAA, 0xAB],
SwitchType.PadMove: [0x97, 0xD4, 0x10C, 0x10B, 0x10A],
SwitchType.MiscActivator: [0x28, 0xC3],
}
switchsanity_maps = []
# Get list of maps which contain a switch affected by switchsanity, to reduce references to pointer table
for slot in spoiler.settings.switchsanity_data:
map_id = spoiler.settings.switchsanity_data[slot].map_id
if map_id not in switchsanity_maps:
switchsanity_maps.append(map_id)
for map_id in switchsanity_maps:
# Get list of ids of objects in map which are affected by switchsanity
ids_in_map = []
for slot in spoiler.settings.switchsanity_data:
if map_id == spoiler.settings.switchsanity_data[slot].map_id:
obj_ids = spoiler.settings.switchsanity_data[slot].ids
ids_in_map.extend(obj_ids)
# Handle setup
file_start = getPointerLocation(TableNames.Setups, map_id)
ROM_COPY.seek(file_start)
model2_count = int.from_bytes(ROM_COPY.readBytes(4), "big")
for model2_item in range(model2_count):
item_start = file_start + 4 + (model2_item * 0x30)
ROM_COPY.seek(item_start + 0x2A)
item_id = int.from_bytes(ROM_COPY.readBytes(2), "big")
if item_id in ids_in_map:
switch_kong = None
switch_type = None
switch_offset = None
switch_slot = None
for slot in spoiler.settings.switchsanity_data:
if map_id == spoiler.settings.switchsanity_data[slot].map_id:
if item_id in spoiler.settings.switchsanity_data[slot].ids:
switch_kong = spoiler.settings.switchsanity_data[slot].kong
switch_type = spoiler.settings.switchsanity_data[slot].switch_type
switch_offset = int(switch_kong)
switch_slot = slot
if switch_type == SwitchType.SlamSwitch:
ROM_COPY.seek(item_start + 0x28)
old_type = int.from_bytes(ROM_COPY.readBytes(2), "big")
old_level = switches[SwitchType.SlamSwitch].index(old_type) % 3
switch_offset = (3 * int(switch_kong)) + old_level
if switch_kong is not None and switch_type is not None and switch_offset is not None:
new_obj = switches[switch_type][switch_offset]
ROM_COPY.seek(item_start + 0x28)
ROM_COPY.writeMultipleBytes(new_obj, 2)
if switch_slot == Switches.IslesHelmLobbyGone and switch_type == SwitchType.MiscActivator:
if switch_kong == Kongs.diddy:
ROM_COPY.seek(item_start + 0xC)
ROM_COPY.writeMultipleBytes(0x3F400000, 4)
# elif switch_kong == Kongs.donkey:
# ROM_COPY.seek(item_start + 0x1C)
# ROM_COPY.writeMultipleBytes(0, 4)
def updateKrushaMoveNames(spoiler):
"""Replace move names for the kong that Krusha replaces."""
move_data = {
Kongs.donkey: [
{"textbox_index": 36, "mode": "replace_whole", "target": "CITRON CANNON"},
{"textbox_index": 6, "mode": "replace_whole", "target": "KANNON BLAST"},
{"textbox_index": 8, "mode": "replace_whole", "target": "STRONG KROC"},
{"textbox_index": 10, "mode": "replace_whole", "target": "GATOR GRAB"},
{"textbox_index": 62, "mode": "replace_whole", "target": "KRUSHA BLUEPRINT"},
{"textbox_index": 81, "mode": "replace_whole", "target": "KRUSHA"},
],
Kongs.diddy: [
{"textbox_index": 37, "mode": "replace_whole", "target": "CHERRY RIFLE"},
{"textbox_index": 12, "mode": "replace_whole", "target": "KREMLING KHARGE"},
{"textbox_index": 14, "mode": "replace_whole", "target": "ROCKET REPTILE"},
{"textbox_index": 16, "mode": "replace_whole", "target": "SALAMANDER SPRING"},
{"textbox_index": 63, "mode": "replace_whole", "target": "KRUSHA BLUEPRINT"},
{"textbox_index": 82, "mode": "replace_whole", "target": "KRUSHA"},
],
Kongs.lanky: [
{"textbox_index": 38, "mode": "replace_whole", "target": "CURRANT CARBINE"},
{"textbox_index": 18, "mode": "replace_whole", "target": "KREMSTAND"},
{"textbox_index": 20, "mode": "replace_whole", "target": "KABOOM BALLOON"},
{"textbox_index": 22, "mode": "replace_whole", "target": "KREMSTAND SPRINT"},
{"textbox_index": 64, "mode": "replace_whole", "target": "KRUSHA BLUEPRINT"},
{"textbox_index": 83, "mode": "replace_whole", "target": "KRUSHA"},
],
Kongs.tiny: [
{"textbox_index": 39, "mode": "replace_whole", "target": "POMEGRANATE MORTAR"},
{"textbox_index": 24, "mode": "replace_whole", "target": "MINI DILE"},
{"textbox_index": 26, "mode": "replace_whole", "target": "LIZARD TWIRL"},
{"textbox_index": 28, "mode": "replace_whole", "target": "KROCOPORT"},
{"textbox_index": 65, "mode": "replace_whole", "target": "KRUSHA BLUEPRINT"},
{"textbox_index": 84, "mode": "replace_whole", "target": "KRUSHA"},
],
Kongs.chunky: [
{"textbox_index": 40, "mode": "replace_whole", "target": "LIME BAZOOKA"},
{"textbox_index": 30, "mode": "replace_whole", "target": "HUNKY KRUSHY"},
{"textbox_index": 32, "mode": "replace_whole", "target": "KREMLING PUNCH"},
{"textbox_index": 34, "mode": "replace_whole", "target": "KHAMELEO GONE"},
{"textbox_index": 66, "mode": "replace_whole", "target": "KRUSHA BLUEPRINT"},
{"textbox_index": 85, "mode": "replace_whole", "target": "KRUSHA"},
],
}
name_replacements = {
Kongs.donkey: [
{"old": "Coconut Gun", "new": "CITRON CANNON"},
{"old": "Baboon Blast", "new": "KANNON BLAST"},
{"old": "Strong Kong", "new": "STRONG KROC"},
{"old": "Gorilla Grab", "new": "GATOR GRAB"},
{"old": "Donkey Kong", "new": "KRUSHA"},
],
Kongs.diddy: [
{"old": "Peanut Popguns", "new": "CHERRY RIFLE"},
{"old": "Chimpy Charge", "new": "KREMLING KHARGE"},
{"old": "Rocketbarrel Boost", "new": "ROCKET REPTILE"},
{"old": "Simian Spring", "new": "SALAMANDER SPRING"},
{"old": "Diddy Kong", "new": "KRUSHA"},
],
Kongs.lanky: [
{"old": "Grape Shooter", "new": "CURRANT CARBINE"},
{"old": "Orangstand", "new": "KREMSTAND"},
{"old": "Baboon Balloon", "new": "KABOOM BALLOON"},
{"old": "Orangstand Sprint", "new": "KREMSTAND SPRINT"},
{"old": "Lanky Kong", "new": "KRUSHA"},
],
Kongs.tiny: [
{"old": "Feather Bow", "new": "POMEGRANATE MORTAR"},
{"old": "Mini Monkey", "new": "MINI DILE"},
{"old": "Pony Tail Twirl", "new": "LIZARD TWIRL"},
{"old": "Monkeyport", "new": "KROCOPORT"},
{"old": "Tiny Kong", "new": "KRUSHA"},
],
Kongs.chunky: [
{"old": "Pineapple Launcher", "new": "LIME BAZOOKA"},
{"old": "Hunky Chunky", "new": "HUNKY KRUSHY"},
{"old": "Primate Punch", "new": "KREMLING PUNCH"},
{"old": "Gorilla Gone", "new": "KHAMELEO GONE"},
{"old": "Chunky Kong", "new": "KRUSHA"},
],
}
settings_values = [
spoiler.settings.kong_model_dk,
spoiler.settings.kong_model_diddy,
spoiler.settings.kong_model_lanky,
spoiler.settings.kong_model_tiny,
spoiler.settings.kong_model_chunky,
]
text_replacements = []
for index, value in enumerate(settings_values):
if value == KongModels.krusha:
text_replacements.extend(move_data[index])
chosen_replacements = name_replacements[index]
for reference in spoiler.location_references:
for replacement in chosen_replacements:
if reference.item_name == replacement["old"]:
reference.item_name = replacement["new"]
chosen_replacements.remove(replacement)
spoiler.text_changes[39] = text_replacements
def remove5DSCameraPoint(spoiler, ROM_COPY: LocalROM):
"""Remove the camera lock triggers for Tiny 5DS entry."""
if not IsItemSelected(spoiler.settings.quality_of_life, spoiler.settings.misc_changes_selected, MiscChangesSelected.vanilla_bug_fixes):
return
file_start = getPointerLocation(TableNames.Cutscenes, Maps.Galleon5DShipDKTiny)
ROM_COPY.seek(file_start)
header_end = 0x30
for x in range(0x18):
count = int.from_bytes(ROM_COPY.readBytes(2), "big")
header_end += 0x12 * count
ROM_COPY.seek(file_start + header_end)
count = int.from_bytes(ROM_COPY.readBytes(2), "big")
for index in range(count):
lock_start = file_start + header_end + (index * 0x1C) + 2
ROM_COPY.seek(lock_start + 0x14)
z_val = int.from_bytes(ROM_COPY.readBytes(2), "big")
if z_val > 0x7FFF:
z_val -= 0x10000
if z_val > 1700:
ROM_COPY.seek(lock_start + 0x10)
for y in range(3):
ROM_COPY.writeMultipleBytes(0, 2)
ROM_COPY.seek(lock_start + 0x19)
ROM_COPY.writeMultipleBytes(0, 1)