diff --git a/README.md b/README.md index 4019ee51..c8baff15 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ CEX2は`CEX2Query`、`CEX2Open`、`CEX2Submit`、`CEX2Poll`、`CEX2Cancel`、`CE packageはユーザーデータの`cemuextend/mods/`へ配置します。`Options` → `General settings` → `CemuExtend`を開くと、manifestに含まれる対象ゲームが自動で一覧化されます。Mod名のチェックを付けると有効、外すと無効になり、変更は次回タイトル起動時に反映されます。Title IDの手入力は不要です。 +Mod単位の権限パネルにある「Trust future updates to this Mod」はopt-inの例外です。有効にすると、その時点で承認した権限をMod ID単位で記録し、以後principalが変わっても要求権限が承認範囲を超えない限り再承認なしで自動的に引き継がれます。要求権限が増えた場合は従来どおり再承認が必要です。デフォルトは無効(未チェック)で、principalごとの再承認という既定の挙動は変わりません。 + 有効なModに未許可または新規要求の権限がある場合、CemuExtendはタイトルのマウント前に権限確認画面を表示します。複数Modは1画面にまとめられ、選択した権限を保存してからゲームを起動します。権限を一部拒否したまま起動することもできますが、不足が残る間は次回起動時にも確認されます。キャンセルした場合、ゲームは準備・起動されません。 ### `isolated` diff --git a/src/Cafe/OS/libs/cemuextend/CemodPermission.h b/src/Cafe/OS/libs/cemuextend/CemodPermission.h index 6c3fede2..d18d3d0e 100644 --- a/src/Cafe/OS/libs/cemuextend/CemodPermission.h +++ b/src/Cafe/OS/libs/cemuextend/CemodPermission.h @@ -13,4 +13,11 @@ namespace cemuextend_hle return enabled && ((requested & ~granted) != 0 || (requested & ~approvedRequests) != 0); } + + [[nodiscard]] constexpr bool CemodTrustAnchorCoversRequest(std::uint32_t requested, + std::uint32_t anchorApprovedRequests) + { + requested &= kCemodPermissionMask; + return (requested & ~anchorApprovedRequests) == 0; + } } diff --git a/src/Cafe/OS/libs/cemuextend/cemuextend.cpp b/src/Cafe/OS/libs/cemuextend/cemuextend.cpp index 6dd9aa7d..fffbb6c9 100644 --- a/src/Cafe/OS/libs/cemuextend/cemuextend.cpp +++ b/src/Cafe/OS/libs/cemuextend/cemuextend.cpp @@ -243,6 +243,20 @@ namespace cemuextend_hle return runtime; } + CemuExtendModGrant ResolveCemodGrant(std::uint64_t titleId, const std::string& modId, + const std::string& principal, std::uint32_t requestedPermissions) + { + if (const auto exact = GetConfig().GetCemuExtendModGrant(titleId, principal)) + return *exact; + const auto anchor = GetConfig().GetCemuExtendModTrustAnchor(titleId, modId); + if (!anchor || !CemodTrustAnchorCoversRequest(requestedPermissions, anchor->approved_request_mask)) + return {}; + const CemuExtendModGrant grant{anchor->permissions & requestedPermissions & kCemodPermissionMask, + anchor->approved_request_mask, true}; + GetConfig().SetCemuExtendModGrant(titleId, principal, grant); + return grant; + } + std::vector DiscoverCemodCatalog() { namespace fs = std::filesystem; @@ -295,8 +309,8 @@ namespace cemuextend_hle for (const auto& package : DiscoverCemods(titleId)) { if (!package.error.empty()) continue; - const auto grant = GetConfig().GetCemuExtendModGrant(titleId, package.principal) - .value_or(CemuExtendModGrant{}); + const auto grant = ResolveCemodGrant(titleId, package.modId, package.principal, + package.requestedPermissions & kCemodPermissionMask); if (!grant.approved) continue; auto found = grouped.try_emplace(package.principal, CemodPermissionRequest{package.modId, package.principal, 0, @@ -353,8 +367,8 @@ namespace cemuextend_hle std::string error; auto package = CemodPackage::Load(info.path, titleId, error); if (!package) continue; - const auto grant = GetConfig().GetCemuExtendModGrant(titleId, package->principal) - .value_or(CemuExtendModGrant{}); + const auto grant = ResolveCemodGrant(titleId, package->manifest.modId, package->principal, + package->manifest.requestedPermissions & kCemodPermissionMask); if (!grant.approved || (package->manifest.requestedPermissions & ~grant.approved_request_mask) != 0) { cemuLog_log(LogType::Force, diff --git a/src/Cafe/OS/libs/cemuextend/cemuextend.h b/src/Cafe/OS/libs/cemuextend/cemuextend.h index 61d4a35b..2d21e325 100644 --- a/src/Cafe/OS/libs/cemuextend/cemuextend.h +++ b/src/Cafe/OS/libs/cemuextend/cemuextend.h @@ -11,6 +11,7 @@ class ModExecutionContext; class CemodRuntime; +struct CemuExtendModGrant; struct CemodPackageInfo { @@ -39,6 +40,8 @@ namespace cemuextend_hle COSModule* GetModule(); void ConfigureCex2HleAccess(ModExecutionContext& context); CemodRuntime& GetCemodRuntime(); + ::CemuExtendModGrant ResolveCemodGrant(std::uint64_t titleId, const std::string& modId, + const std::string& principal, std::uint32_t requestedPermissions); std::vector DiscoverCemodCatalog(); std::vector DiscoverCemods(std::uint64_t titleId); std::vector PendingCemodPermissionRequests(std::uint64_t titleId); diff --git a/src/Cafe/tests/CemodPermissionTests.cpp b/src/Cafe/tests/CemodPermissionTests.cpp index 0ef43972..815a0728 100644 --- a/src/Cafe/tests/CemodPermissionTests.cpp +++ b/src/Cafe/tests/CemodPermissionTests.cpp @@ -17,6 +17,7 @@ namespace int main() { using cemuextend_hle::NeedsCemodPermissionPrompt; + using cemuextend_hle::CemodTrustAnchorCoversRequest; CHECK(!NeedsCemodPermissionPrompt(0x1fU, 0, 0, false)); CHECK(!NeedsCemodPermissionPrompt(0, 0, 0, true)); @@ -24,5 +25,11 @@ int main() CHECK(NeedsCemodPermissionPrompt(0x1fU, 0x0fU, 0x1fU, true)); CHECK(NeedsCemodPermissionPrompt(0x1fU, 0x1fU, 0x0fU, true)); CHECK(!NeedsCemodPermissionPrompt(0x20U, 0, 0, true)); + + CHECK(CemodTrustAnchorCoversRequest(0x0fU, 0x1fU)); + CHECK(CemodTrustAnchorCoversRequest(0x1fU, 0x1fU)); + CHECK(!CemodTrustAnchorCoversRequest(0x1fU, 0x0fU)); + CHECK(!CemodTrustAnchorCoversRequest(0x02U, 0x01U)); + CHECK(CemodTrustAnchorCoversRequest(0x20U, 0)); return 0; } diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 6c60ecab..0515fc29 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -50,6 +50,33 @@ void CemuConfig::RemoveCemuExtendModGrant(uint64 titleId, std::string_view princ if (title->second.empty()) cemuextend_mod_grants.erase(title); } +std::optional CemuConfig::GetCemuExtendModTrustAnchor(uint64 titleId, + std::string_view modId) const +{ + std::shared_lock lock(cemuextend_grants_mutex); + const auto title = cemuextend_mod_trust.find(titleId); + if (title == cemuextend_mod_trust.end()) return std::nullopt; + const auto mod = title->second.find(std::string(modId)); + return mod == title->second.end() ? std::nullopt : std::optional{mod->second}; +} + +void CemuConfig::SetCemuExtendModTrustAnchor(uint64 titleId, std::string modId, + CemuExtendModTrustAnchor anchor) +{ + if (titleId == 0 || modId.empty()) return; + std::unique_lock lock(cemuextend_grants_mutex); + cemuextend_mod_trust[titleId][std::move(modId)] = anchor; +} + +void CemuConfig::RemoveCemuExtendModTrustAnchor(uint64 titleId, std::string_view modId) +{ + std::unique_lock lock(cemuextend_grants_mutex); + const auto title = cemuextend_mod_trust.find(titleId); + if (title == cemuextend_mod_trust.end()) return; + title->second.erase(std::string(modId)); + if (title->second.empty()) cemuextend_mod_trust.erase(title); +} + void CemuConfig::SetMLCPath(fs::path path, bool save) { mlc_path.SetValue(_pathToUtf8(path)); @@ -100,6 +127,7 @@ XMLConfigParser CemuConfig::Load(XMLConfigParser& parser) std::unique_lock grantsLock(cemuextend_grants_mutex); cemuextend_grants.clear(); cemuextend_mod_grants.clear(); + cemuextend_mod_trust.clear(); auto bridge = parser.get("CemuExtend"); for (auto title = bridge.get("Title"); title.valid(); title = bridge.get("Title", title)) { @@ -123,6 +151,16 @@ XMLConfigParser CemuConfig::Load(XMLConfigParser& parser) mod.get_attribute("approved_requests", 0) & 0x1fU, mod.get_attribute("approved", false)}; } + for (auto trust = bridge.get("ModTrust"); trust.valid(); trust = bridge.get("ModTrust", trust)) + { + const auto titleId = trust.get_attribute("title", 0); + const std::string modId = trust.get_attribute("mod_id", ""); + if (titleId == 0 || modId.empty() || modId.size() > 128) + continue; + cemuextend_mod_trust[titleId][modId] = { + trust.get_attribute("permissions", 0) & 0x1fU, + trust.get_attribute("approved_requests", 0) & 0x1fU}; + } } std::unique_lock _lock(game_cache_entries_mutex); @@ -412,6 +450,15 @@ XMLConfigParser CemuConfig::Save(XMLConfigParser& parser) mod.set_attribute("approved_requests", grant.approved_request_mask & 0x1fU); mod.set_attribute("approved", grant.approved); } + for (const auto& [titleId, mods] : cemuextend_mod_trust) + for (const auto& [modId, anchor] : mods) + { + auto trust = bridge.set("ModTrust"); + trust.set_attribute("title", static_cast(titleId)); + trust.set_attribute("mod_id", modId.c_str()); + trust.set_attribute("permissions", anchor.permissions & 0x1fU); + trust.set_attribute("approved_requests", anchor.approved_request_mask & 0x1fU); + } } // game list cache diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index a90d5a87..4333b05e 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -79,6 +79,12 @@ struct CemuExtendModGrant bool approved{}; }; +struct CemuExtendModTrustAnchor +{ + uint32 permissions{}; + uint32 approved_request_mask{}; +}; + enum GraphicAPI { kOpenGL = 0, @@ -530,10 +536,15 @@ struct CemuConfig std::string_view principal) const; void SetCemuExtendModGrant(uint64 titleId, std::string principal, CemuExtendModGrant grant); void RemoveCemuExtendModGrant(uint64 titleId, std::string_view principal); + std::optional GetCemuExtendModTrustAnchor(uint64 titleId, + std::string_view modId) const; + void SetCemuExtendModTrustAnchor(uint64 titleId, std::string modId, CemuExtendModTrustAnchor anchor); + void RemoveCemuExtendModTrustAnchor(uint64 titleId, std::string_view modId); mutable std::shared_mutex cemuextend_grants_mutex; std::unordered_map cemuextend_grants; std::unordered_map> cemuextend_mod_grants; + std::unordered_map> cemuextend_mod_trust; // debug ConfigValueBounds crash_dump{ CrashDump::Disabled }; diff --git a/src/gui/wxgui/GeneralSettings2.cpp b/src/gui/wxgui/GeneralSettings2.cpp index d8853b5c..7d9052e7 100644 --- a/src/gui/wxgui/GeneralSettings2.cpp +++ b/src/gui/wxgui/GeneralSettings2.cpp @@ -1040,6 +1040,9 @@ wxPanel* GeneralSettings2::AddCemuExtendPage(wxNotebook* notebook) wxString::FromUTF8(permissionNames[index])); permissionBox->Add(m_cemod_permissions[index], 0, wxLEFT | wxRIGHT | wxBOTTOM, 6); } + m_cemod_trust_updates = new wxCheckBox(permissionParent, wxID_ANY, + _("Trust future updates to this Mod (keep it enabled after the .cemod file is replaced with a newer version)")); + permissionBox->Add(m_cemod_trust_updates, 0, wxLEFT | wxRIGHT | wxBOTTOM, 6); auto* modButtons = new wxBoxSizer(wxHORIZONTAL); auto* import = new wxButton(permissionParent, wxID_ANY, _("Import legacy data…")); auto* saveMod = new wxButton(permissionParent, wxID_ANY, _("Save permissions")); @@ -1203,6 +1206,7 @@ void GeneralSettings2::RefreshCemodList() if (!m_cemod_list) return; m_cemod_list->Clear(); m_cemod_principals.clear(); + m_cemod_mod_ids.clear(); m_cemod_requested.clear(); m_cemod_trusted.clear(); m_cemod_signed.clear(); @@ -1221,11 +1225,12 @@ void GeneralSettings2::RefreshCemodList() package.executionMode == CemodExecutionMode::TrustedNative ? "trusted native" : "isolated", package.signedPackage ? "signed" : "unsigned"))); m_cemod_principals.push_back(package.principal); + m_cemod_mod_ids.push_back(package.modId); m_cemod_requested.push_back(package.requestedPermissions); m_cemod_trusted.push_back(package.executionMode == CemodExecutionMode::TrustedNative); m_cemod_signed.push_back(package.signedPackage); - const auto grant = GetConfig().GetCemuExtendModGrant(*titleId, package.principal) - .value_or(CemuExtendModGrant{}); + const auto grant = cemuextend_hle::ResolveCemodGrant(*titleId, package.modId, package.principal, + package.requestedPermissions); m_cemod_list->Check(m_cemod_principals.size() - 1, grant.approved && (package.requestedPermissions & ~grant.approved_request_mask) == 0); } @@ -1243,8 +1248,8 @@ void GeneralSettings2::LoadCemodGrant() const auto titleId = SelectedCemuExtendTitle(); const bool valid = selection != wxNOT_FOUND && static_cast(selection) < m_cemod_principals.size() && titleId.has_value(); - const auto grant = valid ? GetConfig().GetCemuExtendModGrant(*titleId, - m_cemod_principals[selection]).value_or(CemuExtendModGrant{}) : CemuExtendModGrant{}; + const auto grant = valid ? cemuextend_hle::ResolveCemodGrant(*titleId, m_cemod_mod_ids[selection], + m_cemod_principals[selection], m_cemod_requested[selection]) : CemuExtendModGrant{}; const auto granted = grant.permissions; const auto requested = valid ? m_cemod_requested[selection] : 0; if (valid && m_cemod_trusted[selection]) @@ -1259,6 +1264,9 @@ void GeneralSettings2::LoadCemodGrant() m_cemod_permissions[index]->Enable(valid && (requested & bit)); m_cemod_permissions[index]->SetValue(valid && (granted & bit)); } + m_cemod_trust_updates->Enable(valid); + m_cemod_trust_updates->SetValue(valid && GetConfig().GetCemuExtendModTrustAnchor(*titleId, + m_cemod_mod_ids[selection]).has_value()); } void GeneralSettings2::ToggleCemod(std::size_t selection, bool enabled) @@ -1276,16 +1284,19 @@ void GeneralSettings2::ToggleCemod(std::size_t selection, bool enabled) return; } } - auto grant = GetConfig().GetCemuExtendModGrant(*titleId, m_cemod_principals[selection]) - .value_or(CemuExtendModGrant{}); + auto grant = cemuextend_hle::ResolveCemodGrant(*titleId, m_cemod_mod_ids[selection], + m_cemod_principals[selection], m_cemod_requested[selection]); grant.permissions &= m_cemod_requested[selection]; grant.approved_request_mask = enabled ? m_cemod_requested[selection] : 0U; grant.approved = enabled; GetConfig().SetCemuExtendModGrant(*titleId, m_cemod_principals[selection], grant); + if (!enabled) + GetConfig().RemoveCemuExtendModTrustAnchor(*titleId, m_cemod_mod_ids[selection]); GetConfigHandle().Save(); m_cemod_details->SetLabel(enabled ? _("Enabled. The Mod will load on the next game launch.") : _("Disabled. The Mod will not load on the next game launch.")); + LoadCemodGrant(); } void GeneralSettings2::StoreCemodGrant() @@ -1302,6 +1313,11 @@ void GeneralSettings2::StoreCemodGrant() const bool enabled = m_cemod_list->IsChecked(static_cast(selection)); GetConfig().SetCemuExtendModGrant(*titleId, m_cemod_principals[selection], {permissions, enabled ? m_cemod_requested[selection] : 0U, enabled}); + if (enabled && m_cemod_trust_updates->IsChecked()) + GetConfig().SetCemuExtendModTrustAnchor(*titleId, m_cemod_mod_ids[selection], + {permissions, m_cemod_requested[selection]}); + else + GetConfig().RemoveCemuExtendModTrustAnchor(*titleId, m_cemod_mod_ids[selection]); GetConfigHandle().Save(); m_cemod_details->SetLabel(_("Permissions saved. Changes apply on the next game launch.")); } diff --git a/src/gui/wxgui/GeneralSettings2.h b/src/gui/wxgui/GeneralSettings2.h index c8a05515..69073379 100644 --- a/src/gui/wxgui/GeneralSettings2.h +++ b/src/gui/wxgui/GeneralSettings2.h @@ -110,7 +110,9 @@ private: wxStaticText* m_cemod_status{}; wxStaticText* m_cemod_details{}; std::array m_cemod_permissions{}; + wxCheckBox* m_cemod_trust_updates{}; std::vector m_cemod_principals; + std::vector m_cemod_mod_ids; std::vector m_cemod_requested; std::vector m_cemod_trusted; std::vector m_cemod_signed;