From 9332649a63e2f7b6e81b4dc72ff17c0764658953 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Mon, 13 Jul 2026 22:52:29 +0300 Subject: [PATCH] initial commit --- .gitignore | 3 + README.md | 18 + .../Blue/_aseprite/death.aseprite | Bin 0 -> 1610 bytes .../Blue/_aseprite/hurt.aseprite | Bin 0 -> 1182 bytes .../Blue/_aseprite/idle.aseprite | Bin 0 -> 1155 bytes .../Blue/_aseprite/walk.aseprite | Bin 0 -> 1984 bytes .../Blue/_shadow/shadow_death.png | Bin 0 -> 240 bytes .../Blue/_shadow/shadow_static.png | Bin 0 -> 5123 bytes assets/Prototype_Character/Blue/death.png | Bin 0 -> 932 bytes assets/Prototype_Character/Blue/hurt.png | Bin 0 -> 870 bytes assets/Prototype_Character/Blue/idle.png | Bin 0 -> 718 bytes assets/Prototype_Character/Blue/walk.png | Bin 0 -> 1076 bytes .../Default/_aseprite/death.aseprite | Bin 0 -> 1596 bytes .../Default/_aseprite/hurt.aseprite | Bin 0 -> 1176 bytes .../Default/_aseprite/idle.aseprite | Bin 0 -> 1144 bytes .../Default/_aseprite/walk.aseprite | Bin 0 -> 1959 bytes .../Default/_shadow/shadow_death.png | Bin 0 -> 240 bytes .../Default/_shadow/shadow_static.png | Bin 0 -> 5123 bytes assets/Prototype_Character/Default/death.png | Bin 0 -> 911 bytes assets/Prototype_Character/Default/hurt.png | Bin 0 -> 853 bytes assets/Prototype_Character/Default/idle.png | Bin 0 -> 690 bytes assets/Prototype_Character/Default/walk.png | Bin 0 -> 1079 bytes .../Green/_aseprite/death.aseprite | Bin 0 -> 1605 bytes .../Green/_aseprite/hurt.aseprite | Bin 0 -> 1179 bytes .../Green/_aseprite/idle.aseprite | Bin 0 -> 1150 bytes .../Green/_aseprite/walk.aseprite | Bin 0 -> 1979 bytes .../Green/_shadow/shadow_death.png | Bin 0 -> 240 bytes .../Green/_shadow/shadow_static.png | Bin 0 -> 5123 bytes assets/Prototype_Character/Green/death.png | Bin 0 -> 925 bytes assets/Prototype_Character/Green/hurt.png | Bin 0 -> 883 bytes assets/Prototype_Character/Green/idle.png | Bin 0 -> 711 bytes assets/Prototype_Character/Green/walk.png | Bin 0 -> 1085 bytes .../Red/_aseprite/death.aseprite | Bin 0 -> 1600 bytes .../Red/_aseprite/hurt.aseprite | Bin 0 -> 1177 bytes .../Red/_aseprite/idle.aseprite | Bin 0 -> 1148 bytes .../Red/_aseprite/walk.aseprite | Bin 0 -> 1967 bytes .../Red/_shadow/shadow_death.png | Bin 0 -> 240 bytes .../Red/_shadow/shadow_static.png | Bin 0 -> 5123 bytes assets/Prototype_Character/Red/death.png | Bin 0 -> 918 bytes assets/Prototype_Character/Red/hurt.png | Bin 0 -> 858 bytes assets/Prototype_Character/Red/idle.png | Bin 0 -> 701 bytes assets/Prototype_Character/Red/walk.png | Bin 0 -> 1056 bytes .../Yellow/_aseprite/death.aseprite | Bin 0 -> 1611 bytes .../Yellow/_aseprite/hurt.aseprite | Bin 0 -> 1181 bytes .../Yellow/_aseprite/idle.aseprite | Bin 0 -> 1155 bytes .../Yellow/_aseprite/walk.aseprite | Bin 0 -> 1982 bytes .../Yellow/_shadow/shadow_death.png | Bin 0 -> 240 bytes .../Yellow/_shadow/shadow_static.png | Bin 0 -> 5123 bytes assets/Prototype_Character/Yellow/death.png | Bin 0 -> 931 bytes assets/Prototype_Character/Yellow/hurt.png | Bin 0 -> 871 bytes assets/Prototype_Character/Yellow/idle.png | Bin 0 -> 719 bytes assets/Prototype_Character/Yellow/walk.png | Bin 0 -> 1071 bytes assets/tranquil_tunnels_transparent.png | Bin 0 -> 154034 bytes build.zig | 393 ++++ build.zig.zon | 31 + libs/stb/build.zig | 37 + libs/stb/build.zig.zon | 17 + libs/stb/src/stb_image.zig | 31 + libs/stb/src/stb_image_impl.c | 3 + libs/stb/src/stb_rect_pack.zig | 36 + libs/stb/src/stb_rect_pack_impl.c | 2 + libs/stb/src/stb_truetype.zig | 116 + libs/stb/src/stb_truetype_impl.c | 4 + libs/stb/src/stb_vorbis.zig | 170 ++ libs/stb/src/stb_vorbis_impl.c | 3 + libs/tiled/build.zig | 29 + libs/tiled/src/buffers.zig | 18 + libs/tiled/src/color.zig | 52 + libs/tiled/src/global_tile_id.zig | 15 + libs/tiled/src/layer.zig | 283 +++ libs/tiled/src/object.zig | 421 ++++ libs/tiled/src/position.zig | 20 + libs/tiled/src/property.zig | 153 ++ libs/tiled/src/root.zig | 16 + libs/tiled/src/tilemap.zig | 235 ++ libs/tiled/src/tileset.zig | 226 ++ libs/tiled/src/xml.zig | 687 ++++++ libs/tracy/build.zig | 138 ++ libs/tracy/build.zig.zon | 13 + libs/tracy/src/root.zig | 376 ++++ scripts/png-to-icon.zig | 57 + src/app.zig | 115 + src/cli.zig | 302 +++ src/color.zig | 40 + src/file_watcher.zig | 320 +++ src/graphics.zig | 1996 +++++++++++++++++ src/imgui.zig | 339 +++ src/inotify.zig | 186 ++ src/input.zig | 238 ++ src/main.zig | 25 + src/math.zig | 447 ++++ src/platform.zig | 674 ++++++ src/shader.glsl | 50 + src/slot_map.zig | 305 +++ 94 files changed, 8640 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 assets/Prototype_Character/Blue/_aseprite/death.aseprite create mode 100644 assets/Prototype_Character/Blue/_aseprite/hurt.aseprite create mode 100644 assets/Prototype_Character/Blue/_aseprite/idle.aseprite create mode 100644 assets/Prototype_Character/Blue/_aseprite/walk.aseprite create mode 100644 assets/Prototype_Character/Blue/_shadow/shadow_death.png create mode 100644 assets/Prototype_Character/Blue/_shadow/shadow_static.png create mode 100644 assets/Prototype_Character/Blue/death.png create mode 100644 assets/Prototype_Character/Blue/hurt.png create mode 100644 assets/Prototype_Character/Blue/idle.png create mode 100644 assets/Prototype_Character/Blue/walk.png create mode 100644 assets/Prototype_Character/Default/_aseprite/death.aseprite create mode 100644 assets/Prototype_Character/Default/_aseprite/hurt.aseprite create mode 100644 assets/Prototype_Character/Default/_aseprite/idle.aseprite create mode 100644 assets/Prototype_Character/Default/_aseprite/walk.aseprite create mode 100644 assets/Prototype_Character/Default/_shadow/shadow_death.png create mode 100644 assets/Prototype_Character/Default/_shadow/shadow_static.png create mode 100644 assets/Prototype_Character/Default/death.png create mode 100644 assets/Prototype_Character/Default/hurt.png create mode 100644 assets/Prototype_Character/Default/idle.png create mode 100644 assets/Prototype_Character/Default/walk.png create mode 100644 assets/Prototype_Character/Green/_aseprite/death.aseprite create mode 100644 assets/Prototype_Character/Green/_aseprite/hurt.aseprite create mode 100644 assets/Prototype_Character/Green/_aseprite/idle.aseprite create mode 100644 assets/Prototype_Character/Green/_aseprite/walk.aseprite create mode 100644 assets/Prototype_Character/Green/_shadow/shadow_death.png create mode 100644 assets/Prototype_Character/Green/_shadow/shadow_static.png create mode 100644 assets/Prototype_Character/Green/death.png create mode 100644 assets/Prototype_Character/Green/hurt.png create mode 100644 assets/Prototype_Character/Green/idle.png create mode 100644 assets/Prototype_Character/Green/walk.png create mode 100644 assets/Prototype_Character/Red/_aseprite/death.aseprite create mode 100644 assets/Prototype_Character/Red/_aseprite/hurt.aseprite create mode 100644 assets/Prototype_Character/Red/_aseprite/idle.aseprite create mode 100644 assets/Prototype_Character/Red/_aseprite/walk.aseprite create mode 100644 assets/Prototype_Character/Red/_shadow/shadow_death.png create mode 100644 assets/Prototype_Character/Red/_shadow/shadow_static.png create mode 100644 assets/Prototype_Character/Red/death.png create mode 100644 assets/Prototype_Character/Red/hurt.png create mode 100644 assets/Prototype_Character/Red/idle.png create mode 100644 assets/Prototype_Character/Red/walk.png create mode 100644 assets/Prototype_Character/Yellow/_aseprite/death.aseprite create mode 100644 assets/Prototype_Character/Yellow/_aseprite/hurt.aseprite create mode 100644 assets/Prototype_Character/Yellow/_aseprite/idle.aseprite create mode 100644 assets/Prototype_Character/Yellow/_aseprite/walk.aseprite create mode 100644 assets/Prototype_Character/Yellow/_shadow/shadow_death.png create mode 100644 assets/Prototype_Character/Yellow/_shadow/shadow_static.png create mode 100644 assets/Prototype_Character/Yellow/death.png create mode 100644 assets/Prototype_Character/Yellow/hurt.png create mode 100644 assets/Prototype_Character/Yellow/idle.png create mode 100644 assets/Prototype_Character/Yellow/walk.png create mode 100644 assets/tranquil_tunnels_transparent.png create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 libs/stb/build.zig create mode 100644 libs/stb/build.zig.zon create mode 100644 libs/stb/src/stb_image.zig create mode 100644 libs/stb/src/stb_image_impl.c create mode 100644 libs/stb/src/stb_rect_pack.zig create mode 100644 libs/stb/src/stb_rect_pack_impl.c create mode 100644 libs/stb/src/stb_truetype.zig create mode 100644 libs/stb/src/stb_truetype_impl.c create mode 100644 libs/stb/src/stb_vorbis.zig create mode 100644 libs/stb/src/stb_vorbis_impl.c create mode 100644 libs/tiled/build.zig create mode 100644 libs/tiled/src/buffers.zig create mode 100644 libs/tiled/src/color.zig create mode 100644 libs/tiled/src/global_tile_id.zig create mode 100644 libs/tiled/src/layer.zig create mode 100644 libs/tiled/src/object.zig create mode 100644 libs/tiled/src/position.zig create mode 100644 libs/tiled/src/property.zig create mode 100644 libs/tiled/src/root.zig create mode 100644 libs/tiled/src/tilemap.zig create mode 100644 libs/tiled/src/tileset.zig create mode 100644 libs/tiled/src/xml.zig create mode 100644 libs/tracy/build.zig create mode 100644 libs/tracy/build.zig.zon create mode 100644 libs/tracy/src/root.zig create mode 100644 scripts/png-to-icon.zig create mode 100644 src/app.zig create mode 100644 src/cli.zig create mode 100644 src/color.zig create mode 100644 src/file_watcher.zig create mode 100644 src/graphics.zig create mode 100644 src/imgui.zig create mode 100644 src/inotify.zig create mode 100644 src/input.zig create mode 100644 src/main.zig create mode 100644 src/math.zig create mode 100644 src/platform.zig create mode 100644 src/shader.glsl create mode 100644 src/slot_map.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e5c1f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +zig-out +zig-pkg +.zig-cache diff --git a/README.md b/README.md new file mode 100644 index 0000000..cfd0eec --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Gaem + +## Run + +Linux and Windows: +```sh +zig build run +``` + +Web: +```sh +zig build -Dtarget=wasm32-emscripten run +``` + +Cross-compile for Windows from Linux: +```sh +zig build -Dtarget=x86_64-windows +``` diff --git a/assets/Prototype_Character/Blue/_aseprite/death.aseprite b/assets/Prototype_Character/Blue/_aseprite/death.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..739f73715ccd50ac4aaf74f44dd73afd47010cf0 GIT binary patch literal 1610 zcmeZbV_~;dPX2m2E;*8{2wgs9^~o52V^NI0PSSBdiCmmIj~A* zAPEC*Kn5E_a7JQEemO#IRCwZlMW76e0`?3w8d3o&Fzazy;#ZOwgGApP^*hl|mL^fUyDtgM)z^$uI#1ACMa( zZu1^8;CW-@(`dzD{NW(mngcwsi=qvr^-o^9YN>AaA^4@=C-=)SkLGF5ot&9vDBs5~ zmZJ4^-a75xGg8Ow@4uV9;)Ct=MTd@;7OXkG`NiAwTfS7Eoxb$xzGHuE&YR!0i`x6# z`GNH}-5J$O?CYHU*;ktvt^Rs^#kKe1`xH2uKG?rmJO98RrMf5Tz0>Ot-v41?vw5rg ztcCy6zb4Q6UAO4C>-qHRz8yaMefBE9)&AcN)H31ty+3z9yMJdDJ)iXN>S^^y)$irM za!-s;-?GJ;y*@r*CF|$tdbaiR?%%BSQscjt@A5}|g5U1zOMzkV>mxHb{edEvM)8&o zjW;%wcoPA~+uLggIS(iZxH`_>*p=`Ko&It#TnAgj1WdyGNHHqH-~o!! z85?;U40u=~+cX834VYP_H!vA*U;)NxNT1we31O{8_9sJ62wIEi&r@nOr#eJ6A-Ui8nt)h!oOZME8F{xiPcA8a?ty_BsMRTr!B z+Ar(Gq1bZ8{jFv2_tc+CH+|D}Et#KfJ@K>X=9g*r54^qG`E>W0*i@&&<9Q7NK&ABy zzS%6Vy{Qv=VgK}{2KKK1m7hIZ{{Q^mbBo-TIoDq)XYvmC^@~oCl!7;sHgX;?;Bg7m zovi8fP5$)CV2fi9yEyJKZ}Z^57OchWJoo$P*kx2YH(fcwB)o6u_R)CU>JjbjCvOlJ0ZY4jsJM zpS{~tD@x_xK+z07fbD?*HuIb=Wxb8t}$PJRCC|!PbZ#9&o4VEbFw`yX7$G< zGxOe`-?nb*JySdHU8i&Hd)7?fv-`)M_b&dgBZYt=|K$IiwZ^*fzNcRlXMKG5Z_WJ& ze@e2e?ADhqmYZ@;=WFWs6>}zbgQ95v!yMb^+s^atyY;yzit9J0^!s4*S>=qi*RL#c QyZo}gR&}5F)QKAB0q_1N0RR91 literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Blue/_aseprite/hurt.aseprite b/assets/Prototype_Character/Blue/_aseprite/hurt.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..b60713d663d42033b7f36e11b83a0af094d6f5cc GIT binary patch literal 1182 zcmbQo!ocugDHDSNLjn*pGB7Yq17ZYVWMBjdG6(=6EkFSi1H-S6JYZXSfEI`WF}niT zN+jzPfNV(&QHTOtRqOw}Kqd>w^^8EC42XlG_&-?MJ;>9A56Dtb0NTlL_3G9Ca$uFr zKoSPrfDAT<;EcqS{BnfasPM%9ia;3_1!T1#Q#l!&GZKpulS@*I5X#riT=PF1D9Ne- zB!J*Q$UZOtg>uE5w>J$L84NfM82oqJ(7?vBAibE8-7;y>+jrL)7!Gu-Vv3kJ6Udm* z!4R?Wu012egGq1yv)}p3==t>fD_@{7tU$kUF^D1=%fsLTG&VUQAz?%4)_?vyX$p?I zJ7kYHV$AQHghcKUyy0T+lrGeKkee}y?4Pyd&f)mLwUIssq8^Z&+ z4jJ&gO|@yQ2zbAsg5@Z8hkDVM#|NWpGG9q7-ekie9eHooJ|}t2)V|9Pe*^}~T*>dh z9ou%C`T5_Pvr3PyNZI_feY7H?o4Iz~^V8wAXCwdq_+PqzYg=E z=o}O~#38PQ!Qls{KZZ>{40}tZ=y4=pzZZyj{eEn zPdeYrwI2lZhxC60d<-1Gm7?e?QqFKm0lciW5CA5z%6wZebx_N`Ib_oUuu zK725^Iv@?Z)W_};D1f=vNQQM{G0Z) LJyrM8WP1Ss9;tZ5 literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Blue/_aseprite/idle.aseprite b/assets/Prototype_Character/Blue/_aseprite/idle.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..89c42f32d70eee4eb26d0f36bfcb3a5673c09f0a GIT binary patch literal 1155 zcmZo>VPJT$l!-xsApwXP85kIJKm;5xGBASq0t^ChF={e0m>3v-edGb#$^$f542ank zP@Di1Q2=5|3{jvU5ZJ0(|K|nLEFjl20(mkZ4vOOcU}^UtPZvHQOF;o>C&SgNSO3d_ zRWbuf7;pnJ*cgH{5>xWa5o)8t6aOm$WmpuD)q+grWN^+%EJ{o+Ni9MsUpsTn|8Srr zs{)V!g8v}*0iexMggA35uYd;>O&31}E6ieY{rH-;VL zJ>D10^brO8Xr*Bd- z_~n1S-9$9`&*!(68#~U&Z0_hUd=ahmzhLM8Vtc!1(eK{hJ@!7~8gutSt3Ce`VrSfb zyxO=R?o4R$v*M7r_0uDkztTLv;<>&E*X!i$U+aHNv|DVqQA~HD{8#Ho?!V4|{qkjg z^_9!@SA+fk8~-*pjt5dTd&0l`7vX9x-DvuE{^x)5H|uZuI^&=JyIqgJ*O%=(^Yrr- zyHmgJzxkkQG#i*Wetl#Hr!!C_4u$x64v!x$l=$%l$Iorf!v;KGjOH$?XgD{aKktI5 zmGkr$VsqD4HoSORHOcuzJG0e={-U5K!gFFz@qaD)yGnzp_}{mR(*dT(`o6NL>;9?!fb@^d06~~};{X5v literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Blue/_aseprite/walk.aseprite b/assets/Prototype_Character/Blue/_aseprite/walk.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..00d4bbc527646f34697c9b0f02b60a893f2376d7 GIT binary patch literal 1984 zcmX@W&cN_sDGP%FLjn*pGB7Yq17ZYVWMBjdG6(=6EkG6%1H-S6JYZXSfEI`WF}niT zN+jzPfNV(&QHTOtRqOw}Kqd>w^^8EC42XlG_&-?MJ;>9A56Dtb0NTlL_3G9Ca$uFr zKoSPrfDAT<;EcqS{BnfasPM%9ia;3_1!T1#Q#l!&GZKpulS@*I5X#riT=PF1D9Ne- zB!J*Q$UZOtg>uE5w>J$L84NfM82oqJ(7?vBAibE8-7;y>*)!7_7!GuFFhpFu3uH`K z#T2peu012egGq1yv)}p3c&4`Qw=d8bR-oUw7(|hbpy>QKKSRm1D~0dChA}a41HFN8BR7K|$c@u3 z@-`drxNPPL4&gb#^Y;Z?Y@_5Zm+c8=QMcG)`!;i4;J2%pR6nWxsl~;IQ}$)eKD|O| z;4%d(O2=4F+P>Fz+MR0NNSo{F{7uEtwgIIH`Jv}$gxcI= zy?*BRy}c`Mt@)nt)mVhFmmEW8HuJHrv#;oB#h5yY{a1 zyQ_QH&lkr(x_e~%d|>kU^^qBz(m>HT6k_N#G={i<5sru<9tK}f429k1J!Bwo+Q_HT zioy89LAEs)BzLhy8%PT*5$-FzyGkoa<=myeF*TL3Da)tuw;leta1qCqKQF5$FU)GH zmwWGPVOOjyBmdf9`|&@AQ-3|%S9B)-TflL}3f?E@?($z_v40o!QMo@mVgHQPzj|uc zmFF`Zzw_HuMQFcke$~1ke))mNBbdL>*#Bzds+pHRf4}K*e(pEz-eS%DYX&+k2+BZrc|ub(8l;Lm*taZVY zN;{LJuhaLwC;P|TSDU}Gb%%bvm+ZVSVE?)YSH)J_h}N?0-26Lj|JEO2>%aXI-LpRF zX1%Tb(O>T?RjwcU9JD&`;ogM-yVpHfx%YGa`v2SREUd`-s%QAW?z%$7zmU4Dv#U<| z_n+IpD*w*mX+N(g?=3ER$1I?|mvQP4jG>1FG7ys?&ud@?%40}-(99drC>hbDJA?Oy z`vKEqU;NrbIwMZ`zgp5C;^$SFcq=FWRF}io_kH4-T53OjU*2kXJUws5<7Am%*G|~W zo_l|%z5nM~$yZhq?;G#{{ok#KwXU)^z zcRBis;k5&%pTcd!42!?~iOzrcr*FN<-tBS4KW(?B{w;rh_@|8hM30{Ki@%Oe`dRN; e|I*g~OZT&NY5(fiHLtw+Xa2MQ)-_AcL<0chLBSIM literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Blue/_shadow/shadow_death.png b/assets/Prototype_Character/Blue/_shadow/shadow_death.png new file mode 100644 index 0000000000000000000000000000000000000000..15b229dbcfc102d045a9c06d7ad62a016bf6c571 GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|R(ZNOhE&XX zd)tt&*?@;N;8bi?*DCGyG*??!{mZt&OohpdzCY{uud3qzZ4*#A7%0l=_TN8$J?!(l zA9de~OFuokp0>Sg#XrVC)^`)`&$@dl(e`uVFT*CQboVlt76t}|?V8hzz4!iddE#F! zAF%6|y4L;L0|{;ydOtD36`j!iTJ-Kk>W%s*%lGon_$s&HiCuz$vPPjD0|U!X#z2l7 Ufz#I~8G@91y85}Sb4q9e0E0ePxc~qF literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Blue/_shadow/shadow_static.png b/assets/Prototype_Character/Blue/_shadow/shadow_static.png new file mode 100644 index 0000000000000000000000000000000000000000..ef0287743b797814f45e994f057104ba55159ee5 GIT binary patch literal 5123 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 AvH$=8 literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Blue/death.png b/assets/Prototype_Character/Blue/death.png new file mode 100644 index 0000000000000000000000000000000000000000..8d893ae6e06cbae42d2a447b2b094543344e95a7 GIT binary patch literal 932 zcmV;V16%xwP)Px&UP(kjRCt{2-7#+RFcb&yKc6~ALPbJs5lc5t0O9~#fPpJ;2L|rI6&QK}4uIeU zY%GWkRRUGYo^L2GI3X#HV~2$Pe^MnH(mMFr&yo6^0z^baL_|bHM5Sq%{Lt`Y5T+ep z?>9!j?=O4H`vb#|LFkR9>F)s{gOE?|0EpImC-(jMaw>c=f00oDlASNdg2InV< z@eDXFXYeQUyVqrS<{D%>1#1T&`JvSgS6|C{t~#SCYmluJ{7>hk;m08KC-d8gKkV+# zkz;aR)+n{5_1=knl_@I)o5+=d&X*JX$sFg)=`ILfvyImEJ=Y!bN^6i-0M`3WW4N4! z0MNS50>9gfog-5)SS=91ehSi^QLa;uwkxnSxQxkW$VgXT@W{Hm@x`~PW@$Fi)D*2NySEc#H2b+ zs@1BZjvF_OW`LC6d7~+U)gn5cESN1*Ro0@ssW^84*85Fk2!xOi+aDwN{sI7aqiNJ& zGB{Rs*;lh^8XNW01E zWZN@9r~P2H&`!mqInNNpduwO>LiCL$stA|fIpA|fIpA|fK96k30+XZ^oB4-2!Z zQgivLv@7GPR&)6(v;y$xVcBIpueoexq+Tmql%z(|c1o&ay?9-D0)6+B79$W3}{{sgg z1hT84{mESWk?p3U(XR{!zuSxLRjXV3h5mtq)ZQoe4A`p0PYZUm9qRhH`lPzGZ@fPM zxXlI^8}iosO(R$>qB?uE-e1z6%k|*d?Gq6Z5fKrQBmV*7ra8IjJ`Px&AW1|)RCt{2oUv{iK@f(AC?%3PkVA(eB@~hJ28ajX72HyCH5E`8DikRxaOsvJ zFW?72<7<_wnH=R>roZ2%QC7_xkxhR>n4||I`t)-E19rsQccIycaK2Ui(xF z;(*(AGf#=_X6wL1ed}*%|MnUxFJD{+abP_krS>n*0vzqXe)|%$``*raFyv_8`20O) z-})PIFcf`DJiNx!i8c)m`vW4fw;*EwAk0SYG0izJf30SYD$=>!qy_~YciY-zyx z0g^X!@X-x-w&ihtfNr?6P3m7HkYJ4fizL(+>jh@FGb^2-Y6Ms$p}trz(73u5`4Px%h)G02RCt{2oH1&{FcgOWl#YgwK*$!lWa|xr50ES53b})a+`(7y&e@Y%0l8x04E2;^F=jVZ4eD7C|+NTD;iEPTWuO6 zv`>5r&^ltO&I6wT#}b{7nGY*}%D>`Y_ay3_po&;8W$g z0Lnh#CV(gi0_TNKo}$dtuZDCH1a>+Net>HQ<_Da)>HGj2H9rik{9bns@Hh6f%@pK3}6^0PNc^2qv=!X)Zbqr9OkY%0%`0j|~ z>3}j%gRcgC3*Zz4Rp<15d3A+G7hpV^dJ{kt1OXTc2~8`r5csx#aejc<@`dzmpAbR_ zA%qY@2qA=AE6fjYt-$<%GdG$t00?}3fZlm^et;rcu7jT+ zpxamH2iWelMZ6T&)sBJ2Q4xy1?Gr)>A%t8#A3qiZZd{0FoB#j-07*qoM6N<$f`-01 AUjP6A literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Blue/walk.png b/assets/Prototype_Character/Blue/walk.png new file mode 100644 index 0000000000000000000000000000000000000000..b6a60c17cd6a603feb5cf470d9119953e8037800 GIT binary patch literal 1076 zcmeAS@N?(olHy`uVBq!ia0vp^4M3d0!3HF+R#kZdDaPU;cPEB*=VV?oFfhOKba4!+ znDch7W8NzRfwuPL+g+U2w(N4gTXLK;ZYuK^rr3hkdyTB+FGM50FxfIR36~sSc8X7t zRWbE^bcnE|dFJF!+hfoEt1L-yNvk`5r7-B28WYeCC8shm;Y`b=e#@qo$r=U(S3eTJ?ZE3BCtJg1U zb%&q-qAU-7*mx}E&GNo$J491@OMmUmuXz+zznta4!q$AtuEX!5FVynxU;Vafg8p-R zwS=GlynYtFyUPB#cHf-vZ9DywF5iFcsUUs2!|%eigsVRg%6aSIa+z^)+S*;ZAM>tlQ$+e`PRzAf)|A@Sn2Xpc+r$spH*4(`Q2;l ztbQ$yQE1Yb`>RHC-52-m4lcqQuRY)Gv?@MEF7A1Q*+TClzDZZ_Yco2rD@>KD)ztg) z-K!yhQKSF&182QJsb8+e4WD+XEB#IE*G}m^+;{5nzE8~^9cKAi+Zz{t6|SAyzWajy zY?i!xEB<_6BgGJGBY3gE#E!e+*drc0CWWKd=I7sYdH!;bQ%B_Db+QcGsyTOXHLy&t zeeg!_fO%P^$;C?dDeu>3)i*AjG~xGwrSf~jI4<%v?6qV3y+Bkzp7Diu{NK6E$NC-5 zdNWS+W4iH~VYgjewupiF=_Gq68@6W`H_V#O@#lpNQ_Ly*m@QU&d8J+p-OPylgc2Zd z&PIh_o`SDFU%n`xT3O-s^=FX$t6N{SnmNAyT=_y=)#38Dr`Ln#&!6-!L#x?m7E^)l zjw|wK?;V`(;`OU~YWx3d&$xFROufAYn5Le;da#P;w2+NV@5{1n0XO&AhA_PO%IVX# z*zn@z?o%#TmezfgIdE^^o~-W?YDXC*`i@;?68t08cT8*BOtrsu9~a(#sdC}D!5uS( zeXqYb#lEh6`F>Ar&55_Sb9cwuz4pHS_O`R$1jUQ@vspCwcIU57{`sn58{>zW4c{t1 zKaHu)WxD>WI#>R@vu1Pl^6GId((2+ o%i%IRueI+s=hy7;U|Y?mu!%rr>mdKI;Vst04^`}$N&HU literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Default/_aseprite/death.aseprite b/assets/Prototype_Character/Default/_aseprite/death.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..91376989847a6c8d0f1c12bfb1edf5708de7219e GIT binary patch literal 1596 zcmcCvV_~;dPX2m2E;*8{2wgs9^~o52V^NI0PSSBdiCmmIj~A* zAPEC*Kn5E_a7JQEemO#IRCwZlMW76e0`?3w8d3o&Fzazy;#ZOwgGApP^*hl|qmQSb>4T!N84Vm;i$h$c+)V zc@H`8yf*S_jOZ3@5EbZQFm8IMJjruz4AY;GO-on2`dY)kd{Xgtoot!1-{RbtrWWNd zx9&Y9ajZU0UjE|4>UFN|{f`r(k8M8jEIsPi&U434cUj-6u|IEq_gL|l;|KC{^mlyo zsK4X9tof<*&bxn-OFn<+ju&8Y{2{+B%>ThJrMf5T_ZIJexH$Z=wRH6|$zS!M_RDsz zS2xnDvYTCeEcI9DC)v#VKjT>(1zN1OJ$paTez*RM3bD`Yb8H3gSN~rV*YRi9qNR5& zfBbIp)0=VsmvP1SKj+>BYrRhR>UN}_+0&8Z(E?z4`Sp<*ob*8PN~0)ChejD2N|cF! zqwMXqgPaEx1Y8|wZ|q9=pE{Fi(V|W5I%gW(<=?!W{^{zg%jNeTye`e&E_t_mpE!fU z;>*2TZ@F(@8)>{tyjGvVfvx^VP~`V}+)jTv7%o90lZ}BNDKbSEJV22-V$J>iP)l7_Blq~hETED8`?b|h<_MDOZ{;)d5_LB8CEkB(vQ|hc+1e`cl_TMUA z_U`m2*G;p}MHR7~$)5Om>86$E@?U&=cjVKVEx?$rIaZB>QXeKJL(s#xoYuCw@}!#?MC10|q=Ufx445oxaJRUKwn0 z>|qzjJ?5?QS7NVTE}fJ7{MFIC?5jTCF99=o$<#ZM@zI;pc1`~$&aj}_K4;~+>i2B7 zTSOjQgGUY@O5}KgBj+IJVFMo5y*|q{VzrgG#4c1(UShoMhr*9gRoAkN;1l%=x)1gr zIrng4P|dl+FEf2NsFwInzd1*4`4748Pjj~cW9ZrZniog6iuIo<-BW859eetosa^M- z)Vux4uVdb z;p!{rPPhVVf35V|-3Ru)&J0}r^2`2xJ?~jhW~N&M E00}=Tg8%>k literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Default/_aseprite/hurt.aseprite b/assets/Prototype_Character/Default/_aseprite/hurt.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..5a966946d12a44171ff67a20f467b1f5a690e76f GIT binary patch literal 1176 zcmbQi!ocugDHDSNLjn*pGB7Yq17ZYVWMBjdG6(=6EkFSi1H-S6JYZXSfEI`WF}niT zN+jzPfNV(&QHTOtRqOw}Kqd>w^^8EC42XlG_&-?MJ;>9A56Dtb0NTlL_3G9Ca$uFr zKoSPrfDAT<;EcqS{BnfasPM%9ia;3_1!T1#Q#l!&GZKpulS@*I5X#riT=PF1D9Ne- zB!J*Q$UZOtg>uE5w>J$L84NfM82oqJ(7?vBAibE8-7;y>+jrL)7!Gu-Vv3kJ6Udm* z!4R?Wu012egGq1yv)}p3==t>fD_@{7tU$kUF^D1=%fsLTG&VUQAz?%4)_?vyX$p?I zJ7kYHV$AQHghcKUyy0T+lrGeKkee}y?4Pyd&f)mLwUIssq8^Z&+ z4jJ&gO|@yQ2zbAsg5@Z8hkDVM#|NWpGG9q7-ekie9eHooJ|}t2)V|9Pe*^}~T*>dh z9ou%C`T5_Pvr3PyNZI_feY7H?o4Iz~^V8wAXCwdq_+PqzYg=3rCfkyS?0=qtlo58ETzw z-Z1g$+Lzj2CI&sTknNKfJ?i-P+YX!53Y&s^->>T^`K>)T`Mz>r*H()Q&#U&;UcEl& zYXATE@6p*75#k>XG_Lb9fAThJ;pLg!X(xuyNe{0P5Q2EEMT;2c3 z#qQAe`Fo#Vi)i2L{xRyi`kzkcuh$;iuDv%cE4R{n&+_)e=|Uf;oHx+lt3L6DyxX_6 rYoE=(C)u2C_Ct)lW`*hh6*DtiemC&SgNSO3d_ zRWbuf7;pnJ*cgH{5>xWa5o)8t6aOm$WmpuD)q+grWN^+%EJ{o+Ni9MsUpsTn|8Srr zs{)V!g8v}*0iexMggA35uYd=55@31}E6ieY{rH-_Ej zJ!HUh#>l5}mzXkxwt@s>8q3qekE-U#uFhJ@^;6~lj_2-gUQN2TZwBA-?VHpt{Qdsf zc~+O@_B7kT8k#h0}c{}=80SFpSP=d|7P*VemlmW<%}_V6tG??ZRw;;$a4 zH4J|{^Yw~7_jES;n+D&zcp_dyWZUt3U*bP1*Sggv=|(BrzbZe{|7HGf$(Q!$GnV|% zS~l-r`n&sQ>TLg>|L`&9;{RZvhNJhs)Svuaf3kMs+v8v4-_F1P|K?w#>91G*)_DD& zLG5UL(o|r&`1O$)oWwx!Hx#1gAv|ihP@={c95n|y4;%1&F`BDXvbOI+O6#S*3ukUT zXn*mwYEtvb!;G>o7SBm}%91|))6uU+wIL#nv+D1~8-?_Kyng(*x{_V7u9Drg#CIM4 zKIi^?d`;%_+Va2Kf5{#;*&sFN@MZSrhwjP6&uuKdGyD6QTM2tFb(rQq74AGzA#wJ< zsQst(FUH^2DgRnr@BZh+f93UGpE>qM{abJH_0Rpw_dnO???3fixlU=K1pnfn_2+-@ jf4=XAb^lNMV>9plk7@q>=y%iil|^0mPvtA`b7=wq|0a57 literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Default/_aseprite/walk.aseprite b/assets/Prototype_Character/Default/_aseprite/walk.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..20e608c45f9b0fffd58a77a666f722b23905be77 GIT binary patch literal 1959 zcmZ3^&cN_sDGP%FLjn*pGB7Yq17ZYVWMBjdG6(=6EkFtr1H-S6JYZXSfEI`WF}niT zN+jzPfNV(&QHTOtRqOw}Kqd>w^^8EC42XlG_&-?MJ;>9A56Dtb0NTlL_3G9Ca$uFr zKoSPrfDAT<;EcqS{BnfasPM%9ia;3_1!T1#Q#l!&GZKpulS@*I5X#riT=PF1D9Ne- zB!J*Q$UZOtg>uE5w>J$L84NfM82oqJ(7?vBAibE8-7;y>*)!7_7!GuFFhpFu3uH`K z#T2peu012egGq1yv)}p3c&4`Qw=d8bR-oUw7(|hbpy>QKKSRm1D}^t>hA}a41HFN8BR7K|$c@u3 z@*XnaaoNlhtZ*%<<&vmCk3;&BE-mllU*9a-BLDM)=f6(*X|G+{y(3F@$L!bQS~+iT z+jgF3y>sWypZ_7^nU0NEeRI0|E4z1(yH=;4cvOB>hVA;dv<1Ho+*~!&aCOBV?sqdk zU);O$-k$Fd&Kghkn!8?Z&#li}I#SnLL~Z}I_K|3|joXvznCZ_y@OOXix4HZ)bl$tY z?O|q9z1CiBUjFWG{r6w_-u%y2O5FRGZOOBBdEVPCKk5ZG>TX^9cl!IR*Y^yrf09!= zKbJAuTIj<}V8ZzIkr|xEKoK|;;^rwdZn%I^jffi_24Bp$ImE@`)*#vaC$UiGdUmL& zW>D><`Z>>+bgeFq`u(EBqEBm9z?1ifo!eG<{wuAnYkfTPY=O?WAf$@a7yX4n++P#nZIPG%Iryq5l7Wo{q7=X3NI!*2(4 zzwz6LnFiO%>HOaLx$f!38v8fTi*3GzUfuEUlcjh1`#JxM>o>^f#R0X|UAes{;`6_F zl?3sqikjHs9IMc8su(Bv0=vhz` zB9Z(&>fLj@Bu)FbySMnN!~^4H!|nYcCP8cN`jtzbDgRbkThp36^Yz1sKWx{pJ+YP7 zw~tw)*ln52-+X`i-1iaT#%UYHyB=FVmaBXIYt6j{5#jGuw}oh(&E}3<8}WSQ3*!@O z&q~*={t>wT+&|Gd*Ehb|KXZQ4-|04!vL{xqToqlJztUu#ZpEuNf2?2cS1x}suj}En z`}^7teqR1L*mSj({onF`rL~Xc`p)c^oIO`KJO7gdS84wcjGEiZr*qA!;mn`CdP9v-WuzI0E4#Ig1y!4)4*f7Du8%ujO*Bi@h-9O2$`1{RnhVZY7?4|t{_Zt5ut$k(u%JTkS z;fiPf=DR3;jQ{fNx!3zYVzoWC1>L`{_J^Jmvj1-yy<^{HZU6kY*Mn-k^KbsY`@6t) pxxb3g--X*NH>Uju=9H-Y>d#m&z5QR<7qa{3yl?;2WhJFv0RZbCvp@g< literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Default/_shadow/shadow_death.png b/assets/Prototype_Character/Default/_shadow/shadow_death.png new file mode 100644 index 0000000000000000000000000000000000000000..15b229dbcfc102d045a9c06d7ad62a016bf6c571 GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|R(ZNOhE&XX zd)tt&*?@;N;8bi?*DCGyG*??!{mZt&OohpdzCY{uud3qzZ4*#A7%0l=_TN8$J?!(l zA9de~OFuokp0>Sg#XrVC)^`)`&$@dl(e`uVFT*CQboVlt76t}|?V8hzz4!iddE#F! zAF%6|y4L;L0|{;ydOtD36`j!iTJ-Kk>W%s*%lGon_$s&HiCuz$vPPjD0|U!X#z2l7 Ufz#I~8G@91y85}Sb4q9e0E0ePxc~qF literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Default/_shadow/shadow_static.png b/assets/Prototype_Character/Default/_shadow/shadow_static.png new file mode 100644 index 0000000000000000000000000000000000000000..ef0287743b797814f45e994f057104ba55159ee5 GIT binary patch literal 5123 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 AvH$=8 literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Default/death.png b/assets/Prototype_Character/Default/death.png new file mode 100644 index 0000000000000000000000000000000000000000..9f53bd816b60a623765e29ea90a2a061bd2f14f5 GIT binary patch literal 911 zcmV;A191F_P)Px&Nl8RORCt{2-Rp_!Koke?&G&sJx&B?p-b!BPV-8VnP0wx4fT`u+aNz2|(p5|J<% zE4!W1WDYVJf|Wv06vi}PE+`7)w8NR}xan)Q^T9jiAgKX3UQRt)Q9_i>@ZuG~GjQws z***qU#r>-H*$zdfa-i}_iZPgHo!I#{KDZbHIsBD%fEXG#tF_T{5huWSIw=johuuDX zEZesBw;-ad&33u9XldP5}PQWg&Rxd0E?%Iq;-J$+W-qed$oF)JeGnEF7G} zjjaV5RqeAVjB{7uVYknn6Oow-A=^GtJ~BLt2Ff{r6+6EvJOjmgea=hVDK;lXS^Gqk zwqHjm1^@s600000000000001h5>o%INBzG&PYbi!QgivPq$}f4|8IxX06e={c3t;t zu3H_U*T@p3v_{f!Na|v}zbfqk;1E>2k+&&r$vw71uhamPH5$YLji;0Jbz6nGZTq1H z)1D5%@p9^sPYHK6tGao$Nsl@}3{Oqu9#ieh|A52p|9Y^hurOaPtp99@xGIPJe*fgw zs&%b>=^T(A*r%jed(SoVrU#)a^buvZs~6iRqRzFiy*_{_yBcgP$m6wWTxT!V`*ZlS lnGc$79{>OV0000i`3EdOOT|A<=`a8Q002ovPDHLkV1m8Zp(+3X literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Default/hurt.png b/assets/Prototype_Character/Default/hurt.png new file mode 100644 index 0000000000000000000000000000000000000000..b7f92a619a1aa9343d0ebdd8f98367ee7edbb604 GIT binary patch literal 853 zcmV-b1FHOqP)Px&4@pEpRCt{2oI#4>Koo|b!yF(QnG19~FlP|2A_TWtbVK7xSGp2G7j7G&FEERN zxqx_vhE^}o7|jiw&Adz{sq~~?tP;}y2SR*h>UGL`uL|GaMFao<008hwX-IDBb-EXw zeY4w&`n#q7uM+2z*;Ly$9*!=y2cg&LUiv|xZ(NhHf8zmQn!cBMe~w~lS0Y*&|0kjU zFOr*@zDMC8Ox}lsFgGAJY55*4=Zob1a=yq7NLfS!Kt!~9UTLpg@$mGhy*5QW0CmA= z066>ZUk|x9wiRXQYruWK9rn34wwd}boiTfz?uDoNIf^NIk5cXaH$TY(;UK*FlGy8X zFFe&xQB249m}+l(?IaH@=Zn(*jaLEA_V+*Ta`tl+TTh0Z?Z4mM=j^8_Hco~{-x5!+ z$!w}!2FJq@5!u@ivHyM$XqUm4_1f6KQ4UZ$R@Z&?+U7<`?OR>!@@}2v;jmyeL!8b0Yq1-4!f)g=#bU~h${XpuDfq8TO6PzgZGbC^v;kEy-Uu~) zFQp4`P0ROEx`3*P1rp=oC>8gU+0?2+Or_;}^s-*3;=UgQBL9_kf}7p8;Uf8c1XUxzA_?`ydVzEakt zG3V`_hrPE9MA$#rhq8Ax@J8iv9?|l+$)IwqgGD+bsj+Uw6;=UnnR02CDZH2XAFSr( z>%DKQzVuK1{2fo4=6-KVp3=^!q@=V+-E@MC$!wq37rxz1)UVoaSKDiwU^esGXB+9+Z~V-kIvUTjW?SOe z9gS;U%e=w*4ZB7F^8uUldna%0&F}sBd4Feyy22~28?xV;Z>B`?yG!~VKlQpN*W&8Q z-JBcbnavr$ed>Sfd?oh4UY_>FJkiM;OrI@Q?!LOIaMr4$x%I(oHfmmr`@QiCLw|16K2Uyp5}6Q1EZa5MX&As$z2PjL?O=|G}-3YU_T!#Nm(wUzw|}{r<(c zFylXW|7leh`Nr4rdFHv^OJ81pEi``>e?jTax`s5S9d!;l>O1NfcGMj>uA_5hrrtIF z3qB5Celfn&IkQea;p}IdpT8LMewY1ixUg><(EID94lk&zn&ol-t+1e=pzWUf2Wo{U UdoD;31tvlUPgg&ebxsLQ07LUL0ssI2 literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Default/walk.png b/assets/Prototype_Character/Default/walk.png new file mode 100644 index 0000000000000000000000000000000000000000..a08623dfc90b4f06241aa7dc6c99b6cf2b5ca70d GIT binary patch literal 1079 zcmV-71jze|P)Px&@kvBMRCt{2ol$P$Fc5}+R(k-{<_0aj1mXfK?ZaN65;th29s{YpK_o8Fhf2Kw z;u5930f}-0_5m3=sk4xYJx;v;_e6~v&+i$WiD7Jj5JCtcgb+dqA%y%FOeFubn9UEa z_RVf9y587GN&nBT|FD?N5BJNZ`^_8&jX{X%fYkJN4gkZ|^|bbPSya+Xbz%q(0MgMf z3V^2gwziPOa)7Jvwl+;0r22nI4R~2SJ6~%s1P7#|UmO4rPmj*mTEuh!kdA&)0DOFX zUep$nm<~uuf2SP4Bp_GU({F0Rsfp*v41jSv?;+%a4YIzW2Hu06--GYuC5+ z<~V3Qg@n*A4uGa%KCW|dU(JA13c#8Tkr;x0LI@#*5JCtcgb+dqA%qY@2qAgD0S4Cq7b5^hBc* zu^o^~jlL8g`g;XHEC=kXee@|8NJGCU2iRA80Q%FOsgNy;3j1n*vI79#U*FUo17qt4 zj^*J#^os)E{q+sM{`^*TfGr*XvT0UxfX3zwAsis3ssCMgoz6Ri*fr|f4lw-=0bdS~ z;ByST6o+ciERP22LAcIywNGHIAT(p((l14n1eyV`2SURE=K8j1vrmBro85L|XZu5# zjr!{E>@c$oYTpU~od9U&0nPe&ty#d{EX&vtV;R_N2w}!VVn^p0U?^X>Xbt)$JUl(N zem@Wj&08KJgb+dqA%qY@2qAOvshr&H>btGYH_iPCY_e%~EKyTPM1ATPKLxmsF9#4b2+lb`qi;i-{*Z*w z@3lPKN4atvmpUNyEEMZrA4-4c(gT#-){HS_Fkv}y!80?m>XnB^!J(p%A&$L z&sC0ZztjPtZD=0f-nu9gpzjd+dj-HL;+2jscR2unN&wcbZ|lv`zA_h}Z{Pa8~;dPX2m2E;*8{2wgs9^~o52V^NI0PSSBdiCmmIj~A* zAPEC*Kn5E_a7JQEemO#IRCwZlMW76e0`?3w8d3o&Fzazy;#ZOwgGApP^*hl|m+9fUyDtgM)z^$uI#1ACMa( zPVzDt^4v*V!Su0NT{M7^^}}OV<|#a<4qtj8Yi;%+GSh8S*zB`UB-WewO-t=PocBM; zaaYj0uZ2qjB@h4j{`{x1Rn7UWFFq8?n>hbp z|LVBO_pdI;T+gQ$pWETH-)o=pTkHSb3LH%V$FKdl`#JqPtL*ute^b}0KdOE||0?&y z`0y=Ttl8`1FPvok99_@$e%}3?wO*;RYwmUY$ln_@VfkWU2>klU3{HKZsHIV?r9)$l z4JFn@z_IrB+Ck0(3IeWbK#@Jzj!Y`>%2?( z?E+35ieCe(&t0jF+~-ktInw)FL#1xfz7VskKY8!on;T&_XZN|5M}2o06k7zG{yDs@ z^!ptYxoSzhzIS3>*FSfkv;P0*pFX=NY?*WYm2$?mHMbuxp-&X0;Eke726M?B&CiOMlF1zkGRCg7=GE`im`X{XU-Ue|x-AGrvU?=w^^8EC42XlG_&-?MJ;>9A56Dtb0NTlL_3G9Ca$uFr zKoSPrfDAT<;EcqS{BnfasPM%9ia;3_1!T1#Q#l!&GZKpulS@*I5X#riT=PF1D9Ne- zB!J*Q$UZOtg>uE5w>J$L84NfM82oqJ(7?vBAibE8-7;y>+jrL)7!Gu-Vv3kJ6Udm* z!4R?Wu012egGq1yv)}p3==t>fD_@{7tU$kUF^D1=%fsLTG&VUQAz?%4)_?vyX$p?I zJ7kYHV$AQHghcKUyy0T+lrGeKkee}y?4Pyd&f)mLwUIssq8^Z&+ z4jJ&gO|@yQ2zbAsg5@Z8hkDVM#|NWpGG9q7-ekie9eHooJ|}t2)V|9Pe*^}~T*>dh z9ou%C`T5_Pvr3PyNZI_feY7H?o4Iz~^V8wAXCwdq_+PqzYg={t7o9BoP3~y+-T5EX`L0Iyf8SCSy=v=I-H-F+ z_~v^a3AQ`D=gT~oq)+qccCuS(~cck@BC%!h^7{wz*gCirZA{JD^yzE2$IU0?Bs J_+)d!hgU literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Green/_aseprite/idle.aseprite b/assets/Prototype_Character/Green/_aseprite/idle.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..f207ebaefdb942237cfe031351b461196cf56803 GIT binary patch literal 1150 zcmbm>3v-edGb#$^$f542ank zP@Di1Q2=5|3{jvU5ZJ0(|K|nLEFjl20(mkZ4vOOcU}^UtPZvHQOF;o>C&SgNSO3d_ zRWbuf7;pnJ*cgH{5>xWa5o)8t6aOm$WmpuD)q+grWN^+%EJ{o+Ni9MsUpsTn|8Srr zs{)V!g8v}*0iexMggA35uYd<8a)31}E6ieY{rH-??$ zJ>yeW{=8zklZ6{(DCL&fhnGm7C0NJenYXuV#b(nYSN9 zdGCl9U!AwJ@6_wFLcc?dpUU?BX?04he&hRJ)c#WX74>V2Og}BIbN}PSg`2e`nuZzx+jsUZHPA#d(4 z2fw@rjvA+fiV{)^7z8gY3fSbtxW)VDqpF#9T3rrj?eCdOt~&N(ed)i>j`K0y9sQ*< ztQYU;MNG l`QQ7+@o(lH|0Dm{=Y9RUgS8)P5B?6ZJevR1JnYc^_W*wIcVYkl literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Green/_aseprite/walk.aseprite b/assets/Prototype_Character/Green/_aseprite/walk.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..bc0b79e64aee491686fe2e0341d26a91d4fe3c23 GIT binary patch literal 1979 zcmdnZ&cN_sDGP%FLjn*pGB7Yq17ZYVWMBjdG6(=6EkHUG1H-S6JYZXSfEI`WF}niT zN+jzPfNV(&QHTOtRqOw}Kqd>w^^8EC42XlG_&-?MJ;>9A56Dtb0NTlL_3G9Ca$uFr zKoSPrfDAT<;EcqS{BnfasPM%9ia;3_1!T1#Q#l!&GZKpulS@*I5X#riT=PF1D9Ne- zB!J*Q$UZOtg>uE5w>J$L84NfM82oqJ(7?vBAibE8-7;y>*)!7_7!GuFFhpFu3uH`K z#T2peu012egGq1yv)}p3c&4`Qw=d8bR-oUw7(|hbpy>QKKSRm1D}}GYhA}a41HFN8BR7K|$c@tu z@*Xnaaox-ltdRHR1k2n8#vO%8jeS0%q0{X4-_P@^K5;PqlWJYWI^o)wQ;Tjt{S_MM z@%7Emou?D--T40H2WNlkxkJex%`3{5y!#;)7Gvuywtw|AgB{zL+smFmZ)M1mwc;D&2p)%ci|PVov>&nb+2T zDtT|aWz))#thI7BrSJdkzFr?)W#E7G!?*a;i7RYNvzH(J-`tVAQugisn(*lFZ9AuZ z7pQ#4^R;z)`&?k!`1O$)oXkLRI25AiB{X`tfU%8;9v%i?SoApXu&kF{**JsbW%ImuAj>+vf`tRZKRcWWrH>cW{-&=1p>wWv*n!e-vR!Lotw@ZKdX71d8NN9{t%O( zHL>#Ul4r`l{j;fWO`iGsVZ(`#_z2j#evqrJoGMT^m{`9%;Bf^c-Hi~yWwtg&E z_x#tIdkZ4M->Ys5(K?&W9k({(`N|i@C)S>ou3P;haQ(S|qI0fqe6xS%{G`9rZ6;+; ztX#P&x-x&I$vWMNS8x7UzuvE0{$gI&!)N#RwIBSv{ByABYAgG{<^M`+AItTf*)KVJ zu5fmK#?c8fGlyX8JcY*&maG%z$kpt?bLaezu19C(elvVzwL1{)U$e}602X1v@QmhsY%KRNtL{;uc!X2-V3 zWgjnIvX6Od^!sMZMbo#ZRdqg=y`jH0wbpI#x$XZrZ8ratZ$Dh9U-Y?n+4etTwLaAk zM87VU-#kaTZl8H{Nv-#J8{4h%?q4XbNzwSVK!P0TU>zx;1K(?$RJ%>dpN!D0Xa literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Green/_shadow/shadow_death.png b/assets/Prototype_Character/Green/_shadow/shadow_death.png new file mode 100644 index 0000000000000000000000000000000000000000..15b229dbcfc102d045a9c06d7ad62a016bf6c571 GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|R(ZNOhE&XX zd)tt&*?@;N;8bi?*DCGyG*??!{mZt&OohpdzCY{uud3qzZ4*#A7%0l=_TN8$J?!(l zA9de~OFuokp0>Sg#XrVC)^`)`&$@dl(e`uVFT*CQboVlt76t}|?V8hzz4!iddE#F! zAF%6|y4L;L0|{;ydOtD36`j!iTJ-Kk>W%s*%lGon_$s&HiCuz$vPPjD0|U!X#z2l7 Ufz#I~8G@91y85}Sb4q9e0E0ePxc~qF literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Green/_shadow/shadow_static.png b/assets/Prototype_Character/Green/_shadow/shadow_static.png new file mode 100644 index 0000000000000000000000000000000000000000..ef0287743b797814f45e994f057104ba55159ee5 GIT binary patch literal 5123 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 AvH$=8 literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Green/death.png b/assets/Prototype_Character/Green/death.png new file mode 100644 index 0000000000000000000000000000000000000000..37316924f378680fc6c569a353bff2d0488b9b04 GIT binary patch literal 925 zcmV;O17iG%P)Px&S4l)cRCt{2-7#+BKoo%C=hYS=k|{`+B2tQU+yEi2kPC1D4#Wkx09S|*H=v`4 zlp>u|U@1=LExcpTcnys089UzmpM*pRF!r1A8%KF#ivR!s0001VCTQ}Z`O_?|dVP1? zH~PMxtaiZWPqTEo9991Auu15IntnI^;9Y2TLKG*hH|8g5)B)w-7q9({PH>rm8y^8L z>knD1CjUI|LZ5Tcr4%fiSz31+tUI8*(|W-5=jwgFg5f#Znx*<^e|`Uw z$NL-OJ+JRyBBFK1+HWxWoP%7Z;94mtiW6C^CKAO-9&lwGrupxxHqfR2X%4dLfZcH) z$T~|ci&eJsz*#>q%G@sh(nUfW2)$y&w8EYX*GIMYd}eE@?amln*V{5h^A5TqCBQ>)WB)EmtSI zJ{z-X|FB7{Q*o^z856MfA{7Gw00000000000DuZs|E;I~-=C`ir2gM8 z>$)K7|NXG)fNK}a?&^BYUF#zCwl2;!YDFEFfe*3Xzplm|pi+=|A#ZnSX{h!7by;;l zRimdkkca165lQokU>`yGZZ~Rh?P>??j{871vos5K;oQZmUAhziJw5d?_qb}``UVd3 zZPwtqw6IuByx+D&UzMXg-rpE&)rQu7s~b3|+WYdF0avy7Rh_NMzSZUG%ZAo|?KNUL zwShhsLQDv3*R00000NkvXXu0mjfpn9%+ literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Green/hurt.png b/assets/Prototype_Character/Green/hurt.png new file mode 100644 index 0000000000000000000000000000000000000000..01d8d61354592d75a5e97cefb29cf220aeab8db1 GIT binary patch literal 883 zcmV-(1C0EMP)Px&ElET{RCt{2oUxADKoEw(e?sQU$D^4;XkmBo{A{n!`my36d7lr#j3CW(tJN9~aX2t$25dZ)H0KhB7 zkzCZM)tU_wYReXrz}Wv=>5Wvpa_oHV zZ=_X;@JjJU@OOczo;Q+OXVDY*gzZclEWuA^*vb zm3!~KC=Rq+?W1RdMztPtSNC@TZFlkNFG^t)2d3jyX8*=nfTR8AFP~EOwZADm7;?1# z_~Bj3?(YKQU}*G~xO?@718o}Yb-F}U+=Phz*P5<24K5~uv45i&pjNEref7A=iI7^i zn)lUXQ~%<50MWkNt2P_(+vjet+DtJX0&33Z$xzZ(zw3XI zHo!Ks<=mn1UK?;sGRJ^J+5lS|(gx(kcp_Bs$}wGlZM>2j(*@*3ERg7Ry0LNJ9}Wst zh^crbmll&CHtuUpSL8p}OmMcGJ2ryfOa7(74*&oF000000001hR|+;zkT^dlkpwnS zkX)n_M4aM}lmD`10s9A-e3^rfuDi3%#{L1i?#?z-|3(4{)(EgjLjAB_V01Goq!Cn& z0E;Bl5AP>%b$w~%-#7?z2GDJwU}Ne0PX-9J8z@MY&ar=h$qk(2{sH270MTiL5-P{g z2;`E=a+goqiW~y!IOi^(v{~6dfFJZ!8UcP#XaoQN0001l{02Yh2WbrV7BT<;002ov JPDHLkV1lNbo5%nF literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Green/idle.png b/assets/Prototype_Character/Green/idle.png new file mode 100644 index 0000000000000000000000000000000000000000..ac79e8871144b7505e23044100148828449fe5a6 GIT binary patch literal 711 zcmV;&0yzDNP)Px%fk{L`RCt{2oUv}gFc5|>RflE>18m(ou<-^6@g%$e50n?k3-lEb;tkjsDs^Bh z1CbiGbOt%4cSw)mqFCNX}n&Le0byg6O-_6ip0!UW_)lQJ7p^0YtX>1c!DxV9$ z_K9RWLRAnXss_By3R-8Uj)Vw;ru{{_qeZ%_*_U$FMrgTNMa#`93cV4!1^WO100000 z00000;9fy~fb;?62ON3m`~VvdmmfgXZH~Y>!Se%%{zs00$^rX43ii!6;X(jW3^+fY z-sh(?`Oo)nRR=UwN8|E?P#ezj1GwGiQrMIqzyf$*1-hF0w)_B-xA{xeH{nv)ItK6| zWR(N(+Y!FiH|@AY6a;y2=D9pyzA*(DB(XCAR6&r4Y6%G!cV-@NZU5^00JY^S>DoR3 z00000000000Jv9>A0T}I`2j~BIzIsQ13ISj`T;g?-@l&`g z4=~~S0hG!EkrbB#pnia~6R00Rs2|YTPLP#l`*s05*AFlU&-DYC(Jb~}Kftul^#g48 t+M?bH*VN7$wx^!x+CBgP006+<^8-pu3&KLJH&g%s002ovPDHLkV1n~~G5!Dm literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Green/walk.png b/assets/Prototype_Character/Green/walk.png new file mode 100644 index 0000000000000000000000000000000000000000..aa3ed79b15433169a8d7669721d17a054f60a3f7 GIT binary patch literal 1085 zcmV-D1j74?P)Px&_en%SRCt{2-92g>K@&scwwf+9_!Rq}|lT&`<4qso+nydM!^JlBF7TF0g2OzinUu`=vDc3w&Fxv| z_fldG08u{qttPjNijSKJUt)M^ma(J2!A2=^zug2^Wnj^ z(5^3M4e#u({Ns5eav|z7zx7>!-T?{!8vv;Z$n8lBk>BbR&~?Qb`YK!vzpm;wu=2(2 z-`t)Zy-)Tfr~JkfU{%(;PB8vn;$rNh9W8My$Qirx^S%kmcYdP@z=syT+{hZAsBw*3j&2le%GvDvlF`y{pe*0+I^Vzm3{b5+(NAA@!cfYbz3-(StU zDr>RNAVTD~z6f+_)VZy)J;&|Z)n7~WA(B4i2LJ#70000000000000000D!(q@=u6z zs?WFKGkEM_9fEKcduf% zfwA)wwdLVH@>@-S=et+B`1MV60MzhAMB}1)HvkfgGlUobTYCOydbN5V;}t^G8g=Ob z;CB1K*8te?xevVE1%#%TM-#o@5mFxl>FrA5ec-bnPAh~+4}iWt0rx>_Ktc?FE7$zS z3qfrx@GN}k{cCPk8_U0000000000000000001xisZ_}`yi+M#vMj|eqQ;L zD-X}DKHrq@{8kg-_~XkzM}2-y`ID&(QXLRNr6CtFBlQT8RAC z9|gNK>fBb@p5u1y>aQjG5J?~M0{{R300000K!W}P-%!|MeWQv#00000NkvXXu0mjf D=P3u! literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Red/_aseprite/death.aseprite b/assets/Prototype_Character/Red/_aseprite/death.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..8b75ade41d89940fa70502f51e8b855b3c1ba9fc GIT binary patch literal 1600 zcmZ=@V_~;dPX2m2E;*8{2wgs9^~o52V^NI0PSSBdiCmmIj~A* zAPEC*Kn5E_a7JQEemO#IRCwZlMW76e0`?3w8d3o&Fzazy;#ZOwgGApP^*hl|oR=vH}BxgMk~#FaZW1kQ*bS z`3@QI+)mxW5bx3O<9Yw-LN>jFHy!?Vq!_9DHq7_jCgS`0mE9aqzuh|7bIOm4b6=WT zB;U^6drIP1eO$i$#fR1FT-*B}Cqy6HeBxPp)UTcIeqZjgzExv?-u&*d;xES!~jQz~cBrep{ITgI`K@Pt@-%-v4lM_+xA7>SdC@>O<|9 z?Od;Jq*rA(yZBh@uh37jnfHIjvp5R0SZ#auexChq{TCHtpV#Nu3f`~&zb3BZ&#pyF z?^^!&-Q=e?itm5Uy$jZQo$%G|NIi3pW9QU`zySF5kr|x$Kru_BNK1!C8XHQa ziGU;R?X`oP2NVQc9cORsO8B2TlWEbSP3<~o8rZ{A;_a3}1&E76~w|k#B zgTms=y<2a&Z(kc}yi2@RpTU8x{zg#b_j}woT~gRNl#reA)f=oDe_07d7Fjl2y8 zJS>rItScrkT)ezd#&`qEX2;Yo5q*VDu3yx5a#<#|KXSdiWah1hTTb14wCh`)+3F*j z`;sH&zaCv48)RuWXY%vzGeWY_=J%>EHfQIjPko;HZU_H;<<}GMZ456=TDbhnZN(M= zCy&}I^NdU5r|D0+TXJoZaY9`89Q~=jrFG8R%hjjxKVN6Oa7VJa1BYVE1p9#Md6)Nv zOw)RK|8&rd`HTJw27kW%|9R2o6;oeckpCLncK6!JMf8cF6uc3%k@J87k4vELWKE}U z@~2k@TO51X#c_{$tNfMNtCvgXBtL(3G%x$A&-Y8fd|op3PGo%a=CobY|A{j!XtvK; zxvu&>+wB&S2iM`T!-o<(p5WMVM7iL@DKl8G%>wV$;duiSNyHoz1Y>$h{{&?cd^3%sR zU)}m9_VMNuvDf}uY?@bD{;~3Y=e&EHRe%w6;-Ab~W8HY~(=Uo&eSBD#qi17(ch_j4FE!q5x5xc$PYh5Q$4_4O*Ou#R_dU$EJ-=ew^^8EC42XlG_&-?MJ;>9A56Dtb0NTlL_3G9Ca$uFr zKoSPrfDAT<;EcqS{BnfasPM%9ia;3_1!T1#Q#l!&GZKpulS@*I5X#riT=PF1D9Ne- zB!J*Q$UZOtg>uE5w>J$L84NfM82oqJ(7?vBAibE8-7;y>+jrL)7!Gu-Vv3kJ6Udm* z!4R?Wu012egGq1yv)}p3==t>fD_@{7tU$kUF^D1=%fsLTG&VUQAz?%4)_?vyX$p?I zJ7kYHV$AQHghcKUyy0T+lrGeKkee}y?4Pyd&f)mLwUIssq8^Z&+ z4jJ&gO|@yQ2zbAsg5@Z8hkDVM#|NWpGG9q7-ekie9eHooJ|}t2)V|9Pe*^}~T*>dh z9ou%C`T5_Pvr3PyNZI_feY7H?o4Iz~^V8wAXCwdq_+PqzYg=6v ze5yVYxKWJ#mreil^}l9q{`K+y>$+@S_SZ~u78gQaw(W`4zI5yM5 I{0Ha70f?l1qyPW_ literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Red/_aseprite/idle.aseprite b/assets/Prototype_Character/Red/_aseprite/idle.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..d653ea6192e4bcb445658f1538057347534ef292 GIT binary patch literal 1148 zcmbC&SgNSO3d_ zRWbuf7;pnJ*cgH{5>xWa5o)8t6aOm$WmpuD)q+grWN^+%EJ{o+Ni9MsUpsTn|8Srr zs{)V!g8v}*0iexMggA35uYdV zf2rkrRN{Z>?(;g2jIZlFN|yO`?ZkhJdG$N)9Jn+ne5|=;i#`0O3?=zn0i*Vge&i}Rk$3(lub{oakPL%&@{mA{-`M*71=BJxn zsy7Yx|8M*~KW+cKf9XFe*1hlr??zT7k&m@ z{nxgsGG|PD%e-DFvU1lHrXHW_=&2chX8LV+SHTrOzn4y2nANl2Zl1m1$C=rJA2;$9 zyML+o{cfLk*6@DS_4?KK4SkqbAGEZoEzo=O_TyE_h~s-J&#&RVrrjIyIm~XNM^F33 zKk*-x>)q^=-&-xR|5bjZ|BwBrsC>CAE1v%M3O@he_OI>#@84F}*gaAg>ilRh>;LyX l|Eu41@BKXEfAXA{f6trl(BG$DF<)!>kF|f;8kg^U2LOvfa_#^C literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Red/_aseprite/walk.aseprite b/assets/Prototype_Character/Red/_aseprite/walk.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..71e253f09e3b963c2db6a0fab81da358266e4772 GIT binary patch literal 1967 zcmZ3_&cN_sDGP%FLjn*pGB7Yq17ZYVWMBjdG6(=6EkGI*1H-S6JYZXSfEI`WF}niT zN+jzPfNV(&QHTOtRqOw}Kqd>w^^8EC42XlG_&-?MJ;>9A56Dtb0NTlL_3G9Ca$uFr zKoSPrfDAT<;EcqS{BnfasPM%9ia;3_1!T1#Q#l!&GZKpulS@*I5X#riT=PF1D9Ne- zB!J*Q$UZOtg>uE5w>J$L84NfM82oqJ(7?vBAibE8-7;y>*)!7_7!GuFFhpFu3uH`K z#T2peu012egGq1yv)}p3c&4`Qw=d8bR-oUw7(|hbpy>QKKSRm1D}}GXhA}a41HFN8BR7K|$c@t+ z`3@QIuzZ%ZJHXkw=-$qcmiG>*|1w*(V$+tK>v>+)Ck|@=6yl%u+NIk&^2?4p`?a`M z&a2H^%=4^w?!5K$KSVs!u@S3pPIrH`Z~ft})#)c5m0y)%yZ$Y0!LI{1SIsnBU9pGz z-OSGy_pZFR=lg@R##6oKu9w?$>+_b5)b$oo+kdTnB${pG_M|#y`tuL`-JknyF8>Oh z_ik@{nAudXwO5;$zq?!i{a3y>|Fe}6_x@#D@@!q6_jb#VdV!6)TNnSG{yyvVJ%j6? zE$4Y5e-g3{GXBC>#p0^BfvGT)@ai#10RGFF1Arxeht-xO_K?JD`8C z>;Jz>%kvLZZK74>4;Iex^Io~(0?!lm-HOu;wpE`0N_W?{ zKAw5Dz~)+FUiV-5;}6?=Wj^mHUz7Yn@`T@A{%b1p%h&!m^H}nRep=}KiO;LP`#+fV zyWVT*rTVM0=WE~lsryT!`hnQ*cKOw_H0}AP{k>DU|5L_3`EQ>s<$kZqimCrOXX)nh zbN_eR-{3db_gWfc|0*|bP38afGxfdqt3PLLo6VRNw_^LxdH4S38%@l%}O#pI&fQH14FY(Q}n;Tb^IDvwGjBFWM`T z-GA7X?epB)xcvBs5zneD#P%QDYFhdJ_ho+Z>=Q}nZXAB_J@vWuwW~dmZ(1MyIJcwy z{!Y8q<{HDpG2j^ zt$G!9FIuZNn|q#>_5X9f-k*@&wLEFZ&+YFMKm2+5C*J5*2mu8(XRYC|5)1d zBe$fTuXjmL9D#V97UO;8N{%`nSfp6OJ93D?R@)Z%xzo#;Oj3V>9xk zf|eegq`z{q-`OWiriedFtkMu`l-e)rH%UwV%lFSud;QMeT5vg8?%1{!^|Jl)x1^G7 zudRFTv5$G{{d?@iPV)I{zX%?;-VnbgwARh``R;nA^LPGF_E7oPeeX}q%enU#^>1X) z3*7%};eG&p283S literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Red/_shadow/shadow_death.png b/assets/Prototype_Character/Red/_shadow/shadow_death.png new file mode 100644 index 0000000000000000000000000000000000000000..15b229dbcfc102d045a9c06d7ad62a016bf6c571 GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|R(ZNOhE&XX zd)tt&*?@;N;8bi?*DCGyG*??!{mZt&OohpdzCY{uud3qzZ4*#A7%0l=_TN8$J?!(l zA9de~OFuokp0>Sg#XrVC)^`)`&$@dl(e`uVFT*CQboVlt76t}|?V8hzz4!iddE#F! zAF%6|y4L;L0|{;ydOtD36`j!iTJ-Kk>W%s*%lGon_$s&HiCuz$vPPjD0|U!X#z2l7 Ufz#I~8G@91y85}Sb4q9e0E0ePxc~qF literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Red/_shadow/shadow_static.png b/assets/Prototype_Character/Red/_shadow/shadow_static.png new file mode 100644 index 0000000000000000000000000000000000000000..ef0287743b797814f45e994f057104ba55159ee5 GIT binary patch literal 5123 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 AvH$=8 literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Red/death.png b/assets/Prototype_Character/Red/death.png new file mode 100644 index 0000000000000000000000000000000000000000..8dfc03b65cc6100663e881c32d9b3b4c704c23e6 GIT binary patch literal 918 zcmV;H18Mw;P)Px&P)S5VRCt{2-9e6mFcb#h$7tNSslj*w;vtL&@eE#qOLztkVmyTL0!Cst?u>?6 z=);0AmeN*m{x6xtVbDTb-Xob;Bmw{c000000PrR-dDA?arDfYU`>oOE{b&8UZeHaz8#* zhF}A=LQo|=pO2(ld!Y{_;0+;=CP;~U@;6{CgARn z(E9wGBf4!H-vtppJ3b2ujzd5qC5b_gc8BEUCSZ3+NDO*TLZCVU_?*i^@N)F*i&*o9 zpwR&Mk_&;+n5ZHLg+frgKROA4MEN@%G3Zt9(x%Tn4>EZbGq>VmLf!;H}Y=+s|!wOFi*!zCNeUFMXXg11@usja5L#IiNVRcp!MzyIdp2uhrw=c8k@?wyyx4_E)E?b||jZ zNooRCKb&K)s|6ZW?XyJ6+!eS!r@3PyvNIuc+b7CLhQFeL)*`(!#m@f}o`Gt;KIbLw z6ql2ttbHQN+V>HP0RR9100000000000001>gw}uSQUCAG+nSQL)Lecm?V_Hx)m(lH ztpRxVuxww?Yxd1Y=(Vy$=~*LbHzakj-oGy60Z_}(iR@P)w88U@$n4kw*$ZnGPtu@)x)c8`l|!f@brn?W2=4bFL0nogH>T6j>g)LY&XTT zU!4roNtj!!*0uHPx&6iGxuRCt{2oI#7)KorNHt?YSCP1Pl>jYo6#q!7@P6xxG(+;cyO_!*WZ zyZ0rMDD<}MVlRE}B$;gU#>@!m{|7?yV&-KgGw%(~-ys430002^PwAN4Gzx}CoqZE; zjmo>E|E`Ji#cHX&HlNOpwi{s-4387P>amAn|KbKyJe`N*u+$(n>3beUadJA3;-u6dbunrHBBJ%}T6^q@C>Usur5HCrTM#t> zN8g9L$I>;nRYm9~;Ii+ZcBN}PwMh{V0X66IWTc-v8Dg|6U=ZsKi6ofQHA00000 z000000PsHI-|pI^4ImOOjI>P~KxC*7Xp1(0sG-$imumt#OxFa6&iq+ickGIyK*AIV zBp|&d0p!KYM;XYs?p^6$Fb*Vh?wZIeoy-?>`!Qpeq0BTfTqVQKpC_e*HGr zWQejrl{R3pTAr51ZBnHTm|VRrjZ3LD1bF?=MJ22JM{#0Y$@i!o1FEzEuBg%moQrrO z)bp=%x&YVoJujyVI2U7q#C$r-4T;5SX;mSn()T>NNndh9V&Zp={GT-w+{D|?rQ!nn zw-FZr0000000000003w)*g(O=d7DHM*g(PLCY>PS5`Uchm#q%iKS1(j4nDf>&NdhO z2k5#x+ob+Q0twa#ut-AvuwGz&ySCB@sz!iC66%Nd6NrL=$iFxUat6?Cpiq?^nuIfe zZUcphFE0=K2S^^Ejr#`}&jW}qBa~2SLL(p^0_r&DE?>0S90KYrkh^@*=4Af>uIQyS k0$fpO1ONa4000d61AWm6Gs5_pdjJ3c07*qoM6N<$g8I9R-~a#s literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Red/idle.png b/assets/Prototype_Character/Red/idle.png new file mode 100644 index 0000000000000000000000000000000000000000..2de5d213bb72b86721970b218bef6003590ec15d GIT binary patch literal 701 zcmV;u0z&Px%cS%G+RCt{2oY9TqFc5}+4nzS)plu+!A`lhPgquhWTL_|vRT@|&ieK&x1#u5q z?>Vf)#)F+K$Nx!@f+J_*`IE6F4+9WF2qA<#DKkl@UADdW+7HE1biXNCgm&0v+sl5{ z*KK=VQ<-fB?Az zvW@qAEMf^Da=_brHK;8mu>=Tmz%JWfbhd$7&hhqMbq=+hLuZ@b2pyA{0t}9rcJM$O zaC||?w&|iwvL>+4P#FVrI(M$A&%4N#|1-}0RRp|e5ogSxMwi{$l_*aljauLZF7fv^>!C7bWwKfhjQ0K=ujMIhvGOJdm;1_>=Qx=A%qY@2qA=!M}_$Tp&ghX z@WE5(2l#lp`~YCu9D!>_&kq3pj~oG!1I+0>+BbiT7y<;vfa~pP(eEg#MLTd}7X=(YPgcJ00000NkvXXu0mjf!tyy( literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Red/walk.png b/assets/Prototype_Character/Red/walk.png new file mode 100644 index 0000000000000000000000000000000000000000..c2b2eee370e63008bcfef02d3fe24dc585151e04 GIT binary patch literal 1056 zcmeAS@N?(olHy`uVBq!ia0vp^4M3d0!3HF+R#kZdDaPU;cPEB*=VV?oFfiZpba4!+ znDcgSVBRAKp0=}_9_7Xd1r{S1ssB_*zc1M{-r&zTFK3_7w(pBrwc5*^V zr|~zH%l{`&xHa*_5Bb@h!W*1ffF?sh9gFbE+4H8o4!-MT-Uv`M--8IF8026ez{D8 z@Px|1I~;Mk&qDM%yqaC!yshOyU3@huuKMGjr=K={wXzhmeED96LBIWcx8Rx7qUMx0 zyIZ&I5J~Cv{Z*N3(z$m3=dhC#Uvxi{7S?a~zq-3n=BAB&{I3?i3)>D}e|7^|g`Gyni1taPW2)&iVbEi}{=y!)vo8 zi5$NeVZn()JEu;uS3I@SF5c*Z zSodqf+$O7Xj*r<_*2*6E%-0yfH$kA7cTp^B#^#mVCU3fRHS5FQ)(d*R-#tZJ_OKbg zeQ{Os$Sua2n*Woxm!CVMD8=`}JO1w?HbIx`Yp&<}Es$kMloPmiJo+tD-DaI_%13h( z?(tUos+5)7V{H4>+I@A={btwrMFOXT`JS;q&m40uw!nS{L{r&sh_MClu z+WczSqx}5DdIoLjS5^CW8;jS!yYXvrMBYzTkKzM2SrYEHneaH;!$N|AK|=9A!>yEU UTNf@6Ed+^ry85}Sb4q9e0AHEutN;K2 literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Yellow/_aseprite/death.aseprite b/assets/Prototype_Character/Yellow/_aseprite/death.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..b449e74f848d48ce64d69a04d2968d884d2bd529 GIT binary patch literal 1611 zcmeZfV_~;dPX2m2E;*8{2wgs9^~o52V^NI0PSSBdiCmmIj~A* zAPEC*Kn5E_a7JQEemO#IRCwZlMW76e0`?3w8d3o&Fzazy;#ZOwgGApP^*hl|mL^fUyDtgM)z^$uI#1ACMa( zZu1^;;CXH2)A%EW)h&VBEkS@Sfq7bj)P-6Xqm`4r8|Hg$6A9fr@BPD&qIc6$dk^RR zPjcK9^zL!hqCm;RKhEF&sccnqe(Q@5g)%v(E5vU8@$t65{vg%8{naN*HqHl-2`iptZm#mufBZ-E`0V%Er~KCXf42fhQ^4_Sf9`%x|IR9VKIz}owd#+m-_O6w zJuyCf%NA?)`uGbcSwBbDv%Q~p|7NXMs_dG39Y6B_{QZ4tJh`fJvAiDMm#YJU}rz z!;!bafXCIand^`sx5oiiA%P|anFJ=&0#cwjhvE{utn)K>>7N!qnP-)KQqo|(>$&NtmRapzczd_->GtC5GXw5ylvZdF za5^Es!uEV-yq5RWFYi+|&-h>b-?8%N%m2kYBd?q)dC~t%Z(^rb+cNq@QVQNk+Q@mp zfX5|Jce19_H~G^mgDs9d?Bck`yjA{6?A6PqbCRFGI+~Y#)#v*qV0JH=dM7eIdUM*Y z>HowT7Bt)EtXx<9p6zyv$b;MP7~(^TAy04&9ppU(jG@gw%N=BPGbwLiRW|Tof58^p zl<8m@@k`=%X^`W@U-O@MFR|I`$?vaw^tsKPdDmE?ovs({pKQsd2bd)(jl!~m6X{N#0iZMnX7-@|O%^DCBan7`rUwOto? XalhF;Pi*TfzyGgK|A?C)5GxA+j$t=T literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Yellow/_aseprite/hurt.aseprite b/assets/Prototype_Character/Yellow/_aseprite/hurt.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..f0f5bd42e92c9ead0e8902c4c49b69456d6cb928 GIT binary patch literal 1181 zcmbQs!ocugDHDSNLjn*pGB7Yq17ZYVWMBjdG6(=6EkFSi1H-S6JYZXSfEI`WF}niT zN+jzPfNV(&QHTOtRqOw}Kqd>w^^8EC42XlG_&-?MJ;>9A56Dtb0NTlL_3G9Ca$uFr zKoSPrfDAT<;EcqS{BnfasPM%9ia;3_1!T1#Q#l!&GZKpulS@*I5X#riT=PF1D9Ne- zB!J*Q$UZOtg>uE5w>J$L84NfM82oqJ(7?vBAibE8-7;y>+jrL)7!Gu-Vv3kJ6Udm* z!4R?Wu012egGq1yv)}p3==t>fD_@{7tU$kUF^D1=%fsLTG&VUQAz?%4)_?vyX$p?I zJ7kYHV$AQHghcKUyy0T+lrGeKkee}y?4Pyd&f)mLwUIssq8^Z&+ z4jJ&gO|@yQ2zbAsg5@Z8hkDVM#|NWpGG9q7-ekie9eHooJ|}t2)V|9Pe*^}~T*>dh z9ou%C`T5_Pvr3PyNZI_feY7H?o4Iz~^V8wAXCwdq_+PqzYg=aDy{aC$HYWaY%#t}Vr(~8N{LY<0#n^w@=Wmw{B04UH#l==egG> zFH7#-@?~4P{8Qo1BmWap-UrS(;r`>_=I?i;UT^rW5%>CEi+$mWdsTauU%ef)RyV)% z`9|Uj1{~I{CvHw)>A7__scjH#P1x`2VM9*{OUb{!e?{ Lo~qxS?E4Y`y_VPJT$l!-xsApwXP85kIJKm;5xGBASq0t^ChF={e0m>3v-edGb#$^$f542ank zP@Di1Q2=5|3{jvU5ZJ0(|K|nLEFjl20(mkZ4vOOcU}^UtPZvHQOF;o>C&SgNSO3d_ zRWbuf7;pnJ*cgH{5>xWa5o)8t6aOm$WmpuD)q+grWN^+%EJ{o+Ni9MsUpsTn|8Srr zs{)V!g8v}*0iexMggA35uYd;>O&31}E6ieY{rH-_Ej zJ!HUh#>l7f*GaCP17bY~6nGA>=^Qj!P!kc;zd1Q{ZHVIhC-UF_bi~G9x+Q-3;g7Iu zEW7?!T6+f@9;?5rZ`}RkjA{1|iS~V_pX}Eym(Tldc)#fT=CAKF%$U0m+U@z75I^Jg zs zOZI+=KYQi!|1*DQ-(0`^zogwf?O*j=t%tS&HRxCV*WanX>Fij4D)n#`!mw0N%$LL>ncNJXm^ZR2x=c^L+wdd>wKhDe+{J4>? z*!@er?sogUvxfJpuGg==Z|K9k`kO|{N8Gj{jc&P{eSE~Mdiy~S@HD0SMd4&wtsE^fB&|+#_o~2Q0GU3S^vN9 l`Ct96d++BN|C8su{CnPXhyFhOiuqd0f2{q(cDKrW0stI4ZjJx| literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Yellow/_aseprite/walk.aseprite b/assets/Prototype_Character/Yellow/_aseprite/walk.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..9d921e1d513f71b775ae151aa3b8f74a64e69cd1 GIT binary patch literal 1982 zcmdnT&cN_sDGP%FLjn*pGB7Yq17ZYVWMBjdG6(=6EkGs{1H-S6JYZXSfEI`WF}niT zN+jzPfNV(&QHTOtRqOw}Kqd>w^^8EC42XlG_&-?MJ;>9A56Dtb0NTlL_3G9Ca$uFr zKoSPrfDAT<;EcqS{BnfasPM%9ia;3_1!T1#Q#l!&GZKpulS@*I5X#riT=PF1D9Ne- zB!J*Q$UZOtg>uE5w>J$L84NfM82oqJ(7?vBAibE8-7;y>*)!7_7!GuFFhpFu3uH`K z#T2peu012egGq1yv)}p3c&4`Qw=d8bR-oUw7(|hbpy>QKKSRm1D}`^thA}a41HFN8BR7K|$c@u3 z@*XnaaoNlhtY8Qug?B1x2JMh(8|wOo5QQ;iEVr%>i#-xf9Rg1-s2rL{jWdC{7WqSH!(eG z|EbG+zh5{7RJpb4!xd|F^?uyMtC< z>93kCo_+6s_OsKM-k+>&e3;fSOZ-*tzUANlO9wehJzO{hqX(9MxUlA*gS-a zLKWt1ao{X*

pYEO8JHVg96?6j@fw`F&@5_$T$5#X`}yinHZq%YIMqa_ic9{Hsdy z&vSP9&({~|6x+&l-+ypx)}HUDKel(9rWmHbN&N78(`VV&S;us~IThBV-#Pp~X8u)a zk=L*G9+~E~v@-YLYSGs=qP1*0w^km`7qx$N_2&Mj&sNj^ntu*n^M8_ik*<+`XsPyl z-Kod6vii@RTc7^7{0ZOPV8c6qZhtqZ_*YW5DlPQ+^8e@dugkx4c;Zj<(R~H22R@(SlAL7%n~@4G2T@^B$2!>bfRCD&dVUFKH+>h0MT>1qyPW_ literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Yellow/_shadow/shadow_death.png b/assets/Prototype_Character/Yellow/_shadow/shadow_death.png new file mode 100644 index 0000000000000000000000000000000000000000..15b229dbcfc102d045a9c06d7ad62a016bf6c571 GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGojKx9jP7LeL$-D$|R(ZNOhE&XX zd)tt&*?@;N;8bi?*DCGyG*??!{mZt&OohpdzCY{uud3qzZ4*#A7%0l=_TN8$J?!(l zA9de~OFuokp0>Sg#XrVC)^`)`&$@dl(e`uVFT*CQboVlt76t}|?V8hzz4!iddE#F! zAF%6|y4L;L0|{;ydOtD36`j!iTJ-Kk>W%s*%lGon_$s&HiCuz$vPPjD0|U!X#z2l7 Ufz#I~8G@91y85}Sb4q9e0E0ePxc~qF literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Yellow/_shadow/shadow_static.png b/assets/Prototype_Character/Yellow/_shadow/shadow_static.png new file mode 100644 index 0000000000000000000000000000000000000000..ef0287743b797814f45e994f057104ba55159ee5 GIT binary patch literal 5123 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 AvH$=8 literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Yellow/death.png b/assets/Prototype_Character/Yellow/death.png new file mode 100644 index 0000000000000000000000000000000000000000..cac1353688e022a761d8a236c4a3a6d46420b059 GIT binary patch literal 931 zcmV;U16=%xP)Px&T}ebiRCt{2-7#+BKoo%Cm(^BjFh#1QiL|M*H$b^Z4nV;Xf<%c#jzGZyxJSed zNEIX+Q@M!Kq-6{CnXzMFJf5+a&HqV}fB<8^@w{=AH?{}>00000Kw~akKJ;+c+m^ll z^0sjLzVEDZz#i^;+sWX%^moJQhEAyHr|So=Zlc`tBu;B@%y;C}0mb0&U)$8(p0nUH z1v`&`{^(U^UXnP?0dI=l{&Etq zTCSxNb%Ufp^8nC&E=$3aK{spLb`Gr70ZqxL!2L)tb)wzQ_eZZ1A_a=%C;Cp*J@}r` zb07c!00000000000I=Yta)1RJ?rH~!1snBvs|#Ymh8w32u&KX1*J9Z^TvBo64#=0P zi&epFQ_gYWhSLbp^1I34`foM#PO)INN$0w7<44s7UfvcL!|5jb;-|+;`AFX-iBp*j zt|f`n>~R~eZlc^z+fHpm?MYaRR6o-9-0RQt&)`GfX3apzxyW|S!ZFTsKyhekM`%h4 zxkgMMo7-2<(h6npZ6ErS4o`aM&R@OGq+75ehvuR z_C@lU;aE1PJO`*^@Y|wCpjof4^O6Q7tHulDI!0L=8%#@yqp zed`-I$b8q}p|mg?-v_^KiMc9A?d~NYjdVtc_G{h1L2K`eYX*GP;+J){mYnP5>Wj{; zedqN7BKy%0VnLp+MbkQav)*6RUO?e+lx0001i`~ez#RvYU87W4oB002ovPDHLk FV1n~@%Dw;q literal 0 HcmV?d00001 diff --git a/assets/Prototype_Character/Yellow/hurt.png b/assets/Prototype_Character/Yellow/hurt.png new file mode 100644 index 0000000000000000000000000000000000000000..f72aacc88faf5b536cd34b137f0448d7605277a1 GIT binary patch literal 871 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 literal 0 HcmV?d00001 diff --git a/assets/tranquil_tunnels_transparent.png b/assets/tranquil_tunnels_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..24c48a4ead5c9ab31e168e7442db3a95306a69af GIT binary patch literal 154034 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 literal 0 HcmV?d00001 diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..d39f4a2 --- /dev/null +++ b/build.zig @@ -0,0 +1,393 @@ +const std = @import("std"); +const sokol = @import("sokol"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const assets_dir = "assets"; + const asset_files = [_][]const u8{ + "tranquil_tunnels_transparent.png" + // "sokoban/sokoban_tilesheet.png", + // "roboto-font/Roboto-Regular.ttf", + // "tiled/map.tmx", + // "tiled/tileset.tsx" + }; + + const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir }); + + const is_wasm = target.result.cpu.arch.isWasm(); + const is_debug = (optimize == .Debug); + + const has_imgui = b.option(bool, "imgui", "ImGui integration") orelse is_debug; + const assets_from_filesystem = b.option(bool, "assets-filesystem", "Allow reading assets from filesystem") orelse is_debug; + + var has_tracy = false; + if (!is_wasm) { + has_tracy = b.option(bool, "tracy", "Tracy integration") orelse is_debug; + } + + const exe_mod = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const build_options = b.addOptions(); + build_options.addOption(bool, "has_imgui", has_imgui); + build_options.addOption(?[]const u8, "assets_dir", if (assets_from_filesystem) asset_dir_realpath else null); + exe_mod.addOptions("build_options", build_options); + + exe_mod.addImport("embedded_assets", try createEmbeddedAssets(b, assets_dir, &asset_files)); + + const math_mod = b.createModule(.{ + .root_source_file = b.path("src/math.zig") + }); + exe_mod.addImport("math", math_mod); + + const dep_tiled = b.dependency("tiled", .{}); + exe_mod.addImport("tiled", dep_tiled.module("tiled")); + + const dep_sokol = b.dependency("sokol", .{ + .target = target, + .optimize = optimize, + .with_sokol_imgui = has_imgui + }); + const mod_sokol = dep_sokol.module("sokol"); + exe_mod.linkLibrary(dep_sokol.artifact("sokol_clib")); + exe_mod.addImport("sokol", mod_sokol); + + const dep_shdc = dep_sokol.builder.dependency("shdc", .{}); + + var slang = sokol.shdc.Slang{}; + if (is_wasm) { + slang.wgsl = true; + } else { + slang.glsl410 = true; + slang.hlsl4 = true; + slang.metal_macos = true; + slang.glsl300es = true; + } + const mod_shader = try sokol.shdc.createModule(b, "shader", mod_sokol, .{ + .shdc_dep = dep_shdc, + .input = "src/shader.glsl", + .output = "src/shader.zig", + .slang = slang + }); + mod_shader.addImport("math", math_mod); + exe_mod.addImport("shader", mod_shader); + + if (has_imgui) { + if (b.lazyDependency("cimgui", .{ + .target = target, + .optimize = optimize, + })) |dep_cimgui| { + const cimgui = b.lazyImport(@This(), "cimgui").?; + const cimgui_conf = cimgui.getConfig(true); + exe_mod.addImport("cimgui", dep_cimgui.module(cimgui_conf.module_name)); + dep_sokol.artifact("sokol_clib").root_module.addIncludePath(dep_cimgui.path(cimgui_conf.include_dir)); + } + } + + const dep_stb = b.dependency("stb", .{}); + exe_mod.addImport("stb_image", dep_stb.module("stb_image")); + exe_mod.addImport("stb_vorbis", dep_stb.module("stb_vorbis")); + exe_mod.addImport("stb_rect_pack", dep_stb.module("stb_rect_pack")); + exe_mod.addImport("stb_truetype", dep_stb.module("stb_truetype")); + + const dep_tracy = b.dependency("tracy", .{ + .target = target, + .optimize = optimize, + .tracy_enable = has_tracy, + .tracy_only_localhost = true + }); + if (has_tracy) { + exe_mod.linkLibrary(dep_tracy.artifact("tracy")); + } + exe_mod.addImport("tracy", dep_tracy.module("tracy")); + + var run_cmd_step: *std.Build.Step = undefined; + if (is_wasm) { + if (true) { + @panic("TODO: test in 0.17"); + } + + const build_step = buildWasm(b, .{ + .name = "index", + .root_module = exe_mod, + .dep_sokol = dep_sokol, + }); + b.getInstallStep().dependOn(build_step); + + const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{}); + const emrun_step = sokol.emRunStep(b, .{ .name = "index", .emsdk = dep_emsdk }); + emrun_step.step.dependOn(build_step); + run_cmd_step = &emrun_step.step; + + // TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload + } else { + const exe = buildNative(b, .{ + .name = "sokol_template_v2", + .root_module = exe_mod + }); + b.installArtifact(exe); + + { + const run_cmd = b.addRunArtifact(exe); + if (b.args) |args| { + run_cmd.addArgs(args); + } + run_cmd_step = &run_cmd.step; + } + + { + const exe_tests = b.addTest(.{ .root_module = exe_mod, }); + const run_exe_tests = b.addRunArtifact(exe_tests); + + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_exe_tests.step); + } + } + + const run_step = b.step("run", "Run game"); + run_step.dependOn(run_cmd_step); +} + +fn createEmbeddedAssets( + b: *std.Build, + assets_dir: []const u8, + asset_files: []const []const u8 +) !*std.Build.Module { + const write_files = b.addWriteFiles(); + + var embedded_assets: std.ArrayList(u8) = .empty; + defer embedded_assets.deinit(b.allocator); + + try embedded_assets.appendSlice(b.allocator, "pub const files = .{\n"); + const mod_embedded_assets = b.createModule(.{}); + for (asset_files) |asset_file| { + mod_embedded_assets.addAnonymousImport(asset_file, .{ + .root_source_file = b.path(b.pathJoin(&.{ assets_dir, asset_file })) + }); + + try embedded_assets.appendSlice(b.allocator, " .{ "); + + try embedded_assets.appendSlice(b.allocator, ".name = \""); + try embedded_assets.appendSlice(b.allocator, asset_file); + try embedded_assets.appendSlice(b.allocator, "\""); + + try embedded_assets.appendSlice(b.allocator, ", "); + + try embedded_assets.appendSlice(b.allocator, ".contents = @embedFile(\""); + try embedded_assets.appendSlice(b.allocator, asset_file); + try embedded_assets.appendSlice(b.allocator, "\")"); + + try embedded_assets.appendSlice(b.allocator, " },\n"); + } + try embedded_assets.appendSlice(b.allocator, "};\n"); + mod_embedded_assets.root_source_file = write_files.add("embedded_assets.zig", embedded_assets.items); + + return mod_embedded_assets; +} + +const BuildNativeOptions = struct { + name: []const u8, + root_module: *std.Build.Module, + win32_has_console: ?bool = null, + win32_png_icon: ?std.Build.LazyPath = null +}; + +fn buildNative( + b: *std.Build, + opts: BuildNativeOptions, +) *std.Build.Step.Compile { + const exe = b.addExecutable(.{ + .name = opts.name, + .root_module = opts.root_module, + }); + + const target = opts.root_module.resolved_target.?; + if (target.result.os.tag == .windows) { + const optimize = opts.root_module.optimize.?; + const has_console = opts.win32_has_console orelse (optimize == .Debug); + exe.subsystem = if (has_console) .Console else .Windows; + + if (opts.win32_png_icon) |win32_png_icon| { + const png_to_icon_tool = buildPngToIconTool(b, b.graph.host, .ReleaseSafe); + + const png_to_icon_step = b.addRunArtifact(png_to_icon_tool); + png_to_icon_step.addFileArg(win32_png_icon); + const icon_file = png_to_icon_step.addOutputFileArg("icon.ico"); + + const add_icon_step = AddExecutableIcon.init(exe, icon_file); + exe.step.dependOn(&add_icon_step.step); + } + } + + return exe; +} + +fn buildPngToIconTool( + b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, +) *std.Build.Step.Compile { + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("tools/png-to-icon.zig"), + }); + + const dep_stb = b.dependency("stb", .{}); + mod.addImport("stb_image", dep_stb.module("stb_image")); + + return b.addExecutable(.{ + .name = "png-to-icon", + .root_module = mod, + }); +} + +const AddExecutableIcon = struct { + obj: *std.Build.Step.Compile, + step: std.Build.Step, + icon_file: std.Build.LazyPath, + resource_file: std.Build.LazyPath, + + fn init(obj: *std.Build.Step.Compile, icon_file: std.Build.LazyPath) *AddExecutableIcon { + const b = obj.step.owner; + const self = b.allocator.create(AddExecutableIcon) catch @panic("OOM"); + + self.obj = obj; + self.step = std.Build.Step.init(.{ + .id = .custom, + .name = "add executable icon", + .owner = b, + .makeFn = make + }); + + self.icon_file = icon_file; + icon_file.addStepDependencies(&self.step); + + const write_files = b.addWriteFiles(); + self.resource_file = write_files.add("resource-file.rc", ""); + self.step.dependOn(&write_files.step); + + self.obj.root_module.addWin32ResourceFile(.{ + .file = self.resource_file, + }); + + return self; + } + + fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { + const b = step.owner; + const self: *AddExecutableIcon = @fieldParentPtr("step", step); + + const relative_icon_path = try std.fs.path.relative( + b.allocator, + "", + null, + self.resource_file.dirname().getPath(b), + self.icon_file.getPath(b) + ); + std.mem.replaceScalar(u8, relative_icon_path, '\\', '/'); + + { + const cwd = std.Io.Dir.cwd(); + const resource_file = try cwd.createFile(b.graph.io, self.resource_file.getPath(b), .{ }); + defer resource_file.close(b.graph.io); + + var buff: [4*4096]u8 = undefined; + var file_writer = resource_file.writer(b.graph.io, &buff); + const w = &file_writer.interface; + + try w.writeAll("IDI_ICON ICON \""); + try w.writeAll(relative_icon_path); + try w.writeAll("\""); + } + } +}; + +const BuildWasmOptions = struct { + name: []const u8, + root_module: *std.Build.Module, + dep_sokol: *std.Build.Dependency, +}; + +fn buildWasm(b: *std.Build, opts: BuildWasmOptions) *std.Build.Step { + opts.root_module.sanitize_c = .off; + + // build the main file into a library, this is because the WASM 'exe' + // needs to be linked in a separate build step with the Emscripten linker + const main_lib = b.addLibrary(.{ + .name = opts.name, + .root_module = opts.root_module, + }); + + const dep_sokol = opts.dep_sokol; + const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{}); + + // TODO: + // const emsdk_install_step = sokol.emSdkInstallStep(b, dep_emsdk, .{}); + // main_lib.step.dependOn(emsdk_install_step); + + patchWasmIncludeDirs( + opts.root_module, + dep_emsdk.path("upstream/emscripten/cache/sysroot/include"), + &(dep_sokol.artifact("sokol_clib").step) + ); + + const link_step = sokol.emLinkStep(b, .{ + .lib_main = main_lib, + .target = opts.root_module.resolved_target.?, + .optimize = opts.root_module.optimize.?, + .emsdk = dep_emsdk, + .use_webgl2 = true, + .use_emmalloc = true, + .use_filesystem = false, + .shell_file_path = b.path("src/engine/shell.html"), + }) catch unreachable; + + return &link_step.step; +} + +fn patchWasmIncludeDirs( + module: *std.Build.Module, + path: std.Build.LazyPath, + depend_step: *std.Build.Step +) void { + if (module.link_libc != null and module.link_libc.?) { + // need to inject the Emscripten system header include path into + // the cimgui C library otherwise the C/C++ code won't find + // C stdlib headers + module.addSystemIncludePath(path); + } + + for (module.import_table.values()) |imported_module| { + patchWasmIncludeDirs(imported_module, path, depend_step); + } + + for (module.link_objects.items) |link_object| { + if (link_object != .other_step) { + continue; + } + const lib = link_object.other_step; + if (&lib.step == depend_step) { + continue; + } + + if (lib.root_module.link_libc != null and lib.root_module.link_libc.?) { + // need to inject the Emscripten system header include path into + // the cimgui C library otherwise the C/C++ code won't find + // C stdlib headers + lib.root_module.addSystemIncludePath(path); + + // all C libraries need to depend on the sokol library, when building for + // WASM this makes sure that the Emscripten SDK has been setup before + // C compilation is attempted (since the sokol C library depends on the + // Emscripten SDK setup step) + lib.step.dependOn(depend_step); + } + patchWasmIncludeDirs(lib.root_module, path, depend_step); + } +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..bf65b84 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,31 @@ +.{ + .name = .sokol_template_v2, + .version = "0.0.0", + .fingerprint = 0xf440b4e6e51f5206, // Changing this has security and trust implications. + .minimum_zig_version = "0.16.0", + .dependencies = .{ + .sokol = .{ + .url = "git+https://github.com/floooh/sokol-zig.git#ba97ac4e82e36a0f6c62f7fad5dc9c2d82beadd8", + .hash = "sokol-0.1.0-pb1HK4prNwDGx2RJFyv7VtYJubDahi19XKioPzPeMCxv", + }, + .stb = .{ + .path = "./libs/stb", + }, + .tiled = .{ + .path = "./libs/tiled", + }, + .tracy = .{ + .path = "./libs/tracy", + }, + .cimgui = .{ + .url = "git+https://github.com/floooh/dcimgui.git#3c2b41a9333b1195e2c2f8ce6a2d3e50660a73bf", + .hash = "cimgui-0.1.0-44ClkR8dmgDqqnfloyLE1bUAtl4zSKTT1wxaHSEC0ikV", + .lazy = true + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/libs/stb/build.zig b/libs/stb/build.zig new file mode 100644 index 0000000..a601475 --- /dev/null +++ b/libs/stb/build.zig @@ -0,0 +1,37 @@ +const std = @import("std"); +const Build = std.Build; +const LazyPath = Build.LazyPath; + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + buildSTBModule(b, target, optimize, "stb_image", b.path("src/stb_image.zig"), b.path("src/stb_image_impl.c")); + buildSTBModule(b, target, optimize, "stb_vorbis", b.path("src/stb_vorbis.zig"), b.path("src/stb_vorbis_impl.c")); + buildSTBModule(b, target, optimize, "stb_rect_pack", b.path("src/stb_rect_pack.zig"), b.path("src/stb_rect_pack_impl.c")); + buildSTBModule(b, target, optimize, "stb_truetype", b.path("src/stb_truetype.zig"), b.path("src/stb_truetype_impl.c")); +} + +fn buildSTBModule( + b: *std.Build, + target: Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + name: []const u8, + zig_file: LazyPath, + impl_file: LazyPath +) void { + const stb_dependency = b.dependency("stb", .{}); + + const mod = b.addModule(name, .{ + .target = target, + .optimize = optimize, + .root_source_file = zig_file, + .link_libc = true, + }); + + mod.addIncludePath(stb_dependency.path(".")); + mod.addCSourceFile(.{ + .file = impl_file, + .flags = &.{} + }); +} diff --git a/libs/stb/build.zig.zon b/libs/stb/build.zig.zon new file mode 100644 index 0000000..867deaa --- /dev/null +++ b/libs/stb/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .stb_image, + .version = "0.0.0", + .fingerprint = 0xe5d3607840482046, // Changing this has security and trust implications. + .minimum_zig_version = "0.15.2", + .dependencies = .{ + .stb = .{ + .url = "git+https://github.com/nothings/stb.git#f1c79c02822848a9bed4315b12c8c8f3761e1296", + .hash = "N-V-__8AABQ7TgCnPlp8MP4YA8znrjd6E-ZjpF1rvrS8J_2I", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/libs/stb/src/stb_image.zig b/libs/stb/src/stb_image.zig new file mode 100644 index 0000000..1c357a8 --- /dev/null +++ b/libs/stb/src/stb_image.zig @@ -0,0 +1,31 @@ +const std = @import("std"); + +const c = @cImport({ + @cDefine("STBI_NO_STDIO", {}); + @cInclude("stb_image.h"); +}); + +const STBImage = @This(); + +rgba8_pixels: [*c]u8, +width: u32, +height: u32, + +pub fn load(png_data: []const u8) !STBImage { + var width: c_int = undefined; + var height: c_int = undefined; + const pixels = c.stbi_load_from_memory(png_data.ptr, @intCast(png_data.len), &width, &height, null, 4); + if (pixels == null) { + return error.InvalidPng; + } + + return STBImage{ + .rgba8_pixels = pixels, + .width = @intCast(width), + .height = @intCast(height) + }; +} + +pub fn deinit(self: *const STBImage) void { + c.stbi_image_free(self.rgba8_pixels); +} diff --git a/libs/stb/src/stb_image_impl.c b/libs/stb/src/stb_image_impl.c new file mode 100644 index 0000000..eeee84a --- /dev/null +++ b/libs/stb/src/stb_image_impl.c @@ -0,0 +1,3 @@ +#define STB_IMAGE_IMPLEMENTATION +#define STBI_NO_STDIO +#include "stb_image.h" diff --git a/libs/stb/src/stb_rect_pack.zig b/libs/stb/src/stb_rect_pack.zig new file mode 100644 index 0000000..18065eb --- /dev/null +++ b/libs/stb/src/stb_rect_pack.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +const c = @cImport({ + @cInclude("stb_rect_pack.h"); +}); + +const STBRectPack = @This(); + +ctx: c.stbrp_context, + +pub fn init(self: *STBRectPack, width: u32, height: u32, nodes: []Node) void { + c.stbrp_init_target(&self.ctx, @intCast(width), @intCast(height), nodes.ptr, @intCast(nodes.len)); +} + +pub fn setupAllowOutOfMem(self: *STBRectPack, enabled: bool) void { + c.stbrp_setup_heuristic(&self.ctx, if (enabled) 1 else 0); +} + +pub fn setupHeuristic(self: *STBRectPack, heuristic: Heuristic) void { + c.stbrp_setup_heuristic(&self.ctx, heuristic); +} + +pub fn packRects(self: *STBRectPack, rects: []Rect) bool { + return c.stbrp_pack_rects(&self.ctx, rects.ptr, @intCast(rects.len)) == 1; +} + +pub const Heuristic = enum(i32) { + Skyline_BL_sortHeight = c.STBRP_HEURISTIC_Skyline_BL_sortHeight, + Skyline_BF_sortHeight = c.STBRP_HEURISTIC_Skyline_BF_sortHeight, + _, + + pub const default = Heuristic.Skyline_BL_sortHeight; +}; + +pub const Node = c.stbrp_node; +pub const Rect = c.stbrp_rect; diff --git a/libs/stb/src/stb_rect_pack_impl.c b/libs/stb/src/stb_rect_pack_impl.c new file mode 100644 index 0000000..31c4106 --- /dev/null +++ b/libs/stb/src/stb_rect_pack_impl.c @@ -0,0 +1,2 @@ +#define STB_RECT_PACK_IMPLEMENTATION +#include "stb_rect_pack.h" diff --git a/libs/stb/src/stb_truetype.zig b/libs/stb/src/stb_truetype.zig new file mode 100644 index 0000000..a63cfe6 --- /dev/null +++ b/libs/stb/src/stb_truetype.zig @@ -0,0 +1,116 @@ +const std = @import("std"); + +const c = @cImport({ + @cInclude("stb_truetype.h"); +}); + +pub const Box = struct { + x0: i32, + y0: i32, + x1: i32, + y1: i32, +}; + +pub const HMetrics = struct { + advance_width: i32, + left_side_bearing: i32 +}; + +pub const VMetrics = struct { + ascent: i32, + descent: i32, + line_gap: i32 +}; + +pub const Font = struct { + info: c.stbtt_fontinfo, + + pub fn getNumberOfFonts(data: [*c]const u8) !u32 { + const result = c.stbtt_GetNumberOfFonts(data); + if (result == -1) { + return error.stbtt_GetNumberOfFonts; + } + return @intCast(result); + } + + pub fn getFontOffsetForIndex(data: [*c]const u8, index: u32) ?u32 { + const result = c.stbtt_GetFontOffsetForIndex(data, @intCast(index)); + if (result == -1) { + return null; + } + return @intCast(result); + } + + pub fn init(data: [*c]const u8, offset: u32) !Font { + var font: Font = .{ + .info = undefined + }; + if (c.stbtt_InitFont(&font.info, data, @intCast(offset)) == 0) { + return error.stbtt_InitFont; + } + return font; + } + + pub fn findGlyphIndex(self: Font, codepoint: u21) ?u32 { + const result = c.stbtt_FindGlyphIndex(&self.info, codepoint); + if (result == 0) { + return null; + } + return @intCast(result); + } + + pub fn scaleForPixelHeight(self: Font, pixel_height: f32) f32 { + return c.stbtt_ScaleForPixelHeight(&self.info, pixel_height); + } + + pub fn getGlyphBitmapBox(self: Font, glyph: u32, scale_x: f32, scale_y: f32) Box { + var result: Box = undefined; + c.stbtt_GetGlyphBitmapBox( + &self.info, + @intCast(glyph), + scale_x, scale_y, + &result.x0, &result.y0, + &result.x1, &result.y1, + ); + return result; + } + + pub fn makeGlyphBitmap( + self: Font, + output: [*]u8, + out_w: u32, + out_h: u32, + out_stride: u32, + scale_x: f32, + scale_y: f32, + glyph: u32 + ) void { + c.stbtt_MakeGlyphBitmap( + &self.info, + output, + @intCast(out_w), + @intCast(out_h), + @intCast(out_stride), + scale_x, + scale_y, + @intCast(glyph) + ); + } + + pub fn getGlyphHMetrics(self: Font, glyph: u32) HMetrics { + var result: HMetrics = undefined; + c.stbtt_GetGlyphHMetrics(&self.info, @intCast(glyph), &result.advance_width, &result.left_side_bearing); + return result; + } + + pub fn getFontVMetrics(self: Font) VMetrics { + var result: VMetrics = undefined; + c.stbtt_GetFontVMetrics(&self.info, &result.ascent, &result.descent, &result.line_gap); + return result; + } + + pub fn getGlyphKernAdvance(self: Font, prev_glyph: u32, next_glyph: u32) i32 { + return c.stbtt_GetGlyphKernAdvance(&self.info, @intCast(prev_glyph), @intCast(next_glyph)); + } +}; + diff --git a/libs/stb/src/stb_truetype_impl.c b/libs/stb/src/stb_truetype_impl.c new file mode 100644 index 0000000..9f0bf61 --- /dev/null +++ b/libs/stb/src/stb_truetype_impl.c @@ -0,0 +1,4 @@ +#define STB_TRUETYPE_IMPLEMENTATION +// TODO: #define STBTT_STATIC +#include "stb_truetype.h" + diff --git a/libs/stb/src/stb_vorbis.zig b/libs/stb/src/stb_vorbis.zig new file mode 100644 index 0000000..ea25583 --- /dev/null +++ b/libs/stb/src/stb_vorbis.zig @@ -0,0 +1,170 @@ +const std = @import("std"); +const assert = std.debug.assert; +const log = std.log.scoped(.stb_vorbis); + +const c = @cImport({ + @cDefine("STB_VORBIS_NO_INTEGER_CONVERSION", {}); + @cDefine("STB_VORBIS_NO_STDIO", {}); + @cDefine("STB_VORBIS_HEADER_ONLY", {}); + @cInclude("stb_vorbis.c"); +}); + +const STBVorbis = @This(); + +pub const Error = error { + NeedMoreData, + InvalidApiMixing, + OutOfMemory, + FeatureNotSupported, + TooManyChannels, + FileOpenFailure, + SeekWithoutLength, + UnexpectedEof, + SeekInvalid, + InvalidSetup, + InvalidStream, + MissingCapturePattern, + InvalidStreamStructureVersion, + ContinuedPacketFlagInvalid, + IncorrectStreamSerialNumber, + InvalidFirstPage, + BadPacketType, + CantFindLastPage, + SeekFailed, + OggSkeletonNotSupported, + + Unknown +}; + +fn errorFromInt(err: c_int) ?Error { + return switch (err) { + c.VORBIS__no_error => null, + c.VORBIS_need_more_data => Error.NeedMoreData, + c.VORBIS_invalid_api_mixing => Error.InvalidApiMixing, + c.VORBIS_outofmem => Error.OutOfMemory, + c.VORBIS_feature_not_supported => Error.FeatureNotSupported, + c.VORBIS_too_many_channels => Error.TooManyChannels, + c.VORBIS_file_open_failure => Error.FileOpenFailure, + c.VORBIS_seek_without_length => Error.SeekWithoutLength, + c.VORBIS_unexpected_eof => Error.UnexpectedEof, + c.VORBIS_seek_invalid => Error.SeekInvalid, + c.VORBIS_invalid_setup => Error.InvalidSetup, + c.VORBIS_invalid_stream => Error.InvalidStream, + c.VORBIS_missing_capture_pattern => Error.MissingCapturePattern, + c.VORBIS_invalid_stream_structure_version => Error.InvalidStreamStructureVersion, + c.VORBIS_continued_packet_flag_invalid => Error.ContinuedPacketFlagInvalid, + c.VORBIS_incorrect_stream_serial_number => Error.IncorrectStreamSerialNumber, + c.VORBIS_invalid_first_page => Error.InvalidFirstPage, + c.VORBIS_bad_packet_type => Error.BadPacketType, + c.VORBIS_cant_find_last_page => Error.CantFindLastPage, + c.VORBIS_seek_failed => Error.SeekFailed, + c.VORBIS_ogg_skeleton_not_supported => Error.OggSkeletonNotSupported, + else => Error.Unknown + }; +} + +handle: *c.stb_vorbis, + +pub fn init(data: []const u8, alloc_buffer: []u8) Error!STBVorbis { + const stb_vorbis_alloc: c.stb_vorbis_alloc = .{ + .alloc_buffer = alloc_buffer.ptr, + .alloc_buffer_length_in_bytes = @intCast(alloc_buffer.len) + }; + var error_code: c_int = -1; + const handle = c.stb_vorbis_open_memory( + data.ptr, + @intCast(data.len), + &error_code, + &stb_vorbis_alloc + ); + if (handle == null) { + return errorFromInt(error_code) orelse Error.Unknown; + } + + return STBVorbis{ + .handle = handle.? + }; +} + +pub fn getMinimumAllocBufferSize(self: STBVorbis) u32 { + const info = self.getInfo(); + return info.setup_memory_required + @max(info.setup_temp_memory_required, info.temp_memory_required); +} + +fn getLastError(self: STBVorbis) ?Error { + const error_code = c.stb_vorbis_get_error(self.handle); + return errorFromInt(error_code); +} + +pub fn seek(self: STBVorbis, sample_number: u32) !void { + const success = c.stb_vorbis_seek(self.handle, sample_number); + if (success != 1) { + return self.getLastError() orelse Error.Unknown; + } +} + +pub fn getStreamLengthInSamples(self: STBVorbis) u32 { + return c.stb_vorbis_stream_length_in_samples(self.handle); +} + +pub fn getStreamLengthInSeconds(self: STBVorbis) f32 { + return c.stb_vorbis_stream_length_in_seconds(self.handle); +} + +pub fn getSamples( + self: STBVorbis, + channels: []const [*]f32, + max_samples_per_channel: u32 +) u32 { + const samples_per_channel = c.stb_vorbis_get_samples_float( + self.handle, + @intCast(channels.len), + @constCast(@ptrCast(channels.ptr)), + @intCast(max_samples_per_channel) + ); + return @intCast(samples_per_channel); +} + +const Frame = struct { + channels: []const [*c]const f32, + samples_per_channel: u32 +}; + +pub fn getFrame(self: STBVorbis) Frame { + var output: [*c][*c]f32 = null; + var channels: c_int = undefined; + const samples_per_channel = c.stb_vorbis_get_frame_float( + self.handle, + &channels, + &output + ); + return Frame{ + .channels = output[0..@intCast(channels)], + .samples_per_channel = @intCast(samples_per_channel) + }; +} + +pub const Info = struct { + sample_rate: u32, + channels: u32, + + setup_memory_required: u32, + setup_temp_memory_required: u32, + temp_memory_required: u32, + + max_frame_size: u32 +}; + +pub fn getInfo(self: STBVorbis) Info { + const info = c.stb_vorbis_get_info(self.handle); + return Info{ + .sample_rate = info.sample_rate, + .channels = @intCast(info.channels), + + .setup_memory_required = info.setup_memory_required, + .setup_temp_memory_required = info.setup_temp_memory_required, + .temp_memory_required = info.temp_memory_required, + + .max_frame_size = @intCast(info.max_frame_size), + }; +} diff --git a/libs/stb/src/stb_vorbis_impl.c b/libs/stb/src/stb_vorbis_impl.c new file mode 100644 index 0000000..4a53456 --- /dev/null +++ b/libs/stb/src/stb_vorbis_impl.c @@ -0,0 +1,3 @@ +#define STB_VORBIS_NO_INTEGER_CONVERSION +#define STB_VORBIS_NO_STDIO +#include "stb_vorbis.c" diff --git a/libs/tiled/build.zig b/libs/tiled/build.zig new file mode 100644 index 0000000..e717649 --- /dev/null +++ b/libs/tiled/build.zig @@ -0,0 +1,29 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const mod = b.addModule("tiled", .{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("src/root.zig") + }); + + const lib = b.addLibrary(.{ + .name = "tiled", + .root_module = mod + }); + b.installArtifact(lib); + + { + const tests = b.addTest(.{ + .root_module = mod + }); + + const run_tests = b.addRunArtifact(tests); + + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_tests.step); + } +} diff --git a/libs/tiled/src/buffers.zig b/libs/tiled/src/buffers.zig new file mode 100644 index 0000000..b9ab166 --- /dev/null +++ b/libs/tiled/src/buffers.zig @@ -0,0 +1,18 @@ +const std = @import("std"); + +const Position = @import("./position.zig"); + +const Buffers = @This(); + +allocator: std.mem.Allocator, +points: std.ArrayList(Position) = .empty, + +pub fn init(gpa: std.mem.Allocator) Buffers { + return Buffers{ + .allocator = gpa + }; +} + +pub fn deinit(self: *Buffers) void { + self.points.deinit(self.allocator); +} diff --git a/libs/tiled/src/color.zig b/libs/tiled/src/color.zig new file mode 100644 index 0000000..d8cd764 --- /dev/null +++ b/libs/tiled/src/color.zig @@ -0,0 +1,52 @@ +const std = @import("std"); + +const Color = @This(); + +r: u8, +g: u8, +b: u8, +a: u8, + +pub const black = Color{ + .r = 0, + .g = 0, + .b = 0, + .a = 255, +}; + +pub fn parse(str: []const u8, hash_required: bool) !Color { + var color = Color{ + .r = undefined, + .g = undefined, + .b = undefined, + .a = 255, + }; + + if (str.len < 1) { + return error.InvalidColorFormat; + } + + const has_hash = str[0] == '#'; + if (hash_required and !has_hash) { + return error.InvalidColorFormat; + } + + const hex_str = if (has_hash) str[1..] else str; + + if (hex_str.len == 6) { + color.r = try std.fmt.parseInt(u8, hex_str[0..2], 16); + color.g = try std.fmt.parseInt(u8, hex_str[2..4], 16); + color.b = try std.fmt.parseInt(u8, hex_str[4..6], 16); + + } else if (hex_str.len == 8) { + color.a = try std.fmt.parseInt(u8, hex_str[0..2], 16); + color.r = try std.fmt.parseInt(u8, hex_str[2..4], 16); + color.g = try std.fmt.parseInt(u8, hex_str[4..6], 16); + color.b = try std.fmt.parseInt(u8, hex_str[6..8], 16); + + } else { + return error.InvalidColorFormat; + } + + return color; +} diff --git a/libs/tiled/src/global_tile_id.zig b/libs/tiled/src/global_tile_id.zig new file mode 100644 index 0000000..96d93ad --- /dev/null +++ b/libs/tiled/src/global_tile_id.zig @@ -0,0 +1,15 @@ + +pub const Flag = enum(u32) { + flipped_horizontally = 1 << 31, // bit 32 + flipped_vertically = 1 << 30, // bit 31 + flipped_diagonally = 1 << 29, // bit 30 + rotated_hexagonal_120 = 1 << 28, // bit 29 + _, + + pub const clear: u32 = ~( + @intFromEnum(Flag.flipped_horizontally) | + @intFromEnum(Flag.flipped_vertically) | + @intFromEnum(Flag.flipped_diagonally) | + @intFromEnum(Flag.rotated_hexagonal_120) + ); +}; diff --git a/libs/tiled/src/layer.zig b/libs/tiled/src/layer.zig new file mode 100644 index 0000000..4efc102 --- /dev/null +++ b/libs/tiled/src/layer.zig @@ -0,0 +1,283 @@ +const std = @import("std"); + +const Property = @import("./property.zig"); +const xml = @import("./xml.zig"); +const Color = @import("./color.zig"); +const Object = @import("./object.zig"); +const Position = @import("./position.zig"); + +const Layer = @This(); + +pub const TileVariant = struct { + width: u32, + height: u32, + data: []u32, + + fn initDataFromXml( + arena: std.mem.Allocator, + scratch: *std.heap.ArenaAllocator, + lexer: *xml.Lexer, + ) ![]u32 { + var iter = xml.TagParser.init(lexer); + const attrs = try iter.begin("data"); + + const encoding = attrs.get("encoding") orelse "csv"; + // TODO: compression + + var temp_tiles: std.ArrayList(u32) = .empty; + + if (std.mem.eql(u8, encoding, "csv")) { + const text = try lexer.nextExpectText(); + var split_iter = std.mem.splitScalar(u8, text, ','); + while (split_iter.next()) |raw_tile_id| { + const tile_id_str = std.mem.trim(u8, raw_tile_id, &std.ascii.whitespace); + const tile_id = try std.fmt.parseInt(u32, tile_id_str, 10); + try temp_tiles.append(scratch.allocator(), tile_id); + } + } else { + return error.UnknownEncodingType; + } + + try iter.finish("data"); + + return try arena.dupe(u32, temp_tiles.items); + } + + pub fn get(self: TileVariant, x: usize, y: usize) ?u32 { + if ((0 <= x and x < self.width) and (0 <= y and y < self.height)) { + return self.data[y * self.width + x]; + } + return null; + } +}; + +pub const ImageVariant = struct { + pub const Image = struct { + // TODO: format + source: []const u8, + transparent_color: ?Color, + width: ?u32, + height: ?u32, + + fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image { + var iter = xml.TagParser.init(lexer); + const attrs = try iter.begin("image"); + + // TODO: format + const source = try attrs.getDupe(arena, "source") orelse return error.MissingSource; + const width = try attrs.getNumber(u32, "width") orelse null; + const height = try attrs.getNumber(u32, "height") orelse null; + const transparent_color = try attrs.getColor("trans", false); + + try iter.finish("image"); + + return Image{ + .source = source, + .transparent_color = transparent_color, + .width = width, + .height = height + }; + } + }; + + repeat_x: bool, + repeat_y: bool, + image: ?Image +}; + +pub const ObjectVariant = struct { + pub const DrawOrder = enum { + top_down, + index, + + const map: std.StaticStringMap(DrawOrder) = .initComptime(.{ + .{ "topdown", .top_down }, + .{ "index", .index }, + }); + }; + + color: ?Color, + draw_order: DrawOrder, + items: []Object +}; + +pub const GroupVariant = struct { + layers: []Layer +}; + +pub const Type = enum { + tile, + object, + image, + group, + + const name_map: std.StaticStringMap(Type) = .initComptime(.{ + .{ "layer", .tile }, + .{ "objectgroup", .object }, + .{ "imagelayer", .image }, + .{ "group", .group } + }); + + fn toXmlName(self: Type) []const u8 { + return switch (self) { + .tile => "layer", + .object => "objectgroup", + .image => "imagelayer", + .group => "group", + }; + } +}; + +pub const Variant = union(Type) { + tile: TileVariant, + object: ObjectVariant, + image: ImageVariant, + group: GroupVariant +}; + +id: u32, +name: []const u8, +class: []const u8, +opacity: f32, +visible: bool, +tint_color: ?Color, +offset_x: f32, +offset_y: f32, +parallax_x: f32, +parallax_y: f32, +properties: Property.List, +variant: Variant, + +pub fn initFromXml( + arena: std.mem.Allocator, + scratch: *std.heap.ArenaAllocator, + lexer: *xml.Lexer, +) !Layer { + const value = try lexer.peek() orelse return error.MissingStartTag; + if (value != .start_tag) return error.MissingStartTag; + + var layer_type = Type.name_map.get(value.start_tag.name) orelse return error.UnknownLayerType; + + var iter = xml.TagParser.init(lexer); + const attrs = try iter.begin(layer_type.toXmlName()); + + const id = try attrs.getNumber(u32, "id") orelse return error.MissingId; + const name = try attrs.getDupe(arena, "name") orelse ""; + const class = try attrs.getDupe(arena, "class") orelse ""; + const opacity = try attrs.getNumber(f32, "opacity") orelse 1; + const visible = try attrs.getBool("visible", "1", "0") orelse true; + const offset_x = try attrs.getNumber(f32, "offsetx") orelse 0; + const offset_y = try attrs.getNumber(f32, "offsety") orelse 0; + const parallax_x = try attrs.getNumber(f32, "parallaxx") orelse 1; + const parallax_y = try attrs.getNumber(f32, "parallaxy") orelse 1; + const tint_color = try attrs.getColor("tintcolor", true); + + var variant: Variant = undefined; + switch (layer_type) { + .tile => { + const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth; + const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight; + variant = .{ + .tile = TileVariant{ + .width = width, + .height = height, + .data = &[0]u32{} + } + }; + }, + .image => { + const repeat_x = try attrs.getBool("repeatx", "1", "0") orelse false; + const repeat_y = try attrs.getBool("repeaty", "1", "0") orelse false; + variant = .{ + .image = ImageVariant{ + .repeat_x = repeat_x, + .repeat_y = repeat_y, + .image = null + } + }; + }, + .object => { + const draw_order = try attrs.getEnum(ObjectVariant.DrawOrder, "draworder", ObjectVariant.DrawOrder.map) orelse .top_down; + const color = try attrs.getColor("color", true); + + variant = .{ + .object = ObjectVariant{ + .color = color, + .draw_order = draw_order, + .items = &.{} + } + }; + }, + .group => { + variant = .{ + .group = .{ + .layers = &.{} + } + }; + }, + } + + var properties: Property.List = .empty; + var objects: std.ArrayList(Object) = .empty; + var layers: std.ArrayList(Layer) = .empty; + + while (try iter.next()) |node| { + if (node.isTag("properties")) { + properties = try Property.List.initFromXml(arena, scratch, lexer); + continue; + } + + if (variant == .tile and node.isTag("data")) { + variant.tile.data = try TileVariant.initDataFromXml(arena, scratch, lexer); + continue; + } + + if (variant == .image and node.isTag("image")) { + variant.image.image = try ImageVariant.Image.initFromXml(arena, lexer); + continue; + } + + if (variant == .object and node.isTag("object")) { + const object = try Object.initFromXml(arena, scratch, lexer); + try objects.append(scratch.allocator(), object); + continue; + } + + if (variant == .group and isLayerNode(node)) { + const layer = try initFromXml(arena, scratch, lexer); + try layers.append(scratch.allocator(), layer); + continue; + } + + try iter.skip(); + } + + try iter.finish(layer_type.toXmlName()); + + if (variant == .object) { + variant.object.items = try arena.dupe(Object, objects.items); + } + + if (variant == .group) { + variant.group.layers = try arena.dupe(Layer, layers.items); + } + + return Layer{ + .id = id, + .name = name, + .class = class, + .opacity = opacity, + .visible = visible, + .tint_color = tint_color, + .offset_x = offset_x, + .offset_y = offset_y, + .parallax_x = parallax_x, + .parallax_y = parallax_y, + .properties = properties, + .variant = variant, + }; +} + +pub fn isLayerNode(node: xml.TagParser.Node) bool { + return node.isTag("layer") or node.isTag("objectgroup") or node.isTag("imagelayer") or node.isTag("group"); +} diff --git a/libs/tiled/src/object.zig b/libs/tiled/src/object.zig new file mode 100644 index 0000000..e52ba05 --- /dev/null +++ b/libs/tiled/src/object.zig @@ -0,0 +1,421 @@ +const std = @import("std"); + +const xml = @import("./xml.zig"); +const Position = @import("./position.zig"); +const Color = @import("./color.zig"); + +const Object = @This(); + +pub const Shape = union (Type) { + pub const Type = enum { + rectangle, + point, + ellipse, + polygon, + tile, + // TODO: template + text + }; + + pub const Rectangle = struct { + x: f32, + y: f32, + width: f32, + height: f32, + }; + + pub const Tile = struct { + x: f32, + y: f32, + width: f32, + height: f32, + gid: u32 + }; + + pub const Ellipse = struct { + x: f32, + y: f32, + width: f32, + height: f32, + }; + + pub const Polygon = struct { + x: f32, + y: f32, + points: []const Position + }; + + pub const Text = struct { + pub const Font = struct { + family: []const u8, + pixel_size: f32, + bold: bool, + italic: bool, + underline: bool, + strikeout: bool, + kerning: bool + }; + + pub const HorizontalAlign = enum { + left, + center, + right, + justify, + + const map: std.StaticStringMap(HorizontalAlign) = .initComptime(.{ + .{ "left", .left }, + .{ "center", .center }, + .{ "right", .right }, + .{ "justify", .justify }, + }); + }; + + pub const VerticalAlign = enum { + top, + center, + bottom, + + const map: std.StaticStringMap(VerticalAlign) = .initComptime(.{ + .{ "top", .top }, + .{ "center", .center }, + .{ "bottom", .bottom }, + }); + }; + + x: f32, + y: f32, + width: f32, + height: f32, + word_wrap: bool, + color: Color, + font: Font, + horizontal_align: HorizontalAlign, + vertical_align: VerticalAlign, + content: []const u8 + }; + + pub const Point = struct { + x: f32, + y: f32, + }; + + rectangle: Rectangle, + point: Point, + ellipse: Ellipse, + polygon: Polygon, + tile: Tile, + text: Text +}; + +id: u32, +name: []const u8, +class: []const u8, +rotation: f32, // TODO: maybe this field should be moved to Shape struct +visible: bool, +shape: Shape, + +pub fn initFromXml( + arena: std.mem.Allocator, + scratch: *std.heap.ArenaAllocator, + lexer: *xml.Lexer, +) !Object { + var iter = xml.TagParser.init(lexer); + const attrs = try iter.begin("object"); + + const id = try attrs.getNumber(u32, "id") orelse return error.MissingId; + const name = try attrs.getDupe(arena, "name") orelse ""; + const class = try attrs.getDupe(arena, "type") orelse ""; + const x = try attrs.getNumber(f32, "x") orelse 0; + const y = try attrs.getNumber(f32, "y") orelse 0; + const width = try attrs.getNumber(f32, "width") orelse 0; + const height = try attrs.getNumber(f32, "height") orelse 0; + const rotation = try attrs.getNumber(f32, "rotation") orelse 0; + const visible = try attrs.getBool("visible", "1", "0") orelse true; + + var shape: ?Shape = null; + + while (try iter.next()) |node| { + if (shape == null) { + if (node.isTag("point")) { + shape = .{ + .point = Shape.Point{ + .x = x, + .y = y, + } + }; + + } else if (node.isTag("ellipse")) { + shape = .{ + .ellipse = Shape.Ellipse{ + .x = x, + .y = y, + .width = width, + .height = height + } + }; + + } else if (node.isTag("text")) { + const HorizontalShape = Shape.Text.HorizontalAlign; + const VerticalAlign = Shape.Text.VerticalAlign; + + const text_attrs = node.tag.attributes; + const word_wrap = try text_attrs.getBool("wrap", "1", "0") orelse false; + const color = try text_attrs.getColor("color", true) orelse Color.black; + const horizontal_align = try text_attrs.getEnum(HorizontalShape, "halign", HorizontalShape.map) orelse .left; + const vertical_align = try text_attrs.getEnum(VerticalAlign, "valign", VerticalAlign.map) orelse .top; + const bold = try text_attrs.getBool("bold", "1", "0") orelse false; + const italic = try text_attrs.getBool("italic", "1", "0") orelse false; + const strikeout = try text_attrs.getBool("strikeout", "1", "0") orelse false; + const underline = try text_attrs.getBool("underline", "1", "0") orelse false; + const kerning = try text_attrs.getBool("kerning", "1", "0") orelse true; + const pixel_size = try text_attrs.getNumber(f32, "pixelsize") orelse 16; + const font_family = try text_attrs.getDupe(arena, "fontfamily") orelse "sans-serif"; + + _ = try lexer.nextExpectStartTag("text"); + var content: []const u8 = ""; + const content_value = try lexer.peek(); + if (content_value != null and content_value.? == .text) { + content = try arena.dupe(u8, content_value.?.text); + } + try lexer.skipUntilMatchingEndTag("text"); + + shape = .{ + .text = Shape.Text{ + .x = x, + .y = y, + .width = width, + .height = height, + .word_wrap = word_wrap, + .color = color, + .horizontal_align = horizontal_align, + .vertical_align = vertical_align, + .content = try arena.dupe(u8, content), + .font = .{ + .bold = bold, + .italic = italic, + .strikeout = strikeout, + .underline = underline, + .pixel_size = pixel_size, + .kerning = kerning, + .family = font_family, + } + } + }; + continue; + + } else if (node.isTag("polygon")) { + const points_str = node.tag.attributes.get("points") orelse ""; + + var points: std.ArrayList(Position) = .empty; + var point_iter = std.mem.splitScalar(u8, points_str, ' '); + while (point_iter.next()) |point_str| { + const point = try Position.parseCommaDelimited(point_str); + try points.append(scratch.allocator(), point); + } + + shape = .{ + .polygon = Shape.Polygon{ + .x = x, + .y = y, + .points = try arena.dupe(Position, points.items) + } + }; + } + } + + try iter.skip(); + } + + if (shape == null) { + if (try attrs.getNumber(u32, "gid")) |gid| { + shape = .{ + .tile = Shape.Tile{ + .x = x, + .y = y, + .width = width, + .height = height, + .gid = gid + } + }; + } else { + shape = .{ + .rectangle = Shape.Rectangle{ + .x = x, + .y = y, + .width = width, + .height = height + } + }; + } + } + + try iter.finish("object"); + + return Object{ + .id = id, + .name = name, + .class = class, + .rotation = rotation, + .visible = visible, + .shape = shape orelse return error.UnknownShapeType + }; +} + +fn expectParsedEquals(expected: Object, body: []const u8) !void { + const allocator = std.testing.allocator; + var ctx: xml.Lexer.TestingContext = undefined; + ctx.init(allocator, body); + defer ctx.deinit(); + + var scratch = std.heap.ArenaAllocator.init(allocator); + defer scratch.deinit(); + + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + const parsed = try initFromXml(arena.allocator(), &scratch, &ctx.lexer); + try std.testing.expectEqualDeep(expected, parsed); +} + +test Object { + try expectParsedEquals( + Object{ + .id = 10, + .name = "rectangle", + .class = "object class", + .rotation = 0, + .visible = true, + .shape = .{ + .rectangle = .{ + .x = 12.34, + .y = 56.78, + .width = 31.5, + .height = 20.25, + } + } + }, + \\ + ); + + try expectParsedEquals( + Object{ + .id = 3, + .name = "point", + .class = "foo", + .rotation = 0, + .visible = true, + .shape = .{ + .point = .{ + .x = 77.125, + .y = 99.875 + } + } + }, + \\ + \\ + \\ + ); + + try expectParsedEquals( + Object{ + .id = 4, + .name = "ellipse", + .class = "", + .rotation = 0, + .visible = true, + .shape = .{ + .ellipse = .{ + .x = 64.25, + .y = 108.25, + .width = 22.375, + .height = 15.375 + } + } + }, + \\ + \\ + \\ + ); + + try expectParsedEquals( + Object{ + .id = 5, + .name = "", + .class = "", + .rotation = 0, + .visible = true, + .shape = .{ + .polygon = .{ + .x = 40.125, + .y = 96.25, + .points = &[_]Position{ + .{ .x = 0, .y = 0 }, + .{ .x = 13.25, .y = -4.25 }, + .{ .x = 10.125, .y = 18.625 }, + .{ .x = 2.25, .y = 17.375 }, + .{ .x = -0.125, .y = 25.75 }, + .{ .x = -3.875, .y = 20.75 }, + } + } + } + }, + \\ + \\ + \\ + ); + + try expectParsedEquals( + Object{ + .id = 2, + .name = "tile", + .class = "", + .rotation = 0, + .visible = true, + .shape = .{ + .tile = .{ + .x = 60.125, + .y = 103.5, + .width = 8, + .height = 8, + .gid = 35 + } + } + }, + \\ + ); + + try expectParsedEquals( + Object{ + .id = 6, + .name = "text", + .class = "", + .rotation = 0, + .visible = true, + .shape = .{ + .text = .{ + .x = 64.3906, + .y = 92.8594, + .width = 87.7188, + .height = 21.7813, + .content = "Hello World", + .word_wrap = true, + .color = .black, + .horizontal_align = .center, + .vertical_align = .top, + .font = .{ + .family = "sans-serif", + .pixel_size = 16, + .bold = false, + .italic = false, + .underline = false, + .strikeout = false, + .kerning = true + }, + } + } + }, + \\ + \\ Hello World + \\ + ); +} diff --git a/libs/tiled/src/position.zig b/libs/tiled/src/position.zig new file mode 100644 index 0000000..79179d4 --- /dev/null +++ b/libs/tiled/src/position.zig @@ -0,0 +1,20 @@ +const std = @import("std"); + +const Position = @This(); + +x: f32, +y: f32, + +pub fn parseCommaDelimited(str: []const u8) !Position { + const comma_index = std.mem.indexOfScalar(u8, str, ',') orelse return error.MissingComma; + const x_str = str[0..comma_index]; + const y_str = str[(comma_index+1)..]; + + const x = try std.fmt.parseFloat(f32, x_str); + const y = try std.fmt.parseFloat(f32, y_str); + + return Position{ + .x = x, + .y = y + }; +} diff --git a/libs/tiled/src/property.zig b/libs/tiled/src/property.zig new file mode 100644 index 0000000..1fa29df --- /dev/null +++ b/libs/tiled/src/property.zig @@ -0,0 +1,153 @@ +const std = @import("std"); +const xml = @import("./xml.zig"); + +const Property = @This(); + +pub const Type = enum { + string, + int, + bool, + + const map: std.StaticStringMap(Type) = .initComptime(.{ + .{ "string", .string }, + .{ "int", .int }, + .{ "bool", .bool }, + }); +}; + +pub const Value = union(Type) { + string: []const u8, + int: i32, + bool: bool +}; + +name: []const u8, +value: Value, + +pub const List = struct { + items: []Property, + + pub const empty = List{ + .items = &[0]Property{} + }; + + pub fn initFromXml( + arena: std.mem.Allocator, + scratch: *std.heap.ArenaAllocator, + lexer: *xml.Lexer + ) !Property.List { + var iter = xml.TagParser.init(lexer); + _ = try iter.begin("properties"); + + var temp_properties: std.ArrayList(Property) = .empty; + + while (try iter.next()) |node| { + if (node.isTag("property")) { + const property = try Property.initFromXml(arena, lexer); + try temp_properties.append(scratch.allocator(), property); + continue; + } + + try iter.skip(); + } + try iter.finish("properties"); + + const properties = try arena.dupe(Property, temp_properties.items); + + return List{ + .items = properties + }; + } + + pub fn get(self: List, name: []const u8) ?Value { + for (self.items) |item| { + if (std.mem.eql(u8, item.name, name)) { + return item.value; + } + } + return null; + } + + pub fn getString(self: List, name: []const u8) ?[]const u8 { + if (self.get(name)) |value| { + return value.string; + } + return null; + } +}; + +pub fn init(name: []const u8, value: Value) Property { + return Property{ + .name = name, + .value = value + }; +} + +pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Property { + var iter = xml.TagParser.init(lexer); + const attrs = try iter.begin("property"); + + const name = try attrs.getDupe(arena, "name") orelse return error.MissingName; + const prop_type_str = attrs.get("type") orelse "string"; + const value_str = attrs.get("value") orelse ""; + + const prop_type = Type.map.get(prop_type_str) orelse return error.UnknownPropertyType; + const value = switch(prop_type) { + .string => Value{ + .string = try arena.dupe(u8, value_str) + }, + .int => Value{ + .int = try std.fmt.parseInt(i32, value_str, 10) + }, + .bool => Value{ + .bool = std.mem.eql(u8, value_str, "true") + } + }; + + try iter.finish("property"); + + return Property{ + .name = name, + .value = value + }; +} + +fn expectParsedEquals(expected: Property, body: []const u8) !void { + const allocator = std.testing.allocator; + var ctx: xml.Lexer.TestingContext = undefined; + ctx.init(allocator, body); + defer ctx.deinit(); + + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + const parsed = try initFromXml(arena.allocator(), &ctx.lexer); + try std.testing.expectEqualDeep(expected, parsed); +} + +test Property { + try expectParsedEquals( + Property.init("solid", .{ .string = "hello" }), + \\ + ); + + try expectParsedEquals( + Property.init("solid", .{ .string = "hello" }), + \\ + ); + + try expectParsedEquals( + Property.init("integer", .{ .int = 123 }), + \\ + ); + + try expectParsedEquals( + Property.init("boolean", .{ .bool = true }), + \\ + ); + + try expectParsedEquals( + Property.init("boolean", .{ .bool = false }), + \\ + ); +} diff --git a/libs/tiled/src/root.zig b/libs/tiled/src/root.zig new file mode 100644 index 0000000..50a97ee --- /dev/null +++ b/libs/tiled/src/root.zig @@ -0,0 +1,16 @@ +const std = @import("std"); + +// Warning: +// This library is not complete, it does not cover all features that the specification provides. +// But there are enough features implemented so that I could use this for my games. + +// Map format specification: +// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/# + +pub const xml = @import("./xml.zig"); +pub const Tileset = @import("./tileset.zig"); +pub const Tilemap = @import("./tilemap.zig"); + +test { + _ = std.testing.refAllDeclsRecursive(@This()); +} diff --git a/libs/tiled/src/tilemap.zig b/libs/tiled/src/tilemap.zig new file mode 100644 index 0000000..c6cde2e --- /dev/null +++ b/libs/tiled/src/tilemap.zig @@ -0,0 +1,235 @@ +const std = @import("std"); +const xml = @import("./xml.zig"); +const Io = std.Io; +const assert = std.debug.assert; + +const Property = @import("./property.zig"); +const Layer = @import("./layer.zig"); +const Object = @import("./object.zig"); +const Position = @import("./position.zig"); +const Tileset = @import("./tileset.zig"); +const GlobalTileId = @import("./global_tile_id.zig"); + +const Tilemap = @This(); + +pub const Orientation = enum { + orthogonal, + staggered, + hexagonal, + isometric, + + const map: std.StaticStringMap(Orientation) = .initComptime(.{ + .{ "orthogonal", .orthogonal }, + .{ "staggered", .staggered }, + .{ "hexagonal", .hexagonal }, + .{ "isometric", .isometric } + }); +}; + +pub const RenderOrder = enum { + right_down, + right_up, + left_down, + left_up, + + const map: std.StaticStringMap(RenderOrder) = .initComptime(.{ + .{ "right-down", .right_down }, + .{ "right-up", .right_up }, + .{ "left-down", .left_down }, + .{ "left-up", .left_up }, + }); +}; + +pub const StaggerAxis = enum { + x, + y, + + const map: std.StaticStringMap(StaggerAxis) = .initComptime(.{ + .{ "x", .x }, + .{ "y", .y } + }); +}; + +pub const TilesetReference = struct { + source: []const u8, + first_gid: u32, + + pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !TilesetReference { + var iter = xml.TagParser.init(lexer); + const attrs = try iter.begin("tileset"); + + const source = try attrs.getDupe(arena, "source") orelse return error.MissingFirstGid; + const first_gid = try attrs.getNumber(u32, "firstgid") orelse return error.MissingFirstGid; + + try iter.finish("tileset"); + + return TilesetReference{ + .source = source, + .first_gid = first_gid + }; + } +}; + +pub const Tile = struct { + tileset: *const Tileset, + id: u32, + + pub fn getProperties(self: Tile) Property.List { + return self.tileset.getTileProperties(self.id) orelse .empty; + } +}; + +arena: std.heap.ArenaAllocator, + +version: []const u8, +tiled_version: ?[]const u8, +orientation: Orientation, +render_order: RenderOrder, +width: u32, +height: u32, +tile_width: u32, +tile_height: u32, +infinite: bool, + +stagger_axis: ?StaggerAxis, +next_layer_id: u32, +next_object_id: u32, + +tilesets: []TilesetReference, +layers: []Layer, + +pub fn initFromBuffer( + gpa: std.mem.Allocator, + scratch: *std.heap.ArenaAllocator, + xml_buffers: *xml.Lexer.Buffers, + buffer: []const u8 +) !Tilemap { + var reader = Io.Reader.fixed(buffer); + return initFromReader(gpa, scratch, xml_buffers, &reader); +} + +pub fn initFromReader( + gpa: std.mem.Allocator, + scratch: *std.heap.ArenaAllocator, + xml_buffers: *xml.Lexer.Buffers, + reader: *Io.Reader, +) !Tilemap { + var lexer = xml.Lexer.init(reader, xml_buffers); + return initFromXml(gpa, scratch, &lexer); +} + +// Map specification: +// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map +pub fn initFromXml( + gpa: std.mem.Allocator, + scratch: *std.heap.ArenaAllocator, + lexer: *xml.Lexer, +) !Tilemap { + var arena_allocator = std.heap.ArenaAllocator.init(gpa); + errdefer arena_allocator.deinit(); + + const arena = arena_allocator.allocator(); + + var iter = xml.TagParser.init(lexer); + const map_attrs = try iter.begin("map"); + + const version = try map_attrs.getDupe(arena, "version") orelse return error.MissingVersion; + const tiled_version = try map_attrs.getDupe(arena, "tiledversion"); + const orientation = try map_attrs.getEnum(Orientation, "orientation", Orientation.map) orelse return error.MissingOrientation; + const render_order = try map_attrs.getEnum(RenderOrder, "renderorder", RenderOrder.map) orelse return error.MissingRenderOrder; + // TODO: compressionlevel + const width = try map_attrs.getNumber(u32, "width") orelse return error.MissingWidth; + const height = try map_attrs.getNumber(u32, "height") orelse return error.MissingHeight; + const tile_width = try map_attrs.getNumber(u32, "tilewidth") orelse return error.MissingTileWidth; + const tile_height = try map_attrs.getNumber(u32, "tileheight") orelse return error.MissingTileHeight; + // TODO: hexidelength + const infinite_int = try map_attrs.getNumber(u32, "infinite") orelse 0; + const infinite = infinite_int != 0; + const next_layer_id = try map_attrs.getNumber(u32, "nextlayerid") orelse return error.MissingLayerId; + const next_object_id = try map_attrs.getNumber(u32, "nextobjectid") orelse return error.MissingObjectId; + // TODO: parallaxoriginx + // TODO: parallaxoriginy + // TODO: backgroundcolor + + var stagger_axis: ?StaggerAxis = null; + if (orientation == .hexagonal or orientation == .staggered) { + stagger_axis = try map_attrs.getEnum(StaggerAxis, "staggeraxis", StaggerAxis.map) orelse return error.MissingRenderOrder; + // TODO: staggerindex + } + + var tileset_list: std.ArrayList(TilesetReference) = .empty; + var layer_list: std.ArrayList(Layer) = .empty; + + while (try iter.next()) |node| { + if (node.isTag("tileset")) { + try tileset_list.append(scratch.allocator(), try TilesetReference.initFromXml(arena, lexer)); + continue; + } else if (Layer.isLayerNode(node)) { + const layer = try Layer.initFromXml( + arena, + scratch, + lexer, + ); + try layer_list.append(scratch.allocator(), layer); + continue; + } + + try iter.skip(); + } + try iter.finish("map"); + + const tilesets = try arena.dupe(TilesetReference, tileset_list.items); + const layers = try arena.dupe(Layer, layer_list.items); + + return Tilemap{ + .arena = arena_allocator, + .version = version, + .tiled_version = tiled_version, + .orientation = orientation, + .render_order = render_order, + .width = width, + .height = height, + .tile_width = tile_width, + .tile_height = tile_height, + .infinite = infinite, + .stagger_axis = stagger_axis, + .next_object_id = next_object_id, + .next_layer_id = next_layer_id, + .tilesets = tilesets, + .layers = layers, + }; +} + +fn getTilesetByGid(self: *const Tilemap, gid: u32) ?TilesetReference { + var result: ?TilesetReference = null; + for (self.tilesets) |tileset| { + if (gid < tileset.first_gid) { + continue; + } + if (result != null and result.?.first_gid < tileset.first_gid) { + continue; + } + result = tileset; + } + + return result; +} + +pub fn getTile(self: *const Tilemap, layer: *const Layer, tilesets: Tileset.List, x: usize, y: usize) ?Tile { + assert(layer.variant == .tile); + const tile_variant = layer.variant.tile; + + const gid = tile_variant.get(x, y) orelse return null; + const tileset_ref = self.getTilesetByGid(gid & GlobalTileId.Flag.clear) orelse return null; + const tileset = tilesets.get(tileset_ref.source) orelse return null; + const id = gid - tileset_ref.first_gid; + + return Tile{ + .tileset = tileset, + .id = id + }; +} + +pub fn deinit(self: *const Tilemap) void { + self.arena.deinit(); +} diff --git a/libs/tiled/src/tileset.zig b/libs/tiled/src/tileset.zig new file mode 100644 index 0000000..04b9305 --- /dev/null +++ b/libs/tiled/src/tileset.zig @@ -0,0 +1,226 @@ +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; + +const xml = @import("./xml.zig"); +const Property = @import("./property.zig"); +const Position = @import("./position.zig"); + +const Tileset = @This(); + +pub const Image = struct { + source: []const u8, + width: u32, + height: u32, + + pub fn intFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image { + var iter = xml.TagParser.init(lexer); + const attrs = try iter.begin("image"); + + const source = try attrs.getDupe(arena, "width") orelse return error.MissingSource; + const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth; + const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight; + + try iter.finish("image"); + + return Image{ + .source = source, + .width = width, + .height = height + }; + } +}; + +pub const Tile = struct { + id: u32, + properties: Property.List, + + pub fn initFromXml( + arena: std.mem.Allocator, + scratch: *std.heap.ArenaAllocator, + lexer: *xml.Lexer, + ) !Tile { + var iter = xml.TagParser.init(lexer); + const tile_attrs = try iter.begin("tile"); + + const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId; + + var properties: Property.List = .empty; + + while (try iter.next()) |node| { + if (node.isTag("properties")) { + properties = try Property.List.initFromXml(arena, scratch, lexer); + continue; + } + + try iter.skip(); + } + + try iter.finish("tile"); + + return Tile{ + .id = id, + .properties = properties + }; + } +}; + +pub const List = struct { + const Entry = struct { + name: []const u8, + tileset: Tileset + }; + + list: std.ArrayList(Entry), + + pub const empty = List{ + .list = .empty + }; + + pub fn add(self: *List, gpa: Allocator, name: []const u8, tileset: Tileset) !void { + if (self.get(name) != null) { + return error.DuplicateName; + } + + const name_dupe = try gpa.dupe(u8, name); + errdefer gpa.free(name_dupe); + + try self.list.append(gpa, .{ + .name = name_dupe, + .tileset = tileset + }); + } + + pub fn get(self: *const List, name: []const u8) ?*const Tileset { + for (self.list.items) |*entry| { + if (std.mem.eql(u8, entry.name, name)) { + return &entry.tileset; + } + } + return null; + } + + pub fn deinit(self: *List, gpa: Allocator) void { + for (self.list.items) |entry| { + gpa.free(entry.name); + entry.tileset.deinit(); + } + self.list.deinit(gpa); + } +}; + +arena: std.heap.ArenaAllocator, + +version: []const u8, +tiled_version: []const u8, +name: []const u8, +tile_width: u32, +tile_height: u32, +tile_count: u32, +columns: u32, + +image: Image, + +tiles: []Tile, + +pub fn initFromBuffer( + gpa: std.mem.Allocator, + scratch: *std.heap.ArenaAllocator, + xml_buffers: *xml.Lexer.Buffers, + buffer: []const u8 +) !Tileset { + var reader = Io.Reader.fixed(buffer); + return initFromReader(gpa, scratch, xml_buffers, &reader); +} + +pub fn initFromReader( + gpa: std.mem.Allocator, + scratch: *std.heap.ArenaAllocator, + xml_buffers: *xml.Lexer.Buffers, + reader: *Io.Reader, +) !Tileset { + var lexer = xml.Lexer.init(reader, xml_buffers); + return initFromXml(gpa, scratch, &lexer); +} + +pub fn initFromXml( + gpa: std.mem.Allocator, + scratch: *std.heap.ArenaAllocator, + lexer: *xml.Lexer +) !Tileset { + var arena_state = std.heap.ArenaAllocator.init(gpa); + const arena = arena_state.allocator(); + + var iter = xml.TagParser.init(lexer); + const tileset_attrs = try iter.begin("tileset"); + + const version = try tileset_attrs.getDupe(arena, "version") orelse return error.MissingTilesetTag; + const tiled_version = try tileset_attrs.getDupe(arena, "tiledversion") orelse return error.MissingTilesetTag; + const name = try tileset_attrs.getDupe(arena, "name") orelse return error.MissingTilesetTag; + const tile_width = try tileset_attrs.getNumber(u32, "tilewidth") orelse return error.MissingTilesetTag; + const tile_height = try tileset_attrs.getNumber(u32, "tileheight") orelse return error.MissingTilesetTag; + const tile_count = try tileset_attrs.getNumber(u32, "tilecount") orelse return error.MissingTilesetTag; + const columns = try tileset_attrs.getNumber(u32, "columns") orelse return error.MissingTilesetTag; + + var image: ?Image = null; + var tiles_list: std.ArrayList(Tile) = .empty; + + while (try iter.next()) |node| { + if (node.isTag("image")) { + image = try Image.intFromXml(arena, lexer); + continue; + } else if (node.isTag("tile")) { + const tile = try Tile.initFromXml(arena, scratch, lexer); + try tiles_list.append(scratch.allocator(), tile); + continue; + } + + try iter.skip(); + } + try iter.finish("tileset"); + + const tiles = try arena.dupe(Tile, tiles_list.items); + + return Tileset{ + .arena = arena_state, + .version = version, + .tiled_version = tiled_version, + .name = name, + .tile_width = tile_width, + .tile_height = tile_height, + .tile_count = tile_count, + .columns = columns, + .image = image orelse return error.MissingImageTag, + .tiles = tiles + }; +} + +pub fn getTileProperties(self: *const Tileset, id: u32) ?Property.List { + for (self.tiles) |tile| { + if (tile.id == id) { + return tile.properties; + } + } + return null; +} + +pub fn getTilePositionInImage(self: *const Tileset, id: u32) ?Position { + if (id >= self.tile_count) { + return null; + } + + const tileset_width = @divExact(self.image.width, self.tile_width); + + const tile_x = @mod(id, tileset_width); + const tile_y = @divFloor(id, tileset_width); + + return Position{ + .x = @floatFromInt(tile_x * self.tile_width), + .y = @floatFromInt(tile_y * self.tile_height), + }; + +} + +pub fn deinit(self: *const Tileset) void { + self.arena.deinit(); +} diff --git a/libs/tiled/src/xml.zig b/libs/tiled/src/xml.zig new file mode 100644 index 0000000..661c065 --- /dev/null +++ b/libs/tiled/src/xml.zig @@ -0,0 +1,687 @@ +const std = @import("std"); +const Io = std.Io; +const assert = std.debug.assert; + +const Color = @import("./color.zig"); + +pub const Attribute = struct { + name: []const u8, + value: []const u8, + + pub const List = struct { + items: []const Attribute, + + pub fn get(self: List, name: []const u8) ?[]const u8 { + for (self.items) |attr| { + if (std.mem.eql(u8, attr.name, name)) { + return attr.value; + } + } + return null; + } + + pub fn getDupe(self: List, gpa: std.mem.Allocator, name: []const u8) !?[]u8 { + if (self.get(name)) |value| { + return try gpa.dupe(u8, value); + } + return null; + } + + pub fn getNumber(self: List, T: type, name: []const u8) !?T { + if (self.get(name)) |value| { + if (@typeInfo(T) == .int) { + return try std.fmt.parseInt(T, value, 10); + } else if (@typeInfo(T) == .float) { + return try std.fmt.parseFloat(T, value); + } + } + return null; + } + + pub fn getBool(self: List, name: []const u8, true_value: []const u8, false_value: []const u8) !?bool { + if (self.get(name)) |value| { + if (std.mem.eql(u8, value, true_value)) { + return true; + } else if (std.mem.eql(u8, value, false_value)) { + return false; + } else { + return error.InvalidBoolean; + } + } + return null; + } + + pub fn getEnum(self: List, T: type, name: []const u8, map: std.StaticStringMap(T)) !?T { + if (self.get(name)) |value| { + return map.get(value) orelse return error.InvalidEnumValue; + } + return null; + } + + pub fn getColor(self: List, name: []const u8, hash_required: bool) !?Color { + if (self.get(name)) |value| { + return try Color.parse(value, hash_required); + } + return null; + } + + pub fn format(self: List, writer: *Io.Writer) Io.Writer.Error!void { + if (self.items.len > 0) { + try writer.writeAll("{ "); + for (self.items, 0..) |attribute, i| { + if (i > 0) { + try writer.writeAll(", "); + } + try writer.print("{f}", .{attribute}); + } + try writer.writeAll(" }"); + } else { + try writer.writeAll("{ }"); + } + } + }; + + pub fn format(self: *const Attribute, writer: *Io.Writer) Io.Writer.Error!void { + try writer.print("{s}{{ .name='{s}', .value='{s}' }}", .{ @typeName(Attribute), self.name, self.value }); + } + + pub fn formatSlice(data: []const Attribute, writer: *Io.Writer) Io.Writer.Error!void { + if (data.len > 0) { + try writer.writeAll("{ "); + for (data, 0..) |attribute, i| { + if (i > 0) { + try writer.writeAll(", "); + } + try writer.print("{f}", .{attribute}); + } + try writer.writeAll(" }"); + } else { + try writer.writeAll("{ }"); + } + } + + fn altSlice(data: []const Attribute) std.fmt.Alt(Attribute.List, Attribute.List.format) { + return .{ .data = data }; + } +}; + +pub const Tag = struct { + name: []const u8, + attributes: Attribute.List +}; + +pub const Lexer = struct { + pub const Buffers = struct { + scratch: std.heap.ArenaAllocator, + text: std.ArrayList(u8), + + pub fn init(allocator: std.mem.Allocator) Buffers { + return Buffers{ + .scratch = std.heap.ArenaAllocator.init(allocator), + .text = .empty + }; + } + + pub fn clear(self: *Buffers) void { + self.text.clearRetainingCapacity(); + _ = self.scratch.reset(.retain_capacity); + } + + pub fn deinit(self: *Buffers) void { + const allocator = self.scratch.child_allocator; + self.scratch.deinit(); + self.text.deinit(allocator); + } + }; + + pub const Token = union(enum) { + start_tag: Tag, + end_tag: []const u8, + text: []const u8, + + pub fn isStartTag(self: Token, name: []const u8) bool { + if (self == .start_tag) { + return std.mem.eql(u8, self.start_tag.name, name); + } + return false; + } + + pub fn isEndTag(self: Token, name: []const u8) bool { + if (self == .end_tag) { + return std.mem.eql(u8, self.end_tag, name); + } + return false; + } + }; + + pub const StepResult = struct { + token: ?Token, + self_closing: bool = false, + }; + + pub const TestingContext = struct { + io_reader: Io.Reader, + buffers: Buffers, + lexer: Lexer, + + pub fn init(self: *TestingContext, allocator: std.mem.Allocator, body: []const u8) void { + self.* = TestingContext{ + .lexer = undefined, + .io_reader = Io.Reader.fixed(body), + .buffers = Buffers.init(allocator) + }; + self.lexer = Lexer.init(&self.io_reader, &self.buffers); + } + + pub fn deinit(self: *TestingContext) void { + self.buffers.deinit(); + } + }; + + io_reader: *Io.Reader, + buffers: *Buffers, + + peeked_value: ?Token, + cursor: usize, + queued_end_tag: ?[]const u8, + + pub fn init(reader: *Io.Reader, buffers: *Buffers) Lexer { + buffers.clear(); + + return Lexer{ + .io_reader = reader, + .buffers = buffers, + .cursor = 0, + .queued_end_tag = null, + .peeked_value = null + }; + } + + fn step(self: *Lexer) !StepResult { + _ = self.buffers.scratch.reset(.retain_capacity); + + if (try self.peekByte() == '<') { + self.tossByte(); + + if (try self.peekByte() == '/') { + // End tag + self.tossByte(); + + const name = try self.parseName(); + try self.skipWhiteSpace(); + + if (!std.mem.eql(u8, try self.takeBytes(1), ">")) { + return error.InvalidEndTag; + } + + const token = Token{ .end_tag = name }; + return .{ .token = token }; + + } else if (try self.peekByte() == '?') { + // Prolog tag + self.tossByte(); + if (!std.mem.eql(u8, try self.takeBytes(4), "xml ")) { + return error.InvalidPrologTag; + } + + const attributes = try self.parseAttributes(); + try self.skipWhiteSpace(); + + if (!std.mem.eql(u8, try self.takeBytes(2), "?>")) { + return error.MissingPrologEnd; + } + + const version = attributes.get("version") orelse return error.InvalidProlog; + if (!std.mem.eql(u8, version, "1.0")) { + return error.InvalidPrologVersion; + } + const encoding = attributes.get("encoding") orelse return error.InvalidProlog; + if (!std.mem.eql(u8, encoding, "UTF-8")) { + return error.InvalidPrologEncoding; + } + + return .{ .token = null }; + + } else { + // Start tag + const name = try self.parseName(); + const attributes = try self.parseAttributes(); + try self.skipWhiteSpace(); + + const token = Token{ + .start_tag = .{ + .name = name, + .attributes = attributes + } + }; + + var self_closing = false; + if (std.mem.eql(u8, try self.peekBytes(1), ">")) { + self.tossBytes(1); + + } else if (std.mem.eql(u8, try self.peekBytes(2), "/>")) { + self.tossBytes(2); + + self_closing = true; + + } else { + return error.UnfinishedStartTag; + } + + return .{ + .token = token, + .self_closing = self_closing + }; + } + + } else { + try self.skipWhiteSpace(); + + const text_start = self.cursor; + while (try self.peekByte() != '<') { + self.tossByte(); + } + var text: []const u8 = self.buffers.text.items[text_start..self.cursor]; + text = std.mem.trimEnd(u8, text, &std.ascii.whitespace); + + var token: ?Token = null; + if (text.len > 0) { + token = Token{ .text = text }; + } + return .{ .token = token }; + } + } + + pub fn next(self: *Lexer) !?Token { + if (self.peeked_value) |value| { + self.peeked_value = null; + return value; + } + + if (self.queued_end_tag) |name| { + self.queued_end_tag = null; + return Token{ + .end_tag = name + }; + } + + while (true) { + if (self.buffers.text.items.len == 0) { + self.readIntoTextBuffer() catch |e| switch (e) { + error.EndOfStream => break, + else => return e + }; + } + + const saved_cursor = self.cursor; + const result = self.step() catch |e| switch(e) { + error.EndOfTextBuffer => { + self.cursor = saved_cursor; + + const unused_capacity = self.buffers.text.capacity - self.buffers.text.items.len; + if (unused_capacity == 0 and self.cursor > 0) { + self.rebaseBuffer(); + } else { + self.readIntoTextBuffer() catch |read_err| switch (read_err) { + error.EndOfStream => break, + else => return read_err + }; + } + + continue; + }, + else => return e + }; + + if (result.token) |token| { + if (token == .start_tag and result.self_closing) { + self.queued_end_tag = token.start_tag.name; + } + return token; + } + } + + return null; + } + + pub fn nextExpectEndTag(self: *Lexer, name: []const u8) !void { + const value = try self.next() orelse return error.MissingEndTag; + if (!value.isEndTag(name)) return error.MissingEndTag; + } + + pub fn nextExpectStartTag(self: *Lexer, name: []const u8) !Attribute.List { + const value = try self.next() orelse return error.MissingStartTag; + if (!value.isStartTag(name)) return error.MissingStartTag; + return value.start_tag.attributes; + } + + pub fn nextExpectText(self: *Lexer) ![]const u8 { + const value = try self.next() orelse return error.MissingTextTag; + if (value != .text) return error.MissingTextTag; + return value.text; + } + + pub fn skipUntilMatchingEndTag(self: *Lexer, name: ?[]const u8) !void { + var depth: usize = 0; + while (true) { + const value = try self.next() orelse return error.MissingEndTag; + + if (depth == 0 and value == .end_tag) { + if (name != null and !std.mem.eql(u8, value.end_tag, name.?)) { + return error.MismatchedEndTag; + } + break; + } + + if (value == .start_tag) { + depth += 1; + } else if (value == .end_tag) { + depth -= 1; + } + } + } + + pub fn peek(self: *Lexer) !?Token { + if (try self.next()) |value| { + self.peeked_value = value; + return value; + } + + return null; + } + + fn readIntoTextBuffer(self: *Lexer) !void { + const gpa = self.buffers.scratch.child_allocator; + const text = &self.buffers.text; + try text.ensureUnusedCapacity(gpa, 1); + + var writer = Io.Writer.fixed(text.allocatedSlice()); + writer.end = text.items.len; + + _ = self.io_reader.stream(&writer, .limited(text.capacity - text.items.len)) catch |e| switch (e) { + error.WriteFailed => unreachable, + else => |ee| return ee + }; + text.items.len = writer.end; + } + + fn rebaseBuffer(self: *Lexer) void { + if (self.cursor == 0) { + return; + } + + const text = &self.buffers.text; + @memmove( + text.items[0..(text.items.len - self.cursor)], + text.items[self.cursor..] + ); + + text.items.len -= self.cursor; + self.cursor = 0; + } + + fn isNameStartChar(c: u8) bool { + return c == ':' or c == '_' or std.ascii.isAlphabetic(c); + } + + fn isNameChar(c: u8) bool { + return isNameStartChar(c) or c == '-' or c == '.' or ('0' <= c and c <= '9'); + } + + fn hasBytes(self: *Lexer, n: usize) bool { + const text = self.buffers.text.items; + return self.cursor + n <= text.len; + } + + fn peekBytes(self: *Lexer, n: usize) ![]const u8 { + if (self.hasBytes(n)) { + const text = self.buffers.text.items; + return text[self.cursor..][0..n]; + } + return error.EndOfTextBuffer; + } + + fn tossBytes(self: *Lexer, n: usize) void { + assert(self.hasBytes(n)); + self.cursor += n; + } + + fn takeBytes(self: *Lexer, n: usize) ![]const u8 { + const result = try self.peekBytes(n); + self.tossBytes(n); + return result; + } + + fn peekByte(self: *Lexer) !u8 { + return (try self.peekBytes(1))[0]; + } + + fn tossByte(self: *Lexer) void { + self.tossBytes(1); + } + + fn takeByte(self: *Lexer) !u8 { + return (try self.takeBytes(1))[0]; + } + + fn parseName(self: *Lexer) ![]const u8 { + const name_start = self.cursor; + + if (isNameStartChar(try self.peekByte())) { + self.tossByte(); + + while (isNameChar(try self.peekByte())) { + self.tossByte(); + } + } + + return self.buffers.text.items[name_start..self.cursor]; + } + + fn skipWhiteSpace(self: *Lexer) !void { + while (std.ascii.isWhitespace(try self.peekByte())) { + self.tossByte(); + } + } + + fn parseAttributeValue(self: *Lexer) ![]const u8 { + const quote = try self.takeByte(); + if (quote != '"' and quote != '\'') { + return error.InvalidAttributeValue; + } + + const value_start: usize = self.cursor; + var value_len: usize = 0; + + while (true) { + const c = try self.takeByte(); + if (c == '<' or c == '&') { + return error.InvalidAttributeValue; + } + if (c == quote) { + break; + } + value_len += 1; + } + + return self.buffers.text.items[value_start..][0..value_len]; + } + + fn parseAttributes(self: *Lexer) !Attribute.List { + const arena = self.buffers.scratch.allocator(); + var attributes: std.ArrayList(Attribute) = .empty; + + while (true) { + try self.skipWhiteSpace(); + const name = try self.parseName(); + if (name.len == 0) { + break; + } + + try self.skipWhiteSpace(); + if (try self.takeByte() != '=') { + return error.MissingAttributeEquals; + } + try self.skipWhiteSpace(); + const value = try self.parseAttributeValue(); + + const list = Attribute.List{ .items = attributes.items }; + if (list.get(name) != null) { + return error.DuplicateAttribute; + } + + try attributes.append(arena, Attribute{ + .name = name, + .value = value + }); + } + + return Attribute.List{ + .items = attributes.items + }; + } + + test "self closing tag" { + const allocator = std.testing.allocator; + var ctx: TestingContext = undefined; + ctx.init(allocator, + \\ + ); + defer ctx.deinit(); + + try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello")); + try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello")); + try std.testing.expect((try ctx.lexer.next()) == null); + } + + test "tag" { + const allocator = std.testing.allocator; + var ctx: TestingContext = undefined; + ctx.init(allocator, + \\ + ); + defer ctx.deinit(); + + try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello")); + try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello")); + try std.testing.expect((try ctx.lexer.next()) == null); + } + + test "tag with prolog" { + const allocator = std.testing.allocator; + var ctx: TestingContext = undefined; + ctx.init(allocator, + \\ + \\ + ); + defer ctx.deinit(); + + try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello")); + try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello")); + try std.testing.expect((try ctx.lexer.next()) == null); + } + + test "text content" { + const allocator = std.testing.allocator; + var ctx: TestingContext = undefined; + ctx.init(allocator, + \\ Hello World + ); + defer ctx.deinit(); + + try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello")); + try std.testing.expectEqualStrings("Hello World", (try ctx.lexer.next()).?.text); + try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello")); + try std.testing.expect((try ctx.lexer.next()) == null); + } + + test "attributes" { + const allocator = std.testing.allocator; + var ctx: TestingContext = undefined; + ctx.init(allocator, + \\ + ); + defer ctx.deinit(); + + const token = try ctx.lexer.next(); + const attrs = token.?.start_tag.attributes; + try std.testing.expectEqualStrings("1", attrs.get("a").?); + try std.testing.expectEqualStrings("2", attrs.get("b").?); + } +}; + +// TODO: The API for this is easy to misuse. +// Design a better API for using Reader +// As a compromise `assert` was used to guard against some of the ways this can be misused +pub const TagParser = struct { + lexer: *Lexer, + + begin_called: bool = false, + finish_called: bool = false, + + pub const Node = union(enum) { + tag: Tag, + text: []const u8, + + pub fn isTag(self: Node, name: []const u8) bool { + if (self == .tag) { + return std.mem.eql(u8, self.tag.name, name); + } + return false; + } + }; + + pub fn init(lexer: *Lexer) TagParser { + return TagParser{ + .lexer = lexer, + }; + } + + pub fn begin(self: *TagParser, name: []const u8) !Attribute.List { + assert(!self.begin_called); + self.begin_called = true; + + return try self.lexer.nextExpectStartTag(name); + } + + pub fn finish(self: *TagParser, name: []const u8) !void { + assert(self.begin_called); + assert(!self.finish_called); + self.finish_called = true; + + try self.lexer.skipUntilMatchingEndTag(name); + } + + pub fn next(self: *TagParser) !?Node { + assert(self.begin_called); + assert(!self.finish_called); + + const value = try self.lexer.peek() orelse return error.MissingEndTag; + if (value == .end_tag) { + return null; + } + + return switch (value) { + .text => |text| Node{ .text = text }, + .start_tag => |start_tag| Node{ .tag = start_tag }, + .end_tag => unreachable, + }; + } + + pub fn skip(self: *TagParser) !void { + assert(self.begin_called); + assert(!self.finish_called); + + const value = try self.lexer.next() orelse return error.MissingNode; + if (value == .end_tag) { + return error.UnexpectedEndTag; + } else if (value == .start_tag) { + // TODO: Make this configurable + var name_buffer: [64]u8 = undefined; + var name: std.ArrayList(u8) = .initBuffer(&name_buffer); + try name.appendSliceBounded(value.start_tag.name); + + try self.lexer.skipUntilMatchingEndTag(name.items); + } + } +}; diff --git a/libs/tracy/build.zig b/libs/tracy/build.zig new file mode 100644 index 0000000..53e8992 --- /dev/null +++ b/libs/tracy/build.zig @@ -0,0 +1,138 @@ +const std = @import("std"); +const digits2 = std.fmt.digits2; + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const tracy_enable = b.option(bool, "tracy_enable", "Enable profiling") orelse true; + const tracy_on_demand = b.option(bool, "tracy_on_demand", "On-demand profiling") orelse false; + const tracy_callstack: ?u8 = b.option(u8, "tracy_callstack", "Enforce callstack collection for tracy regions"); + const tracy_no_callstack = b.option(bool, "tracy_no_callstack", "Disable all callstack related functionality") orelse false; + const tracy_no_callstack_inlines = b.option(bool, "tracy_no_callstack_inlines", "Disables the inline functions in callstacks") orelse false; + const tracy_only_localhost = b.option(bool, "tracy_only_localhost", "Only listen on the localhost interface") orelse false; + const tracy_no_broadcast = b.option(bool, "tracy_no_broadcast", "Disable client discovery by broadcast to local network") orelse false; + const tracy_only_ipv4 = b.option(bool, "tracy_only_ipv4", "Tracy will only accept connections on IPv4 addresses (disable IPv6)") orelse false; + const tracy_no_code_transfer = b.option(bool, "tracy_no_code_transfer", "Disable collection of source code") orelse false; + const tracy_no_context_switch = b.option(bool, "tracy_no_context_switch", "Disable capture of context switches") orelse false; + const tracy_no_exit = b.option(bool, "tracy_no_exit", "Client executable does not exit until all profile data is sent to server") orelse false; + const tracy_no_sampling = b.option(bool, "tracy_no_sampling", "Disable call stack sampling") orelse false; + const tracy_no_verify = b.option(bool, "tracy_no_verify", "Disable zone validation for C API") orelse false; + const tracy_no_vsync_capture = b.option(bool, "tracy_no_vsync_capture", "Disable capture of hardware Vsync events") orelse false; + const tracy_no_frame_image = b.option(bool, "tracy_no_frame_image", "Disable the frame image support and its thread") orelse false; + // NOTE For some reason system tracing on zig projects crashes tracy, will need to investigate + const tracy_no_system_tracing = b.option(bool, "tracy_no_system_tracing", "Disable systrace sampling") orelse true; + const tracy_delayed_init = b.option(bool, "tracy_delayed_init", "Enable delayed initialization of the library (init on first call)") orelse false; + const tracy_manual_lifetime = b.option(bool, "tracy_manual_lifetime", "Enable the manual lifetime management of the profile") orelse false; + const tracy_fibers = b.option(bool, "tracy_fibers", "Enable fibers support") orelse false; + const tracy_no_crash_handler = b.option(bool, "tracy_no_crash_handler", "Disable crash handling") orelse false; + const tracy_timer_fallback = b.option(bool, "tracy_timer_fallback", "Use lower resolution timers") orelse false; + const shared = b.option(bool, "shared", "Build the tracy client as a shared libary") orelse false; + + const options = b.addOptions(); + options.addOption(bool, "tracy_enable", tracy_enable); + options.addOption(bool, "tracy_on_demand", tracy_on_demand); + options.addOption(?u8, "tracy_callstack", tracy_callstack); + options.addOption(bool, "tracy_no_callstack", tracy_no_callstack); + options.addOption(bool, "tracy_no_callstack_inlines", tracy_no_callstack_inlines); + options.addOption(bool, "tracy_only_localhost", tracy_only_localhost); + options.addOption(bool, "tracy_no_broadcast", tracy_no_broadcast); + options.addOption(bool, "tracy_only_ipv4", tracy_only_ipv4); + options.addOption(bool, "tracy_no_code_transfer", tracy_no_code_transfer); + options.addOption(bool, "tracy_no_context_switch", tracy_no_context_switch); + options.addOption(bool, "tracy_no_exit", tracy_no_exit); + options.addOption(bool, "tracy_no_sampling", tracy_no_sampling); + options.addOption(bool, "tracy_no_verify", tracy_no_verify); + options.addOption(bool, "tracy_no_vsync_capture", tracy_no_vsync_capture); + options.addOption(bool, "tracy_no_frame_image", tracy_no_frame_image); + options.addOption(bool, "tracy_no_system_tracing", tracy_no_system_tracing); + options.addOption(bool, "tracy_delayed_init", tracy_delayed_init); + options.addOption(bool, "tracy_manual_lifetime", tracy_manual_lifetime); + options.addOption(bool, "tracy_fibers", tracy_fibers); + options.addOption(bool, "tracy_no_crash_handler", tracy_no_crash_handler); + options.addOption(bool, "tracy_timer_fallback", tracy_timer_fallback); + options.addOption(bool, "shared", shared); + + const dep_src = b.dependency("src", .{}); + + const tracy_module = b.addModule("tracy", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + tracy_module.addImport("tracy-options", options.createModule()); + tracy_module.addIncludePath(dep_src.path("./public")); + + const tracy_client = b.addLibrary(.{ + .linkage = if (shared) .dynamic else .static, + .name = "tracy", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + }), + }); + + if (target.result.os.tag == .windows) { + tracy_client.root_module.linkSystemLibrary("dbghelp", .{}); + tracy_client.root_module.linkSystemLibrary("ws2_32", .{}); + } + if (target.result.abi != .msvc) { + tracy_client.root_module.link_libcpp = true; + } else { + tracy_client.root_module.link_libc = true; + } + tracy_client.root_module.addCSourceFile(.{ + .file = dep_src.path("./public/TracyClient.cpp"), + .flags = if (target.result.os.tag == .windows) &.{"-fms-extensions"} else &.{}, + }); + tracy_client.installHeadersDirectory(dep_src.path("public"), "", .{ + .include_extensions = &.{".h", ".hpp"}, + }); + if (tracy_enable) + tracy_client.root_module.addCMacro("TRACY_ENABLE", "1"); + if (tracy_on_demand) + tracy_client.root_module.addCMacro("TRACY_ON_DEMAND", "1"); + if (tracy_callstack) |depth| { + tracy_client.root_module.addCMacro("TRACY_CALLSTACK", "\"" ++ digits2(depth) ++ "\""); + } + if (tracy_no_callstack) + tracy_client.root_module.addCMacro("TRACY_NO_CALLSTACK", "1"); + if (tracy_no_callstack_inlines) + tracy_client.root_module.addCMacro("TRACY_NO_CALLSTACK_INLINES", "1"); + if (tracy_only_localhost) + tracy_client.root_module.addCMacro("TRACY_ONLY_LOCALHOST", "1"); + if (tracy_no_broadcast) + tracy_client.root_module.addCMacro("TRACY_NO_BROADCAST", "1"); + if (tracy_only_ipv4) + tracy_client.root_module.addCMacro("TRACY_ONLY_IPV4", "1"); + if (tracy_no_code_transfer) + tracy_client.root_module.addCMacro("TRACY_NO_CODE_TRANSFER", "1"); + if (tracy_no_context_switch) + tracy_client.root_module.addCMacro("TRACY_NO_CONTEXT_SWITCH", "1"); + if (tracy_no_exit) + tracy_client.root_module.addCMacro("TRACY_NO_EXIT", "1"); + if (tracy_no_sampling) + tracy_client.root_module.addCMacro("TRACY_NO_SAMPLING", "1"); + if (tracy_no_verify) + tracy_client.root_module.addCMacro("TRACY_NO_VERIFY", "1"); + if (tracy_no_vsync_capture) + tracy_client.root_module.addCMacro("TRACY_NO_VSYNC_CAPTURE", "1"); + if (tracy_no_frame_image) + tracy_client.root_module.addCMacro("TRACY_NO_FRAME_IMAGE", "1"); + if (tracy_no_system_tracing) + tracy_client.root_module.addCMacro("TRACY_NO_SYSTEM_TRACING", "1"); + if (tracy_delayed_init) + tracy_client.root_module.addCMacro("TRACY_DELAYED_INIT", "1"); + if (tracy_manual_lifetime) + tracy_client.root_module.addCMacro("TRACY_MANUAL_LIFETIME", "1"); + if (tracy_fibers) + tracy_client.root_module.addCMacro("TRACY_FIBERS", "1"); + if (tracy_no_crash_handler) + tracy_client.root_module.addCMacro("TRACY_NO_CRASH_HANDLER", "1"); + if (tracy_timer_fallback) + tracy_client.root_module.addCMacro("TRACY_TIMER_FALLBACK", "1"); + if (shared and target.result.os.tag == .windows) + tracy_client.root_module.addCMacro("TRACY_EXPORTS", "1"); + b.installArtifact(tracy_client); +} diff --git a/libs/tracy/build.zig.zon b/libs/tracy/build.zig.zon new file mode 100644 index 0000000..db2a6c3 --- /dev/null +++ b/libs/tracy/build.zig.zon @@ -0,0 +1,13 @@ +.{ + .name = .tracy, + .version = "0.0.1", + .dependencies = .{ + .src = .{ + .url = "https://github.com/wolfpld/tracy/archive/refs/tags/v0.13.1.tar.gz", + .hash = "N-V-__8AAOncKwEm1F9c5LrT7HMNmRMYX8-fAoqpc6YyTu9X", + }, + }, + .minimum_zig_version = "0.16.0", + .paths = .{""}, + .fingerprint = 0x255a89eeaaacb778, +} diff --git a/libs/tracy/src/root.zig b/libs/tracy/src/root.zig new file mode 100644 index 0000000..227867c --- /dev/null +++ b/libs/tracy/src/root.zig @@ -0,0 +1,376 @@ +const std = @import("std"); +const digits2 = std.fmt.digits2; +const builtin = @import("builtin"); + +const options = @import("tracy-options"); + +const c = @cImport({ + if (options.tracy_enable) @cDefine("TRACY_ENABLE", {}); + if (options.tracy_on_demand) @cDefine("TRACY_ON_DEMAND", {}); + if (options.tracy_callstack) |depth| @cDefine("TRACY_CALLSTACK", "\"" ++ digits2(depth) ++ "\""); + if (options.tracy_no_callstack) @cDefine("TRACY_NO_CALLSTACK", {}); + if (options.tracy_no_callstack_inlines) @cDefine("TRACY_NO_CALLSTACK_INLINES", {}); + if (options.tracy_only_localhost) @cDefine("TRACY_ONLY_LOCALHOST", {}); + if (options.tracy_no_broadcast) @cDefine("TRACY_NO_BROADCAST", {}); + if (options.tracy_only_ipv4) @cDefine("TRACY_ONLY_IPV4", {}); + if (options.tracy_no_code_transfer) @cDefine("TRACY_NO_CODE_TRANSFER", {}); + if (options.tracy_no_context_switch) @cDefine("TRACY_NO_CONTEXT_SWITCH", {}); + if (options.tracy_no_exit) @cDefine("TRACY_NO_EXIT", {}); + if (options.tracy_no_sampling) @cDefine("TRACY_NO_SAMPLING", {}); + if (options.tracy_no_verify) @cDefine("TRACY_NO_VERIFY", {}); + if (options.tracy_no_vsync_capture) @cDefine("TRACY_NO_VSYNC_CAPTURE", {}); + if (options.tracy_no_frame_image) @cDefine("TRACY_NO_FRAME_IMAGE", {}); + if (options.tracy_no_system_tracing) @cDefine("TRACY_NO_SYSTEM_TRACING", {}); + if (options.tracy_delayed_init) @cDefine("TRACY_DELAYED_INIT", {}); + if (options.tracy_manual_lifetime) @cDefine("TRACY_MANUAL_LIFETIME", {}); + if (options.tracy_fibers) @cDefine("TRACY_FIBERS", {}); + if (options.tracy_no_crash_handler) @cDefine("TRACY_NO_CRASH_HANDLER", {}); + if (options.tracy_timer_fallback) @cDefine("TRACY_TIMER_FALLBACK", {}); + if (options.shared and builtin.os.tag == .windows) @cDefine("TRACY_IMPORTS", {}); + + @cInclude("tracy/TracyC.h"); +}); + +pub inline fn setThreadName(comptime name: [:0]const u8) void { + if (!options.tracy_enable) return; + c.___tracy_set_thread_name(name); +} + +pub inline fn startupProfiler() void { + if (!options.tracy_enable) return; + if (!options.tracy_manual_lifetime) return; + c.___tracy_startup_profiler(); +} + +pub inline fn shutdownProfiler() void { + if (!options.tracy_enable) return; + if (!options.tracy_manual_lifetime) return; + c.___tracy_shutdown_profiler(); +} + +pub inline fn profilerStarted() bool { + if (!options.tracy_enable) return false; + if (!options.tracy_manual_lifetime) return true; + return c.___tracy_profiler_started() != 0; +} + +pub inline fn isConnected() bool { + if (!options.tracy_enable) return false; + return c.___tracy_connected() > 0; +} + +pub inline fn frameMark() void { + if (!options.tracy_enable) return; + c.___tracy_emit_frame_mark(null); +} + +pub inline fn frameMarkNamed(comptime name: [:0]const u8) void { + if (!options.tracy_enable) return; + c.___tracy_emit_frame_mark(name); +} + +const DiscontinuousFrame = struct { + name: [:0]const u8, + + pub inline fn deinit(frame: *const DiscontinuousFrame) void { + if (!options.tracy_enable) return; + c.___tracy_emit_frame_mark_end(frame.name); + } +}; + +pub inline fn initDiscontinuousFrame(comptime name: [:0]const u8) DiscontinuousFrame { + if (!options.tracy_enable) return .{ .name = name }; + c.___tracy_emit_frame_mark_start(name); + return .{ .name = name }; +} + +pub inline fn frameImage(image: *anyopaque, width: u16, height: u16, offset: u8, flip: bool) void { + if (!options.tracy_enable) return; + c.___tracy_emit_frame_mark_image(image, width, height, offset, @as(c_int, @intFromBool(flip))); +} + +pub const ZoneOptions = struct { + active: bool = true, + name: ?[]const u8 = null, + color: ?u32 = null, +}; + +const ZoneContext = if (options.tracy_enable) extern struct { + ctx: c.___tracy_c_zone_context, + + pub inline fn deinit(zone: *const ZoneContext) void { + if (!options.tracy_enable) return; + c.___tracy_emit_zone_end(zone.ctx); + } + + pub inline fn name(zone: *const ZoneContext, zone_name: []const u8) void { + if (!options.tracy_enable) return; + c.___tracy_emit_zone_name(zone.ctx, zone_name.ptr, zone_name.len); + } + + pub inline fn text(zone: *const ZoneContext, zone_text: []const u8) void { + if (!options.tracy_enable) return; + c.___tracy_emit_zone_text(zone.ctx, zone_text.ptr, zone_text.len); + } + + pub inline fn color(zone: *const ZoneContext, zone_color: u32) void { + if (!options.tracy_enable) return; + c.___tracy_emit_zone_color(zone.ctx, zone_color); + } + + pub inline fn value(zone: *const ZoneContext, zone_value: u64) void { + if (!options.tracy_enable) return; + c.___tracy_emit_zone_value(zone.ctx, zone_value); + } +} else struct { + pub inline fn deinit(_: *const ZoneContext) void {} + pub inline fn name(_: *const ZoneContext, _: []const u8) void {} + pub inline fn text(_: *const ZoneContext, _: []const u8) void {} + pub inline fn color(_: *const ZoneContext, _: u32) void {} + pub inline fn value(_: *const ZoneContext, _: u64) void {} +}; + +pub inline fn initZone(comptime src: std.builtin.SourceLocation, comptime opts: ZoneOptions) ZoneContext { + if (!options.tracy_enable) return .{}; + const active: c_int = @intFromBool(opts.active); + + const static = struct { + var src_loc = c.___tracy_source_location_data{ + .name = if (opts.name) |name| name.ptr else null, + .function = src.fn_name.ptr, + .file = src.file, + .line = 0, + .color = opts.color orelse 0, + }; + }; + + // src.line magically is not comptime https://github.com/ziglang/zig/pull/12016#issuecomment-1178092847 + static.src_loc.line = src.line; + + if (!options.tracy_no_callstack) { + if (options.tracy_callstack) |depth| { + return .{ + .ctx = c.___tracy_emit_zone_begin_callstack(&static.src_loc, depth, active), + }; + } + } + + return .{ + .ctx = c.___tracy_emit_zone_begin(&static.src_loc, active), + }; +} + +pub inline fn plot(comptime T: type, comptime name: [:0]const u8, value: T) void { + if (!options.tracy_enable) return; + + const type_info = @typeInfo(T); + switch (type_info) { + .int => |int_type| { + if (int_type.bits > 64) @compileError("Too large int to plot"); + if (int_type.signedness == .unsigned and int_type.bits > 63) @compileError("Too large unsigned int to plot"); + c.___tracy_emit_plot_int(name, value); + }, + .float => |float_type| { + if (float_type.bits <= 32) { + c.___tracy_emit_plot_float(name, value); + } else if (float_type.bits <= 64) { + c.___tracy_emit_plot(name, value); + } else { + @compileError("Too large float to plot"); + } + }, + else => @compileError("Unsupported plot value type"), + } +} + +pub const PlotType = enum(c_int) { + Number, + Memory, + Percentage, + Watt, +}; + +pub const PlotConfig = struct { + plot_type: PlotType, + step: c_int, + fill: c_int, + color: u32, +}; + +pub inline fn plotConfig(comptime name: [:0]const u8, comptime config: PlotConfig) void { + if (!options.tracy_enable) return; + c.___tracy_emit_plot_config( + name, + @intFromEnum(config.plot_type), + config.step, + config.fill, + config.color, + ); +} + +pub inline fn message(comptime msg: [:0]const u8) void { + if (!options.tracy_enable) return; + const depth = options.tracy_callstack orelse 0; + c.___tracy_emit_messageL(msg, depth); +} + +pub inline fn messageColor(comptime msg: [:0]const u8, color: u32) void { + if (!options.tracy_enable) return; + const depth = options.tracy_callstack orelse 0; + c.___tracy_emit_messageLC(msg, color, depth); +} + +const tracy_message_buffer_size = if (options.tracy_enable) 4096 else 0; +threadlocal var tracy_message_buffer: [tracy_message_buffer_size]u8 = undefined; + +pub inline fn print(comptime fmt: []const u8, args: anytype) void { + if (!options.tracy_enable) return; + const depth = options.tracy_callstack orelse 0; + + var stream = std.io.fixedBufferStream(&tracy_message_buffer); + stream.writer().print(fmt, args) catch {}; + + const written = stream.getWritten(); + c.___tracy_emit_message(written.ptr, written.len, depth); +} + +pub inline fn printColor(comptime fmt: []const u8, args: anytype, color: u32) void { + if (!options.tracy_enable) return; + const depth = options.tracy_callstack orelse 0; + + var stream = std.io.fixedBufferStream(&tracy_message_buffer); + stream.writer().print(fmt, args) catch {}; + + const written = stream.getWritten(); + c.___tracy_emit_messageC(written.ptr, written.len, color, depth); +} + +pub inline fn printAppInfo(comptime fmt: []const u8, args: anytype) void { + if (!options.tracy_enable) return; + + var stream = std.io.fixedBufferStream(&tracy_message_buffer); + stream.reset(); + stream.writer().print(fmt, args) catch {}; + + const written = stream.getWritten(); + c.___tracy_emit_message_appinfo(written.ptr, written.len); +} + +pub const TracingAllocator = struct { + parent_allocator: std.mem.Allocator, + pool_name: ?[:0]const u8, + + const Self = @This(); + + pub fn init(parent_allocator: std.mem.Allocator) Self { + return .{ + .parent_allocator = parent_allocator, + .pool_name = null, + }; + } + + pub fn initNamed(comptime pool_name: [:0]const u8, parent_allocator: std.mem.Allocator) Self { + return .{ + .parent_allocator = parent_allocator, + .pool_name = pool_name, + }; + } + + pub fn allocator(self: *Self) std.mem.Allocator { + return .{ + .ptr = self, + .vtable = &.{ + .alloc = alloc, + .resize = resize, + .remap = remap, + .free = free, + }, + }; + } + + fn alloc( + ctx: *anyopaque, + len: usize, + ptr_align: std.mem.Alignment, + ret_addr: usize, + ) ?[*]u8 { + const self: *Self = @ptrCast(@alignCast(ctx)); + const result = self.parent_allocator.rawAlloc(len, ptr_align, ret_addr); + if (!options.tracy_enable) return result; + + if (self.pool_name) |name| { + c.___tracy_emit_memory_alloc_named(result, len, 0, name.ptr); + } else { + c.___tracy_emit_memory_alloc(result, len, 0); + } + + return result; + } + + fn resize( + ctx: *anyopaque, + buf: []u8, + buf_align: std.mem.Alignment, + new_len: usize, + ret_addr: usize, + ) bool { + const self: *Self = @ptrCast(@alignCast(ctx)); + const result = self.parent_allocator.rawResize(buf, buf_align, new_len, ret_addr); + if (!result) return false; + + if (!options.tracy_enable) return true; + + if (self.pool_name) |name| { + c.___tracy_emit_memory_free_named(buf.ptr, 0, name.ptr); + c.___tracy_emit_memory_alloc_named(buf.ptr, new_len, 0, name.ptr); + } else { + c.___tracy_emit_memory_free(buf.ptr, 0); + c.___tracy_emit_memory_alloc(buf.ptr, new_len, 0); + } + + return true; + } + + fn remap( + ctx: *anyopaque, + buf: []u8, + buf_align: std.mem.Alignment, + new_len: usize, + ret_addr: usize, + ) ?[*]u8 { + const self: *Self = @ptrCast(@alignCast(ctx)); + const result = self.parent_allocator.rawRemap(buf, buf_align, new_len, ret_addr); + const new_buf = result orelse return null; + + if (!options.tracy_enable) return new_buf; + + if (self.pool_name) |name| { + c.___tracy_emit_memory_free_named(buf.ptr, 0, name.ptr); + c.___tracy_emit_memory_alloc_named(new_buf, new_len, 0, name.ptr); + } else { + c.___tracy_emit_memory_free(buf.ptr, 0); + c.___tracy_emit_memory_alloc(new_buf, new_len, 0); + } + + return new_buf; + } + + fn free( + ctx: *anyopaque, + buf: []u8, + buf_align: std.mem.Alignment, + ret_addr: usize, + ) void { + const self: *Self = @ptrCast(@alignCast(ctx)); + + if (options.tracy_enable) { + if (self.pool_name) |name| { + c.___tracy_emit_memory_free_named(buf.ptr, 0, name.ptr); + } else { + c.___tracy_emit_memory_free(buf.ptr, 0); + } + } + + self.parent_allocator.rawFree(buf, buf_align, ret_addr); + } +}; diff --git a/scripts/png-to-icon.zig b/scripts/png-to-icon.zig new file mode 100644 index 0000000..0638586 --- /dev/null +++ b/scripts/png-to-icon.zig @@ -0,0 +1,57 @@ +const std = @import("std"); +const STBImage = @import("stb_image"); + +// https://en.wikipedia.org/wiki/ICO_(file_format)#Icon_file_structure + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + if (args.len != 3) { + std.debug.print("Usage: ./png-to-icon ", .{}); + std.process.exit(1); + } + + const cwd = std.fs.cwd(); + const input_png_path = args[1]; + const output_ico_path = args[2]; + + const input_png_data = try cwd.readFileAlloc(allocator, input_png_path, 1024 * 1024 * 5); + defer allocator.free(input_png_data); + + const png_image = try STBImage.load(input_png_data); + defer png_image.deinit(); + + std.debug.assert(png_image.width > 0 and png_image.width <= 256); + std.debug.assert(png_image.height > 0 and png_image.height <= 256); + + const output_ico_file = try std.fs.cwd().createFile(output_ico_path, .{ }); + defer output_ico_file.close(); + + var buffer: [4096 * 4]u8 = undefined; + var writer = output_ico_file.writer(&buffer); + + // ICONDIR structure + try writer.interface.writeInt(u16, 0, .little); // Must always be zero + try writer.interface.writeInt(u16, 1, .little); // Image type. 1 for .ICO + try writer.interface.writeInt(u16, 1, .little); // Number of images + + // ICONDIRENTRY structure + try writer.interface.writeInt(u8, @truncate(png_image.width), .little); // Image width + try writer.interface.writeInt(u8, @truncate(png_image.height), .little); // Image height + try writer.interface.writeInt(u8, 0, .little); // Number of colors in color pallete. 0 means that color pallete is not used + try writer.interface.writeInt(u8, 0, .little); // Must always be zero + try writer.interface.writeInt(u16, 0, .little); // Color plane + try writer.interface.writeInt(u16, 32, .little); // Bits per pixel + try writer.interface.writeInt(u32, @intCast(input_png_data.len), .little); // Image size in bytes + try writer.interface.writeInt(u32, 22, .little); // Offset to image data from the start + + // PNG image data + try writer.interface.writeAll(input_png_data); + try writer.interface.flush(); +} + diff --git a/src/app.zig b/src/app.zig new file mode 100644 index 0000000..d0b0ac5 --- /dev/null +++ b/src/app.zig @@ -0,0 +1,115 @@ +const std = @import("std"); +const CLI = @import("./cli.zig"); +const log = std.log.scoped(.app); +const Allocator = std.mem.Allocator; + +const Math = @import("math"); +const Vec2 = Math.Vec2; + +const STBImage = @import("stb_image"); +const STBTrueType = @import("stb_truetype"); +const Tiled = @import("tiled"); + +const Platform = @import("./platform.zig"); +const Gfx = Platform.Gfx; +const ImGUI = Platform.ImGUI; +const Rect = Platform.Rect; + +const App = @This(); + +tilesheet: Tilesheet, + +const Tilesheet = struct { + image: Gfx.ImageData, + sprites: []Gfx.Sprite.Id, + tile_size: Vec2, + width: u32, + height: u32, + + pub fn init(gpa: Allocator, image: Gfx.ImageData, width: u32, height: u32, tile_size: Vec2) !Tilesheet { + const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height); + errdefer gpa.free(sprites); + @memset(sprites, Gfx.getNilSprite()); + + return Tilesheet{ + .sprites = sprites, + .image = image, + .width = width, + .height = height, + .tile_size = tile_size + }; + } + + pub fn deinit(self: Tilesheet) void { + for (self.sprites) |sprite| { + Gfx.deinitSprite(sprite); + } + } + + pub fn get(self: Tilesheet, x: u32, y: u32) Gfx.Sprite.Id { + if (x >= self.width or y >= self.height) { + log.warn("Attempt to get tile which is out of bounds", .{}); + return Gfx.getNilSprite(); + } + + const sprite_index = y * self.width + x; + if (self.sprites[sprite_index] == Gfx.getNilSprite()) { + const sprite = Gfx.initSprite(.{ .padding = 1 }); + Gfx.setSprite(sprite, .{ + .image = self.image, + .rect = Rect.init(@floatFromInt(x), @floatFromInt(y), 1, 1).multiply(self.tile_size) + }); + self.sprites[sprite_index] = sprite; + } + + return self.sprites[sprite_index]; + } +}; + +pub fn init(self: *App, plt: Platform.Init) !?u8 { + // const robot_font = Gfx.initFont(); + // Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf")); + + const tilesheet_png = try STBImage.load(plt.assets.readFile("tranquil_tunnels_transparent.png")); + const tilesheet = try Tilesheet.init( + plt.arena, + Gfx.ImageData{ + .width = tilesheet_png.width, + .height = tilesheet_png.height, + .pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels } + }, + 13, + 8, + .init(64, 64) + ); + + self.* = App{ + .tilesheet = tilesheet + }; + + return null; +} + +pub fn frame(self: *App, plt: Platform.Frame) !void { + _ = self; // autofix + const input = plt.input; + _ = input; // autofix + + const dt = plt.deltaTime(); + _ = dt; // autofix + + Gfx.setClearColor(.rgb(40, 40, 40)); + Gfx.drawLine(.init(0, 200), .init(300, 200), .white, 5); + // Gfx.drawSprite(frames[self.player_frame_index], self.player_pos, .init(64, 64), .white); + // Gfx.drawSprite(self.tilesheet.get(11, 6), self.player_pos.add(.init(0, 200)), .init(64, 64), .white); + // Gfx.drawText(self.roboto_font, .{ + // .pos = .init(300, 100), + // .text = "Hello, World!", + // .height = 64, + // }); +} + +pub fn deinit(self: *App, plt: Platform.Deinit) void { + _ = plt; // autofix + _ = self; // autofix +} diff --git a/src/cli.zig b/src/cli.zig new file mode 100644 index 0000000..0f37f36 --- /dev/null +++ b/src/cli.zig @@ -0,0 +1,302 @@ +const std = @import("std"); +const Io = std.Io; +const log = std.log.scoped(.engine); + +const CLI = @This(); + +gpa: std.mem.Allocator, +arena: std.heap.ArenaAllocator, + +options: std.ArrayList(Option), +commands: std.ArrayList(Command), + +stdout: Channel, +stderr: Channel, + +pub const Command = struct { + name: []const u8, + description: []const u8, + + const Id = enum (u32) { _ }; +}; + +pub const Option = struct { + long_name: []const u8, + description: []const u8, + kind: Kind, + + pub const Kind = enum { + toggle, + single_argument + }; + + pub const Id = enum (u32) { _ }; + + const Value = struct { + argument: ?[]const u8 = null + }; +}; + +const Channel = struct { + file: Io.File, + buffer: [4 * 4096]u8, + writer: Io.File.Writer, + + pub fn init(self: *Channel, io: Io, file: Io.File) void { + self.* = Channel{ + .file = file, + .buffer = undefined, + .writer = file.writer(io, &self.buffer) + }; + } + + pub fn write(self: *Channel, comptime fmt: []const u8, args: anytype) void { + self.writer.interface.print(fmt, args) catch |e| { + log.err("Failed to write to channel: {}", .{e}); + }; + } + + pub fn flush(self: *Channel) void { + self.writer.interface.flush() catch |e| { + log.err("Failed to flush channel: {}", .{e}); + }; + } +}; + +pub fn init(self: *CLI, io: Io, gpa: std.mem.Allocator) void { + self.* = CLI{ + .gpa = gpa, + .arena = .init(gpa), + .options = .empty, + .commands = .empty, + .stdout = undefined, + .stderr = undefined + }; + self.stdout.init(io, Io.File.stdout()); + self.stderr.init(io, Io.File.stderr()); +} + +pub fn deinit(self: *CLI) void { + self.stderr.flush(); + self.stdout.flush(); + + self.arena.deinit(); + + self.options.deinit(self.gpa); + self.commands.deinit(self.gpa); +} + +pub const AddOptionOptions = struct { + long_name: []const u8, + description: ?[]const u8 = null, + kind: Option.Kind = .toggle, +}; + +pub fn addOption(self: *CLI, opts: AddOptionOptions) !Option.Id { + const arena = self.arena.allocator(); + + var owned_description: []const u8 = ""; + if (opts.description) |description| { + owned_description = try arena.dupe(u8, description); + } + + const index = self.options.items.len; + try self.options.append(self.gpa, .{ + .long_name = try arena.dupe(u8, opts.long_name), + .description = owned_description, + .kind = opts.kind + }); + + return @enumFromInt(index); +} + +fn getOptionByLongName(self: *CLI, long_name: []const u8) ?Option.Id { + for (0.., self.options.items) |i, option| { + if (std.mem.eql(u8, long_name, option.long_name)) { + return @enumFromInt(i); + } + } + return null; +} + +pub const AddCommandOptions = struct { + name: []const u8, + description: ?[]const u8 = null +}; + +pub fn addCommand(self: *CLI, opts: AddCommandOptions) !Command.Id { + const arena = self.arena.allocator(); + + var owned_description: []const u8 = ""; + if (opts.description) |description| { + owned_description = try arena.dupe(u8, description); + } + + const index = self.commands.items.len; + try self.commands.append(self.gpa, Command{ + .name = try arena.dupe(u8, opts.name), + .description = owned_description, + }); + + return @enumFromInt(index); +} + +pub fn showUsage(self: *CLI, program_name: []const u8) void { + self.stderr.write("Usage: {s}", .{program_name}); + if (self.options.items.len > 0) { + self.stderr.write(" [options]", .{}); + } + if (self.commands.items.len > 0) { + self.stderr.write(" ", .{}); + } + self.stderr.write("\n", .{}); + + if (self.commands.items.len > 0) { + self.stderr.write("\n", .{}); + self.stderr.write("Commands:\n", .{}); + for (self.commands.items) |command| { + self.stderr.write(" {s} {s}\n", .{command.name, command.description}); + } + } + + if (self.options.items.len > 0) { + self.stderr.write("\n", .{}); + self.stderr.write("Options:\n", .{}); + + for (0.., self.options.items) |i, option| { + if (i > 0) { + self.stderr.write("\n", .{}); + } + + self.stderr.write(" --{s}", .{option.long_name}); + if (option.kind == .single_argument) { + // TODO: Make this hint renameable + self.stderr.write(" ", .{}); + } + self.stderr.write("\n", .{}); + if (option.description.len > 0) { + self.stderr.write(" {s}\n", .{option.description}); + } + } + } + + self.stderr.flush(); +} + +pub const ParseResult = struct { + arena: std.heap.ArenaAllocator, + + command: ?Command.Id, + options: []?Option.Value, + + pub fn init(gpa: std.mem.Allocator, options_len: usize) !ParseResult { + var arena = std.heap.ArenaAllocator.init(gpa); + errdefer arena.deinit(); + + const options = try arena.allocator().alloc(?Option.Value, options_len); + @memset(options, null); + + return ParseResult{ + .arena = arena, + .command = null, + .options = options + }; + } + + pub fn deinit(self: *ParseResult) void { + self.arena.deinit(); + } + + pub fn getOption(self: ParseResult, id: Option.Id) ?Option.Value { + return self.options[@intFromEnum(id)]; + } + + pub fn isSet(self: ParseResult, id: Option.Id) bool { + return self.getOption(id) != null; + } + + // TODO: I don't like this function name + pub fn getOptionArgument(self: ParseResult, id: Option.Id) ?[]const u8 { + if (self.getOption(id)) |option| { + return option.argument; + } + return null; + } +}; + +const Parser = struct { + args: []const []const u8, + cursor: u32, + + pub fn peek(self: *Parser) ?[]const u8 { + if (self.cursor < self.args.len) { + return self.args[self.cursor]; + } + return null; + } + + pub fn take(self: *Parser) ?[]const u8 { + if (self.peek()) |value| { + self.cursor += 1; + return value; + } + return null; + } +}; + +fn printError(self: *CLI, comptime fmt: []const u8, args: anytype) void { + self.stderr.write("ERROR: " ++ fmt ++ "\n", args); + self.stderr.flush(); +} + +pub fn parse(self: *CLI, gpa: std.mem.Allocator, args: []const []const u8) !ParseResult { + var result = try ParseResult.init(gpa, self.options.items.len); + errdefer result.deinit(); + + var parser = Parser{ + .args = args, + .cursor = 1 + }; + + next_arg: while (parser.take()) |arg| { + if (std.mem.startsWith(u8, arg, "--")) { + const long_option_name = arg[2..]; + if (self.getOptionByLongName(long_option_name)) |option_id| { + const option = self.options.items[@intFromEnum(option_id)]; + var result_value = Option.Value{}; + + if (option.kind == .single_argument) { + const argument = parser.take() orelse { + self.printError("Missing required argument for '{s}'", .{arg}); + return error.MissingArgument; + }; + + result_value.argument = argument; + } + + result.options[@intFromEnum(option_id)] = result_value; + continue :next_arg; + } + + self.printError("Unknown option '{s}'", .{arg}); + return error.UnknownOption; + } + + if (result.command == null) { + for (0.., self.commands.items) |i, command| { + if (std.mem.eql(u8, command.name, arg)) { + result.command = @enumFromInt(i); + continue :next_arg; + } + } + + self.printError("Unknown command '{s}'", .{arg}); + return error.UnknownCommand; + } else { + self.printError("Unknown argument '{s}'", .{arg}); + return error.UnknownArgument; + } + } + + return result; +} diff --git a/src/color.zig b/src/color.zig new file mode 100644 index 0000000..007f842 --- /dev/null +++ b/src/color.zig @@ -0,0 +1,40 @@ +const std = @import("std"); +const assert = std.debug.assert; + +const Color = @This(); + +r: f32, +g: f32, +b: f32, +a: f32, + +pub const black = rgb(0, 0, 0); +pub const white = rgb(255, 255, 255); +pub const purple = rgb(255, 0, 255); + +pub fn rgba(r: u8, g: u8, b: u8, a: f32) Color { + assert(0 <= a and a <= 1); + return .{ + .r = @as(f32, @floatFromInt(r)) / 255, + .g = @as(f32, @floatFromInt(g)) / 255, + .b = @as(f32, @floatFromInt(b)) / 255, + .a = a, + }; +} + +pub fn rgb(r: u8, g: u8, b: u8) Color { + return rgba(r, g, b, 1); +} + +pub fn rgb_hex(text: []const u8) ?Color { + if (text.len != 7) { + return null; + } + if (text[0] != '#') { + return null; + } + const r = std.fmt.parseInt(u8, text[1..3], 16) catch return null; + const g = std.fmt.parseInt(u8, text[3..5], 16) catch return null; + const b = std.fmt.parseInt(u8, text[5..7], 16) catch return null; + return rgb(r, g, b); +} diff --git a/src/file_watcher.zig b/src/file_watcher.zig new file mode 100644 index 0000000..c749daa --- /dev/null +++ b/src/file_watcher.zig @@ -0,0 +1,320 @@ +const std = @import("std"); +const INotify = @import("inotify.zig"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +const FileWatcher = @This(); + +root_dir: []u8, +inotify: INotify, +files: std.ArrayList(File), +watches: std.ArrayList(Watch), + +const File = struct { + path: []u8, + debounce: Io.Duration, + last_event_at: ?Io.Timestamp, + + pub fn deinit(self: File, gpa: Allocator) void { + gpa.free(self.path); + } +}; + +const Watch = struct { + wd: ?u32, + path: []u8, + + pub fn deinit(self: Watch, gpa: Allocator) void { + gpa.free(self.path); + } +}; + +pub fn init(gpa: Allocator, root_dir: []const u8) !FileWatcher { + if (!std.fs.path.isAbsolute(root_dir)) { + return error.PathIsNotAbsolute; + } + + const inotify_buf = try gpa.alloc(u8, INotify.max_event_size * 10); + errdefer gpa.free(inotify_buf); + + const root_dir_owned = try gpa.dupe(u8, root_dir); + errdefer gpa.free(root_dir_owned); + + return FileWatcher{ + .root_dir = root_dir_owned, + .inotify = try .init(inotify_buf), + .files = .empty, + .watches = .empty + }; +} + +pub fn deinit(self: *FileWatcher, gpa: Allocator) void { + gpa.free(self.root_dir); + + gpa.free(self.inotify.buf); + self.inotify.deinit(); + + for (self.files.items) |file| { + file.deinit(gpa); + } + self.files.deinit(gpa); + + for (self.watches.items) |watch| { + watch.deinit(gpa); + } + self.watches.deinit(gpa); +} + +fn findFileIndex(self: *FileWatcher, path: []const u8) ?usize { + for (0.., self.files.items) |i, file| { + if (std.mem.eql(u8, file.path, path)) { + return i; + } + } + return null; +} + +fn findFile(self: *FileWatcher, path: []const u8) ?*File { + if (self.findFileIndex(path)) |file_index| { + return &self.files.items[file_index]; + } else { + return null; + } +} + +fn findWatchByPath(self: *FileWatcher, path: []const u8) ?usize { + for (0.., self.watches.items) |i, watch| { + if (std.mem.eql(u8, watch.path, path)) { + return i; + } + } + return null; +} + +fn findWatchByDescriptor(self: *FileWatcher, wd: u32) ?usize { + for (0.., self.watches.items) |i, watch| { + if (watch.wd == wd) { + return i; + } + } + return null; +} + +fn bufJoinPaths(path_buff: []u8, paths: []const []const u8) ![:0]u8 { + return try std.fmt.bufPrintZ(path_buff, "{f}", .{ + std.fs.path.fmtJoin(paths) + }); +} + +fn attemptRegisteringWatch(self: *FileWatcher, watch: *Watch) !void { + if (watch.wd != null) { + return; + } + + var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined; + const path = try bufJoinPaths(&path_buff, &.{ self.root_dir, watch.path }); + + watch.wd = self.inotify.add(path, INotify.Mask{ + .create = true, + .modify = true, + .delete_self = true, + .move_self = true, + .moved_to = true, + .moved_from = true, + }) catch |err| switch (err) { + error.FileNotFound => null, + else => return err + }; +} + +pub fn toRelativePath(outer_path: []const u8, inner_path: []const u8) ?[]const u8 { + if (inner_path.len == outer_path.len) { + if (std.mem.eql(u8, inner_path, outer_path)) { + return ""; + } + } else if (inner_path.len > outer_path.len) { + if (std.mem.startsWith(u8, inner_path, outer_path) and inner_path[outer_path.len] == std.fs.path.sep) { + return inner_path[(outer_path.len+1)..]; + } + } + + return null; +} + +fn isInside(outer_path: []const u8, inner_path: []const u8) bool { + return toRelativePath(outer_path, inner_path) != null; +} + +fn unregisterWatchesRecursively(self: *FileWatcher, dir: []const u8) !void { + for (self.watches.items) |*watch| { + const wd = watch.wd orelse continue; + + if (isInside(dir, watch.path)) { + try self.inotify.remove(wd); + watch.wd = null; + } + } +} + +fn registerWatchesRecursively(self: *FileWatcher, dir: []const u8) !void { + for (self.watches.items) |*watch| { + if (watch.wd == null and isInside(dir, watch.path)) { + try self.attemptRegisteringWatch(watch); + } + } +} + +fn ensureWatchExists(self: *FileWatcher, gpa: Allocator, path: []const u8) !usize { + var watch_index: usize = undefined; + if (self.findWatchByPath(path)) |found_watch_index| { + watch_index = found_watch_index; + } else { + try self.watches.ensureUnusedCapacity(gpa, 1); + + const path_owned = try gpa.dupe(u8, path); + errdefer gpa.free(path_owned); + + watch_index = self.watches.items.len; + self.watches.appendAssumeCapacity(Watch{ + .wd = null, + .path = path_owned + }); + } + + try self.attemptRegisteringWatch(&self.watches.items[watch_index]); + + return watch_index; +} + +fn removeWatch(self: *FileWatcher, gpa: Allocator, index: usize) void { + const watch = self.watches.swapRemove(index); + watch.deinit(gpa); +} + +pub fn add(self: *FileWatcher, gpa: Allocator, path: []const u8, debounce: Io.Duration) !void { + if (std.fs.path.isAbsolute(path)) { + return error.PathIsAbsolute; + } + + if (self.findFileIndex(path) != null) { + return; + } + + var dir_iter: ?[]const u8 = path; + while (dir_iter) |current| { + _ = try self.ensureWatchExists(gpa, current); + + dir_iter = std.fs.path.dirname(current); + } + + _ = try self.ensureWatchExists(gpa, ""); + + try self.files.ensureUnusedCapacity(gpa, 1); + + const path_owned = try gpa.dupe(u8, path); + errdefer gpa.free(path_owned); + + self.files.appendAssumeCapacity(File{ + .path = path_owned, + .debounce = debounce, + .last_event_at = null + }); +} + +pub fn remove(self: *FileWatcher, gpa: Allocator, path: []const u8) void { + const index = self.findFileIndex(path); + if (index == null) { + return; + } + + const file = self.files.items[index]; + self.files.swapRemove(index); + + file.deinit(gpa); +} + +fn nextEvents(self: *FileWatcher) !?*File { + while (try self.inotify.next()) |e| { + const watch_index = self.findWatchByDescriptor(e.wd) orelse continue; + var watch = &self.watches.items[watch_index]; + + if (e.mask.ignored) { + watch.wd = null; + + } else if (e.mask.q_overflow) { + return error.QueueOverflow; + + } else if (e.mask.delete_self or e.mask.modify) { + if (self.findFile(watch.path)) |file| { + return file; + } + + } else if (e.mask.create) { + assert(e.name != null); + var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined; + const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) }); + if (self.findWatchByPath(path)) |child_watch_index| { + try self.attemptRegisteringWatch(&self.watches.items[child_watch_index]); + } + + if (self.findFile(path)) |file| { + return file; + } + + } else if (e.mask.moved_from) { + assert(e.name != null); + var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined; + const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) }); + try self.unregisterWatchesRecursively(path); + + if (self.findFile(path)) |file| { + return file; + } + + } else if (e.mask.moved_to) { + assert(e.name != null); + var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined; + const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) }); + try self.registerWatchesRecursively(path); + + if (self.findFile(path)) |file| { + return file; + } + } + } + + return null; +} + +pub fn next(self: *FileWatcher, io: Io) !?[]const u8 { + const now = Io.Clock.real.now(io); + + var queue_overflow = false; + while (true) { + const file = (self.nextEvents() catch |err| switch (err) { + error.QueueOverflow => { + queue_overflow = true; + break; + }, + else => return err + }) orelse break; + file.last_event_at = now; + } + + if (queue_overflow) { + for (self.files.items) |*file| { + file.last_event_at = now; + } + } + + for (self.files.items) |*file| { + const last_event_at = file.last_event_at orelse continue; + if (now.nanoseconds > last_event_at.addDuration(file.debounce).nanoseconds) { + file.last_event_at = null; + return file.path; + } + } + + return null; +} diff --git a/src/graphics.zig b/src/graphics.zig new file mode 100644 index 0000000..8a29f1a --- /dev/null +++ b/src/graphics.zig @@ -0,0 +1,1996 @@ +const std = @import("std"); +const log = std.log.scoped(.graphics); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const sokol = @import("sokol"); +const sg = sokol.gfx; +const sapp = sokol.app; +const sglue = sokol.glue; + +const Math = @import("math"); +const Vec2 = Math.Vec2; +const Vec4 = Math.Vec4; +const Mat4 = Math.Mat4; +const Rect = Math.Rect; + +const STBRectPack = @import("stb_rect_pack"); +const STBTrueType = @import("stb_truetype"); +const ImGUI = @import("imgui.zig"); +const Color = @import("./color.zig"); +const shd = @import("shader"); +const SlotMapType = @import("slot_map.zig").SlotMapType; + +const State = struct { + gpa: std.mem.Allocator, + + frame_index: u64, + + shader: sg.Shader, + pipeline: sg.Pipeline, + bindings: sg.Bindings, + + default_sprite: Sprite.Id, + error_sprite: Sprite.Id, + default_spritesheet: Spritesheet.Id, + default_font_spritesheet: Spritesheet.Id, + + linear_sampler: sg.Sampler, + nearest_sampler: sg.Sampler, + default_sampler: Sampler, + + quads_buffer: [2048]Quad, + quads: std.ArrayList(Quad), + + clear_color: Color, + + textures_buffer: [Texture.max_textures]Texture.SlotMap.Slot, + textures: Texture.SlotMap, + nil_texture: Texture.Id, + + spritesheets_buffer: [Spritesheet.max_spritesheets]Spritesheet.SlotMap.Slot, + spritesheets: Spritesheet.SlotMap, + nil_spritesheet: Spritesheet.Id, + + sprites_buffer: [Sprite.max_sprites]Sprite.SlotMap.Slot, + sprites: Sprite.SlotMap, + nil_sprite: Sprite.Id, + + fonts_buffer: [Font.max_fonts]Font.SlotMap.Slot, + fonts: Font.SlotMap, + nil_font: Font.Id, +}; + +var g_state: State = undefined; + +pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { + const self = &g_state; + + sg.setup(.{ + .environment = sglue.environment(), + .logger = logger + }); + + const shader = sg.makeShader(shd.mainShaderDesc(sg.queryBackend())); + assert(shader.id != sg.invalid_id); + + const linear_sampler = sg.makeSampler(.{ + .min_filter = .LINEAR, + .mag_filter = .LINEAR, + .label = "linear-sampler" + }); + assert(linear_sampler.id != sg.invalid_id); + + const nearest_sampler = sg.makeSampler(.{ + .min_filter = .NEAREST, + .mag_filter = .NEAREST, + .label = "nearest-sampler" + }); + assert(linear_sampler.id != sg.invalid_id); + + const pipeline = sg.makePipeline(.{ + .primitive_type = .TRIANGLES, + .index_type = .UINT16, + .colors = init: { + var s: [8]sg.ColorTargetState = [_]sg.ColorTargetState{.{}} ** 8; + s[0] = .{ + .blend = .{ + .enabled = true, + .src_factor_rgb = .SRC_ALPHA, + .dst_factor_rgb = .ONE_MINUS_SRC_ALPHA, + .op_rgb = .ADD, + .src_factor_alpha = .ONE, + .dst_factor_alpha = .ONE_MINUS_SRC_ALPHA, + .op_alpha = .ADD, + } + }; + break :init s; + }, + .shader = shader, + .layout = init: { + var l = sg.VertexLayoutState{}; + l.attrs[shd.ATTR_main_position].format = .FLOAT2; + l.attrs[shd.ATTR_main_texcoord0].format = .FLOAT2; + l.attrs[shd.ATTR_main_color0].format = .FLOAT4; + break :init l; + }, + .label = "main-pipeline" + }); + assert(pipeline.id != sg.invalid_id); + + var bindings: sg.Bindings = .{}; + { + const max_quads = self.quads_buffer.len; + + bindings.vertex_buffers[0] = sg.makeBuffer(.{ + .size = @sizeOf(Quad) * max_quads, + .usage = .{ .vertex_buffer = true, .stream_update = true }, + .label = "quad-vertices" + }); + + const index_count = max_quads * 6; + var indices: [index_count]u16 = undefined; + for (0..max_quads) |i| { + indices[6*i + 0] = @intCast(4*i + 0); + indices[6*i + 1] = @intCast(4*i + 1); + indices[6*i + 2] = @intCast(4*i + 2); + + indices[6*i + 3] = @intCast(4*i + 1); + indices[6*i + 4] = @intCast(4*i + 3); + indices[6*i + 5] = @intCast(4*i + 2); + } + + bindings.index_buffer = sg.makeBuffer(.{ + .data = sg.asRange(&indices), + .usage = .{ .index_buffer = true, }, + .label = "indices" + }); + } + + self.* = State{ + .gpa = gpa, + .frame_index = 0, + .shader = shader, + .pipeline = pipeline, + .bindings = bindings, + .quads_buffer = undefined, + .quads = .initBuffer(&self.quads_buffer), + .clear_color = .black, + .default_sprite = undefined, + .error_sprite = undefined, + .default_spritesheet = undefined, + .default_font_spritesheet = undefined, + + .linear_sampler = linear_sampler, + .nearest_sampler = nearest_sampler, + .default_sampler = .linear, + + .textures_buffer = undefined, + .textures = .init(.initBuffer(&self.textures_buffer)), + .nil_texture = undefined, + + .sprites_buffer = undefined, + .sprites = .init(.initBuffer(&self.sprites_buffer)), + .nil_sprite = undefined, + + .spritesheets_buffer = undefined, + .spritesheets = .init(.initBuffer(&self.spritesheets_buffer)), + .nil_spritesheet = undefined, + + .fonts_buffer = undefined, + .fonts = .init(.initBuffer(&self.fonts_buffer)), + .nil_font = undefined, + }; + + self.nil_texture = self.textures.insertAssumeCapacity(); + { + const nil_texture = self.textures.getAssumeExists(self.nil_texture); + nil_texture.* = Texture{ + .dynamic = false, + .updated_at = null, + .image = .{ .id = sg.invalid_id }, + .view = .{ .id = sg.invalid_id }, + }; + } + + self.nil_spritesheet = self.spritesheets.insertAssumeCapacity(); + { + const nil_spritesheet = self.spritesheets.getAssumeExists(self.nil_spritesheet); + nil_spritesheet.* = Spritesheet{ + .texture = self.nil_texture, + .format = .rgba8, + .size = .init(0, 0), + .needs_repack = false, + .needs_texture_rebuild = false, + .nodes = undefined, + .packer = null, + }; + } + + self.nil_sprite = self.sprites.insertAssumeCapacity(); + { + const nil_sprite = self.sprites.getAssumeExists(self.nil_sprite); + nil_sprite.* = Sprite{ + .spritesheet = self.nil_spritesheet, + .data = .none, + .position = null, + .padding = 0 + }; + } + + self.nil_font = self.fonts.insertAssumeCapacity(); + { + const nil_font = self.fonts.getAssumeExists(self.nil_font); + nil_font.* = Font{ + .spritesheet = self.nil_spritesheet, + .stb = null, + .cache = .init(), + }; + } + + self.default_spritesheet = initSpritesheet(.{}); + self.default_font_spritesheet = initSpritesheet(.{ .format = .r8 }); + + { + const width = 8; + const height = 8; + + var pixels: [width * height * 4]u8 = undefined; + @memset(&pixels, 0xFF); + + self.default_sprite = initSprite(.{ .padding = 1 }); + setSprite(self.default_sprite, .{ + .image = ImageData{ + .width = width, + .height = height, + .pixels = .{ .rgba8 = &pixels } + } + }); + } + + { + const width = 8; + const height = 8; + + var pixels: [width * height * 4]u8 = undefined; + for (0..height) |y| { + for (0..width) |x| { + const pixel_index = y * width + x; + + var color: Color = undefined; + if ((pixel_index+y) % 2 == 0) { + color = .purple; + } else { + color = .black; + } + + const pixel = pixels[(4*pixel_index)..]; + pixel[0] = @intFromFloat(color.r*255); + pixel[1] = @intFromFloat(color.g*255); + pixel[2] = @intFromFloat(color.b*255); + pixel[3] = @intFromFloat(color.a*255); + } + } + + self.error_sprite = initSprite(.{ .padding = 1 }); + setSprite(self.error_sprite, .{ + .image = .{ + .width = width, + .height = height, + .pixels = .{ .rgba8 = &pixels } + } + }); + } + + repackSpritesheetIfNeeded(self.default_spritesheet); + rebuildSpritesheetTextureIfNeeded(self.default_spritesheet); +} + +pub fn deinit() void { + const self = &g_state; + + var sprite_iter = self.sprites.iterator(); + while (sprite_iter.next()) |sprite_id| { + const sprite = self.sprites.getAssumeExists(sprite_id); + if (sprite.data) |data| { + data.deinit(self.gpa); + } + } + + sg.shutdown(); +} + +fn createProjectionMatrix(screen_size: Vec2) Mat4 { + return Mat4.initIdentity() + // Convert normalized [-1; 1] coordinates to [0; 1] + .multiply(Mat4.initTranslate(.{ .x = -1, .y = -1, .z = 0 })) + .multiply(Mat4.initScale(.{ .x = 2, .y = 2, .z = 0 })) + + // Make the top-left corner (0,0) + .multiply(Mat4.initTranslate(.{ .x = 0, .y = 1, .z = 0 })) + .multiply(Mat4.initScale(.{ .x = 1, .y = -1, .z = 0 })) + + // Scretch [0; 1] so that they map 1-to-1 on to screen coordinates. + .multiply(Mat4.initScale(.{ + .x = 1/screen_size.x, + .y = 1/screen_size.y, + .z = 0 + })); +} + +fn toSokolColor(color: Color) sokol.gfx.Color { + // TODO: Assert and do cast + return sokol.gfx.Color{ + .r = color.r, + .g = color.g, + .b = color.b, + .a = color.a, + }; +} + +pub fn beginFrame() void { + const self = &g_state; + + var pass_action: sg.PassAction = .{}; + pass_action.colors[0] = .{ + .load_action = .CLEAR, + .clear_value = toSokolColor(self.clear_color) + }; + + sg.beginPass(.{ + .action = pass_action, + .swapchain = sglue.swapchain() + }); + + const default_spritesheet = self.spritesheets.getAssumeExists(self.default_spritesheet); + const spritesheet_texture = self.textures.getAssumeExists(default_spritesheet.texture); + self.bindings.views[shd.VIEW_tex] = spritesheet_texture.view; + self.bindings.samplers[shd.SMP_smp] = self.linear_sampler; +} + +pub fn showDebug() void { + const self = &g_state; + _ = self; // autofix + + // TODO: Improve this by using tables or somekind of filtering + + // if (ImGUI.beginWindow(.{ + // .name = "graphics", + // .size = .init(200, 200) + // })) { + // defer ImGUI.endWindow(); + // + // { + // ImGUI.text("Spritesheets:", .{}); + // var spritesheet_iter = self.spritesheets.iterator(); + // while (spritesheet_iter.next()) |spritesheet_id| { + // const spritesheet = self.spritesheets.getAssumeExists(spritesheet_id); + // const texture = self.textures.get(spritesheet.texture); + // ImGUI.text("{f}:", .{spritesheet_id}); + // ImGUI.text(" - needs_texture_rebuild:{}", .{spritesheet.needs_texture_rebuild}); + // ImGUI.text(" - texture:{?}", .{texture}); + // } + // } + // + // { + // ImGUI.text("Sprites:", .{}); + // var sprite_iter = self.sprites.iterator(); + // while (sprite_iter.next()) |sprite_id| { + // const sprite = self.sprites.getAssumeExists(sprite_id); + // ImGUI.text("{f} - {?}", .{sprite_id, sprite.position}); + // } + // } + // } +} + +// TODO: This will always have a 1-frame delay. +// fix this. +pub fn setClearColor(color: Color) void { + const self = &g_state; + + self.clear_color = color; +} + +const FlushOptions = struct { + rebuild_spritesheets: bool = true +}; + +pub fn flush(opts: FlushOptions) void { + const self = &g_state; + + if (self.quads.items.len == 0) { + return; + } + defer self.quads.clearRetainingCapacity(); + + if (opts.rebuild_spritesheets) { + var spritesheet_iter = self.spritesheets.iterator(); + while (spritesheet_iter.next()) |spritesheet_id| { + const spritesheet = self.spritesheets.getAssumeExists(spritesheet_id); + const texture = self.textures.get(spritesheet.texture) orelse continue; + if (isViewBound(texture.view)) { + rebuildSpritesheetTextureIfNeeded(spritesheet_id); + } + } + } + + const vertex_buffer = self.bindings.vertex_buffers[0]; + + const data = sg.asRange(self.quads.items); + if (sg.queryBufferWillOverflow(vertex_buffer, data.size)) { + log.warn("Vertex buffer overflow", .{}); + return; + } + + self.bindings.vertex_buffer_offsets[0] = sg.appendBuffer(vertex_buffer, data); + + // TODO: Rectify `window_size` vs `gfx.window_size` + const window_size = Vec2.init(sapp.widthf(), sapp.heightf()); + + var vs_params: shd.VsParams = .{ + // TODO: Maybe 'view' matrix is not needed. + // We probably should only have a combined 'mvp' matrix + .view = .initIdentity(), + .projection = createProjectionMatrix(window_size) + }; + + var fs_params: shd.FsParams = .{ + .texture_mode = 0, + }; + const image = sg.queryViewImage(self.bindings.views[shd.VIEW_tex]); + if (sg.queryImagePixelformat(image) == .R8) { + fs_params.texture_mode = 1; + } + + sg.applyPipeline(self.pipeline); + sg.applyBindings(self.bindings); + sg.applyUniforms(shd.UB_vs_params, sg.asRange(&vs_params)); + sg.applyUniforms(shd.UB_fs_params, sg.asRange(&fs_params)); + sg.draw(0, @intCast(self.quads.items.len * 6), 1); +} + +pub fn endFrame() void { + const self = &g_state; + + flush(.{}); + + sg.endPass(); + sg.commit(); + self.frame_index += 1; + + var spritesheet_iter = self.spritesheets.iterator(); + while (spritesheet_iter.next()) |spritesheet_id| { + repackSpritesheetIfNeeded(spritesheet_id); + } +} + +pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void { + const self = &g_state; + + drawSprite(self.default_sprite, pos, size, color); +} + +pub fn drawLine(from: Vec2, to: Vec2, color: Color, width: f32) void { + const self = &g_state; + + const step = to.sub(from).normalized().multiplyScalar(width/2); + + const top_left = from.add(step.rotateLeft90()); + const bottom_left = from.add(step.rotateRight90()); + const top_right = to.add(step.rotateLeft90()); + const bottom_right = to.add(step.rotateRight90()); + + drawSpriteQuad( + self.default_sprite, + .{ top_right, top_left, bottom_right, bottom_left }, + color + ); +} + +const TextureOptions = struct { + dynamic: bool = false +}; + +pub fn initTexture(opts: TextureOptions) Texture.Id { + const self = &g_state; + const id = self.textures.insertBounded() catch { + log.warn("Failed to create texture, limit reached! limit: {}", .{self.textures.slots.capacity}); + return self.nil_texture; + }; + + const view = sg.allocView(); + if (sg.queryViewState(view) != .ALLOC) { + log.warn("Failed to alloc texture view", .{}); + return self.nil_texture; + } + + const texture = self.textures.getAssumeExists(id); + texture.* = .{ + .dynamic = opts.dynamic, + .updated_at = null, + .image = .{ .id = sg.invalid_id }, + .view = view, + }; + return id; +} + +fn isViewBound(view: sg.View) bool { + const self = &g_state; + const bound_view = &self.bindings.views[shd.VIEW_tex]; + return bound_view.id == view.id; +} + +fn deinitTextureResources(texture: *Texture) void { + const self = &g_state; + + if (isViewBound(texture.view)) { + const default_sprite = self.sprites.getAssumeExists(self.default_sprite); + const default_spritesheet = self.spritesheets.getAssumeExists(default_sprite.spritesheet); + const default_texture = self.textures.getAssumeExists(default_spritesheet.texture); + bindView(default_texture.view, .{ .rebuild_spritesheets = false }); + } + + sg.destroyImage(texture.image); +} + +pub fn deinitTexture(id: Texture.Id) void { + const self = &g_state; + if (id == self.nil_texture) { + return; + } + + if (self.textures.get(id)) |texture| { + deinitTextureResources(texture); + sg.destroyView(texture.view); + self.textures.removeAssumeExists(id); + } +} + +pub fn setTexture(id: Texture.Id, texture_data: ImageData) void { + const self = &g_state; + if (id == self.nil_texture) { + return; + } + + const texture = self.textures.get(id) orelse { + log.warn("Attempt to set texture that doesn't exist: {}", .{id}); + return; + }; + + const pixel_count = texture_data.width*texture_data.height; + + var image_data: sg.ImageData = .{}; + var pixel_format: sg.PixelFormat = .DEFAULT; + + if (texture_data.pixels) |pixels| { + switch (pixels) { + .rgba8 => |rgba8| { + image_data.mip_levels[0] = sg.asRange(rgba8[0..(pixel_count*4)]); + pixel_format = .RGBA8; + }, + .r8 => |r8| { + image_data.mip_levels[0] = sg.asRange(r8[0..pixel_count]); + pixel_format = .R8; + } + } + } + + var new_image_desc = sg.ImageDesc{ + .type = ._2D, + .width = @intCast(texture_data.width), + .height = @intCast(texture_data.height), + .num_mipmaps = 1, + .pixel_format = pixel_format, + }; + if (texture.dynamic) { + new_image_desc.usage.dynamic_update = true; + } else { + new_image_desc.data = image_data; + } + + var texture_updated: bool = false; + if (sg.queryImageState(texture.image) == .VALID) { + // sg.queryImageInfo + const image_desc = sg.queryImageDesc(texture.image); + if (image_desc.width == new_image_desc.width and + image_desc.height == new_image_desc.height and + image_desc.num_mipmaps == new_image_desc.num_mipmaps and + image_desc.pixel_format == new_image_desc.pixel_format and + image_desc.usage.dynamic_update == new_image_desc.usage.dynamic_update and + image_desc.usage.immutable == new_image_desc.usage.immutable and + image_desc.usage.dynamic_update + ) { + log.debug("Update texture '{f}'", .{id}); + if (texture.updated_at == null or texture.updated_at.? != self.frame_index) { + sg.updateImage(texture.image, image_data); + texture_updated = true; + texture.updated_at = self.frame_index; + } else { + log.warn("Attempt to update the same texture multiple times per frame", .{}); + } + } + } + + if (!texture_updated) { + log.debug("Create image-view, texture={f}", .{id}); + const new_image = sg.makeImage(new_image_desc); + if (sg.queryImageState(new_image) != .VALID) { + log.warn("makeImage() failed", .{}); + return; + } + + if (sg.queryViewState(texture.view) == .VALID) { + sg.uninitView(texture.view); + } + sg.initView(texture.view, .{ + .texture = .{ .image = new_image } + }); + if (sg.queryViewState(texture.view) != .VALID) { + log.warn("makeView() failed", .{}); + return; + } + + deinitTextureResources(texture); + texture.image = new_image; + texture.updated_at = null; + + if (texture.dynamic) { + sg.updateImage(new_image, image_data); + texture.updated_at = self.frame_index; + } + } +} + +pub fn drawTexture(id: Texture.Id, pos: Vec2, size: Vec2, color: Color) void { + var quad: Quad = undefined; + quad.setColor(color); + quad.setRect(.{ .pos = pos, .size = size }); + quad.setUVRect(.init(0, 0, 1, 1)); + + draw(.{ + .texture = id, + .quad = quad + }); +} + +fn bindView(view: sg.View, opts: FlushOptions) void { + const self = &g_state; + + const view_state = sg.queryViewState(view); + if (view_state != .VALID and view_state != .ALLOC) { + log.warn("Attempt to bind non-valid view: state={}", .{view_state}); + return; + } + + const bound_view = &self.bindings.views[shd.VIEW_tex]; + if (bound_view.id != view.id) { + flush(opts); + bound_view.* = view; + } +} + +pub fn setDefaultSampler(sampler: Sampler) void { + const self = &g_state; + self.default_sampler = sampler; +} + +pub const Sampler = enum { + linear, + nearest, +}; + +const DrawOptions = struct { + sampler: ?Sampler = null, + texture: Texture.Id, + quad: Quad +}; + +pub fn draw(opts: DrawOptions) void { + const self = &g_state; + + var quad = opts.quad; + + var view: sg.View = .{ .id = sg.invalid_id }; + if (opts.texture != self.nil_texture and self.textures.exists(opts.texture)) { + const texture = self.textures.getAssumeExists(opts.texture); + view = texture.view; + } + + if (sg.queryViewState(view) != .VALID and sg.queryViewState(view) != .ALLOC) { + if (self.sprites.get(self.error_sprite)) |sprite| { + if (self.spritesheets.get(sprite.spritesheet)) |spritesheet| { + if (self.textures.get(spritesheet.texture)) |texture| { + if (getSpriteUVRect(self.error_sprite)) |uv| { + quad.setUVRect(uv); + quad.setColor(.white); + view = texture.view; + } + } + } + } + } + + if (sg.queryViewState(view) != .VALID and sg.queryViewState(view) != .ALLOC) { + log.warn("Failed to bind view", .{}); + return; + } + + bindView(view, .{}); + + const sampler = switch (opts.sampler orelse self.default_sampler) { + .linear => self.linear_sampler, + .nearest => self.nearest_sampler, + }; + const bound_sampler = &self.bindings.samplers[shd.SMP_smp]; + if (bound_sampler.id != sampler.id) { + flush(.{}); + bound_sampler.* = sampler; + } + + if (self.quads.items.len == self.quads.capacity) { + flush(.{}); + } + + self.quads.appendAssumeCapacity(quad); +} + +const SpriteOptions = struct { + spritesheet: ?Spritesheet.Id = null, + padding: u32 = 0, +}; + +// TODO: I don't like this getter, maybe nil_sprite should be a public global variable. +pub fn getNilSprite() Sprite.Id { + const self = &g_state; + return self.nil_sprite; +} + +pub fn initSprite(opts: SpriteOptions) Sprite.Id { + const self = &g_state; + const id = self.sprites.insertBounded() catch { + log.warn("Failed to create sprite, limit reached! limit: {}", .{self.sprites.slots.capacity}); + return self.nil_sprite; + }; + + const sprite = self.sprites.getAssumeExists(id); + sprite.* = .{ + .spritesheet = opts.spritesheet orelse self.default_spritesheet, + .padding = opts.padding, + .data = null, + .position = null + }; + + return id; +} + +pub fn deinitSprite(id: Sprite.Id) void { + const self = &g_state; + if (id == self.nil_sprite) { + return; + } + + if (self.sprites.exists(id)) { + const sprite = self.sprites.getAssumeExists(id); + if (sprite.data) |data| { + data.deinit(self.gpa); + } + self.sprites.removeAssumeExists(id); + + if (self.spritesheets.get(sprite.spritesheet)) |spritesheet| { + spritesheet.needs_repack = true; + } + } +} + +const SetSpriteOptions = struct { + image: ImageData, + rect: ?Rect = null +}; + +pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void { + const self = &g_state; + if (id == self.nil_sprite) { + return; + } + + const sprite = self.sprites.get(id) orelse { + log.warn("Attempt to set sprite that doesn't exist: {}", .{id}); + return; + }; + + const spritesheet = self.spritesheets.get(sprite.spritesheet) orelse { + log.warn("Attempt to set sprite which doesn't have spritesheet", .{}); + return; + }; + + var sprite_data: ImageData = .none; + errdefer sprite_data.deinit(self.gpa); + + if (opts.image.pixels) |pixels| { + const format = @as(ImageData.Format, pixels); + if (format != spritesheet.format) { + log.warn("Attempt to use image of format '{}' on a spritesheet with '{}' format", .{format, spritesheet.format}); + return; + } + } + + if (opts.rect) |rect| { + var format: ?ImageData.Format = null; + if (opts.image.pixels) |pixels| { + format = @as(ImageData.Format, pixels); + } + + sprite_data = ImageData.init(self.gpa, @trunc(rect.size.x), @trunc(rect.size.y), format) catch |e| { + log.warn("Failed to create texture data: {}", .{e}); + return; + }; + + sprite_data.copyFrom( + opts.image, + 0, 0, + @trunc(rect.pos.x), @trunc(rect.pos.y), + @trunc(rect.size.x), @trunc(rect.size.y) + ); + } else { + sprite_data = opts.image.clone(self.gpa) catch |e| { + log.warn("Failed to clone image: {}", .{e}); + return; + }; + } + + var old_size: ?Vec2 = null; + if (sprite.data) |old_sprite_data| { + old_size = Vec2.initFromInt(u32, old_sprite_data.width, old_sprite_data.height); + old_sprite_data.deinit(self.gpa); + } + sprite.data = sprite_data; + + const new_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height); + + if (old_size != null and old_size.?.eql(new_size) and sprite.position != null) { + spritesheet.needs_texture_rebuild = true; + } else { + sprite.position = null; + spritesheet.needs_repack = true; + } +} + +// WARNING: This will change the UV coordinates of all sprites. +// It's only safe to do this operation between frames where it's guarenteed that sprites aren't being used. +fn repackSpritesheet(id: Spritesheet.Id) void { + const self = &g_state; + + const spritesheet = self.spritesheets.get(id) orelse { + log.warn("Attempt to repack spritesheet which doesn't exist", .{}); + return; + }; + + log.debug("Repack spritesheet: {f}", .{id}); + + // TODO: Add a smarter startegy for picking the initial texture size. + // One idea is to calculate the sum area of all sprites and pick a atlas size based on that. + const texture_sizes = [_]u32{ + 256, 512, 1024, 2048, 4096 + }; + + var sprites_buffer: [Sprite.max_sprites]*Sprite = undefined; + var sprites = std.ArrayList(*Sprite).initBuffer(&sprites_buffer); + + var sprite_iter = self.sprites.iterator(); + while (sprite_iter.next()) |sprite_id| { + const sprite = self.sprites.getAssumeExists(sprite_id); + if (sprite.data != null and sprite.spritesheet == id) { + sprites.appendAssumeCapacity(sprite); + } + } + + var rects: [Sprite.max_sprites]STBRectPack.Rect = undefined; + @memset(&rects, .{}); + for (0.., sprites.items) |i, sprite| { + const sprite_data = sprite.data.?; + rects[i] = .{ + .w = @intCast(sprite_data.width + sprite.padding * 2), + .h = @intCast(sprite_data.height + sprite.padding * 2), + }; + } + + spritesheet.packer = undefined; + const packer = &spritesheet.packer.?; + + for (&texture_sizes) |texture_size| { + packer.init(texture_size, texture_size, &spritesheet.nodes); + packer.setupAllowOutOfMem(true); + + if (!packer.packRects(&rects)) { + continue; + } + + for (0.., sprites.items) |i, sprite| { + assert(rects[i].was_packed == 1); + const x: u32 = @as(u32, @intCast(rects[i].x)) + sprite.padding; + const y: u32 = @as(u32, @intCast(rects[i].y)) + sprite.padding; + sprite.position = .initFromInt(u32, x, y); + } + + spritesheet.size = .initFromInt(u32, texture_size, texture_size); + spritesheet.needs_texture_rebuild = true; + spritesheet.needs_repack = false; + return; + } + + const max_texture_size = texture_sizes[texture_sizes.len-1]; + log.err("Failed to pack sprites, couldn't create a texture that is large enough. Max size tried: {}x{}", .{max_texture_size, max_texture_size}); + return; +} + +fn repackSpritesheetIfNeeded(id: Spritesheet.Id) void { + const self = &g_state; + if (id == self.nil_spritesheet) { + return; + } + + const spritesheet = self.spritesheets.get(id) orelse { + log.warn("Attempt to pack spritesheet that doesn't exist: {f}", .{id}); + return; + }; + + if (!spritesheet.needs_repack) { + return; + } + + repackSpritesheet(id); +} + +fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void { + const self = &g_state; + if (id == self.nil_spritesheet) { + return; + } + + const spritesheet = self.spritesheets.get(id) orelse return; + if (!spritesheet.needs_texture_rebuild) { + return; + } + + log.debug("Rebuild spritesheet texture: {f}", .{id}); + + const image_data = ImageData.init( + self.gpa, + @intFromFloat(spritesheet.size.x), + @intFromFloat(spritesheet.size.y), + spritesheet.format + ) catch |e| { + log.err("ImageData allocation failed: {}", .{e}); + return; + }; + defer image_data.deinit(self.gpa); + + var sprite_iter = self.sprites.iterator(); + while (sprite_iter.next()) |sprite_id| { + const sprite = self.sprites.getAssumeExists(sprite_id); + if (sprite.spritesheet != id) { + continue; + } + + const sprite_data = sprite.data orelse continue; + const sprite_position = sprite.position orelse continue; + const x: u32 = @intFromFloat(sprite_position.x); + const y: u32 = @intFromFloat(sprite_position.y); + image_data.setRectZero( + x - sprite.padding, + y - sprite.padding, + sprite_data.width + sprite.padding * 2, + sprite_data.height + sprite.padding * 2 + ); + image_data.copyFrom( + sprite_data, + x, + y, + 0, + 0, + sprite_data.width, + sprite_data.height + ); + } + + setTexture(spritesheet.texture, image_data); + + spritesheet.needs_texture_rebuild = false; +} + +fn ensureSpritePacked(id: Sprite.Id) bool { + const self = &g_state; + const sprite = self.sprites.get(id) orelse return false; + + if (sprite.position == null) { + const spritesheet = self.spritesheets.get(sprite.spritesheet) orelse return false; + const sprite_data = sprite.data orelse return false; + + if (spritesheet.packer) |*packer| { + var rects: [1]STBRectPack.Rect = .{ + .{ + .w = @intCast(sprite_data.width + sprite.padding * 2), + .h = @intCast(sprite_data.height + sprite.padding * 2), + } + }; + if (packer.packRects(&rects)) { + // Suboptimal sprite packing, will repack at end of frame + assert(rects[0].was_packed == 1); + const x: u32 = @as(u32, @intCast(rects[0].x)) + sprite.padding; + const y: u32 = @as(u32, @intCast(rects[0].y)) + sprite.padding; + sprite.position = .initFromInt(u32, x, y); + spritesheet.needs_repack = true; + } else { + log.debug("Failed to pack a single sprite to spritesheet", .{}); + } + } else { + repackSpritesheet(sprite.spritesheet); + } + } + + return sprite.position != null; +} + +pub fn getSpriteUVRect(id: Sprite.Id) ?Rect { + const self = &g_state; + if (id == self.nil_sprite) { + return null; + } + + const sprite = self.sprites.get(id) orelse return null; + const sprite_data = sprite.data orelse return null; + + const spritesheet = self.spritesheets.get(sprite.spritesheet) orelse return null; + + if (!ensureSpritePacked(id)) { + return null; + } + + assert(sprite.position != null); + + const sprite_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height); + return Rect{ + .pos = sprite.position.?.divide(spritesheet.size), + .size = sprite_size.divide(spritesheet.size), + }; +} + +fn drawSpriteQuad(id: Sprite.Id, points: [4]Vec2, color: Color) void { + const self = &g_state; + + var quad: Quad = undefined; + quad.setColor(color); + quad.vertices[0].position = points[0]; + quad.vertices[1].position = points[1]; + quad.vertices[2].position = points[2]; + quad.vertices[3].position = points[3]; + + var texture: ?Texture.Id = null; + if (id != self.nil_sprite) { + if (getSpriteUVRect(id)) |sprite_uv| { + const sprite = self.sprites.getAssumeExists(id); + const spritesheet = self.spritesheets.getAssumeExists(sprite.spritesheet); + const texel_size = spritesheet.size; + + quad.setUVRect(sprite_uv); + texture = spritesheet.texture; + } + } + + draw(.{ + .texture = texture orelse self.nil_texture, + .quad = quad + }); +} + +pub fn drawSprite(id: Sprite.Id, pos: Vec2, size: Vec2, color: Color) void { + const rect = .{ + pos, + pos.add(.init(size.x, 0)), + pos.add(.init(0, size.y)), + pos.add(size), + }; + drawSpriteQuad(id, rect, color); +} + +pub const SpriteSheetOptions = struct { + format: ImageData.Format = .rgba8 +}; + +pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id { + const self = &g_state; + const id = self.spritesheets.insertBounded() catch { + log.warn("Failed to create spritesheet, limit reached! limit: {}", .{self.spritesheets.slots.capacity}); + return self.nil_spritesheet; + }; + + const spritesheet = self.spritesheets.getAssumeExists(id); + spritesheet.* = .{ + .texture = initTexture(.{ .dynamic = true }), + .size = .init(0, 0), + .format = opts.format, + .needs_repack = false, + .needs_texture_rebuild = false, + .nodes = undefined, + .packer = null + }; + + return id; +} + +pub fn deinitSpritesheet(id: Spritesheet.Id) void { + const self = &g_state; + if (id == self.nil_spritesheet) { + return; + } + + if (self.spritesheets.get(id)) { + const spritesheet = self.spritesheets.getAssumeExists(id); + deinitTexture(spritesheet.texture); + self.spritesheets.removeAssumeExists(id); + } +} + +pub fn initFont() Font.Id { + const self = &g_state; + const id = self.fonts.insertBounded() catch { + log.warn("Failed to create font, limit reached! limit: {}", .{self.fonts.slots.capacity}); + return self.nil_font; + }; + + const font = self.fonts.getAssumeExists(id); + font.* = .{ + .spritesheet = self.default_font_spritesheet, + .stb = null, + .cache = .init(), + }; + + return id; +} + +pub fn deinitFont(id: Font.Id) void { + const self = &g_state; + if (id == self.nil_font) { + return; + } + + if (self.fonts.get(id)) |font| { + font.cache.deinit(); + self.fonts.removeAssumeExists(id); + } +} + +pub fn setFont(id: Font.Id, ttf_data: [:0]const u8) void { + const self = &g_state; + if (id == self.nil_font) { + return; + } + + const font = self.fonts.get(id) orelse { + log.warn("Attempt to set font that doesn't exist: {}", .{id}); + return; + }; + + const offset = STBTrueType.Font.getFontOffsetForIndex(ttf_data, 0) orelse { + log.err("Failed to get offset for font at index 0", .{}); + return; + }; + + const stb_font = STBTrueType.Font.init(ttf_data, offset) catch { + log.err("Failed to initialize font info", .{}); + return; + }; + + // TODO: deinit previous glyphs + + font.stb = stb_font; + + font.cache.deinit(); + font.cache = .init(); +} + +const DrawTextOptions = struct { + text: []const u8, + pos: Vec2, + height: f32, + color: Color = .white +}; + +pub fn drawText(id: Font.Id, opts: DrawTextOptions) void { + const self = &g_state; + if (id == self.nil_font) { + return; + } + + const font = self.fonts.get(id) orelse { + log.warn("Attempt to draw text with font that doesn't exist: {}", .{id}); + return; + }; + const stb_font = font.stb orelse { + log.warn("Attempt to draw text with font that isn't set: {}", .{id}); + return; + }; + + const fallback_glyph = stb_font.findGlyphIndex('?') orelse { + log.warn("Failed to find fallback glyph '?'", .{}); + return; + }; + + const scale = stb_font.scaleForPixelHeight(opts.height); + const scale_x = scale; + const scale_y = scale; + + const vmetrics = stb_font.getFontVMetrics(); + const baseline = @as(f32, @floatFromInt(vmetrics.ascent)) * scale; + + var current_point = opts.pos; + current_point.y += baseline; + + var prev_glyph: ?u32 = null; + var cursor: usize = 0; + while (cursor < opts.text.len) { + const codepoint_len = std.unicode.utf8ByteSequenceLength(opts.text[cursor]) catch |e| { + log.err("Failed to draw text, invalid codepoint at {} in '{s}': {}", .{cursor, opts.text, e}); + return; + }; + const codepoint_bytes = opts.text[cursor..][0..codepoint_len]; + const codepoint = std.unicode.utf8Decode(codepoint_bytes) catch |e| { + log.err("Failed to draw text, invalid codepoint at {} in '{s}': {}", .{cursor, opts.text, e}); + return; + }; + cursor += codepoint_len; + + const glyph_index = stb_font.findGlyphIndex(codepoint) orelse fallback_glyph; + const glyph_key = Font.Glyph.Key{ .index = glyph_index, .scale_x = scale_x, .scale_y = scale_y }; + + var glyph: *Font.Glyph = undefined; + if (font.cache.lookup(glyph_key)) |glyph_id| { + glyph = font.cache.get(glyph_id); + } else { + const glyph_id = font.cache.insert(glyph_key) orelse { + @panic("TODO: render glyph lru_tail"); + }; + glyph = font.cache.get(glyph_id); + glyph.box = stb_font.getGlyphBitmapBox(glyph_key.index, glyph_key.scale_x, glyph_key.scale_y); + + const width: u32 = @intCast(glyph.box.x1 - glyph.box.x0); + const height: u32 = @intCast(glyph.box.y1 - glyph.box.y0); + if (width > 0 and height > 0) { + assert(glyph.sprite == self.nil_sprite); + glyph.sprite = initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 }); + + assert(width < Font.Glyph.max_width); + assert(height < Font.Glyph.max_height); + + var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined; + const bitmap = ImageData{ + .pixels = .{ .r8 = (&bitmap_buffer).ptr }, + .width = width, + .height = height + }; + stb_font.makeGlyphBitmap( + bitmap.pixels.?.r8, + width, height, + width, + glyph_key.scale_x, glyph_key.scale_y, + glyph_key.index + ); + setSprite(glyph.sprite, .{ .image = bitmap }); + } else { + glyph.sprite = self.nil_sprite; + } + } + glyph.used_this_frame = true; + + if (prev_glyph != null) { + const kerning = stb_font.getGlyphKernAdvance(prev_glyph.?, glyph_index); + current_point.x += @as(f32, @floatFromInt(kerning)) * scale; + } + + if (glyph.sprite != self.nil_sprite) { + const glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0); + const glyph_pos = current_point.sub(.initFromInt(i32, glyph.box.x0, -glyph.box.y0)); + drawSprite(glyph.sprite, glyph_pos, glyph_size, opts.color); + } + + const hmetrics = stb_font.getGlyphHMetrics(glyph_index); + current_point.x += @as(f32, @floatFromInt(hmetrics.advance_width)) * scale; + prev_glyph = glyph_index; + } +} + +pub const ImageData = struct { + const Format = enum { + rgba8, + r8 + }; + + const Pixels = union(Format) { + rgba8: [*]u8, + r8: [*]u8, + }; + + pixels: ?Pixels, + width: u32, + height: u32, + + pub const none = ImageData{ + .pixels = null, + .width = 0, + .height = 0, + }; + + pub fn init(gpa: Allocator, width: u32, height: u32, format: ?Format) !ImageData { + const pixel_count = width * height; + var pixels: ?Pixels = null; + if (format != null) { + pixels = switch (format.?) { + .rgba8 => Pixels{ + .rgba8 = (try gpa.alloc(u8, pixel_count * 4)).ptr + }, + .r8 => Pixels{ + .r8 = (try gpa.alloc(u8, pixel_count)).ptr + } + }; + } + + return ImageData{ + .pixels = pixels, + .width = width, + .height = height + }; + } + + pub fn initRGBA8(gpa: Allocator, width: u32, height: u32) !ImageData { + return ImageData.init(gpa, width, height, .rgba8); + } + + pub fn initR8(gpa: Allocator, width: u32, height: u32) !ImageData { + return ImageData.init(gpa, width, height, .r8); + } + + pub fn clone(self: ImageData, gpa: Allocator) !ImageData { + const pixel_count = self.width * self.height; + + var pixels: ?Pixels = null; + if (self.pixels) |self_pixels| { + pixels = switch (self_pixels) { + .rgba8 => |rgba8| Pixels{ + .rgba8 = (try gpa.dupe(u8, rgba8[0..(pixel_count * 4)])).ptr + }, + .r8 => |r8| Pixels{ + .r8 = (try gpa.dupe(u8, r8[0..pixel_count])).ptr + } + }; + } + + return ImageData{ + .pixels = pixels, + .width = self.width, + .height = self.height + }; + } + + pub fn deinit(self: ImageData, gpa: Allocator) void { + const pixel_count = self.width * self.height; + if (self.pixels) |pixels| { + switch (pixels) { + .rgba8 => |rgba8| gpa.free(rgba8[0..(pixel_count*4)]), + .r8 => |r8| gpa.free(r8[0..pixel_count]) + } + } + } + + fn getPixel(self: ImageData, x: u32, y: u32) []u8 { + const pixel_index = y * self.width + x; + if (self.pixels) |pixels| { + return switch (pixels) { + .rgba8 => |rgba8| rgba8[(4*pixel_index)..][0..4], + .r8 => |r8| r8[pixel_index..][0..1] + }; + } else { + return &.{}; + } + } + + pub fn setRect(self: ImageData, x: u32, y: u32, width: u32, height: u32, pixel: []const u8) void { + if (self.pixels == null) { + return; + } + + for (0..height) |oy| { + const iy = y + oy; + if (iy >= self.height) { + continue; + } + + for (0..width) |ox| { + const ix = x + ox; + if (ix >= self.width) { + continue; + } + + const dest_pixel = self.getPixel(@intCast(ix), @intCast(iy)); + @memcpy(dest_pixel, pixel); + } + } + } + + pub fn setRectZero(self: ImageData, x: u32, y: u32, width: u32, height: u32) void { + if (self.pixels == null) { + return; + } + + const pixel = switch (self.pixels.?) { + .rgba8 => &[_]u8{ 0, 0, 0, 0 }, + .r8 => &[_]u8{ 0 } + }; + self.setRect(x, y, width, height, pixel); + } + + pub fn copyFrom( + self: ImageData, + source: ImageData, + cx: u32, + cy: u32, + source_cx: u32, + source_cy: u32, + source_width: u32, + source_height: u32, + ) void { + if (source.pixels == null or self.pixels == null) { + return; + } + + for (0..source_height) |oy| { + const source_y = source_cy+oy; + const dest_y = cy+oy; + if (dest_y >= self.height) { + continue; + } + if (source_y >= source.height) { + continue; + } + + for (0..source_width) |ox| { + const source_x = source_cx+ox; + const dest_x = cx+ox; + if (dest_x >= self.width) { + continue; + } + if (source_x >= source.width) { + continue; + } + + const source_pixel = source.getPixel(@intCast(source_x), @intCast(source_y)); + const dest_pixel = self.getPixel(@intCast(dest_x), @intCast(dest_y)); + + @memcpy(dest_pixel, source_pixel); + } + } + } +}; + +pub const Texture = struct { + dynamic: bool, + updated_at: ?u64, + image: sg.Image, + view: sg.View, + + const max_textures = 64; + const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_textures-1), u8, Texture); + pub const Id = SlotMap.Id; +}; + +pub const Sprite = struct { + spritesheet: Spritesheet.Id, + data: ?ImageData, + position: ?Vec2, + padding: u32, + + const max_sprites = 128; + const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_sprites-1), u8, Sprite); + pub const Id = SlotMap.Id; +}; + +pub const Spritesheet = struct { + texture: Texture.Id, + format: ImageData.Format, + size: Vec2, + + needs_repack: bool, + needs_texture_rebuild: bool, + + packer: ?STBRectPack, + nodes: [256]STBRectPack.Node, + + const max_spritesheets = 64; + const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_spritesheets-1), u8, Spritesheet); + pub const Id = SlotMap.Id; +}; + +pub const Font = struct { + spritesheet: Spritesheet.Id, + stb: ?STBTrueType.Font, + + cache: GlyphCache, + + const max_fonts = 16; + const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_fonts-1), u8, Font); + pub const Id = SlotMap.Id; + + const GlyphCache = struct { + glyphs: [GlyphSlot.limit]GlyphSlot, + glyphs_hashmap: [GlyphSlot.limit*2]GlyphSlot.Id, + lru_head: GlyphSlot.Id, + lru_tail: GlyphSlot.Id, + free_head: GlyphSlot.Id, + + fn init() GlyphCache { + var self: GlyphCache = .{ + .glyphs = undefined, + .glyphs_hashmap = undefined, + .lru_head = GlyphSlot.nil_id, + .lru_tail = GlyphSlot.nil_id, + .free_head = undefined + }; + @memset(&self.glyphs_hashmap, GlyphSlot.nil_id); + @memset(&self.glyphs, .{ + .glyph = undefined, + .lru_prev = GlyphSlot.nil_id, + .lru_next = GlyphSlot.nil_id, + .next_free = GlyphSlot.nil_id, + }); + + assert(self.glyphs_hashmap.len > self.glyphs.len); + + self.free_head = 1; + for (1.., self.glyphs[1..(self.glyphs.len-1)]) |i, *glyph| { + glyph.next_free = @intCast(i + 1); + } + + return self; + } + + fn deinit(self: *GlyphCache) void { + for (self.glyphs_hashmap) |glyph_id| { + if (glyph_id == GlyphSlot.nil_id) { + continue; + } + const slot = self.glyphs[glyph_id]; + slot.glyph.deinit(); + } + self.* = undefined; + } + + fn get(self: *GlyphCache, id: GlyphSlot.Id) *Glyph { + return &self.glyphs[id].glyph; + } + + fn lookup(self: *GlyphCache, key: Glyph.Key) ?GlyphSlot.Id { + const home_bucket = key.hash() % self.glyphs_hashmap.len; + for (0..self.glyphs_hashmap.len) |offset| { + const i = (home_bucket + offset) % self.glyphs_hashmap.len; + const id = self.glyphs_hashmap[i]; + if (id == GlyphSlot.nil_id) { + continue; + } + if (self.glyphs[id].glyph.key == key) { + self.removeFromLRU(id); + self.appendToLRUHead(id); + return id; + } + } + + return null; + } + + fn appendToLRUHead(self: *GlyphCache, id: GlyphSlot.Id) void { + if (self.lru_head != GlyphSlot.nil_id) { + self.glyphs[self.lru_head].lru_prev = id; + self.glyphs[id].lru_next = self.lru_head; + } else { + assert(self.lru_tail == GlyphSlot.nil_id); + self.lru_tail = id; + } + + self.glyphs[id].lru_prev = GlyphSlot.nil_id; + self.lru_head = id; + } + + fn removeFromLRU(self: *GlyphCache, id: GlyphSlot.Id) void { + assert(id != GlyphSlot.nil_id); + + const lru_prev = self.glyphs[id].lru_prev; + const lru_next = self.glyphs[id].lru_next; + + if (lru_prev != GlyphSlot.nil_id) { + self.glyphs[lru_prev].lru_next = lru_next; + } + + if (lru_next != GlyphSlot.nil_id) { + self.glyphs[lru_next].lru_prev = lru_prev; + } + + if (id == self.lru_head) { + self.lru_head = lru_next; + } + if (id == self.lru_tail) { + self.lru_tail = lru_prev; + } + } + + fn popFreeId(self: *GlyphCache) ?GlyphSlot.Id { + if (self.free_head != GlyphSlot.nil_id) { + const result = self.free_head; + self.free_head = self.glyphs[result].next_free; + return result; + } + return null; + } + + fn pushFreeId(self: *GlyphCache, id: GlyphSlot.Id) void { + assert(id != GlyphSlot.nil_id); + self.glyphs[id].next_free = self.free_head; + self.free_head = id; + } + + fn insert(self: *GlyphCache, key: Glyph.Key) ?GlyphSlot.Id { + const key_hash = key.hash(); + + for (0..self.glyphs_hashmap.len) |offset| { + const i = (key_hash + offset) % self.glyphs_hashmap.len; + const hashmap_id = self.glyphs_hashmap[i]; + if (hashmap_id == GlyphSlot.nil_id) { + const id = self.popFreeId() orelse return null; + self.glyphs_hashmap[i] = id; + + self.glyphs[id].glyph = .init(key); + self.glyphs[id].lru_next = GlyphSlot.nil_id; + self.glyphs[id].lru_prev = GlyphSlot.nil_id; + self.appendToLRUHead(id); + return id; + } else { + const glyph = &self.glyphs[hashmap_id].glyph; + if (glyph.key == key) { + // An entry for this key already exists + self.removeFromLRU(hashmap_id); + self.appendToLRUHead(hashmap_id); + return hashmap_id; + } + } + } + + // This can't be reached because the hasmap is guarenteed to have enough slots to store all of the glyph ids + unreachable; + } + + fn removeFromHashmap(self: *GlyphCache, id: GlyphSlot.Id) void { + var bucket = self.glyphs[id].glyph.key.hash() % self.glyphs_hashmap.len; + for (0..self.glyphs_hashmap.len) |_| { + assert(self.glyphs_hashmap[bucket] != GlyphSlot.nil_id); + if (self.glyphs_hashmap[bucket] == id) { + break; + } + + bucket = (bucket + 1) % self.glyphs_hashmap.len; + } + + assert(self.glyphs_hashmap[bucket] == id); + + var empty = bucket; + var current = (bucket + 1) % self.glyphs_hashmap.len; + + while (self.glyphs_hashmap[current] != GlyphSlot.nil_id) { + const current_glyph_id = self.glyphs_hashmap[current]; + const home = self.glyphs[current_glyph_id].glyph.key.hash() % self.glyphs_hashmap.len; + + if (self.hashmapDistance(home, current) > self.hashmapDistance(home, empty)) { + self.glyphs_hashmap[empty] = self.glyphs_hashmap[current]; + empty = current; + } + + current = (current + 1) % self.glyphs_hashmap.len; + } + + assert(self.glyphs_hashmap[empty] != GlyphSlot.nil_id); + self.glyphs_hashmap[empty] = GlyphSlot.nil_id; + } + + fn hashmapDistance(self: GlyphCache, from: usize, to: usize) usize { + if (to >= from) { + return to - from; + } else { + return self.glyphs_hashmap.len - from + to; + } + } + + fn remove(self: *GlyphCache, id: GlyphSlot.Id) void { + assert(id != GlyphSlot.nil_id); + self.pushFreeId(id); + self.removeFromHashmap(id); + self.removeFromLRU(id); + + const glyph = self.glyphs[id].glyph; + glyph.deinit(); + } + + fn getLRUItems(self: *GlyphCache, gpa: Allocator) ![]GlyphSlot.Id { + var lru: std.ArrayList(GlyphSlot.Id) = .empty; + defer lru.deinit(gpa); + + var lru_iter = self.lru_head; + while (lru_iter != GlyphSlot.nil_id) { + try lru.append(gpa, lru_iter); + lru_iter = self.glyphs[lru_iter].lru_next; + } + + return try lru.toOwnedSlice(gpa); + } + + test { + var cache = GlyphCache.init(); + const key = Glyph.Key{ .index = 10, .scale_x = 1, .scale_y = 1 }; + const id1 = cache.insert(key).?; + const id2 = cache.insert(key).?; + try std.testing.expectEqual(id1, id2); + } + + test { + var cache = GlyphCache.init(); + const key = Glyph.Key{ .index = 10, .scale_x = 1, .scale_y = 1 }; + const id1 = cache.insert(key).?; + cache.remove(id1); + const id2 = cache.insert(key).?; + try std.testing.expectEqual(id1, id2); + } + + test { + var cache = GlyphCache.init(); + const id1 = cache.insert(Glyph.Key{ .index = 10, .scale_x = 1, .scale_y = 1 }).?; + const id2 = cache.insert(Glyph.Key{ .index = 11, .scale_x = 1, .scale_y = 1 }).?; + try std.testing.expect(id1 != id2); + } + + test { + var cache = GlyphCache.init(); + for (0..(GlyphSlot.limit-1)) |i| { + const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = 1, .scale_y = 1 }); + try std.testing.expect(id != null); + } + const id = cache.insert(Glyph.Key{ .index = GlyphSlot.limit, .scale_x = 1, .scale_y = 1 }); + try std.testing.expect(id == null); + } + + test { + const gpa = std.testing.allocator; + var cache = GlyphCache.init(); + for (0..10) |_| { + var ids: std.ArrayList(GlyphSlot.Id) = .empty; + defer ids.deinit(gpa); + + for (0..(GlyphSlot.limit-1)) |i| { + const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = 1, .scale_y = 1 }); + try std.testing.expect(id != null); + try ids.append(gpa, id.?); + } + assert(cache.free_head == GlyphSlot.nil_id); + + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + + const expected_lru = try gpa.dupe(GlyphSlot.Id, ids.items); + defer gpa.free(expected_lru); + std.mem.reverse(GlyphSlot.Id, expected_lru); + + try std.testing.expectEqualSlices(GlyphSlot.Id, expected_lru, lru); + + for (ids.items) |id| { + cache.remove(id); + } + assert(cache.lru_head == GlyphSlot.nil_id); + assert(cache.lru_tail == GlyphSlot.nil_id); + } + } + + test { + var cache = GlyphCache.init(); + const id1 = cache.insert(.{ .index = 0, .scale_x = 1, .scale_y = 1 }).?; + const id2 = cache.insert(.{ .index = 1, .scale_x = 1, .scale_y = 1 }).?; + const id3 = cache.insert(.{ .index = 2, .scale_x = 1, .scale_y = 1 }).?; + + const gpa = std.testing.allocator; + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); + } + + test { + var cache = GlyphCache.init(); + const id1 = cache.insert(.{ .index = 0, .scale_x = 1, .scale_y = 1 }).?; + const id2 = cache.insert(.{ .index = 1, .scale_x = 1, .scale_y = 1 }).?; + const id3 = cache.insert(.{ .index = 2, .scale_x = 1, .scale_y = 1 }).?; + + const gpa = std.testing.allocator; + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); + } + + test { + var cache = GlyphCache.init(); + const id = cache.insert(.{ .index = 0, .scale_x = 1, .scale_y = 1 }).?; + try std.testing.expect(cache.lru_head != GlyphSlot.nil_id); + try std.testing.expect(cache.lru_tail != GlyphSlot.nil_id); + cache.remove(id); + try std.testing.expect(cache.lru_head == GlyphSlot.nil_id); + try std.testing.expect(cache.lru_tail == GlyphSlot.nil_id); + } + + test { + const gpa = std.testing.allocator; + var cache = GlyphCache.init(); + const key1 = Glyph.Key{ .index = 0, .scale_x = 1, .scale_y = 1 }; + const id1 = cache.insert(key1).?; + const key2 = Glyph.Key{ .index = 1, .scale_x = 1, .scale_y = 1 }; + const id2 = cache.insert(key2).?; + const key3 = Glyph.Key{ .index = 2, .scale_x = 1, .scale_y = 1 }; + const id3 = cache.insert(key3).?; + + _ = cache.lookup(key2); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id2, id3, id1 }, lru); + } + + _ = cache.lookup(key1); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id1, id2, id3 }, lru); + } + } + + test { + const gpa = std.testing.allocator; + var cache = GlyphCache.init(); + const id1 = cache.insert(.{ .index = 0, .scale_x = 1, .scale_y = 1 }).?; + const id2 = cache.insert(.{ .index = 1, .scale_x = 1, .scale_y = 1 }).?; + const id3 = cache.insert(.{ .index = 2, .scale_x = 1, .scale_y = 1 }).?; + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id2, id1 }, lru); + } + + _ = cache.remove(id2); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id3, id1 }, lru); + } + + _ = cache.remove(id3); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id1 }, lru); + } + + _ = cache.remove(id1); + + { + const lru = try cache.getLRUItems(gpa); + defer gpa.free(lru); + try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ }, lru); + } + } + }; + + const GlyphSlot = struct { + glyph: Glyph, + + lru_next: GlyphSlot.Id, + lru_prev: GlyphSlot.Id, + + next_free: GlyphSlot.Id, + + const limit = 256; + const Id = std.math.IntFittingRange(0, limit-1); + const nil_id: GlyphSlot.Id = 0; + }; + + const Glyph = struct { + key: Key, + + box: STBTrueType.Box, + sprite: Sprite.Id, + used_this_frame: bool, + + const max_width: u32 = 512; + const max_height: u32 = 512; + + const Key = packed struct { + index: u32, + scale_x: f32, + scale_y: f32, + + pub fn hash(self: Key) u64 { + var hasher = std.hash.Wyhash.init(0); + hasher.update(std.mem.asBytes(&self)); + return hasher.final(); + } + }; + + fn init(key: Key) Glyph { + return Glyph{ + .key = key, + .box = .{ + .x0 = 0, + .y0 = 0, + .x1 = 0, + .y1 = 0, + }, + .sprite = g_state.nil_sprite, + .used_this_frame = false + }; + } + + fn deinit(self: Glyph) void { + deinitSprite(self.sprite); + } + }; +}; + +pub const Vertex = extern struct { + position: Vec2, + texcoord: Vec2, + color: Vec4, +}; + +pub const Quad = extern struct { + vertices: [4]Vertex, + + pub fn init(vertices: [4]Vertex) Quad { + return Quad{ + .vertices = vertices + }; + } + + pub fn setColor(self: *Quad, color: Color) void { + for (&self.vertices) |*vertex| { + vertex.color = Vec4{ + .x = color.r, + .y = color.g, + .z = color.b, + .w = color.a, + }; + } + } + + pub fn setUVRect(self: *Quad, rect: Rect) void { + const pos = rect.pos; + const size = rect.size; + self.vertices[0].texcoord = pos; + self.vertices[1].texcoord = pos.add(.init(size.x, 0)); + self.vertices[2].texcoord = pos.add(.init(0, size.y)); + self.vertices[3].texcoord = pos.add(size); + } + + pub fn setRect(self: *Quad, rect: Rect) void { + const pos = rect.pos; + const size = rect.size; + self.vertices[0].position = pos; + self.vertices[1].position = pos.add(.init(size.x, 0)); + self.vertices[2].position = pos.add(.init(0, size.y)); + self.vertices[3].position = pos.add(size); + } +}; diff --git a/src/imgui.zig b/src/imgui.zig new file mode 100644 index 0000000..87fdb02 --- /dev/null +++ b/src/imgui.zig @@ -0,0 +1,339 @@ +const std = @import("std"); +const log = std.log.scoped(.imgui); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const builtin = @import("builtin"); + +const build_options = @import("build_options"); +const Color = @import("./color.zig"); +const Math = @import("math"); +const Vec2 = Math.Vec2; + +const sokol = @import("sokol"); +const sapp = sokol.app; +const simgui = sokol.imgui; + +const ig = @import("cimgui"); + +const ImGUI = @This(); + +const State = struct { + enabled: bool, + frame: ?Allocator +}; + +var g_state: State = undefined; + +pub fn init(logger: simgui.Logger) void { + if (!build_options.has_imgui) { + return; + } + + const self = &g_state; + + simgui.setup(.{ + .logger = logger + }); + + if (@hasDecl(ig, "ImGuiConfigFlags_DockingEnable")) { + ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_DockingEnable; + ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_ViewportsEnable; + } + + self.* = State{ + .enabled = true, + .frame = null + }; +} + +pub fn deinit() void { + if (!build_options.has_imgui) { + return; + } + + simgui.shutdown(); +} + +pub fn event(e: sapp.Event) bool { + if (!build_options.has_imgui) { + return false; + } + + const self = &g_state; + if (!self.enabled) { + return false; + } + + return simgui.handleEvent(e); +} + +pub fn beginFrame(enabled: bool, frame: Allocator) void { + if (!build_options.has_imgui) { + return; + } + + const self = &g_state; + + self.frame = frame; + self.enabled = enabled; + + if (!self.enabled) { + return; + } + + simgui.newFrame(.{ + .width = sapp.width(), + .height = sapp.height(), + .delta_time = sapp.frameDuration(), + .dpi_scale = sapp.dpiScale(), + }); + + const viewport = ig.igGetMainViewport(); + ig.igSetNextWindowPos(viewport[0].WorkPos, ig.ImGuiCond_Always); + ig.igSetNextWindowSize(viewport[0].WorkSize, ig.ImGuiCond_Always); + ig.igSetNextWindowViewport(viewport[0].ID); + + const window_flags = + ig.ImGuiWindowFlags_NoDocking | + ig.ImGuiWindowFlags_NoTitleBar | + ig.ImGuiWindowFlags_NoCollapse | + ig.ImGuiWindowFlags_NoResize | + ig.ImGuiWindowFlags_NoMove | + ig.ImGuiWindowFlags_NoBackground | + ig.ImGuiWindowFlags_NoBringToFrontOnFocus | + ig.ImGuiWindowFlags_NoNavFocus; + + ig.igPushStyleVar(ig.ImGuiStyleVar_WindowRounding, 0.0); + ig.igPushStyleVar(ig.ImGuiStyleVar_WindowBorderSize, 0.0); + ig.igPushStyleVarImVec2(ig.ImGuiStyleVar_WindowPadding, .{ .x = 0, .y = 0 }); + _ = ig.igBegin("DockSpace", null, window_flags); + ig.igPopStyleVarEx(3); + + const dockspace_id = ig.igGetID("MyDockSpace"); + _ = ig.igDockSpaceEx(dockspace_id, ig.ImVec2{ .x = 0, .y = 0 }, ig.ImGuiDockNodeFlags_PassthruCentralNode, null); +} + +pub fn endFrame() void { + if (!build_options.has_imgui) { + return; + } + const self = &g_state; + if (!self.enabled) { + return; + } + + // TODO: + // if (ig.igBegin("Game", null, ig.ImGuiWindowFlags_None)) { + // // std.debug.print("{}\n", .{ig.igGetContentRegionAvail()}); + // } + // ig.igEnd(); + + ig.igEnd(); + + // if (ig.igBeginMainMenuBar()) { + // defer ig.igEndMainMenuBar(); + // + // if (ig.igBeginMenu("foo")) { + // defer ig.igEndMenu(); + // + // if (ig.igMenuItem("bar")) { + // } + // } + // } + + simgui.render(); +} + +pub const WindowOptions = struct { + name: []const u8, + pos: ?Vec2 = null, + size: ?Vec2 = null, + collapsed: ?bool = null, + open: ?*bool = null, +}; + +pub fn beginWindow(opts: WindowOptions) bool { + if (!build_options.has_imgui) { + return false; + } + const self = &g_state; + if (!self.enabled) { + return false; + } + + if (opts.open != null and opts.open.?.* == false) { + return false; + } + + if (opts.pos) |pos| { + ig.igSetNextWindowPos(toImVec2(pos), ig.ImGuiCond_Once); + } + if (opts.size) |size| { + ig.igSetNextWindowSize(toImVec2(size), ig.ImGuiCond_Once); + } + if (opts.collapsed) |collapsed| { + ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once); + } + + ig.igSetNextWindowBgAlpha(1); + + const frame = g_state.frame.?; + const namez = frame.dupeSentinel(u8, opts.name, 0) catch blk: { + log.err("dupeSentinel() failed",.{}); + break :blk ""; + }; + + var open = ig.igBegin(namez, opts.open, ig.ImGuiWindowFlags_None); + if (opts.open) |opts_open| { + if (opts_open.* == false) { + open = false; + } + } + if (!open) { + endWindow(); + } + + return open; +} + +pub fn endWindow() void { + if (!build_options.has_imgui) { + return; + } + const self = &g_state; + if (!self.enabled) { + return; + } + + ig.igEnd(); +} + +fn formatString(comptime fmt: []const u8, args: anytype) [:0]const u8 { + const frame = g_state.frame.?; + return std.fmt.allocPrintSentinel(frame, fmt, args, 0) catch blk: { + log.err("allocPrintSentinel() failed", .{}); + break :blk ""; + }; +} + +pub fn text(comptime fmt: []const u8, args: anytype) void { + if (!build_options.has_imgui) { + return; + } + const self = &g_state; + if (!self.enabled) { + return; + } + + ig.igTextUnformatted(formatString(fmt, args)); +} + +pub fn button(label: []const u8) bool { + if (!build_options.has_imgui) { + return false; + } + const self = &g_state; + if (!self.enabled) { + return false; + } + + return ig.igButton(formatString("{s}", .{label})); +} + +pub const SliderOptions = struct { + label: []const u8, + value: *f32, + min: f32, + max: f32, +}; + +pub fn slider(opts: SliderOptions) bool { + if (!build_options.has_imgui) { + return false; + } + const self = &g_state; + if (!self.enabled) { + return false; + } + + return ig.igSliderFloat( + formatString("{s}", .{opts.label}), + opts.value, + opts.min, + opts.max + ); +} + +pub fn checkbox(label: []const u8, value: *bool) bool { + if (!build_options.has_imgui) { + return false; + } + const self = &g_state; + if (!self.enabled) { + return false; + } + + return ig.igCheckbox( + formatString("{s}", .{label}), + value, + ); +} + +pub fn separator() void { + if (!build_options.has_imgui) { + return; + } + const self = &g_state; + if (!self.enabled) { + return; + } + + ig.igSeparator(); +} + +const ColorEditOptions = struct { + label: []const u8, + value: *Color, + has_alpha: bool = false +}; + +pub fn colorEdit(opts: ColorEditOptions) bool { + if (!build_options.has_imgui) { + return false; + } + const self = &g_state; + if (!self.enabled) { + return false; + } + + const label = formatString("{s}", .{opts.label}); + var color: [4]f32 = .{ + opts.value.r, + opts.value.g, + opts.value.b, + opts.value.a, + }; + + var changed: bool = undefined; + if (opts.has_alpha) { + changed = ig.igColorEdit4(label, &color, ig.ImGuiColorEditFlags_None); + } else { + changed = ig.igColorEdit3(label, &color, ig.ImGuiColorEditFlags_None); + } + + if (changed) { + opts.value.r = color[0]; + opts.value.g = color[1]; + opts.value.b = color[2]; + opts.value.a = color[3]; + } + + return changed; +} + +fn toImVec2(vec2: Vec2) ig.ImVec2 { + return ig.ImVec2{ + .x = vec2.x, + .y = vec2.y, + }; +} diff --git a/src/inotify.zig b/src/inotify.zig new file mode 100644 index 0000000..cbc4c90 --- /dev/null +++ b/src/inotify.zig @@ -0,0 +1,186 @@ +const std = @import("std"); +const Io = std.Io; +const assert = std.debug.assert; +const linux = std.os.linux; + +const INotify = @This(); + +fd: i32, + +buf: []u8, +buf_offset: usize, +buf_used: usize, + +pub const Mask = packed struct(u32) { + access: bool = false, + modify: bool = false, + attrib: bool = false, + close_write: bool = false, + close_nowrite: bool = false, + open: bool = false, + moved_from: bool = false, + moved_to: bool = false, + create: bool = false, + delete: bool = false, + delete_self: bool = false, + move_self: bool = false, + + _unused1: u1 = 0, + + unmount: bool = false, + q_overflow: bool = false, + ignored: bool = false, + + _unused2: u8 = 0, + + onlydir: bool = false, + dont_follow: bool = false, + excl_unlink: bool = false, + + _unused3: u1 = 0, + + mask_create: bool = false, + mask_add: bool = false, + isdir: bool = false, + oneshot: bool = false, + + pub fn format(self: Mask, writer: *Io.Writer) Io.Writer.Error!void { + try writer.writeAll(".{"); + var first = true; + inline for (@typeInfo(Mask).@"struct".fields) |field| { + if (@typeInfo(field.type) == .bool and @field(self, field.name)) { + if (!first) { + try writer.writeAll(","); + } + try writer.print(" .{s} = true", .{field.name}); + first = false; + } + } + try writer.writeAll(" }"); + } + + fn testMask(expected: u32, actual: Mask) !void { + try std.testing.expectEqual(expected, @as(u32, @bitCast(actual))); + } + + test { + try testMask(linux.IN.ACCESS, Mask{ .access = true }); + try testMask(linux.IN.MODIFY, Mask{ .modify = true }); + try testMask(linux.IN.ATTRIB, Mask{ .attrib = true }); + try testMask(linux.IN.CLOSE_WRITE, Mask{ .close_write = true }); + try testMask(linux.IN.CLOSE_NOWRITE, Mask{ .close_nowrite = true }); + try testMask(linux.IN.OPEN, Mask{ .open = true }); + try testMask(linux.IN.MOVED_FROM, Mask{ .moved_from = true }); + try testMask(linux.IN.MOVED_TO, Mask{ .moved_to = true }); + try testMask(linux.IN.CREATE, Mask{ .create = true }); + try testMask(linux.IN.DELETE, Mask{ .delete = true }); + try testMask(linux.IN.DELETE_SELF, Mask{ .delete_self = true }); + try testMask(linux.IN.MOVE_SELF, Mask{ .move_self = true }); + try testMask(linux.IN.UNMOUNT, Mask{ .unmount = true }); + try testMask(linux.IN.Q_OVERFLOW, Mask{ .q_overflow = true }); + try testMask(linux.IN.IGNORED, Mask{ .ignored = true }); + try testMask(linux.IN.ONLYDIR, Mask{ .onlydir = true }); + try testMask(linux.IN.DONT_FOLLOW, Mask{ .dont_follow = true }); + try testMask(linux.IN.EXCL_UNLINK, Mask{ .excl_unlink = true }); + try testMask(linux.IN.MASK_CREATE, Mask{ .mask_create = true }); + try testMask(linux.IN.MASK_ADD, Mask{ .mask_add = true }); + try testMask(linux.IN.ISDIR, Mask{ .isdir = true }); + try testMask(linux.IN.ONESHOT, Mask{ .oneshot = true }); + } +}; + +pub const Event = struct { + wd: u32, + mask: Mask, + cookie: u32, + name: ?[*:0]const u8, +}; + +pub const max_event_size = @sizeOf(linux.inotify_event) + linux.NAME_MAX + 1; + +pub fn init(buf: []u8) !INotify { + const fd = linux.inotify_init1(linux.IN.NONBLOCK); + if (linux.errno(fd) != .SUCCESS) { + return error.InotifyInit; + } + return INotify{ + .fd = @intCast(fd), + .buf = buf, + .buf_offset = 0, + .buf_used = 0, + }; +} + +pub fn add(self: INotify, path: [*:0]const u8, mask: Mask) !u32 { + const wd = linux.inotify_add_watch( + self.fd, + path, + @bitCast(mask) + ); + + switch (linux.errno(wd)) { + .SUCCESS => return @intCast(wd), + .ACCES => return error.AccessDenied, + .BADF => unreachable, // The given file descriptor is not valid. + .EXIST => return error.MarkAlreadyExists, + .FAULT => unreachable, // path points outside of the process's accessible address space. + .INVAL => unreachable, // The given event mask contains no valid events; + .NAMETOOLONG => return error.NameTooLong, + .NOENT => return error.FileNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.UserMarkQuotaExceeded, + .NOTDIR => return error.NotDir, + else => |err| return std.posix.unexpectedErrno(err), + } +} + +pub fn remove(self: INotify, wd: u32) !void { + const rc = linux.inotify_rm_watch(self.fd, @bitCast(wd)); + if (linux.errno(rc) != .SUCCESS) { + return error.InotifyRemoveWatch; + } +} + +pub fn next(self: *INotify) !?Event { + if (self.buf_used <= self.buf.len) { + const n = std.posix.read(self.fd, self.buf[self.buf_used..]) catch |err| switch(err) { + error.WouldBlock => 0, + else => return err + }; + if (n > 0) { + self.buf_used += n; + } + } + + if (self.buf_offset < self.buf_used) { + const event = std.mem.bytesAsValue( + linux.inotify_event, + self.buf[self.buf_offset..][0..@sizeOf(linux.inotify_event)] + ); + self.buf_offset += @sizeOf(linux.inotify_event) + event.len; + assert(self.buf_offset <= self.buf_used); + + if (self.buf_used == self.buf_offset) { + self.buf_offset = 0; + self.buf_used = 0; + } + + var name: ?[*:0]const u8 = null; + if (event.len > 0) { + name = std.mem.span(@as([*:0]const u8, @ptrCast(event)) + @sizeOf(linux.inotify_event)); + } + + return Event{ + .wd = @bitCast(event.wd), + .mask = @bitCast(event.mask), + .cookie = event.cookie, + .name = name + }; + } + + return null; +} + +pub fn deinit(self: INotify) void { + _ = linux.close(self.fd); +} diff --git a/src/input.zig b/src/input.zig new file mode 100644 index 0000000..dcd58c4 --- /dev/null +++ b/src/input.zig @@ -0,0 +1,238 @@ +const std = @import("std"); +const assert = std.debug.assert; + +const sokol = @import("sokol"); + +const Math = @import("math"); +const Vec2 = Math.Vec2; + +const Input = @This(); + +key_code_mapping: std.EnumMap(KeyCode, u21), + +window_size: Vec2, +focused: bool, +keyboard: KeyStateType(KeyCode), +mouse_buttons: KeyStateType(MouseButton), +mouse_position: ?Vec2, +mouse_scroll: Vec2, + +pub fn init(window_size: Vec2) Input { + return Input{ + .key_code_mapping = .{}, + .focused = false, + .keyboard = .empty, + .mouse_buttons = .empty, + .mouse_position = null, + .mouse_scroll = .init(0, 0), + .window_size = window_size + }; +} + +pub fn isKeyPressed(self: Input, key: KeyCode) bool { + return self.keyboard.pressed.contains(key); +} + +pub fn isKeyReleased(self: Input, key: KeyCode) bool { + return self.keyboard.released.contains(key); +} + +pub fn isKeyDown(self: Input, key: KeyCode) bool { + return self.keyboard.down.contains(key); +} + +pub fn isMousePressed(self: Input, button: MouseButton) bool { + return self.mouse_buttons.pressed.contains(button); +} + +pub fn iMouseReleased(self: Input, button: MouseButton) bool { + return self.mouse_buttons.released.contains(button); +} + +pub fn isMouseDown(self: Input, button: MouseButton) bool { + return self.mouse_buttons.down.contains(button); +} + +pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) { + SPACE = 32, + APOSTROPHE = 39, + COMMA = 44, + MINUS = 45, + PERIOD = 46, + SLASH = 47, + _0 = 48, + _1 = 49, + _2 = 50, + _3 = 51, + _4 = 52, + _5 = 53, + _6 = 54, + _7 = 55, + _8 = 56, + _9 = 57, + SEMICOLON = 59, + EQUAL = 61, + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + LEFT_BRACKET = 91, + BACKSLASH = 92, + RIGHT_BRACKET = 93, + GRAVE_ACCENT = 96, + WORLD_1 = 161, + WORLD_2 = 162, + ESCAPE = 256, + ENTER = 257, + TAB = 258, + BACKSPACE = 259, + INSERT = 260, + DELETE = 261, + RIGHT = 262, + LEFT = 263, + DOWN = 264, + UP = 265, + PAGE_UP = 266, + PAGE_DOWN = 267, + HOME = 268, + END = 269, + CAPS_LOCK = 280, + SCROLL_LOCK = 281, + NUM_LOCK = 282, + PRINT_SCREEN = 283, + PAUSE = 284, + F1 = 290, + F2 = 291, + F3 = 292, + F4 = 293, + F5 = 294, + F6 = 295, + F7 = 296, + F8 = 297, + F9 = 298, + F10 = 299, + F11 = 300, + F12 = 301, + F13 = 302, + F14 = 303, + F15 = 304, + F16 = 305, + F17 = 306, + F18 = 307, + F19 = 308, + F20 = 309, + F21 = 310, + F22 = 311, + F23 = 312, + F24 = 313, + F25 = 314, + KP_0 = 320, + KP_1 = 321, + KP_2 = 322, + KP_3 = 323, + KP_4 = 324, + KP_5 = 325, + KP_6 = 326, + KP_7 = 327, + KP_8 = 328, + KP_9 = 329, + KP_DECIMAL = 330, + KP_DIVIDE = 331, + KP_MULTIPLY = 332, + KP_SUBTRACT = 333, + KP_ADD = 334, + KP_ENTER = 335, + KP_EQUAL = 336, + LEFT_SHIFT = 340, + LEFT_CONTROL = 341, + LEFT_ALT = 342, + LEFT_SUPER = 343, + RIGHT_SHIFT = 344, + RIGHT_CONTROL = 345, + RIGHT_ALT = 346, + RIGHT_SUPER = 347, + MENU = 348, +}; + +pub const MouseButton = enum { + left, + right, + middle, + + pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?MouseButton { + return switch(mouse_button) { + .LEFT => .left, + .RIGHT => .right, + .MIDDLE => .middle, + else => null + }; + } +}; + +pub const Event = union(enum) { + mouse_pressed: MouseButton, + mouse_released: MouseButton, + mouse_move: Vec2, + mouse_enter: Vec2, + mouse_leave, + mouse_scroll: Vec2, + key_pressed: KeyCode, + key_released: KeyCode, + char: u21, + window_resize: Vec2, + focused, + unfocused, +}; + +fn KeyStateType(T: type) type { + return struct { + pressed: std.EnumSet(T), + released: std.EnumSet(T), + down: std.EnumSet(T), + + pub const empty = @This(){ + .pressed = .empty, + .released = .empty, + .down = .empty, + }; + + pub fn press(self: *@This(), key: T) void { + self.pressed.insert(key); + self.down.insert(key); + } + + pub fn release(self: *@This(), key: T) void { + self.released.insert(key); + self.down.remove(key); + } + + pub fn releaseAll(self: *@This()) void { + var iter = self.down.iterator(); + while (iter.next()) |key| { + self.release(key); + } + } + }; +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..c1fb15c --- /dev/null +++ b/src/main.zig @@ -0,0 +1,25 @@ +const std = @import("std"); + +const Platform = @import("platform.zig"); +const App = @import("app.zig"); + +pub const std_options: std.Options = .{ + .log_scope_levels = &.{ + .{ .scope = .graphics, .level = .info } + } +}; + +pub fn main(init: std.process.Init) void { + Platform.run(App, .{ + .init = init, + + .window_title = "Gaem", + .width = 800, + .height = 600, + }); +} + +test { + std.testing.refAllDecls(@This()); + _ = @import("./slot_map.zig"); +} diff --git a/src/math.zig b/src/math.zig new file mode 100644 index 0000000..abe04e5 --- /dev/null +++ b/src/math.zig @@ -0,0 +1,447 @@ +const std = @import("std"); +const assert = std.debug.assert; + +pub const bytes_per_kib = 1024; +pub const bytes_per_mib = bytes_per_kib * 1024; +pub const bytes_per_gib = bytes_per_mib * 1024; + +pub const bytes_per_kb = 1000; +pub const bytes_per_mb = bytes_per_kb * 1000; +pub const bytes_per_gb = bytes_per_mb * 1000; + +pub const Range = struct { + from: f32, + to: f32, + + pub const zero = init(0, 0); + + pub fn init(from: f32, to: f32) Range { + return Range{ + .from = from, + .to = to + }; + } + + pub fn getSize(self: Range) f32 { + return @abs(self.from - self.to); + } + + pub fn random(self: Range, rng: std.Random) f32 { + return self.from + rng.float(f32) * (self.to - self.from); + } +}; + +pub const Vec2 = extern struct { + x: f32, + y: f32, + + pub const zero = init(0, 0); + + pub fn init(x: f32, y: f32) Vec2 { + return Vec2{ + .x = x, + .y = y, + }; + } + + pub fn initFromInt(T: type, x: T, y: T) Vec2 { + return .init(@floatFromInt(x), @floatFromInt(y)); + } + + pub fn initAngle(angle: f32) Vec2 { + return Vec2{ + .x = @cos(angle), + .y = @sin(angle), + }; + } + + pub fn rotateLeft90(self: Vec2) Vec2 { + return Vec2.init(self.y, -self.x); + } + + pub fn rotateRight90(self: Vec2) Vec2 { + return Vec2.init(-self.y, self.x); + } + + pub fn rotate(self: Vec2, angle: f32) Vec2 { + return init( + @cos(angle) * self.x - @sin(angle) * self.y, + @sin(angle) * self.x + @cos(angle) * self.y, + ); + } + + pub fn rotateAround(self: Vec2, angle: f32, origin: Vec2) Vec2 { + return self.sub(origin).rotate(angle).add(origin); + } + + pub fn getAngle(self: Vec2) f32 { + return std.math.atan2(self.y, self.x); + } + + pub fn flip(self: Vec2) Vec2 { + return Vec2.init(-self.x, -self.y); + } + + pub fn add(self: Vec2, other: Vec2) Vec2 { + return Vec2.init( + self.x + other.x, + self.y + other.y, + ); + } + + pub fn sub(self: Vec2, other: Vec2) Vec2 { + return Vec2.init( + self.x - other.x, + self.y - other.y, + ); + } + + pub fn multiplyScalar(self: Vec2, value: f32) Vec2 { + return Vec2.init( + self.x * value, + self.y * value, + ); + } + + pub fn multiply(self: Vec2, other: Vec2) Vec2 { + return Vec2.init( + self.x * other.x, + self.y * other.y, + ); + } + + pub fn divide(self: Vec2, other: Vec2) Vec2 { + return Vec2.init( + self.x / other.x, + self.y / other.y, + ); + } + + pub fn divideScalar(self: Vec2, value: f32) Vec2 { + return Vec2.init( + self.x / value, + self.y / value, + ); + } + + pub fn lengthSqr(self: Vec2) f32 { + return self.x*self.x + self.y*self.y; + } + + pub fn length(self: Vec2) f32 { + return @sqrt(self.lengthSqr()); + } + + pub fn distance(self: Vec2, other: Vec2) f32 { + return self.sub(other).length(); + } + + pub fn distanceSqr(self: Vec2, other: Vec2) f32 { + return self.sub(other).lengthSqr(); + } + + pub fn limitLength(self: Vec2, max_length: f32) Vec2 { + const self_length = self.length(); + if (self_length > max_length) { + if (self_length == 0) { + return Vec2.init(0, 0); + } + return self.divideScalar(self_length / max_length); + } else { + return self; + } + } + + pub fn normalized(self: Vec2) Vec2 { + const self_length = self.length(); + if (self_length == 0) { + return Vec2.init(0, 0); + } + return self.divideScalar(self_length); + } + + pub fn initScalar(value: f32) Vec2 { + return Vec2.init(value, value); + } + + pub fn eql(self: Vec2, other: Vec2) bool { + return self.x == other.x and self.y == other.y; + } + + pub fn format(self: Vec2, writer: *std.io.Writer) std.io.Writer.Error!void { + try writer.print("Vec2{{ {d}, {d} }}", .{ self.x, self.y }); + } +}; + +pub const Vec3 = extern struct { + x: f32, y: f32, z: f32, + + pub const zero = init(0, 0, 0); + + pub fn init(x: f32, y: f32, z: f32) Vec3 { + return Vec3{ + .x = x, + .y = y, + .z = z, + }; + } + + pub fn initScalar(value: f32) Vec3 { + return Vec3.init(value, value, value); + } + + pub fn asArray(self: *Vec3) []f32 { + const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(self)))); + return ptr[0..3]; + } + + pub fn lerp(a: Vec3, b: Vec3, t: f32) Vec3 { + return Vec3.init( + std.math.lerp(a.x, b.x, t), + std.math.lerp(a.y, b.y, t), + std.math.lerp(a.z, b.z, t), + ); + } + + pub fn clamp(self: Vec3, min_value: f32, max_value: f32) Vec3 { + return Vec3.init( + std.math.clamp(self.x, min_value, max_value), + std.math.clamp(self.y, min_value, max_value), + std.math.clamp(self.z, min_value, max_value), + ); + } +}; + +pub const Vec4 = extern struct { + x: f32, y: f32, z: f32, w: f32, + + pub const zero = init(0, 0, 0, 0); + + pub fn init(x: f32, y: f32, z: f32, w: f32) Vec4 { + return Vec4{ + .x = x, + .y = y, + .z = z, + .w = w + }; + } + + pub fn initVec3XYZ(vec3: Vec3, w: f32) Vec4 { + return init(vec3.x, vec3.y, vec3.z, w); + } + + pub fn initScalar(value: f32) Vec4 { + return Vec4.init(value, value, value, value); + } + + pub fn multiplyMat4(left: Vec4, right: Mat4) Vec4 { + var result: Vec4 = undefined; + + // TODO: SIMD + + result.x = left.x * right.columns[0][0]; + result.y = left.x * right.columns[0][1]; + result.z = left.x * right.columns[0][2]; + result.w = left.x * right.columns[0][3]; + + result.x += left.y * right.columns[1][0]; + result.y += left.y * right.columns[1][1]; + result.z += left.y * right.columns[1][2]; + result.w += left.y * right.columns[1][3]; + + result.x += left.z * right.columns[2][0]; + result.y += left.z * right.columns[2][1]; + result.z += left.z * right.columns[2][2]; + result.w += left.z * right.columns[2][3]; + + result.x += left.w * right.columns[3][0]; + result.y += left.w * right.columns[3][1]; + result.z += left.w * right.columns[3][2]; + result.w += left.w * right.columns[3][3]; + + return result; + } + + pub fn multiply(left: Vec4, right: Vec4) Vec4 { + return init( + left.x * right.x, + left.y * right.y, + left.z * right.z, + left.w * right.w + ); + } + + pub fn asArray(self: *Vec4) []f32 { + const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(self)))); + return ptr[0..4]; + } + + pub fn initArray(array: []const f32) Vec4 { + return Vec4.init(array[0], array[1], array[2], array[3]); + } + + pub fn lerp(a: Vec4, b: Vec4, t: f32) Vec4 { + return Vec4.init( + std.math.lerp(a.x, b.x, t), + std.math.lerp(a.y, b.y, t), + std.math.lerp(a.z, b.z, t), + std.math.lerp(a.w, b.w, t), + ); + } + + pub fn clamp(self: Vec4, min_value: f32, max_value: f32) Vec4 { + return Vec4.init( + std.math.clamp(self.x, min_value, max_value), + std.math.clamp(self.y, min_value, max_value), + std.math.clamp(self.z, min_value, max_value), + std.math.clamp(self.w, min_value, max_value), + ); + } + + pub fn toVec3XYZ(self: Vec4) Vec3 { + return Vec3.init(self.x, self.y, self.z); + } +}; + +pub const Mat4 = extern struct { + columns: [4][4]f32, + + pub fn initZero() Mat4 { + var self: Mat4 = undefined; + @memset(self.asArray(), 0); + return self; + } + + pub fn initIdentity() Mat4 { + return Mat4.initDiagonal(1); + } + + pub fn initDiagonal(value: f32) Mat4 { + var self = Mat4.initZero(); + self.columns[0][0] = value; + self.columns[1][1] = value; + self.columns[2][2] = value; + self.columns[3][3] = value; + return self; + } + + pub fn multiply(left: Mat4, right: Mat4) Mat4 { + var self: Mat4 = undefined; + + inline for (.{ 0, 1, 2, 3 }) |i| { + var column = Vec4.initArray(&right.columns[i]).multiplyMat4(left); + @memcpy(&self.columns[i], column.asArray()); + } + + return self; + } + + pub fn initScale(scale: Vec3) Mat4 { + var self = Mat4.initIdentity(); + self.columns[0][0] = scale.x; + self.columns[1][1] = scale.y; + self.columns[2][2] = scale.z; + return self; + } + + pub fn initTranslate(offset: Vec3) Mat4 { + var self = Mat4.initIdentity(); + self.columns[3][0] = offset.x; + self.columns[3][1] = offset.y; + self.columns[3][2] = offset.z; + return self; + } + + pub fn asArray(self: *Mat4) []f32 { + const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(&self.columns)))); + return ptr[0..16]; + } +}; + +pub const Rect = struct { + pos: Vec2, + size: Vec2, + + pub const zero = Rect{ + .pos = Vec2.zero, + .size = Vec2.zero + }; + + pub const unit = Rect{ + .pos = Vec2.zero, + .size = Vec2.init(1, 1) + }; + + pub fn init(x: f32, y: f32, width: f32, height: f32) Rect { + return Rect{ + .pos = Vec2.init(x, y), + .size = Vec2.init(width, height) + }; + } + + pub fn clip(self: Rect, other: Rect) Rect { + const left_edge = @max(self.left(), other.left()); + const right_edge = @min(self.right(), other.right()); + const top_edge = @max(self.top(), other.top()); + const bottom_edge = @min(self.bottom(), other.bottom()); + return Rect.init( + left_edge, + top_edge, + right_edge - left_edge, + bottom_edge - top_edge + ); + } + + pub fn left(self: Rect) f32 { + return self.pos.x; + } + + pub fn right(self: Rect) f32 { + return self.pos.x + self.size.x; + } + + pub fn top(self: Rect) f32 { + return self.pos.y; + } + + pub fn bottom(self: Rect) f32 { + return self.pos.y + self.size.y; + } + + pub fn center(self: Rect) Vec2 { + return self.pos.add(self.size.multiplyScalar(0.5)); + } + + pub fn multiply(self: Rect, xy: Vec2) Rect { + return Rect{ + .pos = self.pos.multiply(xy), + .size = self.size.multiply(xy), + }; + } + + pub fn divide(self: Rect, xy: Vec2) Rect { + return Rect{ + .pos = self.pos.divide(xy), + .size = self.size.divide(xy), + }; + } + + pub fn isInside(self: Rect, pos: Vec2) bool { + const x_overlap = self.pos.x <= pos.x and pos.x < self.pos.x + self.size.x; + const y_overlap = self.pos.y <= pos.y and pos.y < self.pos.y + self.size.y; + return x_overlap and y_overlap; + } +}; + +pub const Line = struct { + p0: Vec2, + p1: Vec2 +}; + +pub fn isInsideRect(rect_pos: Vec2, rect_size: Vec2, pos: Vec2) bool { + const rect = Rect{ + .pos = rect_pos, + .size = rect_size + }; + return rect.isInside(pos); +} diff --git a/src/platform.zig b/src/platform.zig new file mode 100644 index 0000000..5d6b26f --- /dev/null +++ b/src/platform.zig @@ -0,0 +1,674 @@ +const builtin = @import("builtin"); +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const log = std.log.scoped(.platform); +const assert = std.debug.assert; +const build_options = @import("build_options"); + +const sokol = @import("sokol"); +const sapp = sokol.app; +const sg = sokol.gfx; +const sglue = sokol.glue; +const sgl = sokol.gl; + +pub const Math = @import("math"); +pub const Vec2 = Math.Vec2; +pub const Vec4 = Math.Vec4; +pub const Mat4 = Math.Mat4; +pub const Rect = Math.Rect; + +const tracy = @import("tracy"); + +pub const Color = @import("./color.zig"); +const shd = @import("shader"); + +pub const ImGUI = @import("./imgui.zig"); +pub const Gfx = @import("./graphics.zig"); +pub const Input = @import("./input.zig"); + +const EmbeddedAssets = @import("embedded_assets"); + +fn PlatformType(App: type) type { + return struct { + const Self = @This(); + + gpa: Allocator, + arena: *std.heap.ArenaAllocator, + io: Io, + cli_args: []const [:0]const u8, + + app: App, + + started_at: Io.Timestamp, + last_frame_at: Io.Timestamp, + + assets: Assets, + input: Input, + events_buffer: [256]Input.Event, + events_overflow: bool, + events: std.ArrayList(Input.Event), + last_key_press_is_repeat: bool, + last_key_pressed: ?Input.KeyCode, + + frame_arena: std.heap.ArenaAllocator, + + show_imgui: bool, + + fn init(self: *Self) !void { + const window_size = Vec2.init(sapp.widthf(), sapp.heightf()); + + self.started_at = Io.Timestamp.now(self.io, .awake); + self.last_frame_at = self.started_at; + self.input = Input.init(window_size); + + try Gfx.init(self.gpa, .{ + .func = sokolLogCallback + }); + ImGUI.init(.{ + .func = sokolLogCallback + }); + + if (@hasDecl(App, "init")) { + const plt = Init{ + .gpa = self.gpa, + .arena = self.arena.allocator(), + .io = self.io, + .assets = &self.assets + }; + if (try self.app.init(plt)) |exit_code| { + std.process.exit(exit_code); + } + } + } + + fn sokolInit(user_data: ?*anyopaque) callconv(.c) void { + const self: *Self = @ptrCast(@alignCast(user_data)); + + self.init() catch |err| { + log.err("init() error: {}", .{err}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpErrorReturnTrace(trace); + } + sapp.quit(); + }; + } + + fn frame(self: *Self) !void { + tracy.frameMark(); + + _ = self.frame_arena.reset(.free_all); // TODO: make arena reset mode configurable + + const now = Io.Timestamp.now(self.io, .awake); + const t = self.started_at.durationTo(now); + const dt = self.last_frame_at.durationTo(now); + self.last_frame_at = now; + + if (self.input.keyboard.pressed.contains(.F4)) { + self.show_imgui = !self.show_imgui; + } + + { + Gfx.beginFrame(); + ImGUI.beginFrame(self.show_imgui, self.frame_arena.allocator()); + + if (@hasDecl(App, "frame")) { + const plt = Frame{ + .t = t.nanoseconds, + .dt = dt.nanoseconds, + .gpa = self.gpa, + .arena = self.arena.allocator(), + .io = self.io, + .input = &self.input, + .input_events = self.events.items, + .frame = self.frame_arena.allocator(), + .assets = &self.assets + }; + try self.app.frame(plt); + } + + Gfx.flush(.{}); + Gfx.showDebug(); + + ImGUI.endFrame(); + Gfx.endFrame(); + } + + self.events_overflow = false; + self.events.clearRetainingCapacity(); + self.input.keyboard.pressed = .empty; + self.input.keyboard.released = .empty; + self.input.mouse_buttons.pressed = .empty; + self.input.mouse_buttons.released = .empty; + } + + fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void { + const self: *Self = @ptrCast(@alignCast(user_data)); + self.frame() catch |err| { + log.err("frame() error: {}", .{err}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpErrorReturnTrace(trace); + } + sapp.quit(); + }; + } + + // TODO: I don't like the names of these 'event' related functions. + // All of them "append an event", but they do different things. + fn applyEventToState(self: *Self, e: Input.Event) void { + const input = &self.input; + + switch (e) { + .key_pressed => |key| { + assert(!input.isKeyDown(key)); + input.keyboard.press(key); + self.last_key_pressed = key; + }, + .key_released => |key| { + assert(input.isKeyDown(key)); + input.keyboard.release(key); + }, + .mouse_pressed => |button| { + assert(input.mouse_position != null); + assert(!input.isMouseDown(button)); + input.mouse_buttons.press(button); + }, + .mouse_released => |button| { + assert(input.mouse_position != null); + assert(input.isMouseDown(button)); + input.mouse_buttons.release(button); + }, + .mouse_enter => |pos| { + assert(input.mouse_position == null); + input.mouse_position = pos; + }, + .mouse_move => |pos| { + assert(input.mouse_position != null); + input.mouse_position = pos; + }, + .mouse_leave => { + assert(input.mouse_position != null); + input.mouse_position = null; + }, + .char => |char| { + // A 'key_pressed' event always occurs before a 'char' event. + // This allows us to differentiate between keycodes and scan codes. + // And how to map key codes to character codes. + assert(self.last_key_pressed != null); + input.key_code_mapping.put(self.last_key_pressed.?, char); + self.last_key_pressed = null; + }, + .mouse_scroll => |offset| { + assert(input.mouse_position != null); + input.mouse_scroll = input.mouse_scroll.add(offset); + }, + .window_resize => |window_size| { + self.input.window_size = window_size; + }, + .focused => { + assert(!input.focused); + input.focused = true; + }, + .unfocused => { + assert(input.focused); + assert(input.mouse_position == null); + assert(input.keyboard.down.eql(.empty)); + assert(input.mouse_buttons.down.eql(.empty)); + input.focused = false; + } + } + } + + fn appendEvent(self: *Self, e: Input.Event) void { + if (self.events.capacity == self.events.items.len) { + if (!self.events_overflow) { + log.warn("too many events, limit: {}", .{self.events.capacity}); + } + self.events_overflow = true; + return; + } + + self.events.appendAssumeCapacity(e); + self.applyEventToState(e); + } + + fn pushEvent(self: *Self, e: Input.Event) void { + const input = &self.input; + + if (input.focused) { + if (e == .focused) { + return; + } + + if (e == .key_pressed and input.isKeyDown(e.key_pressed)) { + return; + } + if (e == .key_released and !input.isKeyDown(e.key_released)) { + return; + } + if (e == .mouse_move and input.mouse_position.?.eql(e.mouse_move)) { + return; + } + + if (e == .unfocused) { + var key_iter = input.keyboard.down.iterator(); + while (key_iter.next()) |key| { + self.appendEvent(.{ .key_released = key }); + } + var mouse_buttons_iter = input.mouse_buttons.down.iterator(); + while (mouse_buttons_iter.next()) |button| { + self.appendEvent(.{ .mouse_released = button }); + } + if (input.mouse_position != null) { + self.appendEvent(.{ .mouse_leave = {} }); + } + } + + } else { + if (e == .unfocused) { + return; + } + + if (e != .focused) { + self.appendEvent(.focused); + + if (e == .mouse_move) { + self.appendEvent(.{ .mouse_enter = e.mouse_move }); + } + } + } + + self.appendEvent(e); + } + + fn event(self: *Self, e: sapp.Event) !bool { + if (self.show_imgui) { + if (ImGUI.event(e)) { + self.pushEvent(.{ + .unfocused = {} + }); + return true; + } + } + + const input = &self.input; + + switch (e.type) { + .MOUSE_DOWN => { + if (input.mouse_position == null) { + self.pushEvent(.{ + .mouse_enter = .init(e.mouse_x, e.mouse_y) + }); + } + + self.pushEvent(.{ + .mouse_pressed = Input.MouseButton.fromSokol(e.mouse_button) orelse return false, + }); + }, + .MOUSE_UP => { + if (input.mouse_position == null) { + self.pushEvent(.{ + .mouse_enter = .init(e.mouse_x, e.mouse_y) + }); + } + + self.pushEvent(.{ + .mouse_released = Input.MouseButton.fromSokol(e.mouse_button) orelse return false, + }); + }, + .MOUSE_MOVE => { + if (input.mouse_position == null) { + self.pushEvent(.{ + .mouse_enter = .init(e.mouse_x, e.mouse_y) + }); + } + + self.pushEvent(.{ + .mouse_move = .init(e.mouse_x, e.mouse_y) + }); + }, + .MOUSE_ENTER => { + self.pushEvent(.{ + .mouse_enter = .init(e.mouse_x, e.mouse_y) + }); + }, + .MOUSE_LEAVE => { + self.pushEvent(.{ + .mouse_leave = {} + }); + }, + .MOUSE_SCROLL => { + self.pushEvent(.{ + .mouse_scroll = .init(e.scroll_x, e.scroll_y) + }); + }, + .KEY_DOWN => { + self.last_key_press_is_repeat = e.key_repeat; + if (!e.key_repeat) { + self.pushEvent(.{ + .key_pressed = @enumFromInt(@intFromEnum(e.key_code)) + }); + } + }, + .KEY_UP => { + self.pushEvent(.{ + .key_released = @enumFromInt(@intFromEnum(e.key_code)), + }); + }, + .CHAR => { + if (!self.last_key_press_is_repeat) { + self.pushEvent(.{ + .char = @intCast(e.char_code) + }); + } + }, + .FOCUSED => { + self.pushEvent(.{ + .focused = {} + }); + }, + .UNFOCUSED => { + self.pushEvent(.{ + .unfocused = {} + }); + }, + else => {} + } + + return true; + } + + fn sokolEvent(ev: [*c]const sapp.Event, user_data: ?*anyopaque) callconv(.c) void { + const self: *Self = @ptrCast(@alignCast(user_data)); + + const consumed_event = self.event(ev.*) catch |err| blk: { + log.err("event() error: {}", .{err}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpErrorReturnTrace(trace); + } + sapp.quit(); + break :blk false; + }; + + if (consumed_event) { + sapp.consumeEvent(); + } + } + + fn sokolCleanup(user_data: ?*anyopaque) callconv(.c) void { + const self: *Self = @ptrCast(@alignCast(user_data)); + if (@hasDecl(App, "deinit")) { + const plt = Deinit{ + .gpa = self.gpa, + .io = self.io + }; + self.app.deinit(plt); + } + self.frame_arena.deinit(); + self.assets.deinit(); + + ImGUI.deinit(); + Gfx.deinit(); + } + }; +} + +pub const RunOptions = struct { + init: std.process.Init, + + window_title: [*c]const u8 = null, + width: i32 = 800, + height: i32 = 600, +}; + +pub const Assets = struct { + arena: std.heap.ArenaAllocator, + gpa: Allocator, + io: Io, + + dir: ?Io.Dir, + files: std.array_hash_map.String(File), + + fn init(gpa: Allocator, io: Io, dir: ?Io.Dir) Assets { + return Assets{ + .gpa = gpa, + .io = io, + .arena = .init(gpa), + .dir = dir, + .files = .empty + }; + } + + fn readFileFromAssetDir(self: *Assets, filename: []const u8) !?[:0]const u8 { + const dir = self.dir orelse return null; + + const arena = self.arena.allocator(); + const contents = try dir.readFileAllocOptions( + self.io, + filename, + arena, + .unlimited, + .of(u8), + 0 + ); + + const filename_owned = try arena.dupe(u8, filename); + try self.files.put(self.gpa, filename_owned, File{ + .contents = contents + }); + + return contents; + } + + pub fn readFile(self: *Assets, filename: []const u8) [:0]const u8 { + if (self.files.get(filename)) |file| { + return file.contents; + } else { + if (self.readFileFromAssetDir(filename)) |maybe_contents| { + if (maybe_contents) |contents| { + log.warn("Read file which isn't defined in assets: '{s}'", .{filename}); + return contents; + } else { + log.err("Attempt to read file '{s}', which doesn't exist", .{filename}); + return ""; + } + } else |err| { + log.err("Failed to read file from filesystem: {}", .{err}); + return ""; + } + } + } + + fn insertStaticFile(self: *Assets, filename: []const u8, contents: [:0]const u8) !void { + try self.files.put(self.gpa, filename, File{ + .contents = contents + }); + } + + fn deinit(self: *Assets) void { + self.arena.deinit(); + self.files.deinit(self.gpa); + } + + const File = struct { + contents: [:0]const u8 + }; +}; + +pub const Init = struct { + gpa: Allocator, + arena: Allocator, + io: Io, + assets: *Assets +}; + +pub const Deinit = struct { + gpa: Allocator, + io: Io, +}; + +pub const Nanoseconds = i96; + +pub const Frame = struct { + t: Nanoseconds, + dt: Nanoseconds, + + gpa: Allocator, + arena: Allocator, + frame: Allocator, + io: Io, + assets: *Assets, + input: *Input, + input_events: []Input.Event, + + fn nanosecondsToSeconds(nanoseconds: i96) f32 { + return @floatCast(@as(f64, @floatFromInt(nanoseconds)) / std.time.ns_per_s); + } + + pub fn deltaTime(self: Frame) f32 { + return nanosecondsToSeconds(self.dt); + } + + pub fn time(self: Frame) f32 { + return nanosecondsToSeconds(self.t); + } +}; + +pub fn run(App: type, opts: RunOptions) void { + const arena = opts.init.arena.allocator(); + const gpa = opts.init.gpa; + const io = opts.init.io; + + var assets_dir: ?Io.Dir = null; + if (build_options.assets_dir) |assets_dir_path| { + const cwd = Io.Dir.cwd(); + assets_dir = cwd.openDir(io, assets_dir_path, .{}) catch |e| blk: { + log.err("Failed to open assets directory: {}", .{e}); + break :blk null; + }; + } + + const PlatformApp = PlatformType(App); + const plt = arena.create(PlatformApp) catch @panic("OOM"); + plt.* = PlatformApp{ + .gpa = gpa, + .arena = opts.init.arena, + .io = io, + .cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM"), + .app = undefined, + .input = undefined, + .events_buffer = undefined, + .events_overflow = false, + .events = .initBuffer(&plt.events_buffer), + .started_at = undefined, + .last_frame_at = undefined, + .last_key_press_is_repeat = false, + .last_key_pressed = null, + .frame_arena = .init(gpa), + .show_imgui = builtin.mode == .Debug, + .assets = .init(gpa, io, assets_dir) + }; + + inline for (EmbeddedAssets.files) |file| { + plt.assets.insertStaticFile(file.name, file.contents) catch @panic("OOM"); + } + + var icon: sapp.IconDesc = .{}; + icon.sokol_default = true; + // TODO: Don't hard code icon path, allow changing through options + // var icon_data = try STBImage.load(@embedFile("../assets/icon.png")); + // defer icon_data.deinit(); + // TODO: + // icon.images[0] = .{ + // .width = @intCast(icon_data.width), + // .height = @intCast(icon_data.height), + // .pixels = .{ + // .ptr = icon_data.rgba8_pixels, + // .size = icon_data.width * icon_data.height * 4 + // } + // }; + + tracy.setThreadName("Main"); + + const is_wasm = builtin.cpu.arch.isWasm(); + if (builtin.os.tag == .linux and !is_wasm) { + var sa: std.posix.Sigaction = .{ + .handler = .{ .handler = posixSignalHandler }, + .mask = std.posix.sigemptyset(), + .flags = std.posix.SA.RESTART, + }; + std.posix.sigaction(std.posix.SIG.INT, &sa, null); + } + + sapp.run(.{ + .init_userdata_cb = PlatformApp.sokolInit, + .frame_userdata_cb = PlatformApp.sokolFrame, + .cleanup_userdata_cb = PlatformApp.sokolCleanup, + .event_userdata_cb = PlatformApp.sokolEvent, + .user_data = plt, + .window_title = opts.window_title, + .width = opts.width, + .height = opts.height, + .icon = icon, + .logger = .{ .func = sokolLogCallback }, + }); +} + +fn posixSignalHandler(sig: std.posix.SIG) callconv(.c) void { + _ = sig; + sapp.requestQuit(); +} + +fn toSokolColor(color: Color) sokol.gfx.Color { + // TODO: Assert and do cast + return sokol.gfx.Color{ + .r = color.r, + .g = color.g, + .b = color.b, + .a = color.a, + }; +} + +fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void { + if (log_level == 0) { + log.err(format, args); + } else if (log_level == 1) { + log.err(format, args); + } else if (log_level == 2) { + log.warn(format, args); + } else { + log.info(format, args); + } +} + +fn cStrToZig(c_str: [*c]const u8) [:0]const u8 { + return std.mem.span(c_str); +} + +fn sokolLogCallback(tag: [*c]const u8, log_level: u32, log_item: u32, message: [*c]const u8, line_nr: u32, filename: [*c]const u8, user_data: ?*anyopaque) callconv(.c) void { + _ = user_data; + + if (filename != null) { + sokolLogFmt( + log_level, + "[{s}][id:{}] {s}:{}: {s}", + .{ + cStrToZig(tag orelse "-"), + log_item, + std.fs.path.basename(cStrToZig(filename orelse "-")), + line_nr, + cStrToZig(message orelse "") + } + ); + } else { + sokolLogFmt( + log_level, + "[{s}][id:{}] {s}", + .{ + cStrToZig(tag orelse "-"), + log_item, + cStrToZig(message orelse "") + } + ); + } +} diff --git a/src/shader.glsl b/src/shader.glsl new file mode 100644 index 0000000..3dc847b --- /dev/null +++ b/src/shader.glsl @@ -0,0 +1,50 @@ +@header const m = @import("math") +@ctype mat4 m.Mat4 + +/* main vertex shader */ +@vs vs +in vec2 position; +in vec2 texcoord0; +in vec4 color0; +out vec4 color; +out vec4 uv; + +layout(binding = 0) uniform vs_params { + mat4 view; + mat4 projection; +}; + +void main() { + gl_Position = projection * view * vec4(position, 0, 1); + color = color0; + uv = vec4(texcoord0, 0.0, 1.0); +} +@end + +/* main fragment shader */ +@fs fs +layout(binding=0) uniform texture2D tex; +layout(binding=0) uniform sampler smp; +in vec4 color; +in vec4 uv; +out vec4 frag_color; + +layout(binding = 1) uniform fs_params { + // Textures modes: + // 0 - rgba8 + // 1 - r8 - use red channel as opacity, used for fonts. + int texture_mode; +}; + +void main() { + vec4 tex_color = texture(sampler2D(tex, smp), uv.xy); + if (texture_mode == 0) { + frag_color = tex_color * color; + } else if (texture_mode == 1) { + frag_color = vec4(1, 1, 1, tex_color.r) * color; + } +} +@end + +/* main shader program */ +@program main vs fs diff --git a/src/slot_map.zig b/src/slot_map.zig new file mode 100644 index 0000000..5eb1cd2 --- /dev/null +++ b/src/slot_map.zig @@ -0,0 +1,305 @@ +const std = @import("std"); +const tracy = @import("tracy"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +/// This array provides: +/// - O(1) insertion +/// - O(1) removal +/// - O(1) lookup +/// - Pointers and IDs stay stable when inserting and removing. +/// - Unique IDs for every inserted item (assuming that generation doesn't overflow) +/// +/// For more details about this data structure checkout this podcast episode from Wookash: +/// * https://www.youtube.com/watch?v=ShSGHb65f3M +pub fn SlotMapType(Index: type, Generation: type, Value: type) type { + assert(@typeInfo(Generation) == .int); + assert(@typeInfo(Index) == .int); + + return struct { + const Self = @This(); + + // TODO: This struct will probably have paddings bytes inserted. + // It's not ideal for a data structure like this which will be commonly used. + // + // The `used` field could be packed into a bitset. + // But that would introduce a second `std.ArrayList` which would make managing the memory a PITA. + // + // So in the end idk if it's worth it. + pub const Slot = struct { + value: Value, + generation: Generation, + next_hole: ?Index, + used: bool + }; + + slots: std.ArrayList(Slot), + first_hole: ?Index, + last_hole: ?Index, + hole_count: usize, + + const empty = Self{ + .slots = .empty, + .first_hole = null, + .last_hole = null, + .hole_count = 0, + }; + + pub const Id = packed struct { + generation: Generation, + index: Index, + + pub fn format(self: Id, writer: *std.Io.Writer) std.Io.Writer.Error!void { + try writer.print("Id({s}){{ {}, {} }}", .{ @typeName(Value), self.index, self.generation }); + } + }; + + pub const Iterator = struct { + slot_map: *Self, + index: Index, + + pub fn next(self: *Iterator) ?Id { + while (self.index < self.slot_map.slots.items.len) { + const index = self.index; + const slot = self.slot_map.slots.items[index]; + self.index += 1; + + if (slot.used) { + return Id{ + .index = @intCast(index), + .generation = slot.generation + }; + } + } + + return null; + } + }; + + pub fn clearRetainingCapacity(self: *Self) void { + var slots = self.slots; + slots.clearRetainingCapacity(); + + self.* = .init(slots); + } + + fn insertHole(self: *Self, index: Index) void { + if (self.last_hole) |last_hole| { + self.slots.items[index].next_hole = last_hole; + self.last_hole = index; + } else { + self.first_hole = index; + self.last_hole = index; + } + self.hole_count += 1; + } + + fn removeUnused(self: *Self) ?Index { + if (self.first_hole) |first_hole| { + self.first_hole = self.slots.items[first_hole].next_hole; + if (self.first_hole == null) { + self.last_hole = null; + } + self.hole_count -= 1; + return first_hole; + } + + const capacity = @min(self.slots.capacity, std.math.maxInt(Index)); + if (self.slots.items.len < capacity) { + const index: Index = @intCast(self.slots.items.len); + self.slots.items.len += 1; + self.slots.items[index] = Slot{ + .value = undefined, + .generation = 0, + .next_hole = null, + .used = false + }; + return index; + } + + return null; + } + + pub fn ensureUnusedCapacity(self: *Self, gpa: Allocator, additional_count: usize) Allocator.Error!void { + if (additional_count > self.hole_count) { + const new_capacity, const overflow = @addWithOverflow( + self.slots.capacity, + additional_count - self.hole_count + ); + if (overflow != 0) return error.OutOfMemory; + if (new_capacity >= std.math.maxInt(Index)) return error.OutOfMemory; + + try self.slots.ensureTotalCapacity(gpa, new_capacity); + } + } + + pub fn unusedCapacity(self: *Self) usize { + const capacity = @min(self.slots.capacity, std.math.maxInt(Index)); + return capacity - self.slots.items.len + self.hole_count; + } + + pub fn insertAssumeCapacity(self: *Self) Id { + const index = self.removeUnused().?; + const slot = &self.slots.items[index]; + assert(!slot.used); + slot.used = true; + + return Id{ + .index = @intCast(index), + .generation = slot.generation + }; + } + + pub fn insert(self: *Self, gpa: Allocator) Allocator.Error!Id { + try self.ensureUnusedCapacity(gpa, 1); + return self.insertAssumeCapacity(); + } + + pub fn insertBounded(self: *Self) Allocator.Error!Id { + if (self.unusedCapacity() == 0) { + return error.OutOfMemory; + } + return self.insertAssumeCapacity(); + } + + pub fn exists(self: *Self, id: Id) bool { + if (id.index >= self.slots.items.len) { + return false; + } + + const slot = self.slots.items[id.index]; + + return slot.used and slot.generation == id.generation; + } + + pub fn get(self: *Self, id: Id) ?*Value { + if (self.exists(id)) { + return &self.slots.items[id.index].value; + } + return null; + } + + pub fn getAssumeExists(self: *Self, id: Id) *Value { + assert(self.exists(id)); + return self.get(id).?; + } + + pub fn removeAssumeExists(self: *Self, id: Id) void { + assert(self.exists(id)); + + const slot = &self.slots.items[id.index]; + + slot.used = false; + slot.generation +%= 1; + self.insertHole(id.index); + } + + pub fn remove(self: *Self, id: Id) bool { + if (!self.exists(id)) { + return false; + } + + self.removeAssumeExists(id); + return true; + } + + pub fn iterator(self: *Self) Iterator { + return Iterator{ + .slot_map = self, + .index = 0 + }; + } + + pub fn init(slots: std.ArrayList(Slot)) Self { + var self: Self = .empty; + self.slots = slots; + return self; + } + + pub fn deinit(self: *Self, gpa: Allocator) void { + self.slots.deinit(gpa); + } + }; +} + +const TestMap = SlotMapType(u24, u8, void); + +// TODO: Add more rigorous test for check if generation usage is nicely distributed. + +test "insert & remove" { + const expect = std.testing.expect; + const gpa = std.testing.allocator; + + var map: TestMap = .empty; + defer map.deinit(gpa); + + const id1 = try map.insert(gpa); + try expect(map.exists(id1)); + try expect(map.remove(id1)); + try expect(!map.exists(id1)); + try expect(!map.remove(id1)); + + const id2 = try map.insert(gpa); + try expect(map.exists(id2)); + try expect(!map.exists(id1)); +} + +test "generation wrap around" { + const expectEqual = std.testing.expectEqual; + const gpa = std.testing.allocator; + + var map: TestMap = .empty; + defer map.deinit(gpa); + + // Grow array list so that at least 1 slot exists + const id1 = try map.insert(gpa); + map.removeAssumeExists(id1); + + // Artificially increase generation count + map.slots.items[id1.index].generation = std.math.maxInt(@FieldType(TestMap.Id, "generation")); + + // Check if generation wraps around + const id2 = try map.insert(gpa); + map.removeAssumeExists(id2); + try expectEqual(id1.index, id2.index); + try expectEqual(0, map.slots.items[id1.index].generation); +} + +test "iterator" { + const expectEqual = std.testing.expectEqual; + const gpa = std.testing.allocator; + + var map: TestMap = .empty; + defer map.deinit(gpa); + + // Create array which has a hole + const id1 = try map.insert(gpa); + const id2 = try map.insert(gpa); + const id3 = try map.insert(gpa); + + map.removeAssumeExists(id2); + + var iter = map.iterator(); + try expectEqual(id1, iter.next().?); + try expectEqual(id3, iter.next().?); + try expectEqual(null, iter.next()); +} + +test "clear retaining capacity" { + const expect = std.testing.expect; + const expectEqual = std.testing.expectEqual; + const gpa = std.testing.allocator; + + var map: TestMap = .empty; + defer map.deinit(gpa); + + const id1 = try map.insert(gpa); + try expect(map.exists(id1)); + map.clearRetainingCapacity(); + + const id2 = try map.insert(gpa); + try expect(map.exists(id2)); + + try expectEqual(id1, id2); +}