From 2e24bbb08387422a1ee48c68c566127624a272ea Mon Sep 17 00:00:00 2001 From: RTTV Date: Tue, 23 Apr 2024 16:30:58 -0400 Subject: [PATCH] - added autosave - added check for closing the window with unsaved tabs - enter to drop held entry - enter to select text - added tooltip effect to make it more readable - fixed bug with snbt editing keys not escaping - added list as root node kind - added bedrock's little endian nbt format --- Cargo.toml | 6 +- build.rs | 3 - src/assets.rs | 16 +- src/assets/atlas.hex | Bin 262144 -> 0 bytes src/assets/atlas.png | Bin 0 -> 24154 bytes src/assets/build/atlas.png | Bin 23443 -> 0 bytes src/{decoder.rs => be_decoder.rs} | 23 +- src/cli.rs | 4 +- src/copy_shader.rs | 36 +++ src/element_action.rs | 4 +- src/elements/array.rs | 60 +++- src/elements/chunk.rs | 36 ++- src/elements/compound.rs | 66 +++-- src/elements/element.rs | 222 +++++++++----- src/elements/list.rs | 131 ++++++++- src/elements/primitive.rs | 23 +- src/elements/string.rs | 27 +- src/encoder.rs | 7 +- src/le_decoder.rs | 127 ++++++++ src/main.rs | 74 +++-- src/search_box.rs | 2 +- src/tab.rs | 94 +++--- src/tooltip_effect_shader.rs | 39 +++ src/vertex_buffer_builder.rs | 62 +++- src/window.rs | 469 ++++++++++++++++++++++++++++-- src/workbench.rs | 141 +++++---- web/index.html | 15 +- 27 files changed, 1376 insertions(+), 311 deletions(-) delete mode 100644 src/assets/atlas.hex create mode 100644 src/assets/atlas.png delete mode 100644 src/assets/build/atlas.png rename src/{decoder.rs => be_decoder.rs} (90%) create mode 100644 src/copy_shader.rs create mode 100644 src/le_decoder.rs create mode 100644 src/tooltip_effect_shader.rs diff --git a/Cargo.toml b/Cargo.toml index 932887d..7655bfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nbtworkbench" -version = "1.2.6" +version = "1.3.0" edition = "2021" description = "A modern NBT Editor written in Rust designed for performance and efficiency." license-file = "LICENSE" @@ -16,7 +16,7 @@ categories = ["graphics", "rendering", "text-editors", "parser-implementations"] #path = "src/main.rs" # Windows Only -#[package.metadata.winres] +[package.metadata.winres] [profile.release] opt-level = 3 @@ -45,7 +45,6 @@ strip = false debug = true [build-dependencies] -zune-png = "0.4.10" flate2 = "1.0.27" winres = "0.1.12" @@ -66,6 +65,7 @@ anyhow = "1.0.79" lz4_flex = { version = "0.11.2", default-features = false, features = ["std", "nightly"] } regex = "1.10.3" glob = "0.3.1" +zune-png = { version = "0.4.10", features = [] } [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3.9", features = [] } diff --git a/build.rs b/build.rs index d1d5cdf..ca75903 100644 --- a/build.rs +++ b/build.rs @@ -7,11 +7,8 @@ use std::io::Read; use std::mem::MaybeUninit; const UNICODE: &[u8] = include_bytes!("src/assets/build/unicode.hex"); -const ATLAS: &[u8] = include_bytes!(r"src/assets/build/atlas.png"); fn main() { - { write(r"src\assets\atlas.hex", zune_png::PngDecoder::new(ATLAS).decode_raw().unwrap()).unwrap(); } - { let mut char_widths: [MaybeUninit; 56832] = MaybeUninit::uninit_array(); for (idx, maybe) in char_widths.iter_mut().enumerate() { diff --git a/src/assets.rs b/src/assets.rs index 25856ce..22244a6 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,11 +1,14 @@ +use std::cell::LazyCell; use std::mem::ManuallyDrop; +use std::ops::Deref; +use zune_png::zune_core::options::DecoderOptions; use crate::since_epoch; use crate::vertex_buffer_builder::Vec2u; pub const HEADER_SIZE: usize = 48; -pub const ATLAS: &[u8] = include_bytes!("assets/atlas.hex"); +pub const ATLAS_ENCODED: &[u8] = include_bytes!("assets/atlas.png"); pub const ATLAS_WIDTH: usize = 256; pub const ATLAS_HEIGHT: usize = 256; pub const UNICODE_LEN: usize = 1_818_624; @@ -58,6 +61,8 @@ pub const GZIP_FILE_TYPE_UV: Vec2u = Vec2u::new(48, 80); pub const ZLIB_FILE_TYPE_UV: Vec2u = Vec2u::new(64, 80); pub const SNBT_FILE_TYPE_UV: Vec2u = Vec2u::new(80, 80); pub const MCA_FILE_TYPE_UV: Vec2u = Vec2u::new(96, 80); +pub const LITTLE_ENDIAN_NBT_FILE_TYPE_UV: Vec2u = Vec2u::new(152, 160); +pub const LITTLE_ENDIAN_HEADER_NBT_FILE_TYPE_UV: Vec2u = Vec2u::new(168, 160); pub const OPEN_FOLDER_UV: Vec2u = Vec2u::new(112, 80); pub const UNSELECTED_TOGGLE_ON_UV: Vec2u = Vec2u::new(0, 64); pub const UNSELECTED_TOGGLE_OFF_UV: Vec2u = Vec2u::new(8, 64); @@ -170,11 +175,18 @@ pub enum ZOffset { HELD_ENTRY_Z = 210, ALERT_Z = 240, ALERT_TEXT_Z = 241, - TOOLTIP_Z = 250, + TOOLTIP_BLUR_Z = 254, + TOOLTIP_Z = 255, } pub use ZOffset::*; +static mut ATLAS_CELL: LazyCell> = LazyCell::new(|| zune_png::PngDecoder::new_with_options(ATLAS_ENCODED, DecoderOptions::new_fast().png_set_confirm_crc(false)).decode_raw().unwrap()); + +pub fn atlas() -> &'static [u8] { + unsafe { ATLAS_CELL.deref().as_slice() } +} + #[allow(clippy::cast_ptr_alignment)] pub fn icon() -> Vec { // error!("Hello, world!"); diff --git a/src/assets/atlas.hex b/src/assets/atlas.hex deleted file mode 100644 index 7f5f67b751d94e35ec49e6264f2537c936ff7c3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 262144 zcmeIbX_RDFbuL&x+dq1l_u4=DM?bs#dM&qqU|WV}u+0MrWDGPQm1!_Mu!Lj=F*HI# zJU|Fk5=cm5uml3}(1;{7)Rgm3IY!o`Qk811QVluJS+fM{hrQ^<>HVFI@8*sj=iImv zk)_N^-Luxtz0W>-?{n_C``hP6WJboSRSV7Sy4R)Af@N50-TRPBqp;V4c9Tq>Mhli< z>3hGLa%mLyTF?%!8+K{5U>TNv{Ya}zqp;V~YBz4&=oXXQBDc`)*!6~Nvg+(|v-*jh z18wz38W1qM`F4KpI&HHE?}j8J0>p9JTTa}&BPU&mIBxxxx_{G6H@Uz0o4;8Ul5250cI=z)JU;i0_&zKQ#J!-b#FV;l`U zX+%LG!OHvQ~xb(n7{=j#Fd+RSX z!H!&C5B%SI-S11t`V;>lf8hUL?ini~ z>!0916hQF5_ny5aWc?HThx`Tqr@r|>30eQ8?8op_O)Zf1Cs>VP>Ox{)uN8G5%YJ5A zCvImXmdEu{3{`qXcX94<=SHeG^4frfT zKk`cWPo52m&jR!#uY~{P*`W9=KtIdbk7}&>Oe6>7hp^H5`tu*|j z7G$XbS@4gV6{!KQ9sK(pDE=tE7SK5dISv2Xy%zjy|61^`{cFO%4p0mJwSNl!W7y|j zSE>Oq?9)G116Csbv($jB_>a_p9REzL9RI6Q&;P9Wmopx(v#_muetwlBahoxQ8o<8g z!1vu(|1KH7<*GdnL_3Uyo((WwPYn?FWDd?%4Yus-f>1oft!0g|=YU$)hug^OE3R?n zy#B}Y%YF~2iTZAK?nG0Prp!I?k_H>G^-k)?liTic zyM{KGnBMr~k2sKSuz{^|_2R=a{f>p{y$kbK`@;sd*8eL%n{>bGS~&kN`;-Y*3<4Y2 zTK^+IKO*n1EzI=9XP$7R1+7S816%8V_MTtKzMM!*yFc`$pbQ(>T7UHQ`TxRq-<;S` zt``5-y=#vHajrD{!={$!Yb=}bEw2S*v3RwtjlUDHuK{XV7hj{`rs6sm5r2pmCC(G$ zX}%bX`B5)6zFGVu=h(FDYeYu;)Bn?Vznh}35n1ui{7sxi$cq2BT@yb4v*Q2Zk3X!Z zU63>4zxC3q6CW*CgMaKhv5%WnE&l)MFP?Xx`8&L7)oT_1_8Fia{PP(=;U|h++4n5? zTWWuw9fDLTvM~?TU1{S9G zF3ex;51Y_?SYP_dlzX&&;rxHo#~Yj`#()iMIWPI)`wvTTp_M#i#PBaRu%#dQ**hL{ zZu8M305DHVln}%Qw)E3{sowwR8frOX#W#2@h*=BHV)1HO8_)k7|M9<3#(hGw_%F#> z-N}f5?V{ys@UQ(SXT?A5EzgR7>zDL1;yPPH% zfT)?V-~TK3s$K(#UljX{SxpUy#j6+l8voU-htK?$G}?v13VB5Hn621`A@$1d$&2m^ss>~{m4K2!*Tc1#)b1gFtt~~2DbDg-}l3P z&h^K#?|JEZo@@iT_>q$@^OwD1v|d4Nw+};eV4%orPlfzx(MqmxW^Z|6S|vTo#Jr|H)nQ z%gM@=<3IeFpvA&JaT%6coF{k*0un2W{BvKD>L$KqfuS=NCwzZioY zu4Qed;U5=x=+L1;8kKE=Q49Zw8+l{py26Hh!*NF!rj*6r`_FQieK zgHd#KbrsUcm@nII7R<@wmmL4aHKgKHZ*m&`tBpl_)`1wOU5v;4sE_$kPrDeWT?&_> z1?-ks$V;=i6)^o`+;dfLS}?P5IUM}5qXdfKIM z8JfpGb6?)xC}@Hn_=gVMJD4B9H*v3hAw!RG$l%7+cY$x>-ugq2ap=KA{=heJZ~dXi zIP~Blf8aaq`Y{fF@Q^?7P2Ai0p~pD%;35BIsM9zL$<;176hrIM;$^iiNp#m>=~Q+Mk;OTqFN&?xGL<)~#Dhh<@b6f5;#BfA-mDONf5t#DB;i_`l3{VOGd^X^-fZg3n_~)|$p9QRcCH(W*fX@QfzY_lS^@pAT zD&e2c27DH<^Rth=-Me>hfqio5Li^5fa_r;1_ugAzpBy@jvA>vHVH23ByAhxg$l2E- zoD00#iR|+~=K-%a7S;yhIUVD)i}9Er^)Wx{X&2+POW`uK@cb`Z5qt)qz83t`7rYky zYyVpCul;MmzxK!VSBrn`pN9V!_Wcc#AC+oA4Eyw7%o?C^$ltlkJhkA+FHiO;Y5cuA z;#vH2E^xgDUSk@!_~$&}x~0WXV>$lSW~80}vbRZ1$?-q`UVz%VL;p# z#$9pvN6yI&^rYo#@K68G+e;EfnEB-%tU48if$)|sktXa>W75`7H`E25^ zk;FfSf7rm*`ak{j(}e>E4kZ4-Jc)CI z!v?n2AN{3z|G!>rU5GO_$G@+oNu6Q6v0T!Ibu!4Uz3np(E&g@RYB?+ZwU3so!N2yS zoE87Hw>&HUtsnbY@lSip)9{~G1Mu1a^?~;*#$&$&M9qx-{(q(8U-rsBmvGPOIsX}p zI4bso*uxm@Vtg6+%M$!&qsrdDx4DZx-oKOmD~L-;!)sd9kdTko!6x+j7k{7E+}vEz zA2zTpnOr(@Uifoa&lQq!cEi^CKl98pnXezV*8lOxA200Qy}RQ2VFO$1zm)xp0sq*a zTGuJACHP0IT5AAf5zoc`ioie4mn;;+KmG>~r0BWhU5%=7)~oV}|c9 z(iHpNGR6VEiTmIF_WyI5pvO3L;Pf%v=cDM7>*Ri9K6DoRhy2vK z5-|(@Q)2n+i{YO7qJOd%wokhlxAwLl^P}F{2l}E~+J^gpzdJ)ta3vOhp+0SiZH>VP zuX~fzq{nDaf9AKI19)d&%h1st{?%fi>xk6=evc8pzerQ;drNo?#J|QpW#Iq5gQrV~ zei5GJE3Y*RmsrXDGjWr6S#VFASiD#bDB8!?V3suB#r@=bnrDdRuZcK|aTO=o1Fehs zQLk-7GVNkK=~a zAET4*e2Zb9;}^OHaGq*8uw?NEjA^pt#2<2+_aC`Hve?&p{;j;M)8iPiG1qT){& z4)&biUMs)H0N-DxDfYbu^iRV-p98>az(1z|PjWa^3yR~~zLK28<2}$k77zF@j*(ZI zCpwP%T4OBlN1VmDiEZav^7jw*#sJ^=AKK92`?K{JWQ@Va>UEzu(YgyQpLq0I_sZcX zOK8XapLUw;IDD`!;$QwII9dl-3xIq1`y)?~#oZweaR1V_`~3S2>%Mk*Du*5HIX~9m zzxNB@`y=!(_l!lq!vJHj548~f-*Zp111%!{_0<0QMWJ60Mc1btBXSatH2|?Awwctd z)5Uw0$tPwce@m9MqK#i~4ENNFZE{^9nRYQA^3l4OANATcB8z&yY5e28C+f*7;lEf* zy}6d5s8koMJ?G5Ai9vn->a#y|HWPoqzs5du51eb(d8B1oo5wLQeWw2h9`{)|>^Z;3 z{>*OY6xr`F7+yE*LZtWR!DHuK2setncw3Wm_OO|J^y}V77qIa|69M~ z+-Q&6^Yg1p2s(V{A8_x6Bmo12G46rIzdrL{1^javOTu|Sz%}NzjC+-3aWTwW{1b1* zxQHDj+QoRxkNTJ&^|Xs|+Qqo|B~9ZWuRW82qL2mu#47HE_K)dlqp`1bdf&B78}J=J z`mo16a76<4A(K-Njv4?T_##I%?kR(VbZ${*Ef9Af2biC3yWQhI3x_@D=l8Dfxo?fr z#P3-@_UJMBUPkBEe|>!kL5DG11KI;Go^>0hex02E#Q)`YHkRmM1KWA8_)lJIjK@90 zBN|sBIqVhJ>UC+H$7G2Vy=J{0ontW>v52Q)T*M9%?P5IUM}5qXdfLS}?P6T~lBV&m zzdIW;jI!XL!Ek>l1N)Sw&BJT@0_rzzOC?V)D#=VQ-pLKvd3;yl<>mf|x+BtV6j{9%- z)%LT*>1Di*ee;xcDWl-uM4;Ul>fw z(CK?eo5kY563<{@s}kw+7dgXw3vTOxUN>Z24`lrw$H4sP#+&``8?$iOC-|4|QpNW! zzwq5RmyrK1W*Iwt|6%9kJIBlLZ@!28XGhMuXU1PHIqQM>w|{$Mi4HcfP22nBgnz7s z_aKJvN_yNUTrcVno4)7O$c#li75hQ#V~ln&z6|_j2@5S&1MqAR`~814`=@OeW}Uw` zx?hdF?audj%mY_h2=xIzoP+tpwXp{9`CnrmxhQ_5Rfq%353l>0$3I?g;O`D%9Conh z{J(F%F8ZCz7~}VnKm75BohE!&x&9vF82)+glV6PZ=l!Xb7w=c*zxQPxgg9dn&ng#~ z6D6E>F}@7^WeH2$P?v!#>^D~eT_vI|u*5SM*s4UZCH@!#ocYE3PWFl4Fbu!TWztj=ofK))VuZ!zS(h|14)e_ethX=I1&r z$35hJR^p*})vSl>(p<-^W-Xz$>R7~cQOCR}N+59YGxy|qOgV!9IcIS%`@`RyFB?2_APeRAFa19Q z%&AIT_B+W@;<>0R!#(x1i*ee;c+8Lbm>>1Di*d2bTYLw2U!xYfri5ya)oJ@^&>9AQ zu;-+!hGJ7xbKXjf1P`%z)ttGpc-5?lwVT|=tY$s79_U!aQ&Gn}DdV(@@nzsIOUPSf z2MVGGy(&%q_mE4YN{f)cQ!J!WCI4D*CNZNmpcbr!;?=S);6D_rmNlUc1Y&tz4A;~b z{perDX&2)c+F#=5t$YUxqDuZB5#%BX3%AC`#zIyKMIi2HW@cQ7I3Dtaj=0D7i-d^d zF@NZ5!I`)rr-Q8)tVy6?yv1DGBCX3~Ifh&6i`P?zd+KQy=~qKcyFO>6_e> zLOrXn*t_n!>k69ecw{cDA;G==e`NUIVi+ruu!V-$+cN&|KiI*3!Fyf;zKMJOj*1+5 zjH7{DU-W@*;{M^Uh#^4P%+)&V@$7oM~;RTSzJNv*sWpLUHUsWykxei+ch<{>UbKrl=mMtZO zxpgD{HSQ?`|Mvf~YG25;U_bU68}Vwv7+-ITd$7h;v&L8qtPQQ2^@(|CJu4Fb_#d9~ zd#?iaNH^#(rge)c%e^m)4PA`%+#w8Vyh0}K$QAxdu&uGKWyG+3S%W{z@44rmLQhXm zW`ez%-(N~4{PWp>&jR}TAy>jbpAGmdVEwDX7I9B5vC8mK4Zetb@=!dkH|sjnVlY{tms^2j3vO?DhU#dUe?zvn&giPizu z0^t60pZlCA$l_l>9AF;b>w)(h)~;Qf%3%k4HNRiRg~0zu?|i%fs)YY6c;Zbu4qc3B1CHT`EJ$VnXi?w zfo*Z^3B&&4yTvu`A^+dEKUSiH4Qy8x8}Opf&U$1C>CaGoe%2!+9@klk^D`RcmF&~T z^?pMR*%D7Ow$%8ocmCw^H2(E>XG4Zj7W^|9&U4B--?X0N^>SbdxCXAW5Ns9xJkE)G z{rnG^Sl9TcEbI1hfcedvH+%eN;jmZp`^(q(ha4j0Vzv0U^w*0%zWGpz4mPk={;B7O z*e&(ATIzx*x#T~arEyQWxaI}p3KF+lK<9$twciV@Je*a&;{%0Kk=k-;~fq&viUqiKVV)j|EkD4I<6|Vv7 z<66^rC{~XD*d3}(ud&Yv(8Y*0;K0&%zx&;UfUyv18;d_3Pvim!uLokdr#<2j)0ME# zaoF4Wfoc2=Ko*MOU-bU(`z!Gn{`FZ}Yc0_D*O*$2T96g{S@53~`?vMp&FvUKL0iTwOmath~dB9T2Kx4xpuk6 zGTh?2tHD0kZDXwz{OfybW$dHX=|0A>pG(>FdMIOEp@w^T^UXJd){@884*%F#NYA;m zavLMG-ZY){ADKGisv-Jk!9Ve2^MNrm&w_tq3S2iG$I94Oy^buh#0cI)%&FIqMGnM5 z?2F<5d-`=2*Iy>{{=e^U|JHdD?7UA1`wJhAzx};$bMN}tJ3K+=xY(?^`|960ko<9Z zCZauj(f<6;|J;GT{q1jOBHF{3>(lul>frC8p(pNzpBMT%V$|De$xZlh-dgezKJ*9f zKmV;?R7LQCe`x>oUZ%;q)^RP>;$LGA*Tlwn;I-_p&QBc+@%5ZpWr>mOcQGFND+2$( z{U_e`HcuZH8H5^u>qCAJ`!~$}R|mT6WBea-RfT(9 zCoq3K>$lf}y{QNP`ns_;#6znU|DhUy`>N-uMwX={Ybwes0{_7NEAM-sr*rRorvsq| z;2M!1IQNPD&rYpzAk+Zv$GzA6-sAt3fB$!vg@Awfh`(=_TylxW|KpE8?y?Z@4<9&( z@>kSBTtTnS|9v7~hZyy=(EJ(~X|l{4TxZstg#Sv%e?8c@_^${1xF^_el`Fo__W@$O z=tJK!9`mFALi;0siGO1M+?(FyKK|CX`n78O6Z=<9zT3UyV+-d$u)O&(=T5)!N+yD> z#s5G5^FOG@^FN>cf&KR>{PX!A_$T(?_TKsD1BtupCtsY$|In#ZE(-zw@PRMd8{haw z2TaTF#4-_V;RD|g{zM%-SD{zue}l*$MT~k{X#TV_UysSW!FA^6|H4^YFaGs2K)vU` zc?MV%{KarfJ#r+CXcyyYz8GWv>i7PDeeN5_@p>Q6{5bc4eU3{EkN<&n&K;Vb&P1@a z`2WmjK9l)%8Eh8;|Bnj(uf+O8EkFO~&vjj|(q!2~Uym=u`HviA9;U4!HpaE#o??CV ztdX&b{GxyTk3HkHgm|TK@mckYuQ~@zKQEt6&qA<;4}8)7?(hE20n_rk@k|6;_`o-m z7orZ{W9ZfS|F0sy2Qlhtq4{HZX!SC0aGm-2zi|HRd&4?V?Ve+wbsWg71J%|770cr$ zcJ5MNwD-0lnRYQA^3l4OANATcB%>zA8uBo$6`zMZ{&zGv z@9l9O`+EPhjJmMt-gz6?TKwZ40{ikmyMu%={N4{7wu^xOs|Ej;$TbEV&;8(_Qofe) zFWbNz0QU{|JgsJ>jY)vAc*U@pn63l%z7%6;iUT8^yY`w8YbD_yxVOLmM;*X*;@WZU z6Z`xfAZh@{g;qWJ9p`}Q8`tb{4?p~{=akW>4)}*2w(x;3+PmNVZjb3d`lCN`@|jx) zP8ogbfPd(5Z{Y)9d!KRMiaL0%La)yMD@6Wt#Hgo*=1)8G^_a{XTxS|a^cehK?eiaV zDiX1@S*+;whGg+hjaM4Ix*qNgsXEVcYh4d8x)SlPfB*lm{GGg9W(od@ef$pa_6NVs z`{wa~e7AGAT(!s7esIdX=ja3f;IOs$m(R<)KlzhCac_R}n?0wDK6Su9^srq7{J%r+ zzZ&;4)bjIx;r)NvAKq*5)RqPR5+HLt;w;8ZEFIU&F+b{G>;AZ3eE#RNKd^uQ?YH~= z!#JP+`TKuh|IVMxzdm3x5ZkD?#6N2u5AGW-x*pQ%-x|9>U; z|6dWKo)((lt|!))d6(_^kF_bvyhSnmt5_xXy;5M}G{#L_y=E`R{HTAe`}2P3_x^x= zoZHwNj^p({p81J=j_dmeOviE^W0tfpK2R@ixK7_fYt`!x$>Ny!*YX;G{(9|29rxd_t=b>@S{5T7aT?=h z4z4}nF+b{G>;A+=Y#%C(i{Fwq;7JxxMzM7({QsfI|I;EazHAMlbt#>ldP)3&|CBY^ zHCF5c|BD%qIE`^Nm!3C~V}8`X*8TA-{(7j0C%&&%HLsXZxxvM{0|^EzVJzeL1=$DI zff%o}wwNFF%iX_-{k6htA>bYVOQUAuO?{ty{t)zm4}2h%Xh%Qtru|J0)OxTrm1svl z@&iqK9q3@|zEq+e{m8kl*!tyr_xyFyj(+6S!+(8qAxIdb9sM-#eE3?|_xPt$2{zhi zk>v#QAN$zH3X4gy&Ls9?u9-NEaWk)8gO_7|)Gv4cdhzc*{NY>cA>K>mwS0eG8NKB# zZ}ETYtH)qVKXTxD-{?N~!iao#PLa@mWXB`!@tuz+W6;5te&oRQ=)Ms*^1@I;=s)+! zR`>kl+mkWqU`s#pwDte?x4-p&uKFV z5%BK>|1}eD+pParR^V_?sp0(E92Bqy9TVE^ME|G3Y4=wta$ z`~%}8;(yEWEgtvOr{RBi_mCq2^T7V`Ek`~6p@%LF|6DuQiuD8cIQv-#((wPtQ=P7V z+g|s@o9-+j=r9($evN;g_t1^^f7#7!`>H$r{LP+F2NnVU3C=C1QqL)MMWHOseiDaT4RjxX~IA^P_&b`!53io#4M_;@tu!G&wT@Q*rBC;pT3UgHxuB^HTM>NJKqmd3R` zW9Zm=T*oP5jVYCh6Q?m=jG5y<{Qh9_chI%&A8*n>{KG%^+`jhOYaQuBANr8H?6S+; z-~avJJIK%@|6xl%a$p*`2gXTVN4nfIyPt6z_HJ;Hp~w3HA^Zc=9{0d_k&bj7axXmd zyxY5Bw}T8l-VX@jA8W$-4veR*ALFp&y2*L{KYO&SKP?(=Nto7vnKM>gzrKWAjqac~ZCxty+crCvM0s{_$QQ z=&N7-YFP~cripuUi+^ArG&DU_Rs%f#750I3i+^ArG(9w3Rs;ARAJ-0y^O+wU*TVJl zIly1I{5ZU`+mU|qP@~(_x;01`i(Nl3t)KBx1E71c?elK1>w1s=Sbur^Fz2tO4ZIgKG+n6XURxZ7P}H`f7jJ3T0}{(-@Z^IsU_EKYm}Bx;NOn*uGNF zn*{t{bImm+#AkoJ{)dd*hfNIsJBM~UO?>vp^FL%@ALD8GAKH1sk@(slum2$f`xp!1 zA9&~bSqp%366*nN_zVEtpPn8qp&vf5!D)KFPo8N4#JvO8_-7r^_#f-P$zvaUXl{SC z_%Fep#v0{HSk>e7u^2`_W+#{E*AN@mde#fQKPhr*4l_?<9P3J>m>>0Nz8IrjjMwX5 z@A+Tv{NnE$@qg7-S2<1iKOew7bXoAf{ls>s3BUUT?xCxMf8t(qU>?8!gDwsKJkzmG z$iO#bu3y&y)Bw~7T=>3$7B@ZeqI>*jcb5=!7z6Hqee%~q!r1)o&EsD`}o7ptPyYET9!q5Olh5#8HZetX}MAy*wQ|2oy9nQugs4q z_TfXE#&|JqG)Byi`la??ZurN0eVTxM5Olx~utn_X`v^P(+jy@}6R;104(C5?EcOwH z_{V#Fnt*)}bi}{KKK!vJtWB>SGVrgT1JKv&-`4uLJ0kA^h6rQrPqn+5V>9l%H_87- zRs@ zhRGS5F{mf!IJwn_`iK>{;wW}uPn^bhF=jMI%#Zq|_Fpdezx2{e9q4WHU1VuQJ9{6W z-u<)#J@Nb#sYE;Lzx(N(4)pvJJ5q^u)}QMNtzAC@&`$gJZ+g&$2(en9Y;|M%#)5<~ z9p{(#e8fF?$A>(j4p7z{Hrgj3mti)9rNzQ7@vGxehk0dvW$V%7+ON_&5i4}PMLovK zh&YY$kdM~I{HWKqA(?hD9`ez;m>=~;{MY;$U_Dnz<~%VjK6$IK1DhV+FRyvvKS3*Xyux+2`C?kGbwL%olCEK9;xCYuk`ayBH7o zXkE;YdTkq$X&2+-leY>x(Es8(2YPO##S{D5ZRc3&S**vNA8T{Zjn4ZH9NXvyj&0C6 zxLRy$Cb_SltcUbEMJ~fXv8C}zEOCq&qYXLqjA?BdgPcCvj=qdf9S-6_w2SeWpVEuC(6$XPA!uX z&GjE^NY+Swh!>76T#Jmau7~8lFBbkwuutp|SHvYb^_tU1+sL(*#MStuueN6ljL|nX zPJbO!+uCvV5m(5!7;dN+BiKfXb}^pji?Ns=^%vS-;``R4a*{GR3`*q%tj>GO|3e<` zT-ej19MZvl%g}-?{TGAx9dC8JrV9?-#_K)S;&x2*y1t{$WnTm_Y`MfOsKp8Iy+wk5+o48J zh%aq*JtX&gG4P+{H?c#m@ul${8>bC9?Bx0q{A)d9kYkMF^uZW)9M_z_NBTX9J#6|%7Ut|azAy&4>qNIF*u`4c@rB=g!)M3zPD9VR5*vxRb#j~FcfpUb z^;((^v^WsQI*)Y;)_ckbb7HJ_c>X$C4oRID>~-C8-G#9&o}dHy{^*wZaj|JRxUi=( z&+2+e@@-M@AHYWP+)n&d!WPG2uVuNuBo_5C&bjDAI~$XBTCZ*OIAUqSSh1huUp4}* z<9Kh9^BeXqJpWQAx3=C{e)NuQ^Zwlk})?_a9 zyG|_B0`xbFJ$PE%Jhb4`IXK`*Tc&o2TyPyAZ6oX1Hn-ca%f{_Iz7VVNVCidKzlKBQ&jhq-AKc>F2e*~d3F#N9_1MDS;CG1K zz|6iP?RAYu`rIaoJuof0>Ah}?@J_K|>>^(+yk=Si{3p1q6-)FZ2iCNVzFec92Rx0m zt&PK&T-4+rG88eQR&~*53B5z3p53Kwne~t>x%e_w0dk zJV4iUZ2sJC9o*r%Cd&T@9-?%O?sh%ndpvEOJLI~>u4`hi@V##H*!=&^fec!hwNz>! zS@>LA=}3C7eR!AapFQNjt!4YjF1O|6VHb+saA^KDy!ENJ-+X+V>z&#sbimVwLj~dc z6&;X0EURs+WuBTz?svV<{~njbl;svfvepv(+HvjAG2C0 z2L#)Pe1Be#v3cqo-yfW#ePmyWzFq!DZ>5E_jg;39=mt)maG|kw8S9@t8XT)MS7^L% z=5W~>n~v{tT~mkL=4nOf3w2G-Q{(Zn+O}Ha*GzK17Y+Z!47tV&Wa`Of-PnVKsMowW zF07_jhG)c5y``R`nTYK9N`DmTBw|%iKYfm4V zKcmQDpye_LrD&fxBKdyIQ|HuC*E#i~S`X}5}_=p8E4;)LBHYne%C5WrC?6_PH z_K757U5#;SI_J4Y@*MxJsm}}kOVYlJ;|pu(IyL1w=O&Uh zJ$s_`S*C4v%sqYlpr>b!A95If_QV3`7-3g0Q|+vgf7kuK@qD`_eEzlH9k3iD563ml+2X?Q_$K zf7|T5taa9y^K*s)l@X{y|(S>p7bJL{Sz7jO#yrin4vb>>t;=rb`mLul($1Ed- zT>GHkG(GE@X3MEzdchWU))#TwPsx1ZzhO+C8-$)ZvGDAn=YfoUYo3$;y)`l6>FHr# zTUX0a&2%C0PdtHV#T;$rno9Or`t`H~|6GS&8|UO0F|KW>(_9vWHKvlqyX~(4|9bAr zqWW%{I_Wk}Ez}#xCH-mX3&kegFnQ8#m=G*Z&IWBZP0y9-8)VL=nbWQ@KpSUD=la!X z>2rl%$GY+Inxn@lH_Tv-GkP7XWvphpkoYH_G>+1+7QhxSyIx*`f3An?Bd4BR>-E}r z4YDZQhg7n7xBV61U+=xSsQow2oOK&gDC@;NJ~ZhbADpLW#!#ouYmAqKd2+aTX=bH3d^m;h1*TivhSwt%KF>dj0`;}r?d)r?I z{<4I;rR~7L(6iSM3_pAI_VEo@2wl2m?Ac4Ujz7Cv`|KQR8Pk0&?-*~mVf*;TPm1rA zTSwPjx>fwu_*#)`rh(gTdtE&wa|f|SjA$GYOBf?@oc831E3{ZGxW=^ga9y+saar7J z0pFEsT**D!I@-K}imjtfw@?P(GQR0;x*w9oNBK9F`j0gP{Rd7o4Gs)8?Pk1zp@xSB zh8up6^hY**VqkRB2a^8y#{V=hvGIRqU-N;X#$n|A?-?&=V@H*Pc=l{nFCmQy%pT-BqHeOWt zC)UX|wumd@o?Nc~g5sa^=(TWtvwSB!X)|`s}fsxMso)gUt^z9vLzP0=Krt5o7G+e!9yzPpC z@zzWGMj9{a9c^6QH_`k$eExsyXxB@<$6Lob4>#=ZJhG`kU&{;QZCA=VF6$d>zNB}o z=}-H{oBo5?tgVOYIs4%&a|h2>Vu+krBB!2Q>mehq&|$pbfO7Xv}YpnIJyC&NH z6!E34t%oio{)r`WVu_r3a;=wZx*+(+9Gs7w^O93fP9Mv$Mtfe{e(^e1^!eX6+BYQE z>0A4;_KlRmJBB*H#D3@K=C@Psn(F>T%6((~i9dARBR!9FkM{hqFje?=|5X2%y2rY& z>K^a@Ann1sCb|;;t`j|pf7ejY#;)P+M}$i7t9`xy-a0`$ooe7&r@zW($?SdVtWJon}@q^mFu})uIK9R zv7Rf0F6|!gxuk2NXZ7aEp4aj7|E-7H$Kc~|3YZ& z8Qb>D&Y{A6ox_FOdd9ci&^5XBn$442*K|&7xlG3wd2Ky(A@E-h#(+V&9;Zmex>^#8 zHGsU@brSdFvZ@^ab={hB=dQ6Gn>t6gKhin2{YSmCyT2`TeaFO(t2-ulT+unT;~nK5 z<(iJs9To9k?j?0&WJlk??7khH0soGP=dS3Q+WC!+sU7;gHy@_qa8H7N8@GLY+pX>6 zJFah+YiOT*?uz!Q=Pnhxqh_tPuPgcd|IqdW{WE(9`e*hu_07ES%jTgi_cad< z+}1Pm!VTRsyRQ*i(>eLvMS*`}jJy)oWZx2;LWW;aC#iCty$Ltm-2kL~X7ojLGa`}poM|F)?; zS8kr!dvn{&3jzEOMDbr~PCec{xcxx)^uB@a>HSUJGy8wpFu47`%`^LN6S|>ucHcEZ zYg(sXSl0L_o`@6ThM3Xga_tEoA?q>9MH{_M)yQcvlesw;xn6T<-PB=i_Icm-i|bG3 zvVCiB`_|s}t-bAAd)v46wr}kN{Z(C)2S!^*Uf3sNHMPwX2S+zg9`gR369*gQ^X`v! zP96H!j_E^dg}&TAbLf-pvxly1n>}cA-_IwX_aD_0w@vPURQE%2>&e4q^`mWUe}CV} zW6!lsl>4_$AH1@2=J3s}vxmy?ABtZ|*M9QQYHQOpwEMv3>B9pZGl!cxW)J_eefIEu zLbtWe9lk;MwL)uJrViLWSSxXArp0{zx40orh%0beYY7(g7-SuTW5!3isV#|5|4>Yh zNu`Tbj*};DZa#VZuWdZ}y#M)@u|s2BbA$VY z`dcOrH?&M1PF+LG=@YAMoJ~V}542Ao9gsa}YMVLwOQHK(XOG?1I(O^_p=(=C9$(Wq z`{Fz4C)e;FMP6GEEf)TP2jYX+A(!=4g|Sd9#DLz$vVGccZoMYTiwHfkCplBc+FEM z9@G7-a?9lLLig#3oh{10d1mN_=GmcZn&*bsw4EHex#`r{8!&(KnQ>sR`1${1=Qo}> zFxGW)bYIuWvHp!Cf`5r2>$q3X-FABH3ig{$jjpClzH#W_ftJY=TUw@1G__0*{<39e z@W(Ru?TvFoH#D9czP9ny@IP*x8%*%uaBgfl;2+B`J2#=zq;aINWHF`fFor(k=&>|e z+cSo>=e*>0oH58F<1$=&owjfBV*A$K_N~3`TYKBL_O@^BZQt74zSys7pBozkuckGf zo$>gAPseP1{(9b~>5;~!nUP;M&5quu^;zVm>9IoR>6x8PGo!zxedFBt4ee*9?`u0V zeP`>L>2J22nfh|`nW;}VoSR?2|NQ^Zk@Fh{UmTOq`0wjHIoZE)^mxO@F~R@zXoCNi zv(r~MpP5eZzwz|cYR&=PIy*N0%+Qg8&kVmf@a)L3=JjJI9$!B`_~Z4HL$`038oF`) z^zb$7XGhnppBuZZ@!a$q;Irx6^s>UgEU7NcYbX-!eTp)^TcXU-QgVfAj2gL-XA9V-2%Y9{(+8Pha1B z_SDr4r{=EMbo%6_8&A(&vhnQ6)r`?PJN0tQ?9_Pk?9>73V!Y|>sdqM>J@p2RZ#sAS zKMGr04=uv;zf8Y?0gbCH*dpdMrypY>K6S98-g3r(o<8&=rww^5Mr>SPtH@|2HP7}f zUTojm+rG88eQR&~*53B5z3p3j+ZTJ^T27z#x!Zi^OcQnBO=r$|{6cOx_tLw-H@^IG zf*fbm$J3b#J`_Bz8pD)9IKo`0hr z^Pm5pnLXb$H#a7J`x;N4>~B1Es-fZ3smC^*I+dLN8&99TdgGb1$@$M1{`3DcbHCm& zJ~^>rV(Q@2BjZ~*W_iQ;^Z&T%{P}loIDh^P8(wi8ZEH!#YXT=yWM_bB> z%h=R~uw0Fwwk6JW>`LRsH3{=A$3J&siTTFaGg~&yoonVY)}MO$JL^yX`YY_OJNL@F z)}8+!|0nyFub&bBnbXZ1X3v!2AGRCL|Jv6t?T63*H_o0u-!OY-tYPl#KB4|iC(ku( zI(7cB^{3A#_=nHaXI?79KVz+%oSC4U#y6b*%{$kh|IHhe&03LbrbRmcv78e86GP-0 zXCbV?j(XZc7mBOL>8E`;Cw;X&?PxLV?Y~AVKl;A&YdmbN|nRt9;_TUp^b6cL7I^VqEO(T;lB#$s}uobh!W`s$c^T+56@TRjGu)wZ48rl7`{qOy1%B7LDxBaWGy2_=I zwYU9a$BwxW9X)!~fvmmlzwwQ4^#0(awzf70LR@Qa`)=304n$w@O`-xpw)V>q|5#$l zxt)eF;tX8Nh`~74pLWobsH--Pwzu=>esMj*N}I*I?TfX)ju^N0wjcAO-rCzf?PA>8 z+kVVX=|!Ax>vrDjKtpr;-EV*UThAeb1_$MPGK%B0xBQleryQtz*PQnUC&8cN){p(q zeeQD(^n2%m#BthNzVpy72b%u%tfCo_=V|Am=ViR?`}dMCz$Y+HtiScGZ*?T-A%`%` z02p(9H#;}l<2ijF7S&!+fm{D&iho>E<{ChpX)IfR;#=El?52$~mX6Oj)-TkDzfiDQ zyx6`ty{7O_tiR`zZ}6DE{h`l$ETfMx)PUedijvp|)?a`5*S!CSztZge(MK(SP0atM zn{M*B2S$Fg^QZ2mZTGn|{XcLgyT0d6ee(f72Ai1w=KVeH@VTQNVV)76y8G(iC1m}VG5&Fx6>C5Ylk|;^Tl}Wg0>&b? z$r;}MVrOD?TaJ)qh#%EKg}0o*539nw7A?i=Ojz0AT2Wq|TgPz#uxS2NTa-8;A{FgcYacvcAfWdW`Kjx?OBF;Bp8#ul3#~<+= zuK^%~Pz%tngn#&7_Nk`CA2JBF0RCzCzvGTO-04?daeID#mFJLeT(ig55Xd0daJ`lT z?@iO~9_Qq&360ZjUXQ-@V;}fE@#wXlW?I%J_=ikAY-n$JE&dk){&AV9HGsG!7OlM< zryX(5adM86L(iDdQBQ9DXfHk$o5hRmO90&OC|P^kPxHl?wYU8X?Jx0@_RB5bO>hpp zgMeq&0m!gP9E|X#pG+k<2i`&SXH9^OaZ9>)-F256lJCgcbKjbT(0|_-n!O$}Y!U|} z0Nam^Kjm@$==iTn_#?&sJ=@~1UoOA=a*y*@4nOHYIKS6@?Q$=pj~W0QGi}nX#s4C~ zKdv!a1BgFxVvjmvl$?6nl845jqu$O>KgOdS`mhzs+F%uUH4U#jEYd{HV9~wokhlxAwLl z^P}F{+rHQ(?Z5YaGx^;gasTPN-|f$E^znN^*dz`{`0NkI-Q7>mc_QxL^zjDok3Pm= zW89MN{rBJRo*x_Y_(u(Z{7--Jyh98S`mjkHjPS+Yo7^qO@0h1!!VP`%*mvC*dcG3% zU$bV7$3Nb03=!}T8#8Uvt;PQ$!#}PysR6_safrSihaNgTPMS#uNt~Z`Zvkgr#)Fe_CNBGkGO4jzQ>I>ek({AgAMI1uf_iK?E z<0XV)j6qL~YMHvMT>C<1Y{;6p9^tjLDE1}T#em;>*I%+{_qJc}(x}8Z(E0csBQA{s zE+wj`o_flqQHgP&^MSUHI@0^w{>fPixRj`X@s4l&t*0Hgyv2do$5@GRpi`~?MT&o3 zxz9afQItua80T26T*uMz5s!W0^)$a`2@vG=yte(AANAJW_GuU6*53AGe$-og+ZVgM zE#VGa~K*TP9n)+rC-X#pu6C+n9%`Wd~Lm{`D4=VL=A#(dpQWDW|T<;@$Qu zU6Zx9{iXJQ)7#$WKJ=jvIq>(r|Nly4>ZsRaZ-3`Iy*+GB{T_d745Ws zF>hoovG)GAFI-v=an3W_wz+DEe&DZL^}3g8A^5)j^{;oJzx>O;Tvo(&VU0`NUeoxN zy-&S<;5&AIfbZBC^r#8Ia?A#pkBvcJX*|t$vBv7bK76qb;J)+9E2|>-z!$jxKURJA z|E-1KOPpP7_s6kqIfl%iN;z0v0)x%EXK zxCb>fG`QB*R{vQ|$fyN(-g&3@ColR(u`lBnJ$`Myv3*=)8fi>xS+KoU{E`-`2Y4Nz zF|B36_S(c*git*Q;lKMtyXz6szKbFEoam6a9c6KI{Lj~kR|EgR{i8$YJ?0-gcFxxT zU>~)BbpY5$T?o|x;J&%J*<-%3vC-E6U>}6fXMzL!s0*qFq~^cHi|tG9t!RJX8u%){ zzF!MD)NDOKt$;5v9KyGbBjamzAGLzM)OQ{4a$6_2x~>!P&*;*&TAsv~IkfLe!9UOQ zOE10DEhTXs_WURIfqRgC_6OEc2ktv~+SdVai+^GtxCiNHe_$Qh-?(w3uLIyY{+)le zkz7x$Uwki5<6A!m5Gz^-+)IBg@edqpeCy```fDBRrLEQY?;PxR+h%vWEmJ!k33Tt_Kum|UZ#JJ|HZ8rY52#vj`JVb=l+ue?|26I!Jd=84utTJa~({UMbs)!oHv9v_8vDdH_Z1x22i_rTd+06piEY@j0qoM&qOa|tgsyXN zyW29oPw;=B$oC81Ud zZ@@Zg1I~Ru1BCF;*Z$TYSVwKZxzA^S9RJzyZ{PpZwE+8UUl-_cj1j}UCxY*_l)-I1 z(Bts!9NsCIJ?sW%4kUcb$-}N6wIIUV#YfKpxnpQ&NsO(DLME;l0ao0AoH(77H$O9)& z$k!~Lv{?TtAJ?I8b4-4K0UF+Dv za*}cAdr!(|1J8_jnQ@>moEep|A<-XFH1E?kdcYw*Vq*^2(>KTe;-3E@`~&ZJZO`ZX z*!%l@eE`n;0}j07wLPEj&HMX_DPJFeQ)%&P`(pQ+#y_yHagKenSWl@BWv~C?Z=VHH z>O*p$;MX#K)b*SibKvNQ8pit1Ot_Y@1slj-Hy;20+_336HD2O}_$w9vzka0EX?nGu z3u5>O_VL~x>OkoH$9sZ!ZxFR1gnwWk@9m)ugwB7wCy4h3Q5$mnFVr+!?`$8~);P0R zM_*{Iwqbn$*Jn6vEY{H%D)DE1AlI>*CkGu!>#W=|J|s0_D6wgs5Fa6Hr~A#5LlSqv zhhsVZU!C)x@9Tl%cYUY<`gef*y&!5p4F7xv;P3hM?*RFGLDYa8|Jm`cu?$RDtOxqZ zYX#H>Vwl`wJgrCyrkz z&UNV5|G+w*`GI{r1K@9d__sg6K6D}M<9_12=QF?lTOY_EU>~)?VjsTtyt92VK@LXA z+S`7bFUG9B?Ju=I@DCgdUn^@#8tX!$5CL|&9`OBt;Fy@EJrtD5VW;as$cDDB7SHj& zxaWTe|G1ZY=I6Q3I)J)>TEN%-vHQ+vesG-otOKYEkonr*`dhr&zSu{xk1=a+`xm>v z#WeOWR1b>!wcu>15f;-J3)O?7el0lLO29v^rIybCh?}pE{QLh}KmWsXmbl}&zMRf~ z-fzwIv%h`b*LE>kKl|(FeQR&=YWrgUn#VuRabOu7*yrB_Y8_;Z!G?C|7qJc~`&O(6 zC3@PB!;p6Ne!bfLde48g-qbt_kiocu>DH=^T&Vu$F3f#Yw;_4x94^b()x^tI+SL|u_wRdTpH1iJjZ{Ie_}1fB~OrdX~a}o1+3%y zseyBFP1M0AaS$Oir`A=NyRO&%q33_%&11NT-3PkA?!UVTT(3POhx}1H+;fNJ_vk-=oNJJs0u6jNkuH%e@u<`!%^&%li6tE)o0h zkngw!-odrZ8lZhN)tF2i9~jfv_t+Ku;yX=T-^~^AAM`&m>dyc8e(&$O^z|O3 z$vz(Nj~XJBRs)EAVEv!o(Br>1?Ut+d_*#HIY5{EYd^!H}^FM?;>MG$+=MwQ=&G*-H zo*1t*{{_K+@w_GG+=(XVDJ%Ys|F0hr{31rtAAP-VX|l(^`1!SP9r8Xz$mbVdoj>=1 zb>Loe)P%h~&cR0e#_-Sg0r_`-t(RWyK-6C-&gT%WOV*0@Mk$sz)TjAkjCL`e`^Wdb zxHrP)8}QHXxyN_j)vY82*drZG!z#0ngU&l&l@=x6Pi{ z)%xpr{yj(365zjAfD8N!*EU)Pwt@2-*X;4!fA6};=%W^(pX0wYpO|McF8iCBe=$Gm zSGvE>zgoV(zTW*=Z2Wu7g|MFy|MmKFKNIZlTKK;5H0;CgCtv)1f^*;_Kebs9%KrLmK&3%lFrdV_o;}g5e)$Eiey~ z|M}-B!}*`!zeay-WD+C8e17ALKY;Dc^0=UJ&pLn_06E8h>Fa%LcN!5VF`nj&v6vtA zE5KhaVZPP!{q^Em*ZyB{{1f}&8St23m-w&J-@ksq^9P>!QG3++&wY;Zd$0Sw+jO7v zMBHEZuKCvj=wl2v+Mnkv&t0C+{QG}$>Mu0sI=C+4B*yE_pWMGb{OMe(<@@XF-Tw=M ze_|IH1QEj-u;}q$qrb;Ke2cH~v(y{C|G>7c0lF3t`#Jve^IzA}dhw_84|28NUtjP1 zi;jQ3&nCF%wE>5|24um1z5e(+L)05JKi83k-+lG(TpHOJJZJg7JI{BX`%C@#f1Rr# zS+1w9=f~W??)|eVRc*hn`*%U`A9#=5*MO|}ufrdk$Me71jjY8!$A8`VUswL(cBfIO z=4oA;FUGWONL~bg2~k(8>-rZP|8-r-Md%#EKc54Z`uG2#oYgwn^1AXT_pf{Z>dpGP z?q81o`nEsEf8F_CSN>k?^+(s@kbJRU--TX(edw*x({e!PN=Q!gEshCWx<;H4cT0tP&dYUhU9vN;>GL(e{*`_Imk#;!GZSaT9dWo+ zoa^Aa^88=f`5!tnwQi-~|Ha0C+GqbUX6GZ$h`Xi2J?G^*xUQ9c|8=g1 zEI5pZ^E$`MG~`)`Ci(|0n*4OLFST zsVAqNoO*KV$*Cu&o;=6@{NE|&_+8%k)wMPxSGs>1zxa&wD~F%-wB!CyJ5AKXM*C

3+0^Ft@QP; z^y}02G9)j;^-GBPw&O=1b}wza&l4~YnVfpq(B5)y$kR-E*zW_b<<%v>ed65|TBBf$cN>KS-rY_{Zn}@mYU7_v5p_nlP5*KmYun&cn3a z#hA7Y$+>^s^)D#_!1l?m@1;^2{`uKoU?2bM2WLO=&(8we&;RE4ukQPoKR@NqPthC* z)l98Rz%K$mJ3ZZYdn&<3`)C>1W(^?THRrhY5jnbcjOn$8+=26 zP+Sr@&)+t$|PE!1T@QTJ&8tAl!_$X}4<Kx%xBWCdqgJfBt_T{NEq{df$8pcvk%5GyeF@FEFnO&jR`9|8xB3_$|$WWl+CX z;WPdG|2`i3#qR)z4(C63+UNfL;V(b*XMa%xfO!%>2Min97kPgE<>y~C2W-8teaWGy zFUG9B?dSfhJpT3n{qfKK#qR*8J}drp4It*p^Uwd6)_&w-o_{%h({jMpE89==#hA6X zeX;TQxBvGC@AGM07W^Yt{`vpXe75^-`?6)JHNe{2e(s;|Us?{>e6fA;t^5D}AVwW6 z&;OJfVDFdh%f9FPm$Czj^%g|NHR&{g6{%>Hq%Z_z&aI=9}&3`4`?d?u*?o z+oxTOTYKA&`B87}ZJ%~AZtZP9=10A?w|&~hxV5)^@muPapI`a;70m&guhsS?xudZ$ zRw-WYpYLB<4&?b${+_1ZleE3d@t4;hohuhrsY5=XS6QOH?Kc={n5D+lJoq@ z>rYw^gmOmf(tPv!lh+@eDVy#A!+KqzOlF3mTuKY9Jpxe}7|{K@N2S`LJAM(fgi^ZHYN{mJ(? z$7fm&Xe@^0G~XPb_2Ved65|Yz= z^ZJw5pHR+dU7kOA{YlFKohucL9MHKElGA+i`jgk6P|j#w zob6&S+hpKlRlgy_X?5 z-{1Q7H{ZW}|I%_GggLEC^Tn994avEGo_}dM5Xu>?OY_b1FUOzGm5`k0PhNl0av+p5 zT9@XV*Pp!p=v)cOdH&?}CoKm;Iiq!HzIpx0>yOTrkeugFUVqYZAe1v&m*$(-pS=F) zTnWi}{^a#1EeAq5qjhP%dHu=jkIt2loaawof6{UwlrvhF=9|}_y#DB13CVf>q}QK( zf716Slv7%l?_a)uX*r;CB_yZ$Vl3pNb-90@e`z_Ob0s9F`R4hT<1dsmT9@ZfUVqYZ zK<7$GPV>#{PhNjQIiq!X{^a#1EeCY2gyb~ey#D0%CzLZl2XwB4)YRa|MLAy%YhK)v@XpTW7;+(=l*&ArR6{zt}a-KhV z{YlG#P|j#wnr~iz^7^B5B_!whlh>cL90=u%)}{I8^(U`CI#)t+ob6&S+hpKY9I0%K@D$ zAvw)AuRnSH3FVB|<@uA>pR^p%xe}7oeDnH~*Pl?%XkDH^dHqSt0i7!$In6h(KY9HL O<&4(l`BPv0`TqfG1fu2u diff --git a/src/assets/atlas.png b/src/assets/atlas.png new file mode 100644 index 0000000000000000000000000000000000000000..1cc9a5df239971cf904ce84191b85846d3ee52af GIT binary patch literal 24154 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 { pub data: *const u8, end: *const u8, sort: SortAlgorithm, + _marker: PhantomData<&'a ()>, } #[allow(improper_ctypes_definitions)] -impl Decoder { +impl<'a> BigEndianDecoder<'a> { #[inline] #[optimize(speed)] - pub const fn new(data: &[u8], sort: SortAlgorithm) -> Self { + pub const fn new(data: &'a [u8], sort: SortAlgorithm) -> Self { Self { end: unsafe { data.as_ptr().add(data.len()) }, data: data.as_ptr(), sort, + _marker: PhantomData, } } @@ -27,7 +30,6 @@ impl Decoder { self.sort.sort(map) } - #[inline] #[optimize(speed)] #[must_use] pub fn assert_len(&self, remaining_len: usize) -> Option<()> { @@ -39,7 +41,6 @@ impl Decoder { } } - #[inline] #[optimize(speed)] pub unsafe fn read_bytes(&mut self) -> [u8; N] { let array = self.data.cast::<[u8; N]>().read(); @@ -47,51 +48,39 @@ impl Decoder { array } - #[inline] #[optimize(speed)] pub unsafe fn u8(&mut self) -> u8 { u8::from_be_bytes(self.read_bytes()) } - #[inline] #[optimize(speed)] pub unsafe fn u16(&mut self) -> u16 { u16::from_be_bytes(self.read_bytes()) } - #[inline] #[optimize(speed)] pub unsafe fn u32(&mut self) -> u32 { u32::from_be_bytes(self.read_bytes()) } - #[inline] #[optimize(speed)] pub unsafe fn u64(&mut self) -> u64 { u64::from_be_bytes(self.read_bytes()) } - #[inline] #[optimize(speed)] pub unsafe fn i8(&mut self) -> i8 { i8::from_be_bytes(self.read_bytes()) } - #[inline] #[optimize(speed)] pub unsafe fn i16(&mut self) -> i16 { i16::from_be_bytes(self.read_bytes()) } - #[inline] #[optimize(speed)] pub unsafe fn i32(&mut self) -> i32 { i32::from_be_bytes(self.read_bytes()) } - #[inline] #[optimize(speed)] pub unsafe fn i64(&mut self) -> i64 { i64::from_be_bytes(self.read_bytes()) } - #[inline] #[optimize(speed)] pub unsafe fn f32(&mut self) -> f32 { f32::from_be_bytes(self.read_bytes()) } - #[inline] #[optimize(speed)] pub unsafe fn f64(&mut self) -> f64 { f64::from_be_bytes(self.read_bytes()) } - #[inline] #[optimize(speed)] pub unsafe fn skip(&mut self, amount: usize) { self.data = self.data.add(amount); } - #[inline] #[optimize(speed)] pub unsafe fn string(&mut self) -> Option { let len = self.u16() as usize; diff --git a/src/cli.rs b/src/cli.rs index 163e15a..8ba5bb7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -251,8 +251,8 @@ pub fn reformat() -> ! { } let tab = workbench.tabs.remove(0); - if let FileFormat::Nbt | FileFormat::Snbt | FileFormat::Gzip | FileFormat::Zlib = tab.compression {} else { - error!("Tab had invalid file format {}", tab.compression.to_string()); + if let FileFormat::Nbt | FileFormat::Snbt | FileFormat::Gzip | FileFormat::Zlib = tab.format {} else { + error!("Tab had invalid file format {}", tab.format.to_string()); } let out = format.encode(&tab.value); diff --git a/src/copy_shader.rs b/src/copy_shader.rs new file mode 100644 index 0000000..3dd5154 --- /dev/null +++ b/src/copy_shader.rs @@ -0,0 +1,36 @@ +use wgsl_inline::wgsl; + +wgsl! { + struct VertexInput { + @location(0) + position: vec2 + } + + struct VertexOutput { + @builtin(position) + position: vec4, + @location(0) + uv: vec2, + } + + @vertex + fn vertex(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.position = vec4(input.position, 1.0 - (254.0 / 255.0), 1.0); + output.uv = vec2((input.position.x + 1.0) / 2.0, (1.0 - input.position.y) / 2.0); + return output; + } + + @group(0) + @binding(0) + var texture: texture_2d; + + @group(0) + @binding(1) + var texture_sampler: sampler; + + @fragment + fn fragment(input: VertexOutput) -> @location(0) vec4 { + return textureSample(texture, texture_sampler, input.uv); + } +} diff --git a/src/element_action.rs b/src/element_action.rs index 36c506e..243e551 100644 --- a/src/element_action.rs +++ b/src/element_action.rs @@ -7,7 +7,7 @@ use compact_str::CompactString; use notify::{EventKind, PollWatcher, RecursiveMode, Watcher}; use uuid::Uuid; -use crate::{panic_unchecked, set_clipboard, FileUpdateSubscription}; +use crate::{panic_unchecked, set_clipboard, FileUpdateSubscription, StrExt}; #[cfg(not(target_arch = "wasm32"))] use crate::{FileUpdateSubscriptionType, assets::{OPEN_ARRAY_IN_HEX_UV, OPEN_IN_TXT}, since_epoch}; use crate::assets::{ACTION_WHEEL_Z, COPY_FORMATTED_UV, COPY_RAW_UV, SORT_COMPOUND_BY_NAME, SORT_COMPOUND_BY_TYPE}; @@ -287,7 +287,7 @@ impl ElementAction { if let Some(key) = key.as_deref() && element.id() != NbtChunk::ID { - if write!(&mut file, "{key}: ").is_err() { break 'm; } + if write!(&mut file, "{k}: ", k = if key.needs_escape() { format!("{key:?}") } else { key.to_owned() }).is_err() { break 'm; } } if write!(&mut file, "{element:#?}").is_err() { break 'm; } drop(file); diff --git a/src/elements/array.rs b/src/elements/array.rs index 51804fe..e080a82 100644 --- a/src/elements/array.rs +++ b/src/elements/array.rs @@ -1,7 +1,6 @@ #[macro_export] macro_rules! array { ($element_field:ident, $name:ident, $t:ty, $my_id:literal, $id:literal, $char:literal, $uv:ident, $element_uv:ident) => { - #[derive(Default)] #[repr(C)] pub struct $name { values: Box>, @@ -9,9 +8,19 @@ macro_rules! array { open: bool, } - impl PartialEq for $name { - fn eq(&self, other: &Self) -> bool { - self.values == other.values + impl $name { + pub fn matches(&self, other: &Self) -> bool { + if self.values.len() != other.values.len() { + return false + } + + for (a, b) in self.values.iter().zip(other.values.iter()) { + if !a.matches(b) { + return false + } + } + + true } } @@ -70,7 +79,7 @@ macro_rules! array { } #[inline] - pub fn from_bytes(decoder: &mut Decoder) -> Option { + pub fn from_be_bytes(decoder: &mut BigEndianDecoder) -> Option { unsafe { decoder.assert_len(4)?; let len = decoder.u32() as usize; @@ -101,12 +110,51 @@ macro_rules! array { } #[inline] - pub fn to_bytes(&self, writer: &mut UncheckedBufWriter) { + pub fn to_be_bytes(&self, writer: &mut UncheckedBufWriter) { writer.write(&(self.len() as u32).to_be_bytes()); for entry in self.values.iter() { writer.write(&Self::transmute(entry).to_be_bytes()); } } + + #[inline] + pub fn from_le_bytes(decoder: &mut LittleEndianDecoder) -> Option { + unsafe { + decoder.assert_len(4)?; + let len = decoder.u32() as usize; + decoder.assert_len(len * core::mem::size_of::<$t>())?; + let vec = alloc(Layout::array::(len).unwrap_unchecked()).cast::(); + for idx in 0..len { + let mut element = NbtElement { + $element_field: core::mem::transmute(<$t>::from_le_bytes( + decoder + .data + .add(idx * core::mem::size_of::<$t>()) + .cast::<[u8; core::mem::size_of::<$t>()]>() + .read(), + )), + }; + element.id.id = $id; + vec.add(idx).write(element); + } + decoder.data = decoder.data.add(len * core::mem::size_of::<$t>()); + let boxx = alloc(Layout::new::>()).cast::>(); + boxx.write(Vec::from_raw_parts(vec, len, len)); + Some(Self { + values: Box::from_raw(boxx), + open: false, + max_depth: 0, + }) + } + } + + #[inline] + pub fn to_le_bytes(&self, writer: &mut UncheckedBufWriter) { + writer.write(&(self.len() as u32).to_le_bytes()); + for entry in self.values.iter() { + writer.write(&Self::transmute(entry).to_le_bytes()); + } + } } impl $name { diff --git a/src/elements/chunk.rs b/src/elements/chunk.rs index 733ffe8..33f0f89 100644 --- a/src/elements/chunk.rs +++ b/src/elements/chunk.rs @@ -29,9 +29,17 @@ pub struct NbtRegion { open: bool, } -impl PartialEq for NbtRegion { - fn eq(&self, other: &Self) -> bool { - self.chunks == other.chunks +impl NbtRegion { + pub fn matches(&self, other: &Self) -> bool { + for (a, b) in self.chunks.deref().1.iter().zip(other.chunks.deref().1.iter()) { + if a.is_null() != b.is_null() { + return false + } + if !a.matches(b) { + return false + } + } + true } } @@ -84,7 +92,7 @@ impl NbtRegion { pub fn new() -> Self { Self::default() } #[must_use] - pub fn from_bytes(bytes: &[u8], sort: SortAlgorithm) -> Option { + pub fn from_be_bytes(bytes: &[u8], sort: SortAlgorithm) -> Option { fn parse(raw: u32, bytes: &[u8], sort: SortAlgorithm) -> Option<(FileFormat, NbtElement)> { if raw < 512 { return Some((FileFormat::Zlib, unsafe { core::mem::zeroed() })) } @@ -100,7 +108,7 @@ impl NbtRegion { let (compression, element) = match compression { 1 => ( FileFormat::Gzip, - NbtElement::from_file( + NbtElement::from_be_file( &DeflateDecoder::new_with_options(data, DeflateOptions::default().set_confirm_checksum(false)) .decode_gzip() .ok()?, @@ -109,15 +117,15 @@ impl NbtRegion { ), 2 => ( FileFormat::Zlib, - NbtElement::from_file( + NbtElement::from_be_file( &DeflateDecoder::new_with_options(data, DeflateOptions::default().set_confirm_checksum(false)) .decode_zlib() .ok()?, sort, )?, ), - 3 => (FileFormat::Nbt, NbtElement::from_file(data, sort)?), - 4 => (FileFormat::Lz4, NbtElement::from_file(&lz4_flex::decompress(data, data.len()).ok()?, sort)?), + 3 => (FileFormat::Nbt, NbtElement::from_be_file(data, sort)?), + 4 => (FileFormat::Lz4, NbtElement::from_be_file(&lz4_flex::decompress(data, data.len()).ok()?, sort)?), _ => return None, }; if element.id() != NbtCompound::ID { return None } @@ -210,7 +218,7 @@ impl NbtRegion { Some(region) }; } - pub fn to_bytes(&self, writer: &mut UncheckedBufWriter) { + pub fn to_be_bytes(&self, writer: &mut UncheckedBufWriter) { unsafe { std::thread::scope(move |s| { let mut chunks = Vec::with_capacity(1024); @@ -223,7 +231,7 @@ impl NbtRegion { .cast::>() .read(); let mut writer = UncheckedBufWriter::new(); - chunk.to_bytes(&mut writer); + chunk.to_be_bytes(&mut writer); (writer.finish(), chunk.last_modified) } })); @@ -733,9 +741,9 @@ pub struct NbtChunk { pub z: u8, } -impl PartialEq for NbtChunk { - fn eq(&self, other: &Self) -> bool { - self.inner == other.inner +impl NbtChunk { + pub fn matches(&self, other: &Self) -> bool { + self.inner.matches(&other.inner) } } @@ -772,7 +780,7 @@ impl NbtChunk { last_modified, } } - pub fn to_bytes(&self, writer: &mut UncheckedBufWriter) { + pub fn to_be_bytes(&self, writer: &mut UncheckedBufWriter) { // todo, mcc unsafe { let encoded = self diff --git a/src/elements/compound.rs b/src/elements/compound.rs index 7e65125..f372635 100644 --- a/src/elements/compound.rs +++ b/src/elements/compound.rs @@ -12,7 +12,7 @@ use fxhash::FxHasher; use hashbrown::raw::RawTable; use crate::assets::{JUST_OVERLAPPING_BASE_TEXT_Z, BASE_Z, COMPOUND_ROOT_UV, COMPOUND_UV, CONNECTION_UV, HEADER_SIZE, LINE_NUMBER_CONNECTOR_Z, LINE_NUMBER_SEPARATOR_UV, ZOffset}; -use crate::decoder::Decoder; +use crate::be_decoder::BigEndianDecoder; use crate::elements::chunk::NbtChunk; use crate::elements::element::NbtElement; use crate::encoder::UncheckedBufWriter; @@ -20,6 +20,7 @@ use crate::{DropFn, OptionExt, RenderContext, SortAlgorithm, StrExt, VertexBuffe use crate::color::TextColor; use crate::formatter::PrettyFormatter; use crate::bookmark::{Bookmark, BookmarkSlice}; +use crate::le_decoder::LittleEndianDecoder; #[allow(clippy::module_name_repetitions)] #[repr(C)] @@ -31,9 +32,9 @@ pub struct NbtCompound { open: bool, } -impl PartialEq for NbtCompound { - fn eq(&self, other: &Self) -> bool { - self.entries == other.entries +impl NbtCompound { + pub fn matches(&self, other: &Self) -> bool { + self.entries.matches(&other.entries) } } @@ -78,7 +79,7 @@ impl NbtCompound { } #[inline] - pub fn from_bytes(decoder: &mut Decoder) -> Option { + pub fn from_be_bytes(decoder: &mut BigEndianDecoder) -> Option { let mut compound = Self::new(); unsafe { decoder.assert_len(1)?; @@ -86,7 +87,7 @@ impl NbtCompound { while current_element != 0 { decoder.assert_len(2)?; let key = decoder.string()?; - let value = NbtElement::from_bytes(current_element, decoder)?; + let value = NbtElement::from_be_bytes(current_element, decoder)?; compound.insert_replacing(key, value); match decoder.assert_len(1) { Some(()) => {} @@ -98,11 +99,45 @@ impl NbtCompound { Some(compound) } } - pub fn to_bytes(&self, writer: &mut UncheckedBufWriter) { + + #[inline] + pub fn to_be_bytes(&self, writer: &mut UncheckedBufWriter) { for (key, value) in self.children() { writer.write(&[value.id()]); - writer.write_str(key); - value.to_bytes(writer); + writer.write_be_str(key); + value.to_be_bytes(writer); + } + writer.write(&[0x00]); + } + + #[inline] + pub fn from_le_bytes(decoder: &mut LittleEndianDecoder) -> Option { + let mut compound = Self::new(); + unsafe { + decoder.assert_len(1)?; + let mut current_element = decoder.u8(); + while current_element != 0 { + decoder.assert_len(2)?; + let key = decoder.string()?; + let value = NbtElement::from_le_bytes(current_element, decoder)?; + compound.insert_replacing(key, value); + match decoder.assert_len(1) { + Some(()) => {} + None => break, // wow mojang, saving one byte, so cool of you + }; + current_element = decoder.u8(); + } + decoder.sort(&mut compound.entries); + Some(compound) + } + } + + #[inline] + pub fn to_le_bytes(&self, writer: &mut UncheckedBufWriter) { + for (key, value) in self.children() { + writer.write(&[value.id()]); + writer.write_le_str(key); + value.to_le_bytes(writer); } writer.write(&[0x00]); } @@ -727,17 +762,14 @@ pub struct CompoundMap { pub entries: Vec, } -impl PartialEq for CompoundMap { - fn eq(&self, other: &Self) -> bool { - // disabled to make the comparison work like the nbt predicate in mc. - // if self.entries.len() != other.entries.len() { return false } - +impl CompoundMap { + pub fn matches(&self, other: &Self) -> bool { for entry in &self.entries { - if other.idx_of(&entry.key).and_then(|idx| other.get_idx(idx)) != Some((&entry.key, &entry.value)) { - return false + if let Some((key, value)) = other.idx_of(&entry.key).and_then(|idx| other.get_idx(idx)) && key == entry.key && entry.value.matches(value) { + continue } + return false } - true } } diff --git a/src/elements/element.rs b/src/elements/element.rs index 393170e..7d68c67 100644 --- a/src/elements/element.rs +++ b/src/elements/element.rs @@ -11,7 +11,7 @@ use compact_str::{format_compact, CompactString, ToCompactString}; use hashbrown::raw::RawTable; use crate::assets::{BASE_Z, BYTE_ARRAY_UV, BYTE_UV, CONNECTION_UV, DOUBLE_UV, FLOAT_UV, INT_ARRAY_UV, INT_UV, LONG_ARRAY_UV, LONG_UV, SHORT_UV, ZOffset}; -use crate::decoder::Decoder; +use crate::be_decoder::BigEndianDecoder; use crate::elements::chunk::{NbtChunk, NbtRegion}; use crate::elements::compound::{CompoundMap, CompoundMapIter, Entry, NbtCompound}; use crate::element_action::ElementAction; @@ -20,6 +20,7 @@ use crate::elements::string::NbtString; use crate::encoder::UncheckedBufWriter; use crate::{panic_unchecked, since_epoch, SortAlgorithm, array, primitive, DropFn, RenderContext, StrExt, VertexBufferBuilder, TextColor, assets::JUST_OVERLAPPING_BASE_TEXT_Z}; use crate::formatter::PrettyFormatter; +use crate::le_decoder::LittleEndianDecoder; use crate::tab::FileFormat; primitive!(BYTE_UV, { Some('b') }, NbtByte, i8, 1); @@ -34,7 +35,7 @@ array!(long, NbtLongArray, i64, 12, 4, 'L', LONG_ARRAY_UV, LONG_UV); #[repr(C)] #[derive(Copy, Clone)] -pub struct NbtElementDiscriminant { +pub struct NbtElementId { _pad: [MaybeUninit; 23], pub id: u8, } @@ -55,29 +56,29 @@ pub union NbtElement { compound: ManuallyDrop, int_array: ManuallyDrop, long_array: ManuallyDrop, - id: NbtElementDiscriminant, + id: NbtElementId, } -impl PartialEq for NbtElement { - fn eq(&self, other: &Self) -> bool { +impl NbtElement { + pub fn matches(&self, other: &Self) -> bool { if self.id() != other.id() { return false } unsafe { match self.id() { - NbtChunk::ID => self.chunk == other.chunk, - NbtRegion::ID => self.region == other.region, - NbtByte::ID => self.byte == other.byte, - NbtShort::ID => self.short == other.short, - NbtInt::ID => self.int == other.int, - NbtLong::ID => self.long == other.long, - NbtFloat::ID => self.float == other.float, - NbtDouble::ID => self.double == other.double, - NbtByteArray::ID => self.byte_array == other.byte_array, - NbtString::ID => self.string == other.string, - NbtList::ID => self.list == other.list, - NbtCompound::ID => self.compound == other.compound, - NbtIntArray::ID => self.int_array == other.int_array, - NbtLongArray::ID => self.long_array == other.long_array, + NbtChunk::ID => self.chunk.matches(&other.chunk), + NbtRegion::ID => self.region.matches(&other.region), + NbtByte::ID => self.byte.matches(&other.byte), + NbtShort::ID => self.short.matches(&other.short), + NbtInt::ID => self.int.matches(&other.int), + NbtLong::ID => self.long.matches(&other.long), + NbtFloat::ID => self.float.matches(&other.float), + NbtDouble::ID => self.double.matches(&other.double), + NbtByteArray::ID => self.byte_array.matches(&other.byte_array), + NbtString::ID => self.string.matches(&other.string), + NbtList::ID => self.list.matches(&other.list), + NbtCompound::ID => self.compound.matches(&other.compound), + NbtIntArray::ID => self.int_array.matches(&other.int_array), + NbtLongArray::ID => self.long_array.matches(&other.long_array), _ => core::hint::unreachable_unchecked(), } } @@ -459,42 +460,84 @@ impl NbtElement { NbtString::from_str0(s).map(|(s, x)| (s, Self::String(x))) } #[inline(never)] - pub fn from_bytes(element: u8, decoder: &mut Decoder) -> Option { + pub fn from_be_bytes(element: u8, decoder: &mut BigEndianDecoder) -> Option { Some(match element { - NbtByte::ID => Self::Byte(NbtByte::from_bytes(decoder)?), - NbtShort::ID => Self::Short(NbtShort::from_bytes(decoder)?), - NbtInt::ID => Self::Int(NbtInt::from_bytes(decoder)?), - NbtLong::ID => Self::Long(NbtLong::from_bytes(decoder)?), - NbtFloat::ID => Self::Float(NbtFloat::from_bytes(decoder)?), - NbtDouble::ID => Self::Double(NbtDouble::from_bytes(decoder)?), - NbtByteArray::ID => Self::ByteArray(NbtByteArray::from_bytes(decoder)?), - NbtString::ID => Self::String(NbtString::from_bytes(decoder)?), - NbtList::ID => Self::List(NbtList::from_bytes(decoder)?), - NbtCompound::ID => Self::Compound(NbtCompound::from_bytes(decoder)?), - NbtIntArray::ID => Self::IntArray(NbtIntArray::from_bytes(decoder)?), - NbtLongArray::ID => Self::LongArray(NbtLongArray::from_bytes(decoder)?), + NbtByte::ID => Self::Byte(NbtByte::from_be_bytes(decoder)?), + NbtShort::ID => Self::Short(NbtShort::from_be_bytes(decoder)?), + NbtInt::ID => Self::Int(NbtInt::from_be_bytes(decoder)?), + NbtLong::ID => Self::Long(NbtLong::from_be_bytes(decoder)?), + NbtFloat::ID => Self::Float(NbtFloat::from_be_bytes(decoder)?), + NbtDouble::ID => Self::Double(NbtDouble::from_be_bytes(decoder)?), + NbtByteArray::ID => Self::ByteArray(NbtByteArray::from_be_bytes(decoder)?), + NbtString::ID => Self::String(NbtString::from_be_bytes(decoder)?), + NbtList::ID => Self::List(NbtList::from_be_bytes(decoder)?), + NbtCompound::ID => Self::Compound(NbtCompound::from_be_bytes(decoder)?), + NbtIntArray::ID => Self::IntArray(NbtIntArray::from_be_bytes(decoder)?), + NbtLongArray::ID => Self::LongArray(NbtLongArray::from_be_bytes(decoder)?), _ => return None, }) } #[inline(never)] - pub fn to_bytes(&self, writer: &mut UncheckedBufWriter) { + pub fn to_be_bytes(&self, writer: &mut UncheckedBufWriter) { unsafe { match self.id() { - NbtByte::ID => self.byte.to_bytes(writer), - NbtShort::ID => self.short.to_bytes(writer), - NbtInt::ID => self.int.to_bytes(writer), - NbtLong::ID => self.long.to_bytes(writer), - NbtFloat::ID => self.float.to_bytes(writer), - NbtDouble::ID => self.double.to_bytes(writer), - NbtByteArray::ID => self.byte_array.to_bytes(writer), - NbtString::ID => self.string.to_bytes(writer), - NbtList::ID => self.list.to_bytes(writer), - NbtCompound::ID => self.compound.to_bytes(writer), - NbtIntArray::ID => self.int_array.to_bytes(writer), - NbtLongArray::ID => self.long_array.to_bytes(writer), - NbtChunk::ID => self.chunk.to_bytes(writer), - NbtRegion::ID => self.region.to_bytes(writer), + NbtByte::ID => self.byte.to_be_bytes(writer), + NbtShort::ID => self.short.to_be_bytes(writer), + NbtInt::ID => self.int.to_be_bytes(writer), + NbtLong::ID => self.long.to_be_bytes(writer), + NbtFloat::ID => self.float.to_be_bytes(writer), + NbtDouble::ID => self.double.to_be_bytes(writer), + NbtByteArray::ID => self.byte_array.to_be_bytes(writer), + NbtString::ID => self.string.to_be_bytes(writer), + NbtList::ID => self.list.to_be_bytes(writer), + NbtCompound::ID => self.compound.to_be_bytes(writer), + NbtIntArray::ID => self.int_array.to_be_bytes(writer), + NbtLongArray::ID => self.long_array.to_be_bytes(writer), + NbtChunk::ID => self.chunk.to_be_bytes(writer), + NbtRegion::ID => self.region.to_be_bytes(writer), + _ => core::hint::unreachable_unchecked(), + }; + } + } + + #[inline(never)] + pub fn from_le_bytes(element: u8, decoder: &mut LittleEndianDecoder) -> Option { + Some(match element { + NbtByte::ID => Self::Byte(NbtByte::from_le_bytes(decoder)?), + NbtShort::ID => Self::Short(NbtShort::from_le_bytes(decoder)?), + NbtInt::ID => Self::Int(NbtInt::from_le_bytes(decoder)?), + NbtLong::ID => Self::Long(NbtLong::from_le_bytes(decoder)?), + NbtFloat::ID => Self::Float(NbtFloat::from_le_bytes(decoder)?), + NbtDouble::ID => Self::Double(NbtDouble::from_le_bytes(decoder)?), + NbtByteArray::ID => Self::ByteArray(NbtByteArray::from_le_bytes(decoder)?), + NbtString::ID => Self::String(NbtString::from_le_bytes(decoder)?), + NbtList::ID => Self::List(NbtList::from_le_bytes(decoder)?), + NbtCompound::ID => Self::Compound(NbtCompound::from_le_bytes(decoder)?), + NbtIntArray::ID => Self::IntArray(NbtIntArray::from_le_bytes(decoder)?), + NbtLongArray::ID => Self::LongArray(NbtLongArray::from_le_bytes(decoder)?), + _ => return None, + }) + } + + #[inline(never)] + pub fn to_le_bytes(&self, writer: &mut UncheckedBufWriter) { + unsafe { + match self.id() { + NbtByte::ID => self.byte.to_le_bytes(writer), + NbtShort::ID => self.short.to_le_bytes(writer), + NbtInt::ID => self.int.to_le_bytes(writer), + NbtLong::ID => self.long.to_le_bytes(writer), + NbtFloat::ID => self.float.to_le_bytes(writer), + NbtDouble::ID => self.double.to_le_bytes(writer), + NbtByteArray::ID => self.byte_array.to_le_bytes(writer), + NbtString::ID => self.string.to_le_bytes(writer), + NbtList::ID => self.list.to_le_bytes(writer), + NbtCompound::ID => self.compound.to_le_bytes(writer), + NbtIntArray::ID => self.int_array.to_le_bytes(writer), + NbtLongArray::ID => self.long_array.to_le_bytes(writer), + NbtChunk::ID => { /* no */ }, + NbtRegion::ID => { /* no */ }, _ => core::hint::unreachable_unchecked(), }; } @@ -536,33 +579,78 @@ impl NbtElement { #[inline] #[must_use] - pub fn from_file(bytes: &[u8], sort: SortAlgorithm) -> Option { - let mut decoder = Decoder::new(bytes, sort); - decoder.assert_len(3)?; + pub fn from_be_file(bytes: &[u8], sort: SortAlgorithm) -> Option { + let mut decoder = BigEndianDecoder::new(bytes, sort); + decoder.assert_len(1)?; unsafe { - if decoder.u8() != 0x0A { return None } - let skip = decoder.u16() as usize; - decoder.skip(skip); + if decoder.u8() != NbtCompound::ID { return None } + // fix for >= 1.20.2 protocol since they removed the empty field + if decoder.assert_len(2).is_none() || decoder.data.cast::().read_unaligned() == 0_u16.to_be() { + let _ = decoder.u16(); + } } - let nbt = Self::Compound(NbtCompound::from_bytes(&mut decoder)?); + let nbt = Self::Compound(NbtCompound::from_be_bytes(&mut decoder)?); Some(nbt) } #[inline] #[must_use] - pub fn to_file(&self) -> Vec { + pub fn to_be_file(&self) -> Vec { let mut writer = UncheckedBufWriter::new(); if self.id() == NbtCompound::ID { - writer.write(&[0x0A, 0x00, 0x00]); + writer.write(&[NbtCompound::ID, 0x00, 0x00]); } - self.to_bytes(&mut writer); + self.to_be_bytes(&mut writer); writer.finish() } #[inline] #[must_use] - pub fn from_mca(bytes: &[u8], sort: SortAlgorithm) -> Option { - NbtRegion::from_bytes(bytes, sort).map(Self::Region) + pub fn from_be_mca(bytes: &[u8], sort: SortAlgorithm) -> Option { + NbtRegion::from_be_bytes(bytes, sort).map(Self::Region) + } + + #[inline] + #[must_use] + pub fn from_le_file(bytes: &[u8], sort: SortAlgorithm) -> Option<(Self, bool)> { + let mut decoder = LittleEndianDecoder::new(bytes, sort); + unsafe { + decoder.assert_len(1)?; + let kind = decoder.u8(); + match kind { + NbtCompound::ID => { + decoder.assert_len(2)?; + let skip = decoder.u16() as usize; + decoder.skip(skip); + Some((Self::Compound(NbtCompound::from_le_bytes(&mut decoder)?), decoder.header())) + }, + NbtList::ID => { + decoder.assert_len(2)?; + let skip = decoder.u16() as usize; + decoder.skip(skip); + Some((Self::List(NbtList::from_le_bytes(&mut decoder)?), decoder.header())) + }, + _ => return None, + } + } + } + + #[inline] + #[must_use] + pub fn to_le_file(&self, header: bool) -> Vec { + let mut writer = UncheckedBufWriter::new(); + writer.write(&[self.id(), 0x00, 0x00]); + self.to_le_bytes(&mut writer); + let raw = writer.finish(); + if header { + let mut header = UncheckedBufWriter::new(); + header.write(&[0x08, 0x00, 0x00, 0x00]); + header.write(&(raw.len() as u32).to_le_bytes()); + header.write(&raw); + header.finish() + } else { + raw + } } #[inline] @@ -575,20 +663,12 @@ impl NbtElement { NbtLong::ID => self.long.render(builder, str, ctx), NbtFloat::ID => self.float.render(builder, str, ctx), NbtDouble::ID => self.double.render(builder, str, ctx), - NbtByteArray::ID => self - .byte_array - .render(builder, str, remaining_scroll, tail, ctx), + NbtByteArray::ID => self.byte_array.render(builder, str, remaining_scroll, tail, ctx), NbtString::ID => self.string.render(builder, str, ctx), NbtList::ID => self.list.render(builder, str, remaining_scroll, tail, ctx), - NbtCompound::ID => self - .compound - .render(builder, str, remaining_scroll, tail, ctx), - NbtIntArray::ID => self - .int_array - .render(builder, str, remaining_scroll, tail, ctx), - NbtLongArray::ID => self - .long_array - .render(builder, str, remaining_scroll, tail, ctx), + NbtCompound::ID => self.compound.render(builder, str, remaining_scroll, tail, ctx), + NbtIntArray::ID => self.int_array.render(builder, str, remaining_scroll, tail, ctx), + NbtLongArray::ID => self.long_array.render(builder, str, remaining_scroll, tail, ctx), NbtChunk::ID => self.chunk.render(builder, remaining_scroll, tail, ctx), NbtRegion::ID => { // can't be done at all diff --git a/src/elements/list.rs b/src/elements/list.rs index 0b6497e..4c874ce 100644 --- a/src/elements/list.rs +++ b/src/elements/list.rs @@ -7,13 +7,14 @@ use std::slice::{Iter, IterMut}; use std::thread::Scope; use crate::assets::{JUST_OVERLAPPING_BASE_TEXT_Z, BASE_Z, CONNECTION_UV, LIST_UV, ZOffset}; -use crate::decoder::Decoder; +use crate::be_decoder::BigEndianDecoder; use crate::elements::chunk::NbtChunk; use crate::elements::element::{id_to_string_name, NbtElement}; use crate::encoder::UncheckedBufWriter; use crate::{DropFn, OptionExt, RenderContext, SortAlgorithm, StrExt, VertexBufferBuilder}; use crate::color::TextColor; use crate::formatter::PrettyFormatter; +use crate::le_decoder::LittleEndianDecoder; #[allow(clippy::module_name_repetitions)] #[repr(C)] @@ -26,12 +27,12 @@ pub struct NbtList { open: bool, } -impl PartialEq for NbtList { - fn eq(&self, other: &Self) -> bool { +impl NbtList { + pub fn matches(&self, other: &Self) -> bool { if self.is_empty() { other.is_empty() } else { - self.elements.iter().all(|a| other.elements.iter().any(|b| a == b)) + self.elements.iter().all(|a| other.elements.iter().any(|b| a.matches(b))) } } } @@ -81,7 +82,7 @@ impl NbtList { } #[allow(clippy::cast_ptr_alignment)] #[inline] - pub fn from_bytes(decoder: &mut Decoder) -> Option { + pub fn from_be_bytes(decoder: &mut BigEndianDecoder) -> Option { unsafe { decoder.assert_len(5)?; let element = decoder.u8(); @@ -89,7 +90,7 @@ impl NbtList { let ptr = alloc(Layout::array::(len).unwrap_unchecked()).cast::(); let mut true_height = 1; for n in 0..len { - let element = NbtElement::from_bytes(element, decoder)?; + let element = NbtElement::from_be_bytes(element, decoder)?; true_height += element.true_height() as u32; ptr.add(n).write(element); } @@ -105,11 +106,49 @@ impl NbtList { }) } } - pub fn to_bytes(&self, writer: &mut UncheckedBufWriter) { + + #[inline] + pub fn to_be_bytes(&self, writer: &mut UncheckedBufWriter) { writer.write(&[self.element]); writer.write(&(self.len() as u32).to_be_bytes()); for element in self.elements.iter() { - element.to_bytes(writer); + element.to_be_bytes(writer); + } + } + + #[allow(clippy::cast_ptr_alignment)] + #[inline] + pub fn from_le_bytes(decoder: &mut LittleEndianDecoder) -> Option { + unsafe { + decoder.assert_len(5)?; + let element = decoder.u8(); + let len = decoder.u32() as usize; + let ptr = alloc(Layout::array::(len).unwrap_unchecked()).cast::(); + let mut true_height = 1; + for n in 0..len { + let element = NbtElement::from_le_bytes(element, decoder)?; + true_height += element.true_height() as u32; + ptr.add(n).write(element); + } + let box_ptr = alloc(Layout::new::>()).cast::>(); + box_ptr.write(Vec::from_raw_parts(ptr, len, len)); + Some(Self { + elements: Box::from_raw(box_ptr), + height: 1 + len as u32, + true_height, + max_depth: 0, + element, + open: false, + }) + } + } + + #[inline] + pub fn to_le_bytes(&self, writer: &mut UncheckedBufWriter) { + writer.write(&[self.element]); + writer.write(&(self.len() as u32).to_le_bytes()); + for element in self.elements.iter() { + element.to_le_bytes(writer); } } } @@ -262,6 +301,82 @@ impl NbtList { } impl NbtList { + #[inline] + pub fn render_root(&self, builder: &mut VertexBufferBuilder, str: &str, ctx: &mut RenderContext) { + let mut remaining_scroll = builder.scroll() / 16; + + 'head: { + if remaining_scroll > 0 { + remaining_scroll -= 1; + ctx.skip_line_numbers(1); + break 'head; + } + + ctx.line_number(); + Self::render_icon(ctx.pos(), BASE_Z, builder); + if !self.is_empty() { + ctx.draw_toggle(ctx.pos() - (16, 0), self.open, builder); + } + ctx.render_errors(ctx.pos(), builder); + if ctx.forbid(ctx.pos()) { + builder.settings(ctx.pos() + (20, 0), false, JUST_OVERLAPPING_BASE_TEXT_Z); + builder.color = TextColor::TreeKey.to_raw(); + let _ = write!(builder, "{str}: {}", self.value()); + } + + let pos = ctx.pos(); + if ctx.draw_held_entry_bar(ctx.pos() + (16, 16), builder, |x, y| pos + (16, 8) == (x, y), |id| (id != NbtChunk::ID) && (id == self.element || self.is_empty())) {} else if self.height() == 1 && ctx.draw_held_entry_bar(ctx.pos() + (16, 16), builder, |x, y| pos + (16, 16) == (x, y), |id| (id != NbtChunk::ID) && (id == self.element || self.is_empty()), ) {} + + ctx.y_offset += 16; + } + + if self.open { + ctx.x_offset += 16; + + for (idx, element) in self.children().enumerate() { + if ctx.y_offset > builder.window_height() { + break; + } + + let height = element.height(); + if remaining_scroll >= height { + remaining_scroll -= height; + ctx.skip_line_numbers(element.true_height()); + continue; + } + + let pos = ctx.pos(); + ctx.draw_held_entry_bar(ctx.pos(), builder, |x, y| pos == (x, y), |id| (id != NbtChunk::ID) && (id == self.element || self.is_empty())); + + if remaining_scroll == 0 { + builder.draw_texture( + ctx.pos() - (16, 0), + CONNECTION_UV, + ( + 16, + (idx != self.len() - 1) as usize * 7 + 9, + ), + ); + } + ctx.check_for_key_duplicate(|_, _| false, false); + element.render( + &mut remaining_scroll, + builder, + None, + idx == self.len() - 1, + ctx, + ); + + let pos = ctx.pos(); + ctx.draw_held_entry_bar(ctx.pos(), builder, |x, y| pos == (x, y + 8), |id| (id != NbtChunk::ID) && (id == self.element || self.is_empty())); + } + + ctx.x_offset -= 16; + } else { + ctx.skip_line_numbers(self.true_height() - 1); + } + } + #[inline] pub fn render(&self, builder: &mut VertexBufferBuilder, name: Option<&str>, remaining_scroll: &mut usize, tail: bool, ctx: &mut RenderContext) { let mut y_before = ctx.y_offset; diff --git a/src/elements/primitive.rs b/src/elements/primitive.rs index d70f2c1..ef975f9 100644 --- a/src/elements/primitive.rs +++ b/src/elements/primitive.rs @@ -4,7 +4,7 @@ macro_rules! primitive { primitive!($uv, $s, $name, $t, $id, |x: $t| x.to_compact_string()); }; ($uv:ident, $s:expr, $name:ident, $t:ty, $id:literal, $compact_format:expr) => { - #[derive(Copy, Clone, Default, PartialEq)] + #[derive(Copy, Clone, Default)] #[repr(transparent)] pub struct $name { pub value: $t, @@ -14,10 +14,10 @@ macro_rules! primitive { pub const ID: u8 = $id; #[inline] - pub fn to_bytes(&self, writer: &mut UncheckedBufWriter) { writer.write(self.value.to_be_bytes().as_ref()); } + pub fn to_be_bytes(&self, writer: &mut UncheckedBufWriter) { writer.write(self.value.to_be_bytes().as_ref()); } #[inline] - pub fn from_bytes(decoder: &mut Decoder) -> Option { + pub fn from_be_bytes(decoder: &mut BigEndianDecoder) -> Option { unsafe { decoder.assert_len(core::mem::size_of::<$t>())?; Some(Self { @@ -26,6 +26,19 @@ macro_rules! primitive { } } + #[inline] + pub fn to_le_bytes(&self, writer: &mut UncheckedBufWriter) { writer.write(self.value.to_le_bytes().as_ref()); } + + #[inline] + pub fn from_le_bytes(decoder: &mut LittleEndianDecoder) -> Option { + unsafe { + decoder.assert_len(core::mem::size_of::<$t>())?; + Some(Self { + value: <$t>::from_le_bytes(decoder.read_bytes::<{ core::mem::size_of::<$t>() }>()), + }) + } + } + #[inline] pub fn render(&self, builder: &mut VertexBufferBuilder, name: Option<&str>, ctx: &mut RenderContext) { ctx.line_number(); @@ -67,5 +80,9 @@ macro_rules! primitive { impl $name { pub fn pretty_fmt(&self, f: &mut PrettyFormatter) { f.write_str(&self.to_string()) } } + + impl $name { + pub fn matches(&self, other: &Self) -> bool { self.value == other.value } + } }; } diff --git a/src/elements/string.rs b/src/elements/string.rs index 672fc4e..4fd90cd 100644 --- a/src/elements/string.rs +++ b/src/elements/string.rs @@ -7,11 +7,12 @@ use std::ptr::NonNull; use compact_str::CompactString; use crate::assets::{BASE_Z, JUST_OVERLAPPING_BASE_TEXT_Z, STRING_UV, ZOffset}; -use crate::decoder::Decoder; +use crate::be_decoder::BigEndianDecoder; use crate::encoder::UncheckedBufWriter; use crate::{RenderContext, StrExt, VertexBufferBuilder}; use crate::color::TextColor; use crate::formatter::PrettyFormatter; +use crate::le_decoder::LittleEndianDecoder; #[repr(transparent)] #[allow(clippy::module_name_repetitions)] @@ -20,8 +21,8 @@ pub struct NbtString { pub str: TwentyThree, } -impl PartialEq for NbtString { - fn eq(&self, other: &Self) -> bool { +impl NbtString { + pub fn matches(&self, other: &Self) -> bool { self.str.as_str() == other.str.as_str() } } @@ -38,7 +39,8 @@ impl NbtString { )) } - pub fn from_bytes(decoder: &mut Decoder) -> Option { + #[inline] + pub fn from_be_bytes(decoder: &mut BigEndianDecoder) -> Option { unsafe { decoder.assert_len(2)?; Some(Self { @@ -46,7 +48,22 @@ impl NbtString { }) } } - pub fn to_bytes(&self, writer: &mut UncheckedBufWriter) { writer.write_str(self.str.as_str()); } + + #[inline] + pub fn to_be_bytes(&self, writer: &mut UncheckedBufWriter) { writer.write_be_str(self.str.as_str()); } + + #[inline] + pub fn from_le_bytes(decoder: &mut LittleEndianDecoder) -> Option { + unsafe { + decoder.assert_len(2)?; + Some(Self { + str: TwentyThree::new(decoder.string()?), + }) + } + } + + #[inline] + pub fn to_le_bytes(&self, writer: &mut UncheckedBufWriter) { writer.write_le_str(self.str.as_str()); } } impl NbtString { diff --git a/src/encoder.rs b/src/encoder.rs index 9e478c7..4693b08 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -52,11 +52,16 @@ impl UncheckedBufWriter { } } - pub fn write_str(&mut self, str: &str) { + pub fn write_be_str(&mut self, str: &str) { self.write(&(str.len() as u16).to_be_bytes()); self.write(str.as_bytes()); } + pub fn write_le_str(&mut self, str: &str) { + self.write(&(str.len() as u16).to_le_bytes()); + self.write(str.as_bytes()); + } + #[cold] #[inline(never)] unsafe fn write_pushing_cold(&mut self, bytes: &[u8]) { diff --git a/src/le_decoder.rs b/src/le_decoder.rs new file mode 100644 index 0000000..9fe5797 --- /dev/null +++ b/src/le_decoder.rs @@ -0,0 +1,127 @@ +use std::intrinsics::likely; +use std::marker::PhantomData; + +use compact_str::CompactString; +use crate::elements::compound::CompoundMap; +use crate::SortAlgorithm; + +pub struct LittleEndianDecoder<'a> { + pub data: *const u8, + end: *const u8, + sort: SortAlgorithm, + _marker: PhantomData<&'a ()>, + header: bool, +} + +#[allow(improper_ctypes_definitions)] +impl<'a> LittleEndianDecoder<'a> { + #[inline] + #[optimize(speed)] + pub fn new(data: &'a [u8], sort: SortAlgorithm) -> Self { + let mut this = Self { + end: unsafe { data.as_ptr().add(data.len()) }, + data: data.as_ptr(), + sort, + _marker: PhantomData, + header: false, + }; + unsafe { + if this.assert_len(8).is_some() && this.data.add(4).cast::().read_unaligned() as usize == this.remaining_len() - 8 { + // todo, what the hell is this version for + let _version = this.u32(); + let _remaining_length = this.u32() as usize; + this.header = true; + } + } + this + } + + #[inline] + pub fn sort(&self, map: &mut CompoundMap) { + self.sort.sort(map) + } + + #[inline] + #[must_use] + pub fn header(&self) -> bool { + self.header + } + + #[optimize(speed)] + #[must_use] + pub fn assert_len(&self, remaining_len: usize) -> Option<()> { + // <= end because it will read *until* that byte + if unsafe { likely((self.data.add(remaining_len) as usize) <= self.end as usize) } { + Some(()) + } else { + None + } + } + + #[optimize(speed)] + #[must_use] + pub fn assert_exact_len(&self, remaining_len: usize) -> Option<()> { + // <= end because it will read *until* that byte + if unsafe { likely((self.data.add(remaining_len) as usize) == self.end as usize) } { + Some(()) + } else { + None + } + } + + #[inline] + #[must_use] + pub fn remaining_len(&self) -> usize { + self.end as usize - self.data as usize + } + + #[optimize(speed)] + pub unsafe fn read_bytes(&mut self) -> [u8; N] { + let array = self.data.cast::<[u8; N]>().read(); + self.data = self.data.add(N); + array + } + + #[optimize(speed)] + pub unsafe fn u8(&mut self) -> u8 { u8::from_le_bytes(self.read_bytes()) } + + #[optimize(speed)] + pub unsafe fn u16(&mut self) -> u16 { u16::from_le_bytes(self.read_bytes()) } + + #[optimize(speed)] + pub unsafe fn u32(&mut self) -> u32 { u32::from_le_bytes(self.read_bytes()) } + + #[optimize(speed)] + pub unsafe fn u64(&mut self) -> u64 { u64::from_le_bytes(self.read_bytes()) } + + #[optimize(speed)] + pub unsafe fn i8(&mut self) -> i8 { i8::from_le_bytes(self.read_bytes()) } + + #[optimize(speed)] + pub unsafe fn i16(&mut self) -> i16 { i16::from_le_bytes(self.read_bytes()) } + + #[optimize(speed)] + pub unsafe fn i32(&mut self) -> i32 { i32::from_le_bytes(self.read_bytes()) } + + #[optimize(speed)] + pub unsafe fn i64(&mut self) -> i64 { i64::from_le_bytes(self.read_bytes()) } + + #[optimize(speed)] + pub unsafe fn f32(&mut self) -> f32 { f32::from_le_bytes(self.read_bytes()) } + + #[optimize(speed)] + pub unsafe fn f64(&mut self) -> f64 { f64::from_le_bytes(self.read_bytes()) } + + #[optimize(speed)] + pub unsafe fn skip(&mut self, amount: usize) { self.data = self.data.add(amount); } + + #[optimize(speed)] + pub unsafe fn string(&mut self) -> Option { + let len = self.u16() as usize; + self.assert_len(len)?; + + let out = CompactString::from_utf8_lossy(core::slice::from_raw_parts(self.data, len)); + self.data = self.data.add(len); + Some(out) + } +} diff --git a/src/main.rs b/src/main.rs index 160ce7a..a5eb185 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,8 @@ )] #![windows_subsystem = "windows"] +extern crate core; + use std::cell::UnsafeCell; use std::cmp::Ordering; use std::convert::identity; @@ -55,7 +57,7 @@ use crate::workbench::Workbench; mod alert; mod assets; mod color; -mod decoder; +mod be_decoder; mod encoder; mod selected_text; mod shader; @@ -64,7 +66,7 @@ mod text_shader; mod tree_travel; mod vertex_buffer_builder; mod window; -pub mod workbench; +mod workbench; mod workbench_action; mod element_action; mod search_box; @@ -73,6 +75,9 @@ mod text; mod cli; mod formatter; mod bookmark; +mod tooltip_effect_shader; +mod copy_shader; +mod le_decoder; #[macro_export] macro_rules! flags { @@ -130,9 +135,9 @@ macro_rules! tab_mut { #[cfg(not(target_arch = "wasm32"))] #[macro_export] macro_rules! error { - ($($arg:tt)*) => { + ($($arg:tt)*) => {{ eprintln!($($arg)*); - }; + }}; } #[cfg(target_arch = "wasm32")] @@ -146,9 +151,9 @@ macro_rules! error { #[cfg(not(target_arch = "wasm32"))] #[macro_export] macro_rules! log { - ($($arg:tt)*) => { + ($($arg:tt)*) => {{ println!($($arg)*); - }; + }}; } #[cfg(target_arch = "wasm32")] @@ -194,6 +199,13 @@ pub fn open_file(name: String, bytes: Vec) { } } +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +pub fn close() -> usize { + let workbench = unsafe { WORKBENCH.get_mut() }; + workbench.close() +} + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))] #[cfg(target_arch = "wasm32")] pub fn wasm_main() { @@ -205,6 +217,17 @@ pub fn wasm_main() { }); } +/// # Refactor +/// * render trees using `RenderLine` struct/enum +/// * rendering code is duplicated af +/// # Long Term Goals +/// * smart screen +/// * [chunk](elements::chunk::NbtChunk) section rendering +/// # Minor Features +/// * [`last_modified`](elements::chunk::NbtChunk) field actually gets some impl +/// # Major Features +/// * macros +/// * keyboard-based element dropping (like vim stuff) #[cfg(not(target_arch = "wasm32"))] pub fn main() -> ! { #[cfg(target_os = "windows")] unsafe { @@ -243,22 +266,6 @@ Options: } } -/// # Refactor -/// * render trees using `RenderLine` struct/enum -/// # Long Term Goals -/// * smart screen -/// * wiki page for docs on minecraft's format of stuff -/// * [chunk](elements::chunk::NbtChunk) section rendering -/// # Minor Features -/// * gear icon to swap toolbar with settings panel -/// * [`last_modified`](elements::chunk::NbtChunk) field actually gets some impl -/// * auto save -/// * blur behind tooltip -/// # Major Features -/// * macros -/// * keyboard-based element dropping (press numbers before to specify count for move operations, right shift to enable mode) -/// * animations!!!! - pub enum DropFn { Dropped(usize, usize, Option, usize), Missed(Option, NbtElement), @@ -282,6 +289,7 @@ pub enum HeldEntry { impl HeldEntry { #[must_use] + #[inline] pub const fn element(&self) -> Option<&NbtElement> { match self { Self::Empty => None, @@ -290,7 +298,13 @@ impl HeldEntry { } #[must_use] + #[inline] pub const fn is_empty(&self) -> bool { matches!(self, Self::Empty) } + + #[inline] + pub fn take(&mut self) -> Self { + core::mem::replace(self, HeldEntry::Empty) + } } #[must_use] @@ -355,6 +369,22 @@ pub fn since_epoch() -> Duration { Duration::from_nanos((web_sys::js_sys::Date::now() * 1_000_000.0) as u64) } +pub fn nth(n: usize) -> String { + let mut buf = String::with_capacity(n.checked_ilog10().map_or(1, |x| x + 1) as usize + 2); + let _ = write!(&mut buf, "{n}"); + if n / 10 % 10 == 1 { + buf.push_str("th"); + } else { + match n % 10 { + 1 => buf.push_str("st"), + 2 => buf.push_str("nd"), + 3 => buf.push_str("rd"), + _ => buf.push_str("th"), + } + } + buf +} + pub fn sum_indices>(indices: I, mut root: &NbtElement) -> usize { unsafe { let mut total = 0; diff --git a/src/search_box.rs b/src/search_box.rs index b420bb8..a63eee0 100644 --- a/src/search_box.rs +++ b/src/search_box.rs @@ -90,7 +90,7 @@ impl SearchPredicate { } SearchPredicateInner::Snbt(k, element) => { // cmp order does matter - let a = element == value; + let a = element.matches(value); let b = k.as_deref() == key; ((self.search_flags == 0b11) & a & b) | ((self.search_flags == 0b01) & a) | ((self.search_flags == 0b10) & b) }, diff --git a/src/tab.rs b/src/tab.rs index 0ec3a4c..ff32195 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -10,8 +10,8 @@ use flate2::Compression; use uuid::Uuid; use zune_inflate::DeflateDecoder; -use crate::{DOUBLE_CLICK_INTERVAL, LinkedQueue, OptionExt, panic_unchecked, RenderContext, since_epoch, SortAlgorithm, StrExt, WindowProperties}; -use crate::assets::{BASE_Z, BYTE_ARRAY_GHOST_UV, BYTE_ARRAY_UV, BYTE_GRAYSCALE_UV, BYTE_UV, CHUNK_GHOST_UV, CHUNK_UV, COMPOUND_GHOST_UV, COMPOUND_ROOT_UV, COMPOUND_UV, DISABLED_REFRESH_UV, DOUBLE_GRAYSCALE_UV, DOUBLE_UV, ENABLED_FREEHAND_MODE_UV, FLOAT_GRAYSCALE_UV, FLOAT_UV, FREEHAND_MODE_UV, GZIP_FILE_TYPE_UV, HEADER_SIZE, HELD_SCROLLBAR_UV, HOVERED_WIDGET_UV, INT_ARRAY_GHOST_UV, INT_ARRAY_UV, INT_GRAYSCALE_UV, INT_UV, JUST_OVERLAPPING_BASE_Z, LINE_NUMBER_SEPARATOR_UV, LIST_GHOST_UV, LIST_UV, LONG_ARRAY_GHOST_UV, LONG_ARRAY_UV, LONG_GRAYSCALE_UV, LONG_UV, MCA_FILE_TYPE_UV, NBT_FILE_TYPE_UV, REDO_UV, REFRESH_UV, REGION_UV, SCROLLBAR_Z, SHORT_GRAYSCALE_UV, SHORT_UV, SNBT_FILE_TYPE_UV, STEAL_ANIMATION_OVERLAY_UV, STRING_GHOST_UV, STRING_UV, UNDO_UV, UNHELD_SCROLLBAR_UV, UNKNOWN_NBT_GHOST_UV, UNKNOWN_NBT_UV, UNSELECTED_WIDGET_UV, ZLIB_FILE_TYPE_UV, ZOffset}; +use crate::{LinkedQueue, OptionExt, panic_unchecked, RenderContext, since_epoch, SortAlgorithm, StrExt, WindowProperties}; +use crate::assets::{BASE_Z, BYTE_ARRAY_GHOST_UV, BYTE_ARRAY_UV, BYTE_GRAYSCALE_UV, BYTE_UV, CHUNK_GHOST_UV, CHUNK_UV, COMPOUND_GHOST_UV, COMPOUND_ROOT_UV, COMPOUND_UV, DISABLED_REFRESH_UV, DOUBLE_GRAYSCALE_UV, DOUBLE_UV, ENABLED_FREEHAND_MODE_UV, FLOAT_GRAYSCALE_UV, FLOAT_UV, FREEHAND_MODE_UV, GZIP_FILE_TYPE_UV, HEADER_SIZE, HELD_SCROLLBAR_UV, HOVERED_WIDGET_UV, INT_ARRAY_GHOST_UV, INT_ARRAY_UV, INT_GRAYSCALE_UV, INT_UV, JUST_OVERLAPPING_BASE_Z, LITTLE_ENDIAN_NBT_FILE_TYPE_UV, LINE_NUMBER_SEPARATOR_UV, LIST_GHOST_UV, LIST_UV, LONG_ARRAY_GHOST_UV, LONG_ARRAY_UV, LONG_GRAYSCALE_UV, LONG_UV, MCA_FILE_TYPE_UV, NBT_FILE_TYPE_UV, REDO_UV, REFRESH_UV, REGION_UV, SCROLLBAR_Z, SHORT_GRAYSCALE_UV, SHORT_UV, SNBT_FILE_TYPE_UV, STEAL_ANIMATION_OVERLAY_UV, STRING_GHOST_UV, STRING_UV, UNDO_UV, UNHELD_SCROLLBAR_UV, UNKNOWN_NBT_GHOST_UV, UNKNOWN_NBT_UV, UNSELECTED_WIDGET_UV, ZLIB_FILE_TYPE_UV, ZOffset, LITTLE_ENDIAN_HEADER_NBT_FILE_TYPE_UV}; use crate::color::TextColor; use crate::elements::chunk::NbtRegion; use crate::elements::compound::NbtCompound; @@ -19,6 +19,7 @@ use crate::elements::element::NbtElement; use crate::selected_text::{SelectedText, SelectedTextAdditional}; use crate::text::Text; use crate::bookmark::Bookmarks; +use crate::elements::list::NbtList; use crate::tree_travel::Navigate; use crate::vertex_buffer_builder::{Vec2u, VertexBufferBuilder}; use crate::workbench_action::WorkbenchAction; @@ -27,7 +28,7 @@ pub struct Tab { pub value: Box, pub name: Box, pub path: Option, - pub compression: FileFormat, + pub format: FileFormat, pub undos: LinkedQueue, pub redos: LinkedQueue, pub unsaved_changes: bool, @@ -41,6 +42,7 @@ pub struct Tab { pub selected_text: Option, pub last_close_attempt: Duration, pub last_selected_text_interaction: (usize, usize, Duration), + pub last_interaction: Duration, } impl Tab { @@ -49,16 +51,20 @@ impl Tab { ("SNBT File", &["snbt"]), ("Region File", &["mca", "mcr"]), ("Compressed NBT File", &["dat", "dat_old", "dat_new", "dat_mcr", "old", "schem", "schematic", "litematic"]), + ("Little Endian NBT File", &["nbt", "mcstructure"]), + ("Little Endian NBT File (With Header)", &["dat"]), ]; + pub const AUTOSAVE_INTERVAL: Duration = Duration::from_secs(30); + pub const AUTOSAVE_MAXIMUM_LINES: usize = 1_000_000; - pub fn new(nbt: NbtElement, path: &Path, compression: FileFormat, window_height: usize, window_width: usize) -> Result { - if !(nbt.id() == NbtCompound::ID || nbt.id() == NbtRegion::ID) { return Err(anyhow!("Parsed NBT was not a Compound or Region")) } + pub fn new(nbt: NbtElement, path: &Path, format: FileFormat, window_height: usize, window_width: usize) -> Result { + if !(nbt.id() == NbtCompound::ID || nbt.id() == NbtRegion::ID || nbt.id() == NbtList::ID) { return Err(anyhow!("Parsed NBT was not a Compound, Region, or List")) } Ok(Self { value: Box::new(nbt), name: path.file_name().map(OsStr::to_string_lossy).context("Could not obtain path filename")?.into(), path: Some(path).filter(|path| path.is_absolute()).map(|path| path.to_path_buf()), - compression, + format, undos: LinkedQueue::new(), redos: LinkedQueue::new(), unsaved_changes: false, @@ -71,40 +77,43 @@ impl Tab { freehand_mode: false, selected_text: None, last_close_attempt: Duration::ZERO, - last_selected_text_interaction: (0, 0, Duration::ZERO) + last_selected_text_interaction: (0, 0, Duration::ZERO), + last_interaction: since_epoch(), }) } #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] pub fn save(&mut self, force_dialog: bool) -> Result<()> { - let path = self.path.as_deref().unwrap_or(self.name.as_ref().as_ref()); - if !path.is_absolute() || force_dialog { + if let Some(path) = self.path.as_deref() && path.is_absolute() && !force_dialog { + std::fs::write(path, self.format.encode(&self.value))?; + self.unsaved_changes = false; + Ok(()) + } else { let mut builder = native_dialog::FileDialog::new(); - let initial_index = match self.compression { + let initial_index = match self.format { FileFormat::Nbt => 0, FileFormat::Snbt => 1, FileFormat::Lz4 | FileFormat::Mca => 2, FileFormat::Gzip | FileFormat::Zlib => 3, + FileFormat::LittleEndianNbt => 4, + FileFormat::LittleEndianHeaderNbt => 5, }; builder = builder.add_filter(Self::FILE_TYPE_FILTERS[initial_index].0, Self::FILE_TYPE_FILTERS[initial_index].1); builder = Self::FILE_TYPE_FILTERS.iter().enumerate().filter_map(|(idx, value)| if idx == initial_index { None } else { Some(value) }).fold(builder, |builder, filter| builder.add_filter(filter.0, filter.1)); let path = builder.show_save_single_file()?.ok_or_else(|| anyhow!("Save cancelled"))?; self.name = path.file_name().and_then(|x| x.to_str()).expect("Path has a filename").to_string().into_boxed_str(); - std::fs::write(&path, self.compression.encode(&self.value))?; + std::fs::write(&path, self.format.encode(&self.value))?; self.path = Some(path); self.unsaved_changes = false; Ok(()) - } else { - std::fs::write(path, self.compression.encode(&self.value))?; - self.unsaved_changes = false; - Ok(()) } } #[cfg(target_arch = "wasm32")] pub fn save(&mut self, _: bool) -> Result<()> { - let bytes = self.compression.encode(&self.value); + let bytes = self.format.encode(&self.value); crate::save(self.name.as_ref(), bytes); + self.unsaved_changes = false; Ok(()) } @@ -121,6 +130,8 @@ impl Tab { compound.render_root(builder, &self.name, ctx); } else if let Some(region) = self.value.as_region() { region.render_root(builder, &self.name, ctx); + } else if let Some(list) = self.value.as_list() { + list.render_root(builder, &self.name, ctx); } builder.color = TextColor::White.to_raw(); ctx.render_line_numbers(builder, &self.bookmarks); @@ -240,7 +251,7 @@ impl Tab { } { - let enabled = self.path.is_some() && cfg!(not(target_os = "wasm32")); + let enabled = self.path.as_deref().is_some_and(|path| path.exists()) && cfg!(not(target_os = "wasm32")); let widget_uv = if (296..312).contains(&ctx.mouse_x) && (26..42).contains(&ctx.mouse_y) { #[cfg(target_arch = "wasm32")] builder.draw_tooltip(&["Refresh Tab (Disabled on WebAssembly version)"], (ctx.mouse_x, ctx.mouse_y), false); @@ -326,6 +337,8 @@ impl Tab { builder.draw_texture_z(pos, z, COMPOUND_ROOT_UV, (16, 16)); } else if id == NbtRegion::ID { builder.draw_texture_z(pos, z, REGION_UV, (16, 16)); + } else if id == NbtList::ID { + builder.draw_texture_z(pos, z, LIST_UV, (16, 16)); } } @@ -589,12 +602,12 @@ impl Tab { pub fn parse_raw(path: &Path, buf: Vec, sort_algorithm: SortAlgorithm) -> Result<(NbtElement, FileFormat)> { Ok(if let Some("mca" | "mcr") = path.extension().and_then(OsStr::to_str) { ( - NbtElement::from_mca(buf.as_slice(), sort_algorithm).context("Failed to parse MCA file")?, + NbtElement::from_be_mca(buf.as_slice(), sort_algorithm).context("Failed to parse MCA file")?, FileFormat::Mca, ) } else if let Some(0x1F8B) = buf.first_chunk::<2>().copied().map(u16::from_be_bytes) { ( - NbtElement::from_file( + NbtElement::from_be_file( &DeflateDecoder::new(buf.as_slice()) .decode_gzip() .context("Failed to decode gzip compressed NBT")?, @@ -605,7 +618,7 @@ impl Tab { ) } else if let Some(0x7801 | 0x789C | 0x78DA) = buf.first_chunk::<2>().copied().map(u16::from_be_bytes) { ( - NbtElement::from_file( + NbtElement::from_be_file( &DeflateDecoder::new(buf.as_slice()) .decode_zlib() .context("Failed to decode zlib compressed NBT")?, @@ -614,8 +627,10 @@ impl Tab { .context("Failed to parse NBT")?, FileFormat::Zlib, ) - } else if let Some(nbt) = NbtElement::from_file(buf.as_slice(), sort_algorithm) { + } else if let Some(nbt) = NbtElement::from_be_file(buf.as_slice(), sort_algorithm) { (nbt, FileFormat::Nbt) + } else if let Some((nbt, header)) = NbtElement::from_le_file(buf.as_slice(), sort_algorithm) { + (nbt, if header { FileFormat::LittleEndianHeaderNbt } else { FileFormat::LittleEndianNbt }) } else { ( core::str::from_utf8(&buf) @@ -637,7 +652,7 @@ impl Tab { pub fn refresh(&mut self, sort_algorithm: SortAlgorithm) -> Result<()> { let Some(path) = self.path.as_deref() else { return Err(anyhow!("File path was not present in tab")) }; - if self.unsaved_changes && (since_epoch() - core::mem::replace(&mut self.last_close_attempt, since_epoch())) > DOUBLE_CLICK_INTERVAL { + if self.unsaved_changes && (since_epoch() - core::mem::replace(&mut self.last_close_attempt, since_epoch())) > crate::DOUBLE_CLICK_INTERVAL { return Ok(()); } @@ -646,14 +661,12 @@ impl Tab { self.bookmarks.clear(); self.scroll = 0; - self.compression = format; + self.format = format; self.unsaved_changes = false; - self.undos.clear(); - self.redos.clear(); self.uuid = Uuid::new_v4(); self.selected_text = None; self.last_close_attempt = Duration::ZERO; - let old = core::mem::replace(&mut self.value, Box::new(value)); + let old = (core::mem::replace(&mut self.value, Box::new(value)), core::mem::replace(&mut self.undos, LinkedQueue::new()), core::mem::replace(&mut self.redos, LinkedQueue::new())); std::thread::Builder::new().stack_size(50_331_648 /*48MiB*/).spawn(move || drop(old)).expect("Failed to spawn thread"); Ok(()) @@ -671,8 +684,12 @@ pub enum FileFormat { Nbt, Gzip, Zlib, - Lz4, Snbt, + LittleEndianNbt, + LittleEndianHeaderNbt, + + Lz4, + Mca, } @@ -682,7 +699,9 @@ impl FileFormat { match self { Self::Nbt => Self::Gzip, Self::Gzip => Self::Zlib, - Self::Zlib => Self::Snbt, + Self::Zlib => Self::LittleEndianNbt, + Self::LittleEndianNbt => Self::LittleEndianHeaderNbt, + Self::LittleEndianHeaderNbt => Self::Snbt, Self::Snbt => Self::Nbt, // has to be separate @@ -697,7 +716,9 @@ impl FileFormat { Self::Nbt => Self::Snbt, Self::Gzip => Self::Nbt, Self::Zlib => Self::Gzip, - Self::Snbt => Self::Zlib, + Self::LittleEndianNbt => Self::Zlib, + Self::LittleEndianHeaderNbt => Self::LittleEndianNbt, + Self::Snbt => Self::LittleEndianHeaderNbt, // has to be separate Self::Mca => Self::Mca, @@ -708,21 +729,20 @@ impl FileFormat { #[must_use] pub fn encode(self, data: &NbtElement) -> Vec { match self { - Self::Nbt | Self::Mca => data.to_file(), + Self::Nbt | Self::Mca => data.to_be_file(), Self::Gzip => { let mut vec = vec![]; - let _ = flate2::read::GzEncoder::new(&*data.to_file(), Compression::best()).read_to_end(&mut vec); + let _ = flate2::read::GzEncoder::new(&*data.to_be_file(), Compression::best()).read_to_end(&mut vec); vec } Self::Zlib => { let mut vec = vec![]; - let _ = flate2::read::ZlibEncoder::new(&*data.to_file(), Compression::best()).read_to_end(&mut vec); + let _ = flate2::read::ZlibEncoder::new(&*data.to_be_file(), Compression::best()).read_to_end(&mut vec); vec } - Self::Lz4 => { - lz4_flex::compress(&*data.to_file()) - } + Self::Lz4 => lz4_flex::compress(&*data.to_be_file()), Self::Snbt => data.to_string().into_bytes(), + format @ (Self::LittleEndianNbt | Self::LittleEndianHeaderNbt) => data.to_le_file(format == Self::LittleEndianHeaderNbt), } } @@ -734,6 +754,8 @@ impl FileFormat { Self::Zlib => ZLIB_FILE_TYPE_UV, Self::Snbt => SNBT_FILE_TYPE_UV, Self::Mca => MCA_FILE_TYPE_UV, + Self::LittleEndianNbt => LITTLE_ENDIAN_NBT_FILE_TYPE_UV, + Self::LittleEndianHeaderNbt => LITTLE_ENDIAN_HEADER_NBT_FILE_TYPE_UV, Self::Lz4 => Vec2u::new(240, 240), } } @@ -747,6 +769,8 @@ impl FileFormat { Self::Snbt => "SNBT", Self::Mca => "MCA", Self::Lz4 => "LZ4", + Self::LittleEndianNbt => "Little Endian NBT", + Self::LittleEndianHeaderNbt => "Little Endian NBT (With Header)", } } } diff --git a/src/tooltip_effect_shader.rs b/src/tooltip_effect_shader.rs new file mode 100644 index 0000000..f4c0779 --- /dev/null +++ b/src/tooltip_effect_shader.rs @@ -0,0 +1,39 @@ +use wgsl_inline::wgsl; + +wgsl! { + struct VertexInput { + @location(0) + position: vec2, + } + + struct VertexOutput { + @builtin(position) + position: vec4, + @location(0) + uv: vec2, + } + + @vertex + fn vertex(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.position = vec4(input.position, 0.0, 1.0); + output.uv = vec2((input.position.x + 1.0) / 2.0, (1.0 - input.position.y) / 2.0); + return output; + } + + @group(0) + @binding(0) + var texture: texture_2d; + + @group(0) + @binding(1) + var texture_sampler: sampler; + + @fragment + fn fragment(input: VertexOutput) -> @location(0) vec4 { + var sample = textureSample(texture, texture_sampler, input.uv); + var gray = (sample.x + sample.y + sample.z) / 3.0; + var result = vec4(gray, gray, gray, sample.w); + return result; + } +} diff --git a/src/vertex_buffer_builder.rs b/src/vertex_buffer_builder.rs index 396f254..fbd0f58 100644 --- a/src/vertex_buffer_builder.rs +++ b/src/vertex_buffer_builder.rs @@ -26,7 +26,7 @@ pub struct VertexBufferBuilder { pub color: u32, two_over_width: f32, negative_two_over_height: f32, - drew_tooltip: bool, + owned_tooltip: Option<(Box<[String]>, Vec2u, bool)>, scale: f32, } @@ -75,7 +75,7 @@ impl VertexBufferBuilder { color: TextColor::White.to_raw(), two_over_width: 2.0 / size.width as f32, negative_two_over_height: -2.0 / size.height as f32, - drew_tooltip: false, + owned_tooltip: None, scale: scale as f32, } } @@ -84,7 +84,7 @@ impl VertexBufferBuilder { pub const fn scroll(&self) -> usize { self.scroll } #[inline] - pub const fn drew_tooltip(&self) -> bool { self.drew_tooltip } + pub const fn drew_tooltip(&self) -> bool { self.owned_tooltip.is_some() } #[inline] pub fn settings(&mut self, pos: impl Into<(usize, usize)>, dropshadow: bool, z: ZOffset) { @@ -124,9 +124,23 @@ impl VertexBufferBuilder { #[inline] pub fn draw_tooltip(&mut self, text: &[&str], pos: impl Into<(usize, usize)>, force_draw_right: bool) { + self.owned_tooltip.get_or_insert_with(move || (text.iter().map(|s| s.to_string()).collect::>().into_boxed_slice(), Vec2u::from(pos.into()), force_draw_right)); + } + + pub fn clear_buffers(&mut self) { + self.vertices.clear(); + self.indices.clear(); + self.text_vertices.clear(); + self.text_indices.clear(); + self.text_vertices_len = 0; + self.vertices_len = 0; + } + + #[inline] + pub fn draw_tooltip0(&mut self) -> Option<[f32; 12]> { use core::fmt::Write; - if self.drew_tooltip { return } + let (text, pos, force_draw_right) = if let Some(tooltip) = self.owned_tooltip.take() { tooltip } else { return None }; let (mut x, y) = pos.into(); let y = y + 16; @@ -134,11 +148,11 @@ impl VertexBufferBuilder { if x >= self.window_width() / 2 && !force_draw_right { x = x.saturating_sub(text_width + 3); } - let old_text_z = core::mem::replace(&mut self.text_z, TOOLTIP_Z); - let old_text_coords = core::mem::replace(&mut self.text_coords, (x + 3, y + 3)); + self.text_z = TOOLTIP_Z; + self.text_coords = (x + 3, y + 3); self.draw_texture_z((x, y), TOOLTIP_Z, TOOLTIP_UV, (3, 3)); let mut max = x + 3; - for &line in text { + for line in text.iter() { let _ = write!(self, "{line}"); max = max.max(self.text_coords.0); self.text_coords.0 = x + 3; @@ -169,7 +183,6 @@ impl VertexBufferBuilder { TOOLTIP_UV + (13, 13), (3, 3), ); - self.draw_texture_region_z( (x, y + 3), TOOLTIP_Z, @@ -193,9 +206,36 @@ impl VertexBufferBuilder { (10, 10), ); - self.text_z = old_text_z; - self.text_coords = old_text_coords; - self.drew_tooltip = true; + let x0 = (x as f32).mul_add(self.two_over_width, -1.0); + let x1 = self.two_over_width.mul_add((width + 6) as f32 * self.scale, x0); + let y1 = (y as f32).mul_add(self.negative_two_over_height, 1.0); + let y0 = self.negative_two_over_height.mul_add((height + 6) as f32 * self.scale, y1); + + Some([ + // 0 + x1, + y1, + + // 1 + x0, + y1, + + // 2 + x0, + y0, + + // 0 + x1, + y1, + + // 2 + x0, + y0, + + // 3 + x1, + y0, + ]) } #[inline] diff --git a/src/window.rs b/src/window.rs index 62a07ed..c8b1ecd 100644 --- a/src/window.rs +++ b/src/window.rs @@ -92,7 +92,7 @@ pub async fn run() -> ! { Err(SurfaceError::Timeout) => { error!("Frame took too long to process") }, } } - WindowEvent::CloseRequested => std::process::exit(0), + WindowEvent::CloseRequested => if workbench.close() == 0 { std::process::exit(0) }, WindowEvent::Resized(new_size) => state.resize(workbench, new_size), _ => {} } @@ -125,7 +125,11 @@ pub struct State<'window> { diffuse_bind_group: BindGroup, text_render_pipeline: RenderPipeline, unicode_bind_group: BindGroup, + tooltip_effect_render_pipeline: RenderPipeline, + texture_reference_bind_group_layout: BindGroupLayout, + sampler: Sampler, last_tick: Duration, + copy_render_pipeline: RenderPipeline, } impl<'window> State<'window> { @@ -203,7 +207,7 @@ impl<'window> State<'window> { origin: Origin3d::ZERO, aspect: TextureAspect::All, }, - assets::ATLAS, + assets::atlas(), ImageDataLayout { offset: 0, bytes_per_row: Some(4 * assets::ATLAS_WIDTH as u32), @@ -324,12 +328,7 @@ impl<'window> State<'window> { usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, view_formats: &[], }); - queue.write_texture(ImageCopyTexture { - texture: &unicode_texture, - mip_level: 0, - origin: Origin3d::ZERO, - aspect: TextureAspect::All, - }, &unsafe { + queue.write_texture(unicode_texture.as_image_copy(), &unsafe { zune_inflate::DeflateDecoder::new_with_options( include_bytes!("assets/unicode.hex.zib"), DeflateOptions::default().set_confirm_checksum(false), @@ -369,14 +368,14 @@ impl<'window> State<'window> { label: Some("Text Shader"), source: ShaderSource::Wgsl(Cow::Borrowed(crate::text_shader::SOURCE)), }); - let text_render_pipeline_payout = device.create_pipeline_layout(&PipelineLayoutDescriptor { + let text_render_pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { label: Some("Text Render Pipeline Layout"), bind_group_layouts: &[&unicode_bind_group_layout], push_constant_ranges: &[], }); let text_render_pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { label: Some("Text Render Pipeline"), - layout: Some(&text_render_pipeline_payout), + layout: Some(&text_render_pipeline_layout), vertex: VertexState { module: &text_shader, entry_point: "vertex", @@ -418,6 +417,139 @@ impl<'window> State<'window> { }, multiview: None, }); + let texture_reference_bind_group_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("Tooltip Effect Bind Group Layout"), + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { + filterable: true, + }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + } + ], + }); + let tooltip_effect_shader = device.create_shader_module(ShaderModuleDescriptor { + label: Some("Tooltip Effect Shader"), + source: ShaderSource::Wgsl(Cow::Borrowed(crate::tooltip_effect_shader::SOURCE)) + }); + let tooltip_effect_render_pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { + label: Some("Tooltip Effect Render Pipeline Layout"), + bind_group_layouts: &[&texture_reference_bind_group_layout], + push_constant_ranges: &[], + }); + let tooltip_effect_render_pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { + label: Some("Tooltip Effect Render Pipeline"), + layout: Some(&tooltip_effect_render_pipeline_layout), + vertex: VertexState { + module: &tooltip_effect_shader, + entry_point: "vertex", + buffers: &[VertexBufferLayout { + array_stride: 8, + step_mode: VertexStepMode::Vertex, + attributes: &vertex_attr_array![0 => Float32x2], + }], + }, + fragment: Some(FragmentState { + module: &tooltip_effect_shader, + entry_point: "fragment", + targets: &[Some(ColorTargetState { + format: config.format, + blend: Some(BlendState::REPLACE), + write_mask: ColorWrites::ALL, + })], + }), + primitive: PrimitiveState { + topology: PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: FrontFace::Ccw, + cull_mode: Some(Face::Back), + polygon_mode: PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + }); + let copy_render_pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { + label: Some("Copy Render Pipeline Layout"), + bind_group_layouts: &[&texture_reference_bind_group_layout], + push_constant_ranges: &[], + }); + let copy_shader = device.create_shader_module(ShaderModuleDescriptor { + label: Some("Copy Shader"), + source: ShaderSource::Wgsl(Cow::Borrowed(crate::copy_shader::SOURCE)) + }); + let copy_render_pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { + label: Some("Copy Render Pipeline"), + layout: Some(©_render_pipeline_layout), + vertex: VertexState { + module: ©_shader, + entry_point: "vertex", + buffers: &[VertexBufferLayout { + array_stride: 8, + step_mode: VertexStepMode::Vertex, + attributes: &vertex_attr_array![0 => Float32x2], + }], + }, + primitive: PrimitiveState { + topology: PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: FrontFace::Ccw, + cull_mode: Some(Face::Back), + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: CompareFunction::LessEqual, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(FragmentState { + module: ©_shader, + entry_point: "fragment", + targets: &[Some(ColorTargetState { + format: config.format, + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + })], + }), + multiview: None, + }); + let sampler = device.create_sampler(&SamplerDescriptor { + label: Some("Sampler"), + address_mode_u: AddressMode::Repeat, + address_mode_v: AddressMode::Repeat, + address_mode_w: AddressMode::Repeat, + mag_filter: FilterMode::Nearest, + min_filter: FilterMode::Nearest, + mipmap_filter: FilterMode::Nearest, + ..Default::default() + }); Self { surface, @@ -429,6 +561,10 @@ impl<'window> State<'window> { diffuse_bind_group, text_render_pipeline, unicode_bind_group, + tooltip_effect_render_pipeline, + texture_reference_bind_group_layout, + copy_render_pipeline, + sampler, last_tick: Duration::ZERO, } } @@ -511,10 +647,24 @@ impl<'window> State<'window> { if let Err(e) = workbench.try_subscription() { workbench.alert(Alert::new("Error!", TextColor::Red, e.to_string())) } - let output = self.surface.get_current_texture()?; - let view = output - .texture - .create_view(&TextureViewDescriptor::default()); + let surface_texture = self.surface.get_current_texture()?; + let size = Extent3d { + width: surface_texture.texture.width(), + height: surface_texture.texture.height(), + depth_or_array_layers: 1, + }; + let surface_view = surface_texture.texture.create_view(&TextureViewDescriptor::default()); + let texture = self.device.create_texture(&TextureDescriptor { + label: Some("Texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: surface_texture.texture.format(), + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_SRC, + view_formats: &[], + }); + let view = texture.create_view(&TextureViewDescriptor::default()); let mut encoder = self .device .create_command_encoder(&CommandEncoderDescriptor { @@ -522,11 +672,7 @@ impl<'window> State<'window> { }); let depth_texture = self.device.create_texture(&TextureDescriptor { label: Some("Depth Texture"), - size: Extent3d { - width: self.config.width, - height: self.config.height, - depth_or_array_layers: 1, - }, + size, mip_level_count: 1, sample_count: 1, dimension: TextureDimension::D2, @@ -536,11 +682,20 @@ impl<'window> State<'window> { }); let depth_texture_view = depth_texture.create_view(&TextureViewDescriptor::default()); + let mut builder = VertexBufferBuilder::new( + self.size, + assets::ATLAS_WIDTH, + assets::ATLAS_HEIGHT, + workbench.scroll(), + workbench.scale, + ); + { let vertex_buffer; let index_buffer; let text_vertex_buffer; let text_index_buffer; + let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor { label: Some("Render Pass"), color_attachments: &[Some(RenderPassColorAttachment { @@ -567,14 +722,6 @@ impl<'window> State<'window> { timestamp_writes: None, occlusion_query_set: None, }); - - let mut builder = VertexBufferBuilder::new( - self.size, - assets::ATLAS_WIDTH, - assets::ATLAS_HEIGHT, - workbench.scroll(), - workbench.scale, - ); workbench.render(&mut builder); let show_cursor = true; @@ -628,8 +775,272 @@ impl<'window> State<'window> { } } - self.queue.submit(std::iter::once(encoder.finish())); - output.present(); + self.queue.submit(Some(encoder.finish())); + + if builder.drew_tooltip() { + builder.clear_buffers(); + let mut encoder = self.device.create_command_encoder(&CommandEncoderDescriptor { label: Some("Tooltip Command Encoder"), }); + + let tooltip_effect_texture = self.device.create_texture(&TextureDescriptor { + label: Some("Tooltip Effect Texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: surface_texture.texture.format(), + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let tooltip_effect_texture_view = tooltip_effect_texture.create_view(&TextureViewDescriptor::default()); + + let vertices = { + let tooltip_effect_vertex_buffer; + let tooltip_effect_bind_group; + + let mut tooltip_effect_render_pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("Tooltip Effect Render Pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &tooltip_effect_texture_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + if let Some(vertices) = builder.draw_tooltip0() { + tooltip_effect_bind_group = self.device.create_bind_group(&BindGroupDescriptor { + label: Some("Tooltip Effect Bind Group"), + layout: &self.texture_reference_bind_group_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&self.sampler), + } + ], + }); + + tooltip_effect_render_pass.set_pipeline(&self.tooltip_effect_render_pipeline); + tooltip_effect_render_pass.set_bind_group(0, &tooltip_effect_bind_group, &[]); + + tooltip_effect_vertex_buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("Tooltip Effect Vertex Buffer"), + contents: unsafe { std::slice::from_raw_parts(vertices.as_ptr().cast::(), vertices.len() * 4) }, + usage: BufferUsages::VERTEX, + }); + + tooltip_effect_render_pass.set_vertex_buffer(0, tooltip_effect_vertex_buffer.slice(..)); + + tooltip_effect_render_pass.draw(0..6, 0..1); + vertices + } else { + panic!("Tooltip existed, then didn't?") + } + }; + + { + let tooltip_vertex_buffer; + let tooltip_index_buffer; + let tooltip_text_vertex_buffer; + let tooltip_text_index_buffer; + + let copy_bind_group; + let copy_vertex_buffer; + + let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("Tooltip Render Pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth_texture_view, + depth_ops: Some(Operations { + load: LoadOp::Clear(1.0), + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + + { + copy_bind_group = self.device.create_bind_group(&BindGroupDescriptor { + label: Some("Copy Bind Group"), + layout: &self.texture_reference_bind_group_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&tooltip_effect_texture_view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&self.sampler), + } + ], + }); + + render_pass.set_pipeline(&self.copy_render_pipeline); + render_pass.set_bind_group(0, ©_bind_group, &[]); + + copy_vertex_buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("Copy Vertex Buffer"), + contents: unsafe { std::slice::from_raw_parts(vertices.as_ptr().cast::(), vertices.len() * 4) }, + usage: BufferUsages::VERTEX, + }); + + render_pass.set_vertex_buffer(0, copy_vertex_buffer.slice(..)); + render_pass.draw(0..6, 0..1); + } + + { + render_pass.set_pipeline(&self.render_pipeline); + render_pass.set_bind_group(0, &self.diffuse_bind_group, &[]); + + tooltip_vertex_buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("Tooltip Vertex Buffer"), + contents: builder.vertices(), + usage: BufferUsages::VERTEX, + }); + + tooltip_index_buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("Tooltip Index Buffer"), + contents: builder.indices(), + usage: BufferUsages::INDEX, + }); + + render_pass.set_vertex_buffer(0, tooltip_vertex_buffer.slice(..)); + render_pass.set_index_buffer(tooltip_index_buffer.slice(..), IndexFormat::Uint16); + + render_pass.draw_indexed(0..builder.indices_len(), 0, 0..1); + } + + { + render_pass.set_pipeline(&self.text_render_pipeline); + render_pass.set_bind_group(0, &self.unicode_bind_group, &[]); + + tooltip_text_vertex_buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("Tooltip Text Vertex Buffer"), + contents: builder.text_vertices(), + usage: BufferUsages::VERTEX, + }); + + tooltip_text_index_buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("Tooltip Text Index Buffer"), + contents: builder.text_indices(), + usage: BufferUsages::INDEX, + }); + + render_pass.set_vertex_buffer(0, tooltip_text_vertex_buffer.slice(..)); + render_pass.set_index_buffer(tooltip_text_index_buffer.slice(..), IndexFormat::Uint32); + + render_pass.draw_indexed(0..builder.text_indices_len(), 0, 0..1); + } + } + + self.queue.submit(Some(encoder.finish())); + } + + let mut encoder = self.device.create_command_encoder(&CommandEncoderDescriptor { label: Some("Copy Command Encoder"), }); + { + const COPY_VERTEX_BUFFER_DATA: &[f32] = &[ + // 0 + 1.0, + 1.0, + + // 1 + -1.0, + 1.0, + + // 2 + -1.0, + -1.0, + + // 0 + 1.0, + 1.0, + + // 2 + -1.0, + -1.0, + + // 3 + 1.0, + -1.0, + ]; + + let copy_vertex_buffer; + let copy_bind_group; + + let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("Copy Render Pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: &surface_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth_texture_view, + depth_ops: Some(Operations { + load: LoadOp::Clear(1.0), + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + + copy_vertex_buffer = self.device.create_buffer_init(&BufferInitDescriptor { + label: Some("Copy Vertex Buffer"), + contents: unsafe { std::slice::from_raw_parts(COPY_VERTEX_BUFFER_DATA.as_ptr().cast::(), COPY_VERTEX_BUFFER_DATA.len() * 4) }, + usage: BufferUsages::VERTEX, + }); + + copy_bind_group = self.device.create_bind_group(&BindGroupDescriptor { + label: Some("Copy Bind Group"), + layout: &self.texture_reference_bind_group_layout, + entries: &[BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&view), + }, BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&self.sampler), + }], + }); + + render_pass.set_pipeline(&self.copy_render_pipeline); + render_pass.set_bind_group(0, ©_bind_group, &[]); + + render_pass.set_vertex_buffer(0, copy_vertex_buffer.slice(..)); + + render_pass.draw(0..6, 0..1); + } + self.queue.submit(Some(encoder.finish())); + + surface_texture.present(); Ok(()) } diff --git a/src/workbench.rs b/src/workbench.rs index a89e7d2..7d31a3c 100644 --- a/src/workbench.rs +++ b/src/workbench.rs @@ -17,7 +17,7 @@ use crate::alert::Alert; use crate::assets::{ACTION_WHEEL_Z, BACKDROP_UV, BASE_TEXT_Z, BASE_Z, BOOKMARK_UV, CLOSED_WIDGET_UV, DARK_STRIPE_UV, SAVE_UV, HEADER_SIZE, HELD_ENTRY_Z, HIDDEN_BOOKMARK_UV, HORIZONTAL_SEPARATOR_UV, HOVERED_STRIPE_UV, HOVERED_WIDGET_UV, JUST_OVERLAPPING_BASE_TEXT_Z, LIGHT_STRIPE_UV, LINE_NUMBER_SEPARATOR_UV, NEW_FILE_UV, OPEN_FOLDER_UV, SELECTED_ACTION_WHEEL, SELECTED_WIDGET_UV, SELECTION_UV, TRAY_UV, JUST_UNDERLAPPING_BASE_Z, SAVE_GRAYSCALE_UV, UNSELECTED_ACTION_WHEEL, UNSELECTED_WIDGET_UV}; use crate::bookmark::Bookmarks; use crate::color::TextColor; -use crate::decoder::Decoder; +use crate::be_decoder::BigEndianDecoder; use crate::elements::chunk::{NbtChunk, NbtRegion}; use crate::elements::compound::NbtCompound; use crate::elements::element::{NbtByte, NbtByteArray, NbtDouble, NbtFloat, NbtInt, NbtIntArray, NbtLong, NbtLongArray, NbtShort}; @@ -136,7 +136,7 @@ impl Workbench { } workbench.new_custom_tab(window_properties, Tab { #[cfg(debug_assertions)] - value: Box::new(NbtElement::from_file(include_bytes!("assets/test.nbt"), SortAlgorithm::None).expect("Included debug nbt contains valid data")), + value: Box::new(NbtElement::from_be_file(include_bytes!("assets/test.nbt"), SortAlgorithm::None).expect("Included debug nbt contains valid data")), #[cfg(debug_assertions)] name: "test.nbt".into(), #[cfg(not(debug_assertions))] @@ -144,7 +144,7 @@ impl Workbench { #[cfg(not(debug_assertions))] name: "new.nbt".into(), path: None, - compression: FileFormat::Nbt, + format: FileFormat::Nbt, undos: LinkedQueue::new(), redos: LinkedQueue::new(), unsaved_changes: false, @@ -157,7 +157,8 @@ impl Workbench { freehand_mode: false, selected_text: None, last_close_attempt: Duration::ZERO, - last_selected_text_interaction: (0, 0, Duration::ZERO) + last_selected_text_interaction: (0, 0, Duration::ZERO), + last_interaction: since_epoch(), }); } workbench @@ -169,8 +170,8 @@ impl Workbench { #[inline] #[allow(clippy::equatable_if_let)] pub fn on_open_file(&mut self, path: &Path, buf: Vec, window_properties: &mut WindowProperties) -> Result<()> { - let (nbt, compressed) = Tab::parse_raw(path, buf, self.sort_algorithm)?; - let mut tab = Tab::new(nbt, path, compressed, self.window_height, self.window_width)?; + let (nbt, format) = Tab::parse_raw(path, buf, self.sort_algorithm)?; + let mut tab = Tab::new(nbt, path, format, self.window_height, self.window_width)?; if !tab.close_selected_text(false, window_properties) { tab.selected_text = None; }; @@ -188,12 +189,12 @@ impl Workbench { (pos.x as f32, pos.y as f32) } }; - let ctrl = self.held_keys.contains(&KeyCode::ControlLeft) | self.held_keys.contains(&KeyCode::ControlRight) | self.held_keys.contains(&KeyCode::SuperLeft) | self.held_keys.contains(&KeyCode::SuperRight); + let ctrl = self.ctrl(); if ctrl { self.set_scale(self.scale.wrapping_add(v.signum() as isize as usize)); return true; } - let shift = self.held_keys.contains(&KeyCode::ShiftLeft) | self.held_keys.contains(&KeyCode::ShiftRight); + let shift = self.shift(); if self.mouse_y < 21 { let scroll = if shift { -v } else { -h }; self.tab_scroll = ((self.tab_scroll as isize + (scroll * 48.0) as isize).max(0) as usize).min( @@ -222,9 +223,10 @@ impl Workbench { #[inline] #[allow(clippy::collapsible_if)] pub fn on_mouse_input(&mut self, state: ElementState, button: MouseButton, window_properties: &mut WindowProperties) -> bool { + tab_mut!(self).last_interaction = since_epoch(); let left_margin = self.left_margin(); let horizontal_scroll = self.horizontal_scroll(); - let shift = self.held_keys.contains(&KeyCode::ShiftLeft) | self.held_keys.contains(&KeyCode::ShiftRight); + let shift = self.shift(); let x = self.mouse_x; let y = self.mouse_y; match state { @@ -347,7 +349,7 @@ impl Workbench { } } - match core::mem::replace(&mut self.held_entry, HeldEntry::Empty) { + match self.held_entry.take() { HeldEntry::Empty => {} HeldEntry::FromAether(x) => { self.drop(x, None, left_margin); @@ -365,14 +367,8 @@ impl Workbench { } } - if button == MouseButton::Middle { - if self.delete(shift) { - break 'a; - } - } - if button == MouseButton::Left { - if self.try_select_text() { + if self.try_select_text(false) { break 'a; } } @@ -653,8 +649,8 @@ impl Workbench { }; buf.write(&data); let buf = buf.finish(); - let mut decoder = Decoder::new(&buf, SortAlgorithm::None); - NbtElement::from_bytes(id, &mut decoder).context("Could not read bytes for array")? + let mut decoder = BigEndianDecoder::new(&buf, SortAlgorithm::None); + NbtElement::from_be_bytes(id, &mut decoder).context("Could not read bytes for array")? })?, }, Err(TryRecvError::Disconnected) => { @@ -978,8 +974,8 @@ impl Workbench { element: (key, element), }); } - tab.selected_text = None; + false } DropFn::Dropped(height, true_height, _, line_number) => { if let Some(from_indices) = from_indices { @@ -1018,9 +1014,9 @@ impl Workbench { recache_along_indices(&indices[..indices.len() - 1], &mut tab.value); tab.bookmarks[line_number..].increment(height, true_height); self.subscription = None; + true } } - true } #[inline] @@ -1077,9 +1073,9 @@ impl Workbench { self.remove_tab(idx, window_properties); } else if idx == self.tab && x > width - 16 && x < width { if button == MouseButton::Left { - tab.compression = tab.compression.cycle(); + tab.format = tab.format.cycle(); } else if button == MouseButton::Right { - tab.compression = tab.compression.rev_cycle(); + tab.format = tab.format.rev_cycle(); } } else if idx == self.tab && x + 1 >= width - 32 && x < width - 16 { if let Err(e) = tab.save(self.held_keys.contains(&KeyCode::ShiftLeft) || self.held_keys.contains(&KeyCode::ShiftRight)) { @@ -1113,7 +1109,7 @@ impl Workbench { value: Box::new(if region { NbtElement::Region(NbtRegion::new()) } else { NbtElement::Compound(NbtCompound::new()) }), name: "new.nbt".into(), path: None, - compression: FileFormat::Nbt, + format: FileFormat::Nbt, undos: LinkedQueue::new(), redos: LinkedQueue::new(), unsaved_changes: false, @@ -1126,7 +1122,8 @@ impl Workbench { freehand_mode: false, selected_text: None, last_close_attempt: Duration::ZERO, - last_selected_text_interaction: (0, 0, Duration::ZERO) + last_selected_text_interaction: (0, 0, Duration::ZERO), + last_interaction: since_epoch(), }); } @@ -1188,6 +1185,18 @@ impl Workbench { tab!(self).left_margin(self.held_entry.element()) } + #[inline] + #[must_use] + pub fn ctrl(&self) -> bool { + self.held_keys.contains(&KeyCode::ControlLeft) | self.held_keys.contains(&KeyCode::ControlRight) | self.held_keys.contains(&KeyCode::SuperLeft) | self.held_keys.contains(&KeyCode::SuperRight) + } + + #[inline] + #[must_use] + pub fn shift(&self) -> bool { + self.held_keys.contains(&KeyCode::ShiftLeft) | self.held_keys.contains(&KeyCode::ShiftRight) + } + #[inline] fn toggle(&mut self, expand: bool, ignore_depth: bool) -> bool { let left_margin = self.left_margin(); @@ -1268,7 +1277,7 @@ impl Workbench { } #[inline] - fn try_select_text(&mut self) -> bool { + fn try_select_text(&mut self, snap_to_ends: bool) -> bool { let left_margin = self.left_margin(); let horizontal_scroll = self.horizontal_scroll(); if self.mouse_x + horizontal_scroll < left_margin { return false } @@ -1283,12 +1292,15 @@ impl Workbench { indices.push(idx); if let Position::Last | Position::Only = position { let child = unsafe { value.get(idx).panic_unchecked("Child didn't exist somehow") }; + let target_x = indices.len() * 16 + 32 + 4 + left_margin; + let k = key.map_or_else(|| child.as_chunk().map(|chunk| (chunk.x.to_string().into_boxed_str(), TextColor::TreePrimitive, true)), |x| Some((x.into_string().into_boxed_str(), TextColor::TreeKey, true))); + let v = Some(child.value()).map(|(a, c)| (a.into_string().into_boxed_str(), c, c != TextColor::TreeKey)); tab.selected_text = SelectedText::new( - indices.len() * 16 + 32 + 4 + left_margin, - self.mouse_x + horizontal_scroll, + target_x, + (self.mouse_x + horizontal_scroll).clamp(if snap_to_ends { target_x } else { 0 }, if snap_to_ends { k.as_ref().map_or(0, |(k, _, b)| (*b as usize) * (k.width() + ": ".width() * v.is_some() as usize)) + v.as_ref().map_or(0, |(v, _, b)| (*b as usize) * v.width()) + target_x } else { usize::MAX }), y * 16 + HEADER_SIZE, - key.map_or_else(|| child.as_chunk().map(|chunk| (chunk.x.to_string().into_boxed_str(), TextColor::TreePrimitive, true)), |x| Some((x.into_string().into_boxed_str(), TextColor::TreeKey, true))), - Some(child.value()).map(|(a, c)| (a.into_string().into_boxed_str(), c, c != TextColor::TreeKey)), + k, + v, child.id() == NbtChunk::ID, indices, ); @@ -1756,10 +1768,10 @@ impl Workbench { #[inline] pub fn force_open(&mut self) { + let shift = self.shift(); let tab = tab_mut!(self); if let Some(SelectedText(Text { additional: SelectedTextAdditional { y, indices, .. }, .. })) = tab.selected_text.as_ref() { let indices = indices.clone(); - let shift = self.held_keys.contains(&KeyCode::ShiftLeft) | self.held_keys.contains(&KeyCode::ShiftRight); let (_, _, element, line_number) = Navigate::new(indices.iter().copied(), &mut tab.value).last(); let true_height = element.true_height(); let predicate = if shift { @@ -1850,6 +1862,7 @@ impl Workbench { clippy::cognitive_complexity )] pub fn on_key_input(&mut self, key: &KeyEvent, window_properties: &mut WindowProperties) -> bool { + tab_mut!(self).last_interaction = since_epoch(); if key.state == ElementState::Pressed { if let PhysicalKey::Code(key) = key.physical_key { self.held_keys.insert(key); @@ -1953,13 +1966,6 @@ impl Workbench { self.search_box.select(0, MouseButton::Left); return true; } - if key == KeyCode::KeyV && flags == flags!(Ctrl) && let Some(element) = get_clipboard().and_then(|x| NbtElement::from_str(&x, self.sort_algorithm)) && (element.1.id() != NbtChunk::ID || tab.value.id() == NbtRegion::ID) { - let old_held_entry = core::mem::replace(&mut self.held_entry, HeldEntry::FromAether(element)); - let HeldEntry::FromAether(pair) = core::mem::replace(&mut self.held_entry, old_held_entry) else { - unsafe { panic_unchecked("we just set it you, bozo") } - }; - return self.drop(pair, None, left_margin); - } if key == KeyCode::Equal && flags == flags!(Ctrl) { self.set_scale(self.scale + 1); return true; @@ -1977,6 +1983,13 @@ impl Workbench { self.held_entry = HeldEntry::Empty; return true; } + if (key == KeyCode::Enter || key == KeyCode::NumpadEnter) && tab.selected_text.is_none() && flags == flags!() { + return match self.held_entry.take() { + HeldEntry::Empty => { self.try_select_text(true); true }, + HeldEntry::FromAether(pair) => self.drop(pair, None, left_margin), + HeldEntry::FromKnown(pair, indices) => return self.drop(pair, Some(indices), left_margin) + } + } if flags == flags!(Ctrl) { if key == KeyCode::Digit1 { self.set_tab(0, window_properties); @@ -2358,6 +2371,19 @@ impl Workbench { #[inline] pub fn tick(&mut self) { + #[cfg(not(target_arch = "wasm32"))] { + let mut alerts = vec![]; + for (idx, tab) in self.tabs.iter_mut().enumerate() { + if let Some(path) = tab.path.as_deref() && path.is_absolute() && (since_epoch() - tab.last_interaction >= Tab::AUTOSAVE_INTERVAL) && tab.unsaved_changes && tab.value.true_height() <= Tab::AUTOSAVE_MAXIMUM_LINES { + if let Err(e) = tab.save(false) { + alerts.push(Alert::new("Error!", TextColor::Red, e.context(format!("Failed to autosave {nth} tab", nth = crate::nth(idx + 1))).to_string())); + } + } + } + for alert in alerts { + self.alert(alert); + } + } if (!self.held_entry.is_empty() || tab!(self).freehand_mode) && self.action_wheel.is_none() && self.scrollbar_offset.is_none() { self.try_mouse_scroll(); } @@ -2370,6 +2396,23 @@ impl Workbench { } } + #[inline] + #[must_use] + pub fn close(&mut self) -> usize { + let mut failed_tabs = 0_usize; + + for tab in &mut self.tabs { + if tab.unsaved_changes && (since_epoch() - core::mem::replace(&mut tab.last_close_attempt, since_epoch())) > DOUBLE_CLICK_INTERVAL { + failed_tabs += 1; + } + } + + if failed_tabs > 0 { + self.alert(Alert::new("Are you sure you want to exit?", TextColor::Yellow, format!("You have {failed_tabs} unsaved tabs."))); + } + failed_tabs + } + #[inline] fn render_held_entry(&self, builder: &mut VertexBufferBuilder) { if let Some(element) = self.held_entry.element() { @@ -2440,12 +2483,12 @@ impl Workbench { }, (16, 16), ); - builder.draw_texture((offset - 16, 3), tab.compression.uv(), (16, 16)); + builder.draw_texture((offset - 16, 3), tab.format.uv(), (16, 16)); if (offset - 32..offset - 16).contains(&self.mouse_x) && (3..19).contains(&self.mouse_y) { builder.draw_tooltip(&["Save"], (self.mouse_x, self.mouse_y), false); } if (offset - 16..offset).contains(&self.mouse_x) && (3..19).contains(&self.mouse_y) { - builder.draw_tooltip(&[tab.compression.into_str()], (self.mouse_x, self.mouse_y), false); + builder.draw_tooltip(&[tab.format.into_str()], (self.mouse_x, self.mouse_y), false); } offset += 6; } @@ -2569,7 +2612,7 @@ impl Workbench { clippy::too_many_lines )] fn char_from_key(&self, key: KeyCode) -> Option { - if self.held_keys.contains(&KeyCode::ControlLeft) | self.held_keys.contains(&KeyCode::ControlRight) | self.held_keys.contains(&KeyCode::SuperLeft) | self.held_keys.contains(&KeyCode::SuperRight) { return None } + if self.ctrl() { return None } let shift = self.held_keys.contains(&KeyCode::ShiftLeft) || self.held_keys.contains(&KeyCode::ShiftRight); Some(match key { KeyCode::Digit1 => if shift { '!' } else { '1' }, @@ -2626,20 +2669,8 @@ impl Workbench { KeyCode::NumpadEqual => '=', KeyCode::NumpadMultiply => '*', KeyCode::NumpadSubtract => '-', - KeyCode::Quote => { - if shift { - '"' - } else { - '\'' - } - } - KeyCode::Backslash => { - if shift { - '|' - } else { - '\\' - } - } + KeyCode::Quote => if shift { '"' } else { '\'' }, + KeyCode::Backslash => if shift { '|' } else { '\\' }, KeyCode::Semicolon => if shift { ':' } else { ';' }, KeyCode::Comma => if shift { '<' } else { ',' }, KeyCode::Equal => if shift { '+' } else { '=' }, diff --git a/web/index.html b/web/index.html index 602b04e..907404e 100644 --- a/web/index.html +++ b/web/index.html @@ -65,7 +65,7 @@ - +

@@ -84,7 +84,7 @@ } while (false) } - import init, { open_file } from "./nbtworkbench.js?version=2"; + import init, { open_file, close } from "./nbtworkbench.js?version=2"; await init(); const canvas = document.getElementById("canvas"); @@ -104,7 +104,7 @@ canvas.ondragover = event => { event.stopPropagation(); event.preventDefault(); - } + }; canvas.ondrop = event => { event.stopPropagation(); event.preventDefault(); @@ -112,7 +112,14 @@ file.arrayBuffer().then(bytes => { open_file(file.name, new Uint8Array(bytes)); }) - } + }; + window.onbeforeunload = event => { + event.preventDefault(); + let unsaved_tabs = close(); + if (unsaved_tabs > 0) { + event.returnValue = "Are you sure you want to close? You have " + unsaved_tabs + " unsaved tabs."; + } + };