mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
resolve_menu_lang falls back from a region tag the catalog lacks to its base language (ru-RU: ru), but probe_menu_text expected English for such a tag, so --probe-services failed on any Windows whose UI language is ru-RU, de-DE and the like; en-US maps directly and passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1099 lines
64 KiB
C
1099 lines
64 KiB
C
// --probe-services: every override of ov_system.c, ov_input.c, ov_media.c, swkbd.c and
|
|
// ov_gfx.c called the way guest code calls it: by its synthetic address through rt_call / rt_dispatch, with AAPCS64
|
|
// arguments, on the guest main thread. Checks that each one matches a relocation of the loaded modules, then the
|
|
// values: language, operation mode, notification messages, account, time round trips, controllers, codec state,
|
|
// graphics records, the system events behind nfp and vi, the software keyboard with arguments built by the guest SDK
|
|
// (answers from TOTK_SWKBD_ANSWER, then the window pressed by TOTK_SWKBD_TEST) and the shared fonts when
|
|
// tools/extract_fonts.py has written them. The named fatal stubs, and the account UID of a second run, are checked in
|
|
// child processes (this executable again, TOTK_PROBE_SERVICES_CHILD=<case>).
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include "amiibo.h"
|
|
#include "cfg.h"
|
|
#include "fs.h"
|
|
#include "gfx.h"
|
|
#include "host.h"
|
|
#include "input.h"
|
|
#include "kernel.h"
|
|
#include "ov.h"
|
|
#include "platform/platform.h"
|
|
#include "utf16.h"
|
|
#include "ui_text.h"
|
|
|
|
#define CHILD_ENV "TOTK_PROBE_SERVICES_CHILD"
|
|
#define TRY_WAIT_SYSTEM_EVENT "_ZN2nn2os18TryWaitSystemEventEPNS0_15SystemEventTypeE"
|
|
#define DESTROY_SYSTEM_EVENT "_ZN2nn2os18DestroySystemEventEPNS0_15SystemEventTypeE"
|
|
|
|
typedef struct { const char *key, *name; int fatal; uint64_t addr; int bound, calls; } Ov;
|
|
static Ov ovs[] = {
|
|
// ov_system.c: nn::oe
|
|
{"oe.Initialize", "_ZN2nn2oe10InitializeEv"},
|
|
{"oe.GetDesiredLanguage", "_ZN2nn2oe18GetDesiredLanguageEv"},
|
|
{"oe.SetPerformanceConfiguration", "_ZN2nn2oe27SetPerformanceConfigurationENS0_15PerformanceModeEi"},
|
|
{"oe.SetCpuBoostMode", "_ZN2nn2oe15SetCpuBoostModeENS0_12CpuBoostModeE"},
|
|
{"oe.GetOperationMode", "_ZN2nn2oe16GetOperationModeEv"},
|
|
{"oe.GetPerformanceMode", "_ZN2nn2oe18GetPerformanceModeEv"},
|
|
{"oe.SetFocusHandlingMode", "_ZN2nn2oe20SetFocusHandlingModeENS0_17FocusHandlingModeE"},
|
|
{"oe.GetCurrentFocusState", "_ZN2nn2oe20GetCurrentFocusStateEv"},
|
|
{"oe.SetResumeNotificationEnabled", "_ZN2nn2oe28SetResumeNotificationEnabledEb"},
|
|
{"oe.SetOperationModeChangedNotificationEnabled", "_ZN2nn2oe42SetOperationModeChangedNotificationEnabledEb"},
|
|
{"oe.SetPerformanceModeChangedNotificationEnabled", "_ZN2nn2oe44SetPerformanceModeChangedNotificationEnabledEb"},
|
|
// applet, am, oe (applet_host.c)
|
|
{"applet.GetPreviousProgramIndex", "_ZN2nn6applet23GetPreviousProgramIndexEv"},
|
|
{"applet.GetAppletResourceUserId", "_ZN2nn6applet23GetAppletResourceUserIdEv"},
|
|
{"am.GetAppletResourceUserId", "_ZN2nn2am23GetAppletResourceUserIdEv"},
|
|
{"oe.TryPopLaunchParameter", "_ZN2nn2oe21TryPopLaunchParameterEPmPvm"},
|
|
{"oe.GetExpectedVolumeBalance", "_ZN2nn2oe24GetExpectedVolumeBalanceEPfS1_"},
|
|
{"oe.SetExpectedVolumeBalance", "_ZN2nn2oe24SetExpectedVolumeBalanceEff"},
|
|
{"oe.GetProgramTotalActiveTime", "_ZN2nn2oe25GetProgramTotalActiveTimeEv"},
|
|
{"oe.SetUserInactivityDetectionTimeExtended", "_ZN2nn2oe38SetUserInactivityDetectionTimeExtendedEb"},
|
|
{"oe.SetUserInactivityDetectionTimeExtendedUnsafe", "_ZN2nn2oe44SetUserInactivityDetectionTimeExtendedUnsafeEb"},
|
|
{"oe.FinishStartupLogo", "_ZN2nn2oe17FinishStartupLogoEv"},
|
|
{"oe.TryPopNotificationMessage", "_ZN2nn2oe25TryPopNotificationMessageEPj"},
|
|
{"oe.PopNotificationMessage", "_ZN2nn2oe22PopNotificationMessageEv"},
|
|
{"prepo.Save", "_ZN2nn5prepo10PlayReport4SaveEv"},
|
|
{"prepo.SaveUid", "_ZN2nn5prepo10PlayReport4SaveERKNS_7account3UidE"},
|
|
{"prepo.RequestImmediateTransmission", "_ZN2nn5prepo28RequestImmediateTransmissionEv"},
|
|
{"prepo.AddInternetConnectionStatus", "_ZN2nn5prepo27AddInternetConnectionStatusEPNS0_10PlayReportEPKcS4_S4_"},
|
|
{"nfp.Initialize", "_ZN2nn3nfp10InitializeEv"},
|
|
{"nfp.Finalize", "_ZN2nn3nfp8FinalizeEv"},
|
|
{"nfp.GetState", "_ZN2nn3nfp8GetStateEv"},
|
|
{"nfp.ListDevices", "_ZN2nn3nfp11ListDevicesEPNS0_12DeviceHandleEPii"},
|
|
{"nfp.AttachAvailabilityChangeEvent", "_ZN2nn3nfp29AttachAvailabilityChangeEventEPNS_2os15SystemEventTypeE"},
|
|
{"nfp.GetDeviceState", "_ZN2nn3nfp14GetDeviceStateERKNS0_12DeviceHandleE"},
|
|
{"nfp.Mount", "_ZN2nn3nfp5MountERKNS0_12DeviceHandleENS0_9ModelTypeENS0_11MountTargetE"},
|
|
{"nfp.GetNpadId", "_ZN2nn3nfp9GetNpadIdEPjRKNS0_12DeviceHandleE"},
|
|
{"nfp.AttachActivateEvent", "_ZN2nn3nfp19AttachActivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE"},
|
|
{"nfp.AttachDeactivateEvent", "_ZN2nn3nfp21AttachDeactivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE"},
|
|
{"nfp.StartDetection", "_ZN2nn3nfp14StartDetectionERKNS0_12DeviceHandleE"},
|
|
{"nfp.StopDetection", "_ZN2nn3nfp13StopDetectionERKNS0_12DeviceHandleE"},
|
|
{"nfp.GetTagInfo", "_ZN2nn3nfp10GetTagInfoEPNS0_7TagInfoERKNS0_12DeviceHandleE"},
|
|
{"nfp.GetModelInfo", "_ZN2nn3nfp12GetModelInfoEPNS0_9ModelInfoERKNS0_12DeviceHandleE"},
|
|
{"err.ShowError", "_ZN2nn3err9ShowErrorENS_6ResultE"},
|
|
// SDK-internal (section 5.2)
|
|
{"nndiagStartup", "nndiagStartup"},
|
|
{"aoc.Initialize", "_ZN2nn3aoc10InitializeEv"},
|
|
{"aoc.ListAddOnContent", "_ZN2nn3aoc16ListAddOnContentEPiii"},
|
|
{"err.InitializeContextWriterApi", "_ZN2nn3err26InitializeContextWriterApiEv"},
|
|
{"pctl.Initialize", "_ZN2nn4pctl10InitializeEv"},
|
|
{"diag.InitializeApplicationAbortObserver", "_ZN2nn4diag34InitializeApplicationAbortObserverEv"},
|
|
// account
|
|
{"account.Initialize", "_ZN2nn7account10InitializeEv"},
|
|
{"account.TryOpenPreselectedUser", "_ZN2nn7account22TryOpenPreselectedUserEPNS0_10UserHandleE"},
|
|
{"account.CloseUser", "_ZN2nn7account9CloseUserERKNS0_10UserHandleE"},
|
|
{"account.GetUserId", "_ZN2nn7account9GetUserIdEPNS0_3UidERKNS0_10UserHandleE"},
|
|
{"account.GetUserExistence", "_ZN2nn7account16GetUserExistenceEPbRKNS0_3UidE"},
|
|
{"account.GetNickname", "_ZN2nn7account11GetNicknameEPNS0_8NicknameERKNS0_3UidE"},
|
|
// time
|
|
{"time.Initialize", "_ZN2nn4time10InitializeEv"},
|
|
{"time.Finalize", "_ZN2nn4time8FinalizeEv"},
|
|
{"time.IsInitialized", "_ZN2nn4time13IsInitializedEv"},
|
|
{"time.UserClock.GetCurrentTime", "_ZN2nn4time23StandardUserSystemClock14GetCurrentTimeEPNS0_9PosixTimeE"},
|
|
{"time.NetworkClock.GetCurrentTime", "_ZN2nn4time26StandardNetworkSystemClock14GetCurrentTimeEPNS0_9PosixTimeE"},
|
|
{"time.SteadyClock.now", "_ZN2nn4time19StandardSteadyClock3nowEv"},
|
|
{"time.ToCalendarTime",
|
|
"_ZN2nn4time14ToCalendarTimeEPNS0_12CalendarTimeEPNS0_22CalendarAdditionalInfoERKNS0_9PosixTimeE"},
|
|
{"time.ToCalendarTimeInUtc", "_ZN2nn4time19ToCalendarTimeInUtcERKNS0_9PosixTimeE"},
|
|
{"time.ToPosixTimeFromUtc", "_ZN2nn4time18ToPosixTimeFromUtcERKNS0_12CalendarTimeE"},
|
|
{"time.ClockSnapshot.C1", "_ZN2nn4time13ClockSnapshotC1Ev"},
|
|
{"time.ClockSnapshot.Create", "_ZN2nn4time13ClockSnapshot29CreateWithStandardSystemClockEPS1_"},
|
|
{"time.CalculateSpanBetween", "_ZN2nn4time20CalculateSpanBetweenEPNS_8TimeSpanERKNS0_13ClockSnapshotES5_"},
|
|
{"time.UserDifference",
|
|
"_ZN2nn4time48CalculateStandardUserSystemClockDifferenceByUserERKNS0_13ClockSnapshotES3_"},
|
|
// pl
|
|
{"pl.GetSharedFontLoadState", "_ZN2nn2pl22GetSharedFontLoadStateENS0_14SharedFontTypeE"},
|
|
{"pl.GetSharedFontAddress", "_ZN2nn2pl20GetSharedFontAddressENS0_14SharedFontTypeE", 1},
|
|
{"pl.GetSharedFontSize", "_ZN2nn2pl17GetSharedFontSizeENS0_14SharedFontTypeE", 1},
|
|
// ov_input.c
|
|
{"hid.InitializeNpad", "_ZN2nn3hid14InitializeNpadEv"},
|
|
{"hid.SetSupportedNpadIdType", "_ZN2nn3hid22SetSupportedNpadIdTypeEPKjm"},
|
|
{"hid.SetSupportedNpadStyleSet",
|
|
"_ZN2nn3hid24SetSupportedNpadStyleSetENS_4util10BitFlagSetILi32ENS0_12NpadStyleTagEEE"},
|
|
{"hid.GetNpadStyleSet", "_ZN2nn3hid15GetNpadStyleSetERKj"},
|
|
{"hid.GetNpadStates.FullKey", "_ZN2nn3hid13GetNpadStatesEPNS0_16NpadFullKeyStateEiRKj"},
|
|
{"hid.GetNpadStates.JoyDual", "_ZN2nn3hid13GetNpadStatesEPNS0_16NpadJoyDualStateEiRKj"},
|
|
{"hid.GetNpadStates.JoyLeft", "_ZN2nn3hid13GetNpadStatesEPNS0_16NpadJoyLeftStateEiRKj"},
|
|
{"hid.GetNpadStates.JoyRight", "_ZN2nn3hid13GetNpadStatesEPNS0_17NpadJoyRightStateEiRKj"},
|
|
{"hid.GetNpadStates.Handheld", "_ZN2nn3hid13GetNpadStatesEPNS0_17NpadHandheldStateEiRKj"},
|
|
{"hid.SetNpadJoyHoldType", "_ZN2nn3hid18SetNpadJoyHoldTypeENS0_15NpadJoyHoldTypeE"},
|
|
{"hid.DisconnectNpad", "_ZN2nn3hid14DisconnectNpadERKj"},
|
|
{"hid.ShowControllerSupport",
|
|
"_ZN2nn3hid21ShowControllerSupportEPNS0_27ControllerSupportResultInfoERKNS0_20ControllerSupportArgE"},
|
|
{"hid.InitializeMouse", "_ZN2nn3hid15InitializeMouseEv"},
|
|
{"hid.InitializeKeyboard", "_ZN2nn3hid18InitializeKeyboardEv"},
|
|
{"hid.GetMouseStates", "_ZN2nn3hid14GetMouseStatesEPNS0_10MouseStateEi"},
|
|
{"hid.GetKeyboardState", "_ZN2nn3hid16GetKeyboardStateEPNS0_13KeyboardStateE"},
|
|
{"hid.GetVibrationDeviceHandles",
|
|
"_ZN2nn3hid25GetVibrationDeviceHandlesEPNS0_21VibrationDeviceHandleEiRKjNS_4util10BitFlagSetILi32ENS0_"
|
|
"12NpadStyleTagEEE"},
|
|
{"hid.GetSixAxisSensorHandles",
|
|
"_ZN2nn3hid23GetSixAxisSensorHandlesEPNS0_19SixAxisSensorHandleEiRKjNS_4util10BitFlagSetILi32ENS0_"
|
|
"12NpadStyleTagEEE"},
|
|
{"hid.InitializeVibrationDevice", "_ZN2nn3hid6detail25InitializeVibrationDeviceERKNS0_21VibrationDeviceHandleE"},
|
|
{"hid.SendVibrationValue",
|
|
"_ZN2nn3hid6detail18SendVibrationValueERKNS0_21VibrationDeviceHandleERKNS0_14VibrationValueE"},
|
|
{"hid.SendVibrationValues",
|
|
"_ZN2nn3hid6detail19SendVibrationValuesEPKNS0_21VibrationDeviceHandleEPKNS0_14VibrationValueEi"},
|
|
{"hid.GetActualVibrationValue",
|
|
"_ZN2nn3hid6detail23GetActualVibrationValueEPNS0_14VibrationValueERKNS0_21VibrationDeviceHandleE"},
|
|
{"hid.GetVibrationDeviceInfo",
|
|
"_ZN2nn3hid6detail22GetVibrationDeviceInfoEPNS0_19VibrationDeviceInfoERKNS0_21VibrationDeviceHandleE"},
|
|
{"hid.IsVibrationDeviceMounted", "_ZN2nn3hid6detail24IsVibrationDeviceMountedEPbRKNS0_21VibrationDeviceHandleE"},
|
|
{"hid.IsVibrationPermitted", "_ZN2nn3hid6detail20IsVibrationPermittedEPb"},
|
|
{"hid.PermitVibration", "_ZN2nn3hid6detail15PermitVibrationEb"},
|
|
{"hid.StartSixAxisSensor", "_ZN2nn3hid6detail18StartSixAxisSensorERKNS0_19SixAxisSensorHandleE"},
|
|
{"hid.StopSixAxisSensor", "_ZN2nn3hid6detail17StopSixAxisSensorERKNS0_19SixAxisSensorHandleE"},
|
|
{"hid.GetSixAxisSensorStates",
|
|
"_ZN2nn3hid6detail22GetSixAxisSensorStatesEPiPNS0_18SixAxisSensorStateEiRKNS0_19SixAxisSensorHandleE"},
|
|
{"hid.GetSixAxisSensorState",
|
|
"_ZN2nn3hid6detail21GetSixAxisSensorStateEPNS0_18SixAxisSensorStateERKNS0_19SixAxisSensorHandleE"},
|
|
// ov_media.c
|
|
{"codec.GetWorkBufferSize", "_ZN2nn5codec36GetHardwareOpusDecoderWorkBufferSizeEiii"},
|
|
{"codec.Initialize", "_ZN2nn5codec29InitializeHardwareOpusDecoderEPNS0_23HardwareOpusDecoderTypeEiiPvmi"},
|
|
{"codec.Finalize", "_ZN2nn5codec27FinalizeHardwareOpusDecoderEPNS0_23HardwareOpusDecoderTypeE"},
|
|
{"codec.GetSampleRate", "_ZN2nn5codec32GetHardwareOpusDecoderSampleRateEPKNS0_23HardwareOpusDecoderTypeE"},
|
|
{"codec.GetChannelCount", "_ZN2nn5codec34GetHardwareOpusDecoderChannelCountEPKNS0_23HardwareOpusDecoderTypeE"},
|
|
{"codec.Reset", "_ZN2nn5codec24ResetHardwareOpusDecoderEPNS0_23HardwareOpusDecoderTypeE"},
|
|
{"codec.Decode", "_ZN2nn5codec33DecodeOpusInterleavedWithHardwareEPNS0_23HardwareOpusDecoderTypeEPmPiPsmPKvm", 1},
|
|
{"swkbd.ShowKeyboard", "_ZN2nn5swkbd12ShowKeyboardEPNS0_6StringERKNS0_15ShowKeyboardArgE", 1},
|
|
// ov_gfx.c
|
|
{"nv.SetGraphicsAllocator", "_ZN2nv20SetGraphicsAllocatorEPFPvmmS0_EPFvS0_S0_EPFS0_S0_mS0_ES0_"},
|
|
{"nv.InitializeGraphics", "_ZN2nv18InitializeGraphicsEPvm"},
|
|
{"nv.SetGraphicsDevtoolsAllocator", "_ZN2nv28SetGraphicsDevtoolsAllocatorEPFPvmmS0_EPFvS0_S0_EPFS0_S0_mS0_ES0_"},
|
|
{"vi.Initialize", "_ZN2nn2vi10InitializeEv"},
|
|
{"vi.OpenDefaultDisplay", "_ZN2nn2vi18OpenDefaultDisplayEPPNS0_7DisplayE"},
|
|
{"vi.CreateLayer", "_ZN2nn2vi11CreateLayerEPPNS0_5LayerEPNS0_7DisplayE"},
|
|
{"vi.GetNativeWindow", "_ZN2nn2vi15GetNativeWindowEPPvPNS0_5LayerE"},
|
|
{"vi.GetDisplayVsyncEvent", "_ZN2nn2vi20GetDisplayVsyncEventEPNS_2os15SystemEventTypeEPNS0_7DisplayE"},
|
|
{"nvnBootstrapLoader", "nvnBootstrapLoader", 1},
|
|
};
|
|
#define NOV ((int)(sizeof ovs / sizeof *ovs))
|
|
|
|
static const char *const observers[] = {"_ZN2nn2os21AllocateAddressRegionEPmm", "_ZN2nn2os19AllocateMemoryPagesEmm",
|
|
"nnosInitialize", "nninitInitializeSdkModule", "nninitStartup", "nnMain"};
|
|
#define NOBS ((int)(sizeof observers / sizeof *observers))
|
|
|
|
// Fatal stubs, each in its own child process: the case name, the override, what its FATAL line must say and an
|
|
// environment variable the child runs with (the shared fonts: a font directory that does not exist).
|
|
#define NO_FONTS "TOTK_SHARED_FONTS=:probe-services:no-such-dir"
|
|
static const struct { const char *child, *key, *fatal, *env; } fatals[] = {
|
|
{"opus", "codec.Decode", "FATAL: nn::codec::DecodeOpusInterleavedWithHardware"},
|
|
{"font-address", "pl.GetSharedFontAddress", "FATAL: nn::pl::GetSharedFontAddress(0)", NO_FONTS},
|
|
{"font-size", "pl.GetSharedFontSize", "FATAL: nn::pl::GetSharedFontSize(0)", NO_FONTS},
|
|
};
|
|
#define NFATAL ((int)(sizeof fatals / sizeof *fatals))
|
|
|
|
static int failures, checks;
|
|
#define CHECK(cond, ...) \
|
|
do { checks++; if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
|
|
|
|
static Cpu *gc; // the guest thread the calls run on
|
|
static uint64_t probe_uid[2];
|
|
|
|
static Ov *ov(const char *key) {
|
|
for (int i = 0; i < NOV; i++)
|
|
if (!strcmp(ovs[i].key, key)) return &ovs[i];
|
|
rt_fatal("probe-services: no case %s", key);
|
|
}
|
|
|
|
static uint64_t U(const void *p) { return (uint64_t)(uintptr_t)p; }
|
|
|
|
// The override called through its synthetic address with x0..x7 = a[0..7]; caller-saved registers hold garbage, the
|
|
// callee-saved ones a pattern that must survive. Returns x0.
|
|
static uint64_t call8(const char *key, const uint64_t a[8]) {
|
|
Ov *o = ov(key);
|
|
for (int r = 8; r < 19; r++) gc->x[r] = 0xdead0000u + r;
|
|
for (int r = 19; r < 30; r++) gc->x[r] = 0x1900u + r;
|
|
uint64_t sp = gc->sp;
|
|
uint64_t v = rt_call(gc, o->addr, 8, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]);
|
|
int kept = gc->sp == sp;
|
|
for (int r = 19; r < 30; r++) kept &= gc->x[r] == 0x1900u + r;
|
|
CHECK(kept, "%s lost a callee-saved register or sp", key);
|
|
o->calls++;
|
|
return v;
|
|
}
|
|
#define CALL(key, ...) call8(key, (const uint64_t[8]){__VA_ARGS__})
|
|
|
|
static void set_s(int i, float f) {
|
|
uint32_t b;
|
|
memcpy(&b, &f, 4);
|
|
gc->v[i] = b;
|
|
}
|
|
|
|
static void bind_addresses(void) {
|
|
for (int i = 0; i < NOV; i++) {
|
|
GuestFn fn = hostfn_override(ovs[i].name);
|
|
if (!fn) rt_fatal("probe-services: no HOST_OVERRIDE for %s", ovs[i].name);
|
|
ovs[i].addr = hostfn_register(ovs[i].name, fn);
|
|
}
|
|
}
|
|
|
|
static void *guest_alloc(uint64_t n) { return mem_kernel_alloc(n, 16); }
|
|
|
|
// ---- the guest-thread part
|
|
static char *lang(void) {
|
|
static char s[9];
|
|
uint64_t v = CALL("oe.GetDesiredLanguage", 0);
|
|
memcpy(s, &v, 8);
|
|
s[8] = 0;
|
|
return s;
|
|
}
|
|
|
|
static void set_env(const char *name, const char *value) { plat_setenv(name, value); } // NULL removes it
|
|
|
|
static void probe_oe(void) {
|
|
char *keep_lang = cfg_get("TOTK_LANG") ? strdup(cfg_get("TOTK_LANG")) : NULL;
|
|
char *keep_locale = cfg_get("TOTK_LOCALE") ? strdup(cfg_get("TOTK_LOCALE")) : NULL;
|
|
static const struct { const char *lang, *locale, *want; } cases[] = {
|
|
{NULL, "en-US", "en-US"}, {NULL, "be-BY", "en-US"}, {NULL, "xx", "en-US"}, {NULL, "ja-JP", "ja"},
|
|
{NULL, "fr-CA", "fr-CA"}, {NULL, "es-MX", "es-419"}, {NULL, "es_ES", "es"}, {NULL, "zh-Hant-TW", "zh-Hant"},
|
|
{NULL, "zh-CN", "zh-Hans"}, {NULL, "en-GB", "en-GB"}, {NULL, "ru", "ru"}, {"ja", "fr-FR", "ja"},
|
|
{"en-US", NULL, "en-US"}, {"zh-CN", NULL, "zh-Hans"}, {"zh-TW", NULL, "zh-Hant"},
|
|
{"zh-Hans", NULL, "zh-Hans"}, {"zh-Hant", NULL, "zh-Hant"}};
|
|
int bad = 0;
|
|
printf("oe: configured language (TOTK_LANG=%s) is %s\n", keep_lang ? keep_lang : "", lang());
|
|
for (size_t i = 0; i < sizeof cases / sizeof *cases; i++) {
|
|
set_env("TOTK_LANG", cases[i].lang);
|
|
set_env("TOTK_LOCALE", cases[i].locale);
|
|
const char *got = lang();
|
|
if (strcmp(got, cases[i].want)) {
|
|
bad++;
|
|
printf(" FAIL: language for TOTK_LANG=%s TOTK_LOCALE=%s is %s, not %s\n", cases[i].lang ? cases[i].lang : "",
|
|
cases[i].locale ? cases[i].locale : "", got, cases[i].want);
|
|
}
|
|
}
|
|
set_env("TOTK_LANG", NULL);
|
|
set_env("TOTK_LOCALE", NULL);
|
|
char sys[16];
|
|
snprintf(sys, sizeof sys, "%s", lang());
|
|
set_env("TOTK_LOCALE", "en-US");
|
|
char def[16];
|
|
snprintf(def, sizeof def, "%s", lang());
|
|
set_env("TOTK_LANG", keep_lang);
|
|
set_env("TOTK_LOCALE", keep_locale);
|
|
free(keep_lang);
|
|
free(keep_locale);
|
|
checks++;
|
|
failures += bad > 0;
|
|
printf("oe: language %s by default (en-US locale), %s for this user's locale; %d locale/TOTK_LANG cases %s\n",
|
|
def, sys, (int)(sizeof cases / sizeof *cases), bad ? "FAILED" : "ok");
|
|
|
|
CALL("oe.Initialize", 0);
|
|
CALL("oe.SetPerformanceConfiguration", 1, 0x92220008);
|
|
CALL("oe.SetCpuBoostMode", 0);
|
|
uint64_t mode = CALL("oe.GetOperationMode", 0), perf = CALL("oe.GetPerformanceMode", 0);
|
|
CHECK((uint32_t)mode == (uint32_t)!ov_handheld() && (uint32_t)perf == (uint32_t)!ov_handheld(),
|
|
"operation/performance mode %llu/%llu", (unsigned long long)mode, (unsigned long long)perf);
|
|
CALL("oe.SetFocusHandlingMode", 1);
|
|
CHECK((uint32_t)CALL("oe.GetCurrentFocusState", 0) == 1, "focus state");
|
|
CALL("oe.SetResumeNotificationEnabled", 1);
|
|
CALL("oe.SetOperationModeChangedNotificationEnabled", 1);
|
|
CALL("oe.SetPerformanceModeChangedNotificationEnabled", 1);
|
|
CHECK((int32_t)CALL("applet.GetPreviousProgramIndex", 0) == -1, "previous program index");
|
|
CHECK(CALL("applet.GetAppletResourceUserId", 0) == 1 && CALL("am.GetAppletResourceUserId", 0) == 1, "ARUID");
|
|
uint64_t size = 99;
|
|
uint8_t param[16];
|
|
CHECK((uint8_t)CALL("oe.TryPopLaunchParameter", U(&size), U(param), sizeof param) == 0 && size == 0,
|
|
"launch parameter");
|
|
float l = 0, r = 0;
|
|
set_s(0, 0.25f);
|
|
set_s(1, 0.75f);
|
|
CALL("oe.SetExpectedVolumeBalance", 0);
|
|
CALL("oe.GetExpectedVolumeBalance", U(&l), U(&r));
|
|
CHECK(l == 0.25f && r == 0.75f, "volume balance %g/%g", (double)l, (double)r);
|
|
int64_t t1 = (int64_t)CALL("oe.GetProgramTotalActiveTime", 0);
|
|
plat_sleep_ms(2);
|
|
int64_t t2 = (int64_t)CALL("oe.GetProgramTotalActiveTime", 0);
|
|
CHECK(t1 > 0 && t2 > t1, "program total active time %lld, %lld", (long long)t1, (long long)t2);
|
|
CALL("oe.SetUserInactivityDetectionTimeExtended", 1);
|
|
CALL("oe.SetUserInactivityDetectionTimeExtendedUnsafe", 1);
|
|
CALL("oe.FinishStartupLogo", 0);
|
|
}
|
|
|
|
static void late_push(void *p) {
|
|
(void)p;
|
|
plat_sleep_ms(50);
|
|
oe_push_message(0x21);
|
|
return;
|
|
}
|
|
|
|
static void probe_messages(void) {
|
|
uint32_t m = 0;
|
|
CHECK((uint8_t)CALL("oe.TryPopNotificationMessage", U(&m)) == 0, "a message before any was pushed");
|
|
oe_push_message(0x1f);
|
|
oe_push_message(0x20);
|
|
CHECK((uint8_t)CALL("oe.TryPopNotificationMessage", U(&m)) == 1 && m == 0x1f, "TryPop after a push: %#x", m);
|
|
CHECK((uint32_t)CALL("oe.PopNotificationMessage", 0) == 0x20, "Pop after a push");
|
|
PlatThread *h = plat_thread_new(late_push, NULL, "late push");
|
|
uint64_t t0 = kern_now_ns();
|
|
uint32_t late = (uint32_t)CALL("oe.PopNotificationMessage", 0);
|
|
uint64_t waited = kern_now_ns() - t0;
|
|
plat_thread_join(h);
|
|
CHECK(late == 0x21 && waited >= 40000000, "blocking Pop got %#x after %llu ms", late,
|
|
(unsigned long long)(waited / 1000000));
|
|
printf("oe: notification messages pushed then popped (TryPop, Pop, Pop blocked %llu ms until a host push): %s\n",
|
|
(unsigned long long)(waited / 1000000), late == 0x21 ? "ok" : "FAILED");
|
|
}
|
|
|
|
// A system event bound by an override: not signalled, then signalled through the kernel, then (auto-clear) cleared.
|
|
static int event_works(uint64_t ev, int autoclear) {
|
|
static uint64_t try_wait;
|
|
if (!try_wait && !(try_wait = ldr_symbol(TRY_WAIT_SYSTEM_EVENT))) rt_fatal("probe-services: no %s", TRY_WAIT_SYSTEM_EVENT);
|
|
int before = (uint8_t)rt_call(gc, try_wait, 1, ev);
|
|
kern_signal_system_event(ev);
|
|
int after = (uint8_t)rt_call(gc, try_wait, 1, ev);
|
|
int again = (uint8_t)rt_call(gc, try_wait, 1, ev);
|
|
return !before && after && again == !autoclear;
|
|
}
|
|
|
|
// A manual-clear system event an override signalled when binding it: set, and still set on a second wait.
|
|
static int event_set(uint64_t ev) {
|
|
static uint64_t try_wait;
|
|
if (!try_wait && !(try_wait = ldr_symbol(TRY_WAIT_SYSTEM_EVENT))) rt_fatal("probe-services: no %s", TRY_WAIT_SYSTEM_EVENT);
|
|
return (uint8_t)rt_call(gc, try_wait, 1, ev) && (uint8_t)rt_call(gc, try_wait, 1, ev);
|
|
}
|
|
|
|
static void probe_applet(void) {
|
|
uint8_t *report = guest_alloc(0x100);
|
|
uint64_t uid[2] = {1, 2};
|
|
CHECK(CALL("prepo.Save", U(report)) == 0 && CALL("prepo.SaveUid", U(report), U(uid)) == 0, "PlayReport::Save");
|
|
CHECK(CALL("prepo.RequestImmediateTransmission", 0) == 0, "RequestImmediateTransmission");
|
|
CHECK(CALL("prepo.AddInternetConnectionStatus", U(report), U("a"), U("b"), U("c")) == 0,
|
|
"AddInternetConnectionStatus");
|
|
CHECK(CALL("nfp.Initialize", 0) == 0 && (uint32_t)CALL("nfp.GetState", 0) == 1, "nfp initialize");
|
|
uint8_t handles[64];
|
|
int32_t count = 0;
|
|
CHECK((uint32_t)CALL("nfp.ListDevices", U(handles), U(&count), 4) == 0 && count == 1, "nfp ListDevices");
|
|
uint64_t ev = U(guest_alloc(0x100));
|
|
CHECK(CALL("nfp.AttachAvailabilityChangeEvent", ev) == 0, "AttachAvailabilityChangeEvent");
|
|
// The emulated reader is there from the start (amiibo.c), so the availability event is set when it is attached.
|
|
int nfp_event = event_set(ev);
|
|
CHECK(nfp_event, "the nfp availability event is not set for the emulated reader, or clears itself");
|
|
CHECK((uint32_t)CALL("nfp.GetDeviceState", U(handles)) == 0, "nfp device state");
|
|
static const char *const none[] = {"nfp.Mount", "nfp.GetTagInfo", "nfp.GetModelInfo"};
|
|
uint8_t out[0x100];
|
|
for (size_t i = 0; i < sizeof none / sizeof *none; i++) {
|
|
int first_is_handle = !strcmp(none[i], "nfp.Mount");
|
|
uint64_t r = first_is_handle ? CALL(none[i], U(handles), 0, 0) : CALL(none[i], U(out), U(handles));
|
|
CHECK((uint32_t)r == 0x8073, "%s: %#x, not device-not-found", none[i], (uint32_t)r);
|
|
}
|
|
CHECK((uint32_t)CALL("nfp.GetNpadId", U(out), U(handles)) == 0, "nfp GetNpadId");
|
|
uint64_t activate = U(guest_alloc(0x100)), deactivate = U(guest_alloc(0x100));
|
|
CHECK(CALL("nfp.AttachActivateEvent", activate, U(handles)) == 0, "nfp AttachActivateEvent");
|
|
CHECK(CALL("nfp.AttachDeactivateEvent", deactivate, U(handles)) == 0, "nfp AttachDeactivateEvent");
|
|
CHECK(event_works(activate, 0), "the nfp activate event is set without an amiibo on the reader");
|
|
CHECK((uint32_t)CALL("nfp.StartDetection", U(handles)) == 0, "nfp StartDetection");
|
|
CHECK((uint32_t)CALL("nfp.StopDetection", U(handles)) == 0, "nfp StopDetection");
|
|
// The game attaches again before each detection: DestroySystemEvent on the attached event, then Attach* on the
|
|
// same SystemEventType. Both events must work again afterwards.
|
|
static uint64_t destroy;
|
|
if (!destroy && !(destroy = ldr_symbol(DESTROY_SYSTEM_EVENT))) rt_fatal("probe-services: no %s", DESTROY_SYSTEM_EVENT);
|
|
rt_call(gc, destroy, 1, activate);
|
|
rt_call(gc, destroy, 1, deactivate);
|
|
CHECK(CALL("nfp.AttachActivateEvent", activate, U(handles)) == 0, "nfp AttachActivateEvent again");
|
|
CHECK(CALL("nfp.AttachDeactivateEvent", deactivate, U(handles)) == 0, "nfp AttachDeactivateEvent again");
|
|
int reattached = event_works(activate, 0) && event_works(deactivate, 0);
|
|
CHECK(reattached, "the nfp events do not work after DestroySystemEvent and a second Attach");
|
|
CHECK((uint32_t)CALL("nfp.StartDetection", U(handles)) == 0, "nfp StartDetection again");
|
|
// An amiibo's NTAG215 header: UID with its two check bytes, static lock 0F E0, capability container F1 10 FF EE.
|
|
// The loader takes it and refuses the same file with a plain NDEF container (E1).
|
|
static const uint8_t head[16] = {0x04, 0x11, 0x22, 0x88 ^ 0x04 ^ 0x11 ^ 0x22, 0x33, 0x44, 0x55, 0x66,
|
|
0x33 ^ 0x44 ^ 0x55 ^ 0x66, 0x48, 0x0F, 0xE0, 0xF1, 0x10, 0xFF, 0xEE};
|
|
uint8_t dump[AMIIBO_DUMP_SIZE] = {0};
|
|
memcpy(dump, head, sizeof head);
|
|
// Link (Archer): character 0x0100, variant 0, figure type 0, model number 0x0353, series 9, format 02
|
|
static const uint8_t id[8] = {0x01, 0x00, 0x00, 0x00, 0x03, 0x53, 0x09, 0x02};
|
|
memcpy(dump + 0x54, id, sizeof id);
|
|
const char *dump_path = "probe-amiibo.bin";
|
|
int amiibo_ok = plat_file_write(dump_path, dump, sizeof dump) && amiibo_load_file(dump_path);
|
|
// TagInfo carries the 7-byte UID without its check byte; ModelInfo has the series at 3, the model number
|
|
// little-endian at 4 and the figure type at 6
|
|
uint8_t ti[0x58], mi[0x40];
|
|
amiibo_get_tag_info(ti);
|
|
amiibo_get_model_info(mi);
|
|
static const uint8_t want_uid[7] = {0x04, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
|
|
static const uint8_t want_model[7] = {0x01, 0x00, 0x00, 0x09, 0x53, 0x03, 0x00};
|
|
CHECK(amiibo_ok && !memcmp(ti, want_uid, 7) && ti[0x0A] == 7, "amiibo TagInfo UID %02x %02x %02x %02x %02x %02x %02x",
|
|
ti[0], ti[1], ti[2], ti[3], ti[4], ti[5], ti[6]);
|
|
CHECK(amiibo_ok && !memcmp(mi, want_model, 7), "amiibo ModelInfo %02x %02x %02x %02x %02x %02x %02x", mi[0], mi[1],
|
|
mi[2], mi[3], mi[4], mi[5], mi[6]);
|
|
dump[0x0C] = 0xE1;
|
|
amiibo_ok = amiibo_ok && plat_file_write(dump_path, dump, sizeof dump) && !amiibo_load_file(dump_path);
|
|
plat_fs_unlink(dump_path);
|
|
CHECK(amiibo_ok, "the amiibo loader refuses a dump with the amiibo capability container, or takes a plain NDEF one");
|
|
CHECK(CALL("nfp.Finalize", 0) == 0 && (uint32_t)CALL("nfp.GetState", 0) == 0, "nfp finalize");
|
|
CALL("err.ShowError", 0x2a8);
|
|
CALL("nndiagStartup", 0);
|
|
CALL("aoc.Initialize", 0);
|
|
CHECK(CALL("aoc.ListAddOnContent", U(out), 0, 8) == 0, "aoc: no add-on content");
|
|
CALL("err.InitializeContextWriterApi", 0);
|
|
CALL("pctl.Initialize", 0);
|
|
CALL("diag.InitializeApplicationAbortObserver", 0);
|
|
printf("applet/prepo/nfp/err: launch parameter none, volume balance, play reports local, an NFC reader without "
|
|
"an amiibo, availability event (bound through the SDK's CreateSystemEvent) %s, activate and deactivate events "
|
|
"destroyed and attached again %s\n", nfp_event ? "ok" : "FAILED", reattached ? "ok" : "FAILED");
|
|
}
|
|
|
|
static void probe_account(void) {
|
|
uint64_t h[3] = {0}, uid[2] = {0}, other[2] = {5, 6}, direct[2];
|
|
uint8_t exists = 0;
|
|
char nick[33];
|
|
CALL("account.Initialize", 0);
|
|
CHECK((uint8_t)CALL("account.TryOpenPreselectedUser", U(h)) == 1, "TryOpenPreselectedUser");
|
|
CHECK(CALL("account.GetUserId", U(uid), U(h)) == 0 && (uid[0] || uid[1]), "GetUserId");
|
|
account_uid(direct);
|
|
CHECK(uid[0] == direct[0] && uid[1] == direct[1], "GetUserId differs from account_uid");
|
|
CHECK(CALL("account.GetUserExistence", U(&exists), U(uid)) == 0 && exists == 1, "GetUserExistence");
|
|
CHECK(CALL("account.GetUserExistence", U(&exists), U(other)) == 0 && exists == 0, "GetUserExistence (other)");
|
|
CHECK(CALL("account.GetNickname", U(nick), U(uid)) == 0 && !strcmp(nick, "Local"), "GetNickname");
|
|
CALL("account.CloseUser", U(h));
|
|
CHECK(CALL("account.GetUserId", U(uid), U(h)) != 0, "GetUserId on a closed handle");
|
|
probe_uid[0] = direct[0];
|
|
probe_uid[1] = direct[1];
|
|
}
|
|
|
|
typedef struct { int16_t year; int8_t month, day, hour, minute, second, pad; } Cal;
|
|
typedef struct { int32_t day_of_week, day_of_year; char name[8]; uint8_t dst, pad[3]; int32_t utc_offset; } CalInfo;
|
|
|
|
static int64_t posix_of(const Cal *c) { return (int64_t)CALL("time.ToPosixTimeFromUtc", U(c)); }
|
|
static Cal utc_of(int64_t t) {
|
|
uint64_t v = CALL("time.ToCalendarTimeInUtc", U(&t));
|
|
Cal c;
|
|
memcpy(&c, &v, 8);
|
|
return c;
|
|
}
|
|
|
|
static void probe_time(void) {
|
|
CHECK(CALL("time.Initialize", 0) == 0 && (uint8_t)CALL("time.IsInitialized", 0) == 1, "time initialize");
|
|
int64_t user = 0, net = 0, now = (int64_t)time(NULL);
|
|
CHECK(CALL("time.UserClock.GetCurrentTime", U(&user)) == 0 && CALL("time.NetworkClock.GetCurrentTime", U(&net)) == 0 &&
|
|
llabs(user - now) <= 2 && llabs(net - now) <= 2, "system clocks %lld/%lld vs %lld", (long long)user,
|
|
(long long)net, (long long)now);
|
|
int64_t s1 = (int64_t)CALL("time.SteadyClock.now", 0), s2 = (int64_t)CALL("time.SteadyClock.now", 0);
|
|
CHECK(s1 > 0 && s2 >= s1, "steady clock %lld, %lld", (long long)s1, (long long)s2);
|
|
|
|
static const int64_t ts[] = {0, 86399, 951782400, 1700000000, 4102444800ll, -31536000, -62135596800ll, 253402300799ll};
|
|
int rt_bad = 0;
|
|
for (size_t i = 0; i < sizeof ts / sizeof *ts; i++) {
|
|
Cal c = utc_of(ts[i]);
|
|
if (posix_of(&c) != ts[i]) rt_bad++;
|
|
}
|
|
Cal leap = utc_of(951782400); // 2000-02-29 00:00:00 UTC
|
|
CHECK(leap.year == 2000 && leap.month == 2 && leap.day == 29 && !leap.hour && !leap.minute && !leap.second,
|
|
"951782400 is %d-%d-%d", leap.year, leap.month, leap.day);
|
|
Cal carry = {2023, 13, 1, 0, 0, 0, 0}; // month 13 carries into the next year
|
|
CHECK(posix_of(&carry) == 1704067200, "ToPosixTimeFromUtc does not carry month 13");
|
|
CHECK(!rt_bad, "%d of %d UTC round trips failed", rt_bad, (int)(sizeof ts / sizeof *ts));
|
|
|
|
Cal local = {0};
|
|
CalInfo info = {0};
|
|
int64_t t = 1700000000;
|
|
CHECK(CALL("time.ToCalendarTime", U(&local), U(&info), U(&t)) == 0, "ToCalendarTime");
|
|
int64_t back = posix_of(&local);
|
|
int64_t day = (t + info.utc_offset) / 86400;
|
|
CHECK(back - t == info.utc_offset && info.day_of_week == (int32_t)((day + 4) % 7),
|
|
"local calendar time does not round trip (offset %d, day of week %d)", info.utc_offset, info.day_of_week);
|
|
int64_t neg = -1;
|
|
CHECK(CALL("time.ToCalendarTime", U(&local), U(&info), U(&neg)) != 0, "ToCalendarTime of an invalid time succeeded");
|
|
|
|
uint8_t *a = guest_alloc(0xd0), *b = guest_alloc(0xd0);
|
|
memset(a, 0xaa, 0xd0);
|
|
CALL("time.ClockSnapshot.C1", U(a));
|
|
int zero = 1;
|
|
for (int i = 0; i < 0xd0; i++) zero &= !a[i];
|
|
CHECK(zero, "ClockSnapshot() did not clear the snapshot");
|
|
CHECK(CALL("time.ClockSnapshot.Create", U(a)) == 0 && CALL("time.ClockSnapshot.Create", U(b)) == 0, "snapshots");
|
|
int64_t span = -1;
|
|
CHECK(CALL("time.CalculateSpanBetween", U(&span), U(a), U(b)) == 0 && span >= 0, "span between snapshots");
|
|
CHECK(CALL("time.UserDifference", U(a), U(b)) == 0, "user clock difference");
|
|
b[0x98] ^= 1; // the steady clock's source id (ClockSnapshot +0x90: value, +0x98: source)
|
|
CHECK((uint32_t)CALL("time.CalculateSpanBetween", U(&span), U(a), U(b)) == 0x18074u, "span across sources");
|
|
CHECK(CALL("time.Finalize", 0) == 0 && (uint8_t)CALL("time.IsInitialized", 0) == 0, "time finalize");
|
|
printf("time: ToCalendarTimeInUtc/ToPosixTimeFromUtc round trip %d values %s; local %04d-%02d-%02d %02d:%02d zone %s "
|
|
"offset %+d s; snapshot location %s\n", (int)(sizeof ts / sizeof *ts), rt_bad ? "FAILED" : "ok", local.year,
|
|
local.month, local.day, local.hour, local.minute, info.name, info.utc_offset, (const char *)(a + 0xa8));
|
|
}
|
|
|
|
#define STD_GRAVITY 9.80665f // SDL_STANDARD_GRAVITY
|
|
|
|
static int near3(const float *v, double x, double y, double z, double tol) {
|
|
return fabs(v[0] - x) <= tol && fabs(v[1] - y) <= tol && fabs(v[2] - z) <= tol;
|
|
}
|
|
static int at_rest(const MotionState *s) {
|
|
return !s->attributes && !s->sampling && near3(s->acc, 0, 0, -1, 0) && near3(s->gyro, 0, 0, 0, 0) &&
|
|
near3(s->dir, 1, 0, 0, 0) && near3(s->dir + 3, 0, 1, 0, 0) && near3(s->dir + 6, 0, 0, 1, 0);
|
|
}
|
|
|
|
// Vibration and six-axis handles, values and states with no gamepad motion (the sensor is stopped before the reads that
|
|
// check the rest state), the SDL -> Switch axis mapping and the sensor fusion on a private instance.
|
|
static void probe_feedback(uint32_t pid, uint32_t style) {
|
|
uint32_t six = ov_handheld() ? 4 : 3, vh[2] = {0}, sh[2] = {0}, dual[2] = {0}, zero = 0;
|
|
CHECK((int32_t)CALL("hid.GetVibrationDeviceHandles", U(vh), 2, U(&pid), style) == 2 && vh[0] == (six | pid << 8) &&
|
|
vh[1] == (0x10000 | six | pid << 8), "vibration handles %#x %#x", vh[0], vh[1]);
|
|
CHECK((int32_t)CALL("hid.GetSixAxisSensorHandles", U(sh), 2, U(&pid), style) == 1 && sh[0] == (0x20000 | six | pid << 8),
|
|
"six-axis handle %#x", sh[0]);
|
|
CHECK((int32_t)CALL("hid.GetVibrationDeviceHandles", U(dual), 2, U(&zero), 4) == 2 && dual[0] == 5 &&
|
|
dual[1] == 0x10005, "Joy-Con pair vibration handles %#x %#x (SDK: 0x5 0x10005)", dual[0], dual[1]);
|
|
char *keep_r = cfg_get("TOTK_RUMBLE") ? strdup(cfg_get("TOTK_RUMBLE")) : NULL;
|
|
char *keep_m = cfg_get("TOTK_MOTION") ? strdup(cfg_get("TOTK_MOTION")) : NULL;
|
|
set_env("TOTK_RUMBLE", "0");
|
|
set_env("TOTK_MOTION", "0");
|
|
uint32_t none[2] = {0xdead, 0xdead}, six_off[2] = {0};
|
|
CHECK((int32_t)CALL("hid.GetVibrationDeviceHandles", U(none), 2, U(&pid), style) == 0 && none[0] == 0xdead,
|
|
"TOTK_RUMBLE=0 still reports vibration handles");
|
|
// TOTK_MOTION is live: the handle stays (the sensor reads at rest while it is 0)
|
|
CHECK((int32_t)CALL("hid.GetSixAxisSensorHandles", U(six_off), 2, U(&pid), style) == 1 && six_off[0] == sh[0],
|
|
"TOTK_MOTION=0 drops the six-axis handle %#x", six_off[0]);
|
|
set_env("TOTK_RUMBLE", keep_r);
|
|
set_env("TOTK_MOTION", keep_m);
|
|
free(keep_r);
|
|
free(keep_m);
|
|
|
|
// Values with no amplitude: a connected gamepad stays still
|
|
float v[2][4] = {{0, 100, 0, 400}, {0, 150, 0, 300}}, got[4] = {0};
|
|
uint32_t info[2] = {9, 9};
|
|
uint8_t flag = 0;
|
|
CHECK(CALL("hid.InitializeVibrationDevice", U(&vh[0])) == 0 && CALL("hid.InitializeVibrationDevice", U(&vh[1])) == 0,
|
|
"InitializeVibrationDevice");
|
|
CHECK(CALL("hid.SendVibrationValue", U(&vh[0]), U(v[0])) == 0 && CALL("hid.GetActualVibrationValue", U(got), U(&vh[0])) == 0 &&
|
|
!memcmp(got, v[0], sizeof got), "the actual value is not the one sent");
|
|
CHECK(CALL("hid.SendVibrationValues", U(vh), U(v), 2) == 0 && CALL("hid.GetActualVibrationValue", U(got), U(&vh[1])) == 0 &&
|
|
!memcmp(got, v[1], sizeof got), "a batch does not reach the right device");
|
|
CHECK(CALL("hid.GetVibrationDeviceInfo", U(info), U(&vh[1])) == 0 && info[0] == 1 && info[1] == 2,
|
|
"right device info %u/%u", info[0], info[1]);
|
|
CHECK(CALL("hid.IsVibrationDeviceMounted", U(&flag), U(&vh[0])) == 0 && flag == 1 &&
|
|
CALL("hid.IsVibrationDeviceMounted", U(&flag), U(&dual[0])) == 0 && flag == 0, "mounted devices");
|
|
CHECK(CALL("hid.PermitVibration", 1) == 0 && CALL("hid.IsVibrationPermitted", U(&flag)) == 0 && flag == 1,
|
|
"vibration permitted");
|
|
|
|
MotionState *st = guest_alloc(17 * sizeof *st);
|
|
memset(st, 0xa5, 17 * sizeof *st);
|
|
int32_t count = -1;
|
|
CALL("hid.StartSixAxisSensor", U(&sh[0]));
|
|
CALL("hid.StopSixAxisSensor", U(&sh[0]));
|
|
CHECK(CALL("hid.GetSixAxisSensorStates", U(&count), U(st), 16, U(&sh[0])) == 0 && count == 16, "six-axis count %d",
|
|
count);
|
|
int rest = 1, guard = 1;
|
|
for (int i = 0; i < 16; i++) rest &= at_rest(&st[i]);
|
|
for (size_t i = 0; i < sizeof *st; i++) guard &= ((uint8_t *)&st[16])[i] == 0xa5;
|
|
CHECK(rest && guard, "stopped sensor: 16 states at rest %d, guard kept %d", rest, guard);
|
|
memset(st, 0xa5, sizeof *st);
|
|
CHECK(CALL("hid.GetSixAxisSensorState", U(st), U(&dual[0])) == 0 && at_rest(st), "a foreign handle reads at rest");
|
|
|
|
// SDL axes: lying flat (push +y) is Switch gravity -z, standing on the bottom edge is -y; yaw about SDL +y is +z.
|
|
double acc[3], gyro[3];
|
|
input_motion_axes((const float[3]){0, STD_GRAVITY, 0}, (const float[3]){0, 1, 0}, acc, gyro);
|
|
int flat = fabs(acc[0]) < 1e-6 && fabs(acc[1]) < 1e-6 && fabs(acc[2] + 1) < 1e-6 && gyro[2] == 1 && !gyro[0] && !gyro[1];
|
|
input_motion_axes((const float[3]){0, 0, -STD_GRAVITY}, (const float[3]){0, 0, 1}, acc, gyro);
|
|
int upright = fabs(acc[1] + 1) < 1e-6 && gyro[1] == -1;
|
|
CHECK(flat && upright, "SDL -> Switch axes (flat %d, upright %d)", flat, upright);
|
|
|
|
// Fusion: flat, then 90 degrees a second about z for 1 s: a quarter turn, x axis now along y
|
|
MotionFusion *m = motion_new();
|
|
uint64_t t = 1000000000ull;
|
|
const double down[3] = {0, 0, -1}, still[3] = {0, 0, 0}, turn[3] = {0, 0, M_PI / 2};
|
|
motion_feed(m, down, still, t);
|
|
for (int i = 0; i < 250; i++) motion_feed(m, down, turn, t += 4000000);
|
|
MotionState ms[16];
|
|
int n = motion_read(m, ms, 16, t + 1000000);
|
|
CHECK(n == 16 && ms[0].sampling == 251 && ms[15].sampling == 236 && ms[0].attributes == 1 &&
|
|
fabs(ms[0].angle[2] - 0.25) < 0.002 && fabs(ms[0].gyro[2] - 0.25) < 1e-6 && near3(ms[0].dir, 0, 1, 0, 0.01) &&
|
|
near3(ms[0].dir + 3, -1, 0, 0, 0.01) && near3(ms[0].dir + 6, 0, 0, 1, 0.01),
|
|
"fusion quarter turn: %d states, sampling %lld, angle %.4f rev, x axis (%.3f %.3f %.3f)", n,
|
|
(long long)ms[0].sampling, (double)ms[0].angle[2], (double)ms[0].dir[0], (double)ms[0].dir[1], (double)ms[0].dir[2]);
|
|
double quarter = ms[0].angle[2];
|
|
CHECK(motion_read(m, ms, 16, t + 300000000ull) == 0, "fusion states 300 ms after the last sample");
|
|
// StartSixAxisSensor again in the middle of the turn (the bow drawn again): the states from before it are gone, and
|
|
// the first one after it starts a fresh angle in a fresh frame; sampling numbers go on.
|
|
for (int i = 0; i < 250; i++) motion_feed(m, down, turn, t += 4000000);
|
|
motion_restart(m);
|
|
int same = motion_read(m, ms, 16, t + 1000000);
|
|
motion_feed(m, down, turn, t += 4000000);
|
|
int next = motion_read(m, ms, 16, t + 1000000);
|
|
CHECK(same == 0 && next == 1 && ms[0].sampling == 502 && fabs(ms[0].angle[2]) < 0.002 &&
|
|
near3(ms[0].dir, 1, 0, 0, 0.01),
|
|
"fusion restart: %d states in the frame of the Start, %d after one sample (sampling %lld, angle %.4f rev)", same,
|
|
next, (long long)ms[0].sampling, (double)ms[0].angle[2]);
|
|
printf("hid feedback: vibration handles %#x %#x, six-axis %#x, at rest when stopped; fusion quarter turn %.4f rev\n",
|
|
vh[0], vh[1], sh[0], quarter);
|
|
}
|
|
|
|
static void probe_input(void) {
|
|
uint32_t pid = ov_handheld() ? 0x20 : 0, other = ov_handheld() ? 0 : 0x20, ids[2] = {0, 0x20};
|
|
uint32_t style = ov_handheld() ? 2 : 1;
|
|
CALL("hid.InitializeNpad", 0);
|
|
CALL("hid.SetSupportedNpadIdType", U(ids), 2);
|
|
CALL("hid.SetSupportedNpadStyleSet", 3);
|
|
CHECK((uint32_t)CALL("hid.GetNpadStyleSet", U(&pid)) == style && (uint32_t)CALL("hid.GetNpadStyleSet", U(&other)) == 0,
|
|
"style sets");
|
|
uint8_t st[5][40];
|
|
memset(st, 0x55, sizeof st);
|
|
static const char *const kinds[] = {"hid.GetNpadStates.FullKey", "hid.GetNpadStates.Handheld",
|
|
"hid.GetNpadStates.JoyDual", "hid.GetNpadStates.JoyLeft",
|
|
"hid.GetNpadStates.JoyRight"};
|
|
int connected = 0, all_one = 1, no_buttons = 1;
|
|
for (int i = 0; i < 5; i++) {
|
|
all_one &= (int32_t)CALL(kinds[i], U(st[i]), 1, U(&pid)) == 1;
|
|
uint32_t attr;
|
|
uint64_t buttons;
|
|
memcpy(&attr, st[i] + 32, 4);
|
|
memcpy(&buttons, st[i] + 8, 8);
|
|
connected += attr & 1;
|
|
no_buttons &= !buttons;
|
|
if (attr & 1) CHECK(i == (ov_handheld() ? 1 : 0), "%s reports a connected controller", kinds[i]);
|
|
}
|
|
CHECK(all_one && connected == 1 && no_buttons, "controller states: %d connected", connected);
|
|
CHECK((int32_t)CALL("hid.GetNpadStates.FullKey", U(st[0]), 0, U(&pid)) == 0, "zero states requested");
|
|
CALL("hid.SetNpadJoyHoldType", 1);
|
|
uint8_t res[8] = {0}, arg[0x80] = {0};
|
|
CHECK(CALL("hid.ShowControllerSupport", U(res), U(arg)) == 0 && res[0] == 1, "controller support applet");
|
|
CALL("hid.InitializeMouse", 0);
|
|
CALL("hid.InitializeKeyboard", 0);
|
|
uint8_t mouse[40], kb[48];
|
|
CHECK((int32_t)CALL("hid.GetMouseStates", U(mouse), 1) == 1, "mouse states");
|
|
CALL("hid.GetKeyboardState", U(kb));
|
|
probe_feedback(pid, style);
|
|
CALL("hid.DisconnectNpad", U(&pid));
|
|
CHECK((uint32_t)CALL("hid.GetNpadStyleSet", U(&pid)) == 0, "a disconnected controller keeps its style set");
|
|
printf("hid: one connected %s controller (id %#x), no buttons, no mouse/keyboard input\n",
|
|
ov_handheld() ? "handheld" : "full-key", pid);
|
|
}
|
|
|
|
static void probe_media_gfx(void) {
|
|
uint8_t *dec = guest_alloc(0x40), *work = guest_alloc(0x9000);
|
|
uint64_t ws = CALL("codec.GetWorkBufferSize", 48000, 2, 0);
|
|
CHECK(ws > 0 && CALL("codec.GetWorkBufferSize", 44100, 2, 0) == 0, "Opus work buffer size");
|
|
CHECK(CALL("codec.Initialize", U(dec), 48000, 2, U(work), ws, 0) == 0, "InitializeHardwareOpusDecoder");
|
|
CHECK(CALL("codec.Initialize", U(dec), 48000, 3, U(work), ws, 0) != 0, "3 channels accepted");
|
|
CHECK((int32_t)CALL("codec.GetSampleRate", U(dec)) == 48000 && (int32_t)CALL("codec.GetChannelCount", U(dec)) == 2,
|
|
"decoder rate/channels");
|
|
CHECK(CALL("codec.Reset", U(dec)) == 0, "ResetHardwareOpusDecoder");
|
|
CALL("codec.Finalize", U(dec));
|
|
|
|
CALL("nv.SetGraphicsAllocator", 0x7000001000, 0x7000002000, 0x7000003000, 0x1234);
|
|
CALL("nv.SetGraphicsDevtoolsAllocator", 0x7000004000, 0x7000005000, 0x7000006000, 0x5678);
|
|
uint8_t *mem = guest_alloc(0x1000);
|
|
CALL("nv.InitializeGraphics", U(mem), 0x1000);
|
|
const OvNvState *s = ov_nv_state();
|
|
CHECK(s->graphics.alloc == 0x7000001000 && s->graphics.realloc == 0x7000003000 && s->graphics.user == 0x1234 &&
|
|
s->devtools.free == 0x7000005000 && s->devtools.user == 0x5678 && s->memory == U(mem) &&
|
|
s->memory_size == 0x1000 && s->initialized, "nv state not recorded");
|
|
void *display = NULL, *layer = NULL, *window = NULL;
|
|
CHECK(CALL("vi.OpenDefaultDisplay", U(&display)) != 0, "OpenDefaultDisplay before vi::Initialize");
|
|
CALL("vi.Initialize", 0);
|
|
CHECK(CALL("vi.OpenDefaultDisplay", U(&display)) == 0 && display, "OpenDefaultDisplay");
|
|
CHECK(CALL("vi.CreateLayer", U(&layer), U(display)) == 0 && layer, "CreateLayer");
|
|
CHECK(CALL("vi.CreateLayer", U(&layer), U(&layer)) != 0, "CreateLayer on an unknown display");
|
|
CHECK(CALL("vi.GetNativeWindow", U(&window), U(layer)) == 0 && window, "GetNativeWindow");
|
|
uint64_t ev = U(guest_alloc(0x100));
|
|
CHECK(CALL("vi.GetDisplayVsyncEvent", ev, U(display)) == 0, "GetDisplayVsyncEvent");
|
|
int vsync = event_works(ev, 1);
|
|
CHECK(vsync, "the vsync event does not work as an auto-clear system event");
|
|
printf("codec/nv/vi: Opus decoder state kept, allocators and work memory recorded, display %p layer %p window %p, "
|
|
"vsync event %s\n", display, layer, window, vsync ? "bound (auto-clear)" : "FAILED");
|
|
}
|
|
|
|
// ---- software keyboard (swkbd.c): ShowKeyboardArg built by the SDK's own setters, as the horse naming does
|
|
enum { KB_TYPE = 0x00, KB_DISABLED = 0x1c, KB_HEADER = 0x24, KB_GUIDE = 0x1a8, KB_MAX_LEN = 0x3ac, KB_MIN_LEN = 0x3b0,
|
|
KB_UTF8 = 0x3bd, KB_INIT_LEN = 0x3c4, STRING_SIZE = 0x7d4, SWKBD_CANCELED = 0x29f };
|
|
#define ARG_WORK (RT_V143 ? 0x518 : 0x4d0) // SDK 20.5 grew KeyboardConfig (swkbd.c)
|
|
#define ARG_WORK_SIZE (ARG_WORK + 8)
|
|
#define ARG_SIZE (RT_V143 ? 0x548 : 0x500)
|
|
|
|
static uint64_t sdk(const char *name) {
|
|
uint64_t a = ldr_symbol(name);
|
|
if (!a) rt_fatal("probe-services: the SDK exports no %s", name);
|
|
return a;
|
|
}
|
|
|
|
// NUL-terminated UTF-16 copy of utf8 in guest memory
|
|
static uint16_t *u16(const char *utf8) {
|
|
uint16_t *g = guest_alloc(0x200);
|
|
utf8_to_utf16(utf8, g, 128);
|
|
return g;
|
|
}
|
|
|
|
static int u16_is(const uint16_t *a, const char *utf8) {
|
|
uint16_t b[128];
|
|
utf8_to_utf16(utf8, b, 128);
|
|
for (size_t i = 0;; i++) {
|
|
if (a[i] != b[i]) return 0;
|
|
if (!a[i]) return 1;
|
|
}
|
|
}
|
|
|
|
typedef struct {
|
|
int preset;
|
|
uint32_t type, max, min, disabled;
|
|
int utf8, window;
|
|
const char *initial, *answer, *want;
|
|
uint32_t result;
|
|
} KbCase;
|
|
|
|
static void probe_swkbd(void) {
|
|
uint64_t make = sdk("_ZN2nn5swkbd10MakePresetEPNS0_14KeyboardConfigENS0_6PresetE");
|
|
uint64_t header = sdk("_ZN2nn5swkbd13SetHeaderTextEPNS0_14KeyboardConfigEPKDs");
|
|
uint64_t guide = sdk("_ZN2nn5swkbd12SetGuideTextEPNS0_14KeyboardConfigEPKDs");
|
|
uint64_t initial = sdk("_ZN2nn5swkbd14SetInitialTextEPNS0_15ShowKeyboardArgEPKDs");
|
|
uint64_t string_size = sdk("_ZN2nn5swkbd27GetRequiredStringBufferSizeEv");
|
|
uint64_t work_size = sdk("_ZN2nn5swkbd25GetRequiredWorkBufferSizeEb");
|
|
CHECK(rt_call(gc, string_size, 0) == STRING_SIZE && rt_call(gc, work_size, 1, 0) == 0x1000, "SDK swkbd buffer sizes");
|
|
uint8_t *arg = guest_alloc(ARG_SIZE), *work = guest_alloc(0x1000);
|
|
uint64_t *str = guest_alloc(16);
|
|
uint16_t *out = guest_alloc(STRING_SIZE);
|
|
str[0] = U(out), str[1] = STRING_SIZE;
|
|
uint64_t w = U(work), ws = 0x1000;
|
|
|
|
// the SDK setters write where swkbd.c reads
|
|
rt_call(gc, make, 2, U(arg), 2);
|
|
rt_call(gc, header, 2, U(arg), U(u16("Hdr")));
|
|
rt_call(gc, guide, 2, U(arg), U(u16("Gd")));
|
|
memcpy(arg + ARG_WORK, &w, 8), memcpy(arg + ARG_WORK_SIZE, &ws, 8);
|
|
rt_call(gc, initial, 2, U(arg), U(u16("Zelda")));
|
|
int32_t init_len;
|
|
memcpy(&init_len, arg + KB_INIT_LEN, 4);
|
|
CHECK(u16_is((uint16_t *)(arg + KB_HEADER), "Hdr") && u16_is((uint16_t *)(arg + KB_GUIDE), "Gd") && init_len == 5,
|
|
"ShowKeyboardArg layout: header, guide or initial text not where swkbd.c reads them");
|
|
|
|
const char *def = ui_text(UI_SWKBD_DEFAULT_NAME);
|
|
const KbCase cases[] = {
|
|
{2, 8, 9, 0, 0x2c, 0, 0, NULL, "Epona", "Epona", 0}, // TotK's horse naming (main+0x1ecbf64)
|
|
{2, 8, 9, 0, 0x2c, 0, 0, NULL, "ABCDEFGHIJKL", "ABCDEFGHI", 0}, // at most 9
|
|
{2, 8, 9, 0, 0x2c, 0, 0, NULL, "a@b%c\\d e/f", "abcd e/f", 0}, // '@' '%' '\' disabled
|
|
{2, 8, 9, 0, 0x2c, 0, 0, NULL, "<cancel>", "", SWKBD_CANCELED},
|
|
{2, 8, 9, 0, 0x2c, 0, 0, NULL, "<initial>", def, 0}, // no initial text: the default name
|
|
{0, 2, 0, 0, 0, 0, 0, "Zelda", "<initial>", "Zelda", 0}, // initial text from the work buffer
|
|
{0, 2, 0, 0, 0, 1, 0, NULL, "\xe5\xb0\x8f\xe9\xa9\xac" "Ab", "\xe5\xb0\x8f\xe9\xa9\xac" "Ab", 0}, // UTF-8 out
|
|
{0, 2, 3, 0, 0, 0, 0, NULL, "\xe5\xb0\x8f\xe9\xa9\xac\xf0\x9f\x90\xb4", "\xe5\xb0\x8f\xe9\xa9\xac", 0}, // no half pair
|
|
{0, 1, 0, 0, 0, 0, 0, NULL, "a1b2c3", "123", 0}, // number pad
|
|
{0, 2, 9, 3, 0, 0, 0, NULL, "ab", "", SWKBD_CANCELED}, // shorter than the minimum
|
|
{2, 8, 9, 0, 0x2c, 0, 1, NULL, "ok", def, 0}, // the window, OK pressed
|
|
{2, 8, 9, 0, 0x2c, 0, 1, NULL, "cancel", "", SWKBD_CANCELED}, // the window, Cancel pressed
|
|
};
|
|
int bad = 0, n = (int)(sizeof cases / sizeof *cases);
|
|
for (int i = 0; i < n; i++) {
|
|
const KbCase *k = &cases[i];
|
|
memset(arg, 0, ARG_SIZE), memset(work, 0, 0x1000), memset(out, 0x55, STRING_SIZE);
|
|
rt_call(gc, make, 2, U(arg), k->preset);
|
|
memcpy(arg + KB_TYPE, &k->type, 4), memcpy(arg + KB_MAX_LEN, &k->max, 4), memcpy(arg + KB_MIN_LEN, &k->min, 4);
|
|
if (k->disabled) memcpy(arg + KB_DISABLED, &k->disabled, 4);
|
|
arg[KB_UTF8] = (uint8_t)k->utf8;
|
|
memcpy(arg + ARG_WORK, &w, 8), memcpy(arg + ARG_WORK_SIZE, &ws, 8);
|
|
if (k->initial) rt_call(gc, initial, 2, U(arg), U(u16(k->initial)));
|
|
set_env(k->window ? "TOTK_SWKBD_TEST" : "TOTK_SWKBD_ANSWER", k->answer);
|
|
uint32_t r = (uint32_t)CALL("swkbd.ShowKeyboard", U(str), U(arg));
|
|
set_env("TOTK_SWKBD_TEST", NULL), set_env("TOTK_SWKBD_ANSWER", NULL);
|
|
int ok = r == k->result && (k->utf8 ? !strcmp((const char *)out, k->want) : u16_is(out, k->want));
|
|
checks++;
|
|
if (!ok) bad++, printf(" FAIL: swkbd case %d (%s%s): result %#x\n", i, k->window ? "window " : "", k->answer, r);
|
|
}
|
|
failures += bad;
|
|
// after the window the game reads nothing until everything is released; with nothing held one read lifts that
|
|
uint64_t b;
|
|
int32_t lx, ly, rx, ry;
|
|
input_poll_ui(&b, &lx, &ly, &rx, &ry);
|
|
if (!b) input_poll(&b, &lx, &ly, &rx, &ry);
|
|
printf("swkbd: %d of %d cases (arguments from the SDK's MakePreset/SetHeaderText/SetGuideText/SetInitialText; "
|
|
"horse-name limits, disabled keys, cancel 0x29f, default name \"%s\", UTF-8 out, surrogate pairs, number pad, "
|
|
"minimum length, the Win32 window with OK and Cancel)\n", n - bad, n, def);
|
|
}
|
|
|
|
// ---- the menu's input gate: Minus+Plus is consumed, a menu that is open leaves the game a neutral pad
|
|
static void probe_menu_gate(void) {
|
|
const uint64_t minus = 1ull << 11, plus = 1ull << 10, a = 1ull;
|
|
int held = 0;
|
|
int32_t st[4] = {100, -200, 300, -400};
|
|
uint64_t b = minus | a; // Minus alone, a frame before Plus: reaches the guest unchanged
|
|
int t1 = input_menu_gate(&held, 0, &b, st);
|
|
CHECK(!t1 && b == (minus | a) && st[0] == 100 && st[3] == -400 && !held, "Minus alone changed: toggle %d buttons %#llx", t1,
|
|
(unsigned long long)b);
|
|
b = minus | plus | a; // both down: the guest sees neither, the menu toggles once
|
|
int t2 = input_menu_gate(&held, 0, &b, st);
|
|
CHECK(t2 == 1 && b == a && held, "Minus+Plus: toggle %d buttons %#llx", t2, (unsigned long long)b);
|
|
b = minus | plus; // still held with the menu open: no second toggle, a neutral pad
|
|
int t3 = input_menu_gate(&held, 1, &b, st);
|
|
CHECK(!t3 && b == 0 && st[0] == 0 && st[1] == 0 && st[2] == 0 && st[3] == 0, "menu open: toggle %d buttons %#llx", t3,
|
|
(unsigned long long)b);
|
|
b = a | plus; st[0] = 5; // the menu is open and the pad is neutral whatever is pressed
|
|
input_menu_gate(&held, 1, &b, st);
|
|
CHECK(b == 0 && st[0] == 0, "an open menu left buttons %#llx", (unsigned long long)b);
|
|
b = 0; // released, then pressed again toggles again
|
|
input_menu_gate(&held, 0, &b, st);
|
|
b = minus | plus;
|
|
CHECK(input_menu_gate(&held, 0, &b, st) == 1, "the second hold did not toggle");
|
|
// Plus released first while Minus is still down: Minus must not leak; a new Minus press after both are up passes
|
|
held = 0;
|
|
b = minus | plus;
|
|
input_menu_gate(&held, 0, &b, st);
|
|
b = minus;
|
|
int t4 = input_menu_gate(&held, 0, &b, st);
|
|
CHECK(!t4 && b == 0 && held, "Minus left down after Plus leaked: buttons %#llx", (unsigned long long)b);
|
|
b = 0;
|
|
input_menu_gate(&held, 0, &b, st);
|
|
b = minus | a;
|
|
int t5 = input_menu_gate(&held, 0, &b, st);
|
|
CHECK(!t5 && b == (minus | a) && !held, "Minus after the release did not pass: buttons %#llx", (unsigned long long)b);
|
|
printf("menu gate: Minus alone passes, Minus+Plus is consumed and toggles once per hold, an open menu gives a neutral pad\n");
|
|
}
|
|
|
|
// ---- shared fonts: the extracted files when present (the missing case is a fatal child)
|
|
static void probe_fonts(void) {
|
|
char dir[PLAT_PATH_MAX], path[PLAT_PATH_MAX + 32];
|
|
snprintf(path, sizeof path, "%s/0-standard.bfttf", ov_shared_font_dir(dir, sizeof dir));
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) {
|
|
printf("pl: no shared fonts in %s (tools/extract_fonts.py); Address/Size checked as named fatals only\n", dir);
|
|
return;
|
|
}
|
|
uint8_t head[12];
|
|
size_t got = fread(head, 1, sizeof head, f);
|
|
plat_fseek64(f, 0, SEEK_END);
|
|
int64_t len = plat_ftell64(f);
|
|
fclose(f);
|
|
uint64_t size = CALL("pl.GetSharedFontSize", 0), addr = CALL("pl.GetSharedFontAddress", 0);
|
|
CHECK(got == sizeof head && size && size + 8 == (uint64_t)len, "shared font 0: size %llu, file %lld bytes",
|
|
(unsigned long long)size, (long long)len);
|
|
const uint8_t *p = (const uint8_t *)(uintptr_t)addr;
|
|
CHECK(addr && !memcmp(p - 8, head, sizeof head) && ov_bfttf_size(p - 8, size + 8) == size,
|
|
"shared font 0: the address is not 8 bytes into the BFTTF file");
|
|
CHECK(CALL("pl.GetSharedFontAddress", 0) == addr, "shared font 0: the address changed");
|
|
printf("pl: shared font 0 (Standard) at %#llx, %llu bytes, BFTTF header checked (%s)\n", (unsigned long long)addr,
|
|
(unsigned long long)size, path);
|
|
}
|
|
|
|
// The host catalog (ui_text.c): every menu id has its own English, Russian, Brazilian Portuguese and Simplified
|
|
// Chinese text, the software keyboard keeps its game-language rows, and menu_text follows TOTK_MENU_LANG.
|
|
static void probe_menu_text(void) {
|
|
static const char *const langs[] = {"en", "ja", "fr", "de", "it", "es", "nl", "pt", "ru", "zh-Hans", "zh-Hant", "ko",
|
|
"pt-BR"};
|
|
for (int id = UI_MENU_FIRST; id <= UI_MENU_LAST_ID; id++) {
|
|
CHECK(ui_text_defined("en", (UiTextId)id) && ui_text_defined("ru", (UiTextId)id) &&
|
|
ui_text_defined("pt-BR", (UiTextId)id), "menu text id %d lacks en, ru or pt-BR", id);
|
|
CHECK(*ui_text_in("en", (UiTextId)id) && *ui_text_in("ru", (UiTextId)id) && *ui_text_in("pt-BR", (UiTextId)id),
|
|
"menu text id %d is empty", id);
|
|
// the description pane keeps the whole text and a note after it (option_rows.cpp desc_set)
|
|
CHECK(strlen(ui_text_in("en", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
|
|
strlen(ui_text_in("ru", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
|
|
strlen(ui_text_in("pt-BR", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
|
|
strlen(ui_text_in("zh-Hans", (UiTextId)id)) + 256 < UI_DESC_BYTES,
|
|
"menu text id %d is too long for the description pane", id);
|
|
CHECK(ui_text_defined("zh-Hans", (UiTextId)id), "menu text id %d lacks Simplified Chinese", id);
|
|
}
|
|
CHECK(!ui_text_defined("en", UI_TEXT_COUNT) && !*ui_text_in("ru", UI_TEXT_COUNT), "an id past the catalog gives text");
|
|
// other languages fall back to English for the menu, and keep their own keyboard texts
|
|
CHECK(!strcmp(ui_text_in("ja", UI_MENU_CONTINUE), ui_text_in("en", UI_MENU_CONTINUE)), "ja menu text is not English");
|
|
CHECK(!strcmp(ui_text_in("ru", UI_SWKBD_OK), "ОК") && !strcmp(ui_text_in("de", UI_SWKBD_CANCEL), "Abbrechen") &&
|
|
!strcmp(ui_text_in("en", UI_SWKBD_OK), "OK"), "keyboard texts changed");
|
|
// language names: the same string in every row
|
|
for (int id = UI_MENU_LANGNAME_EN; id <= UI_MENU_LANGNAME_ES_419; id++)
|
|
for (size_t i = 1; i < sizeof langs / sizeof *langs; i++)
|
|
CHECK(!strcmp(ui_text_in(langs[i], (UiTextId)id), ui_text_in("en", (UiTextId)id)), "language name %d differs in %s", id, langs[i]);
|
|
CHECK(!strcmp(ui_text_in("en", UI_MENU_LANGNAME_RU), "Русский"), "Russian is not shown as Russian");
|
|
// game language list: pt-BR only where the build ships USpt; the Switch variants are entries of their own
|
|
{
|
|
UiGameLang gl[16];
|
|
int n121 = ui_text_game_langs(121, gl, 16), has[16] = {0};
|
|
for (int i = 0; i < n121; i++) CHECK(strcmp(gl[i].code, "pt-BR"), "1.2.1 game language list offers pt-BR");
|
|
int n143 = ui_text_game_langs(143, gl, 16), pt = 0;
|
|
for (int i = 0; i < n143; i++) {
|
|
pt |= !strcmp(gl[i].code, "pt-BR");
|
|
for (int j = 0; j < i; j++) CHECK(strcmp(gl[i].code, gl[j].code), "duplicate game language %s", gl[i].code);
|
|
}
|
|
CHECK(pt && n143 == n121 + 1, "1.4.3 game language list lacks pt-BR");
|
|
static const char *const want[] = {"en-GB", "fr-CA", "es-419"};
|
|
for (int w = 0; w < 3; w++)
|
|
for (int i = 0; i < n143; i++) has[w] |= !strcmp(gl[i].code, want[w]);
|
|
CHECK(has[0] && has[1] && has[2], "game language list lacks en-GB, fr-CA or es-419");
|
|
}
|
|
// the overlay formats keep their conversions
|
|
CHECK(strstr(ui_text_in("ru", UI_MENU_OV_GAME_PCT), "%.0f%%") && strstr(ui_text_in("ru", UI_MENU_OV_MAX_MS), "%.1f") &&
|
|
strstr(ui_text_in("ru", UI_MENU_OV_LOW1), "%.0f") && strstr(ui_text_in("ru", UI_MENU_OV_LOW01), "%.0f"),
|
|
"overlay format lost its conversion");
|
|
CHECK(strstr(ui_text_in("pt-BR", UI_MENU_OV_GAME_PCT), "%.0f%%") &&
|
|
strstr(ui_text_in("pt-BR", UI_MENU_OV_MAX_MS), "%.1f") &&
|
|
strstr(ui_text_in("pt-BR", UI_MENU_OV_LOW1), "%.0f") && strstr(ui_text_in("zh-Hans", UI_MENU_OV_LOW01), "%.0f"), "pt-BR overlay format lost its conversion");
|
|
// a Brazilian game language gets the Brazilian keyboard row, Portugal's Portuguese keeps its own
|
|
CHECK(!strcmp(ui_text_lang("pt-BR"), "pt-BR") && !strcmp(ui_text_lang("pt"), "pt") &&
|
|
strcmp(ui_text_in("pt-BR", UI_SWKBD_PROMPT), ui_text_in("pt", UI_SWKBD_PROMPT)), "pt-BR keyboard row not used");
|
|
// menu_text follows the setting live
|
|
cfg_set("TOTK_MENU_LANG", "ru");
|
|
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in("ru", UI_MENU_CONTINUE), "menu_text ignores MENU_LANG=ru");
|
|
cfg_set("TOTK_MENU_LANG", "pt-BR");
|
|
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in("pt-BR", UI_MENU_CONTINUE), "menu_text ignores MENU_LANG=pt-BR");
|
|
cfg_set("TOTK_MENU_LANG", "en");
|
|
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in("en", UI_MENU_CONTINUE), "menu_text did not follow MENU_LANG=en");
|
|
cfg_set("TOTK_MENU_LANG", "zh-Hans");
|
|
CHECK(menu_text(UI_MENU_TITLE) == ui_text_in("zh-Hans", UI_MENU_TITLE), "menu_text ignores MENU_LANG=zh-Hans");
|
|
cfg_set("TOTK_MENU_LANG", "");
|
|
// the system language as ui_text.c resolve_menu_lang takes it: the tag, else its base language (ru-RU: ru), else en
|
|
const char *sys_lang = ui_text_lang(plat_ui_language());
|
|
char sys_base[16] = "";
|
|
size_t base_n = strcspn(sys_lang, "-");
|
|
if (sys_lang[base_n] && base_n < sizeof sys_base) memcpy(sys_base, sys_lang, base_n);
|
|
const char *auto_lang = ui_text_defined(sys_lang, UI_MENU_TITLE) ? sys_lang
|
|
: *sys_base && ui_text_defined(sys_base, UI_MENU_TITLE) ? sys_base
|
|
: "en";
|
|
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in(auto_lang, UI_MENU_CONTINUE), "menu_text auto ignores the system language");
|
|
}
|
|
|
|
static void probe_main(Cpu *c) {
|
|
gc = c;
|
|
probe_oe();
|
|
probe_messages();
|
|
probe_applet();
|
|
probe_account();
|
|
probe_time();
|
|
CHECK((uint32_t)CALL("pl.GetSharedFontLoadState", 0) == 1, "shared font load state");
|
|
probe_input();
|
|
probe_media_gfx();
|
|
probe_swkbd();
|
|
probe_menu_text();
|
|
probe_menu_gate();
|
|
probe_fonts();
|
|
}
|
|
|
|
// ---- child processes
|
|
// Runs this executable again with the same command line and TOTK_PROBE_SERVICES_CHILD=<mode>; its stdout and stderr
|
|
// into out. Returns the exit code.
|
|
static uint32_t run_child(const char *mode, const char *env, char *out, size_t cap) {
|
|
char name[64] = "";
|
|
const char *eq = env ? strchr(env, '=') : NULL;
|
|
if (eq) snprintf(name, sizeof name, "%.*s", (int)(eq - env), env), plat_setenv(name, eq + 1);
|
|
int code = plat_run_self(CHILD_ENV, mode, out, cap, 0);
|
|
if (eq) plat_setenv(name, NULL);
|
|
if (code < 0) rt_fatal("probe-services: cannot start the %s child", mode);
|
|
return (uint32_t)code;
|
|
}
|
|
|
|
static const char *child_mode;
|
|
|
|
static void child_main(Cpu *c) {
|
|
gc = c;
|
|
if (!strcmp(child_mode, "uid")) {
|
|
uint64_t h[3] = {0}, uid[2] = {0};
|
|
CALL("account.Initialize", 0);
|
|
uint8_t opened = (uint8_t)CALL("account.TryOpenPreselectedUser", U(h));
|
|
uint32_t r = (uint32_t)CALL("account.GetUserId", U(uid), U(h));
|
|
printf("child uid %016llx%016llx (open %u, result %#x)\n", (unsigned long long)uid[1], (unsigned long long)uid[0],
|
|
opened, r);
|
|
return;
|
|
}
|
|
uint8_t *buf = guest_alloc(0x1000);
|
|
if (!strcmp(child_mode, "nvn")) {
|
|
uint64_t known = CALL("nvnBootstrapLoader", U("nvnDeviceGetProcAddress"));
|
|
uint64_t unknown = CALL("nvnBootstrapLoader", U("nvnNoSuchProc"));
|
|
printf("child nvn: known %#llx expected %#llx unknown %#llx\n", (unsigned long long)known,
|
|
(unsigned long long)nvn_proc_address("nvnDeviceGetProcAddress"), (unsigned long long)unknown);
|
|
return;
|
|
}
|
|
if (!strcmp(child_mode, "opus")) CALL("codec.Decode", U(buf), U(buf + 8), U(buf + 16), U(buf + 64), 0x100, U(buf + 0x800), 16);
|
|
else if (!strcmp(child_mode, "font-address")) CALL("pl.GetSharedFontAddress", 0);
|
|
else if (!strcmp(child_mode, "font-size")) CALL("pl.GetSharedFontSize", 0);
|
|
else rt_fatal("probe-services: unknown child case %s", child_mode);
|
|
printf("child %s: the call returned\n", child_mode);
|
|
}
|
|
|
|
static int run_as_child(const char *mode) {
|
|
child_mode = mode;
|
|
bind_addresses();
|
|
hostfn_seal();
|
|
kern_run_on_guest_main(child_main);
|
|
return 0;
|
|
}
|
|
|
|
static void probe_children(void) {
|
|
static char out[16384];
|
|
uint32_t code = run_child("uid", NULL, out, sizeof out);
|
|
unsigned long long hi = 0, lo = 0;
|
|
const char *p = strstr(out, "child uid ");
|
|
int parsed = p && sscanf(p + 10, "%16llx%16llx", &hi, &lo) == 2;
|
|
int same = parsed && code == 0 && hi == probe_uid[1] && lo == probe_uid[0];
|
|
CHECK(same, "the second run's account UID differs (exit %u): %s", code, out);
|
|
printf("account: UID %016llx%016llx in %s/account.uid, the same in a second run: %s\n",
|
|
(unsigned long long)probe_uid[1], (unsigned long long)probe_uid[0], rt_config()->profile, same ? "yes" : "NO");
|
|
|
|
// nvnBootstrapLoader returns the synthetic address of a proc (nvn_procs.c), 0 for an unknown name
|
|
code = run_child("nvn", NULL, out, sizeof out);
|
|
unsigned long long known = 0, expected = 0, unknown = 1;
|
|
p = strstr(out, "child nvn: ");
|
|
parsed = p && sscanf(p, "child nvn: known %llx expected %llx unknown %llx", &known, &expected, &unknown) == 3;
|
|
int nvn_ok = parsed && code == 0 && known && known == expected && unknown == 0;
|
|
CHECK(nvn_ok, "child nvn: exit %u, output: %s", code, out);
|
|
if (nvn_ok) ov("nvnBootstrapLoader")->calls++;
|
|
printf("nvnBootstrapLoader: nvnDeviceGetProcAddress -> %#llx (proc table %#llx), unknown name -> %#llx\n", known,
|
|
expected, unknown);
|
|
|
|
for (int i = 0; i < NFATAL; i++) {
|
|
code = run_child(fatals[i].child, fatals[i].env, out, sizeof out);
|
|
int ok = code == 70 && strstr(out, fatals[i].fatal);
|
|
CHECK(ok, "child %s: exit %u, output: %s", fatals[i].child, code, out);
|
|
if (ok) ov(fatals[i].key)->calls++;
|
|
const char *line = strstr(out, "FATAL: ");
|
|
int len = line ? (int)strcspn(line, "\r\n") : 0;
|
|
printf("fatal stub %-20s child exit %u: %.*s\n", fatals[i].key, code, len, line ? line : "(no FATAL line)");
|
|
}
|
|
}
|
|
|
|
int probe_services(void) {
|
|
const char *child = getenv(CHILD_ENV);
|
|
if (child && *child) return run_as_child(child);
|
|
ldr_load(rt_config()->exefs);
|
|
|
|
// Names bound by the loader: each override and observer probed here must match a relocation.
|
|
int bound = 0, obs_bound = 0;
|
|
for (int i = 0; i < 4096; i++) {
|
|
const char *n = hostfn_name(RT_HOSTFN_BASE + 16ull * (uint64_t)i);
|
|
if (!n) break;
|
|
for (int k = 0; k < NOV; k++)
|
|
if (!strcmp(ovs[k].name, n) && !ovs[k].bound) ovs[k].bound = 1, bound++;
|
|
for (int k = 0; k < NOBS; k++)
|
|
if (!strcmp(observers[k], n)) obs_bound++;
|
|
}
|
|
for (int k = 0; k < NOV; k++) // 1.2.1 lists no add-on content
|
|
CHECK(ovs[k].bound || (!RT_V143 && !strcmp(ovs[k].key, "aoc.ListAddOnContent")), "override %s matches no relocation",
|
|
ovs[k].name);
|
|
for (int k = 0; k < NOBS; k++) CHECK(hostfn_observer(observers[k]), "observer %s is not linked", observers[k]);
|
|
CHECK(obs_bound == NOBS, "%d of %d observers match a JUMP_SLOT", obs_bound, NOBS);
|
|
|
|
// Every linked override is either probed here or belongs to another probe or owner (nn::fs: fs probe, fibers: kernel probe,
|
|
// movie::Decoder: --probe-movie, memcpy: the graphics write-back in nvn_api_core.c).
|
|
int linked = hostfn_table_size(0), others = 0;
|
|
for (int i = 0; i < linked; i++) {
|
|
const char *n = hostfn_table_name(0, i);
|
|
int mine = 0;
|
|
for (int k = 0; k < NOV; k++) mine |= !strcmp(ovs[k].name, n);
|
|
int theirs = !strncmp(n, "_ZN2nn2fs", 9) || !strncmp(n, "_ZN5movie", 9) || !strcmp(n, "memcpy") ||
|
|
!strcmp(n, KERN_SYM_INITIALIZE_FIBER) ||
|
|
!strcmp(n, KERN_SYM_FINALIZE_FIBER) || !strcmp(n, KERN_SYM_SWITCH_TO_FIBER) ||
|
|
!strcmp(n, KERN_SYM_GET_CURRENT_FIBER);
|
|
others += theirs;
|
|
CHECK(mine || theirs, "override %s is linked but not probed", n);
|
|
}
|
|
printf("bound: %d of %d overrides and %d of %d observers match relocations of the loaded modules\n", bound, NOV,
|
|
obs_bound, NOBS);
|
|
|
|
bind_addresses();
|
|
hostfn_seal();
|
|
kern_run_on_guest_main(probe_main);
|
|
probe_children();
|
|
|
|
int called = 0;
|
|
for (int k = 0; k < NOV; k++) {
|
|
called += ovs[k].calls > 0;
|
|
CHECK(ovs[k].calls > 0, "override %s was not called", ovs[k].key);
|
|
}
|
|
printf("called: %d of %d overrides through rt_dispatch (%d named fatal stubs in child processes); %d other "
|
|
"overrides belong to the kernel and fs probes\n", called, NOV, NFATAL, others);
|
|
printf("overrides %d, observers %d\n", linked, hostfn_table_size(1));
|
|
if (failures) {
|
|
printf("probe-services: FAILED (%d of %d checks)\n", failures, checks);
|
|
return 1;
|
|
}
|
|
printf("probe-services: passed\n");
|
|
return 0;
|
|
}
|