2022-04-28 22:42:11 +00:00
|
|
|
"""
|
|
|
|
Archipelago init file for The Witness
|
|
|
|
"""
|
2023-11-24 05:27:03 +00:00
|
|
|
import dataclasses
|
2023-07-20 00:10:48 +00:00
|
|
|
from typing import Dict, Optional
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
from BaseClasses import Region, Location, MultiWorld, Item, Entrance, Tutorial, CollectionState
|
|
|
|
from Options import PerGameCommonOptions, Toggle
|
2022-10-09 02:13:52 +00:00
|
|
|
from .hints import get_always_hint_locations, get_always_hint_items, get_priority_hint_locations, \
|
|
|
|
get_priority_hint_items, make_hints, generate_joke_hints
|
2023-06-25 22:38:39 +00:00
|
|
|
from worlds.AutoWorld import World, WebWorld
|
2022-10-09 02:13:52 +00:00
|
|
|
from .player_logic import WitnessPlayerLogic
|
|
|
|
from .static_logic import StaticWitnessLogic
|
2022-04-28 22:42:11 +00:00
|
|
|
from .locations import WitnessPlayerLocations, StaticWitnessLocations
|
2023-07-19 03:02:57 +00:00
|
|
|
from .items import WitnessItem, StaticWitnessItems, WitnessPlayerItems, ItemData
|
2022-04-28 22:42:11 +00:00
|
|
|
from .regions import WitnessRegions
|
2023-11-24 05:27:03 +00:00
|
|
|
from .rules import set_rules
|
|
|
|
from .Options import TheWitnessOptions
|
2023-07-19 03:02:57 +00:00
|
|
|
from .utils import get_audio_logs
|
|
|
|
from logging import warning, error
|
2022-04-28 22:42:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
class WitnessWebWorld(WebWorld):
|
|
|
|
theme = "jungle"
|
2022-05-11 18:05:53 +00:00
|
|
|
tutorials = [Tutorial(
|
|
|
|
"Multiworld Setup Guide",
|
|
|
|
"A guide to playing The Witness with Archipelago.",
|
|
|
|
"English",
|
|
|
|
"setup_en.md",
|
|
|
|
"setup/en",
|
|
|
|
["NewSoupVi", "Jarno"]
|
|
|
|
)]
|
2022-04-28 22:42:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
class WitnessWorld(World):
|
|
|
|
"""
|
|
|
|
The Witness is an open-world puzzle game with dozens of locations
|
|
|
|
to explore and over 500 puzzles. Play the popular puzzle randomizer
|
|
|
|
by sigma144, with an added layer of progression randomization!
|
|
|
|
"""
|
|
|
|
game = "The Witness"
|
|
|
|
topology_present = False
|
2023-11-24 05:27:03 +00:00
|
|
|
data_version = 14
|
2022-07-01 20:01:41 +00:00
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
StaticWitnessLogic()
|
|
|
|
StaticWitnessLocations()
|
|
|
|
StaticWitnessItems()
|
2022-04-28 22:42:11 +00:00
|
|
|
web = WitnessWebWorld()
|
2023-11-24 05:27:03 +00:00
|
|
|
|
|
|
|
options_dataclass = TheWitnessOptions
|
|
|
|
options: TheWitnessOptions
|
2022-04-28 22:42:11 +00:00
|
|
|
|
|
|
|
item_name_to_id = {
|
2023-07-19 03:02:57 +00:00
|
|
|
name: data.ap_code for name, data in StaticWitnessItems.item_data.items()
|
2022-04-28 22:42:11 +00:00
|
|
|
}
|
|
|
|
location_name_to_id = StaticWitnessLocations.ALL_LOCATIONS_TO_ID
|
2023-07-19 03:02:57 +00:00
|
|
|
item_name_groups = StaticWitnessItems.item_groups
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
required_client_version = (0, 4, 4)
|
2022-11-06 20:26:56 +00:00
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
def __init__(self, multiworld: "MultiWorld", player: int):
|
|
|
|
super().__init__(multiworld, player)
|
|
|
|
|
|
|
|
self.player_logic = None
|
|
|
|
self.locat = None
|
|
|
|
self.items = None
|
|
|
|
self.regio = None
|
|
|
|
|
|
|
|
self.log_ids_to_hints = None
|
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
self.items_placed_early = []
|
|
|
|
self.own_itempool = []
|
|
|
|
|
2022-04-28 22:42:11 +00:00
|
|
|
def _get_slot_data(self):
|
|
|
|
return {
|
2023-10-22 04:48:06 +00:00
|
|
|
'seed': self.random.randrange(0, 1000000),
|
2022-04-28 22:42:11 +00:00
|
|
|
'victory_location': int(self.player_logic.VICTORY_LOCATION, 16),
|
2022-06-16 01:04:45 +00:00
|
|
|
'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID,
|
2023-07-19 03:02:57 +00:00
|
|
|
'item_id_to_door_hexes': StaticWitnessItems.get_item_to_door_mappings(),
|
|
|
|
'door_hexes_in_the_pool': self.items.get_door_ids_in_pool(),
|
|
|
|
'symbols_not_in_the_game': self.items.get_symbol_ids_not_in_pool(),
|
2023-11-24 05:27:03 +00:00
|
|
|
'disabled_entities': [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES],
|
2022-10-09 02:13:52 +00:00
|
|
|
'log_ids_to_hints': self.log_ids_to_hints,
|
2023-07-19 03:02:57 +00:00
|
|
|
'progressive_item_lists': self.items.get_progressive_item_ids_in_pool(),
|
|
|
|
'obelisk_side_id_to_EPs': StaticWitnessLogic.OBELISK_SIDE_ID_TO_EP_HEXES,
|
2023-11-24 05:27:03 +00:00
|
|
|
'precompleted_puzzles': [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS],
|
2023-07-19 22:08:25 +00:00
|
|
|
'entity_to_name': StaticWitnessLogic.ENTITY_ID_TO_NAME,
|
2022-04-28 22:42:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
def generate_early(self):
|
2023-07-19 03:02:57 +00:00
|
|
|
disabled_locations = self.multiworld.exclude_locations[self.player].value
|
|
|
|
|
|
|
|
self.player_logic = WitnessPlayerLogic(
|
2023-11-24 05:27:03 +00:00
|
|
|
self, disabled_locations, self.multiworld.start_inventory[self.player].value
|
2023-07-19 03:02:57 +00:00
|
|
|
)
|
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
self.locat: WitnessPlayerLocations = WitnessPlayerLocations(self, self.player_logic)
|
|
|
|
self.items: WitnessPlayerItems = WitnessPlayerItems(
|
|
|
|
self, self.player_logic, self.locat
|
|
|
|
)
|
|
|
|
self.regio: WitnessRegions = WitnessRegions(self.locat, self)
|
2023-07-19 03:02:57 +00:00
|
|
|
|
|
|
|
self.log_ids_to_hints = dict()
|
2023-02-01 20:18:07 +00:00
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
if not (self.options.shuffle_symbols or self.options.shuffle_doors or self.options.shuffle_lasers):
|
2022-11-01 02:41:21 +00:00
|
|
|
if self.multiworld.players == 1:
|
2023-11-24 05:27:03 +00:00
|
|
|
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression"
|
|
|
|
f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't"
|
|
|
|
f" seem right.")
|
2022-08-22 03:50:01 +00:00
|
|
|
else:
|
2023-11-24 05:27:03 +00:00
|
|
|
raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any"
|
|
|
|
f" progression items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle.")
|
2022-07-17 10:56:22 +00:00
|
|
|
|
2023-03-02 23:08:24 +00:00
|
|
|
def create_regions(self):
|
2023-11-24 05:27:03 +00:00
|
|
|
self.regio.create_regions(self, self.player_logic)
|
2023-03-02 23:08:24 +00:00
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
# Set rules early so extra locations can be created based on the results of exploring collection states
|
|
|
|
|
|
|
|
set_rules(self)
|
|
|
|
|
|
|
|
# Add event items and tie them to event locations (e.g. laser activations).
|
|
|
|
|
|
|
|
event_locations = []
|
|
|
|
|
|
|
|
for event_location in self.locat.EVENT_LOCATION_TABLE:
|
|
|
|
item_obj = self.create_item(
|
|
|
|
self.player_logic.EVENT_ITEM_PAIRS[event_location]
|
|
|
|
)
|
|
|
|
location_obj = self.multiworld.get_location(event_location, self.player)
|
|
|
|
location_obj.place_locked_item(item_obj)
|
|
|
|
self.own_itempool.append(item_obj)
|
|
|
|
|
|
|
|
event_locations.append(location_obj)
|
|
|
|
|
|
|
|
# Place other locked items
|
|
|
|
dog_puzzle_skip = self.create_item("Puzzle Skip")
|
|
|
|
self.multiworld.get_location("Town Pet the Dog", self.player).place_locked_item(dog_puzzle_skip)
|
|
|
|
|
|
|
|
self.own_itempool.append(dog_puzzle_skip)
|
|
|
|
|
|
|
|
self.items_placed_early.append("Puzzle Skip")
|
|
|
|
|
|
|
|
# Pick an early item to place on the tutorial gate.
|
|
|
|
early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()]
|
|
|
|
if early_items:
|
2023-12-28 13:12:37 +00:00
|
|
|
random_early_item = self.random.choice(early_items)
|
2023-11-24 05:27:03 +00:00
|
|
|
if self.options.puzzle_randomization == 1:
|
|
|
|
# In Expert, only tag the item as early, rather than forcing it onto the gate.
|
|
|
|
self.multiworld.local_early_items[self.player][random_early_item] = 1
|
|
|
|
else:
|
|
|
|
# Force the item onto the tutorial gate check and remove it from our random pool.
|
|
|
|
gate_item = self.create_item(random_early_item)
|
|
|
|
self.multiworld.get_location("Tutorial Gate Open", self.player).place_locked_item(gate_item)
|
|
|
|
self.own_itempool.append(gate_item)
|
|
|
|
self.items_placed_early.append(random_early_item)
|
|
|
|
|
|
|
|
# There are some really restrictive settings in The Witness.
|
|
|
|
# They are rarely played, but when they are, we add some extra sphere 1 locations.
|
|
|
|
# This is done both to prevent generation failures, but also to make the early game less linear.
|
|
|
|
# Only sweeps for events because having this behavior be random based on Tutorial Gate would be strange.
|
|
|
|
|
|
|
|
state = CollectionState(self.multiworld)
|
|
|
|
state.sweep_for_events(locations=event_locations)
|
|
|
|
|
|
|
|
num_early_locs = sum(1 for loc in self.multiworld.get_reachable_locations(state, self.player) if loc.address)
|
|
|
|
|
|
|
|
# Adjust the needed size for sphere 1 based on how restrictive the settings are in terms of items
|
2023-07-19 03:02:57 +00:00
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
needed_size = 3
|
|
|
|
needed_size += self.options.puzzle_randomization == 1
|
|
|
|
needed_size += self.options.shuffle_symbols
|
|
|
|
needed_size += self.options.shuffle_doors > 0
|
|
|
|
|
|
|
|
# Then, add checks in order until the required amount of sphere 1 checks is met.
|
|
|
|
|
|
|
|
extra_checks = [
|
|
|
|
("First Hallway Room", "First Hallway Bend"),
|
|
|
|
("First Hallway", "First Hallway Straight"),
|
|
|
|
("Desert Outside", "Desert Surface 3"),
|
|
|
|
]
|
|
|
|
|
|
|
|
for i in range(num_early_locs, needed_size):
|
|
|
|
if not extra_checks:
|
|
|
|
break
|
|
|
|
|
|
|
|
region, loc = extra_checks.pop(0)
|
|
|
|
self.locat.add_location_late(loc)
|
|
|
|
self.multiworld.get_region(region, self.player).add_locations({loc: self.location_name_to_id[loc]})
|
|
|
|
|
|
|
|
player = self.multiworld.get_player_name(self.player)
|
|
|
|
|
|
|
|
warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""")
|
|
|
|
|
|
|
|
def create_items(self):
|
|
|
|
# Determine pool size.
|
|
|
|
pool_size: int = len(self.locat.CHECK_LOCATION_TABLE) - len(self.locat.EVENT_LOCATION_TABLE)
|
2023-07-19 03:02:57 +00:00
|
|
|
|
|
|
|
# Fill mandatory items and remove precollected and/or starting items from the pool.
|
2023-07-20 00:10:48 +00:00
|
|
|
item_pool: Dict[str, int] = self.items.get_mandatory_items()
|
2023-07-19 03:02:57 +00:00
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
# Remove one copy of each item that was placed early
|
|
|
|
for already_placed in self.items_placed_early:
|
|
|
|
pool_size -= 1
|
|
|
|
|
|
|
|
if already_placed not in item_pool:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if item_pool[already_placed] == 1:
|
|
|
|
item_pool.pop(already_placed)
|
|
|
|
else:
|
|
|
|
item_pool[already_placed] -= 1
|
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
for precollected_item_name in [item.name for item in self.multiworld.precollected_items[self.player]]:
|
|
|
|
if precollected_item_name in item_pool:
|
|
|
|
if item_pool[precollected_item_name] == 1:
|
|
|
|
item_pool.pop(precollected_item_name)
|
|
|
|
else:
|
|
|
|
item_pool[precollected_item_name] -= 1
|
|
|
|
|
|
|
|
for inventory_item_name in self.player_logic.STARTING_INVENTORY:
|
|
|
|
if inventory_item_name in item_pool:
|
|
|
|
if item_pool[inventory_item_name] == 1:
|
|
|
|
item_pool.pop(inventory_item_name)
|
|
|
|
else:
|
|
|
|
item_pool[inventory_item_name] -= 1
|
2023-07-28 07:39:56 +00:00
|
|
|
self.multiworld.push_precollected(self.create_item(inventory_item_name))
|
2023-07-19 03:02:57 +00:00
|
|
|
|
|
|
|
if len(item_pool) > pool_size:
|
2023-11-24 05:27:03 +00:00
|
|
|
error(f"{self.multiworld.get_player_name(self.player)}'s Witness world has too few locations ({pool_size})"
|
|
|
|
f" to place its necessary items ({len(item_pool)}).")
|
2023-07-19 03:02:57 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
remaining_item_slots = pool_size - sum(item_pool.values())
|
|
|
|
|
|
|
|
# Add puzzle skips.
|
2023-11-24 05:27:03 +00:00
|
|
|
num_puzzle_skips = self.options.puzzle_skip_amount
|
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
if num_puzzle_skips > remaining_item_slots:
|
2023-11-24 05:27:03 +00:00
|
|
|
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations"
|
|
|
|
f" to place all requested puzzle skips.")
|
2023-07-19 03:02:57 +00:00
|
|
|
num_puzzle_skips = remaining_item_slots
|
|
|
|
item_pool["Puzzle Skip"] = num_puzzle_skips
|
|
|
|
remaining_item_slots -= num_puzzle_skips
|
|
|
|
|
|
|
|
# Add junk items.
|
|
|
|
if remaining_item_slots > 0:
|
|
|
|
item_pool.update(self.items.get_filler_items(remaining_item_slots))
|
|
|
|
|
|
|
|
# Generate the actual items.
|
2023-07-19 23:20:59 +00:00
|
|
|
for item_name, quantity in sorted(item_pool.items()):
|
2023-11-24 05:27:03 +00:00
|
|
|
new_items = [self.create_item(item_name) for _ in range(0, quantity)]
|
|
|
|
|
|
|
|
self.own_itempool += new_items
|
|
|
|
self.multiworld.itempool += new_items
|
2023-07-19 03:02:57 +00:00
|
|
|
if self.items.item_data[item_name].local_only:
|
|
|
|
self.multiworld.local_items[self.player].value.add(item_name)
|
|
|
|
|
2022-04-28 22:42:11 +00:00
|
|
|
def fill_slot_data(self) -> dict:
|
2023-11-24 05:27:03 +00:00
|
|
|
hint_amount = self.options.hint_amount.value
|
2022-10-09 02:13:52 +00:00
|
|
|
|
2022-11-17 16:35:59 +00:00
|
|
|
credits_hint = (
|
2023-02-01 20:18:07 +00:00
|
|
|
"This Randomizer is brought to you by",
|
|
|
|
"NewSoupVi, Jarno, blastron,",
|
2023-07-27 02:23:19 +00:00
|
|
|
"jbzdarkid, sigma144, IHNN, oddGarrett, Exempt-Medic.", -1
|
2022-11-17 16:35:59 +00:00
|
|
|
)
|
2022-10-09 02:13:52 +00:00
|
|
|
|
|
|
|
audio_logs = get_audio_logs().copy()
|
|
|
|
|
|
|
|
if hint_amount != 0:
|
2023-11-24 05:27:03 +00:00
|
|
|
generated_hints = make_hints(self, hint_amount, self.own_itempool)
|
2022-10-09 02:13:52 +00:00
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
self.random.shuffle(audio_logs)
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2023-06-20 22:45:26 +00:00
|
|
|
duplicates = min(3, len(audio_logs) // hint_amount)
|
2022-10-09 02:13:52 +00:00
|
|
|
|
|
|
|
for _ in range(0, hint_amount):
|
2023-02-01 20:18:07 +00:00
|
|
|
hint = generated_hints.pop(0)
|
2022-10-09 02:13:52 +00:00
|
|
|
|
|
|
|
for _ in range(0, duplicates):
|
|
|
|
audio_log = audio_logs.pop()
|
|
|
|
self.log_ids_to_hints[int(audio_log, 16)] = hint
|
|
|
|
|
|
|
|
if audio_logs:
|
|
|
|
audio_log = audio_logs.pop()
|
|
|
|
self.log_ids_to_hints[int(audio_log, 16)] = credits_hint
|
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
joke_hints = generate_joke_hints(self, len(audio_logs))
|
2022-10-09 02:13:52 +00:00
|
|
|
|
|
|
|
while audio_logs:
|
|
|
|
audio_log = audio_logs.pop()
|
|
|
|
self.log_ids_to_hints[int(audio_log, 16)] = joke_hints.pop()
|
|
|
|
|
|
|
|
# generate hints done
|
|
|
|
|
|
|
|
slot_data = self._get_slot_data()
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
for option_name in (attr.name for attr in dataclasses.fields(TheWitnessOptions)
|
|
|
|
if attr not in dataclasses.fields(PerGameCommonOptions)):
|
|
|
|
option = getattr(self.options, option_name)
|
|
|
|
slot_data[option_name] = bool(option.value) if isinstance(option, Toggle) else option.value
|
2022-04-28 22:42:11 +00:00
|
|
|
|
|
|
|
return slot_data
|
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
def create_item(self, item_name: str) -> Item:
|
2023-08-16 14:03:41 +00:00
|
|
|
# If the player's plando options are malformed, the item_name parameter could be a dictionary containing the
|
|
|
|
# name of the item, rather than the item itself. This is a workaround to prevent a crash.
|
|
|
|
if type(item_name) is dict:
|
|
|
|
item_name = list(item_name.keys())[0]
|
|
|
|
|
2022-04-28 22:42:11 +00:00
|
|
|
# this conditional is purely for unit tests, which need to be able to create an item before generate_early
|
2023-07-19 03:02:57 +00:00
|
|
|
item_data: ItemData
|
|
|
|
if hasattr(self, 'items') and self.items and item_name in self.items.item_data:
|
|
|
|
item_data = self.items.item_data[item_name]
|
2022-06-17 01:23:27 +00:00
|
|
|
else:
|
2023-07-19 03:02:57 +00:00
|
|
|
item_data = StaticWitnessItems.item_data[item_name]
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2023-07-19 03:02:57 +00:00
|
|
|
return WitnessItem(item_name, item_data.classification, item_data.ap_code, player=self.player)
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2023-09-23 23:48:20 +00:00
|
|
|
def get_filler_item_name(self) -> str:
|
|
|
|
return "Speed Boost"
|
|
|
|
|
2022-04-28 22:42:11 +00:00
|
|
|
|
|
|
|
class WitnessLocation(Location):
|
|
|
|
"""
|
|
|
|
Archipelago Location for The Witness
|
|
|
|
"""
|
|
|
|
game: str = "The Witness"
|
2023-11-24 05:27:03 +00:00
|
|
|
entity_hex: int = -1
|
2022-04-28 22:42:11 +00:00
|
|
|
|
2023-07-20 00:10:48 +00:00
|
|
|
def __init__(self, player: int, name: str, address: Optional[int], parent, ch_hex: int = -1):
|
2022-04-28 22:42:11 +00:00
|
|
|
super().__init__(player, name, address, parent)
|
2023-11-24 05:27:03 +00:00
|
|
|
self.entity_hex = ch_hex
|
2022-04-28 22:42:11 +00:00
|
|
|
|
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
def create_region(world: WitnessWorld, name: str, locat: WitnessPlayerLocations, region_locations=None, exits=None):
|
2022-04-28 22:42:11 +00:00
|
|
|
"""
|
|
|
|
Create an Archipelago Region for The Witness
|
|
|
|
"""
|
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
ret = Region(name, world.player, world.multiworld)
|
2022-04-28 22:42:11 +00:00
|
|
|
if region_locations:
|
|
|
|
for location in region_locations:
|
|
|
|
loc_id = locat.CHECK_LOCATION_TABLE[location]
|
|
|
|
|
2023-11-24 05:27:03 +00:00
|
|
|
entity_hex = -1
|
|
|
|
if location in StaticWitnessLogic.ENTITIES_BY_NAME:
|
|
|
|
entity_hex = int(
|
|
|
|
StaticWitnessLogic.ENTITIES_BY_NAME[location]["entity_hex"], 0
|
2022-04-28 22:42:11 +00:00
|
|
|
)
|
|
|
|
location = WitnessLocation(
|
2023-11-24 05:27:03 +00:00
|
|
|
world.player, location, loc_id, ret, entity_hex
|
2022-04-28 22:42:11 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
ret.locations.append(location)
|
|
|
|
if exits:
|
|
|
|
for single_exit in exits:
|
2023-11-24 05:27:03 +00:00
|
|
|
ret.exits.append(Entrance(world.player, single_exit, ret))
|
2022-04-28 22:42:11 +00:00
|
|
|
|
|
|
|
return ret
|