Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ The repository now includes `.run/Run xemu.run.xml`, which launches `scripts\run

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.

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.
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.

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.

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

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

<details style="display: none;">
Expand Down
5 changes: 5 additions & 0 deletions cmake/moonlight-dependencies.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ macro(MOONLIGHT_PREPARE_COMMON_DEPENDENCIES)
set(MOONLIGHT_NXDK_NET_INCLUDE_DIR "${NXDK_DIR}/lib/net")
set(MOONLIGHT_NXDK_LIBC_EXTENSIONS_DIR "${NXDK_DIR}/lib/xboxrt/libc_extensions")
set(MOONLIGHT_NXDK_LWIP_POSIX_COMPAT_DIR "${NXDK_DIR}/lib/net/lwip/src/include/compat/posix")
set(MOONLIGHT_NXDK_LWIP_MDNS_SOURCES
"${NXDK_DIR}/lib/net/lwip/src/apps/mdns/mdns.c"
"${NXDK_DIR}/lib/net/lwip/src/apps/mdns/mdns_domain.c"
"${NXDK_DIR}/lib/net/lwip/src/apps/mdns/mdns_out.c"
)

if(TARGET enet)
target_link_libraries(enet PUBLIC NXDK::NXDK NXDK::Net)
Expand Down
4 changes: 3 additions & 1 deletion cmake/xbox-build.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ add_executable(${CMAKE_PROJECT_NAME}
)
target_sources(${CMAKE_PROJECT_NAME}
PRIVATE
"${CMAKE_SOURCE_DIR}/src/_nxdk_compat/stat_compat.cpp")
"${CMAKE_SOURCE_DIR}/src/_nxdk_compat/stat_compat.cpp"
${MOONLIGHT_NXDK_LWIP_MDNS_SOURCES}
)
target_include_directories(${CMAKE_PROJECT_NAME}
SYSTEM PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
Expand Down
163 changes: 119 additions & 44 deletions scripts/setup-xemu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ usage() {
Usage: setup-xemu.sh [--force] [--skip-support-files]

Downloads a portable xemu build into .local/xemu and refreshes launcher manifests.
Existing .local/xemu files are preserved unless --force is passed.
EOF
return 0
}
Expand Down Expand Up @@ -68,6 +69,33 @@ extract_archive() {
return 1
}

copy_tree_preserving_existing() {
local source_root="$1"
local destination_root="$2"
local source_path relative_path destination_path

mkdir -p "$destination_root"

while IFS= read -r -d '' source_path; do
relative_path="${source_path#"$source_root"/}"
destination_path="$destination_root/$relative_path"

if [[ -d "$source_path" ]]; then
mkdir -p "$destination_path"
continue
fi

if [[ -e "$destination_path" ]]; then
continue
fi

mkdir -p "$(dirname "$destination_path")"
cp -p "$source_path" "$destination_path"
done < <(find "$source_root" -mindepth 1 -print0)

return 0
}

latest_xemu_tag() {
curl -fsSL -A 'Moonlight-XboxOG setup-xemu' \
'https://api.github.com/repos/xemu-project/xemu/releases/latest' |
Expand Down Expand Up @@ -147,6 +175,22 @@ find_first_file() {
return 1
}

find_existing_xemu_executable() {
local root="$1"

find_first_file "$root" 'xemu.exe' 'xemu' 'xemu.AppImage'
return $?
}

support_assets_ready() {
local root="$1"

[[ -n "$(find_first_file "$root" 'mcpx*.bin' 2>/dev/null || true)" ]] || return 1
[[ -n "$(find_first_file "$root" 'Complex*.bin' '*4627*.bin' 2>/dev/null || true)" ]] || return 1
[[ -n "$(find_first_file "$root" 'xbox_hdd.qcow2' '*.qcow2' 2>/dev/null || true)" ]] || return 1
return 0
}

write_shell_manifest() {
local manifest_path="$1"
shift
Expand Down Expand Up @@ -179,6 +223,8 @@ write_cmd_manifest() {

force_download=0
skip_support_files=0
reused_existing_xemu=0
reused_existing_support=0

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

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

mapfile -t platform_info < <(detect_platform)
os_name="${platform_info[0]}"
arch_name="${platform_info[1]}"
tag="$(latest_xemu_tag)"
if [[ -z "$tag" ]]; then
echo 'Could not determine the latest xemu release tag.' >&2
exit 1
fi

asset_name="$(select_xemu_asset "$os_name" "$arch_name" "$tag")"
asset_url="https://github.com/xemu-project/xemu/releases/latest/download/${asset_name}"
asset_path="$downloads_dir/$asset_name"

download_file "$asset_url" "$asset_path" "$force_download"

rm -rf "$app_dir"
mkdir -p "$app_dir"
xemu_exe="$(find_existing_xemu_executable "$app_dir" || true)"
if [[ -n "$xemu_exe" && "$force_download" -eq 0 ]]; then
reused_existing_xemu=1
else
mapfile -t platform_info < <(detect_platform)
os_name="${platform_info[0]}"
arch_name="${platform_info[1]}"
tag="$(latest_xemu_tag)"
if [[ -z "$tag" ]]; then
echo 'Could not determine the latest xemu release tag.' >&2
exit 1
fi

case "$asset_name" in
*.zip)
extract_archive "$asset_path" "$temp_dir/xemu-extract"
xemu_exe="$(find_first_file "$temp_dir/xemu-extract" 'xemu.exe' 'xemu')"
if [[ -z "$xemu_exe" ]]; then
echo 'Could not find xemu executable in the downloaded archive.' >&2
asset_name="$(select_xemu_asset "$os_name" "$arch_name" "$tag")"
asset_url="https://github.com/xemu-project/xemu/releases/latest/download/${asset_name}"
asset_path="$downloads_dir/$asset_name"

download_file "$asset_url" "$asset_path" "$force_download"

case "$asset_name" in
*.zip)
extract_archive "$asset_path" "$temp_dir/xemu-extract"
if [[ -z "$(find_existing_xemu_executable "$temp_dir/xemu-extract" || true)" ]]; then
echo 'Could not find xemu executable in the downloaded archive.' >&2
exit 1
fi

if [[ "$force_download" -eq 1 ]]; then
rm -rf "$app_dir"
mkdir -p "$app_dir"
cp -R "$temp_dir/xemu-extract"/. "$app_dir"/
else
copy_tree_preserving_existing "$temp_dir/xemu-extract" "$app_dir"
fi
xemu_exe="$(find_existing_xemu_executable "$app_dir" || true)"
;;
*.AppImage)
if [[ "$force_download" -eq 1 || ! -f "$app_dir/xemu.AppImage" ]]; then
cp "$asset_path" "$app_dir/xemu.AppImage"
fi
chmod +x "$app_dir/xemu.AppImage"
xemu_exe="$app_dir/xemu.AppImage"
;;
*)
echo "Unsupported xemu asset type: $asset_name" >&2
exit 1
fi
cp -R "$temp_dir/xemu-extract"/. "$app_dir"/
xemu_exe="$(find_first_file "$app_dir" 'xemu.exe' 'xemu')"
;;
*.AppImage)
cp "$asset_path" "$app_dir/xemu.AppImage"
chmod +x "$app_dir/xemu.AppImage"
xemu_exe="$app_dir/xemu.AppImage"
;;
*)
echo "Unsupported xemu asset type: $asset_name" >&2
;;
esac

if [[ -z "$xemu_exe" ]]; then
echo 'Could not locate the xemu executable after installation.' >&2
exit 1
;;
esac
fi
fi

support_zip="$downloads_dir/Xbox-Emulator-Files.zip"
if [[ "$skip_support_files" -eq 0 ]]; then
download_file \
'https://github.com/K3V1991/Xbox-Emulator-Files/releases/download/v1/Xbox-Emulator-Files.zip' \
"$support_zip" \
"$force_download"
rm -rf "$support_dir"
mkdir -p "$support_dir"
extract_archive "$support_zip" "$temp_dir/support-extract"
cp -R "$temp_dir/support-extract"/. "$support_dir"/
if [[ "$force_download" -eq 0 ]] && support_assets_ready "$support_dir"; then
reused_existing_support=1
else
download_file \
'https://github.com/K3V1991/Xbox-Emulator-Files/releases/download/v1/Xbox-Emulator-Files.zip' \
"$support_zip" \
"$force_download"
extract_archive "$support_zip" "$temp_dir/support-extract"
if [[ "$force_download" -eq 1 ]]; then
rm -rf "$support_dir"
mkdir -p "$support_dir"
cp -R "$temp_dir/support-extract"/. "$support_dir"/
else
copy_tree_preserving_existing "$temp_dir/support-extract" "$support_dir"
fi
fi
fi

bootrom_path=''
Expand Down Expand Up @@ -352,6 +421,12 @@ fi
chmod +x "$manifest_sh"

printf 'Portable xemu files are ready in %s\n' "$xemu_root"
if [[ "$reused_existing_xemu" -eq 1 ]]; then
echo 'Reused the existing xemu application files in .local/xemu/app.'
fi
if [[ "$reused_existing_support" -eq 1 ]]; then
echo 'Reused the existing xemu support files in .local/xemu/support.'
fi
if [[ "$skip_support_files" -eq 1 ]]; then
echo 'Support files were skipped. Run setup-xemu.sh again without --skip-support-files to fetch them.'
elif [[ -z "$bootrom_path" || -z "$flashrom_path" || -z "$hdd_path" ]]; then
Expand Down
47 changes: 46 additions & 1 deletion src/app/client_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -944,7 +944,7 @@ namespace {
* @param update Update structure that receives the pairing request details.
*/
void request_host_pairing(app::ClientState &state, const app::HostRecord &host, app::AppUpdate *update) {
if (!enter_pair_host_screen(state, host.address, host.port)) {
if (const uint16_t pairingPort = host.resolvedHttpPort == 0U ? host.port : host.resolvedHttpPort; !enter_pair_host_screen(state, host.address, pairingPort)) {
return;
}

Expand Down Expand Up @@ -1323,6 +1323,51 @@ namespace app {
}
}

bool merge_discovered_host(ClientState &state, std::string displayName, const std::string &address, uint16_t port) {
const std::string normalizedAddress = normalize_ipv4_address(address);
if (normalizedAddress.empty()) {
return false;
}

const uint16_t effectivePort = effective_host_port(port);
const uint16_t storedPort = effectivePort == DEFAULT_HOST_PORT ? 0 : effectivePort;
if (HostRecord *host = find_host_by_endpoint(state.hosts.items, normalizedAddress, effectivePort); host != nullptr) {
bool persistedMetadataChanged = false;
if (const std::string defaultDisplayName = build_default_host_display_name(host->address); !displayName.empty() && (host->displayName.empty() || host->displayName == defaultDisplayName)) {
persistedMetadataChanged = host->displayName != displayName;
host->displayName = std::move(displayName);
}
host->reachability = HostReachability::online;
if (host->manualAddress.empty()) {
host->manualAddress = normalizedAddress;
}
if (host->resolvedHttpPort == 0U || host->resolvedHttpPort == DEFAULT_HOST_PORT) {
host->resolvedHttpPort = effectivePort;
}
state.hosts.dirty = state.hosts.dirty || persistedMetadataChanged;
return persistedMetadataChanged;
}

HostRecord discoveredHost = make_host_record(normalizedAddress, storedPort);
if (!displayName.empty()) {
discoveredHost.displayName = std::move(displayName);
}
discoveredHost.reachability = HostReachability::online;
discoveredHost.manualAddress = normalizedAddress;
discoveredHost.resolvedHttpPort = effectivePort;

const bool wasEmpty = state.hosts.items.empty();
state.hosts.items.push_back(std::move(discoveredHost));
state.hosts.loaded = true;
state.hosts.dirty = true;
if (wasEmpty && state.shell.activeScreen == ScreenId::hosts) {
state.hosts.focusArea = HostsFocusArea::grid;
state.hosts.selectedHostIndex = 0U;
}
clamp_selected_host_index(state);
return true;
}

void replace_saved_files(ClientState &state, std::vector<startup::SavedFileEntry> savedFiles) {
state.settings.savedFiles = std::move(savedFiles);
state.settings.savedFilesDirty = false;
Expand Down
17 changes: 17 additions & 0 deletions src/app/client_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,23 @@ namespace app {
*/
void replace_hosts(ClientState &state, std::vector<HostRecord> hosts, std::string statusMessage = {});

/**
* @brief Add or refresh one auto-discovered host in the current host list.
*
* Discovery results are normalized to the same saved-host conventions used by
* manual host entry. When a matching host already exists, transient runtime
* fields such as reachability are refreshed without overwriting a custom saved
* name. When no host matches, a new host record is appended and marked dirty so
* it can be persisted.
*
* @param state Mutable app state.
* @param displayName Discovered host name, or an empty string to use the default label.
* @param address Discovered IPv4 address.
* @param port Discovered host HTTP port.
* @return true when persisted host metadata changed or a new host was added.
*/
bool merge_discovered_host(ClientState &state, std::string displayName, const std::string &address, uint16_t port);

/**
* @brief Replace the in-memory saved-file inventory shown on the settings page.
*
Expand Down
Loading
Loading