Skip to content

feat: add How Long To Beat stats strip to game pages#1282

Open
xXJSONDeruloXx wants to merge 13 commits into
utkarshdalal:masterfrom
xXJSONDeruloXx:feat/hltb-stats
Open

feat: add How Long To Beat stats strip to game pages#1282
xXJSONDeruloXx wants to merge 13 commits into
utkarshdalal:masterfrom
xXJSONDeruloXx:feat/hltb-stats

Conversation

@xXJSONDeruloXx
Copy link
Copy Markdown
Contributor

@xXJSONDeruloXx xXJSONDeruloXx commented Apr 23, 2026

Description

Adds HowLongToBeat (HLTB) completion time stats to every game page as a compact strip overlaid on the hero image, sitting directly above the Play button

Recording

Screenshot 2026-04-23 at 3 45 39 PM

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Add HowLongToBeat playtime stats to each game page as a compact strip above the Play button. Shows four time estimates and an external link; fetches on first view, caches for 12 hours, and opens the HLTB page safely.

  • New Features
    • UI: HltbHeroStrip over the hero with Main, Main+Extras, Completionist, All Styles, plus a link button; labels are localized; handles missing link handlers.
    • Data: Async fetch in BaseAppScreen via HltbService; fuzzy name matching with edit distance; rejects poor matches and zero-data entries; exposed as GameDisplayInfo.hltbStats.
    • Caching: Thread-safe in-memory + DataStore cache (12h TTL, 200-entry cap) via HltbCache/PrefManager.hltbCache.
    • Networking/Robustness & Tests: Init via okhttp3 forced to HTTP/1.1; search POST via HttpURLConnection; IO dispatcher, timeouts, cancellation propagation, single auth retry; unit and integration tests for normalization, formatting, matching, cache, and end-to-end behavior.

Written for commit a57235e. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Displays HowLongToBeat playtime stats in the game library (hero strip above actions) with a quick link to open a game's HLTB page.
    • Added localized labels for HLTB time categories and the view action.
  • Performance

    • Playtime results are cached and persisted (TTL and size-limited) to improve responsiveness and reduce repeated lookups.
  • Tests

    • Added unit and integration tests for HLTB lookup, normalization, formatting, and cache behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

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 HowLongToBeat integration: new HltbService with in-memory + persisted JSON cache, PrefManager.hltbCache persistence, UI plumbing to fetch/display HLTB stats in library screens, a new HltbHeroStrip composable, localized string resources, and unit & integration tests.

Changes

Cohort / File(s) Summary
Persistence
app/src/main/java/app/gamenative/PrefManager.kt
Added hltbCache DataStore-backed property storing HLTB JSON cache (default "{}").
HLTB Service & Cache
app/src/main/java/app/gamenative/utils/HltbService.kt
New service + Stats model and HltbCache object: token-auth calls to HLTB endpoints, result parsing, normalization + Levenshtein matching, retry-on-auth-refresh, 12h TTL persistent JSON cache persisted via PrefManager.hltbCache, and in-memory max-200 eviction.
UI Data Model
app/src/main/java/app/gamenative/ui/data/GameDisplayInfo.kt
Added optional hltbStats: HltbService.Stats? = null to carry HLTB playtime estimates.
UI Screens / Composables
app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt, app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt, app/src/main/java/app/gamenative/ui/screen/library/components/HltbHeroStrip.kt
BaseAppScreen launches async fetch of HLTB stats on name change and augments displayInfo; AppScreenContent renders HltbHeroStrip when present; new composable displays four stat columns and an optional external link.
Strings / Localization
app/src/main/res/values*/strings.xml
Added hltb_main_story, hltb_main_plus_extras, hltb_completionist, hltb_all_styles, hltb_view_on_hltb to base and many locale files.
Tests
app/src/test/java/app/gamenative/utils/HltbCacheTest.kt, app/src/test/java/app/gamenative/utils/HltbServiceTest.kt, app/src/test/java/app/gamenative/utils/HltbServiceIntegrationTest.kt
Added unit tests for cache behavior (TTL/cap/normalization), formatting/utility tests (normalize/levenshtein/formatHours), and an integration-style test using MockWebServer validating auth/search flow and caching.

Sequence Diagram(s)

sequenceDiagram
    participant UI as AppScreen UI
    participant BaseApp as BaseAppScreen
    participant Hltb as HltbService
    participant Mem as In-memory Cache
    participant Pref as PrefManager (persistent cache)
    participant API as HLTB API

    UI->>BaseApp: Render with game name
    BaseApp->>BaseApp: LaunchedEffect on name change
    BaseApp->>Hltb: getStats(name)

    Hltb->>Mem: check in-memory cache
    alt mem hit
        Mem-->>Hltb: return Stats
    else mem miss
        Hltb->>Pref: check persisted JSON cache
        alt persistent hit & TTL valid
            Pref-->>Hltb: return Stats
        else persistent miss
            Hltb->>API: GET /api/find/init (fetch tokens)
            API-->>Hltb: auth tokens
            Hltb->>API: POST /api/find (search with headers, name)
            API-->>Hltb: results array
            Hltb->>Hltb: normalize & select best match (Levenshtein)
            Hltb->>Mem: store result
            Hltb->>Pref: persist JSON with timestamp
            alt auth failure
                Hltb->>API: refresh token & retry
                API-->>Hltb: results or null
            end
        end
    end

    Hltb-->>BaseApp: Stats or null
    BaseApp->>UI: pass augmented displayInfo
    UI->>UI: render HltbHeroStrip if stats present
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • utkarshdalal

Poem

🐰
I hopped through tokens, leapt o'er the log,
I cached the hours in my tiny data bog.
Four columns of time on a glossy strip bright,
I tunneled the stats and brought them to light. 🎮✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.42% 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 title accurately summarizes the main feature added: a How Long To Beat stats strip on game pages, which is the primary focus of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is comprehensive, addresses the required template sections, includes a screenshot demonstrating the feature, and documents the change scope with clear implementation details.

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

@xXJSONDeruloXx xXJSONDeruloXx marked this pull request as ready for review April 23, 2026 19:46
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.

4 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:334">
P2: Implicit ACTION_VIEW launch is unguarded and can crash with ActivityNotFoundException when no handler exists.</violation>
</file>

<file name="app/src/main/java/app/gamenative/utils/HltbService.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/HltbService.kt:95">
P2: HttpURLConnection POST has no explicit connect/read timeout, so a hung network request can block indefinitely.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/utils/HltbService.kt:139">
P2: `getStats()` performs cache load/JSON decode synchronously on the caller context, which is Main at the UI call site, risking UI jank.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt:907">
P2: Broad `Exception` catch in `LaunchedEffect` swallows coroutine cancellation instead of rethrowing `CancellationException`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt Outdated
Comment thread app/src/main/java/app/gamenative/utils/HltbService.kt Outdated
Comment thread app/src/main/java/app/gamenative/utils/HltbService.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt Outdated
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: 2

🤖 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/utils/HltbService.kt`:
- Around line 137-147: getStats blocks the main thread because
HltbCache.get(name) synchronously reads PrefManager.hltbCache via runBlocking
and is invoked from a LaunchedEffect (BaseAppScreen); fix it by moving the whole
function body off the main thread: wrap the contents of suspend fun
getStats(name: String): Stats? in withContext(Dispatchers.IO) so HltbCache.get,
any PrefManager.hltbCache access, auth/fetchAuth and search calls run on the IO
dispatcher and then return the result back to the caller.
- Around line 95-110: The HttpURLConnection in search() (variable conn) lacks
connect/read timeouts and isn't guaranteed to be disconnected, which can hang or
leak resources; fix by setting conn.connectTimeout and conn.readTimeout to
sensible values before writing/connecting, and wrap the request/response
handling in a try/finally so conn.disconnect() is always called in the finally
block (keep the existing outputStream.use for writing and preserve the response
handling logic that reads conn.inputStream). Ensure you reference the conn
variable in search() and perform timeouts/disconnect on that same
HttpURLConnection instance.
🪄 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: 3afc37dc-cce1-437f-99fa-af4f99d2f3dc

📥 Commits

Reviewing files that changed from the base of the PR and between 2620dc7 and c2271db.

📒 Files selected for processing (6)
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/ui/data/GameDisplayInfo.kt
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt
  • app/src/main/java/app/gamenative/utils/HltbService.kt
  • app/src/main/res/values/strings.xml

Comment thread app/src/main/java/app/gamenative/utils/HltbService.kt Outdated
Comment thread app/src/main/java/app/gamenative/utils/HltbService.kt Outdated
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

🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/utils/HltbService.kt (1)

115-135: Consider not caching (or not displaying) zero-value matches.

If HLTB's search returns results but the best-match entry has all comp_* fields at 0 (game exists but no completion data submitted, or the fuzzy match picked a stub entry), secs() returns "--" for every category and that "empty" Stats is still written to cache for 12 hours. For games that genuinely exist in HLTB but haven't been queried long enough to get real data, or when the Levenshtein picker selects a wrong stub, users will see a strip of -- placeholders until the TTL expires.

Optional: skip caching (or skip returning) when all four values are zero, and/or require a Levenshtein distance threshold relative to norm.length before accepting the best match — otherwise any short/unrelated result will still be considered "best" regardless of how far off it is.

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

In `@app/src/main/java/app/gamenative/utils/HltbService.kt` around lines 115 -
135, The code is caching and returning a Stats object even when the chosen HLTB
match (the local variable best / g) has all comp_* fields equal to zero or when
the fuzzy match is a poor match; after you pick the best item in the loop that
uses normalize(...) and levenshtein(...), add a guard that reads
g.optLong("comp_main"), "comp_plus", "comp_100", and "comp_all" and if they are
all zero then skip creating/writing the Stats (do not cache or return it) or
return null/empty; additionally enforce a Levenshtein threshold (e.g. bestDist
<= max(1, norm.length * 0.3)) before accepting the match — if bestDist is too
large, treat as no reliable match and skip caching/returning; ensure this logic
is applied where Stats(...) is constructed and wherever caching is performed so
you don't store 12-hour placeholders for empty entries.
🤖 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/utils/HltbService.kt`:
- Around line 169-209: HltbCache is not thread-safe: protect mem, stamps, loaded
and the load/save sequence with a lock (e.g., a Kotlin Mutex or synchronized
block) so concurrent calls to get()/put() (and load()/save()) cannot mutate
LinkedHashMaps concurrently; move setting loaded = true to after successful
decode/population (inside load() after parsing loop) so a second thread won't
see loaded==true before data is populated; wrap accesses in get(name), put(name,
stats), load(), and save() with the same lock and ensure save() reads from the
guarded mem/stamps, using key(name) and PrefManager.hltbCache as before.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/utils/HltbService.kt`:
- Around line 115-135: The code is caching and returning a Stats object even
when the chosen HLTB match (the local variable best / g) has all comp_* fields
equal to zero or when the fuzzy match is a poor match; after you pick the best
item in the loop that uses normalize(...) and levenshtein(...), add a guard that
reads g.optLong("comp_main"), "comp_plus", "comp_100", and "comp_all" and if
they are all zero then skip creating/writing the Stats (do not cache or return
it) or return null/empty; additionally enforce a Levenshtein threshold (e.g.
bestDist <= max(1, norm.length * 0.3)) before accepting the match — if bestDist
is too large, treat as no reliable match and skip caching/returning; ensure this
logic is applied where Stats(...) is constructed and wherever caching is
performed so you don't store 12-hour placeholders for empty entries.
🪄 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: a201f55e-0420-4e76-b5c6-5ab76d9e1341

📥 Commits

Reviewing files that changed from the base of the PR and between c2271db and a84cb06.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt
  • app/src/main/java/app/gamenative/utils/HltbService.kt

Comment thread app/src/main/java/app/gamenative/utils/HltbService.kt
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.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/utils/HltbService.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/HltbService.kt:143">
P1: `HltbCache` is not thread-safe but is accessed from `Dispatchers.IO` (a multi-threaded dispatcher) here. The backing `mutableMapOf` collections (`mem`, `stamps`) are plain `LinkedHashMap` instances with no synchronization. Concurrent calls to `getStats` from multiple game screens will race on `get()`/`put()`, risking `ConcurrentModificationException` or silent data corruption.

Additionally, `load()` sets `loaded = true` *before* the decode/populate block runs, so a second thread entering `get()` while the first is still decoding will see `loaded == true`, skip loading, and read empty maps — causing unnecessary network fetches.

Wrap `HltbCache.get`, `put`, `load`, and `save` with `synchronized(lock)` (or use a `Mutex` for a coroutine-friendly approach), and move `loaded = true` to *after* the populate step completes.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread app/src/main/java/app/gamenative/utils/HltbService.kt
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.

Looks sensible, however there's a lot of parsing and transformations.

I'd recommend that you add some unit tests & an integration test with stubbed API responses so that this is proven to be handled correctly.

Comment on lines +120 to +127
for (i in 0 until data.length()) {
val item = data.getJSONObject(i)
val d = levenshtein(norm, normalize(item.optString("game_name")))
if (d < bestDist) { bestDist = d; best = item }
if (d == 0) break
}

val g = best
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.

Hate to pick nits, but could you please be more semantic with your naming? Thanks.

Comment on lines +169 to +170
object HltbCache {
private const val TTL = 12 * 3_600_000L
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.

What's the general amount of KBs used in-memory for HLTB entries per game?

Would be good for us to get a general amount and put in a limit in the cache to avoid potential OOM issues, especially on lower-end devices that have 4GB where we gotta be a bit brutal with memory usage.

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.

Like the other PRs, will be good to (for now) generate the other translations

Comment on lines +294 to +307
@Composable
private fun HltbHeroStrip(stats: app.gamenative.utils.HltbService.Stats) {
val context = LocalContext.current
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(Color.Black.copy(alpha = 0.45f))
.padding(horizontal = 8.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
listOf(
stringResource(R.string.hltb_main_story) to stats.mainHours,
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.

Could we pull this out to a composable component? Helps keep the size of LibraryAppScreen down.

We need to be a bit more modular in our UI work to help get things to flow easier. Lemme know if this is an arse to do.

.size(18.dp)
.clickable {
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse("https://howlongtobeat.com/game/${stats.gameId}"))
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.

I think it'd be easier to pull this URL out to a constant for the HLTB work

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: 3

🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/utils/HltbService.kt (1)

47-47: Consider making auth volatile for cross-thread visibility.

auth is read and written from multiple coroutines on Dispatchers.IO (read at line 156, assigned at lines 71 and 112) without synchronization. Reference writes are atomic on the JVM, but without @Volatile a write may not be promptly visible to other threads, leading to redundant fetchAuth() calls on the first concurrent game opens. Benign but trivial to harden.

♻️ Proposed refactor
-    private var auth: Auth? = null
+    `@Volatile` private var auth: Auth? = null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/utils/HltbService.kt` at line 47, The shared
mutable property auth in HltbService is accessed from multiple coroutines
without synchronization; mark the property as volatile by adding the Kotlin
`@Volatile` annotation to its declaration (i.e., change the declaration of auth to
`@Volatile` private var auth: Auth? = null) so writes in fetchAuth() and other
assignment sites are promptly visible to readers and avoid redundant concurrent
fetches.
🤖 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/ui/screen/library/components/HltbHeroStrip.kt`:
- Around line 64-77: The external-link Icon in HltbHeroStrip.kt (the branch
checking stats.gameId > 0 and the Icon with clickable modifier) needs two fixes:
wrap the context.startActivity(Intent(Intent.ACTION_VIEW,
Uri.parse("${HltbService.GAME_URL}${stats.gameId}"))) call in a try/catch that
catches ActivityNotFoundException and logs/ignores it to avoid crashes, and
increase the touch target to at least 48dp by using an IconButton or wrapping
the Icon in a Box/Modifier with minimumInteractiveComponentSize(48.dp) (or a
48.dp clickable Box) and add semantics role = Role.Button for accessibility so
the element is both safe and reachable.

In `@app/src/test/java/app/gamenative/utils/HltbCacheTest.kt`:
- Around line 83-96: The test put_evictsOldestWhenCapReached doesn't assert that
the oldest entry was removed; update the test in HltbCacheTest (function
put_evictsOldestWhenCapReached) to, after inserting "Overflow Game", assert that
HltbCache.get("Game 0") is null (or absent) and optionally assert the cache size
remains ≤ HltbCache.MAX_ENTRIES to confirm eviction occurred; keep the existing
assertNotNull(HltbCache.get("Overflow Game")) and use sampleStats as before.
- Around line 71-79: The test get_returnsNullAfterTtlExpires doesn't actually
exercise TTL expiry; update it so it either (A) is renamed to
get_returnsEntryWithinTtl and only asserts that HltbCache.put("Celeste",
sampleStats) + HltbCache.get("Celeste") returns non-null, or (B) modify
HltbCache to accept an injectable time provider (or clock) and in the test
simulate advancing time > TTL (the 12-hour TTL constant) then assert
HltbCache.get("Celeste") returns null; refer to HltbCache.put and HltbCache.get
and the TTL constant when making the changes.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/utils/HltbService.kt`:
- Line 47: The shared mutable property auth in HltbService is accessed from
multiple coroutines without synchronization; mark the property as volatile by
adding the Kotlin `@Volatile` annotation to its declaration (i.e., change the
declaration of auth to `@Volatile` private var auth: Auth? = null) so writes in
fetchAuth() and other assignment sites are promptly visible to readers and avoid
redundant concurrent fetches.
🪄 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: c1e3ceab-f6db-44ff-a36d-a8a408193309

📥 Commits

Reviewing files that changed from the base of the PR and between a84cb06 and 35d059b.

📒 Files selected for processing (18)
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/HltbHeroStrip.kt
  • app/src/main/java/app/gamenative/utils/HltbService.kt
  • app/src/main/res/values-da/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-ro/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/src/test/java/app/gamenative/utils/HltbCacheTest.kt
  • app/src/test/java/app/gamenative/utils/HltbServiceTest.kt
✅ Files skipped from review due to trivial changes (13)
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-da/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/src/main/res/values-ro/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt

Comment on lines +64 to +77
if (stats.gameId > 0) {
Icon(
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = stringResource(R.string.hltb_view_on_hltb),
tint = Color.White.copy(alpha = 0.7f),
modifier = Modifier
.size(18.dp)
.clickable {
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse("${HltbService.GAME_URL}${stats.gameId}"))
)
},
)
}
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.

⚠️ Potential issue | 🟡 Minor

Icon click risks crash and has an undersized touch target.

Two concerns on the external-link icon:

  1. context.startActivity(Intent.ACTION_VIEW, …) can throw ActivityNotFoundException on devices without a browser/handler (e.g., stripped-down Android TV images). Wrap the call in a try/catch so a missing handler doesn't crash the app.
  2. The clickable area is 18dp, well under Material's 48dp minimum touch target. Consider sizing the clickable region to ≥ 48dp (via minimumInteractiveComponentSize()/IconButton, or wrapping in a 48dp clickable Box) and adding a Role.Button semantic for accessibility.
♻️ Proposed refactor
+import androidx.compose.material3.IconButton
+import androidx.compose.ui.semantics.Role
+import timber.log.Timber
...
         if (stats.gameId > 0) {
-            Icon(
-                imageVector = Icons.AutoMirrored.Filled.OpenInNew,
-                contentDescription = stringResource(R.string.hltb_view_on_hltb),
-                tint = Color.White.copy(alpha = 0.7f),
-                modifier = Modifier
-                    .size(18.dp)
-                    .clickable {
-                        context.startActivity(
-                            Intent(Intent.ACTION_VIEW, Uri.parse("${HltbService.GAME_URL}${stats.gameId}"))
-                        )
-                    },
-            )
+            IconButton(
+                onClick = {
+                    val intent = Intent(Intent.ACTION_VIEW, Uri.parse("${HltbService.GAME_URL}${stats.gameId}"))
+                    try {
+                        context.startActivity(intent)
+                    } catch (e: android.content.ActivityNotFoundException) {
+                        Timber.tag("HLTB").w(e, "No activity to handle HLTB link")
+                    }
+                },
+            ) {
+                Icon(
+                    imageVector = Icons.AutoMirrored.Filled.OpenInNew,
+                    contentDescription = stringResource(R.string.hltb_view_on_hltb),
+                    tint = Color.White.copy(alpha = 0.7f),
+                    modifier = Modifier.size(18.dp),
+                )
+            }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (stats.gameId > 0) {
Icon(
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = stringResource(R.string.hltb_view_on_hltb),
tint = Color.White.copy(alpha = 0.7f),
modifier = Modifier
.size(18.dp)
.clickable {
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse("${HltbService.GAME_URL}${stats.gameId}"))
)
},
)
}
if (stats.gameId > 0) {
IconButton(
onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("${HltbService.GAME_URL}${stats.gameId}"))
try {
context.startActivity(intent)
} catch (e: android.content.ActivityNotFoundException) {
Timber.tag("HLTB").w(e, "No activity to handle HLTB link")
}
},
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = stringResource(R.string.hltb_view_on_hltb),
tint = Color.White.copy(alpha = 0.7f),
modifier = Modifier.size(18.dp),
)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/app/gamenative/ui/screen/library/components/HltbHeroStrip.kt`
around lines 64 - 77, The external-link Icon in HltbHeroStrip.kt (the branch
checking stats.gameId > 0 and the Icon with clickable modifier) needs two fixes:
wrap the context.startActivity(Intent(Intent.ACTION_VIEW,
Uri.parse("${HltbService.GAME_URL}${stats.gameId}"))) call in a try/catch that
catches ActivityNotFoundException and logs/ignores it to avoid crashes, and
increase the touch target to at least 48dp by using an IconButton or wrapping
the Icon in a Box/Modifier with minimumInteractiveComponentSize(48.dp) (or a
48.dp clickable Box) and add semantics role = Role.Button for accessibility so
the element is both safe and reachable.

Comment thread app/src/test/java/app/gamenative/utils/HltbCacheTest.kt Outdated
Comment thread app/src/test/java/app/gamenative/utils/HltbCacheTest.kt
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.

3 issues found across 18 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/utils/HltbService.kt">

<violation number="1" location="app/src/main/java/app/gamenative/utils/HltbService.kt:197">
P2: Cache size cap (`MAX_ENTRIES`) is not enforced during persisted cache load, allowing startup state to exceed the documented 200-entry limit.</violation>
</file>

<file name="app/src/test/java/app/gamenative/utils/HltbCacheTest.kt">

<violation number="1" location="app/src/test/java/app/gamenative/utils/HltbCacheTest.kt:72">
P3: Test name `get_returnsNullAfterTtlExpires` is misleading — it only asserts the entry exists immediately after `put` and never exercises expiry. Either rename to something like `get_returnsEntryWithinTtl` or inject a time provider so TTL expiration can actually be simulated; as-is this gives false confidence that TTL logic is covered.</violation>

<violation number="2" location="app/src/test/java/app/gamenative/utils/HltbCacheTest.kt:95">
P2: Eviction test doesn't actually verify eviction. After inserting the overflow entry, add `assertNull(HltbCache.get("Game 0"))` to confirm the oldest entry was evicted. Without this, a regression in eviction logic (wrong victim, or no eviction at all) would still pass.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread app/src/main/java/app/gamenative/utils/HltbService.kt Outdated
Comment thread app/src/test/java/app/gamenative/utils/HltbCacheTest.kt
// ── TTL eviction ─────────────────────────────────────────────────────────

@Test
fun get_returnsNullAfterTtlExpires() {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 23, 2026

Choose a reason for hiding this comment

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

P3: Test name get_returnsNullAfterTtlExpires is misleading — it only asserts the entry exists immediately after put and never exercises expiry. Either rename to something like get_returnsEntryWithinTtl or inject a time provider so TTL expiration can actually be simulated; as-is this gives false confidence that TTL logic is covered.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/test/java/app/gamenative/utils/HltbCacheTest.kt, line 72:

<comment>Test name `get_returnsNullAfterTtlExpires` is misleading — it only asserts the entry exists immediately after `put` and never exercises expiry. Either rename to something like `get_returnsEntryWithinTtl` or inject a time provider so TTL expiration can actually be simulated; as-is this gives false confidence that TTL logic is covered.</comment>

<file context>
@@ -0,0 +1,107 @@
+    // ── TTL eviction ─────────────────────────────────────────────────────────
+
+    @Test
+    fun get_returnsNullAfterTtlExpires() {
+        // Put with an artificially old timestamp by manipulating via put then expiring via reset trick:
+        // We can't set the stamp directly, so we verify via a fresh cache that
</file context>
Fix with Cubic

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.

🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/utils/HltbService.kt (2)

165-165: Rename secs for clarity.

The function takes seconds and formats hours, but the name (and parameter s) implies it works with seconds only. Given prior feedback on semantic naming, consider formatHours(seconds: Long) or secondsToHoursLabel(seconds: Long).

♻️ Proposed rename
-    internal fun secs(s: Long) = if (s <= 0) "--" else "%.1f".format(s / 3600.0)
+    internal fun formatHours(seconds: Long) =
+        if (seconds <= 0) "--" else "%.1f".format(seconds / 3600.0)

And update call sites at Lines 140–143 accordingly.

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

In `@app/src/main/java/app/gamenative/utils/HltbService.kt` at line 165, The
helper function secs(s: Long) is misnamed and unclear because it accepts seconds
and returns a formatted hours string; rename the function to a clearer name such
as formatHours(seconds: Long) or secondsToHoursLabel(seconds: Long), update its
parameter name from s to seconds, and change its signature and all call sites
that reference secs (e.g., the places currently calling secs(...) around the
HltbService usage) to use the new name and parameter. Keep the implementation
logic identical (return "--" when seconds <= 0 else format hours with
"%.1f".format(seconds / 3600.0)) and update any imports or references in
HltbService to avoid unresolved symbol errors.

166-167: Deduplicate normalize / key.

HltbService.normalize (Lines 166–167) and HltbCache.key (Lines 243–244) are byte-identical. If one ever changes without the other, cache lookups will silently diverge from search matching. Either have HltbCache.key delegate to HltbService.normalize, or extract a shared private helper.

♻️ Proposed fix
-    internal fun key(s: String) =
-        s.lowercase().replace(Regex("[^\\p{L}\\p{N}]"), " ").replace(Regex("\\s+"), " ").trim()
+    internal fun key(s: String) = HltbService.normalize(s)

Also applies to: 243-244

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

In `@app/src/main/java/app/gamenative/utils/HltbService.kt` around lines 166 -
167, HltbService.normalize and HltbCache.key are identical; to avoid future
divergence, remove the duplicate implementation by making HltbCache.key delegate
to the single canonical normalizer or extract a shared private helper used by
both. Concretely, replace the body of HltbCache.key with a call to
HltbService.normalize(...) (or move the regex logic into a private top-level
function like normalizedKey(...) and use that from both HltbService.normalize
and HltbCache.key), ensuring visibility allows the call and updating references
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/java/app/gamenative/utils/HltbService.kt`:
- Line 165: The helper function secs(s: Long) is misnamed and unclear because it
accepts seconds and returns a formatted hours string; rename the function to a
clearer name such as formatHours(seconds: Long) or secondsToHoursLabel(seconds:
Long), update its parameter name from s to seconds, and change its signature and
all call sites that reference secs (e.g., the places currently calling secs(...)
around the HltbService usage) to use the new name and parameter. Keep the
implementation logic identical (return "--" when seconds <= 0 else format hours
with "%.1f".format(seconds / 3600.0)) and update any imports or references in
HltbService to avoid unresolved symbol errors.
- Around line 166-167: HltbService.normalize and HltbCache.key are identical; to
avoid future divergence, remove the duplicate implementation by making
HltbCache.key delegate to the single canonical normalizer or extract a shared
private helper used by both. Concretely, replace the body of HltbCache.key with
a call to HltbService.normalize(...) (or move the regex logic into a private
top-level function like normalizedKey(...) and use that from both
HltbService.normalize and HltbCache.key), ensuring visibility allows the call
and updating references accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e94f197-12dd-41ec-b9f7-71d6b0f7580c

📥 Commits

Reviewing files that changed from the base of the PR and between 35d059b and def120d.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/ui/screen/library/components/HltbHeroStrip.kt
  • app/src/main/java/app/gamenative/utils/HltbService.kt
  • app/src/test/java/app/gamenative/utils/HltbCacheTest.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/test/java/app/gamenative/utils/HltbCacheTest.kt
  • app/src/main/java/app/gamenative/ui/screen/library/components/HltbHeroStrip.kt

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.

🧹 Nitpick comments (3)
app/src/main/java/app/gamenative/utils/HltbService.kt (2)

16-17: Pre-compile the normalization regexes.

normalizedKey allocates and compiles two Regex instances on every call. In search() this runs once per candidate (up to 20) plus once for the query, so 40+ compilations per HLTB lookup on the IO dispatcher. Hoisting them to file-level vals is a trivial win and common Kotlin practice.

♻️ Proposed refactor
-private fun normalizedKey(input: String) =
-    input.lowercase().replace(Regex("[^\\p{L}\\p{N}]"), " ").replace(Regex("\\s+"), " ").trim()
+private val NON_ALPHANUMERIC = Regex("[^\\p{L}\\p{N}]")
+private val WHITESPACE = Regex("\\s+")
+
+private fun normalizedKey(input: String) =
+    input.lowercase().replace(NON_ALPHANUMERIC, " ").replace(WHITESPACE, " ").trim()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/utils/HltbService.kt` around lines 16 - 17,
The normalizedKey function compiles two Regex instances on every call causing
unnecessary allocations; hoist the two Regex patterns (Regex("[^\\p{L}\\p{N}]")
and Regex("\\s+")) to file-level val constants and reuse them inside
normalizedKey so the function simply calls .replace with the precompiled regexes
and then .trim(); update references inside normalizedKey accordingly to
eliminate per-call compilation.

159-171: Clarify the auth == null retry sentinel.

The retry branch depends on search() having nulled the module-level auth to signal an HTTP-level auth rejection — a null firstAttempt due to exception, poor fuzzy match, or zero-value stub intentionally leaves auth set and short-circuits here. That control flow is subtle and easy to break in a future edit (e.g., adding auth = null to another error path would silently change retry semantics). A brief comment, or returning a sealed result from search() that distinguishes "auth invalidated" from "no match", would make the contract explicit.

💡 Minimal clarifying comment
     val firstAttempt = search(name, cachedAuth)
     val stats = firstAttempt ?: run {
-        if (auth != null) return@withContext null
+        // search() nulls `auth` only on HTTP-level auth rejection; any other null
+        // (no match / zero-value stub / exception) leaves `auth` set and must not retry.
+        if (auth != null) return@withContext null
         val refreshedAuth = fetchAuth() ?: return@withContext null
         search(name, refreshedAuth)
     } ?: return@withContext null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/utils/HltbService.kt` around lines 159 -
171, The code relies on a hidden sentinel where search(name) null means multiple
things (no match, error, or auth invalidation), making retry logic in getStats
brittle; change search(...) to return an explicit sealed result (e.g., sealed
class SearchResult { data class Found(val stats: Stats), object NoMatch, object
AuthInvalidated }) and update getStats to pattern-match that result: on Found
use the stats and cache it, on NoMatch return null immediately, and on
AuthInvalidated call fetchAuth(), update auth, re-run search and then proceed
only if Found; also update any callers of search and add a short comment on auth
field to document that AuthInvalidated is the only code path that triggers a
refresh.
app/src/test/java/app/gamenative/utils/HltbServiceIntegrationTest.kt (1)

84-99: Prefer deriving Origin/Referer from the server URL.

Hardcoding "http://localhost:${server.port}" assumes MockWebServer.url("/") always returns localhost. It usually does, but on hosts where IPv4 loopback resolves differently it can come back as 127.0.0.1, making this test environment-sensitive. Reusing the base URL the service was configured with keeps the assertion aligned with the actual header HltbService emits.

♻️ Proposed tweak
+        val baseUrl = server.url("/").toString().removeSuffix("/")
         val initRequest = server.takeRequest()
         assertEquals("GET", initRequest.method)
-        assertEquals("http://localhost:${server.port}", initRequest.getHeader("Origin"))
-        assertEquals("http://localhost:${server.port}/", initRequest.getHeader("Referer"))
+        assertEquals(baseUrl, initRequest.getHeader("Origin"))
+        assertEquals("$baseUrl/", initRequest.getHeader("Referer"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/test/java/app/gamenative/utils/HltbServiceIntegrationTest.kt` around
lines 84 - 99, The test currently hardcodes expected Origin/Referer values using
"http://localhost:${server.port}" which can mismatch MockWebServer's actual URL;
update the assertions that inspect initRequest.getHeader("Origin") and
initRequest.getHeader("Referer") to derive expectations from
server.url("/").toString() (use the URL without a trailing slash for the Origin
expectation and with a trailing slash for the Referer expectation) so the
assertions reflect the real base URL the HltbService was configured with.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/java/app/gamenative/utils/HltbService.kt`:
- Around line 16-17: The normalizedKey function compiles two Regex instances on
every call causing unnecessary allocations; hoist the two Regex patterns
(Regex("[^\\p{L}\\p{N}]") and Regex("\\s+")) to file-level val constants and
reuse them inside normalizedKey so the function simply calls .replace with the
precompiled regexes and then .trim(); update references inside normalizedKey
accordingly to eliminate per-call compilation.
- Around line 159-171: The code relies on a hidden sentinel where search(name)
null means multiple things (no match, error, or auth invalidation), making retry
logic in getStats brittle; change search(...) to return an explicit sealed
result (e.g., sealed class SearchResult { data class Found(val stats: Stats),
object NoMatch, object AuthInvalidated }) and update getStats to pattern-match
that result: on Found use the stats and cache it, on NoMatch return null
immediately, and on AuthInvalidated call fetchAuth(), update auth, re-run search
and then proceed only if Found; also update any callers of search and add a
short comment on auth field to document that AuthInvalidated is the only code
path that triggers a refresh.

In `@app/src/test/java/app/gamenative/utils/HltbServiceIntegrationTest.kt`:
- Around line 84-99: The test currently hardcodes expected Origin/Referer values
using "http://localhost:${server.port}" which can mismatch MockWebServer's
actual URL; update the assertions that inspect initRequest.getHeader("Origin")
and initRequest.getHeader("Referer") to derive expectations from
server.url("/").toString() (use the URL without a trailing slash for the Origin
expectation and with a trailing slash for the Referer expectation) so the
assertions reflect the real base URL the HltbService was configured with.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 755f5a8c-fb3e-45e8-a17b-b96cee74f207

📥 Commits

Reviewing files that changed from the base of the PR and between def120d and 0c99c2f.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/utils/HltbService.kt
  • app/src/test/java/app/gamenative/utils/HltbServiceIntegrationTest.kt
  • app/src/test/java/app/gamenative/utils/HltbServiceTest.kt

Comment on lines +54 to +58
private val httpClient = okhttp3.OkHttpClient.Builder()
.protocols(listOf(okhttp3.Protocol.HTTP_1_1))
.connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.build()
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.

I'd recommend using the HTTPClient from utils.

Comment on lines +104 to +105
conn.connectTimeout = 15_000
conn.readTimeout = 30_000
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.

Duplicate timeouts which is already defined at the top

Comment on lines +175 to +188
internal fun levenshtein(left: String, right: String): Int {
if (left == right) return 0
val dp = Array(left.length + 1) { IntArray(right.length + 1) }
for (leftIndex in 0..left.length) dp[leftIndex][0] = leftIndex
for (rightIndex in 0..right.length) dp[0][rightIndex] = rightIndex
for (leftIndex in 1..left.length) for (rightIndex in 1..right.length)
dp[leftIndex][rightIndex] = minOf(
dp[leftIndex - 1][rightIndex] + 1,
dp[leftIndex][rightIndex - 1] + 1,
dp[leftIndex - 1][rightIndex - 1] +
(if (left[leftIndex - 1] == right[rightIndex - 1]) 0 else 1),
)
return dp[left.length][right.length]
}
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.

Having a really hard time reading this here. What's the purpose of this function?

@phobos665
Copy link
Copy Markdown
Contributor

Some comments, also, would it not be a good idea to persist these into the DB over time?

I think each store's game having a persisted HLTB will help reduce the amount of queries we make over time to HLTB and we could then do filtering/sorting in a follow-up PR.

Thoughts?

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