Skip to content
Closed
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
17 changes: 12 additions & 5 deletions dm/host/hle_fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 <DM_TEMP>_sysdata/<id> 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: <DM_SYSTEM_DIR>/sysdata/<id as 16 hex digits>/ 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
// <DM_TEMP>_sysdata/<id>; 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);
}
Expand All @@ -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 <DM_SYSTEM_DIR>/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) {
Expand Down
96 changes: 80 additions & 16 deletions dm/host/hle_mii.c
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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: <DM_SYSTEM_DIR>/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) {
Expand Down
5 changes: 4 additions & 1 deletion dm/host/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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=<base game title ID>: $(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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading