Skip to content

Fix Steam depot filtering for unlicensed region/platform depots - #929

Merged
utkarshdalal merged 4 commits into
utkarshdalal:masterfrom
jeremybernstein:jb/fix-depot-filtering
Mar 27, 2026
Merged

Fix Steam depot filtering for unlicensed region/platform depots#929
utkarshdalal merged 4 commits into
utkarshdalal:masterfrom
jeremybernstein:jb/fix-depot-filtering

Conversation

@jeremybernstein

@jeremybernstein jeremybernstein commented Mar 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Filter depots against the user's package license (SteamLicense.depotIds) to exclude region depots the user has no access to
  • Parse and filter steamdeck=1 config key to prefer regular Windows depots over Deck-specific builds
  • Apply license filtering to library list size calculation (was previously using raw unfiltered depots)
  • Batch the license DB lookup for the library list to avoid 2×N per-game queries

Test plan

  • Clear local database, re-login, verify Black Desert / RE4 / Blue Prince show correct download sizes
  • Verify games with DLC still show correct sizes and DLC selection works
  • Verify games with no region variants are unaffected

Fixes #928


Summary by cubic

Centralized depot filtering to include only licensed base‑game depots and prefer non‑Deck Windows builds. Fixes #928, cuts inflated download sizes, and speeds up the library view.

  • Bug Fixes

    • Filter base‑game depots by the user’s package license; skip when the license is unknown; DLC depots are gated by DLC ownership.
    • Apply the same license‑aware filtering to library size totals (base game only).
    • Parse steamdeck=1 and drop Deck depots when a non‑Deck Windows depot exists (base game and DLC).
    • Accept manifests with size > 0 even when download = 0 to support older titles.
    • Two‑pass resolver derives 64‑bit and Deck/Windows preferences from eligible, licensed depots.
  • Performance

    • Batch license lookups for the library list via a single chunked DAO query (findLicenses) and a shared buildLicensedDepotMap().

Written for commit 1c49c30. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Per-depot Steam Deck flag added and propagated through selection and copying.
  • Improvements

    • License-aware filtering ensures only depots you own are offered and batch-loads licenses robustly.
    • Windows compatibility detection refined; stale-manifest checks tightened.
    • Two-stage depot resolution yields more accurate arch/Deck-aware selections.
    • Library size now computed from resolved, eligible depots.
  • Tests

    • Added tests for license gating and Steam Deck vs non-Deck selection.

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Steam Deck metadata to DepotInfo, adds chunked license queries in the DAO, builds licensed-depot maps in SteamService, enforces license- and Steam-Deck-aware depot filtering with tightened manifest checks, updates UI size calc to use filtered depots, and updates tests accordingly.

Changes

Cohort / File(s) Summary
Data model & parsing
app/src/main/java/app/gamenative/data/DepotInfo.kt, app/src/main/java/app/gamenative/utils/KeyValueUtils.kt
Added steamDeck: Boolean to DepotInfo, added computed isWindowsCompatible getter, and populate steamDeck from KeyValue parsing.
Database DAO
app/src/main/java/app/gamenative/db/dao/SteamLicenseDao.kt
Added internal Room query _findLicenses(packageIds: List<Int>) and public findLicenses(...) that returns early for empty input and queries packageIds in chunks (SQLITE_MAX_VARS).
Service / Filtering logic
app/src/main/java/app/gamenative/service/SteamService.kt
Added getLicensedDepotIds and buildLicensedDepotMap; introduced eligibleDepots and two-stage resolution; updated filterForDownloadableDepots to use isWindowsCompatible, apply optional license gating, enforce Steam Deck preference, and tighten stale-manifest rejection; propagate steamDeck when copying depots; adjust DLC handling per-DLC license sets.
UI & Tests
app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt, app/src/test/java/app/gamenative/service/SdCardDetectionTest.kt
Library size now sums only resolved/eligible depots; tests updated/added for license filtering and Steam Deck behavior; test helper accepts configurable depotId and steamDeck.

Sequence Diagram(s)

sequenceDiagram
    participant UI as LibraryViewModel
    participant Svc as SteamService
    participant DAO as SteamLicenseDao
    participant DB as Database

    UI->>Svc: request downloadable depots (apps, prefs)
    Svc->>DAO: findLicenses(packageIds)
    DAO->>DB: _findLicenses(packageIds chunk)
    DB-->>DAO: license rows
    DAO-->>Svc: aggregated license rows
    Svc->>Svc: buildLicensedDepotMap(apps)
    Svc->>Svc: compute eligibleDepots (manifests, language, arch)
    Svc->>Svc: resolveDownloadableDepots (apply isWindowsCompatible, steamDeck, license gating)
    Svc-->>UI: filtered depot list per app
    UI-->>UI: compute sizes from filtered depot manifests
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • phobos665

Poem

🐇
I hopped through manifests, nose to the byte,
Marked Deck and Windows by soft moonlight.
Licenses counted, extra depots shorn,
Now downloads hop home neat at dawn. 🎮

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: filtering depots against user licenses to exclude unlicensed region/platform depots.
Linked Issues check ✅ Passed All primary objectives from issue #928 are addressed: license-based depot filtering, Steam Deck depot handling, preference computation from eligible depots only, library size calculation fixes, and batch query optimization.
Out of Scope Changes check ✅ Passed All code changes directly support the linked issue objectives: license filtering, manifest logic, architecture/platform preferences, and batch queries. No unrelated changes detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 6 files

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/src/test/java/app/gamenative/service/SdCardDetectionTest.kt (1)

52-103: Add regression coverage for the new license/Deck branches.

These updates only re-thread the new boolean argument through the old manifest/shared-install cases. The actual fix in this PR lives in the licensedDepotIds and steamDeck && hasNonDeckWindows branches, so please add at least one licensed-vs-unlicensed case and one Deck-vs-regular-Windows case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/test/java/app/gamenative/service/SdCardDetectionTest.kt` around lines
52 - 103, Existing tests only exercise manifest/shared-install branches; add
unit tests calling SteamService.filterForDownloadableDepots that cover the
licensedDepotIds branch and the steamDeck vs hasNonDeckWindows behavior: (1) a
pair of tests that pass a depot id present in the licensedDepotIds set and one
not present to assert licensed depots are accepted/rejected appropriately,
referencing the licensedDepotIds behavior in SteamService; and (2) tests that
toggle the steamDeck boolean and the hasNonDeckWindows condition (simulate a
depot with Windows manifests and set the steamDeck flag true/false) to assert
Deck-specific logic vs regular-Windows logic in filterForDownloadableDepots.
Ensure tests create depots with the same manifest shapes used elsewhere in this
file so they exercise the new branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 735-737: Compute the arch/Deck preference flags from the
already-filtered eligible depots rather than from the raw depots map: after you
apply the license/language/manifest/DLC filtering that produces the eligible
depots collection used by getMainAppDepots() and getDownloadableDepots(), derive
has64Bit and hasNonDeckWindowsDepot (and any similar checks used in the blocks
around getMainAppDepots/getDownloadableDepots) from that eligible set (e.g.,
eligibleDepots.values.any { it.is64Bit } and eligibleDepots.values.any {
!it.steamDeck && it.isWindowsCompatible }) so an unlicensed or rejected depot
cannot suppress a valid installable depot. Ensure you replace uses of the
original depots map in functions/members like has64Bit and
hasNonDeckWindowsDepot with the filtered/eligible depot collection before making
decisions.

---

Nitpick comments:
In `@app/src/test/java/app/gamenative/service/SdCardDetectionTest.kt`:
- Around line 52-103: Existing tests only exercise manifest/shared-install
branches; add unit tests calling SteamService.filterForDownloadableDepots that
cover the licensedDepotIds branch and the steamDeck vs hasNonDeckWindows
behavior: (1) a pair of tests that pass a depot id present in the
licensedDepotIds set and one not present to assert licensed depots are
accepted/rejected appropriately, referencing the licensedDepotIds behavior in
SteamService; and (2) tests that toggle the steamDeck boolean and the
hasNonDeckWindows condition (simulate a depot with Windows manifests and set the
steamDeck flag true/false) to assert Deck-specific logic vs regular-Windows
logic in filterForDownloadableDepots. Ensure tests create depots with the same
manifest shapes used elsewhere in this file so they exercise the new branches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7e17973b-af45-4b67-8575-9027804b26b4

📥 Commits

Reviewing files that changed from the base of the PR and between 0a9fed5 and 6a505e5.

📒 Files selected for processing (6)
  • app/src/main/java/app/gamenative/data/DepotInfo.kt
  • app/src/main/java/app/gamenative/db/dao/SteamLicenseDao.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt
  • app/src/main/java/app/gamenative/utils/KeyValueUtils.kt
  • app/src/test/java/app/gamenative/service/SdCardDetectionTest.kt

Comment thread app/src/main/java/app/gamenative/service/SteamService.kt
Comment thread app/src/main/java/app/gamenative/service/SteamService.kt Outdated
@jeremybernstein
jeremybernstein force-pushed the jb/fix-depot-filtering branch from 6a505e5 to f9cc6f9 Compare March 19, 2026 17:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)

540-559: ⚠️ Potential issue | 🟠 Major

Don't collapse depot grants to one package.

getLicensedDepotIds() and buildLicensedDepotMap() still key off the single SteamApp.packageId, but the package-sync path later rewrites that field every time another owned package references the app. If an app is granted by multiple packages, the last writer wins and depots from the other packages disappear from both download filtering and library-size calculations.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 763-765: The code currently computes has64Bit and hasNonDeckWin
from the full eligibleDepots set, which can cancel out depots when one filter
influences the other; instead, first filter by architecture (compute the
arch-eligible set using eligibleDepots and then set has64Bit based on that set),
and only after that compute the Deck preference (hasNonDeckWin) from the already
arch-filtered collection; apply the same change to the DLC branch where you
compute arch eligibility for ownedDlc/licensedDepots so that arch choice is
applied before evaluating steamDeck/non-Deck compatibility (references:
eligibleDepots, has64Bit, hasNonDeckWin, ownedDlc, licensedDepots,
containerLanguage, appInfo.depots).

In `@app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt`:
- Around line 430-439: The totalSizeBytes computation is summing all licensed
depots and can overcount mutually-exclusive/variant depots; instead, use the
same depot-eligibility logic as SteamService.getDownloadableDepots to pick the
actual downloadable subset. Replace the current filter { licensedDepots == null
|| it.depotId in licensedDepots } and subsequent sum with a call to
SteamService.getDownloadableDepots(item, licensedDepots) (or the appropriate
overload) to produce the list of depots to sum, then compute sizes from those
depots using the existing manifest size resolution logic so the displayed size
matches actual downloadable size.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5bc025a8-194b-4783-9285-12685008d661

📥 Commits

Reviewing files that changed from the base of the PR and between 6a505e5 and f9cc6f9.

📒 Files selected for processing (6)
  • app/src/main/java/app/gamenative/data/DepotInfo.kt
  • app/src/main/java/app/gamenative/db/dao/SteamLicenseDao.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt
  • app/src/main/java/app/gamenative/utils/KeyValueUtils.kt
  • app/src/test/java/app/gamenative/service/SdCardDetectionTest.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/app/gamenative/utils/KeyValueUtils.kt
  • app/src/main/java/app/gamenative/db/dao/SteamLicenseDao.kt
  • app/src/main/java/app/gamenative/data/DepotInfo.kt

Comment thread app/src/main/java/app/gamenative/service/SteamService.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt Outdated
@jeremybernstein
jeremybernstein force-pushed the jb/fix-depot-filtering branch from f9cc6f9 to c28dbbd Compare March 19, 2026 18:06
@jeremybernstein

Copy link
Copy Markdown
Contributor Author

Note that I could only test Blue Prince. I believe that @utkarshdalal has RE4 for testing, though, so that should be verified in a local test pre-merge, as well. There was another game reporting TB(!) of storage necessary, we should check that if possible.

@jeremybernstein
jeremybernstein force-pushed the jb/fix-depot-filtering branch from c28dbbd to 66fa0b1 Compare March 20, 2026 09:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 807-814: The DLC filtering incorrectly reuses the base app's
has64Bit; compute a DLC-specific flag (e.g., val dlcHas64Bit = dlcEligible.any {
it.is64Bit } or similar based on the eligibleDepot model) after calling
eligibleDepots(dlcApp.depots, ...) and pass dlcHas64Bit into
filterForDownloadableDepots instead of the outer has64Bit; keep existing
dlcHasNonDeckWin and dlcLicensedDepots logic (from getLicensedDepotIds and
eligibleDepots) and replace the has64Bit argument in the
dlcApp.depots.asSequence().filter { ... filterForDownloadableDepots(...) } call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 682c2110-8bd6-4543-8fb6-7039222d89e6

📥 Commits

Reviewing files that changed from the base of the PR and between c28dbbd and 66fa0b1.

📒 Files selected for processing (6)
  • app/src/main/java/app/gamenative/data/DepotInfo.kt
  • app/src/main/java/app/gamenative/db/dao/SteamLicenseDao.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt
  • app/src/main/java/app/gamenative/utils/KeyValueUtils.kt
  • app/src/test/java/app/gamenative/service/SdCardDetectionTest.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/app/gamenative/db/dao/SteamLicenseDao.kt
  • app/src/main/java/app/gamenative/data/DepotInfo.kt
  • app/src/test/java/app/gamenative/service/SdCardDetectionTest.kt

Comment thread app/src/main/java/app/gamenative/service/SteamService.kt
Comment on lines +741 to +755
private fun eligibleDepots(
depots: Map<Int, DepotInfo>,
preferredLanguage: String,
ownedDlc: Map<Int, DepotInfo>?,
licensedDepotIds: Set<Int>?,
): Collection<DepotInfo> = depots.values.filter { depot ->
if (depot.manifests.isEmpty() && depot.encryptedManifests.isNotEmpty()) return@filter false
if (depot.manifests.isEmpty() && !depot.sharedInstall) return@filter false
if (depot.manifests.isNotEmpty() && depot.manifests.values.all { it.size == 0L && it.download == 0L }) return@filter false
if (!depot.isWindowsCompatible) return@filter false
if (depot.dlcAppId != INVALID_APP_ID && ownedDlc != null && !ownedDlc.containsKey(depot.depotId)) return@filter false
if (depot.language.isNotEmpty() && depot.language != preferredLanguage) return@filter false
if (licensedDepotIds != null && depot.depotId !in licensedDepotIds) return@filter false
true
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these the same conditions as what's in filterForDownloadableDepots?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept them separate to avoid a chicken/egg issue, but on second thought, I think I can combine them and use flags to get the appropriate behavior for callers.

return appInfo.depots.asSequence()
.filter { (depotId, depot) ->
return@filter filterForDownloadableDepots(depot, has64Bit, containerLanguage, ownedDlc)
return@filter filterForDownloadableDepots(depot, has64Bit, hasNonDeckWin, containerLanguage, ownedDlc, licensedDepots)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not seeing why this change is necessary since we seem to be doing the identical filtering in filterForDownloadableDepots?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

.asSequence()
.filter { (depotId, depot) ->
return@filter filterForDownloadableDepots(depot, has64Bit, preferredLanguage, ownedDlc)
return@filter filterForDownloadableDepots(depot, has64Bit, hasNonDeckWin, preferredLanguage, ownedDlc, licensedDepots)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

.asSequence()
.filter { (depotId, depot) ->
return@filter filterForDownloadableDepots(depot, has64Bit, preferredLanguage, null)
return@filter filterForDownloadableDepots(depot, has64Bit, dlcHasNonDeckWin, preferredLanguage, null, dlcLicensedDepots)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

Comment on lines +434 to +450
// mirror SteamService depot filtering: license, OS, arch, deck, base-game only
val licensedDepots = licensedDepotMap[item.id]
val eligible = item.depots.values.filter { d ->
d.isWindowsCompatible &&
d.dlcAppId == SteamService.INVALID_APP_ID &&
(licensedDepots == null || d.depotId in licensedDepots) &&
d.manifests.isNotEmpty()
}
val has64Bit = eligible.any { it.osArch == OSArch.Arch64 }
val hasNonDeckWin = eligible.any { !it.steamDeck && it.isWindowsCompatible }
val totalSizeBytes = eligible
.filter { d ->
(d.osArch == OSArch.Arch64 || d.osArch == OSArch.Unknown || (!has64Bit && d.osArch == OSArch.Arch32)) &&
!(d.steamDeck && hasNonDeckWin)
}
.sumOf { depot ->
depot.manifests["public"]?.size ?: depot.manifests.values.firstOrNull()?.size ?: 0L

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like repetition of the eligibleDepots logic? can we centralize it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I'll push a cleanup

language = currentDepot["config"]["language"].value.orEmpty(),
realm = currentDepot["config"]["realm"].value.orEmpty(),
optionalDlcId = currentDepot["config"]["optionaldlc"].asInteger(INVALID_APP_ID),
steamDeck = currentDepot["config"]["steamdeck"].asBoolean(),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sure to add a default here, as this won't get populated until someone logs out of steam and back in. Will break for users if there's a value expected here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this default to false if there's no value, but I'll add the explicit default.

@jeremybernstein
jeremybernstein force-pushed the jb/fix-depot-filtering branch from 66fa0b1 to 70be10c Compare March 21, 2026 10:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 777-778: The code is using the base app's licensedDepots for all
depots but appInfo.depots can include inline DLC depots that are licensed via
their DLC package; to fix, compute a per-depot license union before calling
resolveDownloadableDepots: iterate appInfo.depots and for any depot with
dlcAppId != INVALID_APP_ID call getLicensedDepotIds(dlcAppId) and union those
results with the base licensedDepots (or build a map of depotId -> licensed),
then pass that union (or the map) into resolveDownloadableDepots (or adjust
resolveDownloadableDepots signature to accept per-depot licenses); ensure
getMainAppDepots/getDownloadableDepots and downloadApp use the same per-depot
license union logic so inline DLC depots granted by ownedDlc are not filtered
out.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f3b85615-bd06-4b72-a7ed-8dbaeea78950

📥 Commits

Reviewing files that changed from the base of the PR and between 66fa0b1 and 70be10c.

📒 Files selected for processing (6)
  • app/src/main/java/app/gamenative/data/DepotInfo.kt
  • app/src/main/java/app/gamenative/db/dao/SteamLicenseDao.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt
  • app/src/main/java/app/gamenative/utils/KeyValueUtils.kt
  • app/src/test/java/app/gamenative/service/SdCardDetectionTest.kt
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/src/main/java/app/gamenative/utils/KeyValueUtils.kt
  • app/src/main/java/app/gamenative/db/dao/SteamLicenseDao.kt
  • app/src/main/java/app/gamenative/data/DepotInfo.kt
  • app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt
  • app/src/test/java/app/gamenative/service/SdCardDetectionTest.kt

Comment thread app/src/main/java/app/gamenative/service/SteamService.kt
@jeremybernstein
jeremybernstein force-pushed the jb/fix-depot-filtering branch from 70be10c to 36ff728 Compare March 21, 2026 10:30
@jeremybernstein

Copy link
Copy Markdown
Contributor Author

@utkarshdalal addressed concerns, I hope. Let me know if there's anything else to address.

Comment on lines +63 to +73
@Transaction
suspend fun findLicenses(packageIds: List<Int>): List<SteamLicense> {
if (packageIds.isEmpty()) return emptyList()
val out = mutableListOf<SteamLicense>()
for (i in packageIds.indices step SQLITE_MAX_VARS) {
val end = min(i + SQLITE_MAX_VARS, packageIds.size)
out += _findLicenses(packageIds.subList(i, end))
}
return out
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what I understand here is this is essentially iterating through here and getting all licences based on packages.

Would it be a bit easier on the eyes if this was a full SQL query with joins instead?

Maybe it's just the general naming and shorthands that's throwing off readability for me here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a hard limit on bind variables (the WHERE id IN (:list) clause creates these), so we have to do this in batches to avoid potential crashes (we could have a special branch for users with < 999 games, but... maybe overengineering IMO). I can improve the variable names to see if it's clearer, and add a comment explaining why.

* Common Filter for downloadable depots
* Common filter for downloadable depots.
*
* [has64Bit] and [hasNonDeckWindows] are exclusion flags, not selection

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's the case, could we not adjust to "exclude64Bit" and "excludeNonDeckWindows" as params? That way it's easier from an interfacing perspective to understand how these are used?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adjusting to prefer64Bit and preferNonDeckWindows which I think works, too?

Comment on lines +106 to +146
assertTrue(SteamService.filterForDownloadableDepots(d, true, false, "english", null))
}

// -- licensedDepotIds filtering --

@Test
fun `depot in licensed set passes`() {
val d = depot(depotId = 100, manifests = mapOf("public" to manifest()))
assertTrue(SteamService.filterForDownloadableDepots(d, true, false, "english", null, setOf(100, 200)))
}

@Test
fun `depot not in licensed set is rejected`() {
val d = depot(depotId = 100, manifests = mapOf("public" to manifest()))
assertFalse(SteamService.filterForDownloadableDepots(d, true, false, "english", null, setOf(200, 300)))
}

@Test
fun `null licensedDepotIds skips license check`() {
val d = depot(depotId = 100, manifests = mapOf("public" to manifest()))
assertTrue(SteamService.filterForDownloadableDepots(d, true, false, "english", null, null))
}

// -- Steam Deck depot filtering --

@Test
fun `deck depot rejected when non-deck windows depot exists`() {
val d = depot(manifests = mapOf("public" to manifest()), steamDeck = true)
assertFalse(SteamService.filterForDownloadableDepots(d, true, true, "english", null))
}

@Test
fun `deck depot passes when no non-deck windows depot exists`() {
val d = depot(manifests = mapOf("public" to manifest()), steamDeck = true)
assertTrue(SteamService.filterForDownloadableDepots(d, true, false, "english", null))
}

@Test
fun `non-deck depot passes regardless of hasNonDeckWindows`() {
val d = depot(manifests = mapOf("public" to manifest()), steamDeck = false)
assertTrue(SteamService.filterForDownloadableDepots(d, true, true, "english", null))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oddly, these tests are in the wrong test file to me.

in my opinion they should be in a SteamServiceTest file rather than sdcarddetection

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol. will fix, this was a rebasing error.

…sed region depots

games like Black Desert (582660) and RE4 (2050650) ship
multiple region/language depots with no distinguishing config
metadata. without checking the user's license, all depots pass
the OS/arch/language filters, inflating download size 5-10x.

adds getLicensedDepotIds() to cross-reference depot IDs against
the package's depotIds list from PICS. applied in both the
download filter and library list size calculation.

also excludes Steam Deck-specific depots (steamdeck=1) when a
regular Windows depot exists, since we run on Android not Deck.
single DB query for all package IDs instead of 2×N per-game
queries. adds findLicenses() batch DAO query with chunking,
buildLicensedDepotMap() bulk helper on SteamService.
- eligibleDepots now delegates to filterForDownloadableDepots with
  has64Bit=false, hasNonDeckWindows=false — these are exclusion flags,
  so false is permissive (all archs/deck states pass through), giving
  us the pre-filter pool without duplicating logic
- LibraryViewModel reuses eligibleDepots + filterForDownloadableDepots
  instead of inline copy
- steamDeck asBoolean(false) — already the default, made explicit.
  DepotInfo.steamDeck defaults to false so existing users without
  the field are unaffected (non-deck, which is correct)
@jeremybernstein
jeremybernstein force-pushed the jb/fix-depot-filtering branch from f5dc9fe to 1c49c30 Compare March 27, 2026 09:34
Comment on lines +79 to +96
val results = mutableListOf<SteamLicense>()
for (chunkStart in packageIds.indices step SQLITE_MAX_VARS) {
val chunkEnd = min(chunkStart + SQLITE_MAX_VARS, packageIds.size)
val chunkResult = _findStaleLicences(packageIds.subList(chunkStart, chunkEnd))
if (results.isEmpty()) {
results += chunkResult
} else {
out.retainAll(chunkResult) // Intersect to only keep entries in both lists
results.retainAll(chunkResult)
}
}
return out.distinct()
return results.distinct()
}

@Transaction
suspend fun deleteStaleLicenses(packageIds: List<Int>) {
for (i in packageIds.indices step SQLITE_MAX_VARS) {
val end = min(i + SQLITE_MAX_VARS, packageIds.size)
_deleteStaleLicenses(packageIds.subList(i, end))
for (chunkStart in packageIds.indices step SQLITE_MAX_VARS) {
val chunkEnd = min(chunkStart + SQLITE_MAX_VARS, packageIds.size)
_deleteStaleLicenses(packageIds.subList(chunkStart, chunkEnd))

@utkarshdalal utkarshdalal Mar 27, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as far as i can see these are just renamed variables right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct, @phobos665 suggested that the names were too opaque

Comment on lines +691 to +697
* Common filter for downloadable depots.
*
* [prefer64Bit] and [preferNonDeckWindows] are preference flags:
* `true` filters OUT the lesser variant (32-bit / Deck-only), while
* `false` is permissive and lets all architectures or Deck states through.
* [eligibleDepots] passes both as `false` to skip preference checks
* when computing the flags themselves.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can hardcode prefer64Bit and preferNonDeckWindows to true, there's no situation in which they'll be used tbh

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, because false, false is used deliberately in eligibleDepots():

  fun eligibleDepots(...): Collection<DepotInfo> = depots.values.filter { depot ->
      filterForDownloadableDepots(depot, prefer64Bit = false, preferNonDeckWindows = false, ...)
  }

This is the first pass: it computes the pool of depots that pass all checks except arch/Deck preference. From that pool, we then derive whether 64-bit or non-Deck depots exist. If we hardcoded both to true, this first pass would already filter out 32-bit and Deck depots, so we'd never know if those were the only options available.

For instance, a game ships only 32-bit depots. With a hardcoded prefer64Bit = true, eligibleDepots() would return nothing (with all 32-bit depots filtered out), then has64Bit would be false, but we'd have already lost the depot list.

The two-pass design exists exactly because these flags depend on the result of the first pass.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant hardcode them both to false

val archOk = when (depot.osArch) {
OSArch.Arch64, OSArch.Unknown -> true
OSArch.Arch32 -> !has64Bit
OSArch.Arch32 -> !prefer64Bit

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would be a problem - has64Bit was added because some games downloaded both 64 bit and 32 bit depots, which would break the game. axyz was an example I believe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just a variable rename: the exclusion logic remains the same -- if 64-bit depots exist, 32-bit depots are excluded.

// 6. Package grants this depot — prevents grabbing region depots the user has no license for.
// Skip for DLC depots: they're licensed via their own package, already validated by check 4.
if (depot.dlcAppId == INVALID_APP_ID && licensedDepotIds != null && depot.depotId !in licensedDepotIds)
return false

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this change necessary? from what i understand we just need to filter out depot.steamDeck, right? or have I misunderstood? Because I was pretty sure we were filtering out depots that we didn't have license to already.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the core fix! The license filtering is the whole point. The steamDeck filtering was a secondary addition, because we were erroneously also including those depots if a valid, non-steamDeck Windows depot was available.

Pre-PR behavior, there was no depot-level license check. The app checked if you owned the game (app-level license), but not which specific depots your package grants. That's the crux: different packages for the same game can grant different depot sets! So a Japanese regional package might include region-specific depots that a US package doesn't, and vice versa (this is the RE4 case, for example). Without this check, the downloader grabs all depots that passed the OS/arch/language filters, including ones the user's package doesn't actually allow, which would then (probably) fail during download or (worse) produce broken installs.

The existing check nr. 4 (DLC ownership) only validates DLC apps, not individual depot grants within a package.

if (depot.dlcAppId == INVALID_APP_ID && licensedDepotIds != null && depot.depotId !in licensedDepotIds)
return false
// 7. Prefer non-Steam-Deck depot when both exist (we're on Android, not Deck)
if (depot.steamDeck && preferNonDeckWindows)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe instead of filtering out steamDeck always, we do it if there are non steam deck depots?

I can imagine a case where a game only ships steam deck depots.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's what this does: preferNonDeckWindows is only true when non-Deck Windows depots exist in the eligible pool:

  val hasNonDeckWin = eligible.any { !it.steamDeck && it.isWindowsCompatible }

If a game only ships Deck depots, hasNonDeckWin is false, so preferNonDeckWindows is false, and the check is skipped, and Deck depots are permitted. It's the same two-pass filter as the 64-bit logic.

language = currentDepot["config"]["language"].value.orEmpty(),
realm = currentDepot["config"]["realm"].value.orEmpty(),
optionalDlcId = currentDepot["config"]["optionaldlc"].asInteger(INVALID_APP_ID),
steamDeck = currentDepot["config"]["steamdeck"].asBoolean(false),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be noted that this will be only populated once someone signs out of steam and back in (I believe)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PICS data is fetched every session, no? I think only the license data would require a new login. But we can note this either in the code or in the release note.

val map = appInfo.depots
.asSequence()
.filter { (depotId, depot) ->
return@filter filterForDownloadableDepots(depot, has64Bit, preferredLanguage, ownedDlc)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirming that the logic change here is required for the change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was changed to address a valid CR finding, IIRC. The old code computed has64Bit from the unfiltered depot map, then used that to filter. The new code computes it from the eligible pool (after all license/language/manifest/DLC checks).

This would matter if an unlicensed 64-bit depot exists. The old code would see it, set has64Bit = true, and suppress all 32-bit depots, potentially leaving nothing downloadable. The new code only considers depots the user actually has access to when deciding preferences. So this should be more accurate.

@utkarshdalal
utkarshdalal merged commit b3cd554 into utkarshdalal:master Mar 27, 2026
2 checks passed
@jeremybernstein
jeremybernstein deleted the jb/fix-depot-filtering branch March 27, 2026 21:00
@coderabbitai coderabbitai Bot mentioned this pull request May 3, 2026
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Steam depot filtering grabs unlicensed region/platform depots, inflating download sizes

3 participants