async def click_character_select_button(check_open_state: Union[bool, None] = None): await async_sleep(0.5) button_x, button_y = ( CFG.character_select_button_position["x"], CFG.character_select_button_position["y"], ) ACFG.moveMouseAbsolute(x=button_x, y=button_y) ACFG.left_click() if check_open_state is not None: log(f"Checking that character select is {'open' if check_open_state else 'closed'}" ) await async_sleep(2) success = False for _ in range(CFG.character_select_max_close_attempts): if await check_character_menu(check_open_state): success = True break else: ACFG.moveMouseAbsolute(x=button_x, y=button_y) ACFG.left_click() if not success: log("Unable to toggle character menu!\nNotifying dev...") notify_admin( "Failed to find toggle character menu in `click_character_select_button` loop" ) sleep(2) log("") else: await async_sleep(0.5)
async def check_if_should_change_servers( original_current_server_id: str = "N/A", ) -> Tuple[bool, str]: current_server_id = ("" if original_current_server_id == "N/A" else original_current_server_id) current_server_playing = 0 highest_player_server_playing = 0 log("Querying Roblox API for server list") url = f"https://games.roblox.com/v1/games/{CFG.game_id}/servers/Public" try: response = get(url, timeout=10) except Exception: return False, "[WARN] Could not poll Roblox servers. Is Roblox down?" if response.status_code == 200: log("Finding best server and comparing to current...") response_result = response.json() servers = response_result["data"] if current_server_id == "N/A": current_server_id = "" for server in servers: server_id = server.get("id", "undefined") if server.get("playerTokens") is None: server_playing = -1 else: server_playing = len(server["playerTokens"]) if server_id == "undefined" or server_playing == -1: notify_admin( f"Handled Error in `check_if_should_change_servers`\nServers:\n`{servers}`\nProblem:\n`{server}`" ) continue if current_server_id == server_id: current_server_id = server_id current_server_playing = server_playing elif ("playerTokens" in server and server_playing > highest_player_server_playing): highest_player_server_playing = server_playing log("") if current_server_id == "" or current_server_id == "undefined": if highest_player_server_playing == 0: return False, "[WARN] Could not poll Roblox servers. Is Roblox down?" return_message = ( f"[WARN] Could not find FumoCam. Are we in a server?\n" f"Original Server ID: {original_current_server_id}\n" f"Detected Server ID: {current_server_id}") return True, return_message elif (current_server_playing < CFG.player_switch_cap and (current_server_playing + CFG.player_difference_to_switch) < highest_player_server_playing): difference = highest_player_server_playing - current_server_playing return ( True, f"[WARN] There is a server with {difference} more players online.", ) else: return False, "" return False, "[WARN] Could not poll Roblox servers. Is Roblox down?"
async def dev(self, ctx: commands.Context): args = await self.get_args(ctx) if not args: await ctx.send( "[Specify a message, this command is for emergencies! (Please do not misuse it)]" ) return msg = " ".join(args) notify_admin(f"{ctx.message.author.display_name}: {msg}") await ctx.send( "[Notified dev! As a reminder, this command is only for emergencies. If you were unaware of this and used" " the command by mistake, please write a message explaining that or you may be timed-out/banned.]" )
async def manual_dev_command(self, message: TwitchMessage): args = message.content.split(" ", 1) if len(args) < 2: await message.channel.send( "[Specify a message, this command is for emergencies! (Please do not misuse it)]" ) return msg = args[-1] notify_admin(f"{message.author}: {msg}") await message.channel.send( "[Notified dev! As a reminder, you have been blacklisted by a trusted member, so your" " controls will not work. If you feel this is in error, use this commmand.]" )
async def open_roblox_with_selenium_browser(js_code: str) -> bool: log("Opening Roblox via Browser...") try: with open(CFG.browser_cookies_path, "r", encoding="utf-8") as f: cookies = json.load(f) except FileNotFoundError: print("COOKIES PATH NOT FOUND, INITIALIZE WITH TEST FIRST") log("") return False options = webdriver.ChromeOptions() options.add_argument(f"--user-data-dir={CFG.browser_profile_path}") driver = webdriver.Chrome(options=options, executable_path=str(CFG.browser_driver_path)) driver.get(CFG.game_instances_url) for cookie in cookies: try: driver.add_cookie(cookie) except Exception: print(f"ERROR ADDING COOKIE: \n{cookie}\n") driver.refresh() driver.execute_script(js_code) sleep_time = 0.25 success = False log("Verifying Roblox has opened...") for _ in range(int(CFG.max_seconds_browser_launch / sleep_time)): crashed = await do_crash_check(do_notify=False) active = is_process_running(CFG.game_executable_name) if not crashed and active: success = True break await async_sleep(sleep_time) try: driver.quit() kill_process(CFG.browser_driver_executable_name) kill_process(CFG.browser_executable_name) except Exception: print(format_exc()) if not success: log("Failed to launch game. Notifying Dev...") notify_admin("Failed to launch game") await async_sleep(5) log("") return False log("") return True
async def auto_nav(location: str, do_checks: bool = True, slow_spawn_detect: bool = True): log_process("AutoNav") if do_checks: await check_active(force_fullscreen=False) await async_sleep(0.5) await send_chat( f"[AutoNavigating to {CFG.nav_locations[location]['name']}!]") if not CFG.collisions_disabled: log("Disabling collisions") await toggle_collisions() await async_sleep(0.5) await async_sleep(1) log("Respawning") await respawn_character(notify_chat=False) await async_sleep(7) log("Zooming out to full scale") ACFG.zoom(zoom_direction_key="o", amount=105) spawn = spawn_detection_main(CFG.resources_path, slow=slow_spawn_detect) if spawn == "ERROR": log("Failed to detect spawn!\n Notifying Dev...") notify_admin("Failed to find spawn in `auto_nav`") sleep(5) return if spawn == "comedy_machine": comedy_to_main() elif spawn == "tree_house": treehouse_to_main() await async_sleep(1) if location == "shrimp": main_to_shrimp_tree() elif location == "ratcade": main_to_ratcade() elif location == "train": main_to_train() elif location == "classic": main_to_classic() elif location == "treehouse": main_to_treehouse() log("Zooming in to normal scale") default_zoom_in_amount = CFG.zoom_max - CFG.zoom_default zoom_in_amount = CFG.nav_post_zoom_in.get(location, default_zoom_in_amount) ACFG.zoom(zoom_direction_key="i", amount=zoom_in_amount) log(f"Complete! This is experimental, so please re-run \n'!nav {location}' if it didn't work." ) await async_sleep(3)
async def mute_toggle(set_mute: Union[bool, None] = None): log_process("In-game Mute") desired_mute_state = not CFG.audio_muted if set_mute is not None: # If specified, force to state desired_mute_state = set_mute desired_volume = 0 if desired_mute_state else 100 log_msg = "Muting" if desired_mute_state else "Un-muting" log(log_msg) sc_exe_path = str(CFG.resources_path / CFG.sound_control_executable_name) os.system( # nosec f'{sc_exe_path} /SetVolume "{CFG.game_executable_name}" {desired_volume}' ) # Kill the process no matter what, race condition for this is two songs playing (bad) kill_process(executable=CFG.vlc_executable_name, force=True) if desired_mute_state: # Start playing music copyfile( CFG.resources_path / OBS.muted_icon_name, OBS.output_folder / OBS.muted_icon_name, ) vlc_exe_path = str(CFG.vlc_path / CFG.vlc_executable_name) music_folder = str(CFG.resources_path / "soundtracks" / "overworld") Popen( f'"{vlc_exe_path}" --playlist-autostart --loop --playlist-tree {music_folder}' ) output_log("muted_status", "In-game audio muted!\nRun !mute to unmute") sleep(5) # Give it time to load VLC else: # Stop playing music try: if os.path.exists(OBS.output_folder / OBS.muted_icon_name): os.remove(OBS.output_folder / OBS.muted_icon_name) except OSError: log("Error, could not remove icon!\nNotifying admin...") async_sleep(2) notify_admin("Mute icon could not be removed") log(log_msg) output_log("muted_status", "") CFG.audio_muted = desired_mute_state await check_active() log_process("") log("")
async def blacklist(self, ctx: commands.Context): if ctx.message.author.name.lower() not in CFG.vip_twitch_names: await ctx.send("[You do not have permission to run this command!]") args = await self.get_args(ctx) if not args: await ctx.send("[Please specify a user!]") return try: name = args[0].lower() if name[0] == "@": name = name[1:] except Exception: await ctx.send("[Please specify a user!]") return added = False if name not in CFG.twitch_blacklist: CFG.twitch_blacklist.append(name) with open(str(CFG.twitch_blacklist_path), "w") as f: json.dump(CFG.twitch_blacklist, f) added = True await ctx.send( f"['{name}' has {'already' if not added else ''} been blacklisted from interacting with FumoCam.]" ) await async_sleep(1) await ctx.send("[It is recommended you also report them to Twitch, if needed.]") await async_sleep(1) await ctx.send( f"[@{name} if you feel this is in error, please type '!dev unjust ban' in chat." " The dev has already been notified]" ) mod_url = f"<https://www.twitch.tv/popout/becomefumocam/viewercard/{ctx.message.author.name.lower()}>" target_url = ( f"<https://www.twitch.tv/popout/becomefumocam/viewercard/{name.lower()}>" ) notify_admin( f"{ctx.message.author.name} has blacklisted {name}\n{mod_url}\n{target_url}" )
async def check_if_game_loaded() -> bool: game_loaded = False log("Loading into game") for attempt in range(CFG.max_attempts_game_loaded): if await check_ui_loaded(): game_loaded = True break log(f"Loading into game (Check #{attempt}/{CFG.max_attempts_game_loaded})" ) await async_sleep(1) if not game_loaded: log("Failed to load into game.") notify_admin("Failed to load into game") await async_sleep(5) await CFG.add_action_queue(ActionQueueItem("handle_crash")) log("") return False log("") return True
async def event_message(self, message: TwitchMessage): if message.echo: return msg_str = f"[Twitch] {message.author.display_name}: {message.content}" print(msg_str.encode("ascii", "ignore").decode("ascii", "ignore")) log_task = create_task(self.do_discord_log(message)) if message.author.name not in CFG.twitch_blacklist: commands_task = create_task(self.handle_commands(message)) else: if message.content.startswith("!dev"): await self.manual_dev_command(message) if await self.is_new_user(message.author.name): for msg in self.help_msgs: await message.channel.send(msg) await message.channel.send( f"Welcome to the stream {message.author.mention}! " "This is a 24/7 bot you can control with chat commands! See above." ) try: await log_task except Exception: print(traceback.format_exc()) notify_admin(f"```{traceback.format_exc()}```") if message.author.name not in CFG.twitch_blacklist: try: await commands_task except commands.errors.CommandNotFound: pass except Exception: print(traceback.format_exc()) notify_admin(f"```{traceback.format_exc()}```")
async def toggle_collisions() -> bool: log_process( f"{'Enabling' if CFG.collisions_disabled else 'Disabling'} Grief Collisions" ) log("Opening Settings") await check_active(force_fullscreen=False) await async_sleep(1) need_zoom_adjust = False if CFG.zoom_level < CFG.zoom_ui_min_cv: ACFG.zoom("o", CFG.zoom_out_ui_cv) need_zoom_adjust = True if not await click_settings_button(check_open_state=True): notify_admin("Failed to open settings") log("") log_process("") if need_zoom_adjust: ACFG.zoom("i", CFG.zoom_out_ui_cv) return False log(f"Finding {CFG.settings_menu_grief_label} option") await async_sleep(1) if not await ocr_for_settings(): notify_admin("Failed to click settings option") log("") log_process("") if need_zoom_adjust: ACFG.zoom("i", CFG.zoom_out_ui_cv) return False CFG.collisions_disabled = not (CFG.collisions_disabled) collisions_msg = ("" if CFG.collisions_disabled else "[WARN] Griefing/Collisions enabled!") output_log("collisions", collisions_msg) log("Closing Settings") await async_sleep(0.25) if not await click_settings_button(check_open_state=False): notify_admin("Failed to close settings") log("") log_process("") if need_zoom_adjust: ACFG.zoom("i", CFG.zoom_out_ui_cv) return False log("") log_process("") ACFG.resetMouse() if need_zoom_adjust: ACFG.zoom("i", CFG.zoom_out_ui_cv) return True
async def ocr_for_settings(option: str = "", click_option: bool = True) -> bool: desired_option = (CFG.settings_menu_grief_text if not option else option).lower() desired_label = (CFG.settings_menu_grief_label if not option else option).lower() ocr_data = {} found_option = False for attempts in range( CFG.settings_menu_ocr_max_attempts): # Attempt multiple OCRs if not click_option: log(f"Finding '{desired_label.capitalize()}' (Attempt #{attempts}/{CFG.settings_menu_ocr_max_attempts})" ) screenshot = np.array(await take_screenshot_binary(CFG.window_settings)) menu_green = { "upper_bgra": np.array([212, 255, 158, 255]), "lower_bgra": np.array([163, 196, 133, 255]), } green_mask = cv.inRange(screenshot, menu_green["lower_bgra"], menu_green["upper_bgra"]) screenshot[green_mask > 0] = (255, 255, 255, 255) menu_red = { "upper_bgra": np.array([79, 79, 255, 255]), "lower_bgra": np.array([63, 63, 214, 255]), } red_mask = cv.inRange(screenshot, menu_red["lower_bgra"], menu_red["upper_bgra"]) screenshot[red_mask > 0] = (255, 255, 255, 255) gray = cv.cvtColor(screenshot, cv.COLOR_BGR2GRAY) # PyTesseract _, thresh = cv.threshold(gray, 240, 255, cv.THRESH_BINARY) thresh_not = cv.bitwise_not(thresh) kernel = np.ones((2, 1), np.uint8) img = cv.erode(thresh_not, kernel, iterations=1) img = cv.dilate(img, kernel, iterations=1) ocr_data = pytesseract.image_to_data( img, config="--oem 1", output_type=pytesseract.Output.DICT) ocr_data["text"] = [word.lower() for word in ocr_data["text"]] print(ocr_data["text"]) for entry in ocr_data["text"]: if desired_option in entry: desired_option = entry if click_option: log("Found option, clicking") found_option = True break else: return True if found_option: break await async_sleep(0.25) if not found_option: if click_option: log(f"Failed to find '{desired_label.capitalize()}'.\n Notifying Dev..." ) notify_admin(f"Failed to find `{desired_option}`") await async_sleep(5) return False else: # We found the option print(ocr_data) ocr_index = ocr_data["text"].index(desired_option) option_top = ocr_data["top"][ocr_index] option_y_center = ocr_data["height"][ocr_index] / 2 option_y_pos = option_top + option_y_center # The top-offset of our capture window + the found pos of the option desired_option_y = int(CFG.window_settings["top"] + option_y_pos) option_left = ocr_data["left"][ocr_index] option_x_center = ocr_data["width"][ocr_index] / 2 option_x_pos = option_left + option_x_center # The left-offset of our capture window + the found pos of the option desired_option_x = int(CFG.window_settings["left"] + option_x_pos) # Click the option ACFG.moveMouseAbsolute(x=int(desired_option_x), y=int(desired_option_y)) await async_sleep(0.5) ACFG.left_click() await async_sleep(0.5) return True
async def ocr_for_character(character: str = "", click_option: bool = True) -> bool: desired_character = (CFG.character_select_desired.lower() if not character else character.lower()) await async_sleep(0.5) ocr_data = {} last_ocr_text = None times_no_movement = 0 found_character = False scroll_amount = 0 for attempts in range(CFG.character_select_max_scroll_attempts): if click_option: log(f"Scanning list for '{desired_character.capitalize()}'" f" ({attempts}/{CFG.character_select_max_scroll_attempts})") for _ in range( CFG.character_select_scan_attempts): # Attempt multiple OCRs screenshot = np.array(await take_screenshot_binary(CFG.window_character)) gray = cv.cvtColor(screenshot, cv.COLOR_BGR2GRAY) gray, img_bin = cv.threshold(gray, 240, 255, cv.THRESH_BINARY) gray = cv.bitwise_not(img_bin) kernel = np.ones((2, 1), np.uint8) img = cv.erode(gray, kernel, iterations=1) img = cv.dilate(img, kernel, iterations=1) ocr_data = pytesseract.image_to_data( img, config="--oem 1", output_type=pytesseract.Output.DICT) ocr_data["text"] = [word.lower() for word in ocr_data["text"]] print(ocr_data["text"]) for entry in ocr_data["text"]: if desired_character in entry: if click_option: found_character = True break else: return True if found_character: break sleep(0.1) if click_option and found_character: break elif not click_option and not found_character: return False # Do not scroll if last_ocr_text == ocr_data["text"]: times_no_movement += 1 else: times_no_movement = 0 last_ocr_text = ocr_data["text"] if times_no_movement > 3: break # We reached the bottom of the list, OCR isnt changing ACFG.scrollMouse(1, down=True) scroll_amount += 1 sleep(0.4) if not found_character: if click_option: log(f"Failed to find '{desired_character.capitalize()}'.\n Notifying Dev..." ) notify_admin(f"Failed to find `{desired_character}`") sleep(5) return False # We found the character, lets click it ocr_index = ocr_data["text"].index(desired_character) desired_character_height = int(ocr_data["top"][ocr_index] + (ocr_data["height"][ocr_index] / 2)) desired_character_height += CFG.window_character["top"] with open(OBS.output_folder / "character_select.json", "w") as f: json.dump( { "scroll_amount": scroll_amount, "desired_character_height": desired_character_height, }, f, ) CFG.character_select_screen_height_to_click = desired_character_height CFG.character_select_scroll_down_amount = scroll_amount ACFG.moveMouseAbsolute(x=CFG.screen_res["center_x"], y=int(desired_character_height)) sleep(0.5) ACFG.left_click() sleep(0.5) return True
async def check_for_better_server(): last_check_time = time() output_log("last_check_for_better_server", last_check_time) previous_status_text = read_output_log("change_server_status_text") output_log("change_server_status_text", "") log_process("Checking for better server") current_server_id = await get_current_server_id() if current_server_id == "ERROR": for i in range(CFG.max_attempts_better_server): log_process( f"Attempt {i+1}/{CFG.max_attempts_better_server} failed! Retrying better server check..." ) await async_sleep(5) current_server_id = await get_current_server_id() if current_server_id != "ERROR": break if current_server_id == "ERROR": log_process( f"Failed to connect to Roblox API {CFG.max_attempts_better_server} times! Skipping..." ) await async_sleep(5) log_process("") return True if current_server_id == "N/A": for id in list(CFG.game_ids_other.keys()): current_server_id == await get_current_server_id(id) if current_server_id != "N/A": break if current_server_id == "N/A": log_process("Could not find FumoCam in any servers") await CFG.add_action_queue(ActionQueueItem("handle_crash")) return False else: log_process("") log("") return True ( should_change_servers, change_server_status_text, ) = await check_if_should_change_servers(current_server_id) log(change_server_status_text) output_log("change_server_status_text", change_server_status_text) if not should_change_servers: log("PASS! Current server has sufficient players") log("") log_process("") return True elif previous_status_text != change_server_status_text: if "Could not find FumoCam" in change_server_status_text: for attempt in range(CFG.max_attempts_better_server): log(f"Rechecking (attempt {attempt+1}/{CFG.max_attempts_better_server}" ) current_server_id = await get_current_server_id() while current_server_id == "": log_process("Retrying get current server check") await async_sleep(5) current_server_id = await get_current_server_id() ( should_change_servers, change_server_status_text, ) = await check_if_should_change_servers(current_server_id) if "Could not find FumoCam" not in change_server_status_text: break if should_change_servers: notify_admin(change_server_status_text) await CFG.add_action_queue( ActionQueueItem("handle_join_new_server")) else: await CFG.add_action_queue( ActionQueueItem("handle_join_new_server")) log("") log_process("")