Skip to content

Commit dfa40d1

Browse files
feat: Add host auto-discovery and xemu setup improvements (#178)
1 parent 828e766 commit dfa40d1

14 files changed

Lines changed: 1106 additions & 50 deletions

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@ The repository now includes `.run/Run xemu.run.xml`, which launches `scripts\run
184184

185185
If you create a local CLion run configuration that sets the working directory to a build output such as `$CMakeCurrentBuildDir$/xbox`, the Windows wrapper also treats that caller working directory as the xemu target path when no explicit launcher arguments or `MOONLIGHT_XEMU_*` overrides are provided.
186186

187-
The setup script downloads xemu and the emulator support files into `.local/xemu`, then refreshes launcher manifests used by `scripts/run-xemu.sh`. The launcher accepts `MOONLIGHT_XEMU_BUILD_DIR`, `MOONLIGHT_XEMU_ISO_PATH`, `--build-dir <cmake-build-dir>`, `--iso <iso-path>`, or a single positional path that can point at either a build directory or an ISO file. If you do not pass a path, it falls back across available `cmake-build-*` outputs and prefers the newest built ISO.
187+
The setup script downloads xemu and the emulator support files into `.local/xemu`, then refreshes launcher manifests used by `scripts/run-xemu.sh`. Existing files under `.local/xemu` are preserved by default so a local xemu install or support bundle is not overwritten unless you pass `--force`. The launcher accepts `MOONLIGHT_XEMU_BUILD_DIR`, `MOONLIGHT_XEMU_ISO_PATH`, `--build-dir <cmake-build-dir>`, `--iso <iso-path>`, or a single positional path that can point at either a build directory or an ISO file. If you do not pass a path, it falls back across available `cmake-build-*` outputs and prefers the newest built ISO.
188+
189+
When xemu runs with its default user-mode network, multicast mDNS traffic is not forwarded reliably, so automatic host discovery may not find your PC. For reliable discovery, launch xemu with `--network tap --tap-ifname <adapter>` or add the host manually from the Xbox UI.
188190

189191
If you only want the emulator without the ROM/HDD support bundle, run:
190192

@@ -241,6 +243,7 @@ scripts\setup-xemu.cmd --skip-support-files
241243
- Misc.
242244
- [x] Save config and pairing states
243245
- [x] Host pairing
246+
- [x] Auto host discovery
244247
- [ ] Possibly, GPU overclocking, see https://github.com/GXTX/XboxOverclock
245248

246249
<details style="display: none;">

cmake/moonlight-dependencies.cmake

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ macro(MOONLIGHT_PREPARE_COMMON_DEPENDENCIES)
5555
set(MOONLIGHT_NXDK_NET_INCLUDE_DIR "${NXDK_DIR}/lib/net")
5656
set(MOONLIGHT_NXDK_LIBC_EXTENSIONS_DIR "${NXDK_DIR}/lib/xboxrt/libc_extensions")
5757
set(MOONLIGHT_NXDK_LWIP_POSIX_COMPAT_DIR "${NXDK_DIR}/lib/net/lwip/src/include/compat/posix")
58+
set(MOONLIGHT_NXDK_LWIP_MDNS_SOURCES
59+
"${NXDK_DIR}/lib/net/lwip/src/apps/mdns/mdns.c"
60+
"${NXDK_DIR}/lib/net/lwip/src/apps/mdns/mdns_domain.c"
61+
"${NXDK_DIR}/lib/net/lwip/src/apps/mdns/mdns_out.c"
62+
)
5863

5964
if(TARGET enet)
6065
target_link_libraries(enet PUBLIC NXDK::NXDK NXDK::Net)

cmake/xbox-build.cmake

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ add_executable(${CMAKE_PROJECT_NAME}
5656
)
5757
target_sources(${CMAKE_PROJECT_NAME}
5858
PRIVATE
59-
"${CMAKE_SOURCE_DIR}/src/_nxdk_compat/stat_compat.cpp")
59+
"${CMAKE_SOURCE_DIR}/src/_nxdk_compat/stat_compat.cpp"
60+
${MOONLIGHT_NXDK_LWIP_MDNS_SOURCES}
61+
)
6062
target_include_directories(${CMAKE_PROJECT_NAME}
6163
SYSTEM PRIVATE
6264
"${CMAKE_CURRENT_SOURCE_DIR}"

scripts/setup-xemu.sh

Lines changed: 119 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ usage() {
77
Usage: setup-xemu.sh [--force] [--skip-support-files]
88
99
Downloads a portable xemu build into .local/xemu and refreshes launcher manifests.
10+
Existing .local/xemu files are preserved unless --force is passed.
1011
EOF
1112
return 0
1213
}
@@ -68,6 +69,33 @@ extract_archive() {
6869
return 1
6970
}
7071

72+
copy_tree_preserving_existing() {
73+
local source_root="$1"
74+
local destination_root="$2"
75+
local source_path relative_path destination_path
76+
77+
mkdir -p "$destination_root"
78+
79+
while IFS= read -r -d '' source_path; do
80+
relative_path="${source_path#"$source_root"/}"
81+
destination_path="$destination_root/$relative_path"
82+
83+
if [[ -d "$source_path" ]]; then
84+
mkdir -p "$destination_path"
85+
continue
86+
fi
87+
88+
if [[ -e "$destination_path" ]]; then
89+
continue
90+
fi
91+
92+
mkdir -p "$(dirname "$destination_path")"
93+
cp -p "$source_path" "$destination_path"
94+
done < <(find "$source_root" -mindepth 1 -print0)
95+
96+
return 0
97+
}
98+
7199
latest_xemu_tag() {
72100
curl -fsSL -A 'Moonlight-XboxOG setup-xemu' \
73101
'https://api.github.com/repos/xemu-project/xemu/releases/latest' |
@@ -147,6 +175,22 @@ find_first_file() {
147175
return 1
148176
}
149177

178+
find_existing_xemu_executable() {
179+
local root="$1"
180+
181+
find_first_file "$root" 'xemu.exe' 'xemu' 'xemu.AppImage'
182+
return $?
183+
}
184+
185+
support_assets_ready() {
186+
local root="$1"
187+
188+
[[ -n "$(find_first_file "$root" 'mcpx*.bin' 2>/dev/null || true)" ]] || return 1
189+
[[ -n "$(find_first_file "$root" 'Complex*.bin' '*4627*.bin' 2>/dev/null || true)" ]] || return 1
190+
[[ -n "$(find_first_file "$root" 'xbox_hdd.qcow2' '*.qcow2' 2>/dev/null || true)" ]] || return 1
191+
return 0
192+
}
193+
150194
write_shell_manifest() {
151195
local manifest_path="$1"
152196
shift
@@ -179,6 +223,8 @@ write_cmd_manifest() {
179223

180224
force_download=0
181225
skip_support_files=0
226+
reused_existing_xemu=0
227+
reused_existing_support=0
182228

183229
while [[ $# -gt 0 ]]; do
184230
case "$1" in
@@ -214,56 +260,79 @@ manifest_cmd="$xemu_root/paths.cmd"
214260

215261
mkdir -p "$downloads_dir" "$app_dir" "$support_dir" "$temp_dir" "$portable_root"
216262

217-
mapfile -t platform_info < <(detect_platform)
218-
os_name="${platform_info[0]}"
219-
arch_name="${platform_info[1]}"
220-
tag="$(latest_xemu_tag)"
221-
if [[ -z "$tag" ]]; then
222-
echo 'Could not determine the latest xemu release tag.' >&2
223-
exit 1
224-
fi
225-
226-
asset_name="$(select_xemu_asset "$os_name" "$arch_name" "$tag")"
227-
asset_url="https://github.com/xemu-project/xemu/releases/latest/download/${asset_name}"
228-
asset_path="$downloads_dir/$asset_name"
229-
230-
download_file "$asset_url" "$asset_path" "$force_download"
231-
232-
rm -rf "$app_dir"
233-
mkdir -p "$app_dir"
263+
xemu_exe="$(find_existing_xemu_executable "$app_dir" || true)"
264+
if [[ -n "$xemu_exe" && "$force_download" -eq 0 ]]; then
265+
reused_existing_xemu=1
266+
else
267+
mapfile -t platform_info < <(detect_platform)
268+
os_name="${platform_info[0]}"
269+
arch_name="${platform_info[1]}"
270+
tag="$(latest_xemu_tag)"
271+
if [[ -z "$tag" ]]; then
272+
echo 'Could not determine the latest xemu release tag.' >&2
273+
exit 1
274+
fi
234275

235-
case "$asset_name" in
236-
*.zip)
237-
extract_archive "$asset_path" "$temp_dir/xemu-extract"
238-
xemu_exe="$(find_first_file "$temp_dir/xemu-extract" 'xemu.exe' 'xemu')"
239-
if [[ -z "$xemu_exe" ]]; then
240-
echo 'Could not find xemu executable in the downloaded archive.' >&2
276+
asset_name="$(select_xemu_asset "$os_name" "$arch_name" "$tag")"
277+
asset_url="https://github.com/xemu-project/xemu/releases/latest/download/${asset_name}"
278+
asset_path="$downloads_dir/$asset_name"
279+
280+
download_file "$asset_url" "$asset_path" "$force_download"
281+
282+
case "$asset_name" in
283+
*.zip)
284+
extract_archive "$asset_path" "$temp_dir/xemu-extract"
285+
if [[ -z "$(find_existing_xemu_executable "$temp_dir/xemu-extract" || true)" ]]; then
286+
echo 'Could not find xemu executable in the downloaded archive.' >&2
287+
exit 1
288+
fi
289+
290+
if [[ "$force_download" -eq 1 ]]; then
291+
rm -rf "$app_dir"
292+
mkdir -p "$app_dir"
293+
cp -R "$temp_dir/xemu-extract"/. "$app_dir"/
294+
else
295+
copy_tree_preserving_existing "$temp_dir/xemu-extract" "$app_dir"
296+
fi
297+
xemu_exe="$(find_existing_xemu_executable "$app_dir" || true)"
298+
;;
299+
*.AppImage)
300+
if [[ "$force_download" -eq 1 || ! -f "$app_dir/xemu.AppImage" ]]; then
301+
cp "$asset_path" "$app_dir/xemu.AppImage"
302+
fi
303+
chmod +x "$app_dir/xemu.AppImage"
304+
xemu_exe="$app_dir/xemu.AppImage"
305+
;;
306+
*)
307+
echo "Unsupported xemu asset type: $asset_name" >&2
241308
exit 1
242-
fi
243-
cp -R "$temp_dir/xemu-extract"/. "$app_dir"/
244-
xemu_exe="$(find_first_file "$app_dir" 'xemu.exe' 'xemu')"
245-
;;
246-
*.AppImage)
247-
cp "$asset_path" "$app_dir/xemu.AppImage"
248-
chmod +x "$app_dir/xemu.AppImage"
249-
xemu_exe="$app_dir/xemu.AppImage"
250-
;;
251-
*)
252-
echo "Unsupported xemu asset type: $asset_name" >&2
309+
;;
310+
esac
311+
312+
if [[ -z "$xemu_exe" ]]; then
313+
echo 'Could not locate the xemu executable after installation.' >&2
253314
exit 1
254-
;;
255-
esac
315+
fi
316+
fi
256317

257318
support_zip="$downloads_dir/Xbox-Emulator-Files.zip"
258319
if [[ "$skip_support_files" -eq 0 ]]; then
259-
download_file \
260-
'https://github.com/K3V1991/Xbox-Emulator-Files/releases/download/v1/Xbox-Emulator-Files.zip' \
261-
"$support_zip" \
262-
"$force_download"
263-
rm -rf "$support_dir"
264-
mkdir -p "$support_dir"
265-
extract_archive "$support_zip" "$temp_dir/support-extract"
266-
cp -R "$temp_dir/support-extract"/. "$support_dir"/
320+
if [[ "$force_download" -eq 0 ]] && support_assets_ready "$support_dir"; then
321+
reused_existing_support=1
322+
else
323+
download_file \
324+
'https://github.com/K3V1991/Xbox-Emulator-Files/releases/download/v1/Xbox-Emulator-Files.zip' \
325+
"$support_zip" \
326+
"$force_download"
327+
extract_archive "$support_zip" "$temp_dir/support-extract"
328+
if [[ "$force_download" -eq 1 ]]; then
329+
rm -rf "$support_dir"
330+
mkdir -p "$support_dir"
331+
cp -R "$temp_dir/support-extract"/. "$support_dir"/
332+
else
333+
copy_tree_preserving_existing "$temp_dir/support-extract" "$support_dir"
334+
fi
335+
fi
267336
fi
268337

269338
bootrom_path=''
@@ -352,6 +421,12 @@ fi
352421
chmod +x "$manifest_sh"
353422

354423
printf 'Portable xemu files are ready in %s\n' "$xemu_root"
424+
if [[ "$reused_existing_xemu" -eq 1 ]]; then
425+
echo 'Reused the existing xemu application files in .local/xemu/app.'
426+
fi
427+
if [[ "$reused_existing_support" -eq 1 ]]; then
428+
echo 'Reused the existing xemu support files in .local/xemu/support.'
429+
fi
355430
if [[ "$skip_support_files" -eq 1 ]]; then
356431
echo 'Support files were skipped. Run setup-xemu.sh again without --skip-support-files to fetch them.'
357432
elif [[ -z "$bootrom_path" || -z "$flashrom_path" || -z "$hdd_path" ]]; then

src/app/client_state.cpp

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -944,7 +944,7 @@ namespace {
944944
* @param update Update structure that receives the pairing request details.
945945
*/
946946
void request_host_pairing(app::ClientState &state, const app::HostRecord &host, app::AppUpdate *update) {
947-
if (!enter_pair_host_screen(state, host.address, host.port)) {
947+
if (const uint16_t pairingPort = host.resolvedHttpPort == 0U ? host.port : host.resolvedHttpPort; !enter_pair_host_screen(state, host.address, pairingPort)) {
948948
return;
949949
}
950950

@@ -1323,6 +1323,51 @@ namespace app {
13231323
}
13241324
}
13251325

1326+
bool merge_discovered_host(ClientState &state, std::string displayName, const std::string &address, uint16_t port) {
1327+
const std::string normalizedAddress = normalize_ipv4_address(address);
1328+
if (normalizedAddress.empty()) {
1329+
return false;
1330+
}
1331+
1332+
const uint16_t effectivePort = effective_host_port(port);
1333+
const uint16_t storedPort = effectivePort == DEFAULT_HOST_PORT ? 0 : effectivePort;
1334+
if (HostRecord *host = find_host_by_endpoint(state.hosts.items, normalizedAddress, effectivePort); host != nullptr) {
1335+
bool persistedMetadataChanged = false;
1336+
if (const std::string defaultDisplayName = build_default_host_display_name(host->address); !displayName.empty() && (host->displayName.empty() || host->displayName == defaultDisplayName)) {
1337+
persistedMetadataChanged = host->displayName != displayName;
1338+
host->displayName = std::move(displayName);
1339+
}
1340+
host->reachability = HostReachability::online;
1341+
if (host->manualAddress.empty()) {
1342+
host->manualAddress = normalizedAddress;
1343+
}
1344+
if (host->resolvedHttpPort == 0U || host->resolvedHttpPort == DEFAULT_HOST_PORT) {
1345+
host->resolvedHttpPort = effectivePort;
1346+
}
1347+
state.hosts.dirty = state.hosts.dirty || persistedMetadataChanged;
1348+
return persistedMetadataChanged;
1349+
}
1350+
1351+
HostRecord discoveredHost = make_host_record(normalizedAddress, storedPort);
1352+
if (!displayName.empty()) {
1353+
discoveredHost.displayName = std::move(displayName);
1354+
}
1355+
discoveredHost.reachability = HostReachability::online;
1356+
discoveredHost.manualAddress = normalizedAddress;
1357+
discoveredHost.resolvedHttpPort = effectivePort;
1358+
1359+
const bool wasEmpty = state.hosts.items.empty();
1360+
state.hosts.items.push_back(std::move(discoveredHost));
1361+
state.hosts.loaded = true;
1362+
state.hosts.dirty = true;
1363+
if (wasEmpty && state.shell.activeScreen == ScreenId::hosts) {
1364+
state.hosts.focusArea = HostsFocusArea::grid;
1365+
state.hosts.selectedHostIndex = 0U;
1366+
}
1367+
clamp_selected_host_index(state);
1368+
return true;
1369+
}
1370+
13261371
void replace_saved_files(ClientState &state, std::vector<startup::SavedFileEntry> savedFiles) {
13271372
state.settings.savedFiles = std::move(savedFiles);
13281373
state.settings.savedFilesDirty = false;

src/app/client_state.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,23 @@ namespace app {
413413
*/
414414
void replace_hosts(ClientState &state, std::vector<HostRecord> hosts, std::string statusMessage = {});
415415

416+
/**
417+
* @brief Add or refresh one auto-discovered host in the current host list.
418+
*
419+
* Discovery results are normalized to the same saved-host conventions used by
420+
* manual host entry. When a matching host already exists, transient runtime
421+
* fields such as reachability are refreshed without overwriting a custom saved
422+
* name. When no host matches, a new host record is appended and marked dirty so
423+
* it can be persisted.
424+
*
425+
* @param state Mutable app state.
426+
* @param displayName Discovered host name, or an empty string to use the default label.
427+
* @param address Discovered IPv4 address.
428+
* @param port Discovered host HTTP port.
429+
* @return true when persisted host metadata changed or a new host was added.
430+
*/
431+
bool merge_discovered_host(ClientState &state, std::string displayName, const std::string &address, uint16_t port);
432+
416433
/**
417434
* @brief Replace the in-memory saved-file inventory shown on the settings page.
418435
*

0 commit comments

Comments
 (0)