Skip to content

fix: don't let expired free weekend licenses shadow owned games#1148

Open
kiequoo wants to merge 1 commit intoutkarshdalal:masterfrom
kiequoo:fix/free-weekend-library-filter
Open

fix: don't let expired free weekend licenses shadow owned games#1148
kiequoo wants to merge 1 commit intoutkarshdalal:masterfrom
kiequoo:fix/free-weekend-library-filter

Conversation

@kiequoo
Copy link
Copy Markdown
Contributor

@kiequoo kiequoo commented Apr 8, 2026

Summary

I played Dying Light during a Steam free weekend, then later bought it. After purchasing, the game wasn't showing up in GameNative's library at all.

This is related to but distinct from the expired-license filtering work in #945, #982, and #985. Those PRs addressed expired free weekend games appearing in the library. This fixes the inverse: a game you genuinely own disappearing because the expired free weekend license shadows the real purchase.

Root cause: During PICS package processing, each package unconditionally overwrites the packageId on its associated SteamApp rows. If the expired free weekend package happened to be processed after the real purchase package, it clobbered packageId with the expired license's ID — making the game invisible to the owned apps query (which, post-#985, correctly filters out expired packages).

Fix: Before iterating packages, load the set of all expired package IDs. When updating an app's packageId, skip the write if the incoming package is expired but the app's current packageId is not — a real purchase already won, don't let an expired license shadow it.

  • SteamLicenseDao: new query to fetch expired package IDs (license_flags & 8 != 0)
  • SteamService: guard around packageId update — expired packages can only overwrite other expired packages

Summary by cubic

Fixes a bug where expired Steam free weekend licenses could hide games you own. During PICS updates, expired packages no longer overwrite a valid packageId, keeping purchased games visible in the library.

  • Bug Fixes
    • Added SteamLicenseDao.getExpiredPackageIds() to load expired package IDs (license_flags & 8 != 0) once per batch.
    • Updated SteamService to only let an expired package overwrite packageId if the existing one is also expired.

Written for commit 7c4b197. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed license handling to prevent temporary free-weekend licenses from incorrectly overwriting your permanent game purchases.

When a user played a free weekend and later purchased the game, both
packages contain the same app ID. PICS processing was overwriting
package_id unconditionally, so whichever package was processed last
won — if the expired free weekend package landed last, the owned-apps
query excluded the game because it only checked that one package_id.

Pre-load expired package IDs once per PICS transaction batch and skip
updating package_id when the incoming package is expired but the stored
one is already valid.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

A new DAO query method retrieves expired package IDs from the license table. The SteamService now preloads these IDs and uses them to conditionally update existing SteamApp rows during PICS package processing, avoiding overwriting non-expired package IDs with expired ones.

Changes

Cohort / File(s) Summary
License DAO Query
app/src/main/java/app/gamenative/db/dao/SteamLicenseDao.kt
Added getExpiredPackageIds() method to retrieve package IDs where (license_flags & 8) != 0.
PICS Package Processing
app/src/main/java/app/gamenative/service/SteamService.kt
Modified package handling logic to preload expired package IDs and conditionally update existing SteamApp rows only when the package ID is expired and the app's current package ID is also expired; otherwise skips the update.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐰 Expired licenses hop away,
No more shall real purchases stray,
We check the flags with gentle care,
And keep the gems already there! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description provides comprehensive context about the bug, root cause, and implementation details, but is missing the required checklist items from the template. Complete the checklist items (#code-changes discussion, recording attachment, and CONTRIBUTING.md agreement) as specified in the repository template.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main fix: preventing expired free weekend licenses from shadowing owned games in the library.

✏️ 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.

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

No issues found across 2 files

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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 3849-3866: The current update logic only skips updating when the
incoming package is expired, which still allows a temporary free-weekend package
to overwrite a valid purchased package; instead compute a stable precedence
between the existing SteamApp.packageId and the incoming pkg.id before calling
appDao.update: use licenseDao.getExpiredPackageIds() plus whatever
expiry/start/purchase metadata is available on picsCallback.packages[pkg.id] (or
a lookup via licenseDao) to determine which package is higher-priority (e.g.,
prefer non-expired over expired; if both non-expired prefer the package with the
later expiry or explicit purchase timestamp; if tied use deterministic
tie-breaker like package id), and only call
appDao.update(steamApp.copy(packageId = selectedPkgId)) when selectedPkgId !=
steamApp.packageId; reference picsCallback.packages,
licenseDao.getExpiredPackageIds(), appDao.findApp(), appDao.update(), and
SteamApp.packageId when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 313034e5-f5a4-43fa-ab24-30021b59dfd0

📥 Commits

Reviewing files that changed from the base of the PR and between 55c0796 and 7c4b197.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/db/dao/SteamLicenseDao.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt

Comment on lines +3849 to +3866
val expiredPkgIds = licenseDao.getExpiredPackageIds().toHashSet()
picsCallback.packages.values.forEach { pkg ->
val appIds = pkg.keyValues["appids"].children.map { it.asInteger() }
licenseDao.updateApps(pkg.id, appIds)

val depotIds = pkg.keyValues["depotids"].children.map { it.asInteger() }
licenseDao.updateDepots(pkg.id, depotIds)

val isPkgExpired = pkg.id in expiredPkgIds
// Insert a stub row (or update) of SteamApps to the database.
// Only update package_id with an expired package if the existing
// one is also expired — prevents free weekend licenses from
// shadowing a later real purchase of the same game.
appIds.forEach { appid ->
val steamApp = appDao.findApp(appid)?.copy(packageId = pkg.id)
val steamApp = appDao.findApp(appid)
if (steamApp != null) {
appDao.update(steamApp)
if (!isPkgExpired || steamApp.packageId in expiredPkgIds) {
appDao.update(steamApp.copy(packageId = pkg.id))
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 8, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This can still reproduce the disappearing-game bug if the purchase happens during the free weekend.

Line 3865 only protects against an incoming package that is already expired. If the active free-weekend package is processed after the purchase package, it still overwrites SteamApp.packageId; the game then disappears later when that weekend license flips to expired, because the owned-app query keys off that single packageId. This needs a stable precedence between multiple valid packages, not only a check once one of them is already expired.

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

In `@app/src/main/java/app/gamenative/service/SteamService.kt` around lines 3849 -
3866, The current update logic only skips updating when the incoming package is
expired, which still allows a temporary free-weekend package to overwrite a
valid purchased package; instead compute a stable precedence between the
existing SteamApp.packageId and the incoming pkg.id before calling
appDao.update: use licenseDao.getExpiredPackageIds() plus whatever
expiry/start/purchase metadata is available on picsCallback.packages[pkg.id] (or
a lookup via licenseDao) to determine which package is higher-priority (e.g.,
prefer non-expired over expired; if both non-expired prefer the package with the
later expiry or explicit purchase timestamp; if tied use deterministic
tie-breaker like package id), and only call
appDao.update(steamApp.copy(packageId = selectedPkgId)) when selectedPkgId !=
steamApp.packageId; reference picsCallback.packages,
licenseDao.getExpiredPackageIds(), appDao.findApp(), appDao.update(), and
SteamApp.packageId when making the change.

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.

@kiequoo did you see this?

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

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.

@kiequoo did you see this?

Only now, I'll look into it!

// shadowing a later real purchase of the same game.
appIds.forEach { appid ->
val steamApp = appDao.findApp(appid)?.copy(packageId = pkg.id)
val steamApp = appDao.findApp(appid)
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.

why was steamApp changed?

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.

2 participants