A store's library shows from its saved copy at once and is refreshed in
the background at most every six hours, or from the account row's
Refresh. The result is merged so unchanged cards keep their objects:
only changed cards recompose, and the grid's scroll and the pad's focus
stay. Shelves saved on an earlier run count as fresh for three hours.
Sizes are kept per game and version in filesDir/stores/<store>/
sizes.json, format-versioned, and looked up again only for a new
version. An installed card shows its size on disk with a small green
check after it.
Cards are a quarter smaller: 112 dp least width for shelf cards and grid
cells, the grid fitting as many columns as the width allows, so a
density setting changes the column count instead of the card size.
Epic's and Amazon's library answers carry no size, so their cards
showed none. A card on screen, or close to it, now has its size looked
up once - Epic's from the build manifest (the files this device
installs), Amazon's from its download manifest - three at a time in the
background, and kept in the store's prefs where the library reads it.
The size sits at the right of the title strip in the title's own type;
nothing shows until it is known, and a marker keeps the slot.
The dropdown used the menus' right-aligned placement and their 220 dp
minimum, so under a tab near the left it was pushed to the edge over
the rail. AnchoredMenu has a compact mode now - under its anchor, left
edges aligned, as wide as its entries between the tab's width and
260 dp - which the Library dropdown uses.
The All tab's count mixed owned games with whatever the shelves had
loaded (GOG: Library 33, All 120). A store now has Store and Library;
Library's dropdown filters All - every owned game, the default and the
tab's count - or Installed, and the tab reads the filter. The old All
view is gone. Amazon has Library alone. Search follows the filter; the
shoulder buttons cycle Store and Library.
The sticky chip row painted the theme's flat background colour, a near
black box against the front end's tinted background. It paints nothing
now; the content scrolling beneath is clipped at the row's edge and,
once scrolled, faded out there through its own alpha rather than a
colour laid over it.
The row actions are round icon buttons with their labels as
descriptions: pause, resume, play, retry, and a cancel that asks with a
red delete and an undo. Downloads under way stay on top; finished ones
sit below a line whose X clears them all - rows only, a failed
download's kept files stay for its game page's Clear - with installed
games as a compact row with Play. Records the pre-PR device checks.
Import saves and the two Export rows lose their descriptive subtitles,
in Manage saves and Cloud saves alike; the rows that carry live values
keep them. The three strings are removed from every locale.
It opened with a Checking row and re-laid out a second later, shifting
every row and the pad's focus. The switch, Upload and Download are now
there from the first frame - the switch focused, its state read from
the sidecar; the two actions disabled with an ellipsis until the cloud
check is back - and only their subtitles change after. A conflict turns
Upload and Download into Keep local and Keep cloud in place.
A GOG or Epic game's hero button reads Cloud saves and opens its saves
view, which now holds the Cloud saves switch with the sync status,
Upload, Download and the conflict actions, or No cloud saves when the
store keeps none. The switch is gone from the cards: GOG has no card
left, Epic's keeps its sign-in, offline and Resolve actions. Steam and
Custom games keep Manage saves. Records the device proof of cloud saves
with ELDERBORN.
Cloud saves no longer depend on the compat tool's exit hook alone,
which never runs when the session is closed from the drawer. The
pre-launch step marks the game dirty in the app's storage, and the
upload runs on the first of the exit request, the end of the session
(after teardown) or the app's next start for a mark a killed app left,
not while a session runs. The mark goes only after an upload went
through or a final skip; each logs its trigger. The planner test runs
under Robolectric for org.json.
Downloads keep going with the screen off: while one runs the service
holds a Wi-Fi lock (high-performance before Android 14, low-latency
from it) and a partial wake lock taken with a timeout and renewed with
the progress, both released as soon as nothing runs. The notification
has a real progress bar and the Downloads row's line (game, percent,
sizes, speed, time left; the stage while installing), Pause / Resume,
and a Cancel that first asks Delete / Keep for ten seconds. A paused
download leaves a notice to resume from; an installed game posts a
notice that opens it in Games, a failed one opens Downloads. On
Android 13 and later notifications are asked for once, the first time
an install starts from a screen.
On device nothing ran for ELDERBORN: a GOG game without arguments has
no launcher .bat, Steam runs its exe, and droiddeck-store-launch only
looked for the .bat. It now recognises the exe too, looking through the
folders above it for the sidecar, and every path logs a result line.
A first launch has no prefix yet: the download is answered as deferred
and runs once Proton has made the prefix's user folder, within a minute,
without holding the launch.
Uploads are safe now. CloudPlan decides per file against this device's
baseline - the local and cloud state at the last sync: no upload before
a download has completed once, the first download lets the cloud win,
and a file changed on both sides is a conflict that Manage saves offers
to settle with Keep cloud or Keep local. The cloud copy is backed up
before an upload replaces it, as the local folder is before a download.
Wires the engine's new contract (0849a7c): the native start takes the
whole CDN set from the secure link, and the engine's periodic byte
progress - every 250 ms, files still in flight included - drives the
bar and the speed, so a large file no longer moves the bar in one jump.
The count shown adds what earlier runs and verified skips already
credited and never goes back.
GOG's secure link lists several CDN hosts and only the first was taken,
so every request went to one host. All are parsed now and kept as one
set, refreshed together when the link expires; the built-in pool takes
them in turn and the native engine gets the first until its contract
takes a list. The progress bar fill glides to each new value over a
quarter second, drawn without relayout.
Ported from Bannerlator's cloud-save managers. Before every launch the
cloud copy comes down where it is newer: droiddeck-store-launch asks the
app and waits up to 15 s. For a store game with cloud saves it tells the
compat tool so, and the tool waits for Proton instead of exec'ing it,
then asks for the upload without waiting. Newest wins per file, with an
MD5 check so a file the game only touched is not sent again; a download
first copies the folder to the app's storage (last three kept).
The save folder is GOG's remote-config location template (by the game's
Galaxy client id) or Epic's CloudSaveFolder catalog attribute, expanded
inside the game's Proton prefix or install folder by CloudSavePaths,
which refuses a path that leaves its boundary. GOG uploads with a token
issued to the game's own client (its secret is kept at install from now
on), Epic with signed write links. The GOG and Epic cards get a Cloud
saves switch kept in the sidecar; Manage saves shows the last sync with
Upload and Download, or No cloud saves.
Device-proven on bf81104: Metalstorm's EOS corrective action fell back to
a browser, the hand-off opened it in Android's browser, the user approved,
and the game signed in. With the overlay pointer set the game exited
while loading on every launch, so the overlay goes: no switch on the
Epic card, no download, no OverlayPath. A launch removes a pointer an
earlier build wrote, and the app deletes an earlier 656 MB download once;
an overlay field in an older sidecar is read past. The work is parked on
park/epic-eos-overlay. The browser hand-off, the Epic card's sign-in,
offline and Resolve actions, and BL_DEBUG_BROWSER stay.
A game launched from the Games tab with no session running sat at
Launching: the session put steam://rungameid on the client's command
line, the client took it seconds before its interface was up, and its
launch stopped waiting for a response to ShowInterstitials that nothing
gave. The same launch from inside Steam went through. The URL is now
held back and handed over through the in-session steam-game request once
gamescope's focus is the Steam interface and four seconds have passed,
or after two minutes regardless.
EOS's browser flow never reached Android because the hand-off script
was not in the APK: the build copies only usr/local/bin/droiddeck-*,
steam-compatibility and usr/bin/** of the overlay into the assets, so
usr/local/lib/droiddeck/browser/xdg-open was skipped, the session staged
nothing, the wrapper left the PATH alone, and Wine's winebrowser ran the
runtime's own xdg-open, which has no browser to open in a Steam session.
GE-Proton's winebrowser is Wine's own: its Browsers list, then xdg-open
by name, with no steam://openurl route.
The script is now usr/local/bin/droiddeck-open-url: named as Wine's
browser in the prefix by its absolute path, linked as xdg-open first on
the game's PATH, and reading the session channel from a file when Wine
does not pass BL_LAUNCH_DIR on. BL_DEBUG_BROWSER=1 adds Wine's shell and
winebrowser traces. The Epic card offers Resolve Epic sign-in, Epic's
account page behind its sign-in, where a pending corrective action is
presented.
With the overlay pointer set Metalstorm exits while loading; without it
the game signs in and runs. The overlay now defaults to off: the
sidecar's epic block carries a version, and a block from before it (the
old default wrote overlay=true unasked) reads as off, in the app and in
droiddeck-store-launch alike. The overlay is fetched only for a game that
turns it on.
The card's switches read the sidecar fresh, write it and read it back,
and show what was read back. droiddeck-store-launch waits for a
wineserver still holding the prefix before appending to user.reg - one
that exits rewrites the file and drops the line - reads the value back
and logs what the file holds. Its docstring is raw. The game-PATH
xdg-open logs each call as scheme://host only and accepts
steam://openurl.
Metalstorm's Player.log printed its environment, and with it the
exchange code: the launcher read the code into a variable the game
inherited. The variable (now with a plain name) is cleared on the same
line that starts the game - cmd has expanded it there already - so the
game gets the code as arguments and nothing of it in its environment.
A launcher from an earlier build is rewritten before the next Epic
launch, from the app and from the Steam-launch request alike.
Metalstorm's page showed its own name again under the hero, where the
store's description was just the title. Such a description is now no
description.
Epic games get an Epic card in the Games tab's launch settings: Epic
sign-in (EOS) on, Launch offline off, Epic overlay on. The choices live
in the game's sidecar and survive an update, so both launch paths read
the same thing - the app before its own launches and droiddeck-store-
launch for one from the Steam client: no code when sign-in is off or
offline, the overlay pointer written or removed per the switch.
Metalstorm's EOS sign-in asked for a one-time consent, could not load an
overlay to show it, and fell back to a browser flow that never finished.
Epic's own overlay component (the mechanism from Legendary's lfs/eos.py;
credits in EpicOverlay.kt) is now downloaded once with the account's
token into the runtime, after an Epic library sync or at the first
launch that finds it missing. At each launch droiddeck-store-launch
writes the one pointer, HKCU Software\Epic Games\EOS OverlayPath, into
the game's prefix, appended to user.reg as the DirectAudio key is, and
the app logs epic overlay installed=<build> prefix=<appid>.
Wine's browser is set to xdg-open, and on a game's PATH a small xdg-open
hands http(s) addresses to the app, which opens them in Android's
browser, so a device-code sign-in can be finished on the phone.
Metalstorm started from the Steam client's own Play button came up as a
guest: the exchange code was only minted when a launch began in the app.
The compat tool now runs droiddeck-store-launch on every real launch. It
finds the store launcher among Steam's arguments and, for an Epic game,
asks the app through the session's stores/req and stores/resp folders,
waiting up to five seconds. The app refreshes the token, mints the code
and writes it into the game's own folder; the code never crosses the
channel or the log. No answer and the game starts with its offline
identity; a code file older than five minutes is dropped first.
Every attempt logs epic launch id=... code=yes|no reason=...; a sign-in
that ran out dims the Epic chip and brings back the sign-in card.
Amazon has no public catalog, so its Store tab repeated the library row
for row and All was the library again. Its tabs are Installed and
Library; the open-on Store choice falls back to Library for it.
The shelves lose their subtitle lines. The chip row sits on the page's
background with room under it, and the content scrolling beneath is
clipped there, with a short fade where it meets the row.
The storefront's shelves and search leave out titles the store itself
rates or tags as adult unless Show mature content is on (Setup > Stores
and the cog). Library, Installed and owned games always show, and a title
without rating data stays. GOG's catalog gives ratings[].ageRating per
system and content tags; 17+ in any system or a mature / nsfw / sexual
content / nudity tag marks a title. Epic's store GraphQL carries no age
rating on an offer, so only an adult content tag would mark one.
Engines report per chunk or per file, and every report published a new
queue that the whole front end read - the rail badge was computed from
it in the root composition. Progress-only changes now reach the UI at
most four times a second (a stage or state change at once); each row and
card reads only its own entry; the list is replaced only when rows come,
go or change state; the badge count changes only when it changes; the
notification redraws at most once a second.
Download threads run at background priority - the queue's runner and
every manager's pool - and the tier leaves two cores for the UI. The
stores log is written in half-second batches.
The tab's count came from the installs on disk and its grid from the
library filtered by id, so two installs could read Installed (2) over an
empty grid. Both now come from installedCards: each install joined to its
library item by store id, or by title when the ids differ, else a card
made from the sidecar's own title and art.
A size of zero read as nothing ("Downloading 0% · / 1.1 GB"); it reads
0 B now, and small sizes in bytes. GOG and Amazon report the size on disk
from their manifests, shown beside the location as Epic's is. GOG writes
files in place while it downloads, so its Install stage has no file move
to count; it and Amazon's now count the finishing steps (the record and
launcher, the art, Steam) instead of sitting empty. GOG's plan line says
the window the tier gives the engine (Max = 96).
The Downloads page's engine log is gone. StoresState.logLine still
redacts every line and writes it to logcat, and now appends it to an
app-private file per day, filesDir/stores/logs/stores-<date>.log: seven
days kept (pruned as the app starts), each file rolling to .1 near 2 MB,
written in order on one background thread. The session's Share logs zip
carries these files under stores/, so they can be attached to a report.
StoresState.logLine is where every store line goes, the native engines'
included; it now passes the text through StoreLog.redactLine before the
Downloads log and logcat see it. Each URL keeps its scheme, host and a
plain first path segment only - a GOG secure link carries its token in
the path - with no query, fragment or userinfo; token-like values and
Authorization / Cookie headers are blanked outside URLs as well.
Epic's check of the files already there belongs to Manifest (Checking
n/N), not a Verify at 100% before anything is fetched. Install measures
its own write rate and ETA from the assembler's bytes and shows files
n/N. A row shows the download as fetched (compressed) against its total,
and the size on disk beside the location; Epic keeps the disk size for
the card and page.
Cancel asks twice and removes everything the install wrote - folder,
sidecar, chunk caches, scratch - unless the folder is a finished install
being repaired, which keeps itself; the row reads Cancelled for three
seconds, then goes. Pause keeps everything. A failed row reads Failed
with what is kept and offers Resume and Clear, Clear deleting as Cancel
does; the game page offers Resume install and Clear for an unfinished
install. Library sync is a thin bar and its count.
A 13 GB Epic install spent three minutes assembling behind a still bar.
The entry now carries the active stage's own count - an amount and, when
it counts files, the files - and which stages are already behind it. Epic
reports its verify pass and its assembly (native and the fallback loop),
GOG its chunk-cache removal. The Downloads row draws one segment per
stage, passed ones full and the active one filling, and reads e.g.
Installing 42% · 120/515 files; speed and ETA show only while
downloading, and leaving Download clears the measured speed. Cards and
the game page follow the active stage's percent.
A store card is its art edge to edge: sharp above, and under its one line
a blurred, darkened copy of the same picture placed to meet it without a
seam (blur on API 31+, a stronger scrim below). A Downloads row is the
game's wide art cropped to the row with a dark wash from the left, its
copy in white there and the actions on the right; the thumbnail is gone.
The Install dialog is one plain line, Install <game>:, over one row per
place (icon, name, free space), Cancel small beside the title.
A store card is its art and one line: the title, and right-aligned the
one value the tab does not already say - a check when installed, Resume,
the download percentage, the install size, or Free / price / discount -
with a thin bar over the art while downloading. The source chip, the In
library chip and the card's button are gone; every action lives on the
game page, which A on a card opens.
Setup > Stores and the cog popup keep only their control labels; the
download-controls caption, the Downloads footnote, the sign-in blurbs and
footnote, the engine note and the Install-to caption are gone, and empty
states are a few words. The Install-to dialog is the game's name over
"Install to" and the two tiles. A Custom game's hero has no eyebrow; its
chip says it.
Reverts 01c7248. The store and Steam glyphs are gone with their drawables;
the source chips are the text chips again (GOG, Epic, Amazon, Steam,
Custom) in the store colours, and the Stores chip row has its coloured
dots back, dimmed while signed out.
GOG, Epic Games, Amazon Games and Steam glyphs from Simple Icons (CC0) as
vector drawables, a folder-plus for a Custom game, each in a small rounded
square in the source's colours with the name as its description. Rows,
store cards and Downloads show the mark alone; the Games hero and a store
game's page show the mark and the name. The store chips atop the Stores
page carry the marks in place of the dots, dimmed while signed out.
A half-installed Epic game on the card (files up to a 0-byte one, 9.1 GB
of .chunks, no sidecar) showed in Games as a Custom folder: the sidecar
was only written at the very end, the queue's row lives in memory, and
the scan took any sidecar-less folder under a store root for a hand-added
game.
The sidecar is now written when an install starts, in state installing,
and rewritten finished at its end; a finished install being repaired
keeps its record. Folders still installing, or with no sidecar, are never
scanned as games; the Stores card and page offer Resume install, which
reuses the folder (and an Epic .chunks beside it) instead of a fresh one.
Writing the launcher or the finished record fails the download with its
message; the art and the Steam side stay best-effort.
The Epic cache holds whole chunk windows shared with files this device
does not install, so it outgrows the game; the free-space check now runs
after the delta pass and counts the missing chunks and files on their own
volumes.
The Install-to dialog is a 440 dp card with two tiles that are the
action, one line of caption and Cancel in the title row; the cog popup
is capped at the same width.
EpicNative.run passes chunkCacheDir after installDir, as the contract now
has it: "" keeps the cache beside the game, a path is the scratch cache in
the app's own storage that an install to a card uses. EpicNative.assemble
wraps the new nativeAssemble the same blocking way: the pending files are
written from the cache and each chunk goes right after its last use, then
the folder. The manager hands both runs the same path, uses the assembler
when it starts and keeps its own write loop as the fallback; the engine's
cache no longer has to sit beside a game on a card.
The default root for all three stores is the app's own storage, whatever
the session's Game storage says. With a card in the device, Install asks
where first - internal or the card, free space per target, pad-focusable,
last pick only as the dialog's default. A card that is not the Steam
library is bound at its own fixed place, so a game's guest path and appid
hold from one session to the next; a library card keeps the library's
bind and the installs already registered there keep their appid. Every
root is scanned and uninstall works for either.
The Downloads row carries the game's art as a small thumbnail and names
where the game is going; the game page names where it went. Epic installs
on a card keep their in-flight chunks in the app's cache (the Java pool
and the assembly; the engine's own cache stays beside the game until its
contract takes a cache path), cleaned on cancel and uninstall.
The engine's speed is a burst rate at the network side: on a card that
writes at 26 MB/s the fetchers wait on the writer, its windows jump between
0.4 and 20 MB/s while the bar advances at the write rate. The queue now
measures the shown speed from the byte deltas itself - an exponential
average over three seconds, sampled at most every quarter second - and
derives the ETA from it, for all three stores; the engine's figure stays
in its log lines and is stripped from the detail line under the bar.
The speed tier and downloads-at-a-time controls sit on one compact row
with a single caption, on the Downloads page and in the cog's popup.
The user's call: adding a store game to Steam is not optional. The setting
is gone from the cog, Setup and the prefs; the sidecar has no addToSteam
field (an older sidecar's is read past and ignored), the scan lists every
store install, and the game page shows a passive In Steam chip instead of
a button.
The Games hero's eyebrow for a never-played store game now reads Games
storage (the chip names the store) and Custom game for a hand-added
folder, in step with the Custom chip; Steam titles keep their label.
A card opening its page took the pad's focus with it, so the next press
landed on the rail. Each move on the page now says where focus goes:
the game page's main action (or its back link), the card the page was
opened from on the way back, the first card after a tab or chip change;
the page is a focus group, so Left is the way to the rail.
GOG's account endpoints return template keys (product_description_<id>)
for some products, and the v2 record repeats them. Store text now has
those keys dropped, tags stripped and entities decoded; the library
tries the product's lead, then the v2 overview and description, and shows
nothing when all are templates. Epic's and Amazon's text is cleaned too.
GOG's library drew the product's images.background, the store page's
fade-out backdrop, which ends in white on a card. The library now takes
the Galaxy library art from api.gog.com/v2/games (galaxyBackgroundImage,
boxArtImage), cached with the sync; the catalog's product-id filter was
tried and is ignored by the service. Epic cards prefer the DieselGameBox
images, Amazon the second background and never the logo. Cards draw over
a dark surface with a fade into the card at the foot.
On install the store's cover and hero are placed in the game folder as
opaque JPEGs under the names AddedGameArt looks for, so the Games tab and
the client's grid get real art for a store game.
A new Stores rail item (GOG, Epic Games, Amazon Games), off by default behind
Setup > Stores. The page is full width: four chips and a cog whose popup
holds the section's settings (which tab a store opens on, add installs to
Steam, speed tier); a signed-out store shows its sign-in card.
A store install is an added game: its folder sits under the Games storage
with a .droiddeck-store.json sidecar that AddedGames.scanGame reads for the
exe, the title and the source, so the existing listing -> shortcuts writer
path registers it in Steam; a running client gets it live over its DevTools
port. Games that need arguments or environment get a launcher .bat the
shortcut runs. Every Games row and hero now carries a source chip.
The download queue, its foreground service and the Downloads page are in
place for the store clients that follow.