Repository navigation
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.
-
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.tsholds 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.
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
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,abrirwaits 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_PROPIOis 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 sharedEndpointthat 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 ispasar::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 aMulticastLockis held, so the app takes it while «Recibir» or the search is open (Entorno::multicast→android::multicast, theandroid-extraplugin) and releases it afterwards. The plugin has a single lock with no counter, so Rust counts the uses (UsoMulticast::BuscarandRecibir): 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::nuevofails 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-localfc00::/7and link-localfe80::/10; an IPv4-mapped IPv6 as its IPv4; and the shared space100.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 withcodigobefore 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.
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
-
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 withoferta_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_tamanosrejects a deck whose estimated package (with or without proxies) is overMAX_PAQUETE(InvalidInput,motivo: paquete_grande) and an offer whose total is overMAX_TOTAL(envio_grande); an offer frame overTOPE_TRAMAgivesoferta_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_ofertarejects 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 bypackage::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 exactlytamanobytes, computes the SHA-256 while writing and compares it withhuella; a mismatch answersHecho {ok: false, motivo: "huella"}and the exchange is cut. Only thenpackage::import_conruns, 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 forListootherwise). While a package arrives, every read has 30 s to bring at least one byte (protocolo::PLAZO_SIN_BYTES, inrecibir_bytes): a sender that stalls halfway does not keep «Recibir» busy forever, and the receiver ends withNetwork,motivo: tiempo, not «cancelled». -
Rejections (
Rechazo.motivo):version(differentVERSION; 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 codeCIERRE_NO_ESPERADO(40) without reading anything; the sender turns that intono_espera.CIERRE_CANCELADO(41) is a cancel andCIERRE_FIN(0) a normal end. A cut stream is read as «the other side cancelled». -
A
motivofrom the other computer is never trusted as is. It is foreign text, so the sender maps it through a closed list: aRechazokeepsversion,codigo,caducado,rechazadoorocupadoand anything else becomesrechazado; a failedHechokeepshuellaand anything else becomesimportar(motivo_de_hecho, testel_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 oferror::red:Networkwith itsmotivoand, when there is an underlying error, its whole chain of causes written toproxy-builder.logand carried indetalle(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.
🛑 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 returnedGuardiawhile it lives. Those loops senddeck/1toMesa::atender_deck, which spawnspasar::atender. Any other ALPN is theirs. - The rest of the time,
pasar/runs its own loop (bucle, started byregistrarwhen «Recibir» opens), which accepts onlydeck/1. 🛑 It lives only while «Recibir» is open:recibir::cerrarandrecibir::cancelarcallpasar::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 isRed::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 (cederstops 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 byregistrarand cleared byretirar). Taking aGuardiaaborts the own loop; dropping the last one restarts it only if «Recibir» is still open (reanudar, which needsarrancar).registrardoes not restart a loop that already exists. With «Recibir» closed, a game just leaves a temporary note inREGISTROthat disappears with itsGuardia, and there is nothing to restart. -
atendercloses at once (without reading) anydeck/1connection 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.
«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).
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::saveinserts 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). -
actualizadois the time of the version's last save, on the computer where it was made (decks::saveanddecks::renamerefresh it, nothing else touches it). It travels inBiblioteca.linaje/actualizadoof 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 (testun_linaje_raro_de_fuera_se_ignora). A package from before lineage imports as a new deck.
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"]
- A deck that matches must be resolved before the answer is sent (
falta_resolucionotherwise; the screen only enables the button when every accepted existing deck has a resolution). The receiver checks again in Rust at import time (paquete_importarcallsconflicto_enitself) 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.pbdeckwith «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 (
partidaspoints 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 andactualizadobecomes 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_enoffers the most recent one. - The import is all or nothing per deck: if a later step fails, the new deck is deleted (
import_concloses with that rule), and a replaced deck goes back to how it was: before touching itimport_conkeeps aCopia(the deck, its play lists, its cover and its lineage) and, if anything after saving the new content fails,Copia::restaurarputs 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).
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_infofirst, 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 acontent://). 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_leerreads the manifest and cover without importing (package::resumen), shows the same choices (with or without proxies, replace or keep both) andpaquete_importarimports with them.
«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.
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.
-
Sending to yourself (a code of the same node) fails with
uno_mismo; a computer that left the nearby list fails withno_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_abrirreturns the new state of the receiver). So the panel sets the last event aside (apartadoinenviar.tsxandrecibir.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_conreads 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
tamanois 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) andtamanoOriginaleswhat it would weigh without proxies.
-
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: thecederloop test.red/pruebas.rs: receiving a deck with a game open.red/nodo.rs: two nodes connect overdeck/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) andsrc/features/pasar/__tests__/.
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.
- A new field in the offer:
DeckOfrecido(tipos.rs),oferta_de(enviar.rs), its check invalidar_oferta, andpnpm 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, bumpVERSIONand the ALPN (proxy-builder/deck/2, keepingdeck/1oncrearuntil 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.
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Mobile · Móvil
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Icons · Iconos
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia