mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-21 05:52:09 -04:00
merge main
This commit is contained in:
@@ -56,6 +56,8 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$" AND
|
||||
message(FATAL_ERROR "MKW_X86_CPU_PROFILE must be v2 or v3 (got '${MKW_X86_CPU_PROFILE}')")
|
||||
endif()
|
||||
|
||||
set(MKW_SETUP_VERSION "" CACHE STRING "WiiCompiled version recorded in the runtime log")
|
||||
|
||||
option(MKW_PLATFORM_TESTS_ONLY "Build host-platform tests without graphics dependencies or game assets" OFF)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/HostLibraries.cmake")
|
||||
if(MKW_PLATFORM_TESTS_ONLY)
|
||||
|
||||
@@ -80,6 +80,9 @@ target_compile_features(mkw_runtime_common PRIVATE cxx_std_20)
|
||||
target_compile_definitions(mkw_runtime_common PRIVATE
|
||||
SDL_MAIN_HANDLED
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||
if(MKW_SETUP_VERSION)
|
||||
target_compile_definitions(mkw_runtime_common PRIVATE MKW_SETUP_VERSION="${MKW_SETUP_VERSION}")
|
||||
endif()
|
||||
target_link_libraries(mkw_runtime_common PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
|
||||
@@ -138,28 +138,6 @@ inline std::string Trim(std::string_view text) {
|
||||
return std::string(text.substr(begin, end - begin));
|
||||
}
|
||||
|
||||
inline std::string RemoveComment(std::string_view line) {
|
||||
bool inSingle = false;
|
||||
bool inDouble = false;
|
||||
bool escaped = false;
|
||||
for (size_t i = 0; i < line.size(); ++i) {
|
||||
const char ch = line[i];
|
||||
if (inDouble && ch == '\\' && !escaped) {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (ch == '\'' && !inDouble) {
|
||||
inSingle = !inSingle;
|
||||
} else if (ch == '"' && !inSingle && !escaped) {
|
||||
inDouble = !inDouble;
|
||||
} else if (ch == '#' && !inSingle && !inDouble) {
|
||||
return std::string(line.substr(0, i));
|
||||
}
|
||||
escaped = false;
|
||||
}
|
||||
return std::string(line);
|
||||
}
|
||||
|
||||
inline bool IsSupportedResolutionMultiplier(float value) {
|
||||
static constexpr std::array values{0.0f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f, 8.0f};
|
||||
return std::find(values.begin(), values.end(), value) != values.end();
|
||||
@@ -546,68 +524,50 @@ inline const std::optional<std::string>& ControllerButton(size_t index) {
|
||||
return index < Get().controllerButtons.size() ? Get().controllerButtons[index] : empty;
|
||||
}
|
||||
|
||||
// Update one TOML value without discarding comments, unrelated settings, or
|
||||
// user-specific paths. This is used by the in-game F10 settings bar.
|
||||
// Sets section.key (section may be a dotted table path) to a TOML literal, keeping the rest of the
|
||||
// document's keys, order and comments. Returns nullopt when the document or value does not parse.
|
||||
inline std::optional<std::string> UpdateSettingText(const std::string& text, std::string_view section,
|
||||
std::string_view key, std::string_view value) {
|
||||
try {
|
||||
auto root = toml::parse_str<toml::ordered_type_config>(text);
|
||||
auto literal = toml::parse_str<toml::ordered_type_config>("v = " + std::string(value));
|
||||
auto* table = &root;
|
||||
std::istringstream path{std::string(section)};
|
||||
for (std::string part; std::getline(path, part, '.');) {
|
||||
auto& parent = table->as_table();
|
||||
if (!parent.contains(part)) {
|
||||
parent[part] = toml::ordered_value(toml::ordered_table{});
|
||||
}
|
||||
table = &parent.at(part);
|
||||
if (!table->is_table()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
if (table->as_table_fmt().fmt == toml::table_format::implicit) {
|
||||
table->as_table_fmt().fmt = toml::table_format::multiline;
|
||||
}
|
||||
auto& slot = table->as_table()[std::string(key)];
|
||||
auto& updated = literal.as_table().at("v");
|
||||
updated.comments() = slot.comments();
|
||||
slot = std::move(updated);
|
||||
return toml::format(root);
|
||||
} catch (const std::exception&) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
// Update one TOML value without discarding unrelated settings or user-specific paths. This is
|
||||
// used by the in-game F10 settings bar. An unparseable file is left untouched.
|
||||
inline bool WriteSetting(std::string_view section, std::string_view key, std::string_view value) {
|
||||
const auto path = ResolveConfigPath();
|
||||
std::ifstream input(path);
|
||||
std::vector<std::string> lines;
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
lines.push_back(std::move(line));
|
||||
std::ostringstream existing;
|
||||
if (std::ifstream input(path, std::ios::binary); input) {
|
||||
existing << input.rdbuf();
|
||||
}
|
||||
|
||||
const std::string normalizedSection = Trim(section);
|
||||
const std::string normalizedKey = Trim(key);
|
||||
size_t sectionStart = lines.size();
|
||||
size_t sectionEnd = lines.size();
|
||||
for (size_t i = 0; i < lines.size(); ++i) {
|
||||
const std::string trimmed = Trim(RemoveComment(lines[i]));
|
||||
if (trimmed.size() >= 2 && trimmed.front() == '[' && trimmed.back() == ']') {
|
||||
const std::string found = Trim(std::string_view(trimmed).substr(1, trimmed.size() - 2));
|
||||
if (sectionStart != lines.size()) {
|
||||
sectionEnd = i;
|
||||
break;
|
||||
}
|
||||
if (found == normalizedSection) {
|
||||
sectionStart = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string replacement = normalizedKey + " = " + std::string(value);
|
||||
if (sectionStart == lines.size()) {
|
||||
if (!lines.empty() && !lines.back().empty()) {
|
||||
lines.emplace_back();
|
||||
}
|
||||
lines.emplace_back("[" + normalizedSection + "]");
|
||||
lines.push_back(replacement);
|
||||
} else {
|
||||
bool replaced = false;
|
||||
for (size_t i = sectionStart + 1; i < sectionEnd; ++i) {
|
||||
const std::string uncommented = Trim(RemoveComment(lines[i]));
|
||||
const size_t equals = uncommented.find('=');
|
||||
if (equals != std::string::npos && Trim(std::string_view(uncommented).substr(0, equals)) == normalizedKey) {
|
||||
lines[i] = replacement;
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!replaced) {
|
||||
// Append after the section's last real line rather than after the blank line that
|
||||
// separates it from the next header: this file is edited by hand and by the host
|
||||
// installer as well, and a key parked below the separator reads as if it belonged to
|
||||
// the next section. The host writer (Launcher/WiiCompiled.Setup/RuntimeConfiguration.cs)
|
||||
// applies exactly this rule.
|
||||
size_t insertAt = sectionEnd;
|
||||
while (insertAt > sectionStart + 1 && Trim(lines[insertAt - 1]).empty()) {
|
||||
--insertAt;
|
||||
}
|
||||
lines.insert(lines.begin() + static_cast<std::ptrdiff_t>(insertAt), replacement);
|
||||
}
|
||||
const auto updated = UpdateSettingText(existing.str(), section, key, value);
|
||||
if (!updated) {
|
||||
std::cerr << "[runtime-config] Not updating invalid " << PathToUtf8(path) << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
@@ -619,9 +579,7 @@ inline bool WriteSetting(std::string_view section, std::string_view key, std::st
|
||||
std::cerr << "[runtime-config] Unable to write " << PathToUtf8(path) << std::endl;
|
||||
return false;
|
||||
}
|
||||
for (const auto& outputLine : lines) {
|
||||
output << outputLine << '\n';
|
||||
}
|
||||
output << *updated;
|
||||
return static_cast<bool>(output);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,15 +73,8 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample);
|
||||
// no remote or SDL has not delivered a sample yet.
|
||||
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]);
|
||||
|
||||
// Accelerometer zero-point calibration. SDL's Wii driver tries to read the
|
||||
// remote's factory calibration block, but over Bluetooth that read often times
|
||||
// out ("Using fallback accelerometer calibration" in console.log) and it falls
|
||||
// back to a nominal 0x200 zero point; a real remote then carries a per-axis
|
||||
// bias of up to ~0.2 g, which held sideways as a wheel is a permanent steering
|
||||
// offset.
|
||||
// StartAccelCalibration expects the remote lying still with the buttons up;
|
||||
// Poll() then collects samples for about a second and stores the difference to
|
||||
// the ideal (0, 1, 0) g in Config.toml.
|
||||
// Residual zero-point trim on top of SDL's factory-calibrated decode. Expects the
|
||||
// remote lying still, buttons up; Poll() averages about a second and stores it.
|
||||
void StartAccelCalibration(uint32_t chan);
|
||||
// Forgets the stored correction.
|
||||
void ClearAccelCalibration();
|
||||
|
||||
@@ -378,10 +378,15 @@ void AttachParentConsoleForDiagnostics() {
|
||||
void AttachParentConsoleForDiagnostics() {}
|
||||
#endif
|
||||
|
||||
// The setup writes build-fingerprint.json beside every product executable; its SetupVersion is
|
||||
// the only version identity the runtime has (products are compiled locally, so nothing is baked
|
||||
// into the binary). Surface it at the top of the transcript so every attached log self-identifies.
|
||||
// The macOS and Linux local builds bake the setup version in; the Windows setup instead writes it
|
||||
// to build-fingerprint.json beside the executable. Surfaced at the top of the transcript so every
|
||||
// attached log self-identifies.
|
||||
std::string ReadInstalledSetupVersion() {
|
||||
#if defined(MKW_SETUP_VERSION)
|
||||
if (constexpr std::string_view builtIn = MKW_SETUP_VERSION; !builtIn.empty()) {
|
||||
return std::string(builtIn);
|
||||
}
|
||||
#endif
|
||||
const auto directory = RuntimeConfigFile::ExecutableDirectory();
|
||||
if (!directory) {
|
||||
return {};
|
||||
|
||||
@@ -309,9 +309,8 @@ void DrawWiiRemoteAccelerometer(uint32_t port) {
|
||||
} else {
|
||||
ImGui::TextDisabled("No accelerometer data yet.");
|
||||
}
|
||||
// SDL's read of the remote's calibration block often times out over Bluetooth
|
||||
// and it falls back to a nominal zero point, leaving a small per-axis bias;
|
||||
// measured here with the remote at rest.
|
||||
// Residual per-axis bias left after SDL's factory-calibrated decode, measured
|
||||
// here with the remote at rest.
|
||||
if (WiiRemoteInput::IsAccelCalibrating()) {
|
||||
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
|
||||
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
|
||||
@@ -333,7 +332,7 @@ void DrawWiiRemoteAccelerometer(uint32_t port) {
|
||||
const std::array<double, 3> offset = RuntimeConfigFile::WiiAccelOffset();
|
||||
ImGui::TextDisabled("Stored offset: x %+.3f y %+.3f z %+.3f g", offset[0], offset[1], offset[2]);
|
||||
} else {
|
||||
ImGui::TextDisabled("Not calibrated (using SDL's zero point; see console.log for \"fallback accelerometer calibration\").");
|
||||
ImGui::TextDisabled("No trim stored.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,10 +102,10 @@ bool g_accelOffsetLoaded = false;
|
||||
// sample may stray from the first one before the run is declared "moved".
|
||||
constexpr int kCalibrationSamples = 90;
|
||||
constexpr float kCalibrationMaxDeviationG = 0.15f;
|
||||
// |mean| outside this range means the remote was not at rest or the
|
||||
// accelerometer is far off its nominal scale; either way the offset is useless.
|
||||
constexpr float kCalibrationMinGravityG = 0.8f;
|
||||
constexpr float kCalibrationMaxGravityG = 1.2f;
|
||||
// A resting remote reads about 1 g and lies within 20 degrees of face up.
|
||||
constexpr double kCalibrationMinGravityG = 0.8;
|
||||
constexpr double kCalibrationMaxGravityG = 1.2;
|
||||
constexpr double kCalibrationMinTiltCosine = 0.9396926;
|
||||
|
||||
struct AccelCalibration {
|
||||
bool active = false;
|
||||
@@ -320,7 +320,8 @@ void StepAccelCalibration() {
|
||||
std::array<double, 3> mean{};
|
||||
for (int i = 0; i < 3; ++i) mean[i] = g_calibration.sum[i] / g_calibration.count;
|
||||
const double length = std::sqrt(mean[0] * mean[0] + mean[1] * mean[1] + mean[2] * mean[2]);
|
||||
if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG || mean[1] < 0.5) {
|
||||
if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG ||
|
||||
mean[1] / length < kCalibrationMinTiltCosine) {
|
||||
FinishAccelCalibration("Failed: the remote was not resting flat with the buttons up.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "runtime_config.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
bool Require(bool value, const char* message) {
|
||||
@@ -11,6 +13,24 @@ bool Require(bool value, const char* message) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::optional<toml::ordered_value> Update(const std::string& original, const char* section, const char* key,
|
||||
const char* value, std::string* text = nullptr) {
|
||||
const auto updated = RuntimeConfigFile::UpdateSettingText(original, section, key, value);
|
||||
if (!updated) return std::nullopt;
|
||||
if (text) *text = *updated;
|
||||
return toml::parse_str<toml::ordered_type_config>(*updated);
|
||||
}
|
||||
bool SetsFps(const std::string& original, const char* message) {
|
||||
const auto document = Update(original, "video", "show_fps", "true");
|
||||
return Require(document && document->at("video").at("show_fps").as_boolean() &&
|
||||
document->at("video").as_table().size() == 1,
|
||||
message);
|
||||
}
|
||||
bool SetsOffset(const std::string& original, const char* message) {
|
||||
const auto document = Update(original, "controller.wii_accel_offsets", "x", "0.5");
|
||||
return Require(document && document->at("controller").at("wii_accel_offsets").at("x").as_floating() == 0.5,
|
||||
message);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
@@ -31,5 +51,49 @@ int main() {
|
||||
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "invalid.toml");
|
||||
if (!Require(!config.metalFxSpatialUpscaling, "invalid MetalFX setting was accepted")) return 1;
|
||||
}
|
||||
if (!SetsFps("[video]\nshow_fps = false\n", "bare key was not replaced")) return 1;
|
||||
if (!SetsFps("[video]\n\"show_fps\" = false\n", "basic-quoted key was not replaced")) return 1;
|
||||
if (!SetsFps("[video]\n'show_fps'=false\n", "literal-quoted key was not replaced")) return 1;
|
||||
if (!SetsFps("[video]\n\"show\\u005Ffps\" = false\n", "escaped key was not replaced")) return 1;
|
||||
if (!SetsFps("[\"video\"]\n show_fps = false\n", "quoted table header was not matched")) return 1;
|
||||
if (!SetsFps("[video]\nshow_fps = [\n 1,\n]\n", "multi-line value was not replaced")) return 1;
|
||||
if (!SetsFps("[video]\n", "missing key was not created")) return 1;
|
||||
if (!SetsFps("", "missing table was not created")) return 1;
|
||||
{
|
||||
std::string text;
|
||||
const auto document = Update("# top\nfoo = 1\n\n[video]\nzzz = 2 # keep\nshow_fps = false # note\naaa = 3\n\n"
|
||||
"[audio]\nshow_fps = 1\n",
|
||||
"video", "show_fps", "true", &text);
|
||||
if (!Require(document && document->at("foo").as_integer() == 1 &&
|
||||
document->at("audio").at("show_fps").as_integer() == 1 &&
|
||||
document->at("video").at("zzz").as_integer() == 2 &&
|
||||
document->at("video").at("aaa").as_integer() == 3 &&
|
||||
document->at("video").at("show_fps").as_boolean(),
|
||||
"unrelated settings were not preserved")) return 1;
|
||||
const size_t zzz = text.find("zzz"), fps = text.find("show_fps"), aaa = text.find("aaa");
|
||||
if (!Require(text.find("# top") != std::string::npos && text.find("# keep") != std::string::npos &&
|
||||
text.find("# note") != std::string::npos && zzz < fps && fps < aaa,
|
||||
"comments or key order were not preserved")) return 1;
|
||||
}
|
||||
if (!Require(!RuntimeConfigFile::UpdateSettingText("[video]\nshow_fps = 1\nshow_fps = 2\n", "video", "show_fps", "true"),
|
||||
"a document with a duplicate key was rewritten")) return 1;
|
||||
if (!Require(!RuntimeConfigFile::UpdateSettingText("[video\nshow_fps = 1\n", "video", "show_fps", "true"),
|
||||
"an unparseable document was rewritten")) return 1;
|
||||
if (!SetsOffset("[controller]\nrumble = true\n\n[controller.wii_accel_offsets]\nx = 0.1\ny = 2\n",
|
||||
"key in an existing nested table was not replaced")) return 1;
|
||||
if (!SetsOffset("[controller.wii_accel_offsets]\ny = 2\n", "key was not added to a nested table")) return 1;
|
||||
if (!SetsOffset("[controller]\nrumble = true\n", "missing nested table was not created")) return 1;
|
||||
if (!SetsOffset("[controller.\"wii_accel_offsets\"]\nx = 0.1\n", "quoted nested header was not matched")) return 1;
|
||||
if (!SetsOffset("[controller]\nwii_accel_offsets.y = 2\n", "dotted-key table was not extended")) return 1;
|
||||
if (!SetsOffset("controller.wii_accel_offsets.y = 2\n", "root-level dotted table was not extended")) return 1;
|
||||
if (!Require(!RuntimeConfigFile::UpdateSettingText("controller = \"custom\"\n", "controller.wii_accel_offsets",
|
||||
"x", "0.5"),
|
||||
"a scalar was replaced by a table")) return 1;
|
||||
{
|
||||
const auto document = Update("[video.advanced]\nfoo = 1\n", "video", "show_fps", "true");
|
||||
if (!Require(document && document->at("video").at("show_fps").as_boolean() &&
|
||||
document->at("video").at("advanced").at("foo").as_integer() == 1,
|
||||
"key was not added to an implicit table")) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user