Repository navigation
feat(games): custom games, game profile parity and custom game images - #1177
onyxhq-dev wants to merge 1 commit into
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
85d6a15 to
14b5232
Compare
| command: "Set the side button assignments", | ||
| progress: "Saving the side button assignments…", | ||
| preview: (status) => { status.hostButtons = structuredClone(buttons); }, | ||
| apply: async () => {}, |
There was a problem hiding this comment.
🟠 High device/controller.ts:5585
Clicking Apply now reports success without applying the selected side-button assignments, and the subsequent status refresh resets hostButtons to {}, losing the displayed draft. stageHostButtons supplies an empty apply, so flashStagedChanges has nothing to write; implement the Bridge-side write or keep this change pending instead of treating it as applied.
🤖 Copy this AI Prompt to have your agent fix this:
In file @src/device/controller.ts around line 5585:
Clicking Apply now reports success without applying the selected side-button assignments, and the subsequent status refresh resets `hostButtons` to `{}`, losing the displayed draft. `stageHostButtons` supplies an empty `apply`, so `flashStagedChanges` has nothing to write; implement the Bridge-side write or keep this change pending instead of treating it as applied.
| if (!restore) return; | ||
| recordDiagnosticCommand(`Game profile: restore ${JSON.stringify(restore)}`); | ||
| for (let attempt = 0; attempt < 5; attempt += 1) { | ||
| if (await flashGameProfileSnapshot(restore) !== "busy") return; |
There was a problem hiding this comment.
🟠 High device/controller.ts:1606
After switching from mouse A to mouse B, restoreGameProfileApply() writes A's saved settings to B, overwriting B while A remains on its temporary game settings. gameProfileRestore has no device identity, and flashGameProfileSnapshot() writes to the currently active device; associate the restore snapshot with its source device and restore it before switching targets.
Also found in 1 other location(s)
src/features/games/GameProfilePanel.tsx:190
The Apply-now restoration is tied only to panel unmount, not to the mouse it modified. Apply a draft on mouse A, select mouse B using the device list, then go Back:
selectTargetswitches the controller without restoring A, and this cleanup callsrestoreGameProfileApply()on B. The controller keeps one unkeyedgameProfileRestoreand flashes it through the currently active client, so A retains the temporary settings and B is overwritten with A's previous settings. Restore before switching targets and associate the restore state with the originating device.
🤖 Copy this AI Prompt to have your agent fix this:
In file @src/device/controller.ts around line 1606:
After switching from mouse A to mouse B, `restoreGameProfileApply()` writes A's saved settings to B, overwriting B while A remains on its temporary game settings. `gameProfileRestore` has no device identity, and `flashGameProfileSnapshot()` writes to the currently active device; associate the restore snapshot with its source device and restore it before switching targets.
Also found in 1 other location(s):
- src/features/games/GameProfilePanel.tsx:190 -- The Apply-now restoration is tied only to panel unmount, not to the mouse it modified. Apply a draft on mouse A, select mouse B using the device list, then go Back: `selectTarget` switches the controller without restoring A, and this cleanup calls `restoreGameProfileApply()` on B. The controller keeps one unkeyed `gameProfileRestore` and flashes it through the currently active client, so A retains the temporary settings and B is overwritten with A's previous settings. Restore before switching targets and associate the restore state with the originating device.
|
|
||
| /** Drops the saved values without writing them (the game is running and its profile now owns the mouse). */ | ||
| export function forgetGameProfileApply(): void { | ||
| gameProfileRestore = null; |
There was a problem hiding this comment.
🟠 High device/controller.ts:1613
When the game starts while this draft is open, forgetGameProfileApply() discards the original settings, so the game-profile applier later captures the previewed values as its baseline and restores those values when the game exits. Transfer the saved snapshot to the applier or restore it before the applier takes ownership.
Also found in 1 other location(s)
src/features/games/GameProfilePanel.tsx:189
Dropping the Apply-now restore snapshot solely because this game is active loses the original settings when the game starts while the editor is open.
useBridgeProfileAppliercannot apply/capture its baseline during a draft (flashGameProfileSnapshotreturnsbusy). For example, start at 800 DPI, apply 1600 here, launch the game, then leave this panel:forgetGameProfileApply()discards 800, and the applier subsequently captures 1600 as its baseline. Stopping the game then restores 1600 instead of 800. Transfer the saved baseline to the applier or restore it before the applier takes ownership.
🤖 Copy this AI Prompt to have your agent fix this:
In file @src/device/controller.ts around line 1613:
When the game starts while this draft is open, `forgetGameProfileApply()` discards the original settings, so the game-profile applier later captures the previewed values as its baseline and restores those values when the game exits. Transfer the saved snapshot to the applier or restore it before the applier takes ownership.
Also found in 1 other location(s):
- src/features/games/GameProfilePanel.tsx:189 -- Dropping the Apply-now restore snapshot solely because this game is active loses the original settings when the game starts while the editor is open. `useBridgeProfileApplier` cannot apply/capture its baseline during a draft (`flashGameProfileSnapshot` returns `busy`). For example, start at 800 DPI, apply 1600 here, launch the game, then leave this panel: `forgetGameProfileApply()` discards 800, and the applier subsequently captures 1600 as its baseline. Stopping the game then restores 1600 instead of 800. Transfer the saved baseline to the applier or restore it before the applier takes ownership.
| apply: async () => { | ||
| const client = logitechClient(); | ||
| if (!client) throw new Error(st("ctl.gone")); | ||
| if (lastDeviceMode !== "Host") { |
There was a problem hiding this comment.
🟠 High device/controller.ts:5615
When Apply now runs with an Onboard-mode game draft, this branch returns normally without calling setHostBunnyHopMs, so the staging flow treats the change as applied and drops it; the subsequent status refresh also restores the old value in gameProfileDraftSnapshot. Reject or retain the pending change, or switch to Host mode before applying it.
🤖 Copy this AI Prompt to have your agent fix this:
In file @src/device/controller.ts around line 5615:
When `Apply now` runs with an Onboard-mode game draft, this branch returns normally without calling `setHostBunnyHopMs`, so the staging flow treats the change as applied and drops it; the subsequent status refresh also restores the old value in `gameProfileDraftSnapshot`. Reject or retain the pending change, or switch to Host mode before applying it.
14b5232 to
fec697c
Compare
| const staged = hasPendingChanges(); | ||
| if (staged) await flashPendingChanges(); | ||
| restorePendingChanges(stash); | ||
| recordDiagnosticCommand(`Game profile: write ${Object.keys(snapshot).join(", ") || "nothing"} (staged ${staged})`); | ||
| if (staged) { | ||
| gameProfileWriting = true; | ||
| try { | ||
| await flashPendingChanges(); | ||
| } finally { | ||
| gameProfileWriting = false; | ||
| } | ||
| } | ||
| restoreEverything(stash); | ||
| return staged ? "written" : "unchanged"; |
There was a problem hiding this comment.
🟠 High device/controller.ts:1609
A failed restore write is reported as successful, so restoreGameProfileApply() deletes gameProfileRestores and never retries; the temporary Apply-now settings can remain on the mouse. flashGameProfileSnapshot() returns "written" whenever changes were staged, even if flashPendingChanges() leaves them pending. Check whether the staged changes cleared before returning success so the restore state is retained on failure.
const staged = hasPendingChanges();
+ let writeSucceeded = !staged;
recordDiagnosticCommand(`Game profile: write ${Object.keys(snapshot).join(", ") || "nothing"} (staged ${staged})`);
if (staged) {
gameProfileWriting = true;
try {
await flashPendingChanges();
+ writeSucceeded = !hasPendingChanges();
} finally {
gameProfileWriting = false;
}
}
restoreEverything(stash);
- return staged ? "written" : "unchanged";
+ return writeSucceeded ? (staged ? "written" : "unchanged") : "busy";🤖 Copy this AI Prompt to have your agent fix this:
In file @src/device/controller.ts around lines 1609-1620:
A failed restore write is reported as successful, so `restoreGameProfileApply()` deletes `gameProfileRestores` and never retries; the temporary Apply-now settings can remain on the mouse. `flashGameProfileSnapshot()` returns `"written"` whenever changes were staged, even if `flashPendingChanges()` leaves them pending. Check whether the staged changes cleared before returning success so the restore state is retained on failure.
| const held = gameProfileDraft?.held; | ||
| if (!held) return; | ||
| if (held.hostButtons) status.hostButtons = structuredClone(held.hostButtons); | ||
| if (held.hostBunnyHopMs !== undefined) status.hostBunnyHopMs = held.hostBunnyHopMs; |
There was a problem hiding this comment.
🟠 High device/controller.ts:1518
Host-mode bunny-hop edits are omitted from gameProfileDraftSnapshot(), so autosave removes the changed value from the saved game profile. overlayHeldValues puts hostBunnyHopMs into the base used by gameProfileFieldsOf, making the preview appear unchanged and preventing the field from being added to touched; track it before applying the overlay or otherwise preserve it as touched.
🤖 Copy this AI Prompt to have your agent fix this:
In file @src/device/controller.ts around line 1518:
Host-mode bunny-hop edits are omitted from `gameProfileDraftSnapshot()`, so autosave removes the changed value from the saved game profile. `overlayHeldValues` puts `hostBunnyHopMs` into the base used by `gameProfileFieldsOf`, making the preview appear unchanged and preventing the field from being added to `touched`; track it before applying the overlay or otherwise preserve it as touched.
fec697c to
993a64d
Compare
993a64d to
158539f
Compare
- Games page: add a game or an app by picking its executable or choosing a running app. Custom games are kept by Bridge, not in the browser. - Custom games can have a picture, uploaded or taken from a pasted link, shown on the tile and on the game's profile page. Hovering the picture offers "Change image" for custom games only. - Game profiles cover the Logitech settings of the device page: DPI X and Y, lift-off, report rate, bunny hop, HITS, Apply now and side button assignments, with Host profile parity and a default profile. - Strings are added to every locale. - A game profile keeps a report rate for wired and for wireless (like the onboard profiles), and applies the one for the connection in use.
158539f to
c81bd7c
Compare
Lets you add your own games to the Games page, and brings Logitech game profiles to the same level as the Host profile in G HUB. Needs OpenMouse Bridge for all of it.
Custom games
Game profiles
Checks
npx tsc --noEmit,npm test(408 tests) andnpm run buildpass. The JS budget is raised from 2,800,000 to 2,825,000 bytes (measured 2,823,553), with a note inbuild/check-bundle-size.ts, asAGENTS.mdasks.Draft because it uses
MouseStatus.pollingRateWirelessHz,pollingRateWiredHzandhostButtonsfrom OpenMouse-Project/mouse-protocol#246. Once that is released,@openmouse/protocolinpackage.jsonneeds bumping to it, and the type check will pass in CI. Bridge side: OpenMouse-Project/OpenMouse-Bridge#32.Note
Add custom games with images, game profile parity, and Host-button editing
prepareGameImageto at most 800px/450 KiB.buildProfileViewhides the normal profile entry, DPI slots, and bunny-hop support; analog-button writes during a profile operation are no longer persisted to the mouse; ordinary pending-change flashing stays blocked during draft editing while Apply Now uses the internal staged flasher.📊 Macroscope summarized 14b5232. 30 files reviewed, 24 issues evaluated, 8 issues filtered, 15 comments posted
🗂️ Filtered Issues
src/features/devices/cards/PerformanceCards.tsx — 0 comments posted, 1 evaluated, 1 filtered
snapshot.settingInProgress. Clicking a different rate while “Apply now” is flashing callsapplyGameProfilePollingRate, which updatesgameProfileDraft.ratesbeforestageChangerejects the change because a write is in progress. Consequently the slider andgameProfileDraftSnapshot()show the new rate, but the mouse receives the previous rate and no pending change remains to apply the new one. Disable these sliders during writes (as the onboard-profile sliders do), or guard before invoking the controller. [ Cross-file consolidated ]src/features/games/CustomGameDialog.tsx — 1 comment posted, 2 evaluated, 1 filtered
canAddignores pending image preparation/downloads. After clicking “Use link”, the user can immediately click Add;add()captures the currentimage(often null), saves the game and closes without waiting forfetchLink(). Its latersetImage(prepared)never uploads the chosen picture. File preparation has the same race. Track pending image work and await it or disable submission until it finishes. [ Exceeded comment limit ]src/features/games/GameProfilePanel.tsx — 0 comments posted, 3 evaluated, 3 filtered
useBridgeProfileAppliercannot apply/capture its baseline during a draft (flashGameProfileSnapshotreturnsbusy). For example, start at 800 DPI, apply 1600 here, launch the game, then leave this panel:forgetGameProfileApply()discards 800, and the applier subsequently captures 1600 as its baseline. Stopping the game then restores 1600 instead of 800. Transfer the saved baseline to the applier or restore it before the applier takes ownership. [ Cross-file consolidated ]selectTargetswitches the controller without restoring A, and this cleanup callsrestoreGameProfileApply()on B. The controller keeps one unkeyedgameProfileRestoreand flashes it through the currently active client, so A retains the temporary settings and B is overwritten with A's previous settings. Restore before switching targets and associate the restore state with the originating device. [ Cross-file consolidated ]pendingSave.currentand returns here whilerenaming.currentis true. IfonRenamelater returnsfailed(its network-error path), resetting the ref does not rerun the save effect, whose dependencies remain[currentKey, loaded]; unmount also has no pending save left to flush. Reschedule suppressed saves after renaming finishes, or prevent edits during the rename. [ Exceeded comment limit ]src/features/games/GamesPage.tsx — 0 comments posted, 3 evaluated, 3 filtered
/v1/custom-gamesnow hides even a successfully fetched games catalog.bridgeRequestthrows on non-OK responses, andPromise.allrejects when either input rejects (MDN, Promise.all), so the swallowed rejection leaves both lists empty on initial load.GamesPageuses that emptybridgeListwhenever Bridge is active, with no catalog fallback or retry untilactiveorversionchanges. Handle the requests independently or expose a recoverable loading error so a custom-games request failure does not disable all existing game-profile selection. [ Exceeded comment limit ]renameCustomGamecommits the custom-game rename before fetching/saving its profiles, but updates local state only after all those requests succeed. IfbridgeProfiles()orsaveBridgeProfiles()fails, the catch returns"failed"while the custom-game list has already been saved with the new name. The panel andcustomGamesstill use the old name, so retrying operates on stale entries and the saved profile can remain stranded under the old name. Reconcile or roll back the first write on failure, rather than treating the multi-write operation as atomic. [ Exceeded comment limit ]customGames. With games A and B, clicking both remove buttons before the refresh completes sends[B]followed by[A]throughsaveBridgeCustomGames; the second request includes the game just removed. Both promises can report success while one deletion is undone. Disable removals until the write/refetch completes, or serialize updates against the latest list. [ Exceeded comment limit ]