Merge branch 'OriginOverworldShuffle' into OverworldShuffle
This commit is contained in:
278
Main.py
278
Main.py
@@ -28,6 +28,7 @@ from Fill import distribute_items_restrictive, promote_dungeon_items, fill_dunge
|
||||
from Fill import dungeon_tracking
|
||||
from Fill import sell_potions, sell_keys, balance_multiworld_progression, balance_money_progression, lock_shop_locations, set_prize_drops
|
||||
from ItemList import generate_itempool, difficulties, fill_prizes, customize_shops, fill_specific_items, create_farm_locations
|
||||
from UnderworldGlitchRules import create_hybridmajor_connections, create_hybridmajor_connectors
|
||||
from Utils import output_path, parse_player_names
|
||||
|
||||
from source.item.District import init_districts
|
||||
@@ -36,7 +37,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new
|
||||
from source.tools.BPS import create_bps_from_data
|
||||
from source.classes.CustomSettings import CustomSettings
|
||||
|
||||
version_number = '1.2.0.20'
|
||||
version_number = '1.2.0.23'
|
||||
version_branch = '-u'
|
||||
__version__ = f'{version_number}{version_branch}'
|
||||
|
||||
@@ -81,32 +82,33 @@ def check_python_version():
|
||||
|
||||
def main(args, seed=None, fish=None):
|
||||
check_python_version()
|
||||
|
||||
if args.print_template_yaml:
|
||||
return export_yaml(args, fish)
|
||||
|
||||
if args.outputpath:
|
||||
os.makedirs(args.outputpath, exist_ok=True)
|
||||
output_path.cached_path = args.outputpath
|
||||
|
||||
start = time.perf_counter()
|
||||
|
||||
world = init_world(args, fish)
|
||||
|
||||
logger = logging.getLogger('')
|
||||
|
||||
if args.securerandom:
|
||||
random.use_secure()
|
||||
seeded = False
|
||||
# initialize the world
|
||||
if args.code:
|
||||
for player, code in args.code.items():
|
||||
if code:
|
||||
Settings.adjust_args_from_code(code, player, args)
|
||||
customized = None
|
||||
if args.customizer:
|
||||
customized = CustomSettings()
|
||||
customized.load_yaml(args.customizer)
|
||||
seed = customized.determine_seed(seed)
|
||||
if world.customizer:
|
||||
seed = world.customizer.determine_seed(seed)
|
||||
seeded = True
|
||||
customized.adjust_args(args)
|
||||
world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords,
|
||||
args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm,
|
||||
args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints)
|
||||
world.customizer = customized if customized else None
|
||||
logger = logging.getLogger('')
|
||||
world.customizer.adjust_args(args)
|
||||
world = init_world(args, fish)
|
||||
|
||||
for i in zip(args.logic.values(), args.door_shuffle.values()):
|
||||
if i[0] == 'hybridglitches' and i[1] != 'vanilla':
|
||||
raise RuntimeError(BabelFish().translate("cli","cli","hybridglitches.door.shuffle"))
|
||||
|
||||
if seed is None:
|
||||
random.seed(None)
|
||||
world.seed = random.randint(0, 999999999)
|
||||
@@ -118,55 +120,10 @@ def main(args, seed=None, fish=None):
|
||||
if args.securerandom:
|
||||
world.seed = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(9))
|
||||
|
||||
world.boots_hint = args.boots_hint.copy()
|
||||
world.remote_items = args.remote_items.copy()
|
||||
world.mapshuffle = args.mapshuffle.copy()
|
||||
world.compassshuffle = args.compassshuffle.copy()
|
||||
world.keyshuffle = args.keyshuffle.copy()
|
||||
world.bigkeyshuffle = args.bigkeyshuffle.copy()
|
||||
world.bombbag = args.bombbag.copy()
|
||||
world.flute_mode = args.flute_mode.copy()
|
||||
world.bow_mode = args.bow_mode.copy()
|
||||
world.crystals_needed_for_ganon = {player: random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(args.crystals_ganon[player]) for player in range(1, world.players + 1)}
|
||||
world.crystals_needed_for_gt = {player: random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player]) for player in range(1, world.players + 1)}
|
||||
world.crystals_ganon_orig = args.crystals_ganon.copy()
|
||||
world.crystals_gt_orig = args.crystals_gt.copy()
|
||||
world.ganon_item = {player: get_random_ganon_item(args.swords) if args.ganon_item[player] == 'random' else args.ganon_item[player] for player in range(1, world.players + 1)}
|
||||
world.ganon_item_orig = args.ganon_item.copy()
|
||||
world.owTerrain = args.ow_terrain.copy()
|
||||
world.owKeepSimilar = args.ow_keepsimilar.copy()
|
||||
world.owWhirlpoolShuffle = args.ow_whirlpool.copy()
|
||||
world.owFluteShuffle = args.ow_fluteshuffle.copy()
|
||||
world.shuffle_bonk_drops = args.bonk_drops.copy()
|
||||
world.open_pyramid = args.openpyramid.copy()
|
||||
world.boss_shuffle = args.shufflebosses.copy()
|
||||
world.enemy_shuffle = args.shuffleenemies.copy()
|
||||
world.enemy_health = args.enemy_health.copy()
|
||||
world.enemy_damage = args.enemy_damage.copy()
|
||||
world.beemizer = args.beemizer.copy()
|
||||
world.intensity = {player: random.randint(1, 3) if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)}
|
||||
world.door_type_mode = args.door_type_mode.copy()
|
||||
world.trap_door_mode = args.trap_door_mode.copy()
|
||||
world.key_logic_algorithm = args.key_logic_algorithm.copy()
|
||||
world.decoupledoors = args.decoupledoors.copy()
|
||||
world.door_self_loops = args.door_self_loops.copy()
|
||||
world.experimental = args.experimental.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
world.fish = fish
|
||||
world.shopsanity = args.shopsanity.copy()
|
||||
world.dropshuffle = args.dropshuffle.copy()
|
||||
world.pottery = args.pottery.copy()
|
||||
world.potshuffle = args.shufflepots.copy()
|
||||
world.mixed_travel = args.mixed_travel.copy()
|
||||
world.standardize_palettes = args.standardize_palettes.copy()
|
||||
world.shufflelinks = args.shufflelinks.copy()
|
||||
world.shuffletavern = args.shuffletavern.copy()
|
||||
world.pseudoboots = args.pseudoboots.copy()
|
||||
world.overworld_map = args.overworld_map.copy()
|
||||
world.take_any = args.take_any.copy()
|
||||
world.restrict_boss_items = args.restrict_boss_items.copy()
|
||||
world.collection_rate = args.collection_rate.copy()
|
||||
world.colorizepots = args.colorizepots.copy()
|
||||
|
||||
world.treasure_hunt_count = {}
|
||||
world.treasure_hunt_total = {}
|
||||
@@ -210,8 +167,6 @@ def main(args, seed=None, fish=None):
|
||||
for player, name in enumerate(team, 1):
|
||||
world.player_names[player].append(name)
|
||||
logger.info('')
|
||||
world.settings = CustomSettings()
|
||||
world.settings.create_from_world(world, args)
|
||||
|
||||
outfilebase = f'OR_{args.outputname if args.outputname else world.seed}'
|
||||
|
||||
@@ -222,13 +177,10 @@ def main(args, seed=None, fish=None):
|
||||
if hasattr(world,"escape_assist") and player in world.escape_assist:
|
||||
world.escape_assist[player].append('bombs') # enemized escape assumes infinite bombs available and will likely be unbeatable without it
|
||||
|
||||
if args.usestartinventory[player]:
|
||||
for tok in filter(None, args.startinventory[player].split(',')):
|
||||
name = tok.replace("_", " ").strip()
|
||||
name = name if name != 'Ocarina' or world.flute_mode[player] != 'active' else 'Ocarina (Activated)'
|
||||
item = ItemFactory(name, player)
|
||||
if item:
|
||||
world.push_precollected(item)
|
||||
set_starting_inventory(world, args)
|
||||
|
||||
world.settings = CustomSettings()
|
||||
world.settings.create_from_world(world, args)
|
||||
|
||||
if args.create_spoiler and not args.jsonout:
|
||||
logger.info(world.fish.translate("cli", "cli", "create.meta"))
|
||||
@@ -246,15 +198,8 @@ def main(args, seed=None, fish=None):
|
||||
create_dungeons(world, player)
|
||||
adjust_locations(world, player)
|
||||
place_bosses(world, player)
|
||||
|
||||
if world.customizer and world.customizer.get_start_inventory():
|
||||
for p, inv_list in world.customizer.get_start_inventory().items():
|
||||
for inv_item in inv_list:
|
||||
item = ItemFactory(inv_item.strip(), p)
|
||||
if item:
|
||||
world.push_precollected(item)
|
||||
if args.print_custom_yaml:
|
||||
world.settings.record_info(world)
|
||||
if world.logic[player] in ('nologic', 'hybridglitches'):
|
||||
create_hybridmajor_connections(world, player)
|
||||
|
||||
if any(world.potshuffle.values()):
|
||||
logger.info(world.fish.translate("cli", "cli", "shuffling.pots"))
|
||||
@@ -273,8 +218,6 @@ def main(args, seed=None, fish=None):
|
||||
update_world_regions(world, player)
|
||||
mark_light_dark_world_regions(world, player)
|
||||
create_dynamic_exits(world, player)
|
||||
if args.print_custom_yaml:
|
||||
world.settings.record_overworld(world)
|
||||
|
||||
init_districts(world)
|
||||
|
||||
@@ -282,14 +225,14 @@ def main(args, seed=None, fish=None):
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
link_entrances_new(world, player)
|
||||
if world.logic[player] in ('nologic', 'hybridglitches'):
|
||||
create_hybridmajor_connectors(world, player)
|
||||
|
||||
logger.info(world.fish.translate("cli", "cli", "shuffling.prep"))
|
||||
|
||||
for player in range(1, world.players + 1):
|
||||
link_doors_prep(world, player)
|
||||
|
||||
if args.print_custom_yaml:
|
||||
world.settings.record_entrances(world)
|
||||
create_item_pool_config(world)
|
||||
|
||||
logger.info(world.fish.translate("cli", "cli", "shuffling.dungeons"))
|
||||
@@ -297,8 +240,6 @@ def main(args, seed=None, fish=None):
|
||||
for player in range(1, world.players + 1):
|
||||
link_doors(world, player)
|
||||
mark_light_dark_world_regions(world, player)
|
||||
if args.print_custom_yaml:
|
||||
world.settings.record_doors(world)
|
||||
|
||||
logger.info(world.fish.translate("cli", "cli", "generating.itempool"))
|
||||
|
||||
@@ -326,8 +267,6 @@ def main(args, seed=None, fish=None):
|
||||
lock_shop_locations(world, player)
|
||||
|
||||
massage_item_pool(world)
|
||||
if args.print_custom_yaml:
|
||||
world.settings.record_item_pool(world)
|
||||
logger.info(world.fish.translate("cli", "cli", "placing.dungeon.prizes"))
|
||||
|
||||
fill_prizes(world)
|
||||
@@ -377,6 +316,11 @@ def main(args, seed=None, fish=None):
|
||||
ensure_good_pots(world, True)
|
||||
|
||||
if args.print_custom_yaml:
|
||||
world.settings.record_info(world)
|
||||
world.settings.record_overworld(world)
|
||||
world.settings.record_entrances(world)
|
||||
world.settings.record_doors(world)
|
||||
world.settings.record_item_pool(world)
|
||||
world.settings.record_item_placements(world)
|
||||
world.settings.write_to_file(output_path(f'{outfilebase}_custom.yaml'))
|
||||
|
||||
@@ -420,7 +364,8 @@ def main(args, seed=None, fish=None):
|
||||
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
|
||||
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
|
||||
args.ow_palettes[player], args.uw_palettes[player], args.reduce_flashing[player],
|
||||
args.shuffle_sfx[player], args.msu_resume[player])
|
||||
args.shuffle_sfx[player], args.shuffle_sfxinstruments[player], args.shuffle_songinstruments[player],
|
||||
args.msu_resume[player])
|
||||
|
||||
if args.jsonout:
|
||||
jsonout[f'patch_t{team}_p{player}'] = rom.patches
|
||||
@@ -494,6 +439,124 @@ def main(args, seed=None, fish=None):
|
||||
return world
|
||||
|
||||
|
||||
def export_yaml(args, fish):
|
||||
if args.outputpath:
|
||||
os.makedirs(args.outputpath, exist_ok=True)
|
||||
output_path.cached_path = args.outputpath
|
||||
|
||||
outfilebase = f'{args.outputname if args.outputname else "export"}'
|
||||
logger = logging.getLogger('')
|
||||
|
||||
world = init_world(args, fish)
|
||||
|
||||
from OverworldShuffle import __version__ as ORVersion
|
||||
logger.info(
|
||||
world.fish.translate("cli","cli","app.title") + "\n",
|
||||
ORVersion,
|
||||
"(%s)" % outfilebase,
|
||||
Settings.make_code(world, 1) if world.players == 1 else ''
|
||||
)
|
||||
|
||||
for k,v in {"DR":__version__,"OR":ORVersion}.items():
|
||||
logger.info((k + ' Version:').ljust(16) + '%s' % v)
|
||||
|
||||
set_starting_inventory(world, args)
|
||||
|
||||
world.settings = CustomSettings()
|
||||
world.settings.create_from_world(world, args)
|
||||
|
||||
world.settings.record_item_pool(world, True)
|
||||
world.settings.write_to_file(output_path(f'{outfilebase}.yaml'))
|
||||
|
||||
return world
|
||||
|
||||
|
||||
def init_world(args, fish):
|
||||
if args.code:
|
||||
for player, code in args.code.items():
|
||||
if code:
|
||||
Settings.adjust_args_from_code(code, player, args)
|
||||
|
||||
customized = None
|
||||
if args.customizer:
|
||||
customized = CustomSettings()
|
||||
customized.load_yaml(args.customizer)
|
||||
customized.adjust_args(args, False)
|
||||
|
||||
world = World(args.multi, args.ow_shuffle, args.ow_crossed, args.ow_mixed, args.shuffle, args.door_shuffle, args.logic, args.mode, args.swords,
|
||||
args.difficulty, args.item_functionality, args.timer, args.progressive, args.goal, args.algorithm,
|
||||
args.accessibility, args.shuffleganon, args.custom, args.customitemarray, args.hints)
|
||||
|
||||
world.customizer = customized
|
||||
world.boots_hint = args.boots_hint.copy()
|
||||
world.remote_items = args.remote_items.copy()
|
||||
world.mapshuffle = args.mapshuffle.copy()
|
||||
world.compassshuffle = args.compassshuffle.copy()
|
||||
world.keyshuffle = args.keyshuffle.copy()
|
||||
world.bigkeyshuffle = args.bigkeyshuffle.copy()
|
||||
world.bombbag = args.bombbag.copy()
|
||||
world.flute_mode = args.flute_mode.copy()
|
||||
world.bow_mode = args.bow_mode.copy()
|
||||
world.crystals_ganon_orig = args.crystals_ganon.copy()
|
||||
world.crystals_gt_orig = args.crystals_gt.copy()
|
||||
world.owTerrain = args.ow_terrain.copy()
|
||||
world.owKeepSimilar = args.ow_keepsimilar.copy()
|
||||
world.owWhirlpoolShuffle = args.ow_whirlpool.copy()
|
||||
world.owFluteShuffle = args.ow_fluteshuffle.copy()
|
||||
world.shuffle_bonk_drops = args.bonk_drops.copy()
|
||||
world.open_pyramid = args.openpyramid.copy()
|
||||
world.boss_shuffle = args.shufflebosses.copy()
|
||||
world.enemy_shuffle = args.shuffleenemies.copy()
|
||||
world.enemy_health = args.enemy_health.copy()
|
||||
world.enemy_damage = args.enemy_damage.copy()
|
||||
world.beemizer = args.beemizer.copy()
|
||||
world.intensity = {player: 'random' if args.intensity[player] == 'random' else int(args.intensity[player]) for player in range(1, world.players + 1)}
|
||||
world.door_type_mode = args.door_type_mode.copy()
|
||||
world.trap_door_mode = args.trap_door_mode.copy()
|
||||
world.key_logic_algorithm = args.key_logic_algorithm.copy()
|
||||
world.decoupledoors = args.decoupledoors.copy()
|
||||
world.door_self_loops = args.door_self_loops.copy()
|
||||
world.experimental = args.experimental.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
world.fish = fish
|
||||
world.shopsanity = args.shopsanity.copy()
|
||||
world.dropshuffle = args.dropshuffle.copy()
|
||||
world.pottery = args.pottery.copy()
|
||||
world.potshuffle = args.shufflepots.copy()
|
||||
world.mixed_travel = args.mixed_travel.copy()
|
||||
world.standardize_palettes = args.standardize_palettes.copy()
|
||||
world.shufflelinks = args.shufflelinks.copy()
|
||||
world.shuffletavern = args.shuffletavern.copy()
|
||||
world.pseudoboots = args.pseudoboots.copy()
|
||||
world.overworld_map = args.overworld_map.copy()
|
||||
world.take_any = args.take_any.copy()
|
||||
world.restrict_boss_items = args.restrict_boss_items.copy()
|
||||
world.collection_rate = args.collection_rate.copy()
|
||||
world.colorizepots = args.colorizepots.copy()
|
||||
world.aga_randomness = args.aga_randomness.copy()
|
||||
|
||||
|
||||
return world
|
||||
|
||||
|
||||
def set_starting_inventory(world, args):
|
||||
for player in range(1, world.players + 1):
|
||||
if args.usestartinventory[player]:
|
||||
for tok in filter(None, args.startinventory[player].split(',')):
|
||||
name = tok.strip()
|
||||
name = name if name != 'Ocarina' or world.flute_mode[player] != 'active' else 'Ocarina (Activated)'
|
||||
item = ItemFactory(name, player)
|
||||
if item:
|
||||
world.push_precollected(item)
|
||||
|
||||
if world.customizer and world.customizer.get_start_inventory():
|
||||
for p, inv_list in world.customizer.get_start_inventory().items():
|
||||
for inv_item in inv_list:
|
||||
item = ItemFactory(inv_item.strip(), p)
|
||||
if item:
|
||||
world.push_precollected(item)
|
||||
|
||||
|
||||
def copy_world(world):
|
||||
# ToDo: Not good yet
|
||||
ret = World(world.players, world.owShuffle, world.owCrossed, world.owMixed, world.shuffle, world.doorShuffle, world.logic, world.mode, world.swords,
|
||||
@@ -550,6 +613,10 @@ def copy_world(world):
|
||||
ret.intensity = world.intensity.copy()
|
||||
ret.decoupledoors = world.decoupledoors.copy()
|
||||
ret.door_self_loops = world.door_self_loops.copy()
|
||||
ret.door_type_mode = world.door_type_mode.copy()
|
||||
ret.trap_door_mode = world.trap_door_mode.copy()
|
||||
ret.key_logic_algorithm = world.key_logic_algorithm.copy()
|
||||
ret.aga_randomness = world.aga_randomness.copy()
|
||||
ret.experimental = world.experimental.copy()
|
||||
ret.shopsanity = world.shopsanity.copy()
|
||||
ret.dropshuffle = world.dropshuffle.copy()
|
||||
@@ -566,8 +633,10 @@ def copy_world(world):
|
||||
for player in range(1, world.players + 1):
|
||||
create_regions(ret, player)
|
||||
update_world_regions(ret, player)
|
||||
if world.logic[player] in ('owglitches', 'nologic'):
|
||||
if world.logic[player] in ('owglitches', 'hybridglitches', 'nologic'):
|
||||
create_owg_connections(ret, player)
|
||||
if world.logic[player] in ('nologic', 'hybridglitches'):
|
||||
create_hybridmajor_connections(ret, player)
|
||||
create_dynamic_exits(ret, player)
|
||||
create_dungeon_regions(ret, player)
|
||||
create_owedges(ret, player)
|
||||
@@ -613,7 +682,12 @@ def copy_world(world):
|
||||
for exit in region.exits:
|
||||
if exit.connected_region:
|
||||
dest_region = ret.get_region(exit.connected_region.name, region.player)
|
||||
ret.get_entrance(exit.name, exit.player).connect(dest_region)
|
||||
src_exit = ret.get_entrance(exit.name, exit.player)
|
||||
if exit.name not in [e.name for e in dest_region.entrances if e.connected_region is not None]:
|
||||
if exit.name in [e.name for e in dest_region.entrances]:
|
||||
src_exit.connected_region = dest_region
|
||||
else:
|
||||
src_exit.connect(dest_region)
|
||||
|
||||
# fill locations
|
||||
for location in world.get_locations():
|
||||
@@ -670,6 +744,8 @@ def copy_world(world):
|
||||
for player in range(1, world.players + 1):
|
||||
categorize_world_regions(ret, player)
|
||||
create_farm_locations(ret, player)
|
||||
if world.logic[player] in ('nologic', 'hybridglitches'):
|
||||
create_hybridmajor_connectors(ret, player)
|
||||
set_rules(ret, player)
|
||||
|
||||
return ret
|
||||
@@ -726,6 +802,12 @@ def copy_world_premature(world, player):
|
||||
ret.enemy_damage = world.enemy_damage.copy()
|
||||
ret.beemizer = world.beemizer.copy()
|
||||
ret.intensity = world.intensity.copy()
|
||||
ret.decoupledoors = world.decoupledoors.copy()
|
||||
ret.door_self_loops = world.door_self_loops.copy()
|
||||
ret.door_type_mode = world.door_type_mode.copy()
|
||||
ret.trap_door_mode = world.trap_door_mode.copy()
|
||||
ret.key_logic_algorithm = world.key_logic_algorithm.copy()
|
||||
ret.aga_randomness = world.aga_randomness.copy()
|
||||
ret.experimental = world.experimental.copy()
|
||||
ret.shopsanity = world.shopsanity.copy()
|
||||
ret.dropshuffle = world.dropshuffle.copy()
|
||||
@@ -743,8 +825,10 @@ def copy_world_premature(world, player):
|
||||
|
||||
create_regions(ret, player)
|
||||
update_world_regions(ret, player)
|
||||
if world.logic[player] in ('owglitches', 'nologic'):
|
||||
if world.logic[player] in ('owglitches', 'hybridglitches', 'nologic'):
|
||||
create_owg_connections(ret, player)
|
||||
if world.logic[player] in ('nologic', 'hybridglitches'):
|
||||
create_hybridmajor_connections(ret, player)
|
||||
create_dynamic_exits(ret, player)
|
||||
create_dungeon_regions(ret, player)
|
||||
create_owedges(ret, player)
|
||||
@@ -775,7 +859,12 @@ def copy_world_premature(world, player):
|
||||
for exit in region.exits:
|
||||
if exit.connected_region:
|
||||
dest_region = ret.get_region(exit.connected_region.name, region.player)
|
||||
ret.get_entrance(exit.name, exit.player).connect(dest_region)
|
||||
src_exit = ret.get_entrance(exit.name, exit.player)
|
||||
if exit.name not in [e.name for e in dest_region.entrances if e.connected_region is not None]:
|
||||
if exit.name in [e.name for e in dest_region.entrances]:
|
||||
src_exit.connected_region = dest_region
|
||||
else:
|
||||
src_exit.connect(dest_region)
|
||||
|
||||
from OverworldShuffle import categorize_world_regions
|
||||
categorize_world_regions(ret, player)
|
||||
@@ -800,6 +889,9 @@ def copy_world_premature(world, player):
|
||||
for portal in world.dungeon_portals[player]:
|
||||
connect_portal(portal, ret, player)
|
||||
|
||||
if world.logic[player] in ('nologic', 'hybridglitches'):
|
||||
create_hybridmajor_connectors(ret, player)
|
||||
|
||||
set_rules(ret, player)
|
||||
|
||||
return ret
|
||||
|
||||
Reference in New Issue
Block a user