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
312 lines
12 KiB
Python
312 lines
12 KiB
Python
# from https://github.com/icebound777/PMR-SeedGenerator/blob/main/table.py
|
|
|
|
from .data.RomOptionList import rom_option_table, ap_to_rom_option_table
|
|
from .data.palettes_meta import MENU_COLORS
|
|
from .options import (EnemyDamage, PaperMarioOptions, PartnerUpgradeShuffle, ShuffleKootFavors, ShuffleLetters,
|
|
BowserCastleMode, StatusMenuColorPalette, EnemyDifficulty, ShuffleSuperMultiBlocks)
|
|
from .data.MysteryOptions import MysteryOptions
|
|
from .data.starting_maps import starting_maps
|
|
from .data.node import Node
|
|
from .items import PMItem
|
|
from .data.ItemList import item_groups, item_multiples_ids, item_table
|
|
|
|
|
|
class RomTable:
|
|
instance = None
|
|
default_db = {}
|
|
db = {}
|
|
info = {}
|
|
|
|
def __init__(self):
|
|
if RomTable.instance is None:
|
|
RomTable.instance = self
|
|
else:
|
|
self = RomTable.instance
|
|
|
|
def __getitem__(self, key):
|
|
return self.db[key]
|
|
|
|
def generate_pairs(self, options: PaperMarioOptions, placed_items: list[Node], entrances: list,
|
|
actor_attributes: list, move_costs: list, palettes: list, quizzes: list, music_list: list,
|
|
mapmirror_list: list, puzzle_list: list, mystery_opts: MysteryOptions, required_spirits: list,
|
|
battle_list: list, star_beam_area: int, trappable_item_names: list, random):
|
|
table_data = []
|
|
|
|
# Options
|
|
option_dbtuples = get_dbtuples(options, mystery_opts, required_spirits, star_beam_area)
|
|
|
|
for option_data in option_dbtuples:
|
|
option_key = option_data[0]
|
|
option_value = option_data[1]
|
|
if isinstance(option_value, int) and option_value < 0:
|
|
option_value = 0x100000000 + option_value
|
|
table_data.append({
|
|
"key": option_key,
|
|
"value": option_value,
|
|
})
|
|
|
|
# temp fix for multiworld
|
|
table_data.append({
|
|
"key": 0xAF050000,
|
|
"value": 0x00000000
|
|
})
|
|
|
|
# Quizzes
|
|
for key, value in quizzes:
|
|
table_data.append({
|
|
"key": key,
|
|
"value": value
|
|
})
|
|
|
|
# Items
|
|
repeat_items = {}
|
|
for node in placed_items:
|
|
if node.key_name_item is not None and node.current_item is not None:
|
|
item_id = node.current_item.id
|
|
# Progressive items default to their highest ids for in-game placement
|
|
# When received, we receive the lowest IDs
|
|
if item_id in item_multiples_ids.keys():
|
|
if item_id not in repeat_items.keys():
|
|
repeat_items[item_id] = len(item_multiples_ids[item_id]) - 1
|
|
|
|
item_id = item_multiples_ids[item_id][repeat_items[item_id]]
|
|
repeat_items[node.current_item.id] -= 1
|
|
elif item_id == item_table["Damage Trap"][2]:
|
|
# damage traps are fire flowers by default, but if it's local we can set it to be a different item
|
|
trap_item = random.choice(trappable_item_names)
|
|
item_id = get_trapped_item_id(item_table[trap_item][2])
|
|
|
|
table_data.append({
|
|
"key": node.get_item_key(),
|
|
"value": item_id,
|
|
})
|
|
|
|
# Item Prices
|
|
if (node.key_name_price is not None
|
|
and (node.key_name_price.startswith("ShopPrice")
|
|
or node.key_name_price.startswith("RewardAmount"))):
|
|
table_data.append({
|
|
"key": node.get_price_key(),
|
|
"value": node.current_item.base_price
|
|
})
|
|
|
|
# Entrances
|
|
for key, value in entrances:
|
|
table_data.append({
|
|
"key": key,
|
|
"value": value
|
|
})
|
|
|
|
for key, value in battle_list:
|
|
table_data.append({
|
|
"key": key,
|
|
"value": value
|
|
})
|
|
|
|
# Actor Attributes
|
|
for key, value in actor_attributes:
|
|
table_data.append({
|
|
"key": key,
|
|
"value": value
|
|
})
|
|
|
|
# Palettes
|
|
for key, value in palettes:
|
|
table_data.append({
|
|
"key": key,
|
|
"value": value
|
|
})
|
|
|
|
# Move Costs
|
|
for key, value in move_costs:
|
|
table_data.append({
|
|
"key": key,
|
|
"value": value
|
|
})
|
|
|
|
# Audio
|
|
for key, value in music_list:
|
|
table_data.append({
|
|
"key": key,
|
|
"value": value
|
|
})
|
|
|
|
# Map mirroring
|
|
for key, value in mapmirror_list:
|
|
table_data.append({
|
|
"key": key,
|
|
"value": value
|
|
})
|
|
|
|
# Puzzles & Minigames
|
|
for key, value in puzzle_list:
|
|
table_data.append({
|
|
"key": key,
|
|
"value": value
|
|
})
|
|
|
|
table_data.sort(key=lambda pair: pair["key"])
|
|
return table_data
|
|
|
|
def create(self):
|
|
self.info = get_table_info()
|
|
|
|
|
|
def get_table_info():
|
|
# Defaults
|
|
table_info = {
|
|
"magic_value": 0x504D4442,
|
|
"header_size": 0x20,
|
|
"db_size": 0,
|
|
"seed": 0xDEADBEEF,
|
|
"address": 0x1D00000,
|
|
"formations_offset": 0,
|
|
"itemhints_offset": 0,
|
|
"auth_address": 0x1cffff0
|
|
}
|
|
|
|
return table_info
|
|
|
|
|
|
def generate_table_pairs(value_set):
|
|
table_data = []
|
|
|
|
for key, value in value_set:
|
|
table_data.append({
|
|
"key": key,
|
|
"value": value
|
|
})
|
|
|
|
table_data.sort(key=lambda pair: pair["key"])
|
|
return table_data
|
|
|
|
|
|
def get_dbtuples(options: PaperMarioOptions, mystery_opts: MysteryOptions, required_spirits: list,
|
|
star_beam_area: int) -> list:
|
|
dbtuples = []
|
|
|
|
# map tracker check and shop bits
|
|
map_tracker_bits = 0x1 + 0x2
|
|
if options.shuffle_hidden_panels.value:
|
|
map_tracker_bits += 0x4
|
|
if options.partner_upgrades.value >= PartnerUpgradeShuffle.option_Super_Block_Locations:
|
|
map_tracker_bits += 0x8
|
|
if options.overworld_coins.value:
|
|
map_tracker_bits += 0x10
|
|
if options.coin_blocks.value:
|
|
map_tracker_bits += 0x20
|
|
if options.koot_coins.value:
|
|
map_tracker_bits += 0x40
|
|
if options.foliage_coins.value:
|
|
map_tracker_bits += 0x80
|
|
if options.dojo.value:
|
|
map_tracker_bits += 0x100
|
|
if options.koot_favors.value != ShuffleKootFavors.option_Vanilla:
|
|
map_tracker_bits += 0x200
|
|
if options.trading_events.value:
|
|
map_tracker_bits += 0x400
|
|
if options.letter_rewards.value != ShuffleLetters.option_Vanilla:
|
|
map_tracker_bits += 0x800
|
|
if not options.open_forest.value:
|
|
map_tracker_bits += 0x1000
|
|
if options.bowser_castle_mode.value == BowserCastleMode.option_Vanilla:
|
|
map_tracker_bits += 0x2000
|
|
if options.bowser_castle_mode.value <= BowserCastleMode.option_Shortened:
|
|
map_tracker_bits += 0x4000
|
|
if options.super_multi_blocks.value == ShuffleSuperMultiBlocks.option_Anywhere:
|
|
map_tracker_bits += 0x8000
|
|
|
|
map_tracker_check_bits = map_tracker_bits
|
|
map_tracker_shop_bits = 0x7
|
|
if options.bowser_castle_mode.value <= BowserCastleMode.option_Shortened:
|
|
map_tracker_shop_bits += 0x8
|
|
|
|
# status menu palette comes from multiple settings
|
|
color_mode, menu_color_a, menu_color_b = MENU_COLORS[options.status_menu_palette.value]
|
|
|
|
# if specific star spirits are required they need to be encoded
|
|
encoded_spirits = 0
|
|
for spirit in required_spirits:
|
|
encoded_spirits = encoded_spirits | (1 << (spirit - 1))
|
|
|
|
for rom_option, ap_option in ap_to_rom_option_table.items():
|
|
option_key = get_db_key(rom_option)
|
|
option_value = -1
|
|
if ap_option == "":
|
|
# handle options that are calculated, not yet implemented, or otherwise not changeable by the player
|
|
match rom_option:
|
|
# Always turned on
|
|
case "BlocksMatchContent" | "FastTextSkip" | "ShuffleItems" | "RandomQuiz" \
|
|
| "PeachCastleReturnPipe" | "MultiworldEnabled":
|
|
option_value = 1
|
|
# Always turned off
|
|
case "ChallengeMode" | "ShuffleDungeonRooms" | "ShuffleEntrancesByAll" | "MatchEntranceTypes" \
|
|
| "Widescreen" | "PawnsEnabled" | "StartingItem0" | "StartingItem1" | "StartingItem2" \
|
|
| "StartingItem3" | "StartingItem4" | "StartingItem5" | "StartingItem6" | "StartingItem7" \
|
|
| "StartingItem8" | "StartingItem9" | "StartingItemA" | "StartingItemB" | "StartingItemC" \
|
|
| "StartingItemD" | "StartingItemE" | "StartingItemF" | "PlandomizerActive":
|
|
option_value = 0
|
|
# Hammer and boots get received by the server, so we set the rom to jumpless/hammerless to start
|
|
case "StartingBoots" | "StartingHammer":
|
|
option_value = -1
|
|
# One setting on the front end, but two separate flags for the mod
|
|
case "DoubleDamage":
|
|
option_value = options.enemy_damage.value == EnemyDamage.option_Double_Pain
|
|
case "QuadrupleDamage":
|
|
option_value = options.enemy_damage.value == EnemyDamage.option_Quadruple_Pain
|
|
case "ProgressiveScaling":
|
|
option_value = options.enemy_difficulty.value == EnemyDifficulty.option_Progressive_Scaling
|
|
case "EnabledCheckBits":
|
|
option_value = map_tracker_check_bits
|
|
case "EnabledShopBits":
|
|
option_value = map_tracker_shop_bits
|
|
case "ColorMode":
|
|
option_value = color_mode
|
|
case "Box5ColorA":
|
|
option_value = menu_color_a
|
|
case "Box5ColorB":
|
|
option_value = menu_color_b
|
|
case "ItemChoiceA":
|
|
option_value = mystery_opts.mystery_itemA
|
|
case "ItemChoiceB":
|
|
option_value = mystery_opts.mystery_itemB
|
|
case "ItemChoiceC":
|
|
option_value = mystery_opts.mystery_itemC
|
|
case "ItemChoiceD":
|
|
option_value = mystery_opts.mystery_itemD
|
|
case "ItemChoiceE":
|
|
option_value = mystery_opts.mystery_itemE
|
|
case "ItemChoiceF":
|
|
option_value = mystery_opts.mystery_itemF
|
|
case "ItemChoiceG":
|
|
option_value = mystery_opts.mystery_itemG
|
|
# Calculated based on starting stats
|
|
case "StartingLevel":
|
|
option_value = int(options.starting_hp.value / 5 +
|
|
options.starting_fp.value / 5 +
|
|
options.starting_bp.value / 3) - 3
|
|
case "StartingMap":
|
|
option_value = starting_maps[options.starting_map.value][0]
|
|
case "StarWaySpiritsNeededEnc":
|
|
option_value = encoded_spirits
|
|
case "AllowPhysicsGlitches":
|
|
option_value = not options.prevent_ooblzs.value
|
|
case "StarBeamArea":
|
|
option_value = star_beam_area
|
|
|
|
else:
|
|
option_value = getattr(options, ap_option).value
|
|
|
|
dbtuples.append((option_key, option_value))
|
|
# print(f"{rom_option}, {option_value}")
|
|
return dbtuples
|
|
|
|
|
|
def get_db_key(rom_option):
|
|
data = rom_option_table[rom_option]
|
|
return (0xAF << 24) | (data[1] << 16) | (data[2] << 8) | data[3]
|
|
|
|
|
|
def get_trapped_item_id(item_id) -> int:
|
|
return item_id | 0x2000
|