Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
d3129e25e1
|
@ -222,9 +222,16 @@ def parse_arguments(argv, no_defaults=False):
|
||||||
Random: Picks a random value between 0 and 7 (inclusive).
|
Random: Picks a random value between 0 and 7 (inclusive).
|
||||||
0-7: Number of crystals needed
|
0-7: Number of crystals needed
|
||||||
''')
|
''')
|
||||||
parser.add_argument('--open_pyramid', default=defval(False), help='''\
|
parser.add_argument('--open_pyramid', default=defval('auto'), help='''\
|
||||||
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it
|
Pre-opens the pyramid hole, this removes the Agahnim 2 requirement for it.
|
||||||
''', action='store_true')
|
Depending on goal, you might still need to beat Agahnim 2 in order to beat ganon.
|
||||||
|
fast ganon goals are crystals, ganontriforcehunt, localganontriforcehunt, pedestalganon
|
||||||
|
auto - Only opens pyramid hole if the goal specifies a fast ganon, and entrance shuffle
|
||||||
|
is vanilla, dungeonssimple or dungeonsfull.
|
||||||
|
goal - Opens pyramid hole if the goal specifies a fast ganon.
|
||||||
|
yes - Always opens the pyramid hole.
|
||||||
|
no - Never opens the pyramid hole.
|
||||||
|
''', choices=['auto', 'goal', 'yes', 'no'])
|
||||||
parser.add_argument('--rom', default=defval('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc'),
|
parser.add_argument('--rom', default=defval('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc'),
|
||||||
help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
help='Path to an ALttP JAP(1.0) rom to use as a base.')
|
||||||
parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
parser.add_argument('--loglevel', default=defval('info'), const='info', nargs='?', choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
|
||||||
|
|
13
Gui.py
13
Gui.py
|
@ -64,8 +64,13 @@ def guiMain(args=None):
|
||||||
createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar)
|
createSpoilerCheckbutton = Checkbutton(checkBoxFrame, text="Create Spoiler Log", variable=createSpoilerVar)
|
||||||
suppressRomVar = IntVar()
|
suppressRomVar = IntVar()
|
||||||
suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar)
|
suppressRomCheckbutton = Checkbutton(checkBoxFrame, text="Do not create patched Rom", variable=suppressRomVar)
|
||||||
openpyramidVar = IntVar()
|
openpyramidFrame = Frame(checkBoxFrame)
|
||||||
openpyramidCheckbutton = Checkbutton(checkBoxFrame, text="Pre-open Pyramid Hole", variable=openpyramidVar)
|
openpyramidVar = StringVar()
|
||||||
|
openpyramidVar.set('auto')
|
||||||
|
openpyramidOptionMenu = OptionMenu(openpyramidFrame, openpyramidVar, 'auto', 'goal', 'yes', 'no')
|
||||||
|
openpyramidLabel = Label(openpyramidFrame, text='Pre-open Pyramid Hole')
|
||||||
|
openpyramidLabel.pack(side=LEFT)
|
||||||
|
openpyramidOptionMenu.pack(side=LEFT)
|
||||||
mcsbshuffleFrame = Frame(checkBoxFrame)
|
mcsbshuffleFrame = Frame(checkBoxFrame)
|
||||||
mcsbLabel = Label(mcsbshuffleFrame, text="Shuffle: ")
|
mcsbLabel = Label(mcsbshuffleFrame, text="Shuffle: ")
|
||||||
|
|
||||||
|
@ -102,7 +107,7 @@ def guiMain(args=None):
|
||||||
|
|
||||||
createSpoilerCheckbutton.pack(expand=True, anchor=W)
|
createSpoilerCheckbutton.pack(expand=True, anchor=W)
|
||||||
suppressRomCheckbutton.pack(expand=True, anchor=W)
|
suppressRomCheckbutton.pack(expand=True, anchor=W)
|
||||||
openpyramidCheckbutton.pack(expand=True, anchor=W)
|
openpyramidFrame.pack(expand=True, anchor=W)
|
||||||
mcsbshuffleFrame.pack(expand=True, anchor=W)
|
mcsbshuffleFrame.pack(expand=True, anchor=W)
|
||||||
mcsbLabel.grid(row=0, column=0)
|
mcsbLabel.grid(row=0, column=0)
|
||||||
mapshuffleCheckbutton.grid(row=0, column=1)
|
mapshuffleCheckbutton.grid(row=0, column=1)
|
||||||
|
@ -564,7 +569,7 @@ def guiMain(args=None):
|
||||||
guiargs.create_spoiler = bool(createSpoilerVar.get())
|
guiargs.create_spoiler = bool(createSpoilerVar.get())
|
||||||
guiargs.skip_playthrough = not bool(createSpoilerVar.get())
|
guiargs.skip_playthrough = not bool(createSpoilerVar.get())
|
||||||
guiargs.suppress_rom = bool(suppressRomVar.get())
|
guiargs.suppress_rom = bool(suppressRomVar.get())
|
||||||
guiargs.open_pyramid = bool(openpyramidVar.get())
|
guiargs.open_pyramid = openpyramidVar.get()
|
||||||
guiargs.mapshuffle = bool(mapshuffleVar.get())
|
guiargs.mapshuffle = bool(mapshuffleVar.get())
|
||||||
guiargs.compassshuffle = bool(compassshuffleVar.get())
|
guiargs.compassshuffle = bool(compassshuffleVar.get())
|
||||||
guiargs.keyshuffle = {"on": True, "universal": "universal", "off": False}[keyshuffleVar.get()]
|
guiargs.keyshuffle = {"on": True, "universal": "universal", "off": False}[keyshuffleVar.get()]
|
||||||
|
|
49
Main.py
49
Main.py
|
@ -10,7 +10,7 @@ import zlib
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
|
||||||
from BaseClasses import World, CollectionState, Item, Region, Location
|
from BaseClasses import World, CollectionState, Item, Region, Location
|
||||||
from Shops import ShopSlotFill, create_shops, SHOP_ID_START, FillDisabledShopSlots
|
from Shops import ShopSlotFill, create_shops, SHOP_ID_START, FillDisabledShopSlots, total_shop_slots
|
||||||
from Items import ItemFactory, item_table, item_name_groups
|
from Items import ItemFactory, item_table, item_name_groups
|
||||||
from Regions import create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance
|
from Regions import create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance
|
||||||
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
from InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||||
|
@ -112,6 +112,14 @@ def main(args, seed=None):
|
||||||
for player in range(1, world.players + 1):
|
for player in range(1, world.players + 1):
|
||||||
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
|
||||||
|
|
||||||
|
if world.open_pyramid[player] == 'goal':
|
||||||
|
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
|
||||||
|
elif world.open_pyramid[player] == 'auto':
|
||||||
|
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \
|
||||||
|
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull'} or not world.shuffle_ganon)
|
||||||
|
else:
|
||||||
|
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(world.open_pyramid[player], world.open_pyramid[player])
|
||||||
|
|
||||||
for tok in filter(None, args.startinventory[player].split(',')):
|
for tok in filter(None, args.startinventory[player].split(',')):
|
||||||
item = ItemFactory(tok.strip(), player)
|
item = ItemFactory(tok.strip(), player)
|
||||||
if item:
|
if item:
|
||||||
|
@ -148,12 +156,13 @@ def main(args, seed=None):
|
||||||
|
|
||||||
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player])
|
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player])
|
||||||
|
|
||||||
if world.mode[player] != 'inverted':
|
for player in range(1, world.players + 1):
|
||||||
create_regions(world, player)
|
if world.mode[player] != 'inverted':
|
||||||
else:
|
create_regions(world, player)
|
||||||
create_inverted_regions(world, player)
|
else:
|
||||||
create_shops(world, player)
|
create_inverted_regions(world, player)
|
||||||
create_dungeons(world, player)
|
create_shops(world, player)
|
||||||
|
create_dungeons(world, player)
|
||||||
|
|
||||||
logger.info('Shuffling the World about.')
|
logger.info('Shuffling the World about.')
|
||||||
|
|
||||||
|
@ -371,19 +380,22 @@ def main(args, seed=None):
|
||||||
checks_in_area[location.player]["Total"] += 1
|
checks_in_area[location.player]["Total"] += 1
|
||||||
|
|
||||||
oldmancaves = []
|
oldmancaves = []
|
||||||
for region in [world.get_region("Old Man Sword Cave", player) for player in range(1, world.players + 1) if world.retro[player]]:
|
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
|
||||||
item = ItemFactory(region.shop.inventory[0]['item'], region.player)
|
for index, take_any in enumerate(takeanyregions):
|
||||||
player = region.player
|
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if world.retro[player]]:
|
||||||
location_id = SHOP_ID_START + 33
|
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], region.player)
|
||||||
|
player = region.player
|
||||||
|
location_id = SHOP_ID_START + total_shop_slots + index
|
||||||
|
|
||||||
if region.type == RegionType.LightWorld:
|
main_entrance = get_entrance_to_region(region)
|
||||||
checks_in_area[player]["Light World"].append(location_id)
|
if main_entrance.parent_region.type == RegionType.LightWorld:
|
||||||
else:
|
checks_in_area[player]["Light World"].append(location_id)
|
||||||
checks_in_area[player]["Dark World"].append(location_id)
|
else:
|
||||||
checks_in_area[player]["Total"] += 1
|
checks_in_area[player]["Dark World"].append(location_id)
|
||||||
|
checks_in_area[player]["Total"] += 1
|
||||||
|
|
||||||
er_hint_data[player][location_id] = get_entrance_to_region(region).name
|
er_hint_data[player][location_id] = main_entrance.name
|
||||||
oldmancaves.append(((location_id, player), (item.code, player)))
|
oldmancaves.append(((location_id, player), (item.code, player)))
|
||||||
|
|
||||||
precollected_items = [[] for player in range(world.players)]
|
precollected_items = [[] for player in range(world.players)]
|
||||||
for item in world.precollected_items:
|
for item in world.precollected_items:
|
||||||
|
@ -449,6 +461,7 @@ def main(args, seed=None):
|
||||||
return world
|
return world
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def copy_world(world):
|
def copy_world(world):
|
||||||
# ToDo: Not good yet
|
# ToDo: Not good yet
|
||||||
ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
|
ret = World(world.players, world.shuffle, world.logic, world.mode, world.swords, world.difficulty, world.difficulty_adjustments, world.timer, world.progressive, world.goal, world.algorithm, world.accessibility, world.shuffle_ganon, world.retro, world.custom, world.customitemarray, world.hints)
|
||||||
|
|
|
@ -379,7 +379,7 @@ def roll_settings(weights, plando_options: typing.Set[str] = frozenset(("bosses"
|
||||||
|
|
||||||
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
|
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
|
||||||
# fast ganon + ganon at hole
|
# fast ganon + ganon at hole
|
||||||
ret.open_pyramid = ret.goal in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
|
ret.open_pyramid = get_choice('open_pyramid', weights, 'goal')
|
||||||
|
|
||||||
ret.crystals_gt = prefer_int(get_choice('tower_open', weights))
|
ret.crystals_gt = prefer_int(get_choice('tower_open', weights))
|
||||||
|
|
||||||
|
|
53
Rom.py
53
Rom.py
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||||
RANDOMIZERBASEHASH = '93538d51eb018955a90181600e3384ba'
|
RANDOMIZERBASEHASH = '7d9778b7c0a90d71fa5f32a3b56cdd87'
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
@ -18,7 +18,7 @@ import concurrent.futures
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from BaseClasses import CollectionState, Region, Location
|
from BaseClasses import CollectionState, Region, Location
|
||||||
from Shops import ShopType
|
from Shops import ShopType, total_shop_slots
|
||||||
from Dungeons import dungeon_music_addresses
|
from Dungeons import dungeon_music_addresses
|
||||||
from Regions import location_table, old_location_address_to_new_location_address
|
from Regions import location_table, old_location_address_to_new_location_address
|
||||||
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
|
from Text import MultiByteTextMapper, CompressedTextMapper, text_addresses, Credits, TextTable
|
||||||
|
@ -791,6 +791,29 @@ def patch_rom(world, rom, player, team, enemized):
|
||||||
|
|
||||||
write_custom_shops(rom, world, player)
|
write_custom_shops(rom, world, player)
|
||||||
|
|
||||||
|
def credits_digit(num):
|
||||||
|
# top: $54 is 1, 55 2, etc , so 57=4, 5C=9
|
||||||
|
# bot: $7A is 1, 7B is 2, etc so 7D=4, 82=9 (zero unknown...)
|
||||||
|
return 0x53 + int(num), 0x79 + int(num)
|
||||||
|
|
||||||
|
credits_total = 216
|
||||||
|
if world.goal[player] == 'icerodhunt': # Impossible to get 216/216 with Ice rod hunt. Most possible is 215/216.
|
||||||
|
credits_total -= 1
|
||||||
|
if world.retro[player]: # Old man cave and Take any caves will count towards collection rate.
|
||||||
|
credits_total += 5
|
||||||
|
if world.shop_shuffle_slots[player]: # Potion shop only counts towards collection rate if included in the shuffle.
|
||||||
|
credits_total += 30 if 'w' in world.shop_shuffle[player] else 27
|
||||||
|
|
||||||
|
rom.write_byte(0x187010, credits_total) # dynamic credits
|
||||||
|
# collection rate address: 238C37
|
||||||
|
first_top, first_bot = credits_digit((credits_total / 100) % 10)
|
||||||
|
mid_top, mid_bot = credits_digit((credits_total / 10) % 10)
|
||||||
|
last_top, last_bot = credits_digit(credits_total % 10)
|
||||||
|
# top half
|
||||||
|
rom.write_bytes(0x118C46, [first_top, mid_top, last_top])
|
||||||
|
# bottom half
|
||||||
|
rom.write_bytes(0x118C64, [first_bot, mid_bot, last_bot])
|
||||||
|
|
||||||
# patch medallion requirements
|
# patch medallion requirements
|
||||||
if world.required_medallions[player][0] == 'Bombos':
|
if world.required_medallions[player][0] == 'Bombos':
|
||||||
rom.write_byte(0x180022, 0x00) # requirement
|
rom.write_byte(0x180022, 0x00) # requirement
|
||||||
|
@ -1560,6 +1583,7 @@ def write_custom_shops(rom, world, player):
|
||||||
|
|
||||||
shop_data = bytearray()
|
shop_data = bytearray()
|
||||||
items_data = bytearray()
|
items_data = bytearray()
|
||||||
|
retro_shop_slots = bytearray()
|
||||||
|
|
||||||
for shop_id, shop in enumerate(shops):
|
for shop_id, shop in enumerate(shops):
|
||||||
if shop_id == len(shops) - 1:
|
if shop_id == len(shops) - 1:
|
||||||
|
@ -1568,10 +1592,27 @@ def write_custom_shops(rom, world, player):
|
||||||
bytes[0] = shop_id
|
bytes[0] = shop_id
|
||||||
bytes[-1] = shop.sram_offset
|
bytes[-1] = shop.sram_offset
|
||||||
shop_data.extend(bytes)
|
shop_data.extend(bytes)
|
||||||
# [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high][player]
|
|
||||||
for item in shop.inventory:
|
arrow_mask = 0x00
|
||||||
|
for index, item in enumerate(shop.inventory):
|
||||||
|
slot = 0 if shop.type == ShopType.TakeAny else index
|
||||||
if item is None:
|
if item is None:
|
||||||
break
|
break
|
||||||
|
if world.shop_shuffle_slots[player] or shop.type == ShopType.TakeAny:
|
||||||
|
count_shop = (shop.region.name != 'Potion Shop' or 'w' in world.shop_shuffle[player]) and \
|
||||||
|
shop.region.name != 'Capacity Upgrade'
|
||||||
|
rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0)
|
||||||
|
if item['item'] == 'Single Arrow' and item['player'] == 0:
|
||||||
|
arrow_mask |= 1 << index
|
||||||
|
retro_shop_slots.append(shop.sram_offset + slot)
|
||||||
|
|
||||||
|
# [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high][player]
|
||||||
|
for index, item in enumerate(shop.inventory):
|
||||||
|
slot = 0 if shop.type == ShopType.TakeAny else index
|
||||||
|
if item is None:
|
||||||
|
break
|
||||||
|
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:
|
||||||
|
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
|
||||||
item_data = [shop_id, ItemFactory(item['item'], player).code] + int16_as_bytes(item['price']) + \
|
item_data = [shop_id, ItemFactory(item['item'], player).code] + int16_as_bytes(item['price']) + \
|
||||||
[item['max'], ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + \
|
[item['max'], ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + \
|
||||||
int16_as_bytes(item['replacement_price']) + [0 if item['player'] == player else item['player']]
|
int16_as_bytes(item['replacement_price']) + [0 if item['player'] == player else item['player']]
|
||||||
|
@ -1582,6 +1623,10 @@ def write_custom_shops(rom, world, player):
|
||||||
items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
|
items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
|
||||||
rom.write_bytes(0x184900, items_data)
|
rom.write_bytes(0x184900, items_data)
|
||||||
|
|
||||||
|
if world.retro[player]:
|
||||||
|
retro_shop_slots.append(0xFF)
|
||||||
|
rom.write_bytes(0x186540, retro_shop_slots)
|
||||||
|
|
||||||
|
|
||||||
def hud_format_text(text):
|
def hud_format_text(text):
|
||||||
output = bytes()
|
output = bytes()
|
||||||
|
|
4
Shops.py
4
Shops.py
|
@ -96,6 +96,9 @@ class Shop():
|
||||||
if not self.inventory[slot]:
|
if not self.inventory[slot]:
|
||||||
raise ValueError("Inventory can't be pushed back if it doesn't exist")
|
raise ValueError("Inventory can't be pushed back if it doesn't exist")
|
||||||
|
|
||||||
|
if not self.can_push_inventory(slot):
|
||||||
|
logging.warning(f'Warning, there is already an item pushed into this slot.')
|
||||||
|
|
||||||
self.inventory[slot] = {
|
self.inventory[slot] = {
|
||||||
'item': item,
|
'item': item,
|
||||||
'price': price,
|
'price': price,
|
||||||
|
@ -145,6 +148,7 @@ def ShopSlotFill(world):
|
||||||
slot_num = int(location.name[-1]) - 1
|
slot_num = int(location.name[-1]) - 1
|
||||||
shop: Shop = location.parent_region.shop
|
shop: Shop = location.parent_region.shop
|
||||||
if not shop.can_push_inventory(slot_num) or location.shop_slot_disabled:
|
if not shop.can_push_inventory(slot_num) or location.shop_slot_disabled:
|
||||||
|
location.shop_slot_disabled = True
|
||||||
removed.add(location)
|
removed.add(location)
|
||||||
|
|
||||||
if removed:
|
if removed:
|
||||||
|
|
Binary file not shown.
|
@ -97,6 +97,11 @@ goals:
|
||||||
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
|
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
|
||||||
local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
|
local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
|
||||||
ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
|
ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
|
||||||
|
pyramid_open:
|
||||||
|
goal: 50 # Opens pyrymid if goal is fast_ganon, ganon_pedestal, ganon_triforce_hunt, or local_ganon_triforce_hunt
|
||||||
|
auto: 0 # Opens pyramid same as goal, except when an entrance shuffle other than vanilla, dungeonssimple or dungeonsfull is in effect.
|
||||||
|
yes: 0 # pyramid is opened unconditionally. You still have to beat agahnim 2 for ganon and dungeons.
|
||||||
|
no: 0 # access to pyramid requires beating agahnim 2.
|
||||||
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
|
||||||
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
|
||||||
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
|
||||||
|
|
Loading…
Reference in New Issue