diff --git a/docs/3 - Protocolli.md b/docs/3 - Protocolli.md index 41b0168..2da7e80 100644 --- a/docs/3 - Protocolli.md +++ b/docs/3 - Protocolli.md @@ -161,6 +161,8 @@ I canali di trasferimento attualmente disponibili sono: - `unreliable_ordered`, che aggiunge un contatore ad ogni pacchetto inviato tramite UDP, facendo in modo che i peer possano ignorare i pacchetti arrivati nell'ordine sbagliato; - `reliable`, che implementa un protocollo di acknowledgement su UDP molto simile a quello utilizzato da TCP, a costo di una latenza significativamente maggiore. +> Dove non specificato, in questa relazione si assume che una RPC descritta usi sempre il canale `reliable`. + ## Nodi di sincronizzazione Godot mette a disposizione dei `Node` speciali per assistere con la sincronizzazione dello stato del gioco tra tutti i peer. diff --git a/docs/4 - Architettura.md b/docs/4 - Architettura.md index 0c1ec0a..96b0248 100644 --- a/docs/4 - Architettura.md +++ b/docs/4 - Architettura.md @@ -16,7 +16,7 @@ Il `Node` `Main` è alla radice di ogni istanza del gioco, e si occupa di inizi ## [`Game`](../scenes/game.gd) -Il `Node` `Game` viene collocato sempre come figlio di `Main`, e rappresenta lo stato di gioco di un peer, che esso sia un server o un client. +Il `Node` `Game` viene collocato sempre come figlio di `Main`, e rappresenta lo stato di gioco di un peer connesso ad una specifica stanza (anche detta "lobby" in inglese), che esso sia un server o un client. Il server ha sempre autorità su di esso, e: @@ -26,24 +26,136 @@ Il server ha sempre autorità su di esso, e: - quando il giocatore host richiede attraverso l'interfaccia grafica di iniziare la partita, usa il `PhaseTracker` per cambiare ovunque la fase della partita da `LOBBY` a `PLAYING`, e richiede al `LevelManager` di istanziare il primo livello; - quando il `LevelManager` segnala che la `LevelPlaylist` selezionata è esaurita, usa il `PhaseTracker` per aggiornare la fase della partita da `PLAYING` a `ENDED`. +![](img/game.png) + ## [`PeerNodeDirectory`](../scenes/peernode_directory.gd) +Il `Node` `PeerNodeDirectory`, essendo contenuto nella scena `game.tscn`, viene istanziato ricorsivamente in tutte le istanze di `Game`. + +Come menzionato in precedenza, ha la responsabilità di mantenere aggiornato l'indice dei peer connessi, e a tale scopo rende disponibili al server le remote procedure call `rpc_create_peernode(peer_id)` e `rpc_destroy_peernode(peer_id)` in modo da permettere al server (e solo al server) di informare tutti i client dello stato attuale dei client connessi. + +![](img/peernodedirectory.png) + ## [`PeerNode`](../scenes/peernode.gd) +Il `Node` `PeerNode` rappresenta un singolo peer connesso, di cui ne è la `multiplayer_authority`, e ha il suo identificatore impostato come proprietà `name`. + +Ha un singolo metodo, che può essere chiamato come RPC solamente dal peer che controlla il nodo stesso: `rpc_identify`, che comunica al server il nome con cui il peer appena connesso si identifica agli altri. + +Quando il server riceve un'identificazione di un peer, il relativo `PeerNode` emette il segnale `identified`, che viene propagato in alto fino a `Game`, il quale lo gestisce come descritto sopra. + ## [`PlayerNodeDirectory`](../scenes/playernode_directory.gd) +Il `Node` `PlayerNodeDirectory` è molto simile a `PeerNodeDirectory`, ma invece che dei peer, tiene traccia dei giocatori effettivi nella partita, indicizzati per nome del giocatore rappresentato. + +Dato che un giocatore, una volta entrato in una lobby, deve mantenere il proprio punteggio fino a quando la partita non è terminata, anche se non l'ha terminata perchè l'ha abbandonata a metà, non è incluso un modo per rimuovere un giocatore dall'indice, ma soltanto uno per aggiungerne uno nuovo. + +Questo metodo è `rpc_possess_playernode(player_name, peer_id)`, chiamabile solo dal server, che controlla se un giocatore con il dato nome è già indicizzato, creando il relativo `PlayerNode` in caso negativo, e in entrambi i casi (ri)assegnando l'autorità su di esso al peer con l'identificatore specificato. + +In aggiunta, è disponibile `rpc_push_reported_scores()`, chiamabile anch'essa solo dal server, che fa in modo che tutti i `PlayerNode` figli spostino in una lista il numero di colpi impiegati per entrare nella buca attuale (`reported_score`, descritto in seguito), resettando poi il contatore in preparazione alla buca successiva. + +![](img/playernodedirectory.png) + ## [`PlayerNode`](../scenes/playernode.gd) +Il `Node` `PlayerNode` rappresenta un singolo giocatore nella partita, e la sua `multiplayer_authority` può variare in base ai peer attualmente connessi alla partita: essa prende il valore dell'ultimo peer che si è identificato con il relativo nome, se esso è ancora connesso, altrimenti l'autorità è assegnata al server fino a quando un nuovo peer non si riconnetterà con quel nome. + +Esso include tante proprietà e metodi, in quanto è ciò che permette ai dati di uno specifico giocatore di persistere tra un livello e l'altro. + +Tra questi, sono degni di nota: +- `sanitize_player_name`, una funzione statica che sostituisce con un underscore (`_`) tutti i caratteri non validi all'interno per il nome di un nodo (`/`, `*`, ` `) dal nome del giocatore; +- `player_name`, proprietà che assieme al nome del nodo stesso riflette il nome del giocatore (necessaria per via di un dettaglio implementativo di come Godot gestisce le stringhe nei nomi); + - `rpc_set_name`, remote procedure call chiamabile solo dall'autorità che modifica il nome del giocatore in questione + - il segnale `name_changed`, emesso quando il nome viene cambiato; + - `rpc_query_name`, remote procedure call chiamabile da qualsiasi peer che richiede all'autorità di ripetere `rpc_set_name` con il nome attuale, in modo da comunicarlo anche ad un peer che si è connesso successivamente alla prima chiamata; +- `player_color`, proprietà che rappresenta il colore del giocatore; + - `rpc_set_color`, authority RPC che modifica il colore; + - il segnale `color_changed`, emesso quando il colore viene cambiato; + - `rpc_query_color`, che fa ripetere `rpc_set_color` all'autorità con il colore attuale; +- `possess`, funzione di utilità che modifica la `multiplayer_authority` del nodo al valore dato; + - il segnale `possessed`, emesso quando la `multiplayer_authority` è modificata attraverso `possess`; +- `hole_scores`, lista di `int` che rappresentano i colpi richiesti dal giocatore per entrare in buca nelle buche precedenti; + - `rpc_set_scores`, remote procedure call chiamabile solo dall'autorità che modifica la lista dei punteggi; + - `rpc_query_scores`, che fa ripetere `rpc_set_scores` all'autorità con i punteggi attuali; + - il segnale `scores_changed`, emesso quando i punteggi vengono cambiati; +- `reported_score`, proprietà che assume il valore di `-1` mentre il giocatore sta affrontando la buca, e ottiene un valore corrispondente al numero di colpi impiegati per terminarla una volta finita; + - `rpc_report_score`, remote procedure call chiamabile solo dall'autorità che imposta un reported score; + - il segnale `score_reported`, emesso quando viene impostato un `reported_score` tramite `rpc_report_score`. + ## [`PhaseTracker`](../scenes/phase_tracker.gd) +Il `Node` `PhaseTracker` si occupa di tenere traccia della fase in cui si trova la partita in ogni dato momento. + +Le fasi possibili, rappresentate dall'enumerazione `Phase` e immagazzinate nella proprietà `phase`, sono: +- `LOBBY`, il momento pre-partita in cui viene visualizzato l'elenco dei giocatori connessi ed è data all'host la possibilità di iniziare; +- `PLAYING`, il momento in cui la partita sta venendo disputata; +- `ENDED`, il momento in cui la partita è terminata, e stanno venendo mostrati a schermo i risultati. + +La sua autorità è sempre posseduta dal server, e come `PlayerNode`, include una coppia di RPC per modificare la fase attuale, `rpc_set_phase` e `rpc_query_phase`, più un segnale `phase_changed` emesso quando la fase viene effettivamente cambiata. + +![](img/phasetracker.png) + ## [`LevelPlaylist`](../scenes/level_playlist.gd) +Il `Node` `LevelPlaylist` contiene una lista `levels`, che in riferimenti a scene rappresentanti i livelli precostruiti che il server invierà ai client. + +L'indice del livello attuale viene salvato nella proprietà `idx`, mentre una funzione `advance` restituisce il prossimo livello dalla lista emettendo simultaneamente il segnale `advanced(level)`. + +Sui client, `idx` rimane sempre fisso a `-1`, in quanto essi non caricano il livello da una lista predefinita, ma lo costruiscono dinamicamente in base ai dati che il server invia loro. + +![](img/levelplaylist.png) + ## [`LevelManager`](../scenes/level_manager.gd) +Il `Node` `LevelManager` è gestito dal server, e si occupa di inizializzare i livelli sia su client, sia su server. + +Fa uso di tre RPC chiamabili solo dal server: +- `rpc_wipe_level`, che decostruisce il livello attuale, se presente, e ne inizializza uno nuovo, preso dalla `LevelPlaylist`; +- `rpc_destroy_level`, che distrugge il livello attuale e non fa nient'altro, usato per procedere in sicurezza alla schermata dei risultati; +- `rpc_level_completed`, che emette il segnale `level_completed` su tutti i client. + +Sono inclusi numerosi segnali che corrispondono alle varie fasi del ciclo di vita di un livello, come `level_destroying`, `level_destroyed`, `level_determined`, `level_initialized`, `level_built` e `level_completed`. + +Un segnale specifico è relativo al ciclo di vita dell'intera playlist: quando i livelli disponibili sono esauriti, viene emesso il segnale `playlist_complete`, che viene poi raccolto da `Game` per avanzare alla fase `ENDED`. + +![](img/levelmanager.png) + ## [`GolfLevel`](../scenes/golf_level.gd) +Il **`Node2D`** `GolfLevel` è ancora una volta gestito dal server, e contiene gli elementi "effettivi" della buca che i giocatori andranno ad affrontare. + +Si può comportare in due diversi modi in base al contesto in cui viene usato: +- gli può essere specificata nella proprietà `target` una scena contenente un altro `GolfLevel` da `LevelManager`; essa verrà istanziata senza essere aggiunta al mondo di gioco, poi da quella scena verranno lette varie proprietà dai nodi `FollowCamera`, `GolfTee`, `GolfHole` e `TileMap` presenti in essa e attraverso RPC saranno replicate su tutti i peer; +- gli può essere specificato `null` nella proprietà `target`, rimanendo così in attesa di ricevere RPC dal server per costruire il livello secondo istruzioni. + +![](img/golflevel.png) + +### Replicazione remota di `GolfLevel` + +La replicazione remota di un livello segue i seguenti passi: + +1. il server legge la `global_position`, `global_rotation` e `global_scale` della `TileMap` target, poi chiama `rpc_build_tilemap` usandoli come argomenti, istanziando nel mondo di gioco una griglia vuota nella stessa configurazione di quella target; +2. il server itera su tutti i tile della `TileMap` target, e per ciascuno chiama `rpc_build_tilemap_cell` passandogli le caratteristiche del tile da copiare, istanziando così nel mondo di gioco i "muretti" della buca; +3. il server guarda la posizione del `GolfTee` target, e usa `rpc_build_tee` per ricrearlo lì, connettendo inoltre il suo segnale `everyone_entered_hole` a una sua funzione interna che fa arrivare `level_completed` a `LevelManager` dopo qualche secondo; +4. il server itera su tutti i `PlayerNode` presenti nella `PlayerNodeDirectory` del `Game` attuale, e per ognuno di essi chiama `rpc_build_ball` con le relative caratteristiche, che includono la posizione, il numero attuale di colpi effettuati, e se la pallina è entrata in buca; +5. il server guarda la posizione e la dimensione della `GolfHole` target, e chiama `rpc_build_hole` passandoglieli come parametri, +6. il server guarda la posizione di `FollowCamera`, e controlla il parametro `camera_follows_payer` del target, per poi istanziare una nuova `FollowCamera` nel mondo di gioco e, se `camera_follows_player` è attivato, la configura per seguire la `GolfBall` controllata dal giocatore locale. + ## [`GolfTee`](../scenes/golf_tee.gd) +Il `Node2D` `GolfTee` è sempre gestito dal server, e funziona sia da directory delle "palline" che i giocatori controllano nel corso del livello, sia come loro istanziatore. + +Non contiene remote procedure call, perchè la `GolfLevel` ha la responsabilità di richiedere ai peer la creazione di una nuova entità del livello, ma contiene un metodo `spawn` di cui `rpc_build_ball` fa uso. + +Connette i segnali `name_changed`, `color_changed`, `possessed` emessi dal `PlayerNode` alle `GolfBall` `spawn`ate per rimanere sincronizzata con lo stato della stanza: ad esempio, se il giocatore dovesse disconnettersi, la relativa `GolfBall` verrebbe nascosta dal mondo di gioco, ma comunque mantenuta, in modo che qualora il giocatore si riconnetterà, continuerà a disputare la buca dalla posizione in cui si è disconnesso. + +Inoltre, ogni volta che una `GolfBall` da esso istanziata entra nella buca, `GolfTee` chiama `rpc_report_score` sul relativo `PlayerNode`, e poi controlla se tutte le palline dei giocatori connessi (e quindi la cui `multiplayer_authority` del relativo `PlayerNode` è diversa da quella del server) sono entrate in buca, emettendo esso stesso il segnale `everyone_entered_hole`, che `GolfLevel` usa per determinare quando avanzare di livello. + +![](img/golftee.png) + ## [`GolfBall`](../scenes/golf_ball.gd) + + + ## [`PuttController`](../scenes/putt_controller.gd) diff --git a/docs/img/game.png b/docs/img/game.png new file mode 100644 index 0000000..5cb20c5 Binary files /dev/null and b/docs/img/game.png differ diff --git a/docs/img/game.png.import b/docs/img/game.png.import new file mode 100644 index 0000000..c6ea7e0 --- /dev/null +++ b/docs/img/game.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://by4gofram0cbi" +path="res://.godot/imported/game.png-fcdac8ac86fa7d0c58685bcddac3a78a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/img/game.png" +dest_files=["res://.godot/imported/game.png-fcdac8ac86fa7d0c58685bcddac3a78a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/img/golflevel.png b/docs/img/golflevel.png new file mode 100644 index 0000000..50569bf Binary files /dev/null and b/docs/img/golflevel.png differ diff --git a/docs/img/golflevel.png.import b/docs/img/golflevel.png.import new file mode 100644 index 0000000..e20fa06 --- /dev/null +++ b/docs/img/golflevel.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dvuju4q6alwyp" +path="res://.godot/imported/golflevel.png-d27e4983f59b7723a043af42b28f8be5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/img/golflevel.png" +dest_files=["res://.godot/imported/golflevel.png-d27e4983f59b7723a043af42b28f8be5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/img/golftee.png b/docs/img/golftee.png new file mode 100644 index 0000000..e931126 Binary files /dev/null and b/docs/img/golftee.png differ diff --git a/docs/img/golftee.png.import b/docs/img/golftee.png.import new file mode 100644 index 0000000..0a18c3b --- /dev/null +++ b/docs/img/golftee.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://chxkkxrdxhdy3" +path="res://.godot/imported/golftee.png-252e0b2f30e9a9c0c38ea9fb7be1663b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/img/golftee.png" +dest_files=["res://.godot/imported/golftee.png-252e0b2f30e9a9c0c38ea9fb7be1663b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/img/levelmanager.png b/docs/img/levelmanager.png new file mode 100644 index 0000000..b6980d0 Binary files /dev/null and b/docs/img/levelmanager.png differ diff --git a/docs/img/levelmanager.png.import b/docs/img/levelmanager.png.import new file mode 100644 index 0000000..381e4a5 --- /dev/null +++ b/docs/img/levelmanager.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cpx3bq8v74ms0" +path="res://.godot/imported/levelmanager.png-1d5d66e9ab6d9122e97efa1bce2c6829.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/img/levelmanager.png" +dest_files=["res://.godot/imported/levelmanager.png-1d5d66e9ab6d9122e97efa1bce2c6829.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/img/levelplaylist.png b/docs/img/levelplaylist.png new file mode 100644 index 0000000..5c9b1fe Binary files /dev/null and b/docs/img/levelplaylist.png differ diff --git a/docs/img/levelplaylist.png.import b/docs/img/levelplaylist.png.import new file mode 100644 index 0000000..7685fa5 --- /dev/null +++ b/docs/img/levelplaylist.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cexns0yby3b03" +path="res://.godot/imported/levelplaylist.png-81c22442bdcead8e3a74dd48b9e2cf77.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/img/levelplaylist.png" +dest_files=["res://.godot/imported/levelplaylist.png-81c22442bdcead8e3a74dd48b9e2cf77.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/img/peernodedirectory.png b/docs/img/peernodedirectory.png new file mode 100644 index 0000000..a126761 Binary files /dev/null and b/docs/img/peernodedirectory.png differ diff --git a/docs/img/peernodedirectory.png.import b/docs/img/peernodedirectory.png.import new file mode 100644 index 0000000..fe2c255 --- /dev/null +++ b/docs/img/peernodedirectory.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bp8dcwk8spldq" +path="res://.godot/imported/peernodedirectory.png-919e1251618461314bba777535202563.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/img/peernodedirectory.png" +dest_files=["res://.godot/imported/peernodedirectory.png-919e1251618461314bba777535202563.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/img/phasetracker.png b/docs/img/phasetracker.png new file mode 100644 index 0000000..f40a3fe Binary files /dev/null and b/docs/img/phasetracker.png differ diff --git a/docs/img/phasetracker.png.import b/docs/img/phasetracker.png.import new file mode 100644 index 0000000..849f708 --- /dev/null +++ b/docs/img/phasetracker.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b0b237n4m3se1" +path="res://.godot/imported/phasetracker.png-008c365a66e2dc23bda5107f3070a7b6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/img/phasetracker.png" +dest_files=["res://.godot/imported/phasetracker.png-008c365a66e2dc23bda5107f3070a7b6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/img/playernodedirectory.png b/docs/img/playernodedirectory.png new file mode 100644 index 0000000..7b014f1 Binary files /dev/null and b/docs/img/playernodedirectory.png differ diff --git a/docs/img/playernodedirectory.png.import b/docs/img/playernodedirectory.png.import new file mode 100644 index 0000000..12a5915 --- /dev/null +++ b/docs/img/playernodedirectory.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b63iya8v3xypu" +path="res://.godot/imported/playernodedirectory.png-e5070e4b69e8c7c8f32804b2b8f671ab.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/img/playernodedirectory.png" +dest_files=["res://.godot/imported/playernodedirectory.png-e5070e4b69e8c7c8f32804b2b8f671ab.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1