-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstart_no_mans_walk.py
More file actions
254 lines (203 loc) · 9.18 KB
/
start_no_mans_walk.py
File metadata and controls
254 lines (203 loc) · 9.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
from utils import BASE_DIR, focus_nms, log, send_key
from nms_bot import PLANET_LOAD_SECONDS
import obsws_python as obs
import subprocess
import pyautogui
import argparse
import time
import glob
import sys
import os
# ─────────────────────────────────────────────────────────────
# CONFIG
# ─────────────────────────────────────────────────────────────
WAIT_FOR_MODE_SELECT = 60
WAIT_FOR_GAME_LOAD = 90
VENV_PY = os.path.join(BASE_DIR, "venv", "Scripts", "python.exe")
DEV_SERVER_CMD = [VENV_PY, "dev_server.py"]
TWITCH_BOT_CMD = [VENV_PY, "nms_twitch_bot.py"]
DEV_SERVER_URL = "http://127.0.0.1:5050"
OBS_DIR = r"C:\Program Files\obs-studio"
OBS_EXE = os.path.join(OBS_DIR, "bin", "64bit", "obs64.exe")
OBS_LOG_DIR = os.path.join(os.path.expandvars("%APPDATA%"), "obs-studio", "logs")
OBS_INIT_WAIT = 10
OBS_RETRY_WAIT = 10
OBS_MAX_RETRIES = 5
VIRTUAL_AUDIO_DEVICE = "VB-Audio Virtual Cable"
SOUNDVOLUMEVIEW_PATH = r"C:\NoMansWalk\utilities\SoundVolumeView\SoundVolumeView.exe"
NMS_EXE_NAME = "NMS.exe"
NMS_LAUNCH_TIMEOUT = 90
NMS_POLL_INTERVAL = 3
NMS_MAX_RETRIES = 3
# ─────────────────────────────────────────────────────────────
# OBS
# ─────────────────────────────────────────────────────────────
def is_process_running(process_name):
import psutil
for proc in psutil.process_iter(["name"]):
if process_name.lower() in (proc.info["name"] or "").lower():
return True
return False
def _obs_log_since(since):
logs = glob.glob(os.path.join(OBS_LOG_DIR, "*.txt"))
recent = [p for p in logs if os.path.getmtime(p) >= since]
pool = recent or logs
return max(pool, key=os.path.getmtime) if pool else None
def _obs_log_has(log_path, *fragments):
try:
content = open(log_path, encoding="utf-8", errors="ignore").read().lower()
return any(f.lower() in content for f in fragments)
except OSError:
return False
def _wait_for_obs_ready(ws, timeout=30, interval=2):
deadline = time.time() + timeout
while time.time() < deadline:
try:
ws.get_version()
return True
except Exception:
time.sleep(interval)
return False
def _start_stream_with_retry(ws, retries=5, interval=2):
for i in range(retries):
try:
ws.start_stream()
return True
except Exception as e:
if "207" in str(e) or "not ready" in str(e).lower():
log(f"OBS not ready yet, retrying in {interval}s... ({i+1}/{retries})")
time.sleep(interval)
else:
raise
return False
def start_obs():
"""Launch OBS and wait for the stream to be live."""
if is_process_running("obs64.exe"):
log("OBS is already running.")
return
obs_args = [OBS_EXE, "--multi", "--minimize-to-tray",
"--disable-missing-files-check", "--disable-updater"]
for attempt in range(1, OBS_MAX_RETRIES + 1):
log(f"Starting OBS (attempt {attempt}/{OBS_MAX_RETRIES})...")
launch_time = time.time()
subprocess.Popen(obs_args, cwd=os.path.dirname(OBS_EXE),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(OBS_INIT_WAIT)
obs_log = _obs_log_since(launch_time)
if obs_log and _obs_log_has(obs_log, "nvenc not supported",
"encoder type 'obs_nvenc_h264_tex' not available",
"failed to initialize module 'obs-nvenc.dll'"):
log(f"NVENC failure detected (attempt {attempt}), retrying in {OBS_RETRY_WAIT}s...")
subprocess.run(["taskkill", "/F", "/IM", "obs64.exe"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(OBS_RETRY_WAIT)
continue
ws = obs.ReqClient(host="localhost", port=4455, password="")
if not _wait_for_obs_ready(ws):
log("ERROR: OBS websocket connected but OBS never became ready.")
return
if not _start_stream_with_retry(ws):
log("ERROR: Failed to start stream after retries.")
return
log("OBS stream live.")
return
log(f"ERROR: OBS failed to start with NVENC after {OBS_MAX_RETRIES} attempts.")
def set_nms_audio_device():
"""Route NMS audio output to the virtual cable so only game audio hits the stream."""
try:
subprocess.run(
[SOUNDVOLUMEVIEW_PATH, "/SetAppDefault", VIRTUAL_AUDIO_DEVICE, "1", "NMS.exe"],
shell=True,
)
log(f"Set NMS audio output to {VIRTUAL_AUDIO_DEVICE}.")
except Exception as e:
log(f"Failed to set NMS audio device: {e}")
# ─────────────────────────────────────────────────────────────
# NMS helpers
# ─────────────────────────────────────────────────────────────
def launch_nms_with_retry():
"""
Launch NMS via pymhf and block until NMS.exe appears in the process list.
Retries up to NMS_MAX_RETRIES times to handle Steam error 83.
Raises RuntimeError if the game never starts.
"""
for attempt in range(1, NMS_MAX_RETRIES + 1):
log(f"Launching NMS (attempt {attempt}/{NMS_MAX_RETRIES})...")
proc = subprocess.Popen(
[os.path.join("venv", "Scripts", "pymhf.exe"), "run", "nmspy"],
cwd=BASE_DIR,
)
deadline = time.time() + NMS_LAUNCH_TIMEOUT
while time.time() < deadline:
if is_process_running(NMS_EXE_NAME):
log(f"NMS.exe confirmed running (PID {proc.pid}).")
return proc
time.sleep(NMS_POLL_INTERVAL)
# NMS.exe never appeared — kill the launcher and try again
log(f"NMS.exe did not appear within {NMS_LAUNCH_TIMEOUT}s "
f"(possible Steam error 83). Terminating launcher and retrying...")
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
raise RuntimeError(f"NMS failed to launch after {NMS_MAX_RETRIES} attempts.")
def teleport_to_new_planet():
"""Focus NMS, send the teleport key, and wait for the planet to load."""
log("Teleporting to new planet before stream starts...")
hwnd, _ = focus_nms()
if not hwnd:
log("WARNING: Could not focus NMS for teleport — skipping.")
return
send_key("o", 0.1)
log(f"Teleport key sent. Waiting {PLANET_LOAD_SECONDS}s for planet to load...")
time.sleep(PLANET_LOAD_SECONDS)
log("Planet load wait complete.")
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--mode", choices=["dev", "twitch"], default="dev")
return p.parse_args()
# ─────────────────────────────────────────────────────────────
# MAIN
# ─────────────────────────────────────────────────────────────
def main():
args = parse_args()
control_mode = args.mode
if control_mode == "twitch":
log("Starting OBS before NMS so Game Capture hook attaches cleanly...")
# set_nms_audio_device()
start_obs()
time.sleep(10) # let OBS fully settle before NMS creates its DX context
nmspy_proc = launch_nms_with_retry()
log(f"NMS process started (PID {nmspy_proc.pid})")
log(f"Waiting {WAIT_FOR_MODE_SELECT}s...")
time.sleep(WAIT_FOR_MODE_SELECT)
log("Sending quick_load key (F9)...")
send_key("f9", 0.1)
log(f"Waiting {WAIT_FOR_GAME_LOAD}s for game load...")
time.sleep(WAIT_FOR_GAME_LOAD)
log("Disabling HUD via hud_toggle mod...")
send_key("f5", 0.1)
log("Teleporting to starting planet...")
teleport_to_new_planet()
log("Toggling music with the 'm' key...")
send_key("m", 0.1)
if control_mode == "twitch":
log("Starting Twitch bot...")
proc = subprocess.Popen(TWITCH_BOT_CMD, cwd=BASE_DIR)
log(f"Twitch bot started (PID {proc.pid})")
else:
log("Dev mode — skipping OBS and Twitch bot.")
log("Starting dev server...")
proc = subprocess.Popen(DEV_SERVER_CMD, cwd=BASE_DIR)
log(f"Dev server started (PID {proc.pid})")
log("Startup sequence complete.")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
log("Interrupted by user.")
sys.exit(0)
except pyautogui.FailSafeException:
log("PyAutoGUI failsafe triggered.")
sys.exit(1)