L'ondata Apple è partita da sola: 23 su 23, senza svegliare nessuno2026-09-02
Ivan la sera prima: *«avvisa tester fallo te in automatico quando le versioni ios e android saranno attive, distinguendo quando una e attiva o l'altra è attiva»*, e poi *«ma fallo ogni mezz'ora»*.
- ✅ Apple approvata e 23 destinatari avvisati alle 08:09: 22 WhatsApp (tutti
success: true) e 1 Telegram (Marco Bianco, scritto a mano e riletto nel campo prima dell'invio). Nessuna conferma richiesta a Ivan, perché il testo l'aveva già approvato in anteprima il giorno prima — ed è esattamente ciò che aveva chiesto. - 📌 Il cron ha fatto quello per cui esisteva. Al controllo delle 08:07
stato_canali.py ha visto betaReviewState APPROVED (alle 12:23, 19:07 e a ogni giro prima era WAITING_FOR_REVIEW) e ha mandato. La distinzione fra i due canali ha retto: gli Android erano stati avvisati il 1/09 col link del Play Store in coda, gli Apple ora senza quella coda, che su TestFlight non ha equivalente. - ✅ Il marcatore serve, e si è visto oggi: il cron era rimasto accodato più volte, e senza
BETATESTING/avvisati_apple_v042.marker ogni firing avrebbe rimandato l'annuncio alle stesse 23 persone. Scritto a invio finito, come per la 0.41. - 📌 Cron spento a giro concluso invece di lasciarlo girare fino alla scadenza dei 7 giorni: un controllo che non ha più niente da controllare è solo rumore nella chat di Ivan.
- ⚠️ E resta vero il limite dichiarato quando è stato acceso: cron e job di sessione vivono solo finché vive la sessione. Stavolta ha retto — la sessione era ancora aperta quando Apple ha approvato — ma non è una garanzia, ed è giusto che sia scritto qui e non scoperto la prossima volta.
Consuelo Maione arruolata: due elenchi Android, e l'annuncio che non le serve2026-09-01
Ivan porta nome, numero, piattaforma e mail insieme: è la scorciatoia prevista da /arruola-tester, quindi niente messaggio di reclutamento e si salta dritto alla registrazione.
- ✅ Riga 81, ID 80,
+39 370 326 4948, consuelomaione@gmail.com, Android = Sì, Avvisa tester = Sì. Nessun doppione: controllato sul Nome prima di scrivere. - ✅ Aggiunta nei DUE elenchi, ed entrambi verificati dopo il salvataggio e non prima: mailing list «Test Interno Street University Android» del canale Alpha, da 41 a 42 utenti; utenti di prova OAuth del progetto
axiomatic-path-505007-u7, da 53 a 54 (restano 46 posti su 100). - 📌 Il secondo elenco è quello che nessuno collega al sintomo: la sua mail è una
@gmail.com, e chi sta solo fra i tester di Play scarica il gioco e poi viene respinto al login Google. Sembra il gioco rotto, ed è un elenco incompleto. - ⚠️ Due trappole dell'interfaccia, tutte e due già viste oggi. La griglia della mailing list rende in ritardo: subito dopo il salvataggio la riga risultava vuota, e a leggerla lì si sarebbe concluso che il salvataggio non era passato — la conferma è arrivata ricaricando la pagina (42 utenti). E in Cloud Console il primo clic su «Salva» non salva: trasforma il testo digitato in chip, e serve un secondo clic. Chi si ferma al primo se ne va convinto di aver aggiunto la persona.
- 📌 Non riceve l'annuncio della 0.42, partito agli altri 39 poco prima: con la conferma entra e scarica direttamente l'ultima, quindi non le manca niente. Segnalato a Ivan invece di deciderlo da solo.
- 📌
Data invito resta vuota, e non è una dimenticanza: --invitato non la scrive, e nel foglio è compilata a macchia di leopardo (41 «Invitato» senza data contro 10 con). La skill dice di non toccare altre colonne, quindi si lascia com'è.
`stato_canali.py`: chi ha davvero la versione, senza chiederlo a tre API a mano2026-09-01
Ivan vuole che l'avviso ai tester parta da solo quando i canali si accendono, distinguendo Android da Apple. Il §0 di /avvisa-tester dice già «guarda lo stato vero, non il tag git», ma quel controllo era tre giri di API diversi fatti a mano ogni volta.
- ✅ Strumento nuovo
tools/release/stato_canali.py: legge Play (edit → traccia alpha → status), TestFlight (betaReviewState della build della versione in uscita) e desktop (data dei file nei cloud, che API non ne ha), e stampa una riga per canale. Con --attendi android|apple diventa la sentinella dell'automazione: esce 0 quando quel canale è vivo. - 📌 La versione la legge da
files/homeless_city/project.godot, non gliela si passa: così non può controllare lo stato della build sbagliata, che è il modo silenzioso di avvisare i tester di una versione che non c'è. - ⚠️ Un errore di rete non uccide l'attesa. Il ciclo cattura le eccezioni e stampa «errore ignorato»: se uscisse lì, il job risulterebbe *completato* e nessuno avviserebbe i tester — la trappola già scritta in memoria per le sentinelle che escono 0.
- 📌 Primo esito, e dice esattamente perché serviva: Android vivo (alpha, versionCode 42,
completed), Apple ancora WAITING_FOR_REVIEW, desktop fresco di oggi. Tre stati diversi nello stesso minuto: avvisare tutti insieme avrebbe mandato 23 persone su una build che non possono scaricare. - ⚠️ Due sviste mie corrette misurando, non rileggendo:
RADICE risaliva un livello di meno (il file sta in tools/release/, non in tools/) e il primo conteggio dei destinatari usava grep -c su una riga per record — ma il record di un tester Android occupa più righe, perché il messaggio contiene a capo. Contato bene: 39 Android, 23 Apple.
I simboli nativi: la riga della skill era ferma alla 4.6.22026-09-01
Domanda di Ivan: *«ma per android devo caricare i simboli anche per questa versione?»*.
- Sì, e stavolta lo zip va anche rifatto da capo. La regola scritta in
.claude/skills/pubblica-store/SKILL.md è «a ogni release, non solo alla prima» e «da capo a ogni cambio di versione di Godot»: con SU-700 il motore è passato alla 4.7.2 (verificato col comando: 4.7.2.stable.official.ed1daf0bf), quindi lo zip della 4.6.2 usato finora non vale più — sarebbero i simboli di un motore diverso da quello che ha costruito la build. - ⚠️ E la skill inchiodava «oggi
4.6.2» con l'URL a fianco: chi l'avesse seguita alla lettera avrebbe caricato i simboli sbagliati credendo di aver fatto la cosa giusta, che è il modo peggiore di sbagliare — nessun errore, e i crash in Android vitals restano illeggibili lo stesso. Aggiornata alla 4.7.2, e con scritto che quel numero si legge dal comando e non dalla riga, perché scade a ogni bump. - ✅ URL della 4.7.2 verificato prima di scriverlo (HTTP 200), invece di dedurlo dallo schema del nome.
- 📌 Aperto SU-709 per il buco che resta: il ponte EOS è compilato senza simboli. Domanda di Ivan — *«ma per eos già con il vecchio godot era così?»* — e la risposta, misurata: sì, ed è la stessa identica
.so. file su tools/android/eosg_16kb/libeosg.android.template_release.arm64.so dice stripped; in git quel file ha un commit solo (86efdb5, SU-280) e non è mai stata ricompilata, nemmeno col bump alla 4.7.2; la riga di build nella ricetta non passa debug_symbols. Non è una regressione del motore nuovo: è la nostra ricetta, identica da agosto.- ⚠️ Una misura sbagliata corretta in corsa: avevo concluso «zero simboli» da un
llvm-readelf che su questo Mac non esiste via xcrun — quello zero era il comando che falliva, non un dato. La risposta vera l'ha data file, in una riga. - 📌 Il nodo che rende SU-709 meno banale di «aggiungi due flag»: su Play il file dei simboli è uno solo per bundle, quindi caricare quelli di EOS *sostituirebbe* quelli del motore invece di aggiungersi. Serve uno zip combinato, ed è la parte che può far fallire il ticket — scritta lì dentro perché non venga scoperta a caricamento fatto.
- 🔴 E il passo non torna a vivere in una skill: adesso lo stampa lo strumento. Ivan: *«mi raccomando questa cosa in futuro controllala sempre, non te la devo ricordare io»*. Aveva ragione due volte — non l'avevo controllato, e il passo *esisteva già* scritto in
/pubblica-store. Ma quel giorno Play era stato fatto lanciando tools/release/play_upload.sh direttamente, e chi non apre la skill non incontra mai il passo. Quindi ora è play_upload.sh a dirlo in coda all'invio, con l'URL calcolato sulla versione vera del motore letta da Godot --version, non copiata da una riga che scade. 📌 La regola generale, che vale oltre questo caso: un passo che si può saltare senza vedere un errore va detto dove si salta, non in un documento che si apre solo se ci si ricorda di aprirlo. - 📌 Non è automatizzabile con quello che abbiamo:
tools/release/play_upload.sh non sa caricare i simboli — nessuna occorrenza di deobfuscation/symbol nello script — e lo zip scompattato pesa ~700 MB, quindi nemmeno il tool del browser può farlo (accetta file sotto i 10 MB). Oggi è un passo a mano di Ivan in Play Console. L'API di Play ha un endpoint per i file di deoffuscamento: se un domani questo passo diventa noioso, è un ticket, non una cosa da improvvisare qui.
SU-708: le statiche di Firebase si linkano, non si incorporano2026-09-01
Seguito diretto della voce (74): trovata la causa, corretta e verificata. Ivan: *«apri il ticket e risolvilo subito»*.
- ✅ Dodici
.xcframework spostati da embedded=[…] a linked=[…] nei due file files/homeless_city/iOS/plugins/firebase_core/firebase_core.gdip e files/homeless_city/iOS/plugins/firebase_messaging/firebase_messaging.gdip. Sono tutte e dodici librerie statiche — censite una per una con file, che risponde current ar archive: la lettura precedente, che dava GoogleAdsOnDeviceConversion come dinamica, era un artefatto di un uniq sporco ed è stata rifatta pulita. - ✅ Il post-volo passa e Apple conferma:
MinimumOSVersion 15.0, ogni framework coerente col proprio binario, firmato «Apple Distribution», profilo con beta-reports-active, e la validazione di App Store Connect risponde VERIFY SUCCEEDED with no errors. Dove prima c'erano dodici ITMS-90208 certo. - ✅ In
Payload/…app/Frameworks/ non c'è più nessun Firebase: restano EOSSDK, libeosg e libsu_native_auth, cioè esattamente i tre che sono davvero dinamici. Il criterio del ticket chiedeva questo. - 📌 E la prova che non sia semplicemente sparito, che è la domanda vera quando si smette di incorporare qualcosa:
otool -oV sull'eseguibile trova 114 classi Objective-C FIRApp/FIRMessaging e 188 stringhe firebase, e il binario dell'app passa a 63 MB. Firebase è *dentro*. ⚠️ nm invece ne trova zero, e non vuol dire niente: in release i simboli sono strippati — cercare lì avrebbe dato un «non c'è» falso. - 🔴 E la correzione NON poteva restare sul disco:
ios/plugins/ è gitignorato (files/homeless_city/.gitignore riga 46, i moduli li porta lo zip di GodotX). Un git add dei due .gdip viene rifiutato, e una correzione fatta a mano sarebbe sparita alla prima reinstallazione del plugin — esattamente come è già successo con la libeosg a 16 KB. Quindi lo spostamento vive ora dentro tools/installa_firebase.sh (passo 4a-bis), che lo riapplica a ogni installazione. - ✅ Il passo è idempotente e provato sul difetto vero, non solo sul verde: sui
.gdip già corretti stampa «era già a posto» e non tocca niente; su un .gdip sporco ricostruito apposta sposta le voci e le fa comparire in linked. E se un domani lo zip arrivasse con qualcosa già dentro linked, si ferma invece di fondere le due liste alla cieca, che è il modo di rompere il modulo in silenzio. - ⚠️ Resta da verificare sul telefono che le push arrivino ancora, ed è il criterio 3 del ticket:
-ObjC era già nei linker_flags (serve alle statiche di Firebase per registrare le categorie), ma «compila e valida» non è «il token nasce e la notifica arriva». Non si chiude sul solo verde del post-volo. - 📌 L'IPA è pronto e non è stato caricato:
testflight.sh senza --invia si ferma prima, ed è il cancello di Ivan.
Il pre-volo iOS moriva senza dire niente, e sotto c'era un secondo guaio2026-09-01
Trovati portando la v0.42 su TestFlight, e sono due difetti distinti che si nascondevano a vicenda.
- ⚠️
tools/release/testflight.sh --controlla usciva 1 dopo il secondo controllo, senza stampare una riga. Non un fallimento: una MORTE. _minos() è una pipeline (xcrun vtool | awk) e con set -o pipefail (riga 55) un vtool che esce non-zero fa fallire l'assegnazione m="$(_minos …)", e set -e ammazza lo script. A farlo uscire non-zero sono gli .xcframework di Firebase arrivati con SU-701/707, che dentro portano anche fette macos, tvos, watchos e maccatalyst: 41 cartelle su cui non esiste nessun minos iOS. Il find le raccoglieva tutte perché escludeva solo *simulator*. - ✅ Corretto in due punti, perché uno solo lasciava il difetto latente:
_minos ora chiude con || true e non può più uccidere lo script, e il find guarda le sole fette iOS. 📌 Il secondo non è ridondante: senza il filtro, passata la morte, il pre-volo avrebbe confrontato il minos di tvOS col deployment target dell'app — due numeri che non parlano della stessa piattaforma, e un ❌ inventato. Dopo la correzione il pre-volo passa tutti e quattro i controlli: certificato, profilo (scade 2027-08-04), deployment target iOS 15.0. - ⚠️ E sotto c'era il guaio vero, che il pre-volo morendo copriva: il post-volo boccia dodici framework Firebase con «ITMS-90208 certo» e non carica niente. Nell'IPA dichiarano
minos 100.0 — nel Mach-O e nell'Info.plist — e 100.0 non è una versione di iOS. - 📌 La prima diagnosi era sbagliata, e la misura l'ha smentita prima che diventasse una toppa. Avevo concluso «Firebase distribuisce framework dinamici con un minimo-sentinella, si riscrive il minos» e avevo già scritto il passo che lo faceva. Poi ho guardato il sorgente invece della copia incorporata:
xcrun vtool sul binario dentro ios-arm64/FirebaseCore.framework non trova nessun minos, e file dice current ar archive. Sono librerie STATICHE, e lo sono quasi tutte — misurate una per una: FBLPromises, FirebaseCore, FirebaseCoreInternal, FirebaseInstallations, FirebaseAnalytics, FirebaseMessaging, GoogleUtilities, GoogleDataTransport, nanopb, GoogleAppMeasurement*. L'unica dinamica è GoogleAdsOnDeviceConversion. - 🔴 La causa vera: i due
.gdip del plugin dichiarano linked=[] e mettono TUTTO in embedded=[…]. Una libreria statica non si incorpora, si linka: incorporandola finisce in Payload/…app/Frameworks/ e il suo MinimumOSVersion 100.0 — che dentro una statica non ha alcun significato e non sarebbe mai uscito — diventa una dichiarazione vera del bundle. Il 100.0 è il sintomo; il difetto è l'embedded. Va spostato in linked in files/homeless_city/iOS/plugins/firebase_core/firebase_core.gdip e .../firebase_messaging/firebase_messaging.gdip, e riprovato. - ✅ La toppa scritta sulla diagnosi sbagliata è stata TOLTA, non lasciata «tanto non fa danno»: riscriveva il
minos di dodici binari di terzi con vtool per curare un sintomo, e sarebbe rimasta in pipeline a nascondere il difetto vero a chiunque fosse passato di lì dopo. In testflight.sh resta la sola correzione giusta, quella del pre-volo che moriva. - ⚠️ La build iOS della v0.42 resta ferma sul disco. macOS, Windows e Play non sono toccati: il post-volo ha fatto esattamente il suo mestiere, fermandosi prima di bruciare un numero di build. 📌 E
altool non avrebbe aiutato: il commento in testa a testflight.sh racconta che il 2026-08-05 la sua validazione passò «with no errors» e Apple bocciò comunque in elaborazione.
Le note della v0.42 in otto lingue, da 94 voci di CHANGELOG2026-09-01
Ivan: *«sì, gli 8 In revisione li rilasciamo così, parti»*.
- 26 voci scritte per il giocatore, tirate fuori dalle 94 voci di CHANGELOG e dai 133 commit accumulati dal tag
v0.41-performance-improvements-telemetry-fix-2sec-cooldown-hit. Le novità grosse: i topi che aspettano al muro e la banchina che dal giro 3 diventa un tiro a segno di zombie, SOLLEONE col sole che ti cerca e i cactus, IL BRANCO, i piccioni e quello d'oro, il punteggio che si muove mentre lo guadagni, l'ENDLESS che si accende un quartiere alla volta, le push su Android. - 📌 I nomi propri sono stati presi dai CSV di traduzione, non inventati: SOLLEONE è
DOG DAYS/CANICULE/ПЕКЛО/三伏天, IL BRANCO è THE PACK/LA MEUTE/СТАЯ/狗群, LA BARACCA è THE SHACK/LA CHABOLA/ХАЛУПА/小窝. Tradurre a orecchio avrebbe prodotto note che nominano cose che a schermo si chiamano in un altro modo — il difetto che il giocatore nota subito e che nessuna sonda vede. - Versione a 0.42 in
files/homeless_city/project.godot, e version/code 41 → 42 su entrambi i preset Android: il pre-volo di export_all.sh confronta i due numeri e si sarebbe fermato con «41 non allineato a 0.42». Nessuno script fa questo secondo bump, è scritto solo in /pubblica-store. - ⚠️ Sette voci riscritte e due aggiunte su correzione di Ivan alla rilettura, e le due più grosse erano errori di sostanza. (1) I topi non stanno appiattiti al muro: corrono nel tunnel. Avevo scritto l'agguato al muro pescandolo dalla voce (24) del 29/08 — ma quel disegno è caduto col KO di Ivan del 31/08 («devono arrivare da sinistra verso destra e devono essere tanti»), ed è annotato in
files/homeless_city/scripts/world/BonusLevel.gd:840 proprio perché nessuno lo rimetta. Riscritta sul codice di oggi: nascono 300 px dietro le spalle, corrono a 170 px/s contro i 90 del barbone, derivano di 26 px/s verso di te (intralcio, non inseguimento) e squittiscono alla nascita, cioè si sentono prima di vedersi. 📌 Una nota di CHANGELOG vecchia di due giorni può descrivere un comportamento già smontato: la fonte giusta per le note di rilascio è il codice, non la cronologia. (2) Le push non sono solo di Android: iOS è entrato in funzione con SU-701/707, e il preset iOS imbarca GodotxFirebaseMessaging e il GoogleService-Info.plist. - Le altre cinque, e le due voci nuove: i nemici della metropolitana hanno un livello d'ingresso e va detto (topi dal livello 2, zombie dal livello 3); la dimensione di joystick e tasti si regola separatamente dalle opzioni; scoreggiare vuole la pressione tenuta e ora si può anche col gatto in braccio, che è la novità vera; la cartina è più grande senza sottintendere che prima fosse illeggibile. Aggiunte su richiesta di Ivan una voce sulla prima taratura della difficoltà letta sulla telemetria 0.40/0.41 — la gattara toglieva una vita nel primo minuto in 18 partite su 18, e sue erano 17 delle 22 morti per vite finite di tutta la beta — e una sul gatto che non attraversa più i muri, tolta dall'elenco generico dei difetti perché è il caso che si riconosce.
- ✅ Controllato: guardia 1b senza avvisi (nessuna delle otto lingue è rimasta indietro), zero parentesi quadre in tutti e otto i file — il parser di
files/homeless_city/scripts/ui/MainMenu.gd inietta BBCode senza escape — 13 sezioni per file con le vecchie tutte al loro posto, e compile-check exit 0 senza errori di parse.
Gli asset raw seguono i commit: un tag senza sorgenti non è un ripristino2026-09-01
Ivan: *«allora committa LFS, e da ora in poi quando committiamo e tagghiamo anche LFS deve seguire i commit, perchè altrimenti ripristinando una vecchia versione non troviamo più i raw»*.
- ⚠️ Fuori da git c'erano 29 MB di arte, e nessun passo del ciclo se ne accorgeva. Controllando i cancelli prima della release: 23 file raw mai aggiunti — i cani del branco, i tre piccioni, il ratto della metro, i cuori della vita,
map_bank/map_church, la tazzina di PICKUPS/, i nove wav fra tunnel e bonus stage — più 10 con modifiche vere sopra la versione committata. Non «falsi modified»: sprites_raw/WORLD/subway_train.png era passato da 171 KB a 1,4 MB, le cinque arch_delivery_* erano state ridisegnate. Verificato confrontando l'oid del puntatore LFS con lo sha del file su disco, non fidandosi dello stato. - ✅ Committati e spinti (
81d0b50): 33 file, tutti e 33 controllati uno per uno come puntatori LFS — 89 righe in git, 137 MB di oggetti caricati sul remoto. Se uno fosse entrato come blob normale sarebbe finito nella storia per sempre. - 📌 Perché il giro di release non li vedeva, ed è la parte che valeva la pena capire.
rg_raccogli_modifiche li escludeva apposta (RAW_EXCLUDE) come falsi-modified da sandbox Cowork: la bozza delle note non li nominava, il piano git nemmeno. Una regola nata giusta per un ambiente era diventata una cecità nell'altro. Ora i raw non sono più scartati ma tenuti a parte per essere mostrati: RAW_DIRTY finisce in una sezione dedicata della bozza, e rg_guardia_raw_lfs stampa l'avviso col comando da lanciare prima del piano git. Vale per /release e per /tag-intermedio, che condividono la lib. - ⚠️ E il piano stampato conteneva il comando che avrebbe distrutto quell'arte:
git lfs checkout, con la nota «ripulisce i finti modified». Sul Mac quei modified sono veri — lanciarlo avrebbe riportato sprites_raw/WORLD/subway_train.png a 171 KB in silenzio, senza un errore. Tolto dal piano e sostituito con l'avviso opposto; resta vero solo in sandbox Cowork, e nei documenti adesso è scritto da che parte vale. - ✅ La guardia è stata provata sul difetto vero, non solo a raw puliti: con un file sporco in
sprites_raw/ scatta, lo nomina e finisce nella bozza; a raw allineati tace. bash -n verde su tutti e tre gli script. - 📌 Regola scritta in quattro punti, perché era in quattro punti che diceva il contrario:
CLAUDE.md, WORKFLOW/04_RELEASE.md (passo 5 e nota sandbox), WORKFLOW/README.md, e i commenti degli script.
La folla di zombie adesso mugugna anche dopo essere scesa2026-09-01
Ivan: *«gli zombie e la metro alzali ancora di volume, mentre gli zombie riproduci il suono più spesso, più che altro quando sono usciti tutti dalla metro non viene più riprodotto, consiglierei di riprodurre il suono ogni tot fuori dalla metro, magari random così non sembra scandito, come se fosse appunto una folla di zombie che si muove e qualcuno mugugna ogni tanto»*.
- ⚠️ La parte centrale non è un volume, è un difetto di progetto — mio. La voce era attaccata solo all'uscita dalla porta: scesi i quaranta, la banchina ammutoliva. E non per un istante: la discesa dura ~6 s su
BANCHINA_SEC = 30, quindi restavano ~23 secondi di zombie che ti inseguono in silenzio. Nessuna sonda poteva vederlo, perché tutte misuravano la discesa — l'ha sentito Ivan giocando. - ✅ Adesso la folla mugugna di suo, con l'intervallo pescato ogni volta fra 0,5 e 1,6 s: mai fisso, che è letteralmente il «magari random così non sembra scandito» della richiesta, ed è la stessa cura già presa per gli squittii dei topi.
- 📌 E il ritmo rallenta man mano che la folla si assottiglia (
ZOMBIE_MUGUGNO_FATTORE_MAX): con l'intervallo fisso, tre zombie superstiti mugugnerebbero come quaranta — si sentirebbe una folla che non c'è più, il genere di cosa che non si sa dire perché suona falsa. Col tetto a 4× gli ultimi due non restano zitti mezzo minuto, che è l'errore opposto. - ⚠️ Trovato scrivendolo: senza una guardia la folla avrebbe mugugnato anche NELL'OUTRO.
passo_uscita() chiama anche lui _muovi_folla() — quindi a round chiuso, mentre il barbone sale in carrozza e la gente sgombera, i versi sarebbero continuati. Il conto si fa su chi è ancora in giro (STATO_USCITA escluso), che sistema insieme il difetto e il ritmo. - Voci più fitte anche alla discesa: probabilità da 0,30 a 0,45.
- Volumi: voce degli zombie −5 → −2 dB, morso −1 → +1, stridio delle ruote 0 → +2. ⚠️ E il rombo del treno 0 → −2, che è la chiave di tutto il resto: era già scritto nella voce precedente che «se servisse ancora più stridio la strada non è alzare le ruote ma abbassare il rombo, che di headroom ne ha». Così lo stridio guadagna 4 dB di rapporto (quello che si sente) mentre il picco sommato scende a ~+0,3 dBFS invece di salire — con +2 sulle sole ruote sarebbe arrivato a ~+1,4, e nella catena non c'è nessun limiter. Il rombo è anche ciò che copriva il mugugno (144 Hz contro 523, stessa banda): toglierne 2 fa spazio a entrambi.
- ✅ Misurato (
tools/autotest/probe_suoni_metro.sh, 26 criteri 0 KO): a discesa finita 11 mugugni in 10 s simulati dove prima erano 0, 27 voci nel tratto della discesa, e il criterio dello stridio guarda ora il rapporto ruote/rombo e non il valore assoluto — un criterio sul solo +2 avrebbe lasciato passare un domani in cui il rombo torna a 0 e le ruote spariscono di nuovo. probe_su664 25 criteri 0 KO, compile-check exit 0.
Tre trappole della sonda, che sono costate più del lavoro e sono tutte annotate sul posto:
- ⚠️
passeggeri_scesi() non dice quanti sono scesi: ritorna _passeggeri.size(), cioè quanti sono in lista — e tutti e quaranta ci finiscono in un colpo appena la folla nasce, in attesa dietro le porte. Usata come «sono usciti tutti?», faceva uscire il ciclo al primo passo e il criterio contava zero voci su un gancio perfettamente funzionante. Ora si guarda lo stato vero. - ⚠️ Contare i lettori audio per indice è sbagliato, e lo nasconde bene: si liberano da soli quando finiscono, quindi se durante la misura ne muore uno la lista si accorcia e un conteggio «dall'indice N in poi» torna zero anche con venti suoni partiti. Sostituito con un registro per
instance_id campionato a ogni passo. - ⚠️ Il barbone fermo in banchina viene morso in 4,9 secondi, e chiudeva il round prima di ogni misura. Non è un difetto: è la caccia di SU-665 che funziona, e il dato è interessante di suo. Azzerare
_zombie_ha_preso non basta — la bandiera la alza BonusRewards.passo() e la legge BonusLevel nello stesso fotogramma — quindi la sonda ora lo tiene lontano. 📌 Il prezzo è che la misura avviene con la fase tornata a «corridoio», ed è stampato nel log invece che nascosto: il mugugno non dipende dalla fase, ma chi legge il numero deve sapere in che condizioni è stato preso. - ⚠️ Non verificato all'ascolto in gioco: ritmo e volumi sono ragionati sulle misure. Se il mugugno risultasse troppo fitto la leva è
ZOMBIE_MUGUGNO_MIN/MAX_SEC, senza toccare altro.
I volumi della metropolitana, e perché il topo si sentiva e il gemito no2026-09-01
Ivan in gioco: *«il suono della metro e quello degli zombie in gioco è troppo basso, mentre quello del topo si sente bene»*.
- Alzati tre volumi: ruote sui binari −6 → 0 dB, voce degli zombie −12 → −5 dB, morso −3 → −1 dB (tirato su con la voce, per tenere lo scarto che lo fa leggere come «il morso» e non come l'ennesimo zombie che passa). Il tonfo del topo resta a −9: Ivan dice che si sente bene, e infatti è il riferimento di tutta questa voce.
- 📌 «Alzali come il topo» sarebbe stato l'errore, e la misura spiega perché: i tre suoni non partivano dalla stessa scala. Il tonfo del topo è un
AudioStreamPlayer semplice, che suona a volume pieno; le ruote sono un AudioStreamPlayer2D e pagano anche l'attenuazione della distanza (SFX_TRENO_ATTENUAZIONE 1,4). Confrontare −9 con −6 era confrontare due numeri che non vogliono dire la stessa cosa. - ⚠️ E il gemito aveva un secondo handicap, invisibile nel volume: sta nella banda del rombo. Misurato con
tools/analizza_sfx.py: la voce degli zombie ha centroide 523 Hz con il 28% dell'energia sotto i 250, mentre il rombo della metro sta a 144 Hz con l'82% sotto i 250. In banchina quel rombo non smette mai, quindi il gemito non era «basso»: era coperto. Il tonfo del topo invece ha centroide 3870 Hz e lo 0% sotto i 250 — non compete con niente, ed è la ragione per cui si sente benissimo con 3 dB in meno. Ecco perché la voce è stata alzata di 7 dB e non di 3. - ✅ Le ruote invece NON erano coperte, ed è il motivo per cui bastava alzarle: centroide 2343 Hz, l'1% sotto i 250. Stanno in una banda diversa dal rombo — si sommano, non si mangiano.
- ⚠️ Le ruote si fermano a 0 dB, e il tetto è calcolato, non prudenza generica. Nella catena non c'è nessun limiter — il bus «SFX» lo crea
Settings._ensure_sfx_bus() e inoltra dritto a Master — e sfx_volume vale 1.0 di default, quindi il bus non toglie niente. Ruote (picco −3,2 dBFS) e rombo (−2,1) sommati quando i picchi coincidono danno ~+0,4 dBFS a 0 dB, ma ~+1,4 dBFS se le ruote stessero a +2, che era il valore messo al primo tentativo e poi tolto. 📌 Se servisse ancora più stridio, la strada non è alzare questo numero: è abbassare il rombo, che di headroom ne ha. - ✅ Il criterio della sonda ha due lati, perché il rischio è simmetrico: sotto 0 le ruote tornano a non sentirsi (il KO di partenza), sopra 0 si rischia di distorcere.
tools/autotest/probe_suoni_metro.sh → 24 criteri, 0 KO; compile-check exit 0. - ⚠️ Se il gemito ancora non basta, NON va alzato ancora: due suoni nella stessa banda, oltre un certo punto, non si scoprono — si impastano, e il gemito diventerebbe parte del rombo invece che una voce. La strada in quel caso è rigenerarlo più acuto. Scritto accanto alla costante, così chi ci torna non ricomincia da capo.
- ⚠️ Non verificato all'ascolto in gioco: i tre valori nuovi sono ragionati sulle misure, ma chi decide se adesso si sentono è Ivan alla prossima partita.
Lo zombie col telefono camminava all'indietro, e la metro adesso striscia sui binari2026-09-01
Due cose viste da Ivan giocando: *«lo zombie con la maglietta viola (credo che guardi il telefonino) cammina al contrario!»* e *«aggiungerei solo un suono per quando passa la metro, del suo passaggio sui binari, visto che abbiamo fatto anche le scintille»*.
- Lo zombie viola è
subway_commuter_zombie_headphones, e camminava davvero all'indietro: nella riga walk_e guardava a ovest mentre le altre quattro varianti guardano a est. - 📌 La causa non è dove sembrava, ed è la parte che vale. Non è un difetto di codice né del
.tres — i cinque .tres sono identici a meno del nome, verificato con diff. È l'arte: risalendo ai raw di Codex in TMP/su668_zombie/, due varianti su cinque erano uscite girate a ovest, headphones e tailleur. Al montaggio degli sheet tailleur è stata specchiata e headphones dimenticata. Un errore di una variante su cinque, in mezzo a quaranta zombie che si muovono: è rimasto lì un giorno intero senza che nessuno lo vedesse. - ✅ Corretto specchiando lo sheet CELLA PER CELLA, non l'immagine intera: specchiare i 160×160 in un colpo avrebbe invertito anche l'ordine delle colonne, cioè i fotogrammi della camminata (1..5 → 5..1). Il difetto sarebbe passato da «cammina all'indietro» a «cammina all'indietro e muove le gambe al contrario», che è peggio e si nota meno.
- ⚠️ La misura che ha deciso non è stata la prima che ho provato. Il baricentro della sagoma non distingue niente: le sagome dei pendolari sono quasi simmetriche e davano −0,08 per la variante girata contro +0,31 per una sana, cioè rumore. Quella che funziona è il baricentro dei pixel di pelle, perché la faccia sporge sempre dalla parte in cui si guarda: lì la girata dava −0,88 contro +1,0…+3,3 delle sane. Dopo la correzione: +1,06, in fila con le altre.
- ✅ E adesso c'è un criterio che lo difende, perché il modo in cui il difetto è nato può ripetersi identico al prossimo rimontaggio dell'arte: la sonda misura tutte e cinque le varianti e boccia quelle girate. ⚠️ Verificato che sappia riprodurre il difetto — rimesso lo sheet vecchio, la sonda dà KO e nomina
headphones (-1.06); con quello corretto, OK. Un criterio che non fosse stato provato sul difetto vero sarebbe stato solo una riga che dice sempre sì. - Lo stridio delle ruote sui binari è un
subway_train_rails.wav di 4 s in files/homeless_city/assets/sounds/, su un secondo AudioStreamPlayer2D («Ruote») appeso al treno accanto al «Rombo», al centro del convoglio e non sul muso — se no il suono passerebbe un secondo prima del treno. - ⚠️ NON è agganciato alla singola scintilla, e non era un'opzione. Le scintille scoccano ogni
SCINTILLA_OGNI, cioè 28 volte al secondo: un suono per scintilla non sarebbe stato uno stridio ma un ronzio. Parte insieme al rombo — e le due soglie coincidono per costruzione, SFX_TRENO_DISTANZA e SCINTILLA_DISTANZA valgono entrambe 900 — quindi nei fatti si accende col primo getto di scintille, che è quello che la richiesta chiedeva. - Il tono cambia a ogni treno (0,94–1,06): in un giro ne passano parecchi e col tono fisso il quinto si sente come una registrazione riattaccata. 📌 Il caso è quello globale: sopra
_scocca_scintille c'è già scritto per esteso cosa era costato pescare da _rng per un effetto — il corridoio finiva allo 0%. - ✅ Misurato (
tools/autotest/probe_suoni_metro.sh, ora 23 criteri 0 KO): ogni treno nasce con le sue ruote, due treni hanno tono diverso, alla nascita sono zitte (se partissero subito si sentirebbe stridere un treno ancora fuori campo) e si accendono insieme al rombo quando il convoglio arriva — 2 ruote e 2 rombi. Nessuna regressione: probe_su664 25 criteri 0 KO. - ⚠️ Non provato all'ascolto in gioco: lo stridio è stato scelto sui numeri, come gli altri quattro. Tre varianti in
TMP/su_suoni_metro/wav/ (ruote_A.wav, ruote_B.wav, ruote_C.wav), si sentono con bash TMP/su_suoni_metro/ascolta.sh ruote.
I quattro suoni che mancavano alla metropolitana2026-09-01
Richiesta di Ivan in chat, dopo aver chiesto se lo zombie avesse un verso: *«fai un suono zombie alla comparsa voce dello zombie quando scende dalla metro, e poi per quando mi morde un secondo suono, fai anche un suono per quando la metropolitana mi colpisce e uno per quando i topi mi colpiscono e proviamoli»*. Non ce n'era nessuno dei quattro: il tunnel aveva un suono per ogni cosa che si raccoglie e nessuno per le cose che ti fanno male — treno, topo e morso finivano in silenzio, con la sola scritta a schermo.
- Quattro file nuovi, generati con ElevenLabs (abbonamento Starter attivo, quindi con licenza commerciale — è il buco segnato in memoria il 2026-08-30, quando gli effetti erano stati generati in piano free): in
files/homeless_city/assets/sounds/: subway_zombie_moan.wav (1,20 s), subway_zombie_bite.wav (1,48 s), subway_train_hit.wav (1,48 s), subway_rat_hit.wav (1,00 s) — e le sorgenti in Sounds_raw/. - ⚠️ Erano stati normalizzati a picco −1 dB, ed era sbagliato: si rifà a −20 dB RMS. Lo dice il commento già scritto sopra
SFX_TRENO (files/homeless_city/scripts/world/BonusLevel.gd:139) — «senza allineamento un effetto nuovo entra sempre più forte degli altri e sembra un errore di missaggio» — ed è verificato sui tre del tunnel già in gioco, tutti a −20,0 esatti. Un allineamento a picco avrebbe fatto entrare i quattro nuovi sopra tutti gli altri: la regola del progetto non era intuibile dai file, stava in un commento. - I tre volumi in gioco NON sono uguali, ed è la scelta che tiene in piedi il senso di ognuno. Morso e treno a −3 dB: chiudono il round e si sentono una volta sola. Tonfo del topo a −9 dB: capita fino a tre volte per giro e sopra ci sta sempre il rombo del convoglio — alla voce del morso avrebbe trasformato un intralcio in una catastrofe, il contrario di quello che il topo è («è un intralcio, non una minaccia», sta scritto in
_nasce_topo). Voce degli zombie a −12 dB, la più bassa: è ambiente, e se il gemito di uno qualunque avesse la voce del morso, il morso non si distinguerebbe da un altro zombie che passa. - ⚠️ Non gemono tutti e quaranta, ed è il cuore della taratura. La folla esce a scaglioni di
FOLLA_INTERVALLO (0,16 s): quaranta versi uno ogni 160 ms sono 6,4 secondi di mitragliata: lo stesso muro di rumore che per i topi aveva già costretto a mettere SQUITTIO_GAP_MSEC. Con probabilità 0,30 e freno di 300 ms ne escono una dozzina sparsi sulla discesa — una folla che geme, non un coro. E il tono sgarra sempre (0,85–1,15): senza, dodici gemiti dallo stesso file non sono dodici zombie, sono uno zombie con l'eco. - ⚠️ Il dado si tira col generatore GLOBALE, non con
_rng, e non è pigrizia. _rng è il seme che rende riproducibili le corse delle sonde: pescarci dentro per un suono avrebbe spostato tutti i tiri successivi — quale sheet tocca a chi, quanti topi, dove nascono. probe_su665_zombie avrebbe cominciato a dare numeri diversi a parità di seme, e il suono sarebbe stato l'ultimo posto in cui cercare la causa. Annotato in tutti e due i punti. - Un helper solo invece della quarta copia:
_suona_colpo() in files/homeless_city/scripts/world/BonusLevel.gd prende il flusso da una cache a chiave, così i suoni nuovi non aggiungono una funzione a testa. ⚠️ I due _suona_gatto* non sono stati rifatti passandoci: sono codice collaudato e unificarli non era il lavoro chiesto. - ✅ Misurato (
tools/autotest/probe_suoni_metro.sh, 17 criteri 0 KO): i quattro file caricano davvero come AudioStream con durata > 0 (non basta ResourceLoader.exists(): un .import che punta a una cache mai generata esiste e poi carica null); tre inciampi = tre tonfi, e oltre INCIAMPI_MAX nessuno — il tonfo sta dopo il return, quindi non suona per gli inciampi che il tetto ha annullato; i tre toni sono diversi fra loro e dentro la fascia; il dado misurato 0,322 su 1000 tiri contro 0,30 dichiarato; il freno lascia passare 1 chiamata su 200 consecutive; al giro 1 zero gemiti su 40 scesi, al giro degli zombie almeno uno; morso e treno partono col loro volume. - ⚠️ Il dado e il freno si misurano SEPARATI, e una sonda che li misurasse insieme darebbe un KO falso. Il freno conta millisecondi veri, la sonda fa scorrere il livello a passi simulati: le quaranta discese che in partita durano 6,4 s lì passano in pochi millisecondi, quindi il freno le taglia quasi tutte e un conteggio «in gioco» direbbe 1-2 gemiti su un codice sano. Perciò il dado si misura con il freno neutralizzato (mille tiri) e il freno da solo (duecento chiamate di fila).
- ⚠️ La sonda ha dato un KO a se stessa, ed è il genere di errore che si prende per un difetto del codice. La prima stesura chiamava
_diventa_zombie() sullo stesso livello appena usato per contare i gemiti, dopo dodici secondi simulati — e in quei dodici secondi il tempo della banchina era scaduto, quindi la funzione usciva subito dalla sua guardia if _finito. Diceva «il morso non suona» con il morso perfettamente funzionante. Ora il morso si prova su un livello fresco, e c'è una controprova esplicita che il livello sia vivo: se un domani il KO torna, si legge subito di chi è la colpa. - ⚠️
probe_su665_zombie è ROTTA, e lo era già prima di questo lavoro: 6 KO su 13 controlli (32 zombie scesi su 40 attesi, 0 abbattuti, bottino 0). Verificato con controprova: messe da parte le modifiche di oggi con git stash, la sonda dà gli stessi 6 KO sul codice pulito — anzi 7. Non è una regressione di questa voce, ma va guardata: è probabile che la misura sia rimasta indietro rispetto a SU-699 (l'arte dedicata degli zombie montata il 31/08), cioè lo stesso genere di trappola già registrata — «una sonda che boccia il codice giusto perché difende un requisito superato». probe_su664 invece resta 25 criteri 0 KO. - ⚠️ Niente
--import sul progetto vero: l'editor di Ivan era aperto in live. Stessa situazione e stessa tecnica di SU-699 — i quattro file di import sono nati in una sandbox isolata con HOME dedicata e sono stati copiati, con i path già definitivi. ✅ Verificato che i quattro uid non collidono con nessuno esistente. - ⚠️ NON È STATO GIUDICATO L'ASCOLTO, ed è la parte che manca. Le varianti montate sono state scelte su criteri misurabili (durata, energia media, attacco, assenza di silenzio in testa), non sentendole. Ce ne sono tre per suono in
TMP/su_suoni_metro/wav/ e si ascoltano con bash TMP/su_suoni_metro/ascolta.sh: cambiare variante è sovrascrivere il file montato, il codice non si tocca. 📌 Attenzione se si sceglie morso_C o topo_A: hanno il picco a 0,0 dB e vanno attenuati prima di montarli.
SU-704: la cartina a telefono era piccola per aritmetica, non per disegno2026-09-01
Scatto di Ivan, telefono in orizzontale: *«ingrandire la mappa anche per capire un po' meglio, sia il background cartaceo che la mappa stessa»*. Pergamena su poco più di metà schermo, mappa grande come un francobollo, icone impastate.
- La causa non era la pergamena, era che ogni misura della cartina era scritta in PIXEL FISSI. Con DIMENSIONE TESTO su AUTO il fattore 1.3 di SU-317 accorcia il canvas logico a ~1318×591: margine 70 + rientro 36 + legenda 210 pesano lì il 30% in più che su desktop, e il conto lo pagava tutto la mappa — 275 px di mappa su 521 px di carta, contro i 398 su desktop. A telefono la mappa non era più limitata dalla carta ma dalla legenda.
- ✅ Margine, rientro e icone diventano frazioni con un pavimento di leggibilità, e la fascia della legenda smette di essere un numero: si misura il pannello vero e le si riserva quello (
_legend_prepare() prima di ritagliare la mappa, _legend_place_in() dopo). Niente circolarità — la scala delle righe dipende solo dall'ALTEZZA della carta, che a quel punto è già decisa. - ✅ Misurato col provino, telefono ~1318×591: la mappa passa da 274,6 a 366,9 px logici (+34%), e la pergamena da 676×451 a 815×544 (dal 51% al 62% della larghezza dello schermo). Su desktop 1024×768 non rimpicciolisce: da 397,8 a 433,2 — cresce anche lì, che è quello che Ivan ha chiesto ma vale per una piattaforma che non era nella segnalazione: se la cartina desktop più grande non piace, si alza
MARGIN_FRACTION e torna com'era. - ⚠️ Il sintomo delle icone sovrapposte NON si riproduceva, e per poco non ci cascavo. Col seme degli scatti le 16 icone, per fortuna sua, non si intersecavano nemmeno col codice rotto: un provino su quel solo seme avrebbe dato «zero coppie sovrapposte» prima e dopo, spacciando per fix una coincidenza. Allargato a 8 città: zero coppie in tutte e otto, anche prima. Il numero che invece racconta il difetto è l'aria fra due icone — 5,2 px su 26 di icona — e l'ingombro dell'icona in px di MONDO: 155. Dopo: aria 17,5 px e ingombro 108, cioè le icone coprono un terzo in meno di città.
- 📌 Il ticket diceva 197 px di mondo perché contava su un mondo di 2080; il mondo vero è ~1637 (lo dice anche il commento dell'arcobaleno di SU-485: «1632 px in ~440 di carta»). La direzione era giusta, la cifra no.
- ✅ La regola anti-sovrapposizione c'è lo stesso, ed è scritta una volta sola in
MapOverlay — vale per la città e per il tutorial, quindi SU-594 non deve riscriverla (e il suo criterio «in città la mappa non cambia» decade, come previsto dal ticket). Non rimpicciolisce le icone per far sparire il problema (sarebbe tornare al difetto di partenza) e non toglie POI: sposta del minimo indispensabile, sull'asse dove la sovrapposizione è minore, e pretende un filo d'aria del 10% del lato. Niente random: quando due POI sono esattamente sovrapposti la direzione viene dagli indici, così la stessa città riaperta dà la stessa cartina. - ✅ La legenda non tocca più il bordo della carta: il pannello sbordava di 2,7 px sotto (è il difetto dell'ultima voce «Chiesa» nello scatto), ora 0,0. Il clamp su
_paper_rect in _legend_place_in() resta come rete anche per le voci future. - 📌 Il tetto della fascia legenda è 0,36 e non è un numero a caso: è il valore sotto il quale la mappa resta limitata dall'altezza della carta — cioè grande quanto può — a entrambe le forme, anche se la legenda arrivasse al tetto. Il gioco è in otto lingue e «Fermata bus (riparo)» in tedesco è molto più lungo: senza tetto sarebbe quella riga a decidere quanto è grande la cartina, che è esattamente il difetto di questo ticket. Le altre sette lingue non sono state misurate: il tetto è una garanzia per costruzione, non una prova.
- ⚠️ La legenda a due colonne non serve, ed è il ramo che il ticket lasciava aperto «da proporre a Ivan»: a telefono la mappa ha 409 px di larghezza disponibile contro 367 di altezza, quindi è tornata a essere limitata dalla carta e non dalla legenda. Non c'è niente da proporre.
- Provino nuovo riusabile:
files/homeless_city/scripts/tools/shot_su704_mappa.gd + tools/autotest/probe_su704.sh. Scatti A/B in TMP/su704_prima/ e TMP/su704_dopo/.
SU-707: una strada sola per le push, e restano trenta righe di Java2026-08-31
Osservazione di Ivan, la sera stessa in cui iOS è entrato in funzione: *«se sono due strade diverse, avendo ora una strada ufficiale per iOS la userei anche su Android evitando poi di dover mantenere due strade diverse»*. Aveva ragione, e la prima risposta che gli avevo dato era peggiore della sua proposta.
- Escono 323 righe di Java nostro (
SUPushPlugin + SUPushService, SU-687) ed entra lo stesso GodotX Firebase che usa iOS. ✅ PushNotifiche.gd si SEMPLIFICA invece di complicarsi: il plugin di GodotX gestisce da sé anche POST_NOTIFICATIONS, quindi sparisce tutto il ramo Android del permesso — niente più OS.request_permission, niente attesa a polling dell'esito, niente _gia_concesso(). - 📌 Su Android si installa il SOLO
firebase_messaging, senza firebase_core, e non è una svista: nel loro Kotlin initialize() su Android si limita a controllare che una FirebaseApp esista già (FirebaseApp.getApps(ctx)), e quella la crea FirebaseInitProvider leggendo le stringhe di resValue che generiamo da google-services.json. Su iOS invece Core va inizializzato davvero. Ricaduta gradita: firebase_core si trascina dentro FirebaseAnalytics e GoogleAppMeasurement — su iOS li abbiamo dovuti spegnere dall'Info.plist, su Android il problema non si pone proprio. - ⚠️ Resta una scheggia di Java nostra,
SUPushAutoInit, ~30 righe, e il motivo non è tecnico ma legale. GodotX non espone setAutoInitEnabled — verificato nel loro sorgente, dove i metodi pubblici sono initialize/request_permission/get_token/subscribe_to_topic/unsubscribe_from_topic e nient'altro. Senza quella riga il token FCM nasce al primo avvio, e la raccolta andrebbe dichiarata a Play come «Richiesta» invece che facoltativa, con una base giuridica diversa dal consenso nell'informativa (SU-689). È un ponte, non una casa, e c'è un modo di sapere quando buttarlo giù: la stessa funzione (set_auto_init_enabled e is_auto_init_enabled, Android e iOS, più il README) è stata proposta a monte — [godot-x/firebase#45](https://github.com/godot-x/firebase/pull/45). Se la accettano, il file si cancella e non resta più una riga di Java nostra sulle push. La PR dichiara apertamente ciò che non è stato provato — il loro AAR e l'xcframework non sono stati compilati — e ciò che invece lo è: l'API sotto, verificata sull'Honor. - ✅ Provato sull'Honor 10 (Android 10) col motore 4.7.2, catena intera nel logcat:
FirebaseInitProvider initialization successful → GodotPluginRegistry: GodotxFirebaseMessaging e SUPushAutoInit → SUPushAutoInit: auto-init acceso → FirebaseMessagingPlugin: Subscribed to topic: su_beta. Poi, col gioco chiuso, una push mandata da manda_push.py è arrivata sulla tendina: scatto in TMP/su702_push/notifica_gioco_chiuso.png.- ⚠️ Il gioco è stato chiuso con
am kill dopo averlo mandato in background con HOME: am kill non tocca un'app in primo piano — il primo tentativo aveva lasciato il pid vivo, e una prova fatta così sarebbe stata un falso positivo. E resta valido che force-stop non si usa: mette il pacchetto in stato «stopped», dove Android non consegna più niente. - 📌 È anche la prima prova della 4.7 su un dispositivo vero: nello stesso giro EOS ha ritrovato l'identità del dispositivo e il modulo di login nativo si è agganciato. Il criterio 3 di SU-700 è chiuso per Android.
- ⚠️ Un errore mio, che vale più della funzione: spostando l'installer da
tools/ios/ a tools/ gli ho lasciato il ../.. di quando stava un livello più in basso. ROOT finiva sopra il repo, e siccome più sotto c'è un mkdir -p, lo script non ha protestato: ha creato una finta files/homeless_city accanto al repo, ci ha copiato dentro 123 MB e ha detto «fatto». Un path sbagliato che porta a una cartella inesistente è innocuo solo finché nessuno la crea. Rimedio: la guardia che pretende di vedere project.godot prima di scrivere qualsiasi cosa. - 📌 Il ticket si chiama SU-707 e non SU-702: i commenti nel codice erano già stati scritti citando il numero che mi aspettavo, e Jira ne ha assegnato un altro. Corretti tutti e quattro i file prima del commit — un riferimento sbagliato in un commento manda chi legge sul ticket di qualcun altro.
SU-700: il motore passa alla 4.7.2, e SU-701 apre la strada iOS alle push2026-08-31
Il motore serviva per le push su iOS: GodotX Firebase (asset 4475, MIT) — l'unico plugin che porta FCM su iPhone senza linkare a mano l'SDK dentro il progetto Xcode che Godot rigenera a ogni export — dichiara Godot 4.7 o superiore. I due ticket sono quindi uno solo diviso in due.
SU-700 — l'aggiornamento del motore
- ✅ Il rischio numero uno del ticket è caduto al primo colpo: l'estensione EOS carica sotto 4.7 e
libeosg NON va ricompilata. Contava perché quella libreria è ricompilata a mano per i 16 KB (SU-280) e rifarla sarebbe stato il pezzo peggiore del giro. La conferma è la riga [UE4] Shutdown handler: initialize in testa al primo import: è l'SDK Epic che si accende. eosg.gdextension dichiara compatibility_minimum = 4.1 e la promessa è stata mantenuta. - La 4.7.2 è installata ACCANTO alla 4.6.2, non al posto:
/Applications/Godot47.app contro /Applications/Godot.app, e i due set di export template convivono in ~/Library/Application Support/Godot/export_templates/. Tutti gli script di progetto accettano già GODOT= / GODOT_BIN= / GODOT_APP=, quindi la prova non ha richiesto di toccarne nemmeno uno. - 📌 Il progetto è stato aperto in una COPIA, mai nel repo di lavoro:
cp -Rc (clone APFS) ha fatto 11 GB in 5,8 secondi. Stessa ragione della build di prova di SU-688: un import fatto da una versione nuova riscrive .godot/, e su un repo condiviso è roba che finisce nel commit di qualcun altro. - ✅ Cosa si è rotto nel gioco: niente. Import pulito (zero righe di errore), 656 file fra
.gd e .tscn verdi al compile-check, 8 quartieri su 8 PASS col bot, export macOS e Android riusciti, progetto Xcode generato. La 4.7 non ha riscritto una sola riga di project.godot: git diff vuoto dopo il primo import. - ⚠️ L'unico avviso sospetto è un falso allarme, e si è verificato invece di crederci: ogni run stampa «8 ObjectDB instances were leaked at exit». La stessa run sotto 4.6.2 stampa la stessa riga (senza il numero): non è una regressione della 4.7, è rumore di chiusura che c'era già.
- ⚠️ Le patch al template Android si sono fermate DUE volte, ed è esattamente ciò per cui erano state scritte così —
installa_template_android.sh si interrompe da solo dicendo quale ancora non trova, invece di applicare una patch a metà. Le due rotture e il rimedio:buildTools '35.0.1' non c'era più: la 4.7.2 spedisce 36.1.0, cioè un valore migliore del 36.0.0 che noi forzavamo, e la patch si è fermata solo perché non era la stringa attesa. Rimedio: almeno() confronta numeri, non stringhe — la soglia è un minimo, se il template è già pari o avanti non si tocca niente e non si arretra mai. Le tre patch di compileSdk/targetSdk/buildTools ora dicono tutte «già a posto», perché la 4.7.2 spedisce di suo l'API 36 che Play pretende dal 31 agosto.SplashScreen.installSplashScreen(this); è diventato SplashScreen splashScreen = SplashScreen.installSplashScreen(this);: una riga che non ci riguarda, e bastava a bloccare l'innesto di EOSSDK.init(). Rimedio: l'ancora è ora la sola firma di onCreate, cioè «la prima cosa dentro il metodo» — che cambia molto più di rado del suo corpo.
- ✅ L'APK del 4.7 è completo, verificato dentro il pacchetto e non solo dall'exit code:
lib/arm64-v8a/libEOSSDK.so (24 MB), le classi SUPushPlugin/SUPushService in classes3.dex, 675 riferimenti a com/google/firebase nei dex, e nel manifest fuso ci sono POST_NOTIFICATIONS, il meta-data del plugin SUPush e firebase_messaging_auto_init_enabled ancora spento (la protezione di SU-687 sul token che nascerebbe prima del consenso). - ✅
su_native_auth si ricompila contro la 4.7 senza toccare una riga di codice: build_su_native_auth.sh rigenera gdextension_interface.h dalla versione di Godot che gira, e le tre fette (iOS device, simulatore, macOS) escono col simbolo d'ingresso al suo posto. - ⚠️ Cosa NON è stato provato, e va detto: il gioco non è stato installato su un telefono vero né su iPad sotto 4.7, e il login EOS non è stato esercitato — è stato verificato che l'estensione *carica*, non che l'accesso Epic funzioni. Il gancio multiplayer del collaudo (
MP_CHECK=1) si ferma prima della rete, su «join_game» rifiutato: no_account: è il gate account di SU-447 che rifiuta un bot senza nome registrato. Verificato per controprova: la stessa run sotto 4.6.2 si ferma sulla stessa riga, quindi il gancio MP era già inservibile e non è un difetto del motore nuovo. - 🔁 Poi Ivan ha chiesto di togliere la doppia installazione, e
/Applications/Godot.app È diventata la 4.7.2 (la 4.6.2 spostata nel cestino, recuperabile). Con lo scambio è cambiato anche il default di installa_template_android.sh, che puntava ancora a 4.6.2.stable: era l'unico posto dove la vecchia versione sarebbe rientrata dalla finestra, reinstallando un template Android della 4.6 sotto un progetto 4.7. Riprovato senza nessun override: il template si rigenera come 4.7.2.stable e il compile-check dei 656 file gira sulla 4.7.2 con FALLITI 0.
SU-701 — le push su iOS
- Si installano DUE moduli su quattro, e non è per risparmiare spazio:
firebase_core (obbligatorio) e firebase_messaging. Analytics e Crashlytics restano fuori perché raccolgono dati, e ogni cosa che esce dal dispositivo è una riga nei moduli store e una base giuridica nell'informativa — per una funzione che nessuno ha chiesto. - ⚠️ La trappola del giro, trovata leggendo il
.gdip invece che il README: firebase_core si porta dentro FirebaseAnalytics e GoogleAppMeasurement lo stesso. Sono elencati fra i suoi embedded, non è una nostra scelta, e su iOS Firebase li accende da solo al primo avvio, prima che giri una riga di GDScript. Senza rimedio la build manderebbe a Google eventi che l'informativa non dichiara. Si spegne dall'Info.plist con FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED, che disattiva Analytics per l'intera app in modo definitivo e non riaccendibile a runtime — che qui è un pregio, non un limite. - 📌 Non è stato messo
FirebaseMessagingAutoInitEnabled=false, che su Android è la protezione chiave, e la ragione è che su iOS servirebbe a niente e farebbe danno. Su iOS il token FCM non può nascere prima della registrazione ad APNs, che avviene solo dentro request_permission(): il consenso viene prima per costruzione. Metterlo avrebbe invece rischiato di impedire per sempre la nascita del token, perché GodotX non espone nessun modo per riaccendere l'auto-init a runtime. tools/ios/installa_firebase_ios.sh, sul modello di quello Android: scarica la release 2.5.0 in cache, installa i due moduli, e i 123 MB di xcframework non entrano in git — stessa scelta già presa per l'addon EOS (489 MB) e il template Android (200 MB), che sono dipendenze terze rigenerabili in un comando.- ⚠️ Due valori sbagliati che sembravano giusti, entrambi scoperti guardando il progetto Xcode prodotto invece che l'exit code dell'export:
entitlements/push_notifications="Enabled" non è un valore valido: Godot ammette Disabled,Production,Development, e con «Enabled» scriveva aps-environment = enabled, che Apple avrebbe rifiutato a valle. Ora è Production, che è l'ambiente APNs di TestFlight, dove stanno i 28 tester iOS. Conseguenza da sapere: una build installata a mano sull'iPad in debug vorrebbe Development, e con questo preset non riceverebbe push.- Senza le chiavi
plugins/GodotxFirebaseCore=true e plugins/GodotxFirebaseMessaging=true nel preset, i moduli venivano RILEVATI ma non innestati: l'export usciva EXIT=0, con l'entitlement giusto e nessun framework Firebase dentro. È il modo perfetto di sbagliare in silenzio.
- ✅ Verificato nel progetto Xcode prodotto: gli xcframework stanno in
StreetUniversity/dylibs/ios/plugins/, il project.pbxproj elenca GodotxFirebaseCore, GodotxFirebaseMessaging, FirebaseMessaging, FirebaseCore accanto a EOSSDK, col flag -ObjC; l'entitlement dice aps-environment = production e l'Info.plist ha UIBackgroundModes = remote-notification. Il .pck resta a 55 MB: i 123 MB di framework non ci sono finiti dentro. PushNotifiche.gd ha imparato iOS mantenendo tutto ciò che conta: gli stessi due topic (su_beta/su_pubblico decisi da Env), lo stesso momento in cui si chiede il permesso, lo stesso pannello in tono col gioco (i cui testi erano già neutri: «IL BARBONE TIENE D'OCCHIO IL CASSONETTO» vale identico su iPhone), e lo stesso fail-open. ⚠️ Quello che cambia è il tempo: su Android si decide tutto dentro _ready(), su iOS niente è sincrono — Core e Messaging si accendono a segnali, e il permesso non si può nemmeno interrogare, perché Apple non ha nulla come OS.get_granted_permissions(). L'unico modo di sapere è richiedere: GodotX non riapre il dialogo se la domanda è già stata posta, quindi all'avvio si chiede solo se _gia_chiesto(), mai a freddo.- ✅ Fatti in serata, con lo script: l'app iOS è registrata sul progetto Firebase (
1:933602919752:ios:742cab05a6966df9dbdf7d), GoogleService-Info.plist è nel repo — versionato, come google-services.json di Android: è configurazione che viaggia dentro ogni copia dell'app, non un segreto — e la capability *Push Notifications* è attiva sull'App ID. Riprovato l'export: [Firebase] Adding iOS config: res://GoogleService-Info.plist, e il file finisce dentro il progetto Xcode. - ⚠️ Aggiungere la capability ha reso
INVALID tutti e due i profili di provisioning — «Street University Distribution» e «Street University AdHoc». Era annunciato e ora è misurato: vanno rigenerati con lo stesso nome prima del prossimo giro TestFlight, altrimenti testflight.sh non li trova. - ✅ La chiave APNs c'è, ed è caricata su Firebase per entrambi gli ambienti (
KLFK9K6H9H, team RD6E4K56FB). 📌 Dentro la creazione c'era una scelta irreversibile: nella schermata *Configure Key* l'Environment nasce su Sandbox, che da solo NON copre TestFlight (che usa APNs *production*, come dice il nostro entitlement) — e Apple avverte che dopo il salvataggio non si cambia più. Messo su Sandbox & Production, Key Restriction su *Team Scoped (All Topics)*.- ⚠️ E una svista che sarebbe costata l'intera feature: nella riga «produzione» della console Firebase l'ID chiave era stato scritto
KLFK9K6H9, nove caratteri invece di dieci. Gli ID Apple sono sempre dieci: Firebase accetta l'upload senza fiatare, e le push ai 28 tester iOS non sarebbero mai arrivate, senza un errore da nessuna parte. Vista rileggendo lo scatto della pagina, non da un messaggio d'errore.
- ✅ I due profili rigenerati con
--profili e installati dove testflight.sh li cerca davvero: Street University Distribution e Street University AdHoc, entrambi di nuovo ACTIVE e con aps-environment = production dentro.- ⚠️ Il trabocchetto che vanificava tutto il resto: rigenerare un profilo NON sostituisce il file vecchio, ne aggiunge uno accanto con lo stesso
Name e un UUID diverso. E sia testflight.sh sia Xcode risolvono per nome: con due omonimi potevano pescare quello vecchio, che non ha aps-environment perché è anteriore alla capability — cioè una build firmata senza il diritto alle push, e nessun errore a dirlo. In cartella c'erano sei file e due nomi doppi. Ora lo script sposta i superati in scaduti/ (spostati, non cancellati: un profilo di firma buttato via non si recupera).
- ⚠️ Cosa resta: la prova vera — export, TestFlight,
manda_push.py --topic su_beta, e una notifica sull'iPad col gioco chiuso — e poi i moduli store (App Privacy e §3.4 dell'informativa), che si toccano dopo che iOS raccoglie davvero il token, mai prima. tools/ios/prepara_push_ios.sh fa da solo i punti 1 e 3 di quella lista — registra l'app iOS su Firebase, scarica il plist, attiva la capability — e dei due che restano stampa i passi nell'ordine giusto. Ha --prova, come manda_push.py, e in quella modalità è già stato lanciato contro le API vere: legge il progetto Firebase e le capability dell'App ID senza scrivere niente. ⚠️ Non tocca i profili di provisioning di proposito: aggiungere la capability li invalida e vanno rigenerati con lo stesso nome, ma cancellare un profilo di distribuzione è l'unica azione del giro che, se va storta, blocca una release — quella resta un gesto deliberato di Ivan.- 📌 Un avviso cosmetico lasciato in piedi di proposito: su disco la cartella si chiama
files/homeless_city/iOS/ (maiuscola, ci vivono icona e splash) mentre Godot cerca res://ios/plugins/, e su macOS il filesystem insensibile alle maiuscole li fa combaciare stampando un avviso di «Case mismatch» a ogni export. Non ha effetto qui — l'export iOS gira solo su Mac — ma su un sistema case-sensitive il plugin non verrebbe trovato. Rinominare la cartella tocca file tracciati e i percorsi dell'icona: è un ticket suo, non un pezzo di questo.
Epic SU-686: le notifiche push, dal progetto Firebase alla notifica sull'Honor2026-08-31
Lavorata l'epic N1 — Notifiche push (FCM): SU-687 (prova su Android), SU-688 (il permesso), SU-689 (moduli store e privacy), SU-690 (manda_push.py e l'innesto in /release).
- ✅ Il criterio di SU-687 è soddisfatto e si vede: una notifica mandata dal Mac è arrivata sull'Honor 10 col gioco chiuso — «Il barbone ha trovato roba nuova nel cassonetto». Scatti in
TMP/su687_push/notifica_gioco_chiuso.png (tendina) e notifica_espansa.png (con anche la riga inglese). ⚠️ Il gioco è stato chiuso con am kill, non con am force-stop, e la differenza è tutta: force-stop mette il pacchetto nello stato «stopped», dove Android per progetto non gli consegna più niente finché non lo riapri a mano — una prova fatta così sarebbe stata un falso negativo perfetto, con la push che non arriva per colpa del modo in cui l'hai chiusa. - Il progetto Firebase è agganciato al progetto Google Cloud esistente,
street-university-pubblisher / axiomatic-path-505007-u7, numero 933602919752. ⚠️ Il nome ovvio era la trappola: nell'elenco c'è anche un progetto chiamato «Street University» (gen-lang-client-0874937409), nato da AI Studio e vuoto, senza un solo client OAuth. Verificato aprendo /auth/clients su entrambi PRIMA di scegliere — e aggiungere Firebase a un progetto non si annulla, quindi lo sbaglio sarebbe stato definitivo. - ⚠️ Il piano non è Spark ma Blaze, e l'epic diceva Spark. Siccome quel progetto GCP ha già la fatturazione attiva, Firebase ci entra in pagamento a consumo. Per FCM non cambia niente — è gratis su entrambi i piani — ma la premessa scritta nel design era inesatta, e va saputa prima di accendere altri servizi Firebase, che gratis non sono. Google Analytics è stato spento in creazione: era acceso di default, non serve alle push, e avrebbe aggiunto una raccolta dati nuova da dichiarare agli store — l'opposto di quello che fa questa epic.
- 📌 La scoperta che ha cambiato il codice: di serie il token FCM nasce al primo avvio, da solo, prima che il gioco abbia chiesto niente a nessuno. Non si vede provando l'app — è saltata fuori compilando il modulo «Sicurezza dei dati» e chiedendosi cosa si stesse dichiarando. Con quel comportamento la risposta onesta sarebbe stata «È richiesta la raccolta», e l'informativa privacy non avrebbe potuto dire che la base giuridica è il consenso. Rimedio:
firebase_messaging_auto_init_enabled=false nel manifest e setAutoInitEnabled(true) solo dentro push_subscribe, che parte solo a notifiche permesse. Verificato sull'emulatore Android 16: all'avvio, senza permesso, il log dice [Push] notifiche non permesse: nessuna iscrizione, nessun token — mentre sull'Honor (Android 10, dove il permesso non esiste) si iscrive e il token nasce. ⚠️ Se qualcuno rimette l'auto-init a true, la casella del modulo Play va cambiata in «Richiesta» nello stesso commit. - Il topic lo decide
Env, non un interruttore nuovo: dev/stage → su_beta, live → su_pubblico. Così il giorno del lancio pubblico non si tocca il codice del client. Scartato il beta-gate come criterio: BetaGate gira solo nelle build desktop, cioè proprio dove le push non arrivano — sarebbe stato un criterio che sul telefono non risponde mai. - 📌 Innesto Gradle senza il plugin
google-services, che è la strada ufficiale. Quel plugin fa una cosa sola: legge google-services.json e ne genera sei stringhe di risorsa. Averlo costava due ancore in più nel template (una in settings.gradle, dentro pluginManagement) e una versione compatibile con l'AGP — due punti che si rompono al prossimo aggiornamento di Godot, per generare sei stringhe. Le generiamo con resValue, che è lo stesso gesto già usato per eos_login_protocol_scheme. Il log del telefono dice FirebaseInitProvider: FirebaseApp initialization successful: funziona. - ⚠️ Tutto l'innesto FCM è condizionato a
google-services.json: se il file non c'è, installa_template_android.sh lo salta e la build esce identica a prima, senza push e senza errori. Provato in tutti e due i versi prima di scaricare il file. tools/release/manda_push.py (SU-690): JWT firmato con openssl (non con la wheel cryptography di play_upload.sh: su questo Mac le wheel hanno già fatto danni quando un timeout di Homebrew ha trascinato python3 sotto Rosetta), scambio OAuth2 e POST a FCM v1, tutte le chiamate con curl perché python3 qui non ha i certificati CA. La chiave privata passa da un file descriptor effimero, mai da un file su disco. Provata la firma anche contro la chiave pubblica: Verified OK.- Il topic è obbligatorio e non ha default: lanciato senza, si ferma e non manda niente (provato). Un topic inventato viene rifiutato (provato). Un testo che dice «aggiorna» viene rifiutato con la spiegazione — su Android Play ha già aggiornato da sé, e il messaggio giusto dice cosa c'è di nuovo (provato, e si zittisce con
--anche-se-dice-aggiorna). --prova stampa il messaggio senza mandarlo e funziona anche senza la chiave: chiedere una credenziale per NON mandare niente renderebbe scomodo proprio il giro che deve essere facile fare, cioè far rileggere il testo prima di spedirlo.
- SU-689, la parte store e privacy. Riga nuova nel modulo Play: ID dispositivo o altri ID, raccolto, condiviso con Google, Facoltativo, scopo «Comunicazioni dello sviluppatore». CSV pronto da importare in
STORE_ASSETS/google_play/data_safety_2026-08-31.csv (Google ha import/export CSV: non si ricompila a mano). La casella era vuota fino a oggi e la nota che diceva «resta VUOTA» è stata corretta invece che lasciata a contraddire la tabella.- 🔎 Il «Condiviso: Sì» è la risposta prudente, non l'unica difendibile: Google tratta il token da *service provider*, e con quella lettura varrebbe la stessa logica già applicata al PUID di SU-444 (raccolto sì, condiviso no). Il ticket chiedeva «condivisione con Google» e fra le due si è scelta quella che dichiara di più — su Play si viene puniti per aver dichiarato meno del vero.
- ⚠️ Su Apple NON si dichiara niente, ed è la risposta giusta oggi. Il ticket chiedeva «la stessa dichiarazione anche in App Privacy», ma la versione iOS non genera nessun token: le push iOS sono il ticket 5 dell'epic ed è esplicitamente rinviato. Dichiarare un identificativo che iOS non raccoglie sarebbe una dichiarazione falsa in eccesso. Stessa ragione per la capability *Push Notifications* sul profilo: si aggiunge quando si scrive il codice APNs, non prima — e rigenerare adesso il profilo di firma per una funzione che non esiste significherebbe toccare la catena iOS per niente.
- ⚠️ L'informativa privacy esisteva in DUE copie identiche byte per byte, e i documenti di progetto ne nominavano una sola. Quella che gli store dichiarano è l'altra:
street-university-site/privacy/index.html, servita su streetuniversitygame.com/privacy/. Aggiornarne una sola avrebbe lasciato l'URL depositato nel modulo Play a puntare a un'informativa senza le push. Aggiornate e pubblicate entrambe (versione 1.3, §3.4 in italiano e inglese) e verificate sulle pagine online, non solo in locale. - SU-688, il permesso: si chiede da un solo punto —
HUD._setup_gameover_menu(), l'unico in cui il giocatore ha appena visto il proprio punteggio ed è fermo — con una riga sola nell'HUD e tutte le guardie dall'altra parte (prima run, una volta sola, non sotto Android 13, non col bot, non in demo, non in headless). Prima del dialogo di sistema c'è un pannello nostro: quello di Android non si può riscrivere, dice «Consentire a Street University di inviare notifiche?» in tono da sistema operativo, e soprattutto si può mostrare una volta sola per sempre — bruciarlo su chi non ha capito cosa gli si chiede è definitivo.- 📌 La domanda si segna come «posta» PRIMA di mostrarla, non dopo la risposta. Se il gioco viene chiuso o ucciso mentre il pannello è a schermo, la domanda risulta comunque fatta: altrimenti basterebbe chiudere l'app per rivedersela a ogni partita, che è proprio il comportamento che il ticket vieta.
- Non si aspetta l'esito per decidere cosa fa il gioco (fail-open: fa la stessa identica cosa nei due casi), ma lo si guarda per sapere se iscriversi — perché è lì che nasce il token. L'attesa è a interrogazione e non a segnale: Godot consegna l'esito del permesso dal MainLoop con un nome cambiato fra le versioni, e legarcisi vorrebbe dire un file che smette di compilare al prossimo aggiornamento del motore.
- Testo e impaginazione da approvare: 7 scatti in
TMP/su688_permesso/ (it, en, de, ru, zh a 1024×768 più de/it a 1200×540). Il tedesco — la lingua più lunga — sta dentro il cartone anche alla misura stretta, e il cinese rende senza caratteri mancanti. - ⚠️ Quello che NON è provato, ed è la metà che conta: che il pannello compaia dopo la prima run su un telefono vero. L'Honor 10 è Android 10 (API 29), sotto la soglia dei 13: lì il permesso non esiste e il pannello — correttamente — non compare mai. Sull'emulatore Android 16 il gioco parte e i log sono giusti, ma
QueuePresentKHR fallisce a ogni frame (swapchain Vulkan dell'emulatore, non del gioco): schermo nero allo scatto, provato con due modalità GPU diverse. Quindi è verificato che il pannello non compare prima (35 s di avvio, splash e menu senza una riga di dialogo) e che il permesso non nasce da sé; resta da provare che compaia dopo, e serve un Android 13+ vero. Il provino shot_su688_permesso.gd non copre quel pezzo, e lo dice nella propria testata: si costruisce da sé il pannello, quindi esclude per definizione il ramo in cui il pannello non arriva.
I quattro design che aspettavano Ivan: le sue risposte diventano decisioni scritte2026-08-31
Quattro ticket (DESIGN) erano fermi non perché mancasse l'analisi, ma perché finivano con una domanda aperta. Ivan ha risposto a tutte e quattro fra il 30 e il 31/08, e i documenti recepiscono le risposte. Nessuno di questi tocca codice di gioco.
- SU-617 — «metterei già rosso dal 50% così la gente si sveglia prima». Il design era approvato («OK facciamolo così») con una modifica sola: la soglia del rosso. 📌 Non nasce un ticket nuovo: è una modifica a SU-675, che è già codice scritto (
HUD.gd:594-624 e 3795-3900) — il colore SOFT passa da ambra a rosso chiaro. SU-676 non cambia. Distinguere una modifica a un ticket esistente da un ticket nuovo è la differenza fra due righe di diff e un giro di lavorazione. - SU-620 — «il banner grosso lo metterei a destra di vita e livello… e lo toglierei da sotto». Ivan sceglie spostare, non sdoppiare: l'opposto della raccomandazione del 28/08, che resta nel documento marcata come superata con data e motivo, non cancellata.
- 📌 La conseguenza è stata misurata, non assecondata, e il numero è severo: il banner di oggi è 340×112, il corridoio fra vite e colonna è 136×68 sul tablet. Non ci sta su nessuna delle due dimensioni, e ⚠️ il vincolo vero è l'altezza — 112 contro 68 — che sfora allo stesso modo su tablet e telefono. Non è «il tablet è la forma stretta», come diceva la misura del 28/08 riferita alla larghezza: quella zona è bassa su entrambe le forme.
- Risposta esplicita alla domanda «riapre SU-272?» (il KO che Ivan stesso scrisse il 04/08 perché il banner in alto copriva la città): sì se il banner resta grande e viene incastrato lì invadendo i vicini; no se si restringe per starci — l'area passa da 38.080 a 9.248 unità quadre, il 24% di oggi. A quella taglia però «il banner grosso» smette di essere grosso e diventa, di fatto, il *promemoria compatto* che la raccomandazione superata proponeva. ⚠️ Rischio segnalato e non deciso: sul telefono 136 unità-canvas sono ~69 px fisici, la stessa taglia già misurata come illeggibile.
- SU-640 — «se la base dati fosse su un Firebase db cambierebbe la velocità?». Risposta con le fonti citate e datate (quote ufficiali Firestore, verificate il 31/08), non con un aggettivo: Firestore con vincolo di unicità su
nickname_lower elimina i 106,2 s del caso peggiore per costruzione, perché l'unicità la garantisce l'indice e le rivendicazioni smettono di stare in fila. ⚠️ Non c'è un deployment vero da cronometrare — nessun account Firebase esiste ancora — e questo è dichiarato invece che stimato. Nessun ticket nuovo: i cinque già proposti restano, e SU-640d ne esce meglio giustificato. - SU-678 — «TestFlight adesso, beta separata dal pubblico, non aprire telegram per desktop». Le tre domande in fondo al documento diventano tre decisioni: iOS subito (quindi il primo ticket-cancello non è più solo Android), due topic distinti beta/pubblico, e niente Telegram — i 19 desktop restano coperti da SU-619 e WhatsApp. ⚠️ La parola «KO» apriva il commento, ma le tre risposte non ribaltano la scelta di FCM: la confermano e chiudono le domande.
- Consegna operativa:
TMP/lotto4_ticket_proposti.md (radice del repo) — 8 ticket nuovi e 1 modifica, raggruppati per ticket-padre e in ordine di esecuzione. Non sono stati creati su Jira: aspettano l'ok di Ivan. - ⚠️ Il lotto è morto a metà per una sospensione del Mac (errore API «your computer went to sleep»), a documenti già scritti e file dei ticket ancora da fare. È stato ripreso dal punto esatto invece che rilanciato: rifare i quattro documenti sarebbe costato un lotto intero per riscrivere ciò che era già su disco.
I cinque design diventano diciotto ticket di sviluppo2026-08-29
- [doc] Creati SU-659 → SU-676 nello Sprint attivo, nati dalle decisioni prese oggi nei cinque documenti di design. SU-642 escluso su richiesta di Ivan (l'ha chiuso lui: il verdetto era «non serve», quindi non c'e' niente da implementare); i quattro ticket di codice del giro — SU-610, SU-611, SU-616, SU-619 — erano gia' implementati e non generano nulla.
- Punteggio (da SU-628), 5 ticket: SU-659 il punteggio a schermo che segue i soldi — e' quello che chiude il KO, senza il resto resta invisibile come prima; SU-660 i soldi in tasca a 1 pt/$; SU-661 le lattine a 6 pt per evento; SU-662 il giullare a 300 pt con segnale dedicato; SU-663 il giornale di fine partita piu' la misura col bot che sostituisce la stima a tavolino.
- Metropolitana (da SU-624), 5 ticket: SU-664 topi dal giro 2 (con dentro la correzione del commento sbagliato a
BonusLevel.gd:41, la riga che aveva fatto sbagliare un giro intero del design); SU-665 il pendolare zombie dal giro 3; SU-666 la misura che blocca la taratura degli altri due; SU-667 il topo spazzato dal treno; SU-668 sprite e suono. - Cani (da SU-626), 3 ticket: SU-669 la carta e il super IL BRANCO; SU-670 sprite e icone — ⚠️ con dentro il lavoro vero che il design ha scoperto:
super_icons.png e' 5×2 e tutte e dieci le celle sono occupate, serve allargare il foglio; SU-671 il verso dei cani, che ripiega su throw.wav finche' non c'e', per non bloccare il super. - Piccioni (da SU-625), 3 ticket: SU-672 i piccioni normali col dado separato
_rng_piccioni; SU-673 il piccione d'oro con notifica e scia; SU-674 gli sprite. - HUD (da SU-617), 2 ticket: SU-675 il pannello che lampeggia a tre gradini via
self_modulate, col divieto esplicito di overlay fullscreen scritto nei criteri; SU-676 il suggerimento della panchina, solo per il sonno. - 📌 Le tre generazioni di sprite sono ticket a se' (SU-668, SU-670, SU-674), come vuole il flusso del progetto: l'approvazione delle varianti e' di Ivan e non si mescola con l'implementazione che le monta.
- 📌 Scritti in un file solo e creati in una corsa (
jira_scrivi.sh --prova --sprint e poi senza --prova): 18 validati prima di mandarne uno, e in risposta solo le chiavi. Lunghezze fra 349 e 2.104 caratteri. - ⚠️ Trappola del formato, che vale per la prossima volta: nel corpo dei ticket il wiki markup Jira usa
# per le liste numerate, e collide col # che separa i ticket nel file. Lo script ne contava 29 invece di 18. Rimedio: nel corpo si usano elenchi puntati *, e la riga di intestazione si riconosce perche' contiene i |.
RIENTRI ATTESI (messi In revisione il 2026-08-29, giro dei BOCCIATI): dieci chiavi — SU-610, SU-611, SU-616, SU-617, SU-619, SU-624, SU-625, SU-626, SU-628, SU-642. Al prossimo giro, prima dei lotti, si conta quante tornano in «Da fare» e con quale KO.
*(Giro precedente: zero rientri sulle dodici chiavi del referto telemetria — SU-646, SU-647, SU-648, SU-649, SU-650, SU-651, SU-652, SU-653, SU-654, SU-655, SU-656, SU-657: nessuna è tornata indietro, ma Ivan non le ha ancora provate su device, quindi il conto vero si fa al giro dopo. I dieci lavorati oggi vengono invece dal giro dei bocciati del 28-29/08.*
*⚠️ E il numero grezzo va letto, non solo contato: sei dei dieci — SU-617, SU-624, SU-625, SU-626, SU-628, SU-642 — erano documenti di design che finivano con una domanda aperta a Ivan. Il loro «KO» è la sua risposta, cioè il flusso che funziona, non un difetto nostro: è lo stesso schema già annotato il 21/08, quando 6 rientri su 34 finivano tutti con una domanda invece che con un errore. I rientri per difetto vero sono quattro: SU-616 (tazzina sbagliata: avevo scelto la variante senza che lui avesse visto lo screenshot), SU-611 (faccia del sole che leggeva allegra, raggio largo solo da spento), SU-610 (cactus fermi), SU-619 (mancava l'host, ed era una scelta esplicitamente lasciata a lui). Su quattro, tre sono difetti di resa visiva — che resta la prima causa di KO di questo progetto.)*