def list_proper_tag_shortcuts( args: argparse.Namespace) -> Tuple[bool, List[str]]: """Retrieves all shortcuts and their associated tag, or the specified shortcut(s) and their associated tag, depending on arguments. Returns a tuple containing a boolean at the first index to show whether this function failed or succeeded, and a list of messages at the second index. This will not list built-in shortcuts. You can find those by using the 'reverse' flag to see what tags can be called using what shortcuts. """ messages = [] # Get all tag conversions shortcut_dict = { **read_lines_into_dict("./info/recruitops/tagShortcuts.txt", ) } # To translate from zh tag to eng tag rev_translation_dict = read_lines_into_dict( "./info/recruitops/formattedTagConversions.txt", reverse=True) messages = ["\n\nShortcuts\n"] # If specified, find all the shortcuts if args.mode == "all": if len(shortcut_dict.items()) == 0: return (False, ["\n\nNo shortcuts found!"]) else: for key, value in shortcut_dict.items(): messages.append(f"{key:20}---> {rev_translation_dict[value]}") else: if len(args.shortcut) == 0: return (False, ["\n\nNo shortcuts provided."]) # Make sure the tag exists for shortcut in args.shortcut: if shortcut not in shortcut_dict.keys(): return (False, [f"\n\nCould not find shortcut '{shortcut}'."]) # Format the specific shortcuts for shortcut in args.shortcut: messages.append(f"{shortcut:20} ---> " + f"{rev_translation_dict[shortcut_dict[shortcut]]}") return (True, messages)
def parse_info_from_json(args, operator_dict, operator_key): """Gets information for a certain operator from various JSON files, and return an Operator object with the necessary information based on the flags in args. This function assumes a check was already done to ensure the operator exists in the JSON files. Like the Gamepress function, this function independantly gathers the barebones information (name, rarity, description, etc.), but then creates a 'conditional list' (That is, consisting of a function to call for each property, a conditional (usually a flag), and arguments). That is then passed to another function that matches those conditionals, calls the appropriate, necessary functions which return information, which is then assigned to the Operator object that is to be returned. """ description_text = filter_description(operator_dict["description"]) formatted_json_prof = read_lines_into_dict( "./info/scraper/formattedJsonProfessions.txt") # Set up the operator object with the good fetches operator = Operator( operator_dict["name"], operator_dict["rarity"] + 1, formatted_json_prof[operator_dict["profession"].title()], [ description_text + "\n", operator_dict["itemUsage"] + "\n", operator_dict["itemDesc"] + "\n\n" ], operator_dict["tagList"], ) # This is repeated but I feel that's fine for abstraction if args.vskills: skill_tiers_to_check = ([1, 7, 10] if operator_dict["rarity"] + 1 > 3 else [1, 7]) else: skill_tiers_to_check = ([10] if operator_dict["rarity"] + 1 > 3 else [7]) check_skills = args.skills or args.vskills conditionals = [ [ "skills", check_skills, parse_skills, [operator_dict, skill_tiers_to_check] ], ["talent", args.talent, parse_talents, [operator_dict]], ["base skills", args.base, parse_base_skills, [operator_key]], ] stats_requirements = [args.info, create_stats_dict, [operator_dict]] set_operator_properties(args, conditionals, stats_requirements, operator) return operator
def parse_base_skills(operator_key): """Using an operator's key in the info JSON, finds and assembles a list of strings that contain a formatted description of the operator's base skills, and returns them. Since the base skills info is split across three JSON files, we use the key of each operator to find all the base skills of said operator using one JSON. Then, we find the details of each of the base skills using another JSON, and format those details to form the final message list. This function will make 2 other requests to other JSON files in order to properly fetch base skills. """ # We'll have to load in two seperate jsons... base_skills_json, riic_json = get_base_jsons() # If the jsons fail to load, or if the key can't be found in the # base skills json, we have to quit so our program # doesn't kill itself. if base_skills_json == {} or riic_json == {}: return ["\n\nBase Skills\nBase skill JSONs failed to load!"] if operator_key not in base_skills_json["chars"].keys(): return ["\n\nBase Skills\nCould not find matching base skill(s)!"] messages = [] messages.append("\n\nBase Skills\n") # Since we want to remain consistent with gamepress description, # we have a file that converts the shorter room names to the proper # room names that we'll be displaying. formatted_json_rooms = read_lines_into_dict( "./info/scraper/formattedJsonRooms.txt") char = base_skills_json["chars"][operator_key] # Looks messy, but needed for traversing the jsons for bchar in char["buffChar"]: if len(bchar["buffData"]) > 0: for bskill in bchar["buffData"]: # We're just trying to replicate what is got # from gamepress.gg bskill_info = riic_json[bskill["buffId"]] zh_bskill_info = \ base_skills_json["buffs"][bskill["buffId"]] messages.append( bskill_info["name"] + " " + "Lvl: " + str(bskill["cond"]["level"]) + " " + "(" + zh_bskill_info["buffName"] + ")" + " " + "Room Type: " + formatted_json_rooms[zh_bskill_info["roomType"].title()] + " " + "E" + str(bskill["cond"]["phase"])) messages.append(" " + bskill_info["desc"] + "\n") return messages
def delete_tag_shortcut(args: argparse.Namespace) -> None: """Either deletes a shortcut (or shortcuts) from the list of shortcuts, or deletes all the shortcuts from the list of shortcuts. Returns nothing as this function will print directly to the screen. """ spinner = Halo(text="Deleting...", spinner="dots", color="red") spinner.start() # Load dicts shortcut_dict = read_lines_into_dict("./info/recruitops/tagShortcuts.txt") # Make sure the shortcuts exist if they are included if args.mode == "select": if len(args.shortcut) == 0: spinner.fail("Failed.") sys.stdout.write(f"\n\nNo shortcuts were provided!\n\n") return for shortcut in args.shortcut: if shortcut not in shortcut_dict.keys(): spinner.fail("Failed.") sys.stdout.write( f"\n\nCould not find the shortcut '{shortcut}'!\n\n") return # Only one file IO needed (disregarding the read_lines_into_dict) # Actually remove tags with open("./info/recruitops/tagShortcuts.txt", "w", encoding="utf8") as f: # If you open a file as w and do nothing, it simply clears # everything in the file. We'll simply check if args.all # was specified, and if it is, do nothing. # # Otherwise, actually loop through the dict and don't delete # everything. if args.mode == "select": for key, value in shortcut_dict.items(): # Skip any tag that matches the shortcut, basically # deleting it. if key in args.shortcut: continue # oh no a bad continue f.write(key + " " + value + "\n") # Write success message spinner.succeed("Success!") if args.mode == "all": sys.stdout.write("\n\nSuccessfully deleted all tags!\n\n") else: sys.stdout.write( # this is just to keep it under line limit "\n\nSuccessfully deleted " + ", ".join(args.shortcut) + "!\n\n")
def initialize_operator_list() -> Optional[List[Type[TaggedOperator]]]: """Initializes a list of TaggedOperators with names and tags and returns said list. This function uses a recruitment json created by Aceship to find each operator's tags, so if this function is unable to find the json, it will return None which should be caught. Otherwise, it will generate a list of TaggedOperator objects, each with a name, rarity, and recruitment tags. Note that hidden operators (globalHidden or hidden) will not be included in this list. """ operatortags_rawjson = scrape_json( read_line_from_file("./info/recruitops/recruitTagJsonUrl.txt")) # with open("tags_zh.json", "r", encoding="utf8") as f: # operatortags_rawjson = json.load(f) # debug if operatortags_rawjson is None: return None # Getting the json content returns a list, so we call it a # list appropriately operatortags_list = operatortags_rawjson.json() # operatortags_list = operatortags_rawjson # debug operator_list = [] name_replacements = read_lines_into_dict( "./info/recruitops/operatorNameReplacements.txt") # initialize an easy to access list of operators and their tags # # Some operators aren't available in HH and thus they are labelled # as 'hidden' or 'globalHidden' in the json. for operator in operatortags_list: if (operator["hidden"] or ("globalHidden" in operator.keys() and operator["globalHidden"])): continue operator_list.append( TaggedOperator( operator["name_en"], operator["level"], operator["tags"] + [operator["type"].rstrip()]) if operator["name_en"] not in name_replacements else TaggedOperator( name_replacements[operator["name_en"]], operator["level"], operator["tags"] + [operator["type"].rstrip()])) return operator_list
def get_operator_dict(operator): """Searches the Aceship character JSON for a specified operator, and returns the associated character dict and the character key in the JSON if found. If the operator was not found, return an empty dict and None for the key, indicating that the scraper should try using Gamepress instead of the JSON, as the character is not in the JSON yet. """ # Since the JSON I first use to find info uses # properly formatted names, I have to convert any name # to a properly formatted one # TODO: Does this break DRY? This appears in gamepress too replacement_names = read_lines_into_dict( "./info/scraper/jsonOperatorReplacements.txt") formatted_name = operator.replace("-", " ").title() proper_name = (formatted_name if formatted_name not in replacement_names.keys() else replacement_names[formatted_name]) # with open("character_table.json", "r", encoding="utf8") as f: # operator_raw_json = json.load(f) # debug operator_raw_json = scrape_json( read_line_from_file("./info/scraper/operatorJsonUrl.txt")) if operator_raw_json is None: return {}, None operator_dict = {} operator_key = None operator_json = operator_raw_json.json() # operator_json = operator_raw_json #debug for operator in operator_json.keys(): # So that names like "SilverAsh" don't screw up the parser, # we take the key and convert it to the title form (Silverash) if operator_json[operator]["name"].title() == proper_name: operator_key = operator operator_dict = operator_json[operator] break return operator_dict, operator_key
def scrape_for_operator(operator): """Sends a GET request for a certain operator and returns the Response object if status code is 200. Returns None (as per scrape_website() implementation) if server responds with a different code. """ url_replacement_names = read_lines_into_dict( "./info/scraper/urlOperatorReplacements.txt") operator_url = read_line_from_file("./info/scraper/url.txt") operator_url = ( operator_url + "operator/" + operator if operator not in url_replacement_names.keys() else operator_url + "operator/" + url_replacement_names[operator] ) return scrape_website(operator_url)
def get_info_from_gamepress(args, operator_name): """Gets information for a certain operator from a Gamepress page, and return an Operator object with the necessary information based on the flags in args. If no Gamepress page is found with the specified operator's name (or the page is down), this function will return None. This function independantly gathers the barebones information (name, rarity, description, etc.), but then creates a 'conditional list' (That is, consisting of a function to call, a conditional (usually a flag), and arguments). That is then passed to another function that matches those conditionals, calls the appropriate, necessary functions which return information, which is then assigned to the Operator object that is to be returned. """ response = scrape_for_operator(operator_name) if response is not None: # response succeeds src = response.content images_dict = read_lines_into_dict("./info/scraper/imageToText.txt") replacement_names = read_lines_into_dict( "./info/scraper/jsonOperatorReplacements.txt") soup = BeautifulSoup(src, "lxml") # soup = BeautifulSoup(open("debug.html", "r", encoding="utf-8"), "lxml") # debugging # Finding the default information that should be displayed # for every operator (eg. tags, description, etc.) tags = list( map(lambda souptxt: souptxt.text.strip(), soup.find_all("div", "tag-title"))) # We can find the rarity of an operator by finding the div # named rarity-cell and counting how many images of stars # are in it rarity = len(soup.find("div", "rarity-cell").find_all("img")) profession_text = (soup.find("div", "profession-title").text.strip()) desc = soup.find_all("div", "description-box") desc_text = (["No proper description."] if (len(desc) < 3) else [ "".join(find_siblings_of_breakpoint(desc[item])).strip() + "\n" for item in range(3) ]) # Since the alternative JSON I use to find stats may have # another name for an operator, I have to convert any name # to a proper one recognized by that specific json formatted_name = operator_name.replace("-", " ").title() proper_name = (formatted_name if formatted_name not in replacement_names.keys() else replacement_names[formatted_name]) operator = Operator(proper_name, rarity, profession_text, desc_text, tags) # Any optional messages/properties are stored in # operator.properties for convenience and also to make sure # that printing properties doesn't take 50 lines of code. # Also, we can reuse the properties this way as it is stored # in a compact location. # Skills are different; since we have a mutually exclusive group # we need to format input a tiny bit before we continue # with skills info gathering. if args.vskills: skill_tiers_to_check = ([ "skill-upgrade-tab-1", "skill-upgrade-tab-7", "skill-upgrade-tab-10" ] if rarity > 3 else [ "skill-upgrade-tab-1", "skill-upgrade-tab-7" ]) else: skill_tiers_to_check = (["skill-upgrade-tab-10"] if rarity > 3 else ["skill-upgrade-tab-7"]) check_skills = args.skills or args.vskills # Taking advantage of python's functional programming paradigms # to adhere to DRY principles # TODO: is this even good practice??? # I'm trying to adhere to DRY principles but this makes me # start to sweat and foam at the mouth conditionals = [ [ "skills", check_skills, find_skills, [soup, skill_tiers_to_check] ], ["talent", args.talent, find_talents, [soup, images_dict]], ["base skills", args.base, find_base_skills, [soup, images_dict]], ] stats_requirements = [ args.info, create_stats_json, [soup, proper_name] ] # Set the operator object's properties based on conditional # list set_operator_properties(args, conditionals, stats_requirements, operator) else: # The request failed, operator not found operator = None return operator
def find_recruitment_combos(args: argparse.Namespace) -> None: """Taking the specified namespace of arguments, this function will determine combinations of tags, find operators that match those combinations, and print to the screen a formatted list of combinations and operators, sorted by value bottom-to-top.""" spinner = Halo(text="Fetching...", spinner="dots", color="magenta") spinner.start() op_list = initialize_operator_list() if op_list is None: spinner.fail("Failed.") sys.stdout.write( "\n\nThe tag JSON could not be fetched! Try again later.") else: spinner.text = "Calculating..." spinner.color = "yellow" tag_dict = initialize_tag_dictionary(op_list) # Get both a proper translation from en to zh dict with the # new tag shortcuts and the premade tags # and a reversed dict initialized for proper tag conversion translation_dict = { **read_lines_into_dict("./info/recruitops/tagConversions.txt"), **read_lines_into_dict("./info/recruitops/tagShortcuts.txt") } reversed_translation_dict = read_lines_into_dict( "./info/recruitops/formattedTagConversions.txt", reverse=True) # Take in the user tags and find their proper, translated names # so that they can be used with the json. proper_tags = [] # TODO: this tag process could probably be more optimized for tag in args.tags: if tag.lower() in translation_dict.keys(): proper_tags.append(translation_dict[tag.lower()]) else: # TODO: exit nicer raise Exception(f"The tag '{tag.lower()}' does not exist.") # Find all possible combinations of each tag combo all_matches = get_all_combinations(proper_tags, tag_dict, translation_dict, reversed_translation_dict) # Sort based on priority and format all the possible # combinations. # # Consists of all the tag combinations and results, sorted # by priority. all_sorted_selection = sorted(all_matches, key=lambda s: s.priority) messages = format_selections(args, all_sorted_selection) # Print the recruitment results spinner.succeed("Success!") sys.stdout.write("\n\nRecruitment Results\n\n") # padding sys.stdout.write("Note: the lower down the tag collection, " + "the better the tags.\n\n\n") # padding if len(messages) <= 0: sys.stdout.write("Could not find any recruitment results.\n") else: for msg in messages: sys.stdout.write(msg + "\n") sys.stdout.write("\n") # padding
def create_tag_shortcut(args: argparse.Namespace) -> None: """Creates a new shortcut for a tag for later use. If the shortcut already exists in the shortcut list, overwrite the old shortcut with the new tag. If this happens, the overwriting process time is proportional to the size of the shortcut list, as the function will overwrite and rewrite the entire list. Prints directly to the screen. If the tag specified was not found, return early. Returns nothing. """ spinner = Halo(text="Adding...", spinner="dots", color="red") spinner.start() # Load the dicts needed to accomplish this task shortcut_dict = read_lines_into_dict("./info/recruitops/tagShortcuts.txt") translation_dict = read_lines_into_dict( "./info/recruitops/tagConversions.txt") # Make sure the tag specified actually exists and that the # shortcut name doesn't already exist in the translation dict. if args.tag in translation_dict.keys(): tag_name = translation_dict[args.tag] else: spinner.fail("Failed.") sys.stdout.write(f"\n\nCould not find the tag '{args.tag}'!\n\n") return if args.shortcut in translation_dict.keys(): spinner.fail("Failed.") sys.stdout.write( f"\n\nShortcut '{args.shortcut}' cannot be changed!\n\n") return # Actually create the shortcut to a tag if args.shortcut not in shortcut_dict.keys(): with open("./info/recruitops/tagShortcuts.txt", "a", encoding="utf8") as f: f.write(args.shortcut + " " + tag_name + "\n") else: # If the shortcut exists, we'll have to override the tag # and it. key_list, val_list = zip(*shortcut_dict.items()) line_num = key_list.index(args.shortcut) index = 0 with open("./info/recruitops/tagShortcuts.txt", "w", encoding="utf8") as f: while index < len(key_list): if index == line_num: # override the old value with new one f.write(args.shortcut + " " + tag_name + "\n") else: # preserve the old values as they're unrelated f.write(key_list[index] + " " + val_list[index] + "\n") index += 1 # Write success message spinner.succeed("Success!") sys.stdout.write( # this is just to keep it under line limit "\n\nSuccessfully added shortcut '" + args.shortcut + "' for tag '" + args.tag + "'!\n\n")
def list_reversed_tag_shortcuts( args: argparse.Namespace) -> Tuple[bool, List[str]]: """Retrieves either all tags and their shortcuts or a specified tag and its shortcut, formats them, and then prints to the screen. Returns nothing. Returns a tuple containing a boolean at the first index to show whether this function failed or succeeded, and a list of messages at the second index. The user is able to input a shortcut to a tag as a reference to that tag. However, as a result of implementation, this function will take more time the more shortcuts there are. If the shortcut to the tag is built-in, a star will be put next to it indicating that this tag cannot be removed or overwritten. """ messages = ["\n\nTag Shortcuts\n"] # Load dictionaries, this time in reverse # # We avoid combining because the values are lists, and I don't # feel like combining them right now # TODO: these dict calls can probably be optimized rev_shortcut_dict = read_lines_into_dict( "./info/recruitops/tagShortcuts.txt", reverse=True, overwrite=False) rev_conversion_dict = read_lines_into_dict( "./info/recruitops/tagConversions.txt", reverse=True, overwrite=False) # to see what tag the user inputted # this could take some time depending on the amount of shortcuts # though... but I think it's better this way for sake of efficiency conversion_dict = { **read_lines_into_dict("./info/recruitops/tagConversions.txt", ), **read_lines_into_dict("./info/recruitops/tagShortcuts.txt", ) } rev_translation_dict = read_lines_into_dict( "./info/recruitops/formattedTagConversions.txt", reverse=True) # Add stars after the important shortcuts that can't be # deleted or overwritten. for key in rev_conversion_dict: rev_conversion_dict[key] = (list( map(lambda sc: sc + " *", rev_conversion_dict[key]))) # Get all tag-shortcut connections if needed if args.mode == "all": for key in rev_translation_dict: # It may seem redundant to go from tag to key and then key to # tag, but this formats the tag and also has an efficiency # of O(1), so it's fine. # # We know that every tag has at least one builtin shortcut, # so no need to check if there is a shortcut for a tag. # # We're also doing some checks here to make sure that # everything is properly formatted if there's only one # tag for it, and that commas are in a correct place, # and that shortcuts exist for that tag, and that # shortcuts are formatted well... messages.append(f"{rev_translation_dict[key]}\n" + (",\n".join(rev_conversion_dict[key]) if key not in rev_shortcut_dict.keys() else (",\n".join(rev_conversion_dict[key] + rev_shortcut_dict[key]))) + "\n") # Get specified tags else: # Make sure there are shortcuts if len(args.shortcut) == 0: return (False, ["\n\nNo shortcuts provided."]) # Make sure that the tag actually exists for tag in args.shortcut: if tag.lower() not in conversion_dict.keys(): return (False, [f"\n\nCould not find tag '{tag}'."]) # Get the actual tags-shorcut connections requested for tag in args.shortcut: # It may seem redundant to go from tag to key and then key # to tag, but this formats the tag and also has an # efficiency of O(1), so it's fine. key = conversion_dict[tag] messages.append(f"{rev_translation_dict[key]}\n" + (",\n".join(rev_conversion_dict[key]) if key not in rev_shortcut_dict.keys() else (",\n".join(rev_conversion_dict[key] + rev_shortcut_dict[key]))) + "\n") # Get rid of the \n that is always present at the last element # for consistant formatting messages[-1] = messages[-1].rstrip() return (True, messages)