From 46b8e904f4f8454b65135cae8e0fffc99418980d Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Fri, 17 Nov 2023 04:47:31 +1100 Subject: [PATCH] Added Method to Allow Pipelined Asset Loading (#10565) # Objective - Fixes #10518 ## Solution I've added a method to `LoadContext`, `load_direct_with_reader`, which mirrors the behaviour of `load_direct` with a single key difference: it is provided with the `Reader` by the caller, rather than getting it from the contained `AssetServer`. This allows for an `AssetLoader` to process its `Reader` stream, and then directly hand the results off to the `LoadContext` to handle further loading. The outer `AssetLoader` can control how the `Reader` is interpreted by providing a relevant `AssetPath`. For example, a Gzip decompression loader could process the asset `images/my_image.png.gz` by decompressing the bytes, then handing the decompressed result to the `LoadContext` with the new path `images/my_image.png.gz/my_image.png`. This intuitively reflects the nature of contained assets, whilst avoiding unintended behaviour, since the generated path cannot be a real file path (a file and folder of the same name cannot coexist in most file-systems). ```rust #[derive(Asset, TypePath)] pub struct GzAsset { pub uncompressed: ErasedLoadedAsset, } #[derive(Default)] pub struct GzAssetLoader; impl AssetLoader for GzAssetLoader { type Asset = GzAsset; type Settings = (); type Error = GzAssetLoaderError; fn load<'a>( &'a self, reader: &'a mut Reader, _settings: &'a (), load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result> { Box::pin(async move { let compressed_path = load_context.path(); let file_name = compressed_path .file_name() .ok_or(GzAssetLoaderError::IndeterminateFilePath)? .to_string_lossy(); let uncompressed_file_name = file_name .strip_suffix(".gz") .ok_or(GzAssetLoaderError::IndeterminateFilePath)?; let contained_path = compressed_path.join(uncompressed_file_name); let mut bytes_compressed = Vec::new(); reader.read_to_end(&mut bytes_compressed).await?; let mut decoder = GzDecoder::new(bytes_compressed.as_slice()); let mut bytes_uncompressed = Vec::new(); decoder.read_to_end(&mut bytes_uncompressed)?; // Now that we have decompressed the asset, let's pass it back to the // context to continue loading let mut reader = VecReader::new(bytes_uncompressed); let uncompressed = load_context .load_direct_with_reader(&mut reader, contained_path) .await?; Ok(GzAsset { uncompressed }) }) } fn extensions(&self) -> &[&str] { &["gz"] } } ``` Because this example is so prudent, I've included an `asset_decompression` example which implements this exact behaviour: ```rust fn main() { App::new() .add_plugins(DefaultPlugins) .init_asset::() .init_asset_loader::() .add_systems(Startup, setup) .add_systems(Update, decompress::) .run(); } fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2dBundle::default()); commands.spawn(( Compressed:: { compressed: asset_server.load("data/compressed_image.png.gz"), ..default() }, Sprite::default(), TransformBundle::default(), VisibilityBundle::default(), )); } fn decompress( mut commands: Commands, asset_server: Res, mut compressed_assets: ResMut>, query: Query<(Entity, &Compressed)>, ) { for (entity, Compressed { compressed, .. }) in query.iter() { let Some(GzAsset { uncompressed }) = compressed_assets.remove(compressed) else { continue; }; let uncompressed = uncompressed.take::().unwrap(); commands .entity(entity) .remove::>() .insert(asset_server.add(uncompressed)); } } ``` A key limitation to this design is how to type the internally loaded asset, since the example `GzAssetLoader` is unaware of the internal asset type `A`. As such, in this example I store the contained asset as an `ErasedLoadedAsset`, and leave it up to the consumer of the `GzAsset` to handle typing the final result, which is the purpose of the `decompress` system. This limitation can be worked around by providing type information to the `GzAssetLoader`, such as `GzAssetLoader`, but this would require registering the asset loader for every possible decompression target. Aside from this limitation, nested asset containerisation works as an end user would expect; if the user registers a `TarAssetLoader`, and a `GzAssetLoader`, then they can load assets with compound containerisation, such as `images.tar.gz`. --- ## Changelog - Added `LoadContext::load_direct_with_reader` - Added `asset_decompression` example ## Notes - While I believe my implementation of a Gzip asset loader is reasonable, I haven't included it as a public feature of `bevy_asset` to keep the scope of this PR as focussed as possible. - I have included `flate2` as a `dev-dependency` for the example; it is not included in the main dependency graph. --- Cargo.toml | 12 +++ assets/data/compressed_image.png.gz | Bin 0 -> 15317 bytes crates/bevy_asset/src/loader.rs | 56 +++++++++++ examples/README.md | 1 + examples/asset/asset_decompression.rs | 138 ++++++++++++++++++++++++++ 5 files changed, 207 insertions(+) create mode 100644 assets/data/compressed_image.png.gz create mode 100644 examples/asset/asset_decompression.rs diff --git a/Cargo.toml b/Cargo.toml index c6dd76738a..ae4637c77c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -282,6 +282,7 @@ bevy_internal = { path = "crates/bevy_internal", version = "0.12.0", default-fea [dev-dependencies] rand = "0.8.0" ron = "0.8.0" +flate2 = "1.0" serde = { version = "1", features = ["derive"] } bytemuck = "1.7" # Needed to poll Task examples @@ -1077,6 +1078,17 @@ description = "Demonstrates various methods to load assets" category = "Assets" wasm = false +[[example]] +name = "asset_decompression" +path = "examples/asset/asset_decompression.rs" +doc-scrape-examples = true + +[package.metadata.example.asset_decompression] +name = "Asset Decompression" +description = "Demonstrates loading a compressed asset" +category = "Assets" +wasm = false + [[example]] name = "custom_asset" path = "examples/asset/custom_asset.rs" diff --git a/assets/data/compressed_image.png.gz b/assets/data/compressed_image.png.gz new file mode 100644 index 0000000000000000000000000000000000000000..3d96da393547e5c431c56b82c247a3cddc8bd8c3 GIT binary patch literal 15317 zcmV;`J1WE0BK`yZZ2?cX8<&s1yoeu7RE36AA0DHQKY011nEHmX^;|7 zx*L=(ap;zo77&$2Qb4*J9*A^FNDI>O&b(kP7K}1??mcIpy}$3bcZ7zT0ueqPJ_JET zPZb|)K@bLn{)XX#A4YDUtiTUE7sco95JWbE{>B(OzIg-wk@nRS{a4SNtzLPWx>-V= zo}Rq6PWJBRrY@Ge&TiHjdlGaI!~#8iEUWF6xtrzfr#(4;D{Y;!gP0|J{$@ug@kP7c z5+}Z*@{`~4rKL)gvp2t=aZhUf)!Q~Vq%JYc$sy4%4xj2_#G!m5lWW4lZK~Wp*M}ri zL@HWU?WZ?voGQE%NJLqrh`aBzZe08PN}kK`9l5p;Tl7uYN46f=t*oqE$sv(sUfJqP z-cnj@iJv(N?I6;-QhvD3c=T60SS&6F+iH;iTjPgV5TbxO$hI>qVZ%FLg{?TX&qAlP zv@}^*Sa^Q#hL(2kZ8%SmfIve-Lw{KrFHzwG0vX)G2MKU85*8+zg0vaP2txxe*Y3cb z6j8vz!qTdtuRqQAMX(#ikHsQ@zKMlMFK16G27j{cgGz>MzG=uSV&s$KmjXX+~}5yD%H&(SfYmZFqinMGW*o{5NkqE`PSbHXGo(G$6II+5`F69<3llWbgH5B z14EI@aD<|eq4k!FBJyEmR1{wXPdB|OZmaD0_;}f}_YDHFhd8>XIw0tn94QswXTg`0 zl$1$o82zw~Fet0w<44|ieHMYxfn3F6MpEBD6%niTe61hQRi(mY+WD9XscJZ^yVig{f zzHO@@dgie0qC^|CwzkIFV01%*?A5BQ;V<~7@R&d7k_h*$tOHLrlD8I8udwJN3n?$( zcV2TlS3tpIz%q=Z;DS+bw;ZKHEqQr)CR+!G%;VY6!~kr5nomc#)3r<;0&3*QDX?kJ z%m`fB6TPunCTt#T<01C3!YK_2JGR4dUBH{)(Z_Umq!rj>CAwk9XJ<=Im{nd8v<8Nk zh6r$q_8(o1C@CxRh4VbNVVj4^oJp)AIu zp{lAX%*%_58;qqQGka4Ny9Ing!mHP&rlzUWNR){dP0;c2u|-tZAG{zH@Sq>DHwLYa zcz*r*MG0<^vV7!u(ZrUn0e8x(NmkCXSQQ0+7iX5*>D|hZ`pAdn7;y0WKT2El5N0P; zN1EFi=9%l`$EcP*iy0EW`FjWboUKcKw~|nC=Ty519O=Q8+@F&z?pgNCfhTt5HLgt2 z#FJ-A(mgVqJh*aPnZg-E4^vW8e}waNk~Z~21%-u$M-nXrt=!Z;FWI9TZ8`G|4Mx3*d`7O_hKtUWI!Xn0? zlvju@=AKzpI;@W#oF!lBpi+neEC?Y>>VWKLJzgJQe6gU6NxRpjV>S{zwP}a($8b$G2@!AGs1krl<9d+=qk}RaKuD88LuN1ZQA~URpBQ7T(?6wQ+VHd%osp6FO*a?YqJC!;vpM%nnN~&jXe*x(Drn)#6i?pZwgyI(npY)e zWyORs`M@F~BA5bC#_|;oSEBe<{`?wX3aowXm9|<>-AN^YvLFlsCn@U9WI&(Idxx|0 z^X0mhYaXYjLwU{DAh?#gsEs>QjRQEp5GUk90e!A>(p^-TiWc-5#ghwoCiK&GEwkZq zaXkclO+h#WRN*{0_#v24)O_*|4&1Sf;@rj9v zK`VDD1go|RV`93Yb2BB0)ESF~;1+Qi*tvto#cN_BA|f;8!v~HKl`DfDYYH?**lMD^ zsL8(=P*>?R|ET0eqpkz3ho+uhSPcC`ObCfY!s+ND4-QS0A>V$4m=8F8c5RQ`~c}^M`R@%&?m6#_!yuJ$@R(I?RJlbxA#*7!T-% z5>fG&lGk&e+rQ`(SD1F-!tY#Mlx?EI!pxW3L+W>HXW>*-q3_<2frJ^sW3K{37}OXz z3ed%bNn9AIOMy>afSIsQd*m4Q4i0t(-kt`|4yMvBj!jL4>KXe}<^cDuIw{ub5Yhh&7)T=PGWzhD@yd$CLApO=;C5rZ?EEDjx_UE z>yebJP4nBE>$7d+Tdv75nXf+`vbR4ctumb}6R{m~L-ETI8>+$-_&6`Ue^FXk)ZhM? z9UFU-`qBr__hg6mi06G~X0-IpVVt6xnnxp*o~Z8$H`h@_oH;*J41goDDe#yd`H>Gy41f<43lgg=YHn^z`u9 zSdT_F>xjrmvYCfQwEN5^y|TheTRCN5|D)i?3EYj_=9P;_)OEecLGzah^Q zaAq1;e?~=3y*i4xr2b@RU~q6U>)fcBVDGTd^!$$iOk%`&Z0nWN>m>IgjQI4S^*Rhx z!y)an_j7YbgIVG~?vA*vJ2qI4Fg^9C^1nP;?oGIBHjoBKDsqe*-3HSa{dN+9=wj9d zAmzD9DXg95;h`G+*5WnEuMOpP6CiY zsYojP54&ge5Dkl%812!9?{MO`;(-PnTh3Cf1BtkzoB%qogl5XFNzz4yg)dfPLvbEL z$|>BMkQWhjB=33`WU%ASfX1&kI2~VMQ9~Kd-y?#9F)J-a-@e;8f9US+jW2D2{wFn4@z!&*YOhO?(dTwGiSo#g6X{<>z!-o&k~tzaL0 zJX;7B!~P#}8_Csq+Hmt|zG6uV2Yq zZm;**4L|1Hql>XB)ri3plp5&k!vmF1ye-nx2PJGj#UdgywK|eK@Js)@pV*+45Fu_y zn(tL#U!T{-R#8G?;;O5E$JXv{mmG;CcJoWVFT|cQu;-$VGu4424n@fQQgCnkt zg11p~(3l^!{lgq?28B+{%|&;Fk>r<@5WaZvB0Ms(JyXm*J}ZmI=ceuZcPWRtdQy;Q zD0ITa5vVKG@*mEx8W{EHL9LtYx2SSAnGIlCO;nd6_4ne@xwyPMoHmbJ-WG@E^X)b)CNPRX_k1IJmI{LA;HpQL0ck^_MNoifE$%Ds+hVb!y?dgse^aQiC7Sr)tlL7q z{2Fe9rkM*kqc#uhVTjwUoom94^M%vz>{}M}*_}2I%sN7e@`IB>IdL74JgrY+77!Js zdHVEe@4x^sNtmGYuu{(QD^?qnO4&c4V~m>$KDrAvsIXn~MX>2+9;;OOI~Fy#EMtO0 zzO3022Ttr zI(Ct(O9=Eg@`2SkopX=-WZJ{X$T;2PEfg)EZ$pR6xgyF-PrL&YJvJ@?d!)#D-ER7v ztQD-TK`9leXTT2NKAEmF^k z{fEg53J3_e!&!BYo14&c{x$VW|Gzqwna{2Uq@0-=~P_rM9rTMkZ6Sxd`88#->P z0bIz#(!A3C>jOziN&ME`vT(9@;E76%TbO3+oO@bs{_@TF?l+elsbk1rNGVp9P$RG( zF8Y0)KBV(xy?5^?C9mBNSSv6?40jZ+?y<6({H(GBNwT5mXjz~@R!xog`}sQkwyU>y zLl1twC=z55H+T1*(!5P8KVVxVEAASMGGWO;bK6y{>}ev7bh;(_Z$M1N8ep5LNdEcr z=KzRXgJvMtCAq1hSjpmIEw;uBIk>qgct|-B7K}j~0&OWvC|njEo?V~p4N2YY=WK}x zAtCbnx+SfVl=mgHPGgrDRdS>l>0-L+&Vbf@TU8}!Q)3`IQ(j))Q@0op_2-YbhLvJMDH^)Tz~(IbGuUO^Q$jbZHmN+HX{hb|9GNv(r@3sRlMHTO&cQb8P(R+U74{jubG{OBS%L^ zS*+e@ASmhK8;?(*k~f`q=CJ&ILc@=ijTl9XMb}(#rU= zkd>9CwvU@B;XS_R{Y|L5+d(A%)2CieN2ClV5r4Xhv+?ew-h9cwwqA`vf95{)KLry0 z@TYao@?PV9Hx`LR(DmhM)UHP#{0(q!(_Y@Xy1E~}e_!}q%Rp3GR$BUc^I~nyLKCYF zhENt16fAob_Wd`E<<6ZIDP*tGX|G!r&^$}6Z?Mill#POF{QRpGf}dZ*;W;5mt~M9i zP>hf73z}sNSrrr&iO)>|$w_n=%VnFAZ&N6wn*kAT>ISxkSQgX(L9YE(d*#K;qolaR@WUOioS)VN!W{ z0bX|Y^oSovfBMA1tzQ8NSq;j_$zg%WVfeKO_=o{SbZjC3L^8qOZ}r}8 zcpKHbU^IH{wl8;tArkxG%*-Qho9bG3?%dJP)eQx{4U9=f-zMR=5#^5y}daY^<%qL zbB~`rBmZ-Bba=jzlV4MltV+Vh3BqN%!M*79vfRaO> zsR^Gq7;1dM<{?)mfd*N8Z+NZ{MT$Uc%G!Q;gVSr11yQhw4cc0HZOtz8~B`e`xu2nnJhGU zZ|?1R*zmODtpIssY~$ci{ZoYA68%80p{a&v5ov_GOn!eS^?=n>R8g@=4{k~e4~L

V(F9BFCkFuiRImIn_g@bK_}v}uc`6_U7+CIy?*+4&f4)xlv8u2bi91n2KE zG>3iwYQmP`Ih(i_`O`uE=~KK~=f&9f@97591m1en`p|+!JUBdT1KFo!<4bXILPo~@ zhH6uCw0im1zqB(YeW!R&EfiQ#DuDGEhUR<~fM{uH6M$19JO2CP0S)8*a_VK@J?}k2Uf4LOES-(9o6HIs697Uk7*eYXZ$7z*g zN(d(>e@j5koBQQUzYky(_(vNfr~NI4ptb?Xj2*+4)Mt?jPU=FQdB$R-LFtW9kbYaX z$Wwr$dcZ1GfJ6&nVK5DLXuK!nff4h3cR4v!*er3+4|R3aKss|!2~nV+4FxZCMNtEg zwVq7-am`K4&(AOS$99OFn~#RZ=n)SOkClnObzDqLGgYG3t+AqyudnOHu`R=+SFq2Y zKXaccnt{s5Anq|}AQz++IWuDb)z&ZXn62@vU2AH+h8co!W7(|IvEwEQ)d zHt@zSEG&BsRr1FGqeAA7@z!k2}k%|1LPriO}4Rb9QOExVy>wh~IjcVK7~&XN$p|aLZ-U|8yCo zc67-JdV|we{`~^KZjm?rj-H1s=M#T_Nf-jzfM=L>7lsBtT;kx1KLRso2LXft*w7vz zPvaWgH>yziiSfXst&S-$*D#SI4*>m`S*BPtli>_$GihCD_H_l%hsfBA0r2h-nzUm; zpr#q9Bn&eH24y*rvJ8YbfR^SR7f*))TB&qdMlwkHP_)VvT>w>lxb&7p>BM<)b~Xwq z$d!ZTj(WFWk>X6tv(wXQ_)ATgG&DELe^U|I05RSkGT)->96IYbkOW2^&Mw|ig@lAm zndE_>2Pe%#yU$giQlA@rq_!rCG>wdQP8?(?-QjNo5!z#7r0ik+BcJ zg5bdb2rf+-I(LC3Pa~1Wg3tid44h#bTU!k1fr!Wlkoxm>WXr}E7GnGRm6DQ^^T1Jg z(c#;eo5Y6swhM|iJ(m3Td^gC-R)0`u8g z$Ehn{9UYx1rDwxtmHIB=WYNUP+lM(jIZ>5z*JH6lL3i%ozh6;aKC(>~#F4Bt_i945 ze!HxZq5rl!?He{o$iMoN&(2okN;ihHCAZ&UEQt~``yVI3|4686`}sBH3_6JPbVU8R z9r)#jey`W<^=TzQcT?xI#Fc<=WURVx3^)8MJMHzY5$f#XVpvNJLdvbCZ-j2pK6TPyk&nuV*-) z^hfg&+P+InG&hGpmKN|maR{5GJwH)@`m~aYcz1ta0pt~+RYu3hg8^Fuwuc~lbK;3M zK16C(OcS+T-omu~qukovFLWuoWN%yX_0qDow)Qjw$p}H7HWw)wS#W$jeEZJbkt0ww zfD=`4bSy1m4D7yxbi-v~W5cBvaX9F&cMSD}Gcpo|AWi-0#mym**B$nb4iD2^=zwa; z)hz~iApuk?u-DtV1&WG2AZlG^9qX3$S}rJ+zjnFY2W3!GQ*%no2mrAq?*&Bp7&vwr zqee9byBaQ}2yF0f04vaNfe;tV&fb3a7J(fECqrPdT+oOM3X<`)TnRGy9ms+$Vlq9O zeXsS~unDrI|$!;;((e01sLxM)l!D=J8`}H1cL;XK$QKk%S}LDWJ!nvnW6_qKCp%xZuA$e!aAYk&r0prWn5SXk3JVG=Z15+?rYucZ(?V?%aa_qtSAhGrU;N#Et|k@A3aoxj!0yJz#wcBD zuZ^_2W%|kAzaikbRQ0`VBa)GkfkYf(*~Gt zv8HDBQCRL%S(w11Bdiy`yESMw|GQFB16PhCGVY8PJJX7~-ob<4ggHY2ufS@w` z{jA>&#}W>v+o%Su*5mpAgSIhhc=^0)tq!GiUao%*Q)ZgKy zSg|=_#;9dh6@$88uiYwFfkc? z%8F+blE!_~!8`=r{s+LAnhx3th@;H;3*;3P{sES_v`1D}R;?66^qalaes**I1gXLi zO{{ET3FsV=wWdiYC>6H5k4SFRCMghxvWt2DDHt*{0zvf5sVTM_6EJT)p^L#xv5DnU zN$UFqV+#ulc287QRqJ$%9kKcS&pffPunw<|MtldG>wuC2>n>V+%-T>W=CVfYv%(ab;zJm?g;MEXEHvomdTOY$JeFQJg&k^7Nx3#qaKv)$Z zc_n)14zhx~f^Y=wyfapT+%1}MeT5AN1p!^XJ0r-=zt^m;ti%I$t&ssfri=>FY^0+z zDO4S;A7P3s^I3akt0>2Vt0GJ*G9`s-TLJjK_wV12&di`+W)MqVeP!qRQd?aOo0^KJ z)&trX6frm@WeCVnXioU((M&!L6U~Y1N+({%Dlh^?GqmUHHSESY2BqCqmIE3uUJxbr zVMlcZx3LiFkVOMgvIxCuSN{So%^@agmdh!@DSJ94~Oui zae~%d3`j%bI0n>I1}VSk!fsmgXTbJfT=?w#{Q3_HZu5X{?bk0;0JV_aUPAIRoLzvf znY;5!O3rW9hQ>kIZhfz5Ft~6jF|foiWMQ0cHNv6h8cJ=h3=3YXM04I7S@rwhFAo3` zsT@n8=iuV1I_xGOAaH-AT+?S!^}O7THAq3xNR?cT^LiM1EHCc{=NG2wa0S&d+jmcF z&TW9l>-z5x$9KAdMymi7SBnh;1OF&0E?(!&3&fo}n1_dl!wU<~fU&_PqzeK|@}9Hs z;&9*8EBNHZ!(q0TsBYea9^LV?f*BQ*mOB3HYB5>qUjUObL|H;XZ)~FzZQGS0{UBK^ z2tviXV{VzjWB?6Y2)y;5bM0X~+su#q*f|_2K3!u^?c>`*KgY((D%wmxvAF1L4#5x~ zKDIsMN*;EyG`PGtu)(Y)W|0L+tTFOjx)JA zGdLU3(S$`aACgu;V@_nFpu+ub>OdAPj{fP#Qe1Ex_eH0XxdmJM`#nIO9)dgn_WK{H zt-Pd_p@)mrVs$oScZ$q6M{=amG<>@f*fDXtzhYg$kud&@+}YjQ5)*!9A`F2sj%G-W zpd@A)7YN)+(3FyWk-<){5X&G7WZd6rz^BoL_zRB`=l7cn`wVvw$nQ9P!8Fb8!W1hi z6EqI0zDvsAV4oxL&FkZUS6TIJxy%~MOgdx)n)j<%_Cx3F3B)eQ&>~KqQc_YtTf05U zUYc&a*L&2Y%3LP^q2N|fQ~3C}mIUDDW&`mIpncZjNEQe*u;0G#qWhOpu(@bxA+iB1 z>Ah4cK7RhuGT3B-|6Df&8qfk*OO}2hZ1St-R4jo$ue2PX%5eAg_ErmX4g{D_WSa-1 zqIbTRB&G^JLUtk<1fe+<0uUu7B@vAanTZ!ET18Kjv3B$@2Sx*wuw9vxlQZFb3tGFm z1)a6x7M7OYO;owf)1*coWlc?UY>_r4I??6bqCN5aU0q$|#t(3S*JuN?0+#N3-?q!# z`;8YZ1Wh-FvqUylR#&r#5hvIHm5|dGY5Mynq5CF{ArxIM(y4sLR=gdMi1R`;x@qF- zn=zfQxg`y~abN30cR!NDu6^${d_`eDmgw!hi6Sl9JK| z!~oZ)?idm^tZ)+?1=(yPCvaii6AXi7;-}BI1WHFnIyA3LjE{ph|9X|0I{)xq=XZeP zG`&x|k1cYOs%(Q&5tKy~0%twaH>RtFMcQ14`OMe7%O1lZ;3R!drZb~rW2sm}^uXx? zaiyuH6%1%l>TDz3YP44O&V>1U0;Mf$RJ@)GC01KJ6$)NNZsa8nniSpt>3WpvS7HLd zOk6Voyw>Sr;Gz`;X!M2u{A%4;QrXPPZH1Ce44OZhRTcPPD zM}DW-rZ@LE0HbUM$lg!qm~!tyR#)wtAp+FF3&eyYvT${EmGR}-_TFAMu$vAim&r=^ zsJQi)+^UV;-Gu-yf50{I3}CC_OtH5>R?>l;Mp~TX@WLNC#U~j*T$_iu!gGaz8GW4{8 z!IS^l5ioxBVk|(f%!t-rVA@P1L%Lx`5jg#OZlrzsUXtI~V@Iq>d{TFe-p4)(O za}wIu`>b96cgk?rDIcg0&>bB9bQSQ`YRFA&whV|6S=l#BKeJ|MXB8&zLbx7*Fod2e ztpe=aq{*Wvi<=ZFh21Ksu9yUtwB%*8fdeg5KIpE;NnLzWRnXu{t_LJ%m-odY(2+l0 z`Z{WF01)-MUTRT46C~{Xm+2BCajAS&hItxBug^$SOd!!%A&hB(1z|#07L*C-?ta)P zIHIunN*gDAQ$OUF1rJ6y=DE1~%S2Sz$ZH*b3ijIzK6*swzCP&c&8qX_g=1o{3Z@@$ zssEB#z#+#Kex`K$6xJXos!E8^|7sPZjK|xIm%VYA+H!>hj&q$@%d0Id3v27yN!%V+ zul?pzd7>e6b92Bmx7{}OcXk+l$#RGYrtkcXC`%uOXb3~G3`%PWgSPiH2xUQvZRZO- zSFW`5iq?DSZK-WrJ~*htZT#J@-Pb$22T-EQz}p+m`)iXP@ABp(TQ?biG_SPrY5od&$Nel=E{&CkBB zx_YUDMEWsks||T0VPKpvK0cl$yc?))(2U0$PYJpr$MW|y+Ud71(BN2jkTDd9%P-89 zN!B$oLIT?3=t(&+iA9PY`T;$#*>xiz;J~1h`0`8-P~;V0I7L$~05mtj3w+(t{JJ_% z?QuBX=#Rz4+mp`JpSBPfUoGE!^^Jt1P}*IUy5srP)m4S<1Y2sqfiF2YyceH3vq3tk zzrDHa;X#do*2?Z|-K+&R(;dK_0TrRF&b4-RtyI3ec3PP~Jh=%o_q3AB-nf;AopT0# z{<_6Y3Uc2^m5rjL6?9{kJ!g9FWk_h~$_+S%UwoYc1EtZm(|J)U<1tffYrHg=cmZQ5 z!51n3Kdk^*A6&yuAu-b8uwO%bEdnMHUP1#5x}7fXK{rU^a_6gu^J{M*5K6$1(j`yd z1^3i)FtX`udpoOTd+ShlXB%V_KTbOD zMf^+TW72mS7?|M2f+Nwa9&}9?oY<+IKLUfKE^&w*%Oq*%9$+&qU3EnA`Uc;0AxDo$=Gy2X-2tu@nqoRM0eJ7XR$gy&rESO<$0coBf~?6nfX?lA5|LP|k_jqFV`E$`6e zN(P4=sipN!;cXrOX|5O5tOPdH?hL6!COe36QBm=+wSN>aO4yRI(6f60wVsW|1+2M@ zD>)Z|lxkS#)CJn3VbJEpdH3k*|Bc3wp`afY*R{NqOe|2L-@As7q9A ziBJDsv-!s}oyT$fJ7b11zhcX;RwQ#qg7uBZEIxJE+wgm&n=tWnr%A>zd-ryva*~|C*vaqnm#6%I) zHq`hXCuc&PVb=E$*y}6o+D}-S##!KbyOHu^^#vU~5Klrv)}h`#s4xKxo9Tn;Yj$U# zkg0BTrGq*R4=zi20h6(nqAvW56|aZ>;Ih(D!olJ2&G}tA00Jl@rNaY1sHQt6h+J|5 zp&Rmw@QhrmD|zw$^zuSuhxy{yuWBG@fgufWAjQhc`=`8g02JAC@AlUmK9~-6?wZQY ztJ?<{z{x6$@6zgCnt~QL`q$ek3G4=N&TnYY?P=mns6*wE+#L5gqsiYuSC;^637Bym znQ2BMo6}2n|Fjbp`rT~Z&lk(m@qvidN;fK{L{nUn?wj5D5=>V|m|{7b4%r@diApa4 zObL!i71%ATU)lLz^QM6TX>9B~)AYMNCnlz-y4s^QdTB@H#jaO>0A!GxpeDPJ8fx|P zWer+`VxLeGl$9M35t}NT=MJDIU)%aq*S(m`3|M}|g}KS!-pGuAGY72srui+~sMGys zW@hq|C>2v*K87+D(ac;n+cBhGoL$ra+nYW&su6T-GYd=0>Xr`yeuos8J zVWHF}`X~x?%KE~_Ikq?DM)3+FO)z1lHV-SljguQgBvCFe?%ZEyC#KmH33~liX?z5q zd#p9RJ=mh8q=W@E87hs!>MC`uw2ylctAEwK;11YnQDC2!eS2AsD*fe3Usw^Wre7X? z1C|}CFY)<&1--1b^xVPlTLv;Q@nYq{Qykmp21cT8#pji(p!K7ct*NOpn8e^4t93Qe z-~7?v|7Tg%tvkRbrz9tTxZ#5CYxeesrb~3D3hV1vXqz$b!4xT3*|SBT2vGHzEM>(A zS!L|*v(UIN{9LUHlx-x8Q$kAcS= z7!;(oVufLoX*0kU=*>~5_fAH$9AhF6P_`y{qQ0ip=SIJr!@z-rGrq^zW$m&7;0RXS zxzyCV(A=2^wGqGz#lq?A)jQem35x18%vzn1lb@6om6Q%K5isgu4cP+tP_*}&tebcO z53CLB+KM1hgSBDQf!`)SJ{lX(g#J(Jt`PAwwUdON?l7xiqGy}Exy|5@H~ z{+9f%;~O;jHxSvULo{*|B|>;?pojB2&er zKQ}dA`-GjcMxa)&>It>Ft!WV{2Ju>eOJ`=|FvZyD^`@%+K*qN%6=C|Yg{zG$6+YQb$=5P1ANUaV)pvzCipWD5+dv0cC z@qTx=nT9yr=jq4f=8)sPP@@9;IQqvmFJO)6z)Ib$Vx#Je{o}#G`NH+WmugrKP|L17 zo`@(CXvwg@AB?|#bP}XQaN)F|aRHYeAgk;H!Pw6twaS)IPiwyGy@%U~w$ zF%tP>*e47C>UG)vZErDjSdgrMtKbhroSz>lWtv};8jBpPNrVf+6NS6W_KeT|)ePN& z8M4$lxEXNStb)nsNhw^R1XwuVE$0i|??Kko;VDvXF!svVb&*f{0UZ zx#ca#_V>A=BTO~FVHh506foV38yd7w_@um?9uqSgJbW|6M)E_(XvA6GkDzKX%%IHs zL{wB9BC>w8{0uX?i!1$6o}O;`ern1QxD=3u9znkd;uh3h0N&`w#Kgdm@~OA&6U^J4 z!L&Anru-Avt~Ru)2WqeSz1Qp*#%{-6#f$c)2n3q2nYTbi3XL)_gFL_*(S08F@NEpIB-A3aQnLc1WaLhxhqi`f`ro^TnwA( z46d~fgo+?((x!;IGQBUX{#ODvdAoPQJr)`;bH?bBNyE8pr-%Q3Ex;~u(pGC6 zDJ{URCF2E=j^%Z807z?k|9s?-$YQyr$t81^EkbhvR8euCSTfh z`zx*ThtamN@f67l#a3G6n?+tOj2AdHYa9+Yj7YuEOB2E)BP0D+?{li!!0ppy$YyX{#>X%9d!*4p?HBSZZB^2naTsZ{o$}kjODHt}I5j+3$Y1jjv z?ep{ufN`n1^3}v_I8`eUW&%&nnM^<`kR_rd>V1kS-DzC`1}XPCa}%>s%}*(4FIybG zdE^k2$z0Q*T9hZiu~Z=>*RMenVx?;!VdoQwo7R*>olefJ{J7^{@Md_EAYdQi3Qz0F zXs$c#YV7{e?sjEe-Awra?O8{OudHrnTU*;-kU6LT9lf zg9AyvsgT}GB>6nZ@kZx6m~zzz;psYmj6`lrZ7RLPDZ)UA%I$#SalZ>b7Pq|wqI^FU z3ROSKL(R}IK(p&Axh#{G)Ddi}p`mf3HLzwTsBl}+?y|L(Yj-;Emd*3KZVGa8^x;91 zTwB$6=;7R9*X`hD+#Coh>3HbOm@oi1 zKE5W&2@`@iQbX}VY_zd3T;RdDY(*j%U<(6NvlSm8Y07ZKZISu1fwvKYewWUw7RyhU zGpm;U5v`h8C*=^NZFgl)Xgm!+Z-0?8ZDJ()4o^7^ai~+Sd(E~{SU9meGz{9Ha62Gh zCp10s@!$aIrJgNq0db zij<$$LCAq`^NI!Pv<5I0oA-Nq_N;0kMJAFFG(gaV#6v_6k=d1FIXGdMcoHYIhKHy; zKpioJ_-9s;Cq~3krUWaL{&H2MQ;pbc$IpwOJo!8dtWn)L)6iNmftL?oqn4OupmKr~ zZ}8yxfF~E(4JkNdO+FE1!8fg1R%En+gy~G8wWpJ)vYm)*>PF}5b(jHp))vZ`60oNe zccWU9$q4a$ety}z@o`^S=|W=G`-FMzOCrJ4y2N)X1i}NejT_%-`#thLLq@Wl-Q9(y z-X^a-8QPe3Y&Gnx?X}O6$m0FTyr4JIRI9TE$XdsmU2cajVzNlE4&p&3jt;gMv?HXc zSYS%9(<^-P?#>`KM5!)sHDkg&we?snxP?B!htdKdmMgb$e=QwQ*8E}*qgo``gQ0=B zhjl_!O|RQXEqDy%0vI0%KUQeQPfs`9ib65LN7EGG!FQ5%&EY>+CDPSstBph^hzX*G z0Dc&N$lxOLl7F0ZHslqyMC*~9G6Gt}G+l(_$;dbNkm;o3zt>$Hax$*CD%dwWJNqqs nX1P*HaAV3cvGyLVJ$Q3L7y=*XUGDTs8tcs8D=EuN^gRFos_VST literal 0 HcmV?d00001 diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 9810e08097..008312f200 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -562,6 +562,62 @@ impl<'a> LoadContext<'a> { self.loader_dependencies.insert(path, hash); Ok(loaded_asset) } + + /// Loads the asset at the given `path` directly from the provided `reader`. This is an async function that will wait until the asset is fully loaded before + /// returning. Use this if you need the _value_ of another asset in order to load the current asset, and that value comes from your [`Reader`]. + /// For example, if you are deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a + /// "load dependency". + /// + /// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadAndSave`] preprocessor, + /// changing a "load dependency" will result in re-processing of the asset. + /// + /// [`Process`]: crate::processor::Process + /// [`LoadAndSave`]: crate::processor::LoadAndSave + pub async fn load_direct_with_reader<'b>( + &mut self, + reader: &mut Reader<'_>, + path: impl Into>, + ) -> Result { + let path = path.into().into_owned(); + + let loader = self + .asset_server + .get_path_asset_loader(&path) + .await + .map_err(|error| LoadDirectError { + dependency: path.clone(), + error: error.into(), + })?; + + let meta = loader.default_meta(); + + let loaded_asset = self + .asset_server + .load_with_meta_loader_and_reader( + &path, + meta, + &*loader, + reader, + false, + self.populate_hashes, + ) + .await + .map_err(|error| LoadDirectError { + dependency: path.clone(), + error, + })?; + + let info = loaded_asset + .meta + .as_ref() + .and_then(|m| m.processed_info().as_ref()); + + let hash = info.map(|i| i.full_hash).unwrap_or_default(); + + self.loader_dependencies.insert(path, hash); + + Ok(loaded_asset) + } } /// An error produced when calling [`LoadContext::read_asset_bytes`] diff --git a/examples/README.md b/examples/README.md index 70b1842643..0450ef8815 100644 --- a/examples/README.md +++ b/examples/README.md @@ -181,6 +181,7 @@ Example | Description Example | Description --- | --- +[Asset Decompression](../examples/asset/asset_decompression.rs) | Demonstrates loading a compressed asset [Asset Loading](../examples/asset/asset_loading.rs) | Demonstrates various methods to load assets [Asset Processing](../examples/asset/processing/asset_processing.rs) | Demonstrates how to process and load custom assets [Custom Asset](../examples/asset/custom_asset.rs) | Implements a custom asset loader diff --git a/examples/asset/asset_decompression.rs b/examples/asset/asset_decompression.rs new file mode 100644 index 0000000000..16538d9fca --- /dev/null +++ b/examples/asset/asset_decompression.rs @@ -0,0 +1,138 @@ +//! Implements loader for a Gzip compressed asset. + +use bevy::utils::thiserror; +use bevy::{ + asset::{ + io::{Reader, VecReader}, + AssetLoader, AsyncReadExt, ErasedLoadedAsset, LoadContext, LoadDirectError, + }, + prelude::*, + reflect::TypePath, + utils::BoxedFuture, +}; +use flate2::read::GzDecoder; +use std::io::prelude::*; +use std::marker::PhantomData; +use thiserror::Error; + +#[derive(Asset, TypePath)] +pub struct GzAsset { + pub uncompressed: ErasedLoadedAsset, +} + +#[derive(Default)] +pub struct GzAssetLoader; + +/// Possible errors that can be produced by [`GzAssetLoader`] +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum GzAssetLoaderError { + /// An [IO](std::io) Error + #[error("Could not load asset: {0}")] + Io(#[from] std::io::Error), + /// An error caused when the asset path cannot be used ot determine the uncompressed asset type. + #[error("Could not determine file path of uncompressed asset")] + IndeterminateFilePath, + /// An error caused by the internal asset loader. + #[error("Could not load contained asset: {0}")] + LoadDirectError(#[from] LoadDirectError), +} + +impl AssetLoader for GzAssetLoader { + type Asset = GzAsset; + type Settings = (); + type Error = GzAssetLoaderError; + fn load<'a>( + &'a self, + reader: &'a mut Reader, + _settings: &'a (), + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { + let compressed_path = load_context.path(); + let file_name = compressed_path + .file_name() + .ok_or(GzAssetLoaderError::IndeterminateFilePath)? + .to_string_lossy(); + let uncompressed_file_name = file_name + .strip_suffix(".gz") + .ok_or(GzAssetLoaderError::IndeterminateFilePath)?; + let contained_path = compressed_path.join(uncompressed_file_name); + + let mut bytes_compressed = Vec::new(); + + reader.read_to_end(&mut bytes_compressed).await?; + + let mut decoder = GzDecoder::new(bytes_compressed.as_slice()); + + let mut bytes_uncompressed = Vec::new(); + + decoder.read_to_end(&mut bytes_uncompressed)?; + + // Now that we have decompressed the asset, let's pass it back to the + // context to continue loading + + let mut reader = VecReader::new(bytes_uncompressed); + + let uncompressed = load_context + .load_direct_with_reader(&mut reader, contained_path) + .await?; + + Ok(GzAsset { uncompressed }) + }) + } + + fn extensions(&self) -> &[&str] { + &["gz"] + } +} + +#[derive(Component, Default)] +struct Compressed { + compressed: Handle, + _phantom: PhantomData, +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .init_asset::() + .init_asset_loader::() + .add_systems(Startup, setup) + .add_systems(Update, decompress::) + .run(); +} + +fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2dBundle::default()); + + commands.spawn(( + Compressed:: { + compressed: asset_server.load("data/compressed_image.png.gz"), + ..default() + }, + Sprite::default(), + TransformBundle::default(), + VisibilityBundle::default(), + )); +} + +fn decompress( + mut commands: Commands, + asset_server: Res, + mut compressed_assets: ResMut>, + query: Query<(Entity, &Compressed)>, +) { + for (entity, Compressed { compressed, .. }) in query.iter() { + let Some(GzAsset { uncompressed }) = compressed_assets.remove(compressed) else { + continue; + }; + + let uncompressed = uncompressed.take::().unwrap(); + + commands + .entity(entity) + .remove::>() + .insert(asset_server.add(uncompressed)); + } +}