online, HDD, and dashboard stuff

This commit is contained in:
izzy2lost
2026-03-16 11:35:09 -04:00
parent 754f77aa13
commit c8b373d6e2
14 changed files with 1499 additions and 40 deletions
+107 -3
View File
@@ -78,6 +78,83 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(tomlplusplus)
# --- libslirp ---
set(LIBSLIRP_GIT_REV "26be815b86e8d49add8c9a8b320239b9594ff03d")
FetchContent_Declare(
libslirp
GIT_REPOSITORY "https://gitlab.freedesktop.org/slirp/libslirp.git"
GIT_TAG "${LIBSLIRP_GIT_REV}"
GIT_SHALLOW FALSE
)
FetchContent_GetProperties(libslirp)
if(NOT libslirp_POPULATED)
if(POLICY CMP0169)
cmake_policy(PUSH)
cmake_policy(SET CMP0169 OLD)
endif()
FetchContent_Populate(libslirp)
if(POLICY CMP0169)
cmake_policy(POP)
endif()
endif()
set(LIBSLIRP_GENERATED_DIR "${CMAKE_BINARY_DIR}/libslirp-generated")
file(MAKE_DIRECTORY "${LIBSLIRP_GENERATED_DIR}")
file(STRINGS "${libslirp_SOURCE_DIR}/meson.build" LIBSLIRP_VERSION_LINE
REGEX "^[ ]*version : '[^']+'")
if(NOT LIBSLIRP_VERSION_LINE)
message(FATAL_ERROR "Failed to determine libslirp version from meson.build")
endif()
string(REGEX REPLACE ".*version : '([^']+)'.*" "\\1"
LIBSLIRP_VERSION "${LIBSLIRP_VERSION_LINE}")
string(REPLACE "." ";" LIBSLIRP_VERSION_COMPONENTS "${LIBSLIRP_VERSION}")
list(LENGTH LIBSLIRP_VERSION_COMPONENTS LIBSLIRP_VERSION_COMPONENT_COUNT)
if(NOT LIBSLIRP_VERSION_COMPONENT_COUNT EQUAL 3)
message(FATAL_ERROR "Unexpected libslirp version format: ${LIBSLIRP_VERSION}")
endif()
list(GET LIBSLIRP_VERSION_COMPONENTS 0 SLIRP_MAJOR_VERSION)
list(GET LIBSLIRP_VERSION_COMPONENTS 1 SLIRP_MINOR_VERSION)
list(GET LIBSLIRP_VERSION_COMPONENTS 2 SLIRP_MICRO_VERSION)
set(SLIRP_VERSION_STRING "\"${LIBSLIRP_VERSION}\"")
configure_file(
"${libslirp_SOURCE_DIR}/src/libslirp-version.h.in"
"${LIBSLIRP_GENERATED_DIR}/libslirp-version.h"
@ONLY
)
set(LIBSLIRP_SOURCES
"${libslirp_SOURCE_DIR}/src/arp_table.c"
"${libslirp_SOURCE_DIR}/src/bootp.c"
"${libslirp_SOURCE_DIR}/src/cksum.c"
"${libslirp_SOURCE_DIR}/src/dhcpv6.c"
"${libslirp_SOURCE_DIR}/src/dnssearch.c"
"${libslirp_SOURCE_DIR}/src/if.c"
"${libslirp_SOURCE_DIR}/src/ip6_icmp.c"
"${libslirp_SOURCE_DIR}/src/ip6_input.c"
"${libslirp_SOURCE_DIR}/src/ip6_output.c"
"${libslirp_SOURCE_DIR}/src/ip_icmp.c"
"${libslirp_SOURCE_DIR}/src/ip_input.c"
"${libslirp_SOURCE_DIR}/src/ip_output.c"
"${libslirp_SOURCE_DIR}/src/mbuf.c"
"${libslirp_SOURCE_DIR}/src/misc.c"
"${libslirp_SOURCE_DIR}/src/ncsi.c"
"${libslirp_SOURCE_DIR}/src/ndp_table.c"
"${libslirp_SOURCE_DIR}/src/sbuf.c"
"${libslirp_SOURCE_DIR}/src/slirp.c"
"${libslirp_SOURCE_DIR}/src/socket.c"
"${libslirp_SOURCE_DIR}/src/state.c"
"${libslirp_SOURCE_DIR}/src/stream.c"
"${libslirp_SOURCE_DIR}/src/tcp_input.c"
"${libslirp_SOURCE_DIR}/src/tcp_output.c"
"${libslirp_SOURCE_DIR}/src/tcp_subr.c"
"${libslirp_SOURCE_DIR}/src/tcp_timer.c"
"${libslirp_SOURCE_DIR}/src/tftp.c"
"${libslirp_SOURCE_DIR}/src/udp.c"
"${libslirp_SOURCE_DIR}/src/udp6.c"
"${libslirp_SOURCE_DIR}/src/util.c"
"${libslirp_SOURCE_DIR}/src/version.c"
"${libslirp_SOURCE_DIR}/src/vmstate.c"
)
if(XEMU_ENABLE_VULKAN)
# --- Vulkan deps (volk, glslang, SPIRV-Reflect, VMA) ---
set(VOLK_GIT_REV "0b17a763ba5643e32da1b2152f8140461b3b7345")
@@ -517,7 +594,6 @@ list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "ui[\\\\/]thirdparty[\\\\/]noc_file_
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "ui[\\\\/]x_keymap\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "ui[\\\\/]vdagent\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "ui[\\\\/]xemu-os-utils-windows\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "ui[\\\\/]xemu-net\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "util[\\\\/]aio-win32\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "util[\\\\/]atomic64\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "util[\\\\/]coroutine-ucontext\\.c$")
@@ -549,7 +625,6 @@ list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "net[\\\\/]af-xdp\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "net[\\\\/]netmap\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "net[\\\\/]pcap\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "net[\\\\/]passt\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "net[\\\\/]slirp\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "net[\\\\/]tap-bsd\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "net[\\\\/]tap-solaris\\.c$")
list(FILTER XEMU_CORE_SOURCES EXCLUDE REGEX "net[\\\\/]tap-win32\\.c$")
@@ -654,7 +729,6 @@ list(APPEND XEMU_CORE_SOURCES
"${CMAKE_CURRENT_LIST_DIR}/watchdog_stub.c"
"${CMAKE_CURRENT_LIST_DIR}/async_teardown_stub.c"
"${CMAKE_CURRENT_LIST_DIR}/block_export_stub.c"
"${CMAKE_CURRENT_LIST_DIR}/xemu_net_stub.c"
"${CMAKE_CURRENT_LIST_DIR}/xemu_notifications_stub.c"
"${CMAKE_CURRENT_LIST_DIR}/xemu_snapshots_stub.c"
"${CMAKE_CURRENT_LIST_DIR}/xemu_hud_stub.c"
@@ -872,6 +946,7 @@ target_compile_definitions(xemu_core PRIVATE
ANDROID
__ANDROID__
XBOX
CONFIG_SLIRP
CONFIG_SDL
CONFIG_OPENGL
$<$<BOOL:${XEMU_ENABLE_VULKAN}>:CONFIG_VULKAN>
@@ -906,6 +981,8 @@ target_include_directories(xemu_core PRIVATE
"${CMAKE_CURRENT_LIST_DIR}"
"${CMAKE_BINARY_DIR}"
"${QAPI_GEN_DIR}"
"${libslirp_SOURCE_DIR}/src"
"${LIBSLIRP_GENERATED_DIR}"
"${GLIB_INSTALL_DIR}/include/glib-2.0"
"${GLIB_INSTALL_DIR}/lib/glib-2.0/include"
"${tomlplusplus_SOURCE_DIR}/include"
@@ -946,6 +1023,32 @@ set_target_properties(gthread-2.0 PROPERTIES
IMPORTED_LOCATION "${GLIB_INSTALL_DIR}/lib/libgthread-2.0.a"
)
add_library(slirp_static STATIC
${LIBSLIRP_SOURCES}
)
set_target_properties(slirp_static PROPERTIES
POSITION_INDEPENDENT_CODE ON
)
target_compile_definitions(slirp_static PRIVATE
BUILDING_LIBSLIRP
G_LOG_DOMAIN=\"Slirp\"
GLIB_STATIC_COMPILATION
GIO_STATIC_COMPILATION
GMODULE_STATIC_COMPILATION
GI_STATIC_COMPILATION
GOBJECT_STATIC_COMPILATION
)
target_include_directories(slirp_static PRIVATE
"${libslirp_SOURCE_DIR}/src"
"${LIBSLIRP_GENERATED_DIR}"
"${GLIB_INSTALL_DIR}/include/glib-2.0"
"${GLIB_INSTALL_DIR}/lib/glib-2.0/include"
)
target_link_libraries(slirp_static PRIVATE
glib-2.0
)
add_dependencies(slirp_static glib_ep)
add_library(xemu SHARED
xemu_android.cpp
android_crash_handler.cpp
@@ -1006,6 +1109,7 @@ endif()
target_link_libraries(xemu PRIVATE
xemu_core
slirp_static
iconv_stub
glib-2.0
gobject-2.0
+1
View File
@@ -8,6 +8,7 @@
#define CONFIG_SDL 1
#define CONFIG_SDL2 1
#define CONFIG_AUDIO_SDL 1
#define CONFIG_SLIRP 1
#define CONFIG_AUDIO_DRIVERS "sdl",
#define CONFIG_DEVICES "config-devices.h"
+8 -1
View File
@@ -725,6 +725,7 @@ struct EmulatorSettings {
bool hard_fpu = true;
bool vsync = true;
bool skip_boot_anim = false;
bool network_enabled = false;
};
struct SetupFiles {
@@ -773,11 +774,13 @@ static bool WriteConfigToml(const std::string& config_path,
toml::table* audio = EnsureTable(tbl, "audio");
toml::table* audio_vp = EnsureTable(*audio, "vp");
toml::table* android = EnsureTable(tbl, "android");
toml::table* net = EnsureTable(tbl, "net");
toml::table* net_nat = EnsureTable(*net, "nat");
toml::table* sys = EnsureTable(tbl, "sys");
toml::table* perf = EnsureTable(tbl, "perf");
toml::table* files = EnsureTable(*sys, "files");
if (!general || !display || !display_quality || !display_window || !audio ||
!audio_vp || !android || !sys || !perf || !files) {
!audio_vp || !android || !net || !net_nat || !sys || !perf || !files) {
LogErrorFmt("Failed to build config tables at %s", config_path.c_str());
return false;
}
@@ -826,6 +829,8 @@ static bool WriteConfigToml(const std::string& config_path,
if (!android->contains("audio_driver")) {
android->insert_or_assign("audio_driver", "openslES");
}
net->insert_or_assign("enable", settings.network_enabled);
net->insert_or_assign("backend", "nat");
sys->insert_or_assign(
"mem_limit", settings.system_memory_mib == 128 ? "128" : "64");
@@ -965,6 +970,8 @@ static SetupFiles SyncSetupFiles() {
emuSettings.hard_fpu = GetPrefBool(env, activity, "setting_hard_fpu", true);
emuSettings.skip_boot_anim =
GetPrefBool(env, activity, "setting_skip_boot_anim", false);
emuSettings.network_enabled =
GetPrefBool(env, activity, "setting_network_enable", false);
{
std::string tcgThread = GetPrefString(env, activity, "setting_tcg_thread");
if (tcgThread == "single") {
+167 -3
View File
@@ -157,6 +157,38 @@ static void xemu_fatx_propagate_error(XemuFatxFs *fs, Error **errp, const char *
}
}
static bool xemu_fatx_ascii_equal_n(const char *left, const char *right, size_t len)
{
size_t i;
for (i = 0; i < len; ++i) {
if (!left[i] || !right[i]) {
return false;
}
if (g_ascii_tolower(left[i]) != g_ascii_tolower(right[i])) {
return false;
}
}
return true;
}
static bool xemu_fatx_ascii_equal(const char *left, const char *right)
{
size_t left_len = strlen(left);
size_t right_len = strlen(right);
return left_len == right_len && xemu_fatx_ascii_equal_n(left, right, left_len);
}
static bool xemu_fatx_ascii_starts_with(const char *value, const char *prefix)
{
size_t prefix_len = strlen(prefix);
return strlen(value) >= prefix_len &&
xemu_fatx_ascii_equal_n(value, prefix, prefix_len);
}
static bool xemu_fatx_read_bytes(BlockBackend *blk,
uint64_t offset,
void *buffer,
@@ -785,7 +817,7 @@ static int xemu_fatx_open_dir(XemuFatxFs *fs, const char *path, XemuFatxDir *dir
if (status == XEMU_FATX_STATUS_SUCCESS) {
if ((attr.attributes & XEMU_FATX_ATTR_DIRECTORY) != 0 &&
strlen(dirent.filename) == len &&
memcmp(dirent.filename, start, len) == 0) {
xemu_fatx_ascii_equal_n(dirent.filename, start, len)) {
dir->cluster = attr.first_cluster;
dir->entry = 0;
break;
@@ -817,7 +849,7 @@ static int xemu_fatx_get_attr_dir(XemuFatxFs *fs,
while (1) {
status = xemu_fatx_read_dir(fs, dir, dirent, attr);
if (status == XEMU_FATX_STATUS_SUCCESS) {
if (strcmp(basename, dirent->filename) == 0) {
if (xemu_fatx_ascii_equal(basename, dirent->filename)) {
return XEMU_FATX_STATUS_SUCCESS;
}
} else if (status == XEMU_FATX_STATUS_FILE_DELETED) {
@@ -1283,7 +1315,7 @@ static int xemu_fatx_unlink(XemuFatxFs *fs, const char *path)
while (1) {
status = xemu_fatx_read_dir(fs, &dir, &entry, &attr);
if (status == XEMU_FATX_STATUS_SUCCESS) {
if (strcmp(entry.filename, basename) == 0) {
if (xemu_fatx_ascii_equal(entry.filename, basename)) {
break;
}
} else if (status == XEMU_FATX_STATUS_FILE_DELETED) {
@@ -1692,6 +1724,138 @@ static bool xemu_dashboard_import_partition(BlockBackend *blk,
return true;
}
static bool xemu_fatx_probe_path(XemuFatxFs *fs,
const char *path,
bool *exists_out,
bool *is_dir_out)
{
XemuFatxAttr attr;
int status = xemu_fatx_get_attr(fs, path, &attr);
if (status == XEMU_FATX_STATUS_SUCCESS) {
if (exists_out) {
*exists_out = true;
}
if (is_dir_out) {
*is_dir_out = (attr.attributes & XEMU_FATX_ATTR_DIRECTORY) != 0;
}
return true;
}
if (status == XEMU_FATX_STATUS_FILE_NOT_FOUND) {
if (exists_out) {
*exists_out = false;
}
if (is_dir_out) {
*is_dir_out = false;
}
return true;
}
return false;
}
static bool xemu_fatx_scan_root_dashboard_markers(XemuFatxFs *fs,
XemuFatxDashboardStatus *status,
Error **errp)
{
XemuFatxDir dir;
XemuFatxDirent dirent;
XemuFatxAttr attr;
int scan_status;
if (!status) {
return true;
}
scan_status = xemu_fatx_open_dir(fs, "/", &dir);
if (scan_status != XEMU_FATX_STATUS_SUCCESS) {
xemu_fatx_propagate_error(fs, errp, NULL);
return false;
}
while (1) {
scan_status = xemu_fatx_read_dir(fs, &dir, &dirent, &attr);
if (scan_status == XEMU_FATX_STATUS_SUCCESS) {
if ((attr.attributes & XEMU_FATX_ATTR_DIRECTORY) != 0) {
if (xemu_fatx_ascii_equal(dirent.filename, "xodash")) {
status->xodash_dir_present = true;
} else if (xemu_fatx_ascii_equal(dirent.filename, "audio")) {
status->audio_dir_present = true;
} else if (xemu_fatx_ascii_equal(dirent.filename, "fonts")) {
status->fonts_dir_present = true;
} else if (xemu_fatx_ascii_starts_with(dirent.filename, "xboxdashdata.")) {
status->xboxdashdata_dir_present = true;
}
} else {
if (xemu_fatx_ascii_equal(dirent.filename, "xboxdash.xbe")) {
status->xboxdash_xbe_present = true;
} else if (xemu_fatx_ascii_equal(dirent.filename, "msdash.xbe")) {
status->msdash_xbe_present = true;
} else if (xemu_fatx_ascii_equal(dirent.filename, "xbox.xtf")) {
status->xbox_xtf_present = true;
}
}
} else if (scan_status == XEMU_FATX_STATUS_FILE_DELETED) {
/* Skip. */
} else if (scan_status == XEMU_FATX_STATUS_END_OF_DIR) {
return true;
} else {
xemu_fatx_propagate_error(fs, errp, NULL);
return false;
}
scan_status = xemu_fatx_next_dir_entry(fs, &dir);
if (scan_status != XEMU_FATX_STATUS_SUCCESS) {
xemu_fatx_propagate_error(fs, errp, NULL);
return false;
}
}
}
bool xemu_fatx_inspect_retail_dashboard(BlockBackend *blk,
XemuFatxDashboardStatus *status,
Error **errp)
{
XemuFatxFs fs;
bool ok = true;
if (status) {
memset(status, 0, sizeof(*status));
}
if (!xemu_fatx_open_fs(&fs, blk,
XEMU_XBOX_HDD_PARTITION_C_OFFSET,
XEMU_XBOX_HDD_PARTITION_C_SIZE,
errp)) {
return false;
}
if (status) {
bool is_dir = false;
ok = xemu_fatx_probe_path(&fs, "/xboxdash.xbe",
&status->xboxdash_xbe_present, NULL) &&
xemu_fatx_probe_path(&fs, "/msdash.xbe",
&status->msdash_xbe_present, NULL) &&
xemu_fatx_probe_path(&fs, "/xbox.xtf",
&status->xbox_xtf_present, NULL) &&
xemu_fatx_probe_path(&fs, "/xodash",
&status->xodash_dir_present, &is_dir);
status->xodash_dir_present = status->xodash_dir_present && is_dir;
ok = ok && xemu_fatx_scan_root_dashboard_markers(&fs, status, errp);
}
if (!ok) {
xemu_fatx_propagate_error(&fs, errp, NULL);
xemu_fatx_close_fs(&fs);
return false;
}
xemu_fatx_close_fs(&fs);
return true;
}
bool xemu_fatx_import_dashboard(BlockBackend *blk,
const char *source_root,
const char *backup_root,
@@ -6,9 +6,23 @@
#include <stdbool.h>
typedef struct XemuFatxDashboardStatus {
bool xboxdash_xbe_present;
bool msdash_xbe_present;
bool xbox_xtf_present;
bool xodash_dir_present;
bool audio_dir_present;
bool fonts_dir_present;
bool xboxdashdata_dir_present;
} XemuFatxDashboardStatus;
bool xemu_fatx_import_dashboard(BlockBackend *blk,
const char *source_root,
const char *backup_root,
Error **errp);
bool xemu_fatx_inspect_retail_dashboard(BlockBackend *blk,
XemuFatxDashboardStatus *status,
Error **errp);
#endif
@@ -27,6 +27,16 @@
#define ANDROID_XBOX_HDD_REFURB_OFFSET 0x600ULL
#define ANDROID_XBOX_HDD_REFURB_SIGNATURE 0x42524652U
#define ANDROID_XBOX_HDD_CONFIG_STATIC_PRIMARY_DNS_OFFSET 0x102CULL
#define ANDROID_XBOX_HDD_CONFIG_STATIC_SECONDARY_DNS_OFFSET 0x1030ULL
#define ANDROID_XBOX_HDD_CONFIG_XBLIVE_PRIMARY_DNS_OFFSET 0x1060ULL
#define ANDROID_XBOX_HDD_CONFIG_XBLIVE_SECONDARY_DNS_OFFSET 0x1064ULL
#define ANDROID_XBOX_HDD_CONFIG_DHCP_PRIMARY_DNS_OFFSET 0x1178ULL
#define ANDROID_XBOX_HDD_CONFIG_DHCP_SECONDARY_DNS_OFFSET 0x117CULL
#define ANDROID_XBOX_HDD_CONFIG_PPPOE_PRIMARY_DNS_OFFSET 0x118CULL
#define ANDROID_XBOX_HDD_CONFIG_PPPOE_SECONDARY_DNS_OFFSET 0x1190ULL
#define ANDROID_XBOX_HDD_CONFIG_MINIMUM_BYTES 0x1194ULL
#define ANDROID_XBOX_HDD_FATX_SIGNATURE 0x58544146U
#define ANDROID_XBOX_HDD_FATX_SUPERBLOCK_SIZE 4096ULL
#define ANDROID_XBOX_HDD_FATX_FAT_OFFSET ANDROID_XBOX_HDD_FATX_SUPERBLOCK_SIZE
@@ -82,6 +92,13 @@ static pthread_mutex_t g_android_xbox_hdd_init_lock = PTHREAD_MUTEX_INITIALIZER;
static bool g_android_xbox_hdd_init_complete;
static char *g_android_xbox_hdd_init_failure;
static bool android_xbox_hdd_write_bytes(BlockBackend *blk,
uint64_t offset,
const void *buffer,
uint64_t size,
const char *label,
Error **errp);
static void android_xbox_hdd_throw_exception(JNIEnv *env,
const char *class_name,
const char *message)
@@ -231,6 +248,38 @@ static BlockBackend *android_xbox_hdd_open_backend(const char *path,
return blk_new_open(path, NULL, options, flags, errp);
}
static bool android_xbox_hdd_copy_ipv4(JNIEnv *env,
jbyteArray array,
uint8_t out[4],
const char *label)
{
jbyte buffer[4];
jsize length;
if (!array) {
android_xbox_hdd_throw_exception(env, "java/lang/IllegalArgumentException",
label);
return false;
}
length = (*env)->GetArrayLength(env, array);
if (length != 4) {
android_xbox_hdd_throw_exception(env, "java/lang/IllegalArgumentException",
label);
return false;
}
(*env)->GetByteArrayRegion(env, array, 0, 4, buffer);
if ((*env)->ExceptionCheck(env)) {
return false;
}
for (size_t i = 0; i < 4; ++i) {
out[i] = (uint8_t)buffer[i];
}
return true;
}
static bool android_xbox_hdd_inspect_locked(const char *path,
AndroidXboxHddFormat *format,
int64_t *total_bytes,
@@ -266,6 +315,119 @@ cleanup:
return ok;
}
static bool android_xbox_hdd_inspect_dashboard_locked(const char *path,
jint *flags_out,
Error **errp)
{
BlockBackend *blk = NULL;
XemuFatxDashboardStatus status;
bool ok = false;
jint flags = 0;
blk = android_xbox_hdd_open_backend(path, false, errp);
if (!blk) {
return false;
}
if (!xemu_fatx_inspect_retail_dashboard(blk, &status, errp)) {
goto cleanup;
}
if (status.xboxdash_xbe_present) {
flags |= 1 << 0;
}
if (status.xodash_dir_present) {
flags |= 1 << 1;
}
if (status.xbox_xtf_present) {
flags |= 1 << 2;
}
if (status.msdash_xbe_present) {
flags |= 1 << 3;
}
if (status.audio_dir_present) {
flags |= 1 << 4;
}
if (status.fonts_dir_present) {
flags |= 1 << 5;
}
if (status.xboxdashdata_dir_present) {
flags |= 1 << 6;
}
if (flags_out) {
*flags_out = flags;
}
ok = true;
cleanup:
if (blk) {
blk_drain(blk);
blk_unref(blk);
}
return ok;
}
static bool android_xbox_hdd_write_insignia_dns_locked(const char *path,
const uint8_t primary_dns[4],
const uint8_t secondary_dns[4],
Error **errp)
{
static const uint64_t primary_offsets[] = {
ANDROID_XBOX_HDD_CONFIG_STATIC_PRIMARY_DNS_OFFSET,
ANDROID_XBOX_HDD_CONFIG_XBLIVE_PRIMARY_DNS_OFFSET,
ANDROID_XBOX_HDD_CONFIG_DHCP_PRIMARY_DNS_OFFSET,
ANDROID_XBOX_HDD_CONFIG_PPPOE_PRIMARY_DNS_OFFSET,
};
static const uint64_t secondary_offsets[] = {
ANDROID_XBOX_HDD_CONFIG_STATIC_SECONDARY_DNS_OFFSET,
ANDROID_XBOX_HDD_CONFIG_XBLIVE_SECONDARY_DNS_OFFSET,
ANDROID_XBOX_HDD_CONFIG_DHCP_SECONDARY_DNS_OFFSET,
ANDROID_XBOX_HDD_CONFIG_PPPOE_SECONDARY_DNS_OFFSET,
};
BlockBackend *blk = NULL;
int64_t total_bytes;
bool ok = false;
blk = android_xbox_hdd_open_backend(path, true, errp);
if (!blk) {
return false;
}
total_bytes = blk_getlength(blk);
if (total_bytes < 0) {
error_setg(errp, "Could not determine the HDD size: %s", g_strerror(-total_bytes));
goto cleanup;
}
if ((uint64_t)total_bytes < ANDROID_XBOX_HDD_CONFIG_MINIMUM_BYTES) {
error_setg(errp, "The current HDD image is too small to contain the Xbox config sector");
goto cleanup;
}
for (size_t i = 0; i < G_N_ELEMENTS(primary_offsets); ++i) {
if (!android_xbox_hdd_write_bytes(blk, primary_offsets[i], primary_dns, 4,
"Insignia primary DNS", errp) ||
!android_xbox_hdd_write_bytes(blk, secondary_offsets[i], secondary_dns, 4,
"Insignia secondary DNS", errp)) {
goto cleanup;
}
}
if (blk_flush(blk) < 0) {
error_setg(errp, "Failed to flush Insignia DNS writes");
goto cleanup;
}
ok = true;
cleanup:
if (blk) {
blk_drain(blk);
blk_unref(blk);
}
return ok;
}
static uint32_t android_xbox_hdd_retail_sectors_per_cluster(void)
{
return (uint32_t)(ANDROID_XBOX_HDD_FATX_RETAIL_CLUSTER_SIZE /
@@ -820,6 +982,102 @@ Java_com_izzy2lost_x1box_XboxHddFormatter_00024NativeBridge_nativeInitializeHdd(
(*env)->ReleaseStringUTFChars(env, jpath, path);
}
JNIEXPORT jint JNICALL
Java_com_izzy2lost_x1box_XboxInsigniaHelper_00024NativeBridge_nativeInspectDashboardFlags(
JNIEnv *env, jobject obj, jstring jhdd_path)
{
const char *hdd_path;
Error *err = NULL;
jint flags = 0;
(void)obj;
if (!jhdd_path) {
android_xbox_hdd_throw_exception(env, "java/io/IOException",
"No local HDD image is configured.");
return 0;
}
hdd_path = (*env)->GetStringUTFChars(env, jhdd_path, NULL);
if (!hdd_path) {
return 0;
}
if (!android_xbox_hdd_ensure_block_layer(&err)) {
(*env)->ReleaseStringUTFChars(env, jhdd_path, hdd_path);
android_xbox_hdd_throw_error(env, "java/io/IOException",
"Failed to initialize HDD tools: ", err);
return 0;
}
{
BQL_LOCK_GUARD();
if (!android_xbox_hdd_inspect_dashboard_locked(hdd_path, &flags, &err)) {
(*env)->ReleaseStringUTFChars(env, jhdd_path, hdd_path);
android_xbox_hdd_throw_error(env, "java/io/IOException", NULL, err);
return 0;
}
}
(*env)->ReleaseStringUTFChars(env, jhdd_path, hdd_path);
return flags;
}
JNIEXPORT void JNICALL
Java_com_izzy2lost_x1box_XboxInsigniaHelper_00024NativeBridge_nativeApplyConfigSectorDns(
JNIEnv *env,
jobject obj,
jstring jhdd_path,
jbyteArray jprimary_dns,
jbyteArray jsecondary_dns)
{
const char *hdd_path;
uint8_t primary_dns[4];
uint8_t secondary_dns[4];
Error *err = NULL;
(void)obj;
if (!jhdd_path) {
android_xbox_hdd_throw_exception(env, "java/io/IOException",
"No local HDD image is configured.");
return;
}
if (!android_xbox_hdd_copy_ipv4(env, jprimary_dns, primary_dns,
"Insignia primary DNS must be an IPv4 address.") ||
!android_xbox_hdd_copy_ipv4(env, jsecondary_dns, secondary_dns,
"Insignia secondary DNS must be an IPv4 address.")) {
return;
}
hdd_path = (*env)->GetStringUTFChars(env, jhdd_path, NULL);
if (!hdd_path) {
return;
}
if (!android_xbox_hdd_ensure_block_layer(&err)) {
(*env)->ReleaseStringUTFChars(env, jhdd_path, hdd_path);
android_xbox_hdd_throw_error(env, "java/io/IOException",
"Failed to initialize HDD tools: ", err);
return;
}
{
BQL_LOCK_GUARD();
if (!android_xbox_hdd_write_insignia_dns_locked(hdd_path,
primary_dns,
secondary_dns,
&err)) {
(*env)->ReleaseStringUTFChars(env, jhdd_path, hdd_path);
android_xbox_hdd_throw_error(env, "java/io/IOException", NULL, err);
return;
}
}
(*env)->ReleaseStringUTFChars(env, jhdd_path, hdd_path);
}
JNIEXPORT void JNICALL
Java_com_izzy2lost_x1box_XboxDashboardImporter_00024NativeBridge_nativeImportDashboard(
JNIEnv *env, jobject obj, jstring jhdd_path, jstring jsource_root, jstring jbackup_root)
@@ -12,10 +12,14 @@
#include <cctype>
#include <sstream>
#include <string>
#include <type_traits>
#include <vector>
#include "xemu-settings.h"
struct config g_config;
using NetNatForwardPort =
std::remove_pointer_t<decltype(g_config.net.nat.forward_ports)>;
static std::string settings_path_storage;
static const char *settings_path;
@@ -146,10 +150,79 @@ static bool parse_filtering(const std::string &value, CONFIG_DISPLAY_FILTERING *
static std::string to_lower_ascii(std::string value)
{
for (char &c : value) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
for (char &c : value) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return value;
}
static bool parse_network_backend(toml::node_view<toml::node> node,
CONFIG_NET_BACKEND *out)
{
if (!out) {
return false;
}
return value;
if (auto backend = node.value<std::string>()) {
std::string normalized = to_lower_ascii(*backend);
if (normalized == "nat" || normalized == "user") {
*out = CONFIG_NET_BACKEND_NAT;
return true;
}
if (normalized == "udp" || normalized == "udp_tunnel" ||
normalized == "udp tunnel") {
*out = CONFIG_NET_BACKEND_UDP;
return true;
}
if (normalized == "pcap" || normalized == "bridge" ||
normalized == "bridged adapter") {
*out = CONFIG_NET_BACKEND_PCAP;
return true;
}
return false;
}
if (auto backend = node.value<int64_t>()) {
if (*backend < 0 || *backend >= CONFIG_NET_BACKEND__COUNT) {
return false;
}
*out = static_cast<CONFIG_NET_BACKEND>(*backend);
return true;
}
return false;
}
static bool parse_nat_forward_protocol(
toml::node_view<const toml::node> node,
CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL *out)
{
if (!out) {
return false;
}
if (auto protocol = node.value<std::string>()) {
std::string normalized = to_lower_ascii(*protocol);
if (normalized == "tcp") {
*out = CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL_TCP;
return true;
}
if (normalized == "udp") {
*out = CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL_UDP;
return true;
}
return false;
}
if (auto protocol = node.value<int64_t>()) {
if (*protocol < 0 || *protocol >= CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL__COUNT) {
return false;
}
*out = static_cast<CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL>(*protocol);
return true;
}
return false;
}
const char *xemu_settings_get_error_message(void)
@@ -240,6 +313,10 @@ bool xemu_settings_load(void)
auto audio_vp = audio["vp"];
auto perf = tbl["perf"];
auto android_cfg = tbl["android"];
auto net = tbl["net"];
auto net_udp = net["udp"];
auto net_pcap = net["pcap"];
auto net_nat = net["nat"];
auto sys = tbl["sys"];
auto sys_files = sys["files"];
@@ -318,6 +395,69 @@ bool xemu_settings_load(void)
g_config.audio.volume_limit = volume;
}
if (auto net_enable = net["enable"].value<bool>()) {
g_config.net.enable = *net_enable;
}
{
CONFIG_NET_BACKEND parsed_backend;
if (parse_network_backend(net["backend"], &parsed_backend)) {
g_config.net.backend = parsed_backend;
}
}
if (auto bind_addr = net_udp["bind_addr"].value<std::string>()) {
xemu_settings_set_string(&g_config.net.udp.bind_addr, bind_addr->c_str());
}
if (auto remote_addr = net_udp["remote_addr"].value<std::string>()) {
xemu_settings_set_string(&g_config.net.udp.remote_addr, remote_addr->c_str());
}
if (auto netif = net_pcap["netif"].value<std::string>()) {
xemu_settings_set_string(&g_config.net.pcap.netif, netif->c_str());
}
if (auto forward_ports = net_nat["forward_ports"].as_array()) {
std::vector<NetNatForwardPort> parsed_ports;
parsed_ports.reserve(forward_ports->size());
for (const auto &entry : *forward_ports) {
const toml::table *table = entry.as_table();
if (!table) {
continue;
}
auto host = (*table)["host"].value<int64_t>();
auto guest = (*table)["guest"].value<int64_t>();
if (!host || !guest || *host < 1 || *host > 65535 ||
*guest < 1 || *guest > 65535) {
continue;
}
CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL protocol =
CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL_TCP;
parse_nat_forward_protocol((*table)["protocol"], &protocol);
NetNatForwardPort port = {};
port.host = static_cast<int>(*host);
port.guest = static_cast<int>(*guest);
port.protocol = protocol;
parsed_ports.push_back(port);
}
free(g_config.net.nat.forward_ports);
g_config.net.nat.forward_ports = NULL;
g_config.net.nat.forward_ports_count = 0;
if (!parsed_ports.empty()) {
auto *ports = static_cast<NetNatForwardPort *>(calloc(
parsed_ports.size(), sizeof(NetNatForwardPort)));
if (ports) {
memcpy(ports, parsed_ports.data(),
parsed_ports.size() * sizeof(NetNatForwardPort));
g_config.net.nat.forward_ports = ports;
g_config.net.nat.forward_ports_count =
static_cast<unsigned int>(parsed_ports.size());
}
}
}
// Android-specific settings
if (auto egl_offscreen = android_cfg["egl_offscreen"].value<bool>()) {
if (!*egl_offscreen) {
@@ -456,14 +596,58 @@ void xemu_settings_save(void)
void add_net_nat_forward_ports(int host, int guest,
CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL protocol)
{
(void)host;
(void)guest;
(void)protocol;
if (host < 1 || host > 65535 || guest < 1 || guest > 65535 ||
protocol < 0 || protocol >= CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL__COUNT) {
return;
}
unsigned int old_count = g_config.net.nat.forward_ports_count;
unsigned int new_count = old_count + 1;
auto *ports = static_cast<NetNatForwardPort *>(realloc(
g_config.net.nat.forward_ports, sizeof(NetNatForwardPort) * new_count));
if (!ports) {
return;
}
ports[old_count].host = host;
ports[old_count].guest = guest;
ports[old_count].protocol = protocol;
g_config.net.nat.forward_ports = ports;
g_config.net.nat.forward_ports_count = new_count;
}
void remove_net_nat_forward_ports(unsigned int index)
{
(void)index;
if (index >= g_config.net.nat.forward_ports_count) {
return;
}
unsigned int old_count = g_config.net.nat.forward_ports_count;
unsigned int new_count = old_count - 1;
auto *ports =
static_cast<NetNatForwardPort *>(g_config.net.nat.forward_ports);
if (index + 1 < old_count) {
memmove(&ports[index], &ports[index + 1],
(old_count - index - 1) * sizeof(NetNatForwardPort));
}
if (new_count == 0) {
free(ports);
g_config.net.nat.forward_ports = NULL;
g_config.net.nat.forward_ports_count = 0;
return;
}
auto *resized = static_cast<NetNatForwardPort *>(realloc(
ports, sizeof(NetNatForwardPort) * new_count));
if (!resized) {
g_config.net.nat.forward_ports_count = new_count;
return;
}
g_config.net.nat.forward_ports = resized;
g_config.net.nat.forward_ports_count = new_count;
}
bool xemu_settings_load_gamepad_mapping(const char *guid,
@@ -257,6 +257,22 @@ class GameLibraryActivity : AppCompatActivity() {
return
}
resolveConfiguredLocalHddFile()?.let { hddFile ->
runCatching { XboxInsigniaHelper.inspectDashboard(hddFile) }
.getOrNull()
?.let { dashboardStatus ->
if (!dashboardStatus.looksRetailDashboardInstalled) {
val messageRes = if (dashboardStatus.hasAnyRetailDashboardFiles) {
R.string.library_boot_dashboard_incomplete_retail
} else {
R.string.library_boot_dashboard_missing_retail
}
Toast.makeText(this, getString(messageRes), Toast.LENGTH_LONG).show()
return
}
}
}
// MainActivity runs in :xemu, so the disc selection must be flushed before
// the other process reads SharedPreferences during startup.
prefs.edit()
@@ -285,6 +301,12 @@ class GameLibraryActivity : AppCompatActivity() {
return uri != null && hasPersistedReadPermission(uri)
}
private fun resolveConfiguredLocalHddFile(): File? {
val path = prefs.getString("hddPath", null) ?: return null
val file = File(path)
return file.takeIf { it.isFile }
}
private fun slotName(slot: Int) = "android_slot_$slot"
private fun snapshotPreviewDir(): File = File(filesDir, "x1box/snapshots")
File diff suppressed because it is too large Load Diff
@@ -14,6 +14,8 @@ internal object XboxEepromEditor {
private const val USER_CHECKSUM_START = 0x64
private const val USER_CHECKSUM_LENGTH = 0x5C
private const val XBOX_LIVE_DNS_OFFSET = 0xAC
private const val VIDEO_STANDARD_OFFSET = 0x58
private const val LANGUAGE_OFFSET = 0x90
private const val VIDEO_SETTINGS_OFFSET = 0x94
@@ -189,6 +191,34 @@ internal object XboxEepromEditor {
return true
}
@Throws(IOException::class, IllegalArgumentException::class)
fun applyXboxLiveDns(
file: File,
dnsServer: ByteArray,
): Boolean {
require(dnsServer.size == 4) { "Xbox Live DNS must be a 4-byte IPv4 address." }
val data = file.readBytes()
ensureValidSize(file, data)
var changed = false
for (index in dnsServer.indices) {
if (data[XBOX_LIVE_DNS_OFFSET + index] != dnsServer[index]) {
data[XBOX_LIVE_DNS_OFFSET + index] = dnsServer[index]
changed = true
}
}
if (!changed) {
return false
}
val userChecksum = xboxChecksum(data, USER_CHECKSUM_START, USER_CHECKSUM_LENGTH)
writeLeUInt32(data, USER_CHECKSUM_OFFSET, userChecksum)
file.writeBytes(data)
return true
}
private fun ensureValidSize(file: File, data: ByteArray) {
if (data.size != EEPROM_SIZE) {
throw IllegalArgumentException(
@@ -0,0 +1,102 @@
package com.izzy2lost.x1box
import java.io.File
import java.io.IOException
internal object XboxInsigniaHelper {
const val PRIMARY_DNS = "46.101.64.175"
const val SECONDARY_DNS = "8.8.8.8"
private const val FLAG_XBOXDASH_XBE = 1 shl 0
private const val FLAG_XODASH_DIR = 1 shl 1
private const val FLAG_XBOX_XTF = 1 shl 2
private const val FLAG_MSDASH_XBE = 1 shl 3
private const val FLAG_AUDIO_DIR = 1 shl 4
private const val FLAG_FONTS_DIR = 1 shl 5
private const val FLAG_XBOXDASHDATA_DIR = 1 shl 6
data class DashboardStatus(
val flags: Int,
) {
val hasBootXbe: Boolean
get() = (flags and FLAG_XBOXDASH_XBE) != 0
val hasXodashAssets: Boolean
get() = (flags and FLAG_XODASH_DIR) != 0
val hasXboxFont: Boolean
get() = (flags and FLAG_XBOX_XTF) != 0
val hasMsdashXbe: Boolean
get() = (flags and FLAG_MSDASH_XBE) != 0
val hasAudioDir: Boolean
get() = (flags and FLAG_AUDIO_DIR) != 0
val hasFontsDir: Boolean
get() = (flags and FLAG_FONTS_DIR) != 0
val hasXboxdashdataDir: Boolean
get() = (flags and FLAG_XBOXDASHDATA_DIR) != 0
val looksRetailDashboardInstalled: Boolean
get() = hasBootXbe && (
hasXodashAssets ||
hasXboxFont ||
hasMsdashXbe ||
hasAudioDir ||
hasFontsDir ||
hasXboxdashdataDir
)
val hasAnyRetailDashboardFiles: Boolean
get() = flags != 0
}
private val primaryDnsBytes = parseIpv4(PRIMARY_DNS)
private val secondaryDnsBytes = parseIpv4(SECONDARY_DNS)
@Throws(IOException::class, IllegalArgumentException::class)
fun inspectDashboard(hddFile: File): DashboardStatus {
require(hddFile.isFile) { "No local HDD image is configured." }
return DashboardStatus(NativeBridge.nativeInspectDashboardFlags(hddFile.absolutePath))
}
@Throws(IOException::class, IllegalArgumentException::class)
fun applyConfigSectorDns(hddFile: File) {
require(hddFile.isFile) { "No local HDD image is configured." }
NativeBridge.nativeApplyConfigSectorDns(
hddFile.absolutePath,
primaryDnsBytes,
secondaryDnsBytes,
)
}
fun primaryDnsBytes(): ByteArray = primaryDnsBytes.copyOf()
private fun parseIpv4(value: String): ByteArray {
val octets = value.split('.')
require(octets.size == 4) { "Invalid IPv4 address: $value" }
return ByteArray(4) { index ->
val octet = octets[index].toIntOrNull()
?: throw IllegalArgumentException("Invalid IPv4 address: $value")
require(octet in 0..255) { "Invalid IPv4 address: $value" }
octet.toByte()
}
}
private object NativeBridge {
init {
System.loadLibrary("SDL2")
System.loadLibrary("xemu")
}
external fun nativeInspectDashboardFlags(hddPath: String): Int
external fun nativeApplyConfigSectorDns(
hddPath: String,
primaryDns: ByteArray,
secondaryDns: ByteArray,
)
}
}
@@ -83,6 +83,14 @@
app:iconTint="@color/xemu_black"
app:iconPadding="10dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/library_boot_dashboard_note"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@color/xemu_text_muted" />
<!-- Secondary actions: outlined pills with explicit spring (not in toggle group) -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_settings"
@@ -504,6 +504,78 @@
</com.google.android.material.button.MaterialButtonToggleGroup>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:background="@color/xemu_outline_variant" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:background="@drawable/section_header_background"
android:text="@string/settings_section_online"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/xemu_green_light" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_network_enable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_online_enable"
android:textColor="@color/xemu_text_muted" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginBottom="4dp"
android:text="@string/settings_online_hint"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@color/xemu_text_muted" />
<TextView
android:id="@+id/tv_insignia_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@color/xemu_text_muted" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_insignia_guide"
style="@style/Widget.Xemu.Button.Outlined.Pill"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="6dp"
android:text="@string/settings_insignia_guide_action" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_insignia_sign_up"
style="@style/Widget.Xemu.Button.Outlined.Pill"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:text="@string/settings_insignia_sign_up_action" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_prepare_insignia"
style="@style/Widget.Xemu.Button.Outlined.Pill"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:text="@string/settings_insignia_prepare_action" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_register_insignia"
style="@style/Widget.Xemu.Button.Outlined.Pill"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_insignia_register_action" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
@@ -641,40 +713,74 @@
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="@drawable/section_header_background"
android:text="@string/settings_section_hdd_tools"
android:text="@string/settings_section_advanced_experimental"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/xemu_green_light" />
<TextView
android:id="@+id/tv_hdd_tools_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:text="@string/settings_advanced_experimental_hint"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@color/xemu_text_muted" />
<TextView
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_toggle_advanced_experimental"
style="@style/Widget.Xemu.Button.Outlined.Pill"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="10dp"
android:text="@string/settings_hdd_tools_hint"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@color/xemu_text_muted" />
android:text="@string/settings_advanced_experimental_show" />
<LinearLayout
android:id="@+id/layout_advanced_experimental_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="@drawable/section_header_background"
android:text="@string/settings_section_hdd_tools"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="@color/xemu_green_light" />
<TextView
android:id="@+id/tv_hdd_tools_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@color/xemu_text_muted" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="10dp"
android:text="@string/settings_hdd_tools_hint"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="@color/xemu_text_muted" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_initialize_retail_hdd"
style="@style/Widget.Xemu.Button.Outlined.Pill"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:text="@string/settings_hdd_init_action" />
style="@style/Widget.Xemu.Button.Outlined.Pill"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:text="@string/settings_hdd_init_action" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_import_dashboard"
style="@style/Widget.Xemu.Button.Outlined.Pill"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_dashboard_import_action" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_import_dashboard"
style="@style/Widget.Xemu.Button.Outlined.Pill"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/settings_dashboard_import_action" />
</LinearLayout>
<View
android:layout_width="match_parent"
@@ -39,6 +39,7 @@
<string name="library_title">Game Library</string>
<string name="library_boot_dashboard">Boot Dashboard</string>
<string name="library_boot_dashboard_note">Requires a hard drive image with the Microsoft dashboard installed. Barebones HDD layouts will not work.</string>
<string name="library_snapshots">Snapshots</string>
<string name="library_convert_iso">Convert ISO to XISO</string>
<string name="library_view_list">List</string>
@@ -66,6 +67,8 @@
<string name="library_convert_resolve_failed">Could not resolve destination folder for selected game.</string>
<string name="library_restart_failed">Unable to restart. The selected game is no longer accessible.</string>
<string name="library_boot_dashboard_failed">Unable to boot the dashboard. One or more core files are no longer accessible.</string>
<string name="library_boot_dashboard_missing_retail">No retail dashboard was found on the current HDD image. Import the Microsoft dashboard in Settings first.</string>
<string name="library_boot_dashboard_incomplete_retail">Some retail dashboard files are present, but the dashboard looks incomplete. Re-import the Microsoft dashboard files in Settings.</string>
<string name="library_delete_option">Delete Game</string>
<string name="library_delete_title">Delete Game</string>
<string name="library_delete_action">Delete</string>
@@ -144,6 +147,37 @@
<string name="settings_audio_driver_opensles">OpenSL ES</string>
<string name="settings_audio_driver_aaudio">AAudio</string>
<string name="settings_audio_driver_disabled">Disabled</string>
<string name="settings_section_online">Online</string>
<string name="settings_online_enable">Enable online networking (Insignia)</string>
<string name="settings_online_hint">Turns on NAT networking for dashboards and Insignia. Use the helper buttons below to write the DNS preset and boot the Setup Assistant.</string>
<string name="settings_insignia_guide_action">Insignia Guide</string>
<string name="settings_insignia_sign_up_action">Sign Up With Insignia</string>
<string name="settings_insignia_prepare_action">Prepare Insignia Networking</string>
<string name="settings_insignia_register_action">Register With Setup Assistant</string>
<string name="settings_insignia_status_checking">Checking Insignia setup...</string>
<string name="settings_insignia_status_dns">DNS preset: %1$s / %2$s</string>
<string name="settings_insignia_status_dashboard_ready">Retail dashboard: detected on C:</string>
<string name="settings_insignia_status_dashboard_partial">Retail dashboard: some files were found, but the install looks incomplete</string>
<string name="settings_insignia_status_dashboard_missing">Retail dashboard: not detected on C:</string>
<string name="settings_insignia_status_dashboard_unavailable">Retail dashboard: unavailable because no local HDD image is configured</string>
<string name="settings_insignia_status_dashboard_error">Retail dashboard: failed to inspect the HDD image (%1$s)</string>
<string name="settings_insignia_status_eeprom_ready">EEPROM: ready for DNS updates</string>
<string name="settings_insignia_status_eeprom_missing">EEPROM: missing. Launch emulation once to create eeprom.bin</string>
<string name="settings_insignia_status_setup_selected">Setup Assistant: %1$s</string>
<string name="settings_insignia_status_setup_inaccessible">Setup Assistant: %1$s (pick it again to restore access)</string>
<string name="settings_insignia_status_setup_missing">Setup Assistant: not selected yet</string>
<string name="settings_insignia_prepare_working">Writing Insignia DNS settings...</string>
<string name="settings_insignia_prepare_success">Insignia networking is ready. Restart emulation, then sign in through the dashboard or boot the Setup Assistant.</string>
<string name="settings_insignia_prepare_success_missing_dashboard">Insignia DNS was written, but the retail dashboard is still missing from C:.</string>
<string name="settings_insignia_prepare_failed">Failed to prepare Insignia networking: %1$s</string>
<string name="settings_insignia_prepare_no_hdd">No local HDD image is configured. Pick one in the setup wizard first.</string>
<string name="settings_insignia_prepare_no_eeprom">EEPROM not found yet. Launch emulation once to create eeprom.bin, then try again.</string>
<string name="settings_insignia_register_title">Setup Assistant</string>
<string name="settings_insignia_register_message">Boot %1$s now? This launches the Insignia Setup Assistant disc image.</string>
<string name="settings_insignia_register_boot_action">Boot Setup Assistant</string>
<string name="settings_insignia_register_choose_new">Choose Different Image</string>
<string name="settings_insignia_register_pick_prompt">Pick your Insignia Setup Assistant disc image.</string>
<string name="settings_insignia_setup_source_unknown">Unknown source</string>
<string name="settings_section_eeprom">EEPROM</string>
<string name="settings_eeprom_language">EEPROM Language</string>
<string name="settings_eeprom_language_english">English</string>
@@ -183,6 +217,10 @@
<string name="settings_saved_with_eeprom">Settings and EEPROM saved</string>
<string name="settings_saved_eeprom_missing">Settings saved. Launch a game once, then edit EEPROM.</string>
<string name="settings_saved_eeprom_failed">Settings saved, but EEPROM update failed</string>
<string name="settings_section_advanced_experimental">Advanced / Experimental</string>
<string name="settings_advanced_experimental_hint">Less common HDD management tools live here so the main settings flow stays focused on everyday setup.</string>
<string name="settings_advanced_experimental_show">Show Advanced / Experimental Tools</string>
<string name="settings_advanced_experimental_hide">Hide Advanced / Experimental Tools</string>
<string name="settings_section_hdd_tools">HDD Tools</string>
<string name="settings_hdd_tools_hint">Initializes the current local HDD image with a stock retail layout or optional extended F/G partitions. Raw and QCOW2 images are supported. This does not install a dashboard.</string>
<string name="settings_hdd_status_missing">No local HDD image is configured. Pick one in the setup wizard first.</string>