diff --git a/dm/host/hle_fs.c b/dm/host/hle_fs.c index 7f40189c31..a8e29f050e 100644 --- a/dm/host/hle_fs.c +++ b/dm/host/hle_fs.c @@ -596,13 +596,19 @@ HLE_FORCE("_ZN2nn2fs17MountAddOnContentEPKciPvm", "fsp-srv; add-on content → D } return mount_aoc(name, dir); } -// System data archives are firmware content, which the host does not have. The one a game has needed so far is 0100000000000802, MiiModel (Mario Kart 8 Deluxe mounts it as MiiResource: and -// builds Mii heads from it at boot, for the title screen's demo race): the host writes a stand-in with every part empty (hle_mii.c dm_mii_standin) into _sysdata/ and mounts it -// read-only. Any other archive answers "target not found", as for a title that is not installed +// System data archives are firmware content, which the host does not ship. A player can supply their own console's: /sysdata// holds an archive's +// RomFS files, mounted read-only (Nintendo Switch Sports Resort's MiiResource, 0400000000000849: the Mii head textures and shapes). Without it, 0100000000000802, MiiModel (Mario Kart 8 +// Deluxe mounts it as MiiResource: and builds Mii heads from it at boot, for the title screen's demo race), is a stand-in with every part empty (hle_mii.c dm_mii_standin), written into +// _sysdata/; any other archive answers "target not found", as for a title that is not installed #define SYSDATA_MIIMODEL 0x0100000000000802ull int dm_mii_standin(const char *dir); +static int sysdata_own(char *dir, size_t n, uint64_t id) { + const char *sys = getenv("DM_SYSTEM_DIR"); struct stat st; + return sys && *sys && snprintf(dir, n, "%s/sysdata/%016llx", sys, (unsigned long long)id) < (int)n && !stat(dir, &st) && S_ISDIR(st.st_mode); +} static int sysdata_dir(char *dir, size_t n, uint64_t id) { char t[1024]; + if (sysdata_own(dir, n, id)) return 1; if (id != SYSDATA_MIIMODEL || snprintf(dir, n, "%s_sysdata/%016llx", envdir("DM_TEMP", "run/platform/temp", t, sizeof t), (unsigned long long)id) >= (int)n) return 0; return dm_mii_standin(dir); } @@ -611,9 +617,10 @@ HLE_FORCE("_ZN2nn2fs29QueryMountSystemDataCacheSizeEPmNS_3ncm12SystemDataIdE", " *out = sysdata_dir(dir, sizeof dir, id) ? romfs_work_size(dir) : 0x1000; return RES_OK; } -HLE_FORCE("_ZN2nn2fs15MountSystemDataEPKcNS_3ncm12SystemDataIdEPvm", "fsp-srv; system data: the host's MiiModel stand-in, nothing else", Result, h_fs_MountSysData, const char *name, uint64_t id, void *cache, size_t sz) { +HLE_FORCE("_ZN2nn2fs15MountSystemDataEPKcNS_3ncm12SystemDataIdEPvm", "fsp-srv; system data: the console's own from DM_SYSTEM_DIR, else the host's MiiModel stand-in", Result, h_fs_MountSysData, const char *name, uint64_t id, void *cache, size_t sz) { (void)cache; (void)sz; char dir[1024]; - if (!sysdata_dir(dir, sizeof dir, id)) { LOG("[RED] fs mount system data %s: the host has no %016llx", name, (unsigned long long)id); return RES_TARGET_NOT_FOUND; } + if (!sysdata_dir(dir, sizeof dir, id)) { LOG("[RED] fs mount system data %s: the host has no %016llx (a console's own goes in /sysdata/%016llx)", name, (unsigned long long)id, (unsigned long long)id); return RES_TARGET_NOT_FOUND; } + char own[1024]; if (sysdata_own(own, sizeof own, id)) LOG("fs mount system data %s: %016llx from %s", name, (unsigned long long)id, dir); return mount(name, dir, 1); } static int host_mounted(const char *name) { diff --git a/dm/host/hle_mii.c b/dm/host/hle_mii.c index d2148a0f5d..286e4f8123 100644 --- a/dm/host/hle_mii.c +++ b/dm/host/hle_mii.c @@ -1,5 +1,6 @@ // Mii service layer (nn::mii::Database, the Mii editor applet). On hardware Database talks to the mii:u service (an IDatabaseService the SDK keeps at Database +0) and AppendMii / -// ShowMiiEdit start the MiiEdit library applet; the host has neither. It answers as a console with no Mii of its own: the six default Miis, an empty database, and the editor cancelled. +// ShowMiiEdit start the MiiEdit library applet; the host has neither. It answers with the six default Miis, the editor cancelled, and the database empty unless the player supplies their +// console's own (mii_db_load: the mii service's system save, read-only). // Mario Kart 8 Deluxe (SDK 4.4.0) initializes a Database at boot (mii:e and mii:u were the first services it opened itself) and builds 12 random Miis for its title screen's demo race; their // heads are drawn by the SDK's Mii rendering code linked into main, from the MiiModel system data (the host's stand-in below, mounted by hle_fs.c MountSystemData). // The CharInfo the host builds (0x58 bytes) is the system's default Mii as the mii service's StoreData::BuildDefault makes it: the 3DS-era default parameters, with the colours converted to @@ -49,38 +50,101 @@ static void mii_default(mii_charinfo *c, int gender, int index) { c->mole_scale = 4; c->mole_x = 2; c->mole_y = 20; } +// The console's Mii database, from the player's own console: /save/8000000000000030/MiiDatabase.dat, the mii service's system save as the Switch and the Switch 2 keep it +// (Switch 2 program 0400000000000039 mounts that save as "mii" and reads mii:/MiiDatabase.dat). The file is 0x1a98 bytes: "NFDB", 100 StoreData of 0x44 bytes, the version (1) at +0x1a94, +// the count at +0x1a95 and a CRC-16/CCITT (polynomial 0x1021, initial 0) of the bytes before it, big-endian, at +0x1a96. A StoreData is the CoreData (seven 32-bit words of bit fields and a +// 10-character nickname), the create ID and two CRCs; mii_from_store unpacks it into the CharInfo the SDK's Get returns, as the mii service does (its stored eyebrow y is 3 below the +// CharInfo's). Read once, never written: a game's changes (AppendMii, the editor) do not reach it +enum { MII_DB_SIZE = 0x1a98, MII_DB_MAX = 100, MII_STORE = 0x44 }; +static uint16_t mii_crc16(const uint8_t *p, size_t n) { + uint16_t c = 0; + for (size_t i = 0; i < n; i++) { c ^= (uint16_t)(p[i] << 8); for (int b = 0; b < 8; b++) c = (uint16_t)(c & 0x8000 ? (c << 1) ^ 0x1021 : c << 1); } + return c; +} +static unsigned mii_bits(const uint8_t *w, int word, int at, int len) { uint32_t x; memcpy(&x, w + 4 * word, 4); return (x >> at) & ((1u << len) - 1); } +static void mii_from_store(mii_charinfo *c, const uint8_t *sd) { + memset(c, 0, sizeof *c); + memcpy(c->create_id, sd + 0x30, 16); memcpy(c->nickname, sd + 0x1c, 20); + c->hair_type = mii_bits(sd, 0, 0, 8); c->height = mii_bits(sd, 0, 8, 7); c->mole_type = mii_bits(sd, 0, 15, 1); c->build = mii_bits(sd, 0, 16, 7); + c->hair_flip = mii_bits(sd, 0, 23, 1); c->hair_color = mii_bits(sd, 0, 24, 7); c->type = mii_bits(sd, 0, 31, 1); + c->eye_color = mii_bits(sd, 1, 0, 7); c->gender = mii_bits(sd, 1, 7, 1); c->eyebrow_color = mii_bits(sd, 1, 8, 7); c->mouth_color = mii_bits(sd, 1, 16, 7); c->beard_color = mii_bits(sd, 1, 24, 7); + c->glass_color = mii_bits(sd, 2, 0, 7); c->eye_type = mii_bits(sd, 2, 8, 6); c->region_move = mii_bits(sd, 2, 14, 2); c->mouth_type = mii_bits(sd, 2, 16, 6); + c->font_region = mii_bits(sd, 2, 22, 2); c->eye_y = mii_bits(sd, 2, 24, 5); c->glass_scale = mii_bits(sd, 2, 29, 3); + c->eyebrow_type = mii_bits(sd, 3, 0, 5); c->mustache_type = mii_bits(sd, 3, 5, 3); c->nose_type = mii_bits(sd, 3, 8, 5); c->beard_type = mii_bits(sd, 3, 13, 3); + c->nose_y = mii_bits(sd, 3, 16, 5); c->mouth_aspect = mii_bits(sd, 3, 21, 3); c->mouth_y = mii_bits(sd, 3, 24, 5); c->eyebrow_aspect = mii_bits(sd, 3, 29, 3); + c->mustache_y = mii_bits(sd, 4, 0, 5); c->eye_rotate = mii_bits(sd, 4, 5, 3); c->glass_y = mii_bits(sd, 4, 8, 5); c->eye_aspect = mii_bits(sd, 4, 13, 3); + c->mole_x = mii_bits(sd, 4, 16, 5); c->eye_scale = mii_bits(sd, 4, 21, 3); c->mole_y = mii_bits(sd, 4, 24, 5); + c->glass_type = mii_bits(sd, 5, 0, 5); c->favorite_color = mii_bits(sd, 5, 8, 4); c->faceline_type = mii_bits(sd, 5, 12, 4); c->faceline_color = mii_bits(sd, 5, 16, 4); + c->faceline_wrinkle = mii_bits(sd, 5, 20, 4); c->faceline_make = mii_bits(sd, 5, 24, 4); c->eye_x = mii_bits(sd, 5, 28, 4); + c->eyebrow_scale = mii_bits(sd, 6, 0, 4); c->eyebrow_rotate = mii_bits(sd, 6, 4, 4); c->eyebrow_x = mii_bits(sd, 6, 8, 4); c->eyebrow_y = (uint8_t)(mii_bits(sd, 6, 12, 4) + 3); + c->nose_scale = mii_bits(sd, 6, 16, 4); c->mouth_scale = mii_bits(sd, 6, 20, 4); c->mustache_scale = mii_bits(sd, 6, 24, 4); c->mole_scale = mii_bits(sd, 6, 28, 4); +} +static mii_charinfo g_mii_db[MII_DB_MAX]; +static int g_mii_n; +// <0: the file is not a database (logged); else the Miis read +static int mii_db_parse(const uint8_t *b, size_t size, mii_charinfo *out) { + if (size != MII_DB_SIZE || memcmp(b, "NFDB", 4)) return -1; + if (b[0x1a94] != 1 || b[0x1a95] > MII_DB_MAX) return -2; + if (mii_crc16(b, 0x1a96) != (uint16_t)(b[0x1a96] << 8 | b[0x1a97])) return -3; + for (int i = 0; i < b[0x1a95]; i++) mii_from_store(&out[i], b + 4 + (size_t)i * MII_STORE); + return b[0x1a95]; +} +static void mii_db_load(void) { + const char *sys = getenv("DM_SYSTEM_DIR"); char p[1024]; g_mii_n = 0; + if (!sys || !*sys) return; + snprintf(p, sizeof p, "%s/save/8000000000000030/MiiDatabase.dat", sys); + FILE *f = fopen(p, "rb"); if (!f) return; + uint8_t *b = malloc(MII_DB_SIZE + 1); size_t n = b ? fread(b, 1, MII_DB_SIZE + 1, f) : 0; fclose(f); + int r = b ? mii_db_parse(b, n, g_mii_db) : -1; free(b); + static const char *why[] = {"", "not a Mii database (size or NFDB)", "an unknown version or count", "its CRC does not match"}; + if (r < 0) LOG("[RED] mii: %s is %s: the database stays empty", p, why[-r]); + else { g_mii_n = r; LOG("mii: %d Miis of the console's database from %s", r, p); } +} + // The object keeps its service pointer at +0, which the constructor zeroes and the destructor releases through its vtable when set; the host never sets it -HLE_FORCE("_ZN2nn3mii8Database10InitializeEv", "mii:u service; the host's database: the six default Miis, none of the console's own", Result, h_mii_Init, void *self) { - (void)self; LOG("mii: Database::Initialize (the six default Miis, none of the console's own)"); return RES_OK; +HLE_FORCE("_ZN2nn3mii8Database10InitializeEv", "mii:u service; the host's database: the six default Miis and the console's own when supplied", Result, h_mii_Init, void *self) { + (void)self; static pthread_once_t once = PTHREAD_ONCE_INIT; pthread_once(&once, mii_db_load); + LOG("mii: Database::Initialize (the six default Miis, %d of the console's own)", g_mii_n); return RES_OK; } HLE_FORCE("_ZN2nn3mii8Database8FinalizeEv", "mii:u service; nothing to release", void, h_mii_Fin, void *self) { (void)self; } HLE_FORCE("_ZNK2nn3mii8Database11IsAvailableEv", "mii:u service; always available", int, h_mii_Avail, const void *self) { (void)self; return 1; } -HLE_FORCE("_ZN2nn3mii8Database9IsUpdatedEi", "mii:u service; the empty database never changes", int, h_mii_Updated, void *self, int src) { (void)self; (void)src; return 0; } -HLE_FORCE("_ZNK2nn3mii8Database14IsFullDatabaseEv", "mii:u service; empty, so not full", int, h_mii_Full, const void *self) { (void)self; return 0; } -// Source flags: 1 the console's database (empty here), 2 the six default Miis every console has. Mario Kart 8 Deluxe asks for the defaults with room for 6 and never reads the count +HLE_FORCE("_ZN2nn3mii8Database9IsUpdatedEi", "mii:u service; the database never changes", int, h_mii_Updated, void *self, int src) { (void)self; (void)src; return 0; } +HLE_FORCE("_ZNK2nn3mii8Database14IsFullDatabaseEv", "mii:u service; full at 100 Miis", int, h_mii_Full, const void *self) { (void)self; return g_mii_n >= MII_DB_MAX; } +// Source flags: 1 the console's database, 2 the six default Miis every console has; the database's come first, as the mii service lists them. Mario Kart 8 Deluxe asks for the defaults with room for 6 and never reads the count // (main+0x81e954): its boot-time list of Mii icons (main+0x419dc8) builds a head for each of the six, and zeroed entries failed the model's CharInfo check enum { MII_SRC_DATABASE = 1, MII_SRC_DEFAULT = 2, MII_DEFAULTS = 6 }; static int mii_get(void *buf, int n, int src, size_t stride) { int k = 0; - for (; (src & MII_SRC_DEFAULT) && buf && k < n && k < MII_DEFAULTS; k++) { + for (int i = 0; (src & MII_SRC_DATABASE) && buf && k < n && i < g_mii_n; i++, k++) { + uint8_t *e = (uint8_t *)buf + (size_t)k * stride; + memcpy(e, &g_mii_db[i], sizeof(mii_charinfo)); + if (stride > sizeof(mii_charinfo)) { uint32_t source = 0; memcpy(e + sizeof(mii_charinfo), &source, 4); } // CharInfoElement: the CharInfo and its source (0 = database) + } + for (int d = 0; (src & MII_SRC_DEFAULT) && buf && k < n && d < MII_DEFAULTS; d++, k++) { uint8_t *e = (uint8_t *)buf + (size_t)k * stride; - mii_default((mii_charinfo *)e, k >= 3, k); - if (stride > sizeof(mii_charinfo)) { uint32_t source = 1; memcpy(e + sizeof(mii_charinfo), &source, 4); } // CharInfoElement: the CharInfo and its source (1 = default) + mii_default((mii_charinfo *)e, d >= 3, d); + if (stride > sizeof(mii_charinfo)) { uint32_t source = 1; memcpy(e + sizeof(mii_charinfo), &source, 4); } // 1 = default } return k; } -HLE_FORCE("_ZNK2nn3mii8Database8GetCountEi", "mii:u service; the six default Miis", int, h_mii_Count, const void *self, int src) { (void)self; return src & MII_SRC_DEFAULT ? MII_DEFAULTS : 0; } -HLE_FORCE("_ZN2nn3mii8Database3GetEPiPNS0_15CharInfoElementEii", "mii:u service; the six default Miis", Result, h_mii_GetE, void *self, int *out, void *buf, int n, int src) { +HLE_FORCE("_ZNK2nn3mii8Database8GetCountEi", "mii:u service; the console's Miis and the six default ones", int, h_mii_Count, const void *self, int src) { + (void)self; return (src & MII_SRC_DATABASE ? g_mii_n : 0) + (src & MII_SRC_DEFAULT ? MII_DEFAULTS : 0); +} +HLE_FORCE("_ZN2nn3mii8Database3GetEPiPNS0_15CharInfoElementEii", "mii:u service; the console's Miis and the six default ones", Result, h_mii_GetE, void *self, int *out, void *buf, int n, int src) { (void)self; *out = mii_get(buf, n, src, sizeof(mii_charinfo) + 4); return RES_OK; } -HLE_FORCE("_ZN2nn3mii8Database3GetEPiPNS0_8CharInfoEii", "mii:u service; the six default Miis", Result, h_mii_Get, void *self, int *out, void *buf, int n, int src) { +HLE_FORCE("_ZN2nn3mii8Database3GetEPiPNS0_8CharInfoEii", "mii:u service; the console's Miis and the six default ones", Result, h_mii_Get, void *self, int *out, void *buf, int n, int src) { (void)self; *out = mii_get(buf, n, src, sizeof(mii_charinfo)); return RES_OK; } -HLE_FORCE("_ZNK2nn3mii8Database8GetIndexEPiRKNS0_8CharInfoE", "mii:u service; no Mii is in the database", Result, h_mii_Index, const void *self, int *out, const void *info) { - (void)self; (void)info; *out = -1; return MII_RES_NOT_FOUND; +// A Mii is found by its create ID (CharInfo +0) +static int mii_find(const void *info) { for (int i = 0; info && i < g_mii_n; i++) if (!memcmp(g_mii_db[i].create_id, info, 16)) return i; return -1; } +HLE_FORCE("_ZNK2nn3mii8Database8GetIndexEPiRKNS0_8CharInfoE", "mii:u service; the Mii's place in the console's database", Result, h_mii_Index, const void *self, int *out, const void *info) { + (void)self; *out = mii_find(info); return *out < 0 ? MII_RES_NOT_FOUND : RES_OK; } -HLE_FORCE("_ZNK2nn3mii8Database12UpdateLatestEPNS0_8CharInfoEi", "mii:u service; no Mii is in the database", Result, h_mii_Latest, const void *self, void *info, int src) { - (void)self; (void)info; (void)src; return MII_RES_NOT_FOUND; +HLE_FORCE("_ZNK2nn3mii8Database12UpdateLatestEPNS0_8CharInfoEi", "mii:u service; the database's copy of the Mii", Result, h_mii_Latest, const void *self, void *info, int src) { + (void)self; int i = (src & MII_SRC_DATABASE) ? mii_find(info) : -1; + if (i < 0) return MII_RES_NOT_FOUND; + memcpy(info, &g_mii_db[i], sizeof(mii_charinfo)); return RES_OK; } // RandomDesc {age, gender, race}: gender 0 male, 1 female, 2 either. The host's "random" Mii is the default one of that gender HLE_FORCE("_ZN2nn3mii8Database11BuildRandomEPNS0_8CharInfoERKNS0_10RandomDescE", "mii:u service; the default Mii", Result, h_mii_Random, void *self, mii_charinfo *out, const int *desc) { diff --git a/dm/host/tests/Makefile b/dm/host/tests/Makefile index 8029418d52..e928d70639 100644 --- a/dm/host/tests/Makefile +++ b/dm/host/tests/Makefile @@ -12,7 +12,7 @@ QUIET = $(if $(findstring -fsanitize,$(SAN)),,2>/dev/null) GINC = $(if $(strip $(GAME)),-I../gen/$(GAME) -I../../games/$(GAME),$(error GAME is not set: make GAME=: $(shell python3 ../../games/game.py list | awk -F'\t' '{printf "%s (%s) ", $$1, $$3}'))) $(B)/t_fs $(B)/t_fs_case $(B)/t_fs_commit_list $(B)/t_fs_commit_list_tsan: ../romfs_image.h GAME_H = ../gen/$(GAME)/game_profile.h -run: $(B)/t_net_eventfd $(B)/t_language $(B)/t_motion $(B)/t_heap_size $(B)/t_touch $(B)/t_gesture $(B)/t_oe $(B)/t_fs $(B)/t_fs_commit_list $(B)/t_thread_leak $(B)/t_fs_case $(B)/t_audio $(B)/t_output_stream $(B)/t_audio_spsc $(B)/t_swkbd $(B)/t_altstack $(B)/t_condvar $(B)/t_condvar_old $(B)/t_pollwait $(B)/t_clockwait $(B)/t_pause $(B)/t_pause_stress $(B)/t_pctl $(B)/t_sf $(B)/t_time $(B)/t_shared_font $(B)/t_vorbis $(B)/t_vorbis_divides $(B)/t_hid $(B)/t_haptics $(B)/t_haptics_apple $(B)/t_cheats $(B)/t_pad_hid $(B)/t_switch2 +run: $(B)/t_mii $(B)/t_net_eventfd $(B)/t_language $(B)/t_motion $(B)/t_heap_size $(B)/t_touch $(B)/t_gesture $(B)/t_oe $(B)/t_fs $(B)/t_fs_commit_list $(B)/t_thread_leak $(B)/t_fs_case $(B)/t_audio $(B)/t_output_stream $(B)/t_audio_spsc $(B)/t_swkbd $(B)/t_altstack $(B)/t_condvar $(B)/t_condvar_old $(B)/t_pollwait $(B)/t_clockwait $(B)/t_pause $(B)/t_pause_stress $(B)/t_pctl $(B)/t_sf $(B)/t_time $(B)/t_shared_font $(B)/t_vorbis $(B)/t_vorbis_divides $(B)/t_hid $(B)/t_haptics $(B)/t_haptics_apple $(B)/t_cheats $(B)/t_pad_hid $(B)/t_switch2 $(B)/t_language $(B)/t_heap_size $(B)/t_touch @@ -26,6 +26,7 @@ run: $(B)/t_net_eventfd $(B)/t_language $(B)/t_motion $(B)/t_heap_size $(B)/t_to TMPDIR="$(abspath $(B))" $(B)/t_save_sync_metadata $(QUIET) $(B)/t_audio $(B)/t_output_stream + $(B)/t_mii $(B)/t_audio_spsc $(B)/t_audio_stats $(B)/t_fpslock @@ -161,6 +162,8 @@ save-sync: $(B)/t_save_sync $(B)/t_save_sync_metadata TMPDIR="$(abspath $(B))" $(B)/t_save_sync_metadata $(QUIET) $(B)/t_audio: t_audio.c ../hle_audio.c ../hle_bigstack.c ../hle.h ../dm.h $(GAME_H) | $(B) $(CLANG) -O1 -Wno-unused-function -I.. $(GINC) -o $@ t_audio.c ../hle_bigstack.c -framework AudioToolbox +$(B)/t_mii: t_mii.c ../hle_mii.c ../hle.h ../dm.h $(GAME_H) | $(B) + $(CLANG) -O1 -g -Wall -Werror -Wno-unused-function -fsanitize=address,undefined -I.. $(GINC) -o $@ t_mii.c $(B)/t_output_stream: t_output_stream.c ../hle_audio.c ../hle_bigstack.c ../hle.h ../dm.h $(GAME_H) | $(B) $(CLANG) -O1 -Wall -Wno-unused-function -I.. $(GINC) -o $@ t_output_stream.c ../hle_bigstack.c -framework AudioToolbox $(B)/t_audio_spsc: t_audio_spsc.c ../hle_audio.c ../hle_bigstack.c ../hle.h ../dm.h $(GAME_H) | $(B) diff --git a/dm/host/tests/t_mii.c b/dm/host/tests/t_mii.c new file mode 100644 index 0000000000..3963f97ec3 --- /dev/null +++ b/dm/host/tests/t_mii.c @@ -0,0 +1,65 @@ +// hle_mii.c unit test: the console's Mii database (/save/8000000000000030/MiiDatabase.dat). A StoreData's bit fields unpack into the CharInfo fields they hold +// (eyebrow y 3 higher), a file with a wrong CRC, size or version leaves the database empty, and Get lists the database's Miis (source 0) before the six default ones (source 1) +#include "../hle_mii.c" +#include +uint64_t dm_entropy64(void) { return 0x0123456789abcdefull; } +void dm_applet_enter(const char *name) { (void)name; } +void dm_applet_leave(void) {} + +static int fails; +#define CHECK(c, ...) do { if (!(c)) { printf("FAIL " __VA_ARGS__); puts(""); fails++; } } while (0) +static void put_bits(uint8_t *sd, int word, int at, int len, unsigned v) { + uint32_t x; memcpy(&x, sd + 4 * word, 4); x = (x & ~(((1u << len) - 1) << at)) | ((v & ((1u << len) - 1)) << at); memcpy(sd + 4 * word, &x, 4); +} +static void db_crc(uint8_t *b) { uint16_t c = mii_crc16(b, 0x1a96); b[0x1a96] = (uint8_t)(c >> 8); b[0x1a97] = (uint8_t)c; } + +int main(void) { + static uint8_t db[MII_DB_SIZE]; + memcpy(db, "NFDB", 4); db[0x1a94] = 1; db[0x1a95] = 2; + uint8_t *a = db + 4, *b = db + 4 + MII_STORE; + put_bits(a, 0, 0, 8, 131); put_bits(a, 0, 8, 7, 100); put_bits(a, 0, 24, 7, 99); put_bits(a, 0, 31, 1, 1); // hair 131, height 100, hair colour 99, special + put_bits(a, 1, 7, 1, 1); put_bits(a, 2, 8, 6, 59); put_bits(a, 2, 24, 5, 18); put_bits(a, 3, 0, 5, 23); // female, eye type 59, eye y 18, eyebrow type 23 + put_bits(a, 5, 8, 4, 11); put_bits(a, 5, 28, 4, 12); put_bits(a, 6, 12, 4, 15); put_bits(a, 6, 28, 4, 8); // favourite colour 11, eye x 12, eyebrow y 15 (stored), mole scale 8 + const char nick[] = "Jo"; for (int i = 0; nick[i]; i++) { a[0x1c + 2 * i] = (uint8_t)nick[i]; } + for (int i = 0; i < 16; i++) { a[0x30 + i] = (uint8_t)(0xa0 + i); b[0x30 + i] = (uint8_t)(0xb0 + i); } + db_crc(db); + + mii_charinfo c[MII_DB_MAX]; + CHECK(mii_db_parse(db, sizeof db, c) == 2, "two Miis parsed"); + CHECK(c[0].hair_type == 131 && c[0].height == 100 && c[0].hair_color == 99 && c[0].type == 1, "word 0: hair %d height %d colour %d type %d", c[0].hair_type, c[0].height, c[0].hair_color, c[0].type); + CHECK(c[0].gender == 1 && c[0].eye_type == 59 && c[0].eye_y == 18 && c[0].eyebrow_type == 23, "words 1-3"); + CHECK(c[0].favorite_color == 11 && c[0].eye_x == 12 && c[0].mole_scale == 8, "words 5-6"); + CHECK(c[0].eyebrow_y == 18, "eyebrow y is the stored one + 3: %d", c[0].eyebrow_y); + CHECK(c[0].nickname[0] == 'J' && c[0].nickname[1] == 'o' && c[0].nickname[2] == 0 && c[0].nickname[10] == 0, "nickname"); + CHECK(c[0].create_id[0] == 0xa0 && c[0].create_id[15] == 0xaf && c[1].create_id[0] == 0xb0, "create IDs"); + CHECK(c[0].height == 100 && c[1].height == 0, "the second Mii is its own record"); + + static uint8_t bad[MII_DB_SIZE]; memcpy(bad, db, sizeof db); bad[0x100] ^= 1; + CHECK(mii_db_parse(bad, sizeof bad, c) == -3, "a CRC mismatch is refused"); + CHECK(mii_db_parse(db, sizeof db - 1, c) == -1, "a short file is refused"); + memcpy(bad, db, sizeof db); bad[0x1a94] = 2; db_crc(bad); + CHECK(mii_db_parse(bad, sizeof bad, c) == -2, "version 2 is refused"); + + // Through the service calls, from a DM_SYSTEM_DIR in the build folder + char root[] = "build/t_mii.XXXXXX", p[256]; + CHECK(mkdtemp(root) != NULL, "temporary folder"); + snprintf(p, sizeof p, "%s/save", root); mkdir(p, 0755); snprintf(p, sizeof p, "%s/save/8000000000000030", root); mkdir(p, 0755); + snprintf(p, sizeof p, "%s/save/8000000000000030/MiiDatabase.dat", root); + FILE *f = fopen(p, "wb"); CHECK(f && fwrite(db, 1, sizeof db, f) == sizeof db, "write the database"); if (f) fclose(f); + setenv("DM_SYSTEM_DIR", root, 1); + CHECK(h_mii_Init(NULL) == RES_OK && g_mii_n == 2, "Initialize reads the database: %d", g_mii_n); + CHECK(h_mii_Count(NULL, MII_SRC_DATABASE) == 2 && h_mii_Count(NULL, MII_SRC_DEFAULT) == 6 && h_mii_Count(NULL, 3) == 8, "counts"); + struct __attribute__((packed)) { mii_charinfo c; uint32_t source; } el[10]; int n = -1; + CHECK(h_mii_GetE(NULL, &n, el, 10, 3) == RES_OK && n == 8, "Get of both: %d", n); + CHECK(el[0].source == 0 && el[1].source == 0 && el[2].source == 1 && !memcmp(el[0].c.create_id, a + 0x30, 16), "the database's Miis first, then the defaults"); + CHECK(h_mii_GetE(NULL, &n, el, 1, 3) == RES_OK && n == 1, "Get stops at the room given"); + int idx = -1; + CHECK(h_mii_Index(NULL, &idx, &el[0].c) == RES_OK && idx == 0, "GetIndex by create ID"); + CHECK(h_mii_Index(NULL, &idx, &el[2].c) == MII_RES_NOT_FOUND, "a default Mii is not in the database"); + mii_charinfo latest = el[1].c; latest.height = 7; + CHECK(h_mii_Latest(NULL, &latest, MII_SRC_DATABASE) == RES_OK && latest.height == c[1].height, "UpdateLatest gives the database's copy"); + unlink(p); snprintf(p, sizeof p, "%s/save/8000000000000030", root); rmdir(p); snprintf(p, sizeof p, "%s/save", root); rmdir(p); rmdir(root); + + printf("t_mii: %s\n", fails ? "FAILED" : "all checks pass"); + return fails != 0; +} diff --git a/dm/ios/Sources/main.m b/dm/ios/Sources/main.m index 7cd198ec54..78389b385b 100644 --- a/dm/ios/Sources/main.m +++ b/dm/ios/Sources/main.m @@ -178,6 +178,7 @@ static int boot_env(void) { if (!dm) { romfs_identity_report(doc); fprintf(stderr, "boot: game files not found (Documents/romfs.img, Documents/romfs or /romfs)\n"); return 0; } setenv("DM_SAVE", [doc stringByAppendingPathComponent:@"save"].fileSystemRepresentation, 0); setenv("DM_TEMP", [doc stringByAppendingPathComponent:@"temp"].fileSystemRepresentation, 0); + setenv("DM_SYSTEM_DIR", [doc stringByAppendingPathComponent:@"system"].fileSystemRepresentation, 0); setenv("DM_GUEST_FILE_DIR", NSTemporaryDirectory().fileSystemRepresentation, 0); // DM_GUEST_FILE (hle_svc.c) keeps the guest's memory in files there setenv("DM_FONT_DIR", [[res stringByAppendingPathComponent:@"dm"] stringByAppendingPathComponent:@"font"].fileSystemRepresentation, 0); // Shared-font stand-ins (standard.ttf / nintendo_ext.ttf): iOS lacks the macOS font path aoc_locate(); // add-on content: Documents/aoc, else /aoc; an empty Documents/aoc explicitly selects no DLC diff --git a/dm/mac/launcher.c b/dm/mac/launcher.c index 1268b1837c..d4f8a766c1 100644 --- a/dm/mac/launcher.c +++ b/dm/mac/launcher.c @@ -206,6 +206,7 @@ int main(int argc, char **argv) { if (shown == 2) saved_choices(); cloud_saves_folder(); setenv("DM_ROOT", sup, 0); setenv("DM_ROMFS", romfs, 0); setenv("DM_SAVE", save, 0); setenv("DM_TEMP", temp, 0); + snprintf(p, sizeof p, "%s/Library/Application Support/nxnative/system", home); setenv("DM_SYSTEM_DIR", p, 0); // the player's own console files, for every game snprintf(p, sizeof p, "%s/msl", res); setenv("DM_MSL_DIR", p, 0); snprintf(p, sizeof p, "%s/font", dm); setenv("DM_FONT_DIR", p, 0); snprintf(p, sizeof p, "%s/shader_map.tsv", dm); if (!stat(p, &st)) setenv("DM_NVN_SHMAP", p, 0); diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index 3a52815bc9..b20e10619f 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -320,6 +320,7 @@ One line per symptom in the [subsystem references](../reference/): what you see - `CRASH sig=10` (SIGBUS) in a network thread right after start (Minecraft's `NetherNet Network`, `main+0x18b2ae8`, `x0=0x4000`), at a load from its own stack. → [Event descriptors in an `fd_set`](../reference/services.md#event-descriptors-in-an-fd_set) - `Break#1 reason=0 Result=0` right after the first save is created, then exit 42; the stack has `nn::diag::detail::OnAssertionFailure` under `nn::image::JpegDecoder::Decode` (`sdk+0x394e4`), called from the game (Burnout Paradise Remastered, SDK 9.3). → [The local user's profile image](../reference/services.md#the-local-users-profile-image) - `[RED] sf: the game connects to service mii:e … mii:u itself`, then an abort. → [Mii database answered by the host](../reference/services.md#mii-database-answered-by-the-host) +- the game shows only the six default Miis, and a Switch 2 game logs `[RED] fs mount system data MiiResource: the host has no 0400000000000849` (Nintendo Switch Sports Resort: Mii heads cannot be drawn). → [A console's own Mii database and system data](../reference/services.md#a-consoles-own-mii-database-and-system-data) - the abort with no message (`SVC 0x15` / `0x1f` / `0x21` placeholders, then `Break` with `sm::Finalize` under `ShopServiceAccessServer::Proxy::Initialize`) from `ec::InitializeForShopServiceAccessors`, called from Unreal's `FOnlineSubsystemSwitch::Init` at the start of `nnMain`. → [Shop service accessors](../reference/services.md#shop-service-accessors) - a purchase watcher crashes in `nn::ec::InitializePurchasedEvent` after account selection, before it can wait for a notification. → [Offline purchased-item notifications](../reference/services.md#offline-purchased-item-notifications) - abort in `nn::sm::Initialize` under `nn::pctl::IsFreeCommunicationAvailable` (Engage, entering its first battle map). → [Parental controls and free communication](../reference/services.md#parental-controls-and-free-communication) diff --git a/docs/env_switches.tsv b/docs/env_switches.tsv index 31bf522bd9..d0bf8e0123 100644 --- a/docs/env_switches.tsv +++ b/docs/env_switches.tsv @@ -351,6 +351,7 @@ DM_SW2_MOTION switch `=0` leaves out the motion of Switch 2 controllers (buttons DM_SW2_MOTION_SIDE switch `=L` takes a Joy-Con 2 pair's motion from the left Joy-Con instead of the right one DM_SW2_VERBOSE diagnostic `=1` logs the first report of each second of every Switch 2 controller: buttons, sticks, raw sensors, battery DM_SYMCHECK diagnostic `=1` checks every hashed module symbol lookup against a linear search and logs a red line for each mismatch +DM_SYSTEM_DIR path The player's own console files, for every game: `sysdata/<16 hex digits>/` a system data archive's RomFS files, `save/8000000000000030/MiiDatabase.dat` the Mii database (docs/reference/services.md, A console's own Mii database and system data); the Mac launcher sets `~/Library/Application Support/nxnative/system`, the iOS app `Documents/system` DM_TBUF_ALIGN diagnostic `=` uses that value as the linear-texture alignment when it is larger than the device's `minimumLinearTextureAlignmentForPixelFormat`, for buffer textures and for linear textures on a pool's buffer: a Mac can then reproduce a phone's larger alignment (a 64-byte one fails the buffers and linear textures a device with 64 bytes cannot view), to check `tbuf_as_2d`'s aligned-row copy and the upload of unaligned linear textures (`linear_fit`) DM_TEAM_ID switch Apple team ID for the per-game Xcode project, used when `--team-id` / `TEAM_ID` is not given; `build_ipa.sh` uses the placeholder `0000000000` (unsigned) when unset DM_TEMP path Temporary storage folder (default `$DM_ROOT/run/platform/temp`); stand-in system data goes in `_sysdata`; the Mac launcher, the iOS app and the NSP script set it diff --git a/docs/reference/services.md b/docs/reference/services.md index 7ab4bc142d..3f697e124a 100644 --- a/docs/reference/services.md +++ b/docs/reference/services.md @@ -226,7 +226,7 @@ Most service calls can be found before they crash, before the first boot or the - **Symptom:** abort in `fs::detail::GetFileSystemProxyServiceObject` under `fs::QueryMountSystemDataCacheSize` (`main+0x8f23bc`). - **Cause:** the game mounts system data 0100000000000802, MiiModel, as `MiiResource:` and reads `NXTexture{Low,Mid}{Linear,SRGB}.dat` and `Shape{Mid,High}.dat` whole; the SDK's Mii rendering is linked into `main` (`main+0x8f1400`–`+0x8fc000`). -- **Fix:** firmware content is not the host's to ship, so the host writes a stand-in of the same layout with every part empty (`dm_mii_standin` in `hle_mii.c`, mounted by `hle_fs.c` `MountSystemData`; any other system data answers "target not found"). Heads build and draw nothing; bodies and favourite colours show. +- **Fix:** firmware content is not the host's to ship, so the host writes a stand-in of the same layout with every part empty (`dm_mii_standin` in `hle_mii.c`, mounted by `hle_fs.c` `MountSystemData`; any other system data answers "target not found"). A player's own archive takes its place ([A console's own Mii database and system data](#a-consoles-own-mii-database-and-system-data)). Heads build and draw nothing; bodies and favourite colours show. - **Found on:** [Mario Kart 8 Deluxe](../../dm/games/0100152000022000/README.md) (4.0.0, SDK 4.4.0; one boot). - **Note:** the layout as the code reads it: texture file +0xc the largest decompressed size of each of 11 part kinds, +0x38 their alignments, from +0x64 one 0x1c-byte entry per part (offset, size, stored size, 4 zlib parameters, then width, height, format, mips); shape file +0xc / +0x3c the same for 12 kinds, from +0x6c one 0x58-byte entry per part (offset, size, stored size, parameters, five vertex attribute offsets and sizes, index offset and byte size, bounding box), then hair (132 × 0x48, +0x12704) and faceline (12 × 0x24, +0x14c24) transforms. A texture of size 0 is skipped (`main+0x8faaf4`); shapes cannot all be absent, because the faceline is drawn without a check (a null buffer read at `main+0xa690d0` under `main+0x8286c4`), so every shape entry points at one shared mesh of one vertex and the triangle (0, 0, 0), 32 bytes of zeros in a stored zlib block. The largest sizes must not be 0: they size the model's memory pool (`main+0x8f2e6c`), whose alignment is the first one. The Mii database itself: [Mii database answered by the host](#mii-database-answered-by-the-host). @@ -763,6 +763,18 @@ Most service calls can be found before they crash, before the first boot or the - **Found on:** [Mario Kart 8 Deluxe](../../dm/games/0100152000022000/README.md) (4.0.0, SDK 4.4.0; one boot). The game asks for the default Miis with `Get(…, 6, SourceFlag 2)` and never reads the count (`main+0x81e954`): its boot-time list of Mii icons (`main+0x419dc8`) builds a head model for each of the six, and zeroed entries failed the model's `CharInfoAccessor::IsValid` (`CharModel::Initialize` returned 0xd67e, `main+0x8f15b0`) and crashed at `main+0x8f152c`. - **Note:** the heads' model data is a stand-in with every part empty ([System data stand-in for the MiiModel archive](#system-data-stand-in-for-the-miimodel-archive)). +### A console's own Mii database and system data + +- **Symptom:** the game shows only the six default Miis, and a Switch 2 game logs `[RED] fs mount system data MiiResource: the host has no 0400000000000849` (Nintendo Switch Sports Resort: Mii heads cannot be drawn). +- **Cause:** both live on the console: the Miis in the mii service's system save, the head textures and shapes in a system data archive. The host ships neither. +- **Fix:** a player can supply their own, under `DM_SYSTEM_DIR` (the Mac launcher sets `~/Library/Application Support/nxnative/system`, the iOS app `Documents/system`): + - `sysdata//`: an archive's RomFS files. `MountSystemData` mounts it read-only (`hle_fs.c` `sysdata_own`), ahead of the MiiModel stand-in. + - `save/8000000000000030/MiiDatabase.dat`: the mii service's database. The Switch 2's service (program `0400000000000039`) mounts system save `8000000000000030` as `mii` and reads `mii:/MiiDatabase.dat`, as the Switch does. The file is 0x1a98 bytes: `NFDB`, 100 StoreData of 0x44 bytes, the version (1) at +0x1a94, the count at +0x1a95, and a CRC-16/CCITT (polynomial 0x1021, initial 0) of the bytes before it at +0x1a96, big-endian. `hle_mii.c` reads it once at `Database::Initialize`. Each StoreData's seven words of bit fields, nickname and create ID become a CharInfo (`mii_from_store`; the stored eyebrow y is 3 below the CharInfo's). `Get` lists them before the six default Miis (source 0, then 1), `GetIndex` and `UpdateLatest` find them by create ID, and `IsFullDatabase` is true at 100. A file of another size, magic, version or CRC is logged and leaves the database empty. The file is never written: Miis a game adds do not reach it, and the editor is still cancelled. +- **Found on:** Nintendo Switch Sports Resort (`04008E001F098000`, v65536; no profile on main). The save's ID, mount name and file checks were read from the Switch 2's system program `0400000000000039`. + - **The archive it needs:** the game mounts system data `0400000000000849` as `MiiResource:` and reads `ShapeMid.dat` / `ShapeHigh.dat` and `OunceTexture{Low,Mid,High}{Linear,SRGB}.dat`. Its code also names the four `NXTexture*` files of the Switch's MiiModel (`0100000000000802`), but it picks the texture file from a table by a flag (`main+0x1cdeb50`) that its Mii setup (`main+0x320040`) only ever clears, so it always reads the `OunceTexture*` files: the Switch's archive, or the host's stand-in of it, cannot take its place. Without the archive the setup function's size query fails and it mounts nothing, and the game shows no Mii heads. + - **Run:** with a console's `0400000000000849` (its eight files) and a database of one Mii in place, the game mounts the archive, lists the Mii first on Select User, and draws Mii heads there, on its Customize screen and in play (M4 Pro, macOS 27.2). +- **Check:** `make -C dm/host/tests GAME= build/t_mii && dm/host/tests/build/t_mii`: a database with known fields unpacks into them; a wrong CRC, size or version is refused; `Get`, `GetCount`, `GetIndex` and `UpdateLatest` answer from it. + ### Shop service accessors - **Symptom:** the abort with no message (`SVC 0x15` / `0x1f` / `0x21` placeholders, then `Break` with `sm::Finalize` under `ShopServiceAccessServer::Proxy::Initialize`) from `ec::InitializeForShopServiceAccessors`, called from Unreal's `FOnlineSubsystemSwitch::Init` at the start of `nnMain`.