Skip to content

Deck Sharing

Pablo Martínez López edited this page Nov 4, 2026 · 2 revisions

Deck Sharing

English · Español

Moving decks between computers and phones, or to a friend, with no account and no server of our own: from the library, «Enviar / Recibir» sends one or several decks to another copy of the app, and «Compartir…» hands a .pbdeck file to any other app. Both honour the same two questions: with or without proxies, and what to do if the receiver already has that deck.

Source: src-tauri/src/pasar/ (protocolo.rs, encontrar.rs, enviar.rs, recibir.rs, imagenes.rs, tipos.rs, mod.rs), src-tauri/src/commands/pasar.rs, src-tauri/src/package.rs (import_con, export_en_fichero, resumen), src-tauri/src/store/linaje.rs, src-tauri/crates/proxy-core/src/compartir.rs, src-tauri/src/red/codigo.rs and red/nodo.rs, and on the screen src/features/pasar/ and src/lib/escaner.ts.

What it is (and is not)

  • Peer to peer over iroh, the same piece the Game Table uses. The sender dials the receiver's public key; the data is a .pbdeck (Deck Package Format) sent over a QUIC stream. There is no account, no upload and no server: nothing is stored anywhere but on the two computers.
  • Rust does everything. Finding each other, the protocol, building and checking the package, importing it. The screen (features/pasar/) only paints the state Rust emits (pasar://receptor, pasar://emisor, pasar://cerca) and sends the person's choices; cuentas.ts holds the pure rules of what must be chosen before continuing.
  • Bluetooth was ruled out on purpose. WebKitGTK has no Web Bluetooth, the Rust Bluetooth libraries behave badly on desktop (and a computer must be able to be the sender), and Bluetooth is very slow for several MB of images. iroh is also already in the app. The system's own Bluetooth stays available by hand through «Compartir…» (a file handed to another app).
  • Not a sync. A transfer is a one-off copy. «Is it the same deck?» is answered by the lineage, but nothing is merged: the receiver replaces or keeps both.

How the two computers find each other

flowchart TD
    R["Receiver opens «Recibir»<br/>(5 minutes)"] --> C["Code PBD1-… + QR<br/>(address with local IPs and relay + one-use secret)"]
    R --> M["Announces its nickname by mDNS<br/>(only if the network allows it)"]
    S["Sender opens «Enviar»"] --> L["Nearby computers list"]
    S --> P["Pastes the code or scans the QR"]
    M --> L
    L -->|"tap a computer: no secret"| A["Receiver compares the 6 digits<br/>and accepts"]
    P -->|"secret in the code"| Z["Straight in"]
    A --> X["Exchange"]
    Z --> X
Loading

There are three ways, all ending in the same protocol:

Way Where it works What proves it is the right computer
Nearby computers (encontrar.rs::Anuncio / Busqueda) Same local network Three things, all needed: the receiver is announcing on the local network (cerca), the connection arrives by a direct path to a local-network IP (camino_local, which waits up to 3 s, ESPERA_CAMINO, for iroh to find it), and the person accepts the 6-digit code both screens show (encontrar::codigo6: 20 bits of the SHA-256 of the two public keys, sorted, as six digits). The code is not a secret (keys are public): it lets two people check they reached the computer they meant, like Bluetooth pairing.
QR (qr_svg, generated in Rust as an SVG) Same network or far away The one-use secret inside the code. The sender scans it with the camera (mobile only, src/lib/escaner.ts, the only door to the scanner plugin) or pastes the text (on a computer it is always pasted).
Text code PBD1-… Same network or far away The same secret, copied by hand.
  • The code (red/codigo.rs) is the game's direct-code format (EndpointAddr + a 16-byte secret, base64url of the JSON) with another prefix: PBD1- for decks, PB1- for games. Why two prefixes: pasting a game code where a deck code goes (or the other way round) gives a clear error (motivo: codigo_de_partida / codigo_de_deck) and not a strange connection. The address carries up to six useful local IPs, the most likely first (encontrar::recortar: no loopback, link-local or unspecified ones; private IPv4, then other IPv4, then IPv6) plus the relay URL; more IPs would make a QR that cannot be read. When there is a relay, abrir waits up to 5 s for the node to be online so the code already carries it.
  • Far away. With the relay in the address, iroh goes direct on the same Wi-Fi and through the relay otherwise. The relay (n0's public ones today, the project's own plus n0's when RELE_PROPIO is set) only forwards bytes that are already encrypted between the two keys: QUIC with TLS 1.3 and ed25519 identities, so end to end. It cannot read or change a deck, and nobody needs an account. Inferred: a relay can see that two keys talk and how much, as any relay does.
  • Nearby computers use their own mDNS service (pbdeck, iroh-mdns-address-lookup), not iroh's. Why: only Proxy Builder copies that are waiting for a deck see each other, not every iroh app nor the nodes of a game, and nothing is added to the shared Endpoint that could not be removed (the announcement and the search are independent of it). The announcement carries the device's name as user data and is republished every 3 s in case an address changes, which is also how a new name reaches others without restarting anything (Anuncio::cambiar_apodo). That name is pasar::nombre_equipo: the one typed in «Recibir» (red.apodo, pasar_nombre_set) or, while empty, the system's (the computer's name without its domain; on Android the name given to the device or else make and model, android::nombre_equipo). A list full of «Equipo sin nombre» could not tell two computers apart, and only a copy older than this feature still shows it. On Android the Wi-Fi drops multicast unless a MulticastLock is held, so the app takes it while «Recibir» or the search is open (Entorno::multicast → android::multicast, the android-extra plugin) and releases it afterwards. The plugin has a single lock with no counter, so Rust counts the uses (UsoMulticast::Buscar and Recibir): the lock is taken with the first and released with the last, and closing «Recibir» does not leave a running search without it (Mobile). If mDNS cannot start, Anuncio::nuevo fails quietly (the screen says «Sin red local: solo con el QR o el código.») and the code still works.
  • Why the local path when there is no secret. The announcement only says «someone here is waiting», but the receiver's public key also travels in any old code, and iroh can dial a key from anywhere through the relay; without the check, anyone holding that key could pop up a 6-digit request. So a connection with no secret is accepted only if one of its paths goes straight to an IP of the local network (recibir::ip_de_la_red_local: private, link-local and loopback IPv4; IPv6 loopback, unique-local fc00::/7 and link-local fe80::/10; an IPv4-mapped IPv6 as its IPv4; and the shared space 100.64.0.0/10, used by CGNAT and by mesh VPNs such as Tailscale, which is not routed on the internet either). Otherwise it is rejected with codigo before anyone is asked.
  • The code is a password. It expires after 5 minutes (recibir::CADUCIDAD) and the secret is used once: the first connection that brings it burns it (r.secreto = None), compared in constant time (mismo_secreto). It also contains the receiver's IPs, so share it like the game's.

The protocol (pasar/protocolo.rs)

ALPN proxy-builder/deck/1 (the version is in the ALPN) on the same iroh node as the game (see Online Play). One bidirectional stream per exchange. Frames are a u32 big-endian length plus JSON, as in the game protocol; after a Paquete frame come exactly tamano raw bytes of the .pbdeck, with no frame.

sequenceDiagram
    participant E as Sender
    participant R as Receiver
    E->>R: Hola {version, secreto?, apodo}
    Note over R: secret valid, or a person accepts the 6 digits
    R-->>E: Listo {apodo} | Rechazo {motivo}
    E->>R: Oferta {decks: nombre, cartas, proxies, tamano, tamanoOriginales, linaje, actualizado, portada}
    Note over R: validates the offer, looks for decks of the same lineage, the person chooses
    R-->>E: Respuesta {decisiones: No | ConProxies | SoloOriginales, one per deck}
    loop each accepted deck
        E->>R: Paquete {indice, tamano, huella}
        E->>R: tamano bytes of the .pbdeck
        Note over R: checks size and SHA-256, then package::import_con
        R-->>E: Hecho {indice, ok, motivo?}
    end
    E->>R: Fin
    R-->>E: Adios
Loading
  • Limits, all checked before reading or trusting (protocolo.rs): a frame is at most 64 KiB (TOPE_TRAMA, the offer included), 32 decks per offer, a package at most 500 MB (MAX_PAQUETE), 2 GB the whole offer (MAX_TOTAL), a deck name 120 characters, a nickname 32, a cover 6 KiB of base64. If the offer does not fit with covers, the sender drops them (OFERTA_SIN_PORTADAS); if it still does not fit it fails with oferta_grande.
  • The sender checks the caps before sending, so the person reads why and not just «cancelled» when the receiver cuts: before dialling anyone, enviar::comprobar_tamanos rejects a deck whose estimated package (with or without proxies) is over MAX_PAQUETE (InvalidInput, motivo: paquete_grande) and an offer whose total is over MAX_TOTAL (envio_grande); an offer frame over TOPE_TRAMA gives oferta_grande; and after building each real package it checks again, because the estimate could fit and the real ZIP not (paquete_grande).
  • A hostile peer is assumed (the module's first rule). validar_oferta rejects an empty or oversized offer, names that are empty, too long or contain control characters, absurd card counts, sizes over the cap, a lineage that is not a short id ([A-Za-z0-9_-], ≤ 64) and a too-big cover. Nicknames are cleaned (apodo_limpio: no control characters, collapsed spaces). Covers are re-made locally (recibir::sanear_portada: base64 decoded, size read from the header before decoding anything, ≤ 512 px, re-encoded to a small JPEG by package::miniatura) so what the screen paints is always an image this app produced, never another computer's bytes. The receiver's decisions are checked too (plan_de: exactly one per deck, no repeats, a resolution for every accepted deck that already exists).
  • The package. The receiver streams it to a file in app_cache_dir/pasar/<uuid>/ (deleted when dropped, also if the exchange is cancelled and the task aborted), counts exactly tamano bytes, computes the SHA-256 while writing and compares it with huella; a mismatch answers Hecho {ok: false, motivo: "huella"} and the exchange is cut. Only then package::import_con runs, which distrusts everything again (zip-slip, per-image hash, falta_imagen...; see Deck Package Format).
  • The sender builds each package just before sending it (package::export_en_fichero, into the cache, an image at a time rather than the whole ZIP in memory) and deletes it after.
  • Timeouts (a stalled peer cannot hold the other forever): 10 s for the Hola, 60 s for the offer, 10 minutes to wait for a package (ESPERA_PAQUETE: the sender builds each package just before sending it and compressing can take minutes on a phone; if the sender goes away the connection closes sooner), 20 s for the end; the person has 15 minutes to decide, and 60 s to accept a 6-digit request (the sender waits up to 90 s for that, 30 s to connect and 30 s for Listo otherwise). While a package arrives, every read has 30 s to bring at least one byte (protocolo::PLAZO_SIN_BYTES, in recibir_bytes): a sender that stalls halfway does not keep «Recibir» busy forever, and the receiver ends with Network, motivo: tiempo, not «cancelled».
  • Rejections (Rechazo.motivo): version (different VERSION; both apps must be updated), codigo (wrong secret, or no secret from a receiver that is not announcing on the local network or over a path that is not a direct one to a local IP), caducado, rechazado (the person said no, or did not answer), ocupado (another exchange is in progress). A connection the receiver does not expect is closed with code CIERRE_NO_ESPERADO (40) without reading anything; the sender turns that into no_espera. CIERRE_CANCELADO (41) is a cancel and CIERRE_FIN (0) a normal end. A cut stream is read as «the other side cancelled».
  • A motivo from the other computer is never trusted as is. It is foreign text, so the sender maps it through a closed list: a Rechazo keeps version, codigo, caducado, rechazado or ocupado and anything else becomes rechazado; a failed Hecho keeps huella and anything else becomes importar (motivo_de_hecho, test el_motivo_de_fuera_va_por_una_lista_cerrada). The screen therefore only ever translates a reason it knows.
  • Network failures that are not HTTP (iroh, mDNS, a timeout, a rejection) are built with error::red_sin_http(motivo, causa), the sibling of error::red: Network with its motivo and, when there is an underlying error, its whole chain of causes written to proxy-builder.log and carried in detalle (IPC and Error Handling).
  • One exchange at a time on a receiver (Receptor.ocupado; reservar). A rejected attempt (Salida::Rechazo: bad code, wrong version) leaves the receiver waiting for whoever comes next, so a wrong code does not end the receiver's wait.

One node, one accept loop (pasar::ceder)

🛑 There is one iroh node per installation (Red::nodo()), the one the game uses, with ALPN_DECK among its ALPNs. Two nodes with the same key would step on each other at the relay, and a different key would show the other computer two identities. The node is created on first use and not closed when the exchange ends (the same as after a game); it only goes down with the app or when the network identity is replaced.

The problem: a node can only be accepted from one place, and two loops reading from the same endpoint would each get an arbitrary share of the connections. While a game or a room is open, its own loop already accepts (red/anfitrion.rs::atender, red/servidor/sala.rs::aceptar). So the rule:

  • Whoever accepts connections on the node calls pasar::ceder(ep.id()) and keeps the returned Guardia while it lives. Those loops send deck/1 to Mesa::atender_deck, which spawns pasar::atender. Any other ALPN is theirs.
  • The rest of the time, pasar/ runs its own loop (bucle, started by registrar when «Recibir» opens), which accepts only deck/1. 🛑 It lives only while «Recibir» is open: recibir::cerrar and recibir::cancelar call pasar::retirar, which aborts it and forgets how to start it. Why: while that loop lives nobody else accepts on the node, so a game connection arriving then would be cut.
  • Another ALPN in the own loop. With no online game, a connection of another ALPN is closed at once (CIERRE_NO_ESPERADO). With an online game or a room open (Entorno::partida_en_red, which is Red::ocupada), it is instead held for 10 s without an answer (RETENER): it may be another player looking for a new host (red/sucesion.rs), and closing it would tell them nobody is here. Held, it looks like a node that has not answered yet; the other side gives up that attempt and tries again, by then with the host's loop if this node became the host (ceder stops the own one).
  • REGISTRO (a static map by node id, Entrada) counts how many game loops hold the node (cedido: a host loop and a room loop can both be there) and whether «Recibir» wants its own loop (arrancar, set by registrar and cleared by retirar). Taking a Guardia aborts the own loop; dropping the last one restarts it only if «Recibir» is still open (reanudar, which needs arrancar). registrar does not restart a loop that already exists. With «Recibir» closed, a game just leaves a temporary note in REGISTRO that disappears with its Guardia, and there is nothing to restart.
  • atender closes at once (without reading) any deck/1 connection when «Recibir» is not open and waiting, so a stale code fails immediately («the other computer is not waiting for a deck») and not after a timeout.

A test (el_bucle_de_pasar_cede_el_nodo_a_la_partida_y_lo_recupera) checks there are never two loops, and pasar_un_deck_con_la_partida_abierta_no_la_corta (red/pruebas.rs) that receiving a deck during a game does not disturb it.

How a deck travels: optimized, original or without proxies

«Proxies» here means whatever the person made rather than Scryfall's (compartir::resumen_proxies: own images, designed cards, own backs, own default back). The sender chooses a package::Modo: «Optimizado» (the default), «Calidad original» or «Sin proxies»; the receiver then chooses per deck «Recibirlo» (as sent), «Solo cartas originales» (only if the deck carries proxies) or «No». The sender's choice is the ceiling: a receiver can ask for fewer proxies, never for more (SoloOriginales turns the deck's mode into SinProxies, never the other way round).

«Optimizado» exists because own images weigh far more than the deck needs: a fully designed deck of 110 cards was 520 MB, over the 500 MB cap, and is about 136 MB optimized. PNGs without transparency travel as JPEG at the same size; see Deck Package Format. The sender's own images do not change. The package is made in the cache during the phase FaseEmisor::Preparando, which shows progress by images and can be cancelled. The offer says DeckOfrecido.optimizado (absent from an older sender, hence #[serde(default)]), and choosing «Reemplazar el de aquí» for such a deck warns that the local originals may be lost: the deck that comes back from a friend must not silently downgrade the author's copy.

proxy_core::compartir::sin_proxies (pure, idempotent) rewrites a deck so the package is a fraction of the size:

Card Without proxies
Scryfall card or token Unchanged (a hand-picked own back becomes the deck's back).
Own card linked to Scryfall (and a designed one with a link) Becomes that Scryfall card, with no image and no design: the receiver downloads it.
Card from another source (Cockatrice tokens) Keeps its reference, no image; the receiver downloads it from there.
Own or designed card without a link A blank slot that keeps its name (meta.name), so the receiver knows which card it was.
Collections, own images, own default back Not in the package; the receiver's app back is used.

🛑 Every card keeps its id. The play lists (which point to card ids, the commander too), the chosen cover and the marks (DeckCard.marca) stay valid. The receiver's import also calls sin_proxies on what arrives, so «Solo cartas originales» works the same on a package that did bring proxies.

After importing «solo originales», the cards without an image (compartir::pendientes) are fetched by a job with progress and cancellation (pasar_completar, pasar/imagenes.rs): Scryfall ones by id with traer_carta, other sources with their own fetcher, completing each card in place (compartir::completar, which keeps id, mark and place) and saving the deck every 10 cards so a cancel loses nothing. If that deck is the one open, the screen lends it with prestarDeck(id, { conservar: true }) (features/pasar/imagenes.ts): what the person changes meanwhile is kept, and at the end it is mixed with what Rust downloaded (mezclarCartas: Rust's card where the person did not touch it, theirs where they did). The screen reports downloaded and failed images (pasar://imagenes-progress, pasar://imagenes-done). A failed image stays blank until looked up again. Known: if the deck is almost all Scryfall cards, «solo originales» saves little (in one trial, 92 MB became 89 MB).

Lineage: is it the same deck?

Every import creates new ids on purpose (package::import_con), so comparing ids does not work, and names do not identify a deck. Decks therefore carry a lineage (deck_linaje, see Storage and Blobs; store/linaje.rs), kept apart from the deck like the tags and play lists:

  • A new deck takes its own id as lineage (decks::save inserts it). A duplicate is another deck, so another lineage.
  • One that is imported or received keeps the lineage it came with (Destino::Nuevo).
  • A deck with no row (from before the migration) counts as having its own id as lineage (linaje::de).
  • actualizado is the time of the version's last save, on the computer where it was made (decks::save and decks::rename refresh it, nothing else touches it). It travels in Biblioteca.linaje/actualizado of the .pbdeck, so the offer can say which copy is newer. Inferred / known limit: it compares clocks of two different computers, which may not be in sync, and an autosave counts as an update, so «newer» is a hint, not proof.
  • It never changes updated_at, so organising or receiving does not reorder the library.
  • A lineage from outside is accepted only if it is a short id (linaje_sano), so a crafted package cannot smuggle odd text (test un_linaje_raro_de_fuera_se_ignora). A package from before lineage imports as a new deck.

When the deck already exists

flowchart TD
    O["Offered deck"] --> Q{"Same lineage as a deck here?<br/>(conflicto_en: the most recent of them)"}
    Q -- "no" --> N["New deck<br/>(keeps the lineage it came with)"]
    Q -- "yes" --> W["Dialog shows which is newer:<br/>Aqui · Llega · Igual · Desconocido"]
    W --> D{"Person chooses"}
    D -- "Reemplazar el de aquí" --> RE["Same deck here receives the content<br/>(Destino::Reemplazar)"]
    D -- "Guardar los dos" --> GD["A new, separate deck<br/>(own lineage, like a duplicate)"]
    D -- "No" --> NO["Nothing is received"]
Loading
  • A deck that matches must be resolved before the answer is sent (falta_resolucion otherwise; the screen only enables the button when every accepted existing deck has a resolution). The receiver checks again in Rust at import time (paquete_importar calls conflicto_en itself) and does not trust what the screen showed.
  • Replacing the deck that is open. Rust writes the deck in the database while the screen holds it in memory, and the autosave would write the old content over the new. So before answering, the screen lends each replaced deck to Rust with prestarDeck(id, { conservar: false }) (src/stores/deck-store.ts, see State Stores): it saves what is pending, the autosave waits while the loan lasts, and when the exchange ends (well or badly) the deck is read again from Rust, without history. Importing a .pbdeck with «Reemplazar el de aquí» does the same (importar-dialog.tsx).
  • «Reemplazar el de aquí» keeps what belongs to this computer: the deck's id, its name, its tags, favourite, notes, language override and game history (partidas points to the deck id). It takes from the incoming deck its cards (with new card ids), its settings and default back, the play lists in the package (the old ones pointed to card ids that no longer exist, so they are deleted) and the cover if it brings one (otherwise none). The lineage stays and actualizado becomes the incoming one, so both now read «same version». A game saved halfway should keep working: the saved game carries its own prepared decks (Storage and Blobs). Inferred: not retested with a replace.
  • «Guardar los dos» creates another deck (new id, its own lineage, renamed «… (importado)» if the name exists). If there were several local decks with the lineage, conflicto_en offers the most recent one.
  • The import is all or nothing per deck: if a later step fails, the new deck is deleted (import_con closes with that rule), and a replaced deck goes back to how it was: before touching it import_con keeps a Copia (the deck, its play lists, its cover and its lineage) and, if anything after saving the new content fails, Copia::restaurar puts them back. Why a copy and not a transaction: each step asks for the database connection on its own, so they do not fit in a single one. Cutting a transfer halfway leaves nothing half-written in the library; decks that had already arrived whole stay (each is complete).

Sharing and opening the .pbdeck

The reserve for when there is no common network or relay: the file through any app.

  • «Compartir…» in a deck's menu (compartir-dialog.tsx, deck_paquete_info first, to show the weight with and without proxies). On Android it writes the package to the cache and offers it through the system sheet (deck_paquete_compartir → compartir_fichero); on a computer it is «Guardar como…» (deck_paquete_guardar, which also accepts a content://). With several decks selected the dialog only offers sending to another computer.
  • Opening one (the library's «Importar .pbdeck», or on Android «Abrir con…» from Files, Downloads or a message, see Deck Package Format) always opens the same import dialog as receiving (importar-dialog.tsx): paquete_leer reads the manifest and cover without importing (package::resumen), shows the same choices (with or without proxies, replace or keep both) and paquete_importar imports with them.

The screen (src/features/pasar/)

«Enviar / Recibir» in the library bar (icon only on a phone) opens pasar-dialog.tsx, a dialog that cancels what is running when it closes. Tab «Enviar» (enviar.tsx): which decks, with or without proxies, and to whom (nearby list, scan a QR, or paste the code; the code field validates the prefix with pasar_codigo_comprobar). Tab «Recibir» (recibir.tsx): the QR and code with a countdown, «Código nuevo» when it expires, the accept request, the offer (oferta.tsx), the progress and a result per deck (piezas.tsx) with «Abrir». The stores and events are in estado.ts and store.ts; no decision lives in the screen. Errors are translated by motivo (cuentas.ts::MOTIVOS) to texts of the pasar area of the 15 dictionaries.

Commands (commands/pasar.rs)

pasar_recibir_abrir, pasar_recibir_estado, pasar_recibir_cerrar, pasar_recibir_cancelar, pasar_peticion (accept or refuse the 6-digit request), pasar_responder (the offer decisions), pasar_cerca_buscar / pasar_cerca_parar, pasar_codigo_comprobar, pasar_enviar, pasar_enviar_cancelar, pasar_emisor_estado, pasar_emisor_olvidar, pasar_completar (missing images), deck_paquete_info, deck_paquete_guardar, deck_paquete_compartir, paquete_leer and paquete_importar. States use ts-rs types; those with generic names carry #[ts(rename = "…Pasar")] (DecisionPasar, DestinoPasar, EleccionPasar, ProgresoPasar, RemotoPasar, ResolucionPasar, ResultadoPasar, ConflictoPasar, EquipoCerca) because ts-rs writes one file per name and would overwrite the game's Destino, Decision and Resultado.

The code is written against a small trait (pasar::Entorno, implemented by AppHandle) so the exchanges can be tested with two nodes in one process and no Tauri.

Edge cases and decisions

  • Sending to yourself (a code of the same node) fails with uno_mismo; a computer that left the nearby list fails with no_esta.
  • Only one send at a time (Busy, motivo: enviando); an offer takes 1–32 decks.
  • «Send more» and «Receive another». The screen follows Rust's events (pasar://emisor, pasar://receptor) with a hook that keeps the last event it received, and that last event is the "finished" one. Once an exchange is over, pressing «Send more» or «Receive another» has to go back to the start, but the hook would keep returning the old "finished" state and it would win over whatever Rust answers next (pasar_recibir_abrir returns the new state of the receiver). So the panel sets the last event aside (apartado in enviar.tsx and recibir.tsx): it ignores that exact event until a newer one arrives. «Send more» also tells Rust to forget the last send (pasar_emisor_olvidar); «Receive another» opens «Receive» again and drops the previous first state. Without this, both buttons looked dead.
  • Cancelling. Either side can cancel; the other sees it. What had already arrived whole is kept and shown in the results; the deck that was in flight does not exist.
  • Memory. Sending streams from a file, but the receiver's package::import_con reads every image of the package into memory before storing (the package page's known gap). Inferred: with a 500 MB package that is a lot for a modest phone.
  • Background (mobile). There is no logic to keep an exchange alive when the phone goes to the background; the connection may break and the exchange is read as cancelled. Receiving or sending is meant to be done with the app in front.
  • Android emulator. Its network does not pass multicast and the computer cannot call it, so between a computer and the emulator the emulator calls, or the relay is used; «nearby computers» between a computer and a phone is only really testable on a real Wi-Fi.
  • The offer's tamano is what the package would weigh as sent (package::tamano_estimado, the sum of the images, without building it; when optimized, an estimate by pixels, peso_jpeg, because knowing the real figure means compressing, and the real package is checked against the cap again) and tamanoOriginales what it would weigh without proxies.

Tests

  • pasar/pruebas.rs (real nodes in one process, no relay unless noted): three decks (one with proxies, one «solo originales», one refused); a one-use code and a bad one do not let anyone in; a node that does not expect a deck closes without reading; an existing deck asks, and «Reemplazar» keeps what is local; cutting halfway leaves nothing half-done; cancelling from the receiver stops the send; a thousand-deck offer is cut and the nickname arrives clean; a package larger than the cap or with another hash is cut and not imported; an optimized deck arrives in JPEG and the sender's stays as it was; the 6-digit code accepts someone from the local network; the code works through the relay with no direct addresses (el_codigo_vale_por_el_rele_sin_direcciones_directas, against n0's real relays, so it needs internet).
  • pasar/mod.rs: the ceder loop test. red/pruebas.rs: receiving a deck with a game open. red/nodo.rs: two nodes connect over deck/1.
  • package.rs (lineage round trip, «Guardar los dos», a strange lineage, replacing, a 3.2.3 package without lineage still imports, «solo originales» weighs less), store/linaje.rs, proxy-core::compartir (a deck of each card type keeps ids, play list and marks) and src/features/pasar/__tests__/.

Not verified

Written from the code and its tests; not exercised in the field at the time of writing: a computer ↔ phone transfer, scanning a QR with a real camera, «nearby computers» on a real phone, several MB through the relay from mobile data, and pasting a long code by hand in the GUI.

Where to start when extending

  • A new field in the offer: DeckOfrecido (tipos.rs), oferta_de (enviar.rs), its check in validar_oferta, and pnpm bindings. Anything that the other computer sends is untrusted: cap it and clean it.
  • A new protocol message: add it to AlReceptor/AlEmisor; if old and new apps cannot talk, bump VERSION and the ALPN (proxy-builder/deck/2, keeping deck/1 on crear until it can go).
  • Never accept connections on the node from a new place without calling pasar::ceder.
  • A new rule for «which is the same deck» belongs in store/linaje.rs, not in the screen.

See also

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Mobile · Móvil

Frontend · Frontend

Reference · Referencia

Clone this wiki locally