diff --git a/src/Hook/Hooks_Package.cpp b/src/Hook/Hooks_Package.cpp index 1e265d2..3fc6177 100644 --- a/src/Hook/Hooks_Package.cpp +++ b/src/Hook/Hooks_Package.cpp @@ -3,7 +3,6 @@ #include "Hooks_SteamUI.h" #include "dllmain.h" #include "Utils/VehCommon.h" -#include namespace { RESOLVE_FUNC(CUtlMemoryGrow, void*, CUtlVector*, int); @@ -134,9 +133,6 @@ namespace Hooks_Package { UNHOOK_END(); } - constexpr size_t kBatchSize = 50; - constexpr DWORD kBatchSleepMs = 20; - void NotifyLicenseChanged() { PackageInfo* pPkg = g_pInjectedPackageInfo; if (!pPkg) { @@ -159,12 +155,19 @@ namespace Hooks_Package { // ── Add depots that are newly loaded ── std::vector additions = LuaConfig::TakePendingAdditions(); + std::unordered_set addedIds; LOG_PACKAGE_DEBUG("NotifyLicenseChanged: processing {} additions", additions.size()); if (!additions.empty()) { uint32_t oldSize = pPkg->AppIdVec.m_Size; if (CUtlMemoryGrowWrap(&pPkg->AppIdVec, additions.size())) { + // An applied addition invalidates any UI removal that has not + // reached the UI thread yet. + for (AppId_t id : additions) + Hooks_SteamUI::CancelRemoval(id); + for (size_t i = 0; i < additions.size(); ++i) { pPkg->AppIdVec.m_Memory.m_pMemory[oldSize + i] = additions[i]; + addedIds.insert(additions[i]); LOG_PACKAGE_DEBUG("NotifyLicenseChanged: inserted AppId {} at [{}]", additions[i], oldSize + i); } }else { @@ -172,7 +175,7 @@ namespace Hooks_Package { } } - if (additions.empty() && removedCount == 0) { + if (addedIds.empty() && removedCount == 0) { LOG_PACKAGE_DEBUG("NotifyLicenseChanged: no changes"); return; } @@ -182,16 +185,20 @@ namespace Hooks_Package { LOG_PACKAGE_WARN("NotifyLicenseChanged: failed to mark license as changed"); return; } - LOG_PACKAGE_INFO("NotifyLicenseChanged: {} added, {} removed", additions.size(), removedCount); + LOG_PACKAGE_INFO("NotifyLicenseChanged: {} added, {} removed", addedIds.size(), removedCount); - // every kBatchSize ids changed, sleep kBatchSleepMs milliseconds - size_t i = 0; + // Queue UI removals for the main-thread RunFrame hook to drain. + // Never touch MarkAppChange from this (FileWatcher) thread. + size_t queuedRemovalCount = 0; for (AppId_t id : removals) { - if (++i % kBatchSize == 0) { - LOG_PACKAGE_DEBUG("NotifyLicenseChanged: processed {} removals, sleeping for {} ms...", i, kBatchSleepMs); - std::this_thread::sleep_for(std::chrono::milliseconds(kBatchSleepMs)); + // ParseFile unloads the old file before parsing the replacement. + // Do not queue that transient removal when the id was added again. + if (!addedIds.contains(id)) { + Hooks_SteamUI::QueueRemoval(id); + ++queuedRemovalCount; } - Hooks_SteamUI::RemoveAppAndSendChange(id); } + LOG_PACKAGE_DEBUG("NotifyLicenseChanged: queued {} UI removals, skipped {} transient removals", + queuedRemovalCount, removals.size() - queuedRemovalCount); } } diff --git a/src/Hook/Hooks_SteamUI.cpp b/src/Hook/Hooks_SteamUI.cpp index 3c41947..868ebb1 100644 --- a/src/Hook/Hooks_SteamUI.cpp +++ b/src/Hook/Hooks_SteamUI.cpp @@ -4,17 +4,22 @@ #include "dllmain.h" #include "steam_messages.pb.h" #include "Utils/VehCommon.h" +#include -namespace { +namespace +{ + RESOLVE_FUNC(RepeatedFieldUint32_Add, void, void* field, const uint32* value); CAPTURE_THIS_FUNC(GetAppByID, CSteamApp*, g_pController,void* pThis, AppId_t appId, bool bCreate); CAPTURE_THIS_FUNC(MarkAppChange,void*,g_pAppChangeSource,void* pThis,AppId_t appId, EAppChangeFlags changeFlags); - HOOK_FUNC(FillInAppOverview,void*,void* pThis,void* pAppOverview,CSteamApp* pApp) + HOOK_FUNC(FillInAppOverview, void *, void *pThis, void *pAppOverview, CSteamApp *pApp) { - if (pApp && LuaConfig::HasDepot(pApp->nAppID,false)) { + if (pApp && LuaConfig::HasDepot(pApp->nAppID, false)) + { uint32_t t = LuaConfig::GetPurchaseTime(pApp->nAppID); - if(t) { + if (t) + { pApp->PurchasedTime = t; LOG_STEAMUI_TRACE("FillInAppOverview: set PurchasedTime={} for appId={}", pApp->PurchasedTime, pApp->nAppID); @@ -22,42 +27,101 @@ namespace { } return oFillInAppOverview(pThis, pAppOverview, pApp); } -} -namespace Hooks_SteamUI { - void Install() { + // Apps to drop from the library: queued off-thread, marked on the UI thread. + std::mutex g_removalMutex; + std::vector g_pendingRemovals; + std::unordered_set g_removedAppIds; + + // A full rebuild never lists removed_appid for apps still in the map + // so re-assert our set after the snapshot is built. + HOOK_FUNC(BuildCompleteAppOverviewChange, void, void *pController, + CAppOverview_Change *pChange, void *optionalCallbackSlot) + { + oBuildCompleteAppOverviewChange(pController, pChange, optionalCallbackSlot); + std::lock_guard lock(g_removalMutex); + if (pChange && !g_removedAppIds.empty() && oRepeatedFieldUint32_Add) + { + auto* field = pChange->mutable_removed_appid(); + for (AppId_t appId : g_removedAppIds){ + oRepeatedFieldUint32_Add(field, &appId); + } + LOG_STEAMUI_DEBUG("BuildCompleteAppOverviewChange: appended {} removed_appid entries", + g_removedAppIds.size()); + } + } + + + // Clearing ownership makes ShouldShowAppInLibrary() false (delta drops it, + // the full snapshot skips it); MarkAppChange triggers the flush. + HOOK_FUNC(CSteamUIAppControllerRunFrame, void *, void *pController) + { + if (CAPTURE_READY(GetAppByID) && CAPTURE_READY(MarkAppChange)) + { + std::vector draining; + { + std::lock_guard lock(g_removalMutex); + draining.swap(g_pendingRemovals); + } + for (AppId_t appId : draining) + { + if (LuaConfig::IsOwned(appId)) + { + LOG_STEAMUI_DEBUG("RunFrame: appId {} is owned again, skipping removal", appId); + continue; + } + if (CSteamApp *pApp = oGetAppByID(g_pController, appId, false)) + { + // Only remove from the library if it's not already uninstalled + pApp->OwnershipFlags = k_EAppOwnershipFlags_None; + if(pApp->AppStateFlags == k_EAppStateUninstalled){ + std::lock_guard lock(g_removalMutex); + g_removedAppIds.insert(appId); + } + } + + oMarkAppChange(g_pAppChangeSource, appId, EAppChangeFlags::AppInfoOrConfig); + } + } + return oCSteamUIAppControllerRunFrame(pController); + } +} +namespace Hooks_SteamUI +{ + void Install() + { ARM_CAPTURE_U(GetAppByID); ARM_CAPTURE_U(MarkAppChange); + RESOLVE_U(RepeatedFieldUint32_Add); + HOOK_BEGIN(); INSTALL_HOOK_U(FillInAppOverview); + INSTALL_HOOK_U(BuildCompleteAppOverviewChange); + INSTALL_HOOK_U(CSteamUIAppControllerRunFrame); HOOK_END(); - } - void Uninstall() { + void Uninstall() + { UNHOOK_BEGIN(); UNINSTALL_HOOK(FillInAppOverview); + UNINSTALL_HOOK(BuildCompleteAppOverviewChange); + UNINSTALL_HOOK(CSteamUIAppControllerRunFrame); UNHOOK_END(); } - void RemoveAppAndSendChange(AppId_t appId) { - // skip on owned apps - if(LuaConfig::IsOwned(appId)){ - LOG_STEAMUI_WARN("RemoveAppAndSendChange: appId={} is owned, skipping", appId); - return; - } - if(CAPTURE_READY(GetAppByID) && CAPTURE_READY(MarkAppChange)) { - CSteamApp* pApp = oGetAppByID(g_pController, appId, false); - if(pApp) { - pApp->OwnershipFlags = k_EAppOwnershipFlags_None; - LOG_STEAMUI_DEBUG("RemoveAppAndSendChange: cleared owned flag for appId={}", appId); - oMarkAppChange(g_pAppChangeSource, appId, EAppChangeFlags::AddedOrCreated); - } else { - LOG_STEAMUI_WARN("RemoveAppAndSendChange: appId={} not found in GetAppByID", appId); - } - } + void QueueRemoval(AppId_t appId) + { + std::lock_guard lock(g_removalMutex); + g_pendingRemovals.push_back(appId); } + void CancelRemoval(AppId_t appId) + { + std::lock_guard lock(g_removalMutex); + std::erase(g_pendingRemovals, appId); + g_removedAppIds.erase(appId); + } } diff --git a/src/Hook/Hooks_SteamUI.h b/src/Hook/Hooks_SteamUI.h index 26f1025..062febe 100644 --- a/src/Hook/Hooks_SteamUI.h +++ b/src/Hook/Hooks_SteamUI.h @@ -8,7 +8,8 @@ namespace Hooks_SteamUI { void Install(); void Uninstall(); - // Clears ownership flag for the given appId and - // sends an app change notification to update the library UI. - void RemoveAppAndSendChange(AppId_t appId); + // Queues an appId for removal from the library UI + void QueueRemoval(AppId_t appId); + // Cancels a queued removal when the app is added again before the UI drains it. + void CancelRemoval(AppId_t appId); } diff --git a/src/Steam/Enums.h b/src/Steam/Enums.h index 5fc23c3..157c7ca 100644 --- a/src/Steam/Enums.h +++ b/src/Steam/Enums.h @@ -1923,4 +1923,35 @@ enum class EAppChangeFlags { GameAction = 0x2000, LibraryAssetCleanup = 0x4000, MRURegenerated = 0x8000, +}; + +// Steam account types +enum EAccountType +{ + k_EAccountTypeInvalid = 0, + k_EAccountTypeIndividual = 1, // single user account + k_EAccountTypeMultiseat = 2, // multiseat (e.g. cybercafe) account + k_EAccountTypeGameServer = 3, // game server account + k_EAccountTypeAnonGameServer = 4, // anonymous game server account + k_EAccountTypePending = 5, // pending + k_EAccountTypeContentServer = 6, // content server + k_EAccountTypeClan = 7, + k_EAccountTypeChat = 8, + k_EAccountTypeConsoleUser = 9, // Fake SteamID for local PSN account on PS3 or Live account on 360, etc. + k_EAccountTypeAnonUser = 10, + + // Max of 16 items in this field + k_EAccountTypeMax +}; + +// Steam universes. Each universe is a self-contained Steam instance. +enum EUniverse +{ + k_EUniverseInvalid = 0, + k_EUniversePublic = 1, + k_EUniverseBeta = 2, + k_EUniverseInternal = 3, + k_EUniverseDev = 4, + // k_EUniverseRC = 5, // no such universe anymore + k_EUniverseMax }; \ No newline at end of file diff --git a/src/Steam/Structs.h b/src/Steam/Structs.h index 3974e76..b63efd8 100644 --- a/src/Steam/Structs.h +++ b/src/Steam/Structs.h @@ -10,6 +10,7 @@ #include #include +#include template struct CUtlMemory{ @@ -60,6 +61,10 @@ struct CUtlBuffer{ const uint8* Base() const { return m_Memory.m_pMemory; } int32 TellPut() const { return m_Put; } int32 TellGet() const { return m_Get; } + + uint32 Size() const { return m_Memory.m_nAllocationCount; } + uint32 Capacity() const { return m_Memory.m_nAllocationCount; } + // Debug helper std::string DebugString() const{ return std::format("m_Memory:0x{:X} m_AllocCnt:{} m_Grow:{} m_Get:{} m_Put:{} m_nOffset:{} m_flags:{}", @@ -114,16 +119,6 @@ struct AppOwnership bool bAllSiteLicenses; bool bAllActivationRequired; bool bFamilyShared; - - std::string DebugString() const { - return std::format("PackageId={} ReleaseState={} SteamId32={} MasterSubscriptionAppID={} TrialSeconds={} ExistInPackageNums={} \ - CountryCode={} TimeStamp={} TimeExpire={} OwnsLicense={} LicenseExpired={} IsPermanent={} LowViolence={} \ - FreeLicense={} RegionRestricted={} FromFreeWeekend={} LicenseLocked={} LicensePending={} RetailLicense={} \ - AutoGrant={} LicensePermanent={} GuestPass={} Borrowed={} AnySiteLicense={} AllSiteLicenses={} AllActivationRequired={} FamilyShared={}", - PackageId, static_cast(ReleaseState), SteamId32, MasterSubscriptionAppID, TrialSeconds, ExistInPackageNums, PurchaseCountryCode, TimeStamp, TimeExpire, - bOwnsLicense, bLicenseExpired, bIsPermanent, bLowViolence, bFreeLicense, bRegionRestricted, bFromFreeWeekend, bLicenseLocked, bLicensePending, - bRetailLicense, bAutoGrant, bLicensePermanent, bGuestPass, bBorrowed, bAnySiteLicense, bAllSiteLicenses, bAllActivationRequired, bFamilyShared); - } }; // Single depot manifest entry (0x20 bytes) produced by BuildDepotDependency. @@ -274,7 +269,7 @@ struct CGameID{ k_EGameIDTypeShortcut = 2, k_EGameIDTypeP2P = 3, }; - + bool IsSteamApp() const { return ( m_gameID.m_nType == k_EGameIDTypeApp ); @@ -306,9 +301,56 @@ struct CGameID{ }; }; +struct CSteamID +{ + CSteamID() + { + m_steamid.m_comp.m_unAccountID = 0; + m_steamid.m_comp.m_EAccountType = k_EAccountTypeInvalid; + m_steamid.m_comp.m_EUniverse = k_EUniverseInvalid; + m_steamid.m_comp.m_unAccountInstance = 0; + } + + CSteamID( uint64 ulSteamID ) + { + SetFromUint64( ulSteamID ); + } + + void SetFromUint64( uint64 ulSteamID ) + { + m_steamid.m_unAll64Bits = ulSteamID; + } + + uint64 ConvertToUint64() const + { + return m_steamid.m_unAll64Bits; + } + + void SetAccountID( uint32 unAccountID ) { m_steamid.m_comp.m_unAccountID = unAccountID; } + AccountID_t GetAccountID() const { return m_steamid.m_comp.m_unAccountID; } + + friend std::ostream& operator<<(std::ostream& os, const CSteamID& steamId){ + return os << steamId.ConvertToUint64(); + } + +private: + union + { + struct SteamIDComponent_t + { + uint32 m_unAccountID : 32; // unique account identifier + unsigned int m_unAccountInstance : 20; // dynamic instance ID + unsigned int m_EAccountType : 4; // type of account - can't show as EAccountType, due to signed / unsigned difference + EUniverse m_EUniverse : 8; // universe this account belongs to + } m_comp; + uint64 m_unAll64Bits; + } m_steamid; + +}; + struct CAppData { - void** fptr; + void** vfptr; AppId_t nAppID; uint32 ChangeNumber; uint32 LastChangeTimeStamp; @@ -326,25 +368,25 @@ struct CAppData bool IsUnresolvedAppInfo() const { return HasEmptyAppInfoSha() && !bSkipFlag; } - - std::string DebugString() const { - return std::format("AppID={} ChangeNumber={} LastChangeTimeStamp={} SkipFlag={} DeniedToken={} MissingToken={} AccessToken={}", - nAppID, ChangeNumber, LastChangeTimeStamp, bSkipFlag, bDeniedToken, bMissingToken, accessToken); - } }; #pragma pack(push,1) struct CSteamApp { - void** fptr; + void** vfptr; CGameID GameID; AppId_t nAppID; uint16 _unknown1; uint16 _unknown2; EAppReleaseState ReleaseState; EAppOwnershipFlags OwnershipFlags; - uint32 _unknown3; - uint64 SteamID; + EAppState AppStateFlags; + CSteamID SteamID; uint32 PurchasedTime; + uint32 ChangeNumber; + uint32 LicenseExpirationTime; + AppId_t MasterSubAppID; + uint32 eProtoAppType; + AppId_t ParentAppID; }; -#pragma pack(pop) \ No newline at end of file +#pragma pack(pop) diff --git a/src/proto/steam_messages.proto b/src/proto/steam_messages.proto index da3604c..941c2e3 100644 --- a/src/proto/steam_messages.proto +++ b/src/proto/steam_messages.proto @@ -321,11 +321,155 @@ message CMsgClientRichPresenceUpload { repeated fixed64 steamid_broadcast = 2; } +// ============================================================ +// CAppOverview +// ============================================================ +message CAppOverview { + optional uint32 appid = 1; + optional string display_name = 2; + optional bool visible_in_game_list = 4; + optional bool subscribed_to = 5; + optional string sort_as = 6; + optional EProtoAppType app_type = 7; + optional uint32 mru_index = 13; + optional uint32 rt_recent_activity_time = 14 [default = 0]; + optional uint32 minutes_playtime_forever = 16 [default = 0]; + optional uint32 minutes_playtime_last_two_weeks = 17 [default = 0]; + optional uint32 rt_last_time_played = 18 [default = 0]; + repeated uint32 store_tag = 19; + repeated uint32 store_category = 23; + optional uint32 rt_original_release_date = 25 [default = 0]; + optional uint32 rt_steam_release_date = 26 [default = 0]; + optional string icon_hash = 27; + optional EAppControllerSupportLevel xbox_controller_support = 31; + optional bool vr_supported = 32; + optional uint32 metacritic_score = 36; + optional uint64 size_on_disk = 37; + optional bool third_party_mod = 38; + optional string icon_data = 39; + optional string icon_data_format = 40; + optional string gameid = 41; + optional string library_capsule_filename = 42; + repeated CAppOverview_PerClientData per_client_data = 43; + optional uint64 most_available_clientid = 44 [default = 0]; + optional uint64 selected_clientid = 45 [default = 0]; + optional uint32 rt_store_asset_mtime = 46; + optional uint32 rt_custom_image_mtime = 47; + optional uint32 optional_parent_app_id = 48; + optional uint32 owner_account_id = 49; + optional uint32 review_score_with_bombs = 53 [default = 0]; + optional uint32 review_percentage_with_bombs = 54 [default = 0]; + optional uint32 review_score_without_bombs = 55 [default = 0]; + optional uint32 review_percentage_without_bombs = 56 [default = 0]; + optional string library_id = 57; + optional bool vr_only = 58; + optional uint32 mastersub_appid = 59; + optional string mastersub_includedwith_logo = 60; + optional string site_license_site_name = 62; + optional uint32 shortcut_override_appid = 63; + optional uint32 rt_last_time_locally_played = 65; + optional uint32 rt_purchased_time = 66; + optional string header_filename = 67; + optional uint32 local_cache_version = 68; + optional uint32 number_of_copies = 72 [default = 1]; + optional uint32 steam_hw_compat_category_packed = 73 [default = 0]; + optional string album_cover_hash = 74; + optional int32 display_name_elanguage = 75 [default = -1]; + optional bool has_custom_sort_as = 76; + optional uint64 bitfield_supported_languages = 77 [default = 0]; + repeated CAppOverview_PerClientData remote_per_client_data = 78; +} +enum EProtoAppType { + k_EAppTypeInvalid = 0; + k_EAppTypeGame = 1; + k_EAppTypeApplication = 2; + k_EAppTypeTool = 4; + k_EAppTypeDemo = 8; + k_EAppTypeDeprected = 16; + k_EAppTypeDLC = 32; + k_EAppTypeGuide = 64; + k_EAppTypeDriver = 128; + k_EAppTypeConfig = 256; + k_EAppTypeHardware = 512; + k_EAppTypeFranchise = 1024; + k_EAppTypeVideo = 2048; + k_EAppTypePlugin = 4096; + k_EAppTypeMusicAlbum = 8192; + k_EAppTypeSeries = 16384; + k_EAppTypeComic = 32768; + k_EAppTypeBeta = 65536; + k_EAppTypeShortcut = 1073741824; + k_EAppTypeDepotOnly = -2147483648; +} +enum EAppControllerSupportLevel { + k_EAppControllerSupportLevelNone = 0; + k_EAppControllerSupportLevelPartial = 1; + k_EAppControllerSupportLevelFull = 2; +} + // ============================================================ // CAppOverview_Change // ============================================================ message CAppOverview_Change { - repeated uint32 removed_appid = 2; - optional bool full_update = 3; - optional bool update_complete = 4; + repeated CAppOverview app_overview = 1; + repeated uint32 removed_appid = 2; + optional bool full_update = 3; + optional bool update_complete = 4; +} +// ============================================================ +// CAppOverview_PerClientData +// ============================================================ +message CAppOverview_PerClientData { + optional uint64 clientid = 1 [default = 0]; + optional string client_name = 2; + optional EDisplayStatus display_status = 3; + optional uint32 status_percentage = 4; + optional string active_beta = 5; + optional bool installed = 6; + optional bool streaming_to_local_client = 9; + optional bool is_available_on_current_platform = 10; + optional bool is_invalid_os_type = 11; + optional uint32 playtime_left = 12; + optional bool update_available_but_disabled_by_app = 14; } +enum EDisplayStatus { + k_EDisplayStatusInvalid = 0; + k_EDisplayStatusLaunching = 1; + k_EDisplayStatusUninstalling = 2; + k_EDisplayStatusInstalling = 3; + k_EDisplayStatusRunning = 4; + k_EDisplayStatusValidating = 5; + k_EDisplayStatusUpdating = 6; + k_EDisplayStatusDownloading = 7; + k_EDisplayStatusSynchronizing = 8; + k_EDisplayStatusReadyToInstall = 9; + k_EDisplayStatusReadyToPreload = 10; + k_EDisplayStatusReadyToLaunch = 11; + k_EDisplayStatusRegionRestricted = 12; + k_EDisplayStatusPresaleOnly = 13; + k_EDisplayStatusInvalidPlatform = 14; + k_EDisplayStatusPreloadComplete = 16; + k_EDisplayStatusBorrowerLocked = 17; + k_EDisplayStatusUpdatePaused = 18; + k_EDisplayStatusUpdateQueued = 19; + k_EDisplayStatusUpdateRequired = 20; + k_EDisplayStatusUpdateDisabled = 21; + k_EDisplayStatusDownloadPaused = 22; + k_EDisplayStatusDownloadQueued = 23; + k_EDisplayStatusDownloadRequired = 24; + k_EDisplayStatusDownloadDisabled = 25; + k_EDisplayStatusLicensePending = 26; + k_EDisplayStatusLicenseExpired = 27; + k_EDisplayStatusAvailForFree = 28; + k_EDisplayStatusAvailToBorrow = 29; + k_EDisplayStatusAvailGuestPass = 30; + k_EDisplayStatusPurchase = 31; + k_EDisplayStatusUnavailable = 32; + k_EDisplayStatusNotLaunchable = 33; + k_EDisplayStatusCloudError = 34; + k_EDisplayStatusCloudOutOfDate = 35; + k_EDisplayStatusTerminating = 36; + k_EDisplayStatusOwnerLocked = 37; + k_EDisplayStatusDownloadFailed = 38; + k_EDisplayStatusUpdateFailed = 39; +} \ No newline at end of file