123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
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()