import glob, os, sys, time import cv2, ffmpeg, keyboard, tqdm, numpy as np, pyautogui as pg map_position_url = "https://satisfactory-calculator.com/en/interactive-map#5.5;284600;-215237|gameLayer|" saves_path = max(glob.glob(f"{os.getenv('LOCALAPPDATA')}/FactoryGame/Saved/SaveGames/[0-9]*"), key=os.path.getmtime) count = len([f for f in os.listdir(saves_path) if f.endswith(".sav")]) origin = "https://satisfactory-calculator.com/en/interactive-map#5;0;0|gameLayer|" width, height = pg.screenshot().width, pg.screenshot().height should_exit = False def sleep(t): time.sleep(t) if should_exit: raise KeyboardInterrupt def await_visibility(img, hidden=False): for _ in range(80): try: pg.locateOnScreen(f"buttons/{img}.png", confidence=0.97) if not hidden: return except pg.ImageNotFoundException: if hidden: return sleep(0.5) raise TimeoutError def wait_until_loaded(): for _ in range(50): sleep(0.5) s = pg.screenshot() for x, y in np.ndindex((20, 20)): if s.getpixel((x * width // 20, y * height // 20)) == (221, 221, 221): pass else: return raise TimeoutError def pan_map(x): pg.moveTo(max(0, x), 1) pg.mouseDown() sleep(0.2) pg.moveTo(max(0, -x), 1) sleep(0.2) pg.mouseUp() sleep(0.2) def set_map_position(pos): pg.hotkey("ctrl", "l") pg.typewrite(pos) pg.press("enter") c = pg.pixel(width // 2, height // 2) while pg.pixelMatchesColor(width // 2, height // 2, c): sleep(0.2) wait_until_loaded() def generate_missing_images(missing): for i in tqdm.tqdm(missing, desc="Generating images"): while True: try: await_visibility("upload_icon") if i != 0: pg.click(pg.locateCenterOnScreen("buttons/upload_icon.png", confidence=0.97)) await_visibility("file_upload_dialog") pg.hotkey("shift", "tab") pg.hotkey("shift", "tab") pg.press(["down"] + (["up"] if i == count else ["down"] * (count - i - 2)) + ["enter"]) await_visibility("upload_icon", hidden=True) await_visibility("upload_icon") await_visibility("player_icon") set_map_position(origin) for button in os.listdir("buttons/visibility_buttons"): try: pg.click(pg.locateCenterOnScreen(f"buttons/visibility_buttons/{button}", confidence=0.97)) await_visibility(f"visibility_buttons/{button.split('.')[0]}", hidden=True) except pg.ImageNotFoundException: continue set_map_position(map_position_url) pg.click("buttons/view_fullscreen.png") pg.moveTo(1, 1) sleep(4) wait_until_loaded() pan_map(-60) left = cv2.cvtColor(np.array(pg.screenshot()), cv2.COLOR_RGB2BGR) pan_map(120) right = cv2.cvtColor(np.array(pg.screenshot()), cv2.COLOR_RGB2BGR) cv2.imwrite(f"img/{i}.png", np.hstack([left[:, 60:-60], right[:, -180:-60]])) pg.hotkey("ctrl", "F5") break except (TimeoutError, pg.ImageNotFoundException): pg.keyDown("ctrl") pg.press(["t", "tab", "w", "9"], interval=0.01) pg.keyUp("ctrl") pg.typewrite(origin) pg.press("enter") finally: time.sleep(1) def render_video(): if "timelapse.mp4" in os.listdir(): os.remove("timelapse.mp4") prev_img = cv2.imread(f"img/0.png") video = cv2.VideoWriter("temp/timelapse.avi", 0, 30, (width, height)) mask = np.zeros((height, width)) frames = [] for i in tqdm.tqdm(range(count + 1), desc="Processing frames"): img = cv2.imread(f"img/{i}.png") diff = cv2.cvtColor(cv2.absdiff(prev_img, img), cv2.COLOR_BGR2GRAY) > 10 for f in range(5 if i < count else 120): mask = np.minimum(1, diff + mask * 0.95) frame = (img * (mask[..., None] * 0.4 + 0.6)).astype(np.uint8) video.write(frame) if f % 5 == 0: frames.append(frame) diff = np.zeros((height, width)) prev_img = img for frame in tqdm.tqdm(frames[::-2], desc="Adding reverse frames"): video.write(frame) video.release() ffmpeg.input("temp/timelapse.avi").output("timelapse.mp4", vcodec="libx264", crf=18).run() def exit_handler(callback): if callback.name == "esc": global should_exit should_exit = True def main(): try: pg.PAUSE = 0.01 keyboard.hook(exit_handler) for d in ["buttons/visibility_buttons", "img", "temp"]: os.makedirs(d, exist_ok=True) missing = [i for i in range(count + 1) if not os.path.exists(f"img/{i}.png")][::-1] generate_missing_images(missing) render_video() except KeyboardInterrupt: sys.exit(-1) finally: for file_name in os.listdir("temp"): os.remove(os.path.join("temp", file_name)) if __name__ == "__main__": main()