diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 94e484b31..e7ae4fef6 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -32,4 +32,14 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_pad_mix PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(audio_out_mix_level_pad_spk_tests tests/AudioOutMixLevelPadSpk.cpp) + target_include_directories(audio_out_mix_level_pad_spk_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_mix_level_pad_spk_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_mix_level_pad_spk_tests) + add_test(NAME audio_out_mix_level_pad_spk COMMAND audio_out_mix_level_pad_spk_tests) + if(WIN32) + set_tests_properties(audio_out_mix_level_pad_spk PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 04a0fc850..8f865806a 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -26,6 +26,7 @@ static constexpr int PORT_TYPE_AUX = 127; static constexpr int PORTS_MAX = 32; static constexpr int DEFAULT_VOLUME = 32768; +static constexpr int DEFAULT_PADSPK_MIX_LEVEL = 11626; static constexpr std::uint32_t FORMAT_MASK = 0xFFu; static constexpr std::uint64_t TARGET_LATENCY_US = 40000; static constexpr std::uint64_t DRAIN_TIMEOUT_US = 200000; @@ -86,6 +87,7 @@ struct Port { Format format = Format::Unknown; int channels = 0; int volume[8] = {}; + int mixLevel = DEFAULT_VOLUME; std::uint64_t lastOutputTime = 0; SDL_AudioDeviceID device = 0; SDL_AudioSpec spec = {}; @@ -143,7 +145,7 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector const auto bps = bytesPerSample(port.format); const auto size = frames * ch * bps; - bool volumeChanged = false; + bool volumeChanged = port.mixLevel != DEFAULT_VOLUME; for (std::uint32_t i = 0; i < ch; i++) { if (port.volume[i] != DEFAULT_VOLUME) { volumeChanged = true; @@ -161,11 +163,12 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector if (formatIsFloat(port.format)) { auto* dst = reinterpret_cast(buf.data()); const auto* src = static_cast(data); + const float mixLevel = static_cast(port.mixLevel) / static_cast(DEFAULT_VOLUME); for (std::uint32_t fr = 0; fr < frames; fr++) { for (std::uint32_t c = 0; c < ch; c++) { const auto srcCh = isStd ? STD_8CH_MAP[c] : c; dst[fr * ch + c] = src[fr * ch + srcCh] * - (static_cast(port.volume[c]) / static_cast(DEFAULT_VOLUME)); + (static_cast(port.volume[c]) / static_cast(DEFAULT_VOLUME)) * mixLevel; } } } else { @@ -175,7 +178,7 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector for (std::uint32_t c = 0; c < ch; c++) { const auto srcCh = isStd ? STD_8CH_MAP[c] : c; std::int64_t s = static_cast(src[fr * ch + srcCh]) * - port.volume[c] / DEFAULT_VOLUME; + port.volume[c] * port.mixLevel / (static_cast(DEFAULT_VOLUME) * DEFAULT_VOLUME); s = std::clamp(s, static_cast(std::numeric_limits::min()), static_cast(std::numeric_limits::max())); @@ -325,6 +328,7 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len port.format = format; port.channels = channelsForFormat(format); port.lastOutputTime = 0; + port.mixLevel = type == PORT_TYPE_PADSPK ? DEFAULT_PADSPK_MIX_LEVEL : DEFAULT_VOLUME; for (int c = 0; c < port.channels; c++) { port.volume[c] = DEFAULT_VOLUME; } @@ -494,9 +498,23 @@ int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) { return 0; } -int APS5_VABI sceAudioOutSetMixLevelPadSpk(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) { + std::lock_guard lock(g_mutex); + Port* port = getPort(handle); + if (port == nullptr) { + return -2144993277; + } + if (port->type != PORT_TYPE_PADSPK) { + return -2144993270; + } + if (mixLevel > DEFAULT_VOLUME) { + return -2144993260; + } + if (mixLevel < 0) { + throw std::runtime_error("sceAudioOutSetMixLevelPadSpk: negative mix level not supported"); + } + port->mixLevel = mixLevel; + return 0; } } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp new file mode 100644 index 000000000..6a889cb8b --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutMixLevelPadSpk.cpp @@ -0,0 +1,183 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOutClose(int); +int APS5_VABI sceAudioOutOutput(int, const void*); +int APS5_VABI sceAudioOutSetVolume(int, std::uint32_t, int*); +int APS5_VABI sceAudioOutSetMixLevelPadSpk(int, int); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +namespace { + +constexpr int user = 0x10000000; +constexpr int portTypeMain = 0; +constexpr int portTypePadSpeaker = 4; +constexpr std::uint32_t formatS16Mono = 0; +constexpr std::uint32_t formatFloatStereo = 4; +constexpr std::uint32_t frames = 256; +constexpr std::uint32_t frequency = 48000; +constexpr int unity = 32768; +constexpr int defaultMixLevel = 11626; +constexpr int invalidPort = static_cast(0x80260003); +constexpr int invalidPortType = static_cast(0x8026000A); +constexpr int invalidMixLevel = static_cast(0x80260014); + +template +std::vector Play(int type, std::uint32_t format, const std::vector& block, TSetup setup) { + const auto path = std::filesystem::temp_directory_path() / "anyps5_audio_out_mix_level_pad_spk.raw"; + std::filesystem::remove(path); + SetEnvironment("SDL_DISKAUDIOFILE", path.string()); + + const int handle = sceAudioOutOpen(user, type, 0, frames, frequency, format); + Require(handle > 0); + setup(handle); + Require(sceAudioOutOutput(handle, block.data()) == static_cast(frames)); + Require(sceAudioOutOutput(handle, nullptr) == static_cast(frames)); + Require(sceAudioOutClose(handle) == 0); + + std::ifstream file(path, std::ios::binary); + const std::vector bytes{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + file.close(); + std::filesystem::remove(path); + Require(bytes.size() >= block.size() * sizeof(TSample) && bytes.size() % sizeof(TSample) == 0); + + const auto* samples = reinterpret_cast(bytes.data()); + std::size_t first = 0; + std::size_t last = bytes.size() / sizeof(TSample); + while (first < last && samples[first] == TSample{}) first++; + while (last > first && samples[last - 1] == TSample{}) last--; + return {samples + first, samples + last}; +} + +std::vector Ramp() { + std::vector block(frames); + for (std::uint32_t frame = 0; frame < frames; frame++) { + block[frame] = static_cast((frame % 2 == 0 ? 1 : -1) * 256 * (static_cast(frame % 100) + 1)); + } + return block; +} + +std::vector Scaled(const std::vector& block, int level) { + std::vector scaled; + for (const std::int16_t sample : block) { + Require(sample * level % unity == 0); + scaled.push_back(static_cast(sample * level / unity)); + } + return scaled; +} + +void TestDefaultLevel() { + std::vector block(frames); + for (std::uint32_t frame = 0; frame < frames; frame++) block[frame] = frame % 2 == 0 ? 16384 : -16384; + const auto played = Play(portTypePadSpeaker, formatS16Mono, block, [](int) {}); + Require(played == Scaled(block, defaultMixLevel)); + Require(played[0] == 5813 && played[1] == -5813); +} + +void TestLevels() { + const auto block = Ramp(); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, unity) == 0); + }) == block); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0); + }) == Scaled(block, unity / 2)); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, unity) == 0); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 4) == 0); + }) == Scaled(block, unity / 4)); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, 0) == 0); + }).empty()); +} + +void TestLevelWithVolume() { + const auto block = Ramp(); + Require(Play(portTypePadSpeaker, formatS16Mono, block, [](int handle) { + int volume = unity / 2; + Require(sceAudioOutSetVolume(handle, 1, &volume) == 0); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0); + }) == Scaled(block, unity / 4)); + + std::vector stereo(frames * 2); + for (std::uint32_t frame = 0; frame < frames; frame++) { + stereo[frame * 2] = (frame % 2 == 0 ? 0.5f : -0.25f); + stereo[frame * 2 + 1] = (frame % 2 == 0 ? -1.0f : 0.125f); + } + const auto played = Play(portTypePadSpeaker, formatFloatStereo, stereo, [](int handle) { + int volume[2] = {unity, unity / 2}; + Require(sceAudioOutSetVolume(handle, 2, volume) == 0); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 4) == 0); + }); + Require(played.size() == stereo.size()); + for (std::uint32_t frame = 0; frame < frames; frame++) { + Require(played[frame * 2] == stereo[frame * 2] * 0.25f); + Require(played[frame * 2 + 1] == stereo[frame * 2 + 1] * 0.125f); + } +} + +void TestOtherPortType() { + const auto block = Ramp(); + Require(Play(portTypeMain, formatS16Mono, block, [](int) {}) == block); + Require(Play(portTypeMain, formatS16Mono, block, [](int handle) { + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == invalidPortType); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidPortType); + }) == block); +} + +void TestRejectedLevels() { + const auto block = Ramp(); + int closed = 0; + Require(Play(portTypePadSpeaker, formatS16Mono, block, [&closed](int handle) { + closed = handle; + Require(sceAudioOutSetMixLevelPadSpk(handle, unity / 2) == 0); + Require(sceAudioOutSetMixLevelPadSpk(handle, unity + 1) == invalidMixLevel); + Require(ThrowsRuntimeError([handle] { sceAudioOutSetMixLevelPadSpk(handle, -1); })); + Require(sceAudioOutSetMixLevelPadSpk(0, unity) == invalidPort); + Require(sceAudioOutSetMixLevelPadSpk(handle + 1, unity) == invalidPort); + }) == Scaled(block, unity / 2)); + Require(sceAudioOutSetMixLevelPadSpk(closed, unity) == invalidPort); +} + +} + +int main() { + SetEnvironment("SDL_AUDIODRIVER", "disk"); + TestDefaultLevel(); + TestLevels(); + TestLevelWithVolume(); + TestOtherPortType(); + TestRejectedLevels(); + return 0; +} diff --git a/core/libs/prx/libSceErrorDialog/CMakeLists.txt b/core/libs/prx/libSceErrorDialog/CMakeLists.txt new file mode 100644 index 000000000..2dd29a15a --- /dev/null +++ b/core/libs/prx/libSceErrorDialog/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(libSceErrorDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceErrorDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceErrorDialog) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_error_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestErrorDialog.cpp) + target_include_directories(guest_error_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_error_dialog_tests PRIVATE libSceErrorDialog) + add_test(NAME guest_error_dialog COMMAND guest_error_dialog_tests) + if(WIN32) + set_tests_properties(guest_error_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceErrorDialog/Export.cpp b/core/libs/prx/libSceErrorDialog/Export.cpp index 7588126f3..17d76ba8e 100644 --- a/core/libs/prx/libSceErrorDialog/Export.cpp +++ b/core/libs/prx/libSceErrorDialog/Export.cpp @@ -1,39 +1,95 @@ #include #include +#include +#include + #include "SceTypes.hpp" +#include "HitLog.hpp" #include "prx/libc/include/General.hpp" +#ifdef _WIN32 +#include +#endif + +namespace { +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusRunning = 2; +constexpr int kStatusFinished = 3; + +constexpr int kErrOk = 0; +constexpr int kErrNotInitialized = static_cast(0x80ED0001u); +constexpr int kErrAlreadyInitialized = static_cast(0x80ED0002u); +constexpr int kErrParam = static_cast(0x80ED0003u); +constexpr int kErrInvalidState = static_cast(0x80ED0005u); + +constexpr std::size_t kParamSize = 16; + +std::mutex g_lock; +int g_status = kStatusNone; + +bool Readable(const void* p, std::size_t n) { +#ifdef _WIN32 + if (p == nullptr) return false; + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(p, &mbi, sizeof(mbi)) == 0 || mbi.State != MEM_COMMIT) return false; + if ((mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0) return false; + const auto end = reinterpret_cast(mbi.BaseAddress) + mbi.RegionSize; + return reinterpret_cast(p) + n <= end; +#else + return p != nullptr; +#endif +} +} // namespace + extern "C" { -int APS5_VABI sceErrorDialogClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceErrorDialogGetStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceErrorDialogInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard g(g_lock); + if (g_status != kStatusNone) return kErrAlreadyInitialized; + g_status = kStatusInitialized; + return kErrOk; } int APS5_VABI sceErrorDialogOpen(const void* param) { - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceErrorDialogTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard g(g_lock); + if (g_status != kStatusInitialized && g_status != kStatusFinished) return kErrInvalidState; + if (!Readable(param, kParamSize)) return kErrParam; + std::int32_t size = 0; + std::memcpy(&size, param, sizeof(size)); + if (static_cast(size) != kParamSize) return kErrParam; + std::int32_t errorCode = 0; + std::int32_t userId = 0; + std::memcpy(&errorCode, static_cast(param) + 4, sizeof(errorCode)); + std::memcpy(&userId, static_cast(param) + 8, sizeof(userId)); + APS5_HIT("ERRORDIALOG", "open: error_code=0x%08X user_id=%d", errorCode, userId); + g_status = kStatusRunning; + return kErrOk; } int APS5_VABI sceErrorDialogUpdateStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard g(g_lock); + if (g_status == kStatusRunning) g_status = kStatusFinished; + return g_status; +} + +int APS5_VABI sceErrorDialogGetStatus(void) { + std::lock_guard g(g_lock); + return g_status; +} + +int APS5_VABI sceErrorDialogClose(void) { + std::lock_guard g(g_lock); + if (g_status != kStatusRunning) return kErrInvalidState; + g_status = kStatusFinished; + return kErrOk; +} + +int APS5_VABI sceErrorDialogTerminate(void) { + std::lock_guard g(g_lock); + if (g_status == kStatusNone) return kErrNotInitialized; + g_status = kStatusNone; + return kErrOk; } } diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 80cd10216..f2437a5a5 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -171,10 +171,9 @@ int APS5_VABI scePadResetOrientation(int handle) { } int APS5_VABI scePadSetAngularVelocityDeadbandState(int handle, bool enable) { - (void)handle; - (void)enable; - NotImplemented_nid_no_patch(__func__); - return 0; + if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + if (enable) NotImplemented_nid_no_patch(__func__); + return PAD_OK; } int APS5_VABI scePadSetLightBar(int handle, const PadLightBarParam* param) { diff --git a/core/libs/prx/libSceRtc/CMakeLists.txt b/core/libs/prx/libSceRtc/CMakeLists.txt index a0e4a2444..faf3100ac 100644 --- a/core/libs/prx/libSceRtc/CMakeLists.txt +++ b/core/libs/prx/libSceRtc/CMakeLists.txt @@ -11,7 +11,8 @@ endif() if(BUILD_TESTING) add_test_executable(guest_rtc_tests ${LIBS_INCLUDE_DIR}/tests/GuestRtc.cpp) target_include_directories(guest_rtc_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_rtc_tests PRIVATE libSceRtc) + target_link_libraries(guest_rtc_tests PRIVATE libSceRtc libc) + configure_windows_unwind(guest_rtc_tests) add_test(NAME guest_rtc COMMAND guest_rtc_tests) if(WIN32) set_tests_properties(guest_rtc PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 6b87f3616..272b78cb3 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -139,6 +139,23 @@ bool parseDigits(const char*& cursor, int count, int& value) { return true; } +bool parseZoneSuffix(const char*& cursor, std::int64_t& offsetMinutes) { + offsetMinutes = 0; + if (*cursor == 'Z' || *cursor == 'z') { + ++cursor; + return true; + } + if (*cursor == '+' || *cursor == '-') { + const int sign = *cursor++ == '-' ? -1 : 1; + int offsetHours = 0, offsetMinute = 0; + if (!parseDigits(cursor, 2, offsetHours) || *cursor++ != ':' || !parseDigits(cursor, 2, offsetMinute)) return false; + if (offsetHours > 23 || offsetMinute > 59) return false; + offsetMinutes = sign * (offsetHours * 60 + offsetMinute); + return true; + } + return false; +} + } extern "C" { @@ -282,18 +299,37 @@ int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { for (; *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast(*cursor - '0') * scale; } std::int64_t offsetMinutes = 0; - if (*cursor == 'Z' || *cursor == 'z') { + if (!parseZoneSuffix(cursor, offsetMinutes)) return SCE_RTC_ERROR_BAD_PARSE; + if (*cursor != 0) return SCE_RTC_ERROR_BAD_PARSE; + const RtcDateTime time{static_cast(year), static_cast(month), static_cast(day), + static_cast(hour), static_cast(minute), static_cast(second), microsecond}; + if (const int result = validate(&time); result != 0) return result; + const RtcTick local{toTick(time)}; + return addTicks(utc, &local, -offsetMinutes, TICKS_PER_MINUTE); +} + +int APS5_VABI sceRtcParseDateTime(RtcTick* utc, const char* date_time) { + if (!utc || !date_time) return SCE_RTC_ERROR_INVALID_POINTER; + const char* cursor = date_time; + int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; + bool ok = parseDigits(cursor, 4, year) && *cursor++ == '-' && parseDigits(cursor, 2, month) && *cursor++ == '-' && parseDigits(cursor, 2, day); + ok = ok && (*cursor == 'T' || *cursor == 't' || *cursor == ' '); + if (ok) ++cursor; + ok = ok && parseDigits(cursor, 2, hour) && *cursor++ == ':' && parseDigits(cursor, 2, minute) && *cursor++ == ':' && parseDigits(cursor, 2, second); + std::uint32_t microsecond = 0; + if (ok && *cursor == '.') { ++cursor; - } else if (*cursor == '+' || *cursor == '-') { - const int sign = *cursor++ == '-' ? -1 : 1; - int offsetHours = 0, offsetMinute = 0; - if (!parseDigits(cursor, 2, offsetHours) || *cursor++ != ':' || !parseDigits(cursor, 2, offsetMinute)) return SCE_RTC_ERROR_BAD_PARSE; - if (offsetHours > 23 || offsetMinute > 59) return SCE_RTC_ERROR_BAD_PARSE; - offsetMinutes = sign * (offsetHours * 60 + offsetMinute); - } else { + std::uint32_t scale = 100000; + ok = *cursor >= '0' && *cursor <= '9'; + for (; ok && *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast(*cursor - '0') * scale; + } + std::int64_t offsetMinutes = 0; + if (ok && *cursor != 0) ok = parseZoneSuffix(cursor, offsetMinutes); + if (ok && *cursor != 0) ok = false; + if (!ok) { + NotImplemented_nid_no_patch(__func__); return SCE_RTC_ERROR_BAD_PARSE; } - if (*cursor != 0) return SCE_RTC_ERROR_BAD_PARSE; const RtcDateTime time{static_cast(year), static_cast(month), static_cast(day), static_cast(hour), static_cast(minute), static_cast(second), microsecond}; if (const int result = validate(&time); result != 0) return result; diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index ff18d4bad..de42305b7 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -15,12 +15,15 @@ if(BUILD_TESTING) configure_windows_unwind(guest_system_exit_tests) add_test(NAME guest_system_exit_parameters COMMAND guest_system_exit_tests) add_test(NAME guest_system_exit COMMAND guest_system_exit_tests --exit) + add_test(NAME guest_quick_exit COMMAND guest_system_exit_tests --quick-exit) add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate) add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main) set_tests_properties(guest_system_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed") + set_tests_properties(guest_quick_exit PROPERTIES + PASS_REGULAR_EXPRESSION "Guest quick_exit handlers completed") if(WIN32) - set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index 6df7bf8db..17128de2e 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -160,12 +160,6 @@ std::uint64_t APS5_VABI libcCyberUnknown14(void) { return 0; } -APS5_EXPORT("qdGFBoLVNKI", libcCyberUnknown15); -std::uint64_t APS5_VABI libcCyberUnknown15(void) { - NotImplemented_nid_no_patch("qdGFBoLVNKI"); - return 0; -} - APS5_EXPORT("vEaqE-7IZYc", libcCyberUnknown16); std::uint64_t APS5_VABI libcCyberUnknown16(void) { NotImplemented_nid_no_patch("vEaqE-7IZYc"); diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 48526ffff..d8ce47284 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -162,4 +162,27 @@ int APS5_VABI atexit_nid_postfix(GuestExitCallback func) { return result; } +namespace { +std::mutex quickExitMutex; +std::vector quickExitCallbacks; +} + +int APS5_VABI at_quick_exit_nid_postfix(GuestExitCallback func) { + if (func == nullptr) + return 0; + std::lock_guard lock(quickExitMutex); + quickExitCallbacks.push_back(func); + return 0; +} + +[[noreturn]] void APS5_VABI quick_exit_nid_postfix(int status) { + std::vector callbacks; + { + std::lock_guard lock(quickExitMutex); + callbacks = std::move(quickExitCallbacks); + } + for (auto it = callbacks.rbegin(); it != callbacks.rend(); ++it) (*it)(); + _Exit_nid_postfix(status); +} + } diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index a4f388e1e..2e06061f3 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -1,5 +1,6 @@ #ifdef _WIN32 #include +#include #include "prx/libc/include/GuestArena.hpp" #include #include @@ -8,6 +9,7 @@ #include #include #include +#include #include #include #include "Sse4aEmulation.hpp" @@ -28,6 +30,8 @@ void Report(const char* format, ...) { WriteFile(GetStdHandle(STD_ERROR_HANDLE), buffer, static_cast(length), &written, nullptr); } +void ReportAllThreads(); + void DescribeAddress(std::uint64_t address, char* buffer, std::size_t size) { HMODULE module = nullptr; char path[MAX_PATH] = "?"; @@ -330,12 +334,99 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { } } std::fflush(stderr); + ReportAllThreads(); + std::fflush(stderr); return EXCEPTION_CONTINUE_SEARCH; } +std::size_t CaptureFrames(const CONTEXT* context, std::uint64_t* frames, std::size_t capacity) { + std::size_t count = 0; + std::uint64_t frame = context->Rbp; + while (count < capacity && frame != 0 && (frame & 7) == 0 && IsReadable(frame) && IsReadable(frame + 8)) { + const auto returnAddress = reinterpret_cast(frame)[1]; + if (!IsExecutable(returnAddress)) break; + frames[count++] = returnAddress; + const auto next = reinterpret_cast(frame)[0]; + if (next <= frame) break; + frame = next; + } + return count; +} + +void ReportThreadContext(const CONTEXT* context, DWORD threadId, const char* name, const std::uint64_t* frames, std::size_t frameCount) { + char line[MAX_PATH + 64]; + Report(" thread %lu '%s': rip ", static_cast(threadId), name ? name : ""); + DescribeAddress(context->Rip, line, sizeof(line)); + Report("%s rsp 0x%016llx rbp 0x%016llx\n", line, static_cast(context->Rsp), static_cast(context->Rbp)); + for (std::size_t depth = 0; depth < frameCount; ++depth) { + DescribeAddress(frames[depth], line, sizeof(line)); + Report(" #%llu %s\n", static_cast(depth), line); + } +} + +void ReportAllThreads() { + const DWORD current = GetCurrentThreadId(); + const DWORD pid = GetCurrentProcessId(); + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (snapshot == INVALID_HANDLE_VALUE) return; + THREADENTRY32 entry{}; + entry.dwSize = sizeof(entry); + struct CapturedThread { + DWORD id; + char name[128]; + CONTEXT context; + std::uint64_t frames[12]; + std::size_t frameCount; + }; + static CapturedThread captured[128]; + std::size_t capturedCount = 0; + std::size_t skippedCount = 0; + if (Thread32First(snapshot, &entry)) { + do { + if (entry.th32OwnerProcessID != pid || entry.th32ThreadID == current) continue; + if (capturedCount >= sizeof(captured) / sizeof(captured[0])) { + ++skippedCount; + continue; + } + HANDLE thread = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, entry.th32ThreadID); + if (!thread) continue; + if (SuspendThread(thread) == static_cast(-1)) { + CloseHandle(thread); + continue; + } + CONTEXT context{}; + context.ContextFlags = CONTEXT_FULL; + const BOOL haveContext = GetThreadContext(thread, &context); + std::uint64_t frames[12] = {}; + const std::size_t frameCount = haveContext ? CaptureFrames(&context, frames, sizeof(frames) / sizeof(frames[0])) : 0; + ResumeThread(thread); + char name[128] = ""; + PWSTR description = nullptr; + if (SUCCEEDED(GetThreadDescription(thread, &description)) && description) { + WideCharToMultiByte(CP_UTF8, 0, description, -1, name, sizeof(name), nullptr, nullptr); + LocalFree(description); + } + CloseHandle(thread); + if (!haveContext) continue; + captured[capturedCount].id = entry.th32ThreadID; + std::memcpy(captured[capturedCount].name, name, sizeof(captured[capturedCount].name)); + captured[capturedCount].context = context; + std::memcpy(captured[capturedCount].frames, frames, sizeof(captured[capturedCount].frames)); + captured[capturedCount].frameCount = frameCount; + ++capturedCount; + } while (Thread32Next(snapshot, &entry)); + } + CloseHandle(snapshot); + Report(" all threads:\n"); + for (std::size_t i = 0; i < capturedCount; ++i) { + ReportThreadContext(&captured[i].context, captured[i].id, captured[i].name, captured[i].frames, captured[i].frameCount); + } + if (skippedCount != 0) Report(" %llu further thread(s) left out\n", static_cast(skippedCount)); +} + // abort() and the UCRT's invalid-parameter path end the process with a silent fast fail // (0xc0000409); both are reported with the calling thread's stack first. -void ReportBacktrace(const char* what) { +void ReportBacktrace(const char* what, bool reportThreads) { void* frames[48]; const auto count = RtlCaptureStackBackTrace(0, 48, frames, nullptr); Report("FATAL: %s on thread %lu\n", what, static_cast(GetCurrentThreadId())); @@ -344,22 +435,23 @@ void ReportBacktrace(const char* what) { DescribeAddress(reinterpret_cast(frames[i]), line, sizeof(line)); Report(" #%u %s\n", static_cast(i), line); } + if (reportThreads) ReportAllThreads(); std::fflush(stderr); } void AbortSignalHandler(int) { - ReportBacktrace("abort() (SIGABRT)"); + ReportBacktrace("abort() (SIGABRT)", true); } void InvalidParameterHandler(const wchar_t*, const wchar_t*, const wchar_t*, unsigned, std::uintptr_t) { - ReportBacktrace("UCRT invalid parameter"); + ReportBacktrace("UCRT invalid parameter", true); } // Static destruction runs on the thread that ends the process (ExitProcess -> DLL_PROCESS_DETACH), // so its stack names the exit's caller when no libc exit/abort path was taken. struct ExitReporter { ~ExitReporter() { - if (std::getenv("APS5_TRACE_EXIT") != nullptr) ReportBacktrace("process exit (static destruction)"); + if (std::getenv("APS5_TRACE_EXIT") != nullptr) ReportBacktrace("process exit (static destruction)", false); } } g_exitReporter; diff --git a/core/libs/tests/GuestErrorDialog.cpp b/core/libs/tests/GuestErrorDialog.cpp new file mode 100644 index 000000000..cb91dc4fb --- /dev/null +++ b/core/libs/tests/GuestErrorDialog.cpp @@ -0,0 +1,63 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceErrorDialogInitialize(void); +int APS5_VABI sceErrorDialogOpen(const void* param); +int APS5_VABI sceErrorDialogUpdateStatus(void); +int APS5_VABI sceErrorDialogGetStatus(void); +int APS5_VABI sceErrorDialogClose(void); +int APS5_VABI sceErrorDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusRunning = 2; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80ED0001); +constexpr int kErrAlreadyInitialized = static_cast(0x80ED0002); +constexpr int kErrParam = static_cast(0x80ED0003); +constexpr int kErrInvalidState = static_cast(0x80ED0005); + +struct ErrorDialogParam { + std::int32_t size; + std::int32_t error_code; + std::int32_t user_id; + std::int32_t reserved; +}; + +} + +int main() { + ErrorDialogParam param{16, static_cast(0x80020001), 0, 0}; + + Require(sceErrorDialogTerminate() == kErrNotInitialized); + Require(sceErrorDialogOpen(¶m) == kErrInvalidState); + + Require(sceErrorDialogInitialize() == 0); + Require(sceErrorDialogInitialize() == kErrAlreadyInitialized); + + Require(sceErrorDialogClose() == kErrInvalidState); + + Require(sceErrorDialogOpen(nullptr) == kErrParam); + ErrorDialogParam bad{8, 0, 0, 0}; + Require(sceErrorDialogOpen(&bad) == kErrParam); + + Require(sceErrorDialogOpen(¶m) == 0); + Require(sceErrorDialogGetStatus() == kStatusRunning); + Require(sceErrorDialogOpen(¶m) == kErrInvalidState); + + Require(sceErrorDialogUpdateStatus() == kStatusFinished); + Require(sceErrorDialogGetStatus() == kStatusFinished); + + Require(sceErrorDialogOpen(¶m) == 0); + Require(sceErrorDialogGetStatus() == kStatusRunning); + Require(sceErrorDialogClose() == 0); + Require(sceErrorDialogGetStatus() == kStatusFinished); + + Require(sceErrorDialogTerminate() == 0); + Require(sceErrorDialogTerminate() == kErrNotInitialized); +} diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 70a193bb2..8abd4f04f 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -13,6 +13,7 @@ int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool); int APS5_VABI scePadInit_nid_postfix(void); int APS5_VABI scePadSetTiltCorrectionState(int, bool); int APS5_VABI scePadResetOrientation(int); +int APS5_VABI scePadSetAngularVelocityDeadbandState(int, bool); } static void Require(bool value) { if (!value) std::abort(); } @@ -60,4 +61,6 @@ int main() { Require(scePadClose_nid_postfix(handle) == 0); Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(true) == 0); + Require(scePadSetAngularVelocityDeadbandState(handle, false) == 0); + Require(scePadSetAngularVelocityDeadbandState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); } diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 8016c239b..23d203f06 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -3,6 +3,7 @@ #include #include #include +#include extern "C" { int APS5_VABI sceRtcCheckValid(const RtcDateTime*); @@ -21,6 +22,7 @@ int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime*, std::uint64_t*); int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime*, std::uint64_t); int APS5_VABI sceRtcFormatRFC3339(char*, const RtcTick*, int); int APS5_VABI sceRtcParseRFC3339(RtcTick*, const char*); +int APS5_VABI sceRtcParseDateTime(RtcTick*, const char*); int APS5_VABI sceRtcTickAddTicks(RtcTick*, const RtcTick*, std::int64_t); int APS5_VABI sceRtcTickAddSeconds(RtcTick*, const RtcTick*, std::int64_t); int APS5_VABI sceRtcTickAddDays(RtcTick*, const RtcTick*, std::int32_t); @@ -123,6 +125,30 @@ int main() { Require(sceRtcParseRFC3339(&tick, "2024-02-29T12:34:56.789-23:59") == 0 && tick.tick == leapDayTick + 86340000000ull); Require(sceRtcParseRFC3339(nullptr, "1970-01-01T00:00:00Z") == invalidPointer); + Require(sceRtcParseDateTime(&tick, "2024-02-29T12:34:56.789") == 0 && tick.tick == leapDayTick); + Require(sceRtcParseDateTime(&tick, "2024-02-29 12:34:56") == 0 && tick.tick == leapDayTick - 789000ull); + Require(sceRtcParseDateTime(&tick, "2024-02-29T14:04:56.789+01:30") == 0 && tick.tick == leapDayTick); + Require(sceRtcParseDateTime(&tick, "2024-02-29t12:34:56.789z") == 0 && tick.tick == leapDayTick); + Require(sceRtcParseDateTime(&tick, "1970-01-01T00:00:00Z") == 0 && tick.tick == unixEpochTick); + Require(sceRtcParseDateTime(&tick, "2023-02-29T00:00:00") == invalidDay); + Require(sceRtcParseDateTime(nullptr, "1970-01-01T00:00:00Z") == invalidPointer); + const char* unparseable[] = { + "2024-02-29", + "2024/02/29T12:34:56", + "2024-02-29T12:34:56Zjunk", + "2024-02-29T12:34:56+99:99", + "2024-02-29T12:34:56.", + }; + for (const char* text : unparseable) { + bool thrown = false; + try { + sceRtcParseDateTime(&tick, text); + } catch (const std::exception&) { + thrown = true; + } + Require(thrown); + } + RtcTick source{leapDayTick}; RtcTick result{}; Require(sceRtcTickAddSeconds(&result, &source, 3600) == 0 && result.tick == leapDayTick + 3600000000ull); diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 5afefa49e..3a22d960f 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -8,6 +8,9 @@ extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); extern "C" void APS5_VABI _Exit_nid_postfix(int); extern "C" void APS5_VABI catchReturnFromMain_nid_postfix(int); +using GuestQuickExitCallback = void (APS5_VABI*)(); +extern "C" int APS5_VABI at_quick_exit_nid_postfix(GuestQuickExitCallback); +extern "C" void APS5_VABI quick_exit_nid_postfix(int); namespace { bool cleaned = false; void Cleanup() { cleaned = true; } @@ -15,6 +18,13 @@ void VerifyExit() { if (!cleaned) std::_Exit(1); std::puts("Guest shutdown and atexit completed"); } +int quickOrder = 0; +void APS5_VABI QuickSecond() { quickOrder = 1; } +void APS5_VABI QuickFirst() { + if (quickOrder != 1) std::_Exit(1); + std::puts("Guest quick_exit handlers completed"); + std::fflush(stdout); +} void Require(bool value) { if (!value) std::abort(); } void UnexpectedCleanup() { std::_Exit(3); } } @@ -29,6 +39,12 @@ int main(int argc, char** argv) { catchReturnFromMain_nid_postfix(0); return 2; } + if (argc > 1 && std::strcmp(argv[1], "--quick-exit") == 0) { + Require(at_quick_exit_nid_postfix(QuickFirst) == 0); + Require(at_quick_exit_nid_postfix(QuickSecond) == 0); + quick_exit_nid_postfix(0); + return 2; + } if (argc > 1) { LibcRegisterShutdown_nid_postfix(Cleanup); Require(std::atexit(VerifyExit) == 0); diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 0b009c39f..a2ba2e2f2 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -173,6 +175,8 @@ std::set CodeInstructionCollector::Collect(const std::ve if (roots.empty()) throw Domain::RelinkerException("Code analysis: no code entry points"); std::set instructions; const Codegen::X64InstructionDecoder decoder; + std::vector> skipped; + std::set staticTargets; for (const auto& [begin, end] : functions) { const auto offset = fileOffset(begin, end - begin); for (auto address = begin; address < end;) { @@ -180,16 +184,36 @@ std::set CodeInstructionCollector::Collect(const std::ve try { info = decoder.DecodeInstruction(bytes.data() + offset + address - begin, end - address); } catch (const Codegen::CodegenException& error) { - throw Domain::RelinkerException(std::string("Code analysis: ") + error.what(), address); + const auto tailBytes = end - address; + bool tailHasFsPrefix = false; + for (auto probe = address; probe < end; ++probe) { + if (bytes[offset + probe - begin] == 0x64) { + tailHasFsPrefix = true; + break; + } + } + if (tailBytes > 15 || tailHasFsPrefix) + throw Domain::RelinkerException(std::string("Code analysis: ") + error.what(), address); + skipped.emplace_back(address, end); + break; } if (info.Length == 0 || info.Length > end - address) throw Domain::RelinkerException("Code analysis: instruction crosses function boundary", address); instructions.insert(address); - if (info.HasBranchTarget && !info.HasRipRelativeDisp) - addRoot(address + info.Length + static_cast(info.BranchDisp)); + if (info.HasBranchTarget && !info.HasRipRelativeDisp) { + const auto target = address + info.Length + static_cast(info.BranchDisp); + staticTargets.insert(target); + addRoot(target); + } address += info.Length; } } + for (const auto target : staticTargets) { + for (const auto& [skipBegin, skipEnd] : skipped) { + if (skipBegin <= target && target < skipEnd) + throw Domain::RelinkerException("Code analysis: branch into skipped range tail", target); + } + } std::size_t previousRoots = 0; do { previousRoots = roots.size(); @@ -211,7 +235,47 @@ std::set CodeInstructionCollector::Collect(const std::ve } while (roots.size() != previousRoots); std::uint64_t previousEnd = 0; for (const auto address : instructions) { - if (address < previousEnd) throw Domain::RelinkerException("Code analysis: overlapping instruction boundaries", address); + if (address < previousEnd) { + std::uint64_t spanEnd = previousEnd; + try { + for (const auto& header : headers) { + if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; + const auto offset = address - header.MappedAddress; + const auto length = decoder.DecodeInstruction(bytes.data() + header.Offset + offset, header.FileSize - offset).Length; + if (length == 0 || length > header.FileSize - offset) break; + spanEnd = std::max(spanEnd, address + length); + break; + } + } catch (const Codegen::CodegenException&) { + } + bool spanHasFsPrefix = false; + for (const auto& header : headers) { + if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; + const auto base = header.Offset + address - header.MappedAddress; + for (auto probe = base; probe < base + (spanEnd - address) && probe < bytes.size(); ++probe) { + if (bytes[probe] == 0x64) { + spanHasFsPrefix = true; + break; + } + } + break; + } + if (!spanHasFsPrefix) { + const auto previous = instructions.lower_bound(address); + if (previous != instructions.begin()) { + const auto previousStart = *std::prev(previous); + for (const auto& header : headers) { + if (header.Type != 1 || (header.Flags & 1) == 0 || previousStart < header.MappedAddress || previousStart - header.MappedAddress >= header.FileSize) continue; + const auto base = header.Offset + previousStart - header.MappedAddress; + if (decoder.DecodeInstruction(bytes.data() + base, bytes.size() - base).SegmentPrefix == 0x64) spanHasFsPrefix = true; + break; + } + } + } + if (spanHasFsPrefix) throw Domain::RelinkerException("Code analysis: overlapping instruction boundaries", address); + previousEnd = spanEnd; + continue; + } for (const auto& header : headers) { if (header.Type != 1 || (header.Flags & 1) == 0 || address < header.MappedAddress || address - header.MappedAddress >= header.FileSize) continue; const auto offset = address - header.MappedAddress; diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 31606aa11..825e7f543 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -235,6 +235,14 @@ def main(): overlapping = make_image("register", "unwind") overlapping[0x1200:0x1205] = b"\xe9" + struct.pack("> 18) & 63], CHARSET[(triple >> 12) & 63], + CHARSET[(triple >> 6) & 63], CHARSET[triple & 63]] + tail = (rev[6] << 16) | (rev[7] << 8) + out += [CHARSET[(tail >> 18) & 63], CHARSET[(tail >> 12) & 63], + CHARSET[(tail >> 6) & 63]] + return "".join(out) + + +def load_db(path): + if not path.exists(): + path.parent.mkdir(parents=True, exist_ok=True) + print("downloading NID database to %s ..." % path, file=sys.stderr) + urllib.request.urlretrieve(DB_URL, str(path)) + db = {} + with open(path, errors="replace") as handle: + for line in handle: + parts = line.strip().split() + if len(parts) >= 2: + db.setdefault(parts[0], parts[1]) + return db + + +def collect_unknowns(): + found = {} + for path in sorted(PRX.rglob("*.cpp")): + try: + text = path.read_text(errors="replace") + except OSError: + continue + for match in ALIAS.finditer(text): + found.setdefault(match.group(1), "%s:%s" % ( + path.relative_to(ROOT), match.group(2))) + return found + + +def collect_real_names(): + names = set() + for path in sorted((ROOT / "core" / "libs").rglob("*.cpp")): + try: + text = path.read_text(errors="replace") + except OSError: + continue + for match in DEFINITION.finditer(text): + name = match.group(1) + names.add(name) + if name.endswith(NID_POSTFIX): + names.add(name[:-len(NID_POSTFIX)]) + return names + + +def main(): + parser = argparse.ArgumentParser( + description="Suggest real names for Unknown NID stubs") + parser.add_argument("--db", default=os.environ.get("ANYPS5_NID_DB"), + help="NID database CSV (downloaded to a cache dir by default)") + parser.add_argument("--nid", nargs="*", default=[], + help="resolve only these NIDs instead of scanning the tree") + parser.add_argument("--json", action="store_true", + help="emit machine-readable JSON") + args = parser.parse_args() + + db_path = Path(args.db) if args.db else DEFAULT_CACHE + db = load_db(db_path) + + if args.nid: + unknowns = {nid: "" for nid in args.nid} + else: + unknowns = collect_unknowns() + real_names = collect_real_names() + + rows = [] + failures = 0 + for nid in sorted(unknowns): + suggestion = db.get(nid, "") + status = "unknown" + if suggestion: + if compute_nid(suggestion) != nid: + status = "MISMATCH" + failures += 1 + elif suggestion in real_names: + status = "already-implemented" + else: + status = "rename-ready" + rows.append({"nid": nid, "stub": unknowns[nid], + "suggestion": suggestion, "status": status}) + + if args.json: + print(json.dumps({"db_entries": len(db), "rows": rows}, indent=1)) + else: + print("%-12s %-40s %-12s %s" % ("NID", "STUB", "STATUS", "SUGGESTION")) + for row in rows: + print("%-12s %-40s %-12s %s" % ( + row["nid"], row["stub"][:40], row["status"], row["suggestion"])) + print("%d unknowns, %d suggestions (%d already implemented)" % ( + len(rows), sum(1 for r in rows if r["suggestion"]), + sum(1 for r in rows if r["status"] == "already-implemented"))) + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main())