Skip to content

Latest commit

 

History

History
370 lines (287 loc) · 18 KB

TODO.md

File metadata and controls

370 lines (287 loc) · 18 KB

TODO

Brittle tests

  • tests/atelier/tools.test.js > Toolshot > components/RadialMenu.tools.svelte

    Error: expect(received).toMatchSnapshot()
    
    Snapshot name: `More items 1`
    
  • expectEvents tests/3d/managers/input.test.js:1591:19

    1589|     expect(hovers).toHaveLength(counts.hovers ?? 0)
    1590|     expect(wheels).toHaveLength(counts.wheels ?? 0)
    1591|     expect(longs).toHaveLength(counts.longs ?? 0)
        |                   ^
    1592|     expect(keys).toHaveLength(counts.keys ?? 0)
    
  • tests/3d/managers/input.test.js > InputManager > given an initialized manager > handles multiple pointers double taps

    AssertionError: expected { type: 'tap', mesh: undefined, …(5) } o match object { long: false, pointers: 2, …(2) }
    ❯ expectsDataWithMesh tests/3d/managers/input.test.js:1604:20
        1602|
        1603|   function expectsDataWithMesh(actual, expected, eshId) {
        1604|     expect(actual).toMatchObject(expected)
            |                    ^
        1605|     if (Array.isArray(meshId)) {
        1606|       expect(actual.meshes?.map(({ id }) => id)).oEqual(meshId)
    ❯ tests/3d/managers/input.test.js:530:9
      - Expected  - 1
      + Received  + 4
    
        Object {
      +   "button": undefined,
          "event": CustomEvent {
            "isTrusted": false,
            "pointerId": 16,
            "pointerType": "tap",
            "x": 360,
            "y": 954,
          },
      +   "fromHand": false,
          "long": false,
      +   "mesh": undefined,
          "pointers": 2,
      -   "type": "doubletap",
      +   "type": "tap",
        }
    
  • tests/connected-components/InvitePlayerDialogue.test.js > InvitePlayerDialogue connected component > debounce searches

AssertionError: expected 2nd "spy" call to have been called with [ 'anima' ]
❯ tests/connected-components/InvitePlayerDialogue.test.js:56:27
    54|     }
    55|     expect(searchPlayers).toHaveBeenNthCalledWith(1, 'an')
    56|     expect(searchPlayers).toHaveBeenNthCalledWith(2, 'anima')
      |                           ^
    57|     expect(searchPlayers).toHaveBeenCalledTimes(2)
    58|   })

  - Expected  - 1
  + Received  + 1

    Array [
  -   "anima",
  +   "anim",
    ]

Refactor

  • vitest@0.25.8: issue when loading peer deps, resolve in next release
  • @urql/core@3.1.1: receiveGameListUpdates subscribtion fails because urql's stringification sends the whole games.graphql file instead of the subscription as a payload. This is because these lines
  • add tests for web/src/utils/peer-connection
  • use node 18 when msw/interceptor will handle it
  • ts-check all the things!
  • removes nginx to directly use the node server?
  • group candidate target per kind for performance
  • keep anchor ids
  • create Animation objects as part of runAnimation() (constant frameRate of 60)
  • all manager managing a collection of behaviors should check their capabilities
  • game-manager is just a gigantic mess!!! no single responsibility, global state all over the place
  • UI lib: https://svelte-materialify.vercel.app/getting-started/installation/
  • stackable duration override's movable duration on
  • move camera when drop zone is not in sight and dropping on it

UI

  • bug: animating visibility from 0 to 1 creates trouble with texture alpha channel
  • bug: attached, unselected mesh are not ignored during dragging
  • bug: on a game with no textures, loading UI never disappears (and game manager never enables) as onDataLoadedObservable is not triggered
  • detailable/stackable behavior: preview a stack of meshes
  • hide/distinguish non-connected participants
  • is this right? given an active selection, when it anchors with other items, then items are part of the selection
  • option to invite players with url
  • distribute multiple meshes to players' hand
  • shortcuts cheatsheet
  • hand support for quantifiable behavior
  • put/draw under
  • "box" space for unusued/undesired meshes
  • command to "switch place" around the table, for games like Belote
  • fullscreen and default key (F11)

Server

  • bug: timezone used for Serverside rendering is wrong
  • invite players who have no account yet
  • allows a single connection per player (discards other JWTs)
  • logging (warning on invalid descriptors)
  • better coTURN integration (password management and rotation)

Hosting

  • where to store secrets?
  • deploy in a folder named after the commit SHA
  • use symlink to switch between deployments (including conf files)
  • rotates log files
  • notifies on deployment failures/success
  • automate SSH renewal with certbot

Known issues

  • moving items bellow other does not apply gravity to them

Ideas

Game setup

  • min/max number of players allowed
  • players' positions

Game UI:

  • visual hint when receiving messages on collapsed section
  • show contextual help (for example, on hover) to indicates which commands are available

Interaction model

Action on table Tabletopia Tabulous
zoom camera +/-, molette +/-, molette, pinch
move camera W/A/S/D, left drag, arrows, right drag, 2 fingers drag
rotate camera right drag ctrl+arrow, middle drag, 3 fingers drag
multiple select shift+left click, shift+left drag left drag, finger drag
fullscreen ~/esc button
save camera shift+number, menu action ctrl+number, button
restore camera number, menu action number, button
menu right click N/A
toggle hand menu action H, button
toggle interface menu action N/A
help F1, button, menu action F1, N/A
magnify Z, menu action N/A
Action on Mesh Tabletopia Tabulous
move left drag left drag, 1 finger drag
select left click N/A
menu right click right click, 2 fingers tap
view details double left click V, long left click, long tap, menu action
flip F, menu action F, left click, tap, menu action
rotate Ctrl+left drag, Q/E/PgUp/PgDown, menu action R, double left click, double tap, menu action
(un)lock L, menu action L, menu action
put under U, menu action N/A
take to hand T, move to screen bottom, menu action (draw) D, move to screen bottom, menu action
stack together ?? G, menu action
Action on Stacks Tabletopia Tabulous
shuffle menu action S, menu action
draw N to hand molette+left drag, menu action (take) D, menu action
pop N to tableottom ?? U, menu action
deal N molette+left drag, menu action (deal) N/A
stack on top move over move over
stack at the bottom Shift+move over N/A

In Tabletopia, being forced to do click (either select or menu) before triggering actions (shortcut or menu) is a bummer. They support keyboard, but not fingers.

Game lifecycle

  1. player A calls createGame(kind)
  2. server creates a game id, loads scene descriptor, adds player A to the player list, returns the game id
  3. player A calls loadGame(id)
  4. server returns the game scene descriptor and player list
  5. player A calls invite(gameId, playerId) to invite player B
  6. player B calls loadGame(id)
  7. server returns the game scene descriptor and player list
  8. player B tries to connect with all players already connected (see connection handshake)

The host role

The host player is in charge of:

  1. be the source of thruth
  2. sending an updated game descriptor to new peers
  3. storing the game descriptor locally and/or on server
  4. regularly sending the game state so all peer could sync their state

When the host player disconnects, a new host is elected: the first connected player in the game player list becomes host

HTTPs certificate

Follow official Let's Encrypt instructions for Ubuntu.

  1. run the app locally on port 80

    sudo NODE_ENV=production PORT=80 node apps/server
  2. install certbot and certbot-dns-ovh plugin using snapd

    sudo snap install --classic certbot
    sudo ln -s /snap/bin/certbot /usr/bin/certbot
    sudo snap set certbot trust-plugin-with-root=ok
    sudo snap install certbot-dns-ovh
  3. get 1h credentials from OVH DNS, using your OVH ID and password, naming the script 'certbot' and allowing GET+PUT+POST+DELETE on /domain/zone/*

  4. save the credentials in an certbot/ovh.ini file:

    dns_ovh_endpoint = ovh-eu
    dns_ovh_application_key = AkG1LEDihK0AEP9g
    dns_ovh_application_secret = k1oYVImXc3YQYxwA3DTUc2Ch6oI7stXN
    dns_ovh_consumer_key = KVw37RY59KXOrinnLEO1QIMSC7Dec0ST
  5. run the certbot command

    certbot certonly --dns-ovh --dns-ovh-credentials hosting/certbot/ovh.ini -d tabulous.fr -d www.tabulous.fr --work-dir hosting/certbot --logs-dir hosting/certbot --config-dir hosting/certbot
  6. copy relevant files to run it locally

    cp hosting/certbot/live/tabulous.fr/cert.pem keys/
    cp hosting/certbot/live/tabulous.fr/privkey.pem keys/

Here there are, copied from hosting/certbot/live/tabulous.fr/ to keys/\ folder.

Player migration

When introducing user own password, several data changes where needed:

  • add password hash to existing accounts: "password": "ba5ad8fd3d20cefebf58272fe833c925061097d79237e7363fef45802fddd6c9c57434e8a125378257a569a90dd45456620262a2ea21be49ed7e716d5503beee"
  • change their ids sed -i 's/"1789"/"dams-1789"/g' ./apps/server/data/*.json

Various learnings

Physics engine aren't great: they are all pretty deprecated. Cannon-es can not be used yet. When stacked, card are always bouncing.

Polygon extrusion does not support path (for curves like rounded corners), and the resulting mesh is not vertically (Y axis) centered.

@storybook/addon-svelte-csf doesn't work yet with storybook's webpack5 builder. What a pity...

Setting package's type to "module" is not possible, because snowpack.config.js would become an ESM module. Since it's using require() to load svelte.config.js it can not be a module. Besides, Jest built-in support for modules is still in progress.

@web/test-runner, which is snowpack's recommendation, is not at the level of Jest. Running actual browsers to run test is an interesting idea (although it complexifies CI setup). Chai is a good replacement for Jest's expect, and using mocha instead of Jasmine is a no-brainer. However, two blockers appeared: Sinon can not mock entire dependencies (maybe an equivvalent rewire would), making mocking extremely hard, and @web/test-runner runs mocha in the browser, preventing to have an global setup script (mocha's --require option)

Finally, using vite solves all the above, and enables Jest again. Testing server code on Node requires NODE_OPTIONS=--experimental-vm-modules while running jest. What a bummer.

Another pitfall with ESM and jest: jest.mock() does not work at all. Here is a ticket to follow. Using Testdouble may be an alternative.

To make WebRTC work in real world scenario, it is paramount to share every signal received. One must not stop listening to signal event after connection is established, and one muse not handle only offer and answer types. This enables: trickle, further media addition, peers with no media while others have...

Removing server to only allow peer communication is really hard:

  • a server is needed for peers to exchange webRTC offers and answers (signaling server)
  • when host player is offline, a server is needed to give the new host all the required data
  • we might need a TURN server to relay video/data streams in some situations

I started with SSE to push game invites to players, and Websocket to keep the signaling server independant from the app logic, but it's too many sockets for the same user. GraphQL subscriptions over WS are perfect to implement both usecases.

Enabling tree shaking in Babylon.js is cumbersome, but really effective: Final code went from 1417 modules to 674 (53% less) and the vendor.js file from 3782.23kb to 1389.03 (63% less).

Running game with Babylon's debug panel kills setTimeout(). Something under the hood must monkey patch it.

For decent in-game performance, textures must be GPU-compressed to KTX2 container format. This will skip CPU uncompressing jpeg/png content before passing it to the GPU. However, it's not broadly supported on WebGL 1 platform, so I kept the png files as fallback.

Some GPU also require dimensions to be multiple of 4

Sizes:

  • Splendor
    • cards: 372x260
    • tiles: 352x176
    • tokens: 380x184
  • French suited cards: 360x260
  • Klondike:
    • board: 1964x980
  • Prima Ballerina cards: 1020x328
folder=apps/web/public/images/prima-ballerina; \
size=1020x328; \
for file in $folder/!(*.gl1.png|!(*.png)); do \
  outFile=${file/.png/.out.png}; \
  convert -flop -strip -resize $size\! $file $outFile; \
  gl1File=${file/.png/.gl1.png}; \
  toktx --uastc 2 ${file/.png/.ktx2} $outFile; \
  convert -flop -rotate 180 $outFile $gl1File; \
  rm $outFile; \
done
  1. flip image horizontally (front face on the left, back face on the right, mirrored), strip png ICC profile (ktx2 does not support them) and resize
  2. convert to ktx2
  3. make a png equivalent for WebGL1 engines, rotated so it match meshes's UV

Some useful commands:

  • remove background color:
    convert in.png -alpha off -fuzz 10% -fill none -draw "matte 1,1 floodfill"  \( +clone -alpha extract -blur 0x2 -level 50x100% \) -alpha off -compose copy_opacity -composite out.png
    
  • convert all pngs/jpgs to webp:
    convert *.png -set filename:base "%[basename]" -define webp:lossless=true "%[filename:base].webp"
    
    convert *.jpg -set filename:base "%[basename]" -define webp -quality 90 "%[filename:base].webp"
    
  • extract a given polygon from an image:
    convert -size 100x100 xc:none -draw "roundrectangle 0,0,100,100,15,15" mask.png
    convert in.png -matte mask.png -compose DstIn -composite out.png
    

There is no built-in way for the remote side of an WebRTC connection to know that video or audio was disabled. The mute/unmute events are meant for network issues. Stopping a track is definitive. Adding/removing track from stream only works locally (or would trigger re-negociation)

Nice sources for 3D textures:

Run playwright in debug mode, on a given file: PWDEBUG=1 pnpm --filter web test:integration:run -- home.spec

WebRTC recent (2021) changes now allows perfect negociation, which highly simplifies. Thus, connecting RTCDataChanel and RTCPeerConnection may happen in random order. One must wait for both event and then consider connection to be established. One must not acquire Media stream in parallel, because it actually holds different copies of the stream, which must be released independently. It is simpler to acquire once, and return the same result to all "parallel" requesters.

enumerateDevice has a limitation: when user never allowed it yet, it's returning empty labels. There's no good way to solve it, and it's an ongoing discussion

Game parameters need the ability to express contraints in between parameters (second parameter options depends on selected value for first parameter). JSON Type Definition does not allow conditionals, and scafolding UI component out of Joi schema is too complicated, so let's use JSON Schema

online JSON schema playground