From dada247460a32720a231826a28897c56afe0f898 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 5 Jan 2025 22:00:08 -0800 Subject: [PATCH] Initial download from archive http://web.archive.org/web/20210423124630/https://github.com/jdah/tetris-os/blob/master/README.md --- .gitignore | 59 ++++ LICENSE | 21 ++ Makefile | 60 ++++ README.md | 53 ++++ images/0.png | Bin 0 -> 105092 bytes src/font.c | 158 +++++++++++ src/font.h | 21 ++ src/fpu.c | 15 + src/fpu.h | 8 + src/idt.c | 39 +++ src/idt.h | 10 + src/irq.c | 82 ++++++ src/irq.h | 10 + src/isr.c | 172 +++++++++++ src/isr.h | 16 ++ src/keyboard.c | 66 +++++ src/keyboard.h | 87 ++++++ src/link.ld | 29 ++ src/main.c | 756 +++++++++++++++++++++++++++++++++++++++++++++++++ src/math.c | 46 +++ src/math.h | 15 + src/music.c | 361 +++++++++++++++++++++++ src/music.h | 9 + src/screen.c | 41 +++ src/screen.h | 53 ++++ src/sound.c | 356 +++++++++++++++++++++++ src/sound.h | 50 ++++ src/speaker.c | 44 +++ src/speaker.h | 10 + src/stage0.S | 203 +++++++++++++ src/start.S | 126 +++++++++ src/system.c | 35 +++ src/system.h | 21 ++ src/timer.c | 48 ++++ src/timer.h | 12 + src/util.h | 202 +++++++++++++ 36 files changed, 3294 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 images/0.png create mode 100644 src/font.c create mode 100644 src/font.h create mode 100644 src/fpu.c create mode 100644 src/fpu.h create mode 100644 src/idt.c create mode 100644 src/idt.h create mode 100644 src/irq.c create mode 100644 src/irq.h create mode 100644 src/isr.c create mode 100644 src/isr.h create mode 100644 src/keyboard.c create mode 100644 src/keyboard.h create mode 100644 src/link.ld create mode 100644 src/main.c create mode 100644 src/math.c create mode 100644 src/math.h create mode 100644 src/music.c create mode 100644 src/music.h create mode 100644 src/screen.c create mode 100644 src/screen.h create mode 100644 src/sound.c create mode 100644 src/sound.h create mode 100644 src/speaker.c create mode 100644 src/speaker.h create mode 100644 src/stage0.S create mode 100644 src/start.S create mode 100644 src/system.c create mode 100644 src/system.h create mode 100644 src/timer.c create mode 100644 src/timer.h create mode 100644 src/util.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3345747 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +bin/ +*.iso + +*.icloud +.DS_Store +.vscode + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2e2ef67 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 jdah + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d911607 --- /dev/null +++ b/Makefile @@ -0,0 +1,60 @@ +UNAME := $(shell uname) + +ifeq ($(UNAME),Linux) + CC=gcc -elf_i386 + AS=as --32 + LD=ld -m elf_i386 +else + CC=i386-elf-gcc + AS=i386-elf-as + LD=i386-elf-ld +endif + +GFLAGS= +CCFLAGS=-m32 -std=c11 -O2 -g -Wall -Wextra -Wpedantic -Wstrict-aliasing +CCFLAGS+=-Wno-pointer-arith -Wno-unused-parameter +CCFLAGS+=-nostdlib -nostdinc -ffreestanding -fno-pie -fno-stack-protector +CCFLAGS+=-fno-builtin-function -fno-builtin +ASFLAGS= +LDFLAGS= + +BOOTSECT_SRCS=\ + src/stage0.S + +BOOTSECT_OBJS=$(BOOTSECT_SRCS:.S=.o) + +KERNEL_C_SRCS=$(wildcard src/*.c) +KERNEL_S_SRCS=$(filter-out $(BOOTSECT_SRCS), $(wildcard src/*.S)) +KERNEL_OBJS=$(KERNEL_C_SRCS:.c=.o) $(KERNEL_S_SRCS:.S=.o) + +BOOTSECT=bootsect.bin +KERNEL=kernel.bin +ISO=boot.iso + +all: dirs bootsect kernel + +clean: + rm -f ./**/*.o + rm -f ./*.iso + rm -f ./**/*.elf + rm -f ./**/*.bin + +%.o: %.c + $(CC) -o $@ -c $< $(GFLAGS) $(CCFLAGS) + +%.o: %.S + $(AS) -o $@ -c $< $(GFLAGS) $(ASFLAGS) + +dirs: + mkdir -p bin + +bootsect: $(BOOTSECT_OBJS) + $(LD) -o ./bin/$(BOOTSECT) $^ -Ttext 0x7C00 --oformat=binary + +kernel: $(KERNEL_OBJS) + $(LD) -o ./bin/$(KERNEL) $^ $(LDFLAGS) -Tsrc/link.ld + +iso: dirs bootsect kernel + dd if=/dev/zero of=boot.iso bs=512 count=2880 + dd if=./bin/$(BOOTSECT) of=boot.iso conv=notrunc bs=512 seek=0 count=1 + dd if=./bin/$(KERNEL) of=boot.iso conv=notrunc bs=512 seek=1 count=2048 diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc7d950 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# TETRIS-OS: An operating system that only plays Tetris. + +![screenshot](images/0.png) + +[Video with an explanation of the development process.](https://www.youtube.com/watch?v=FaILnmUYS_U) + +#### Features: +- It's Tetris. +- 32-bit (x86) +- Fully custom bootloader +- Soundblaster 16 driver +- Custom music track runner +- Fully hardcoded tetris theme +- Double-buffered 60 FPS graphics at 320x200 pixels with custom 8-bit RGB palette + +#### Resources Used +- [osdev.org wiki](https://wiki.osdev.org/Main_Page) +- [Sortix](https://sortix.org) +- [ToaruOS](https://toaruos.org) +- [James Molloy's Kernel Development Tutorials](http://www.jamesmolloy.co.uk/tutorial_html/) + +### Building & Running +~~**NOTE**: This has *only* been tested in an emulator. Real hardware might not like it.~~ + +EDIT: this is not true anymore! [@parkerlreed has run this on a Thinkpad T510](https://github.com/jdah/tetris-os/issues/5#issuecomment-824507979). + +#### Mac OS +For the cross-compiler: `$ brew tap nativeos/i386-elf-toolchain && brew install i386-elf-binutils i386-elf-gcc` +``` +$ make iso +$ qemu-system-i386 -drive format=raw,file=boot.iso -d cpu_reset -monitor stdio -device sb16 -audiodev coreaudio,id=coreaudio,out.frequency=48000,out.channels=2,out.format=s32 +``` + +#### Unix-like +You should not need a cross-compiler in *most* cases as the `gcc` shipped in most linux distros will support `i386` targets. + +[If this isn't the case for you, read here about getting a cross-compiler.](https://wiki.osdev.org/GCC_Cross-Compiler) + +To run: +``` +$ make iso +$ qemu-system-i386 -drive format=raw,file=boot.iso -d cpu_reset -monitor stdio -device sb16 -audiodev pulseaudio,id=pulseaudio,out.frequency=48000,out.channels=2,out.format=s32 +``` + +If you have sound device issues, try building without the `#define ENABLE_MUSIC` in `main.c` and running with `$ qemu-system-i386 -drive format=raw,file=boot.iso`. + +If you're having issues with no image showing up/QEMU freezing, this is a known bug with QEMU SB16 emulation under GTK. [Please read what @takaswie has written in #2 for a workaround](https://github.com/jdah/tetris-os/issues/2#issuecomment-824773889). + +#### Windows +Absolutely no idea. Maybe try WSL. + +#### Real hardware +You probably know what you're doing if you're going to try this. Just burn `boot.iso` onto some bootable media and give it a go. If things break, try disabling all of the music (remove `#define ENABLE_MUSIC` in `main.c`) since you *probably* don't have something with a SB16 in it. diff --git a/images/0.png b/images/0.png new file mode 100644 index 0000000000000000000000000000000000000000..ec605f70877ecbd5a1e2f746ec98fa934f29f953 GIT binary patch literal 105092 zcmYhi1ymeO6E2JfcXxMphu{|6J-EAjf;+*3yIXLV;J(3wE)o`Z_kVf6d(VH*={~SC zJzY;#bx)T~w5qZ!3L+sQ1Ox<%yquIe1O&_~1Ozk^{3r00pUO|l5D<{zwvv*n@{*F| zE^f|NwhopM5cElbNg{HCpNWS}<`twFL%isBeiRBu!Kz`%)=FS7hUph-!KEnn7064@ z?d;IE;}XB<)(E!qA0KTo!iSKNJ2@%Gu&VF8CriZQqa{3Fo^9@)k&tswd})H({Z^(k ztx3sh)mUwIfdlz4n+PvsX+_|koEo5Ln@6D077kCDktp+iFdXU=Dv-+vEgR*lt zUG>{HpMNT*Q6WHC*lG?onyjO){bi(M>sLS5pp_tx3NbA%5=o5*J5v_nCAO>PqQ?Xt zCu?<18^8S?q5VcmAA9P&PI`y3J3Ioj83LvheMMmyXI0c}Rf-vf^s_74d%y z7?}iE%>r?+m#yH7E(0bpfohh)`Hh6!54p7>@)!gHa@!C@1sP856ixNC ztWlxf^A*7T=%#f5Kxn=5Uc2f@w6A8O=bmKd7eL^d;EpRQs-nsaKf;t5kNQn)>J_m@ zU>mmkY@+RZ^JnT_eVJXc@xH!P8(~qky-G5%DYPILx`Ln0oXCT^PK_~!vcq^m6YxbP`|wa~M%vYosF;y2ZiUWXeF zWe6G_*1jMo`==rB9-;6E*SJAT-D{_~YCYozBp z$MJc7G{$5lfeN{1qb^eG{RG*;`|s1)`|NbtreH)LM3A{m5qF2aKh+ROC^(65XA8#5 z<>yA-h~Y+(&xsq-ospq0yVQVrA8||4P)~x&nq(^K#=Igh%#jf38b;k2hZJ*!yyZ%F~Z4fd}tdQK^S}k++^{5OZ0AjMWaOjcjwoF8kkhE z80T1erYr_u?d#?r4Dl&K@N#9xl@35C#1AQWm00!2Yh(jhYDtBts6_n8_@HprPwWW( zE8|Qh{bYb*#Ipjjx3GBy=q4%A{N=D}{-6XGtsZMfr2l|&-#he=e7wgYJ>L#|UH7O0 zOt}Fj)qY2=@_DWQJWbSG`9tcvzxrRSKjs$xAKV5QUU@Yvx*Wakee*wzBfp6l)s^l~s#d&yMRlzG zH;?As-oqdG-vLe^JRm5N@PM!W*YJ~T?kadrIlw=%{vWB01pFyF{=jXT+VDS#(S;P; za?d>FzaF*ApP%18b(0sE4VR;UK@MZv1X}6=0la~-6K*cL6xJWm;HGA+J|OY%hUufU znxdr94i2B7e{d~_9^CR&97;(3vwj+kVeJ2*D$9rD5`s`lZx1`%ned_Mp8r-5_#beS z<-3+AIen_oZl%&*>jyu15Gt_x!5phSboi9HST^PKAqU)v0Lx6&WR2VN zHFP5Ic*tY<59$8@E%w`gGyEsN|DnO$waa|aq|W5w4vw4|AwtUJ#b;p z^#f=)QnN%vv|PqXRw8qCQ2h@(?V}GqAl#cj-YiJs`_HaT5+4I46UoDoF|&y7k^4BL zsNb4KM8NlDaQ-(g617R{V^FbpPUTLItWw>_!#w{-+*~vYean&%phW&k2mNV@4|YIo zN{DKdJmb{T|5nwtNh@UkqbBozq(G>!;($@8Pwxns9_vS9(LpX(Nt?smP-y{Xm$+lD zij|C$E?X)I^9`4}#D64T)!-rXJV;BezM>3d!3CbZ^Z%?}51Ai&(aeq|zd;O3^U5SpB5 ztf>}AL{9w?#khmLPaUM=_Gk$v#q9h)xne&&Q0112v{kd~M>GiAz{6MQ32M<9-M%uv zeRXm&asA*tVisBrd?Ng`KZm;QIDuDlY~h6W1Q-O;SZ_V=nG6P>-a!U9T$v+Dg2(<} z=?~PoYzcoM{rh_)9%#Oa%2qqvKRtM*+8?`h@Xd}}ex`B)K&t<@0# z@GoN(!voL9d3EzzU}S`I7>QJR5i1J=hDyR;SV9Tu?e!#H91-`hT&S7BOSQ5^xNM>Jo-EhsAQYo(bax;grLc*%?;8UjJ+_6ze6~UIh+| z5@xo=@WwD)QzIIPIb_#ni)dqRc;vV&O=sh@D-kHCwb9Vx`Nz}pYqb5@u0@yIi!ST* zF%;%nE+Kb+GFa)qFvs#8R^lT{2uBFlQUJ|f`gmg|s>}f}RSh>0$ncDMwV4-RDwguT zk92vMRzE>S;bM4Xi8L-D|7MS);SnuW3{pJp3T@@4*ROhnAMVtdx$a~fp0hYmIW1NP zsd-*{viRZLbVLLK(WL)AO;*z)R7JkyD^!*l6-x5OE8O{&_1O(VOkn!s`xD^ZF5@l3 zC&i$un3ufuTMK(tiL_fAep+wIoY_+tEmSnWMhUEO`6pn5Vt&XD=G2V1+U@Oe-yPiV zv8%m-?tEQdr-XL?@;eXX`2qfH-ie#o=--GrA7tficOd)B_1k}w9nTjUGjtFc|-%fDF2v!ocXn-3#=U^As^1kES^ zD&9GOsu}_CFvPE8beq2FYn#DH%WtR&5m<)4qlVc};Uht;rp3SMwN^Xz1+cL6mqqNi zt^{$xz{3MVj>jC+filT$MS3xU;VH#Yy*jEg&21?(LP?Miz;VN3d4xUAw53SmQ(_&Z zz&&MmoD9KQtw{mvGRY_BCXmH}rGnSuv-(8%Gmg#anvC^)U_qr(V`#?&ROP^gR=N9} zJl@`n;y519#^;TW`~boOLl#4#C>RmX(V0;J_Kth@VBP`W-tAjwi^ZcfH8L&oLfs{n z<3rY?MTX2Zo98={m)H9-b5NwFYyz`=EP&xYL=An>j^+ z!;%z*Wi5Lz1s`cCxp^BnWZUdoqptK=)=rzeYur-zdlc{7f~=jMeInq)TEMo6{rzng zkA3^g)nw#q6Za}X=(R(m0FRPL@O6A&nSb;EdpaA)MkuEJtn>KcKvrz^`qjJlI`?JK z%1d-*~r(j&hq7^t8QB(*vGuihB?!z;Nss1oxu6} z^%Wjl--}!CQ-%qP#66ewXHAdyEwneJ_a~EAtI!K4Ou7MU(d*F~lXrgy{y7@i@{< zZ^b|Iq^zc$ui>WoZOupN<3Vd3$JGvNcfqL7NmyI+`Hxgf^}!UwewjBoyS|&@0~_AJ z>0@K>y&{rXFrx%Z^(Mfo5FVlGY87I1au8(^eXpZ+JFIe~qo_=-_X-qm& zLNbx+a45Srqr{Gr(gd@Q!M)XBKP|&UC?RK}J0_1!YCCDwPTr`NQB*4#utVeqb<}r@ zCWOW$W7M*Y z!B0E-TGBei%9>Bpd}(#Sxdw$Y{d9|x2i=a-Ul~_=wGqCH{$qRY#*#&M-7Mr9zs5oM zCGu=p8*0^~bRr3oj3^GC?XILx{FW6=87ASrd0j{9+$TR+4UiQh_~MA&n_1wR^mr1# zr2N#^dlxW)#MKU;oG?`@7N` zXwcp-7JlDQ_@3UQ#V>(!t~9`2GLS({XSHRi`MxSyCn0JcvV{=YeAXosGHKY-Zr6|k zLNrx;K{~TOrd-C&e94qV7G)5B^MPr6qg&bjM~1Y7scAE<2r+|Jk2UqNAj zOXEsFYgB7chh4U2OFeAO+>Ij3^rrf3jA*Uw#Lre7mmpXEtxy?83h91uv$ z@-*J0->Zi5rxOaN$_fjs%zS}LcS`ksOSdPvWpmE2Ihun@W=!W@8S#rAo7?`YE@(%> z=cbkM+XdBHDupbL#TJ9|hXM*W+vz^0l{zAC&0e< zf>z&k*Mw?dCvG8@<5lgt+hoRd+6oF0ugi8g-jb9J@2l6})>8~x>sC}lBUktX80*|y z#IMNMwAv+xw@X~&RuDcblPc66vJQ*1cRykpyfzdzUs^SaLUJy)0@qev9J;8bA~cXS zDO{TjU(;`+zCgMxbvAty_zLrFCG*r_?TM-Y1(8Xo-f|jYUpuW&eRj@Bh-|n2yY&FP zZET9NFNXOp-LO6HsAauCESCxpwWy+QYiW%)`Kc3~J8oF|yRv`GN@mZcm#2liCX`4M zKdv92RkqSR11+6*a_P78)!UMd;|S9!ciwTqc9?tFrHG0Y#1yWWs=;Zhp+U5tAvrf< zd8mIBtS>eE(Qo-Nl8w3=TsFcLm@KWV$a3#276^^T#WhdYd2A~7$XAtSrp_#_yi|I# zq2clb9AuBP?CZ9txRyt>Yk&FOHqeX!#mDK|jqE7O6l!R`X(oe&oRsfrevuJ~9S$E+dCT z-_#(K1#UI#`rZaRnvRoX!QT0r)A6B)-+cDwP%N8|EAkAMvdHbiTsi1(<0Hk~%-O@E z>r$OR>@O&-(nUMh*W2ZNaEPPF|8Ymj#K8~=8-vvMA4c!X9^3vi@Yi;!!(SKE#OJA| zz~jL$01-Sz!B3$qQg{BEBfh^^b)cBgG4yMbTsD(7y6#=(mH75hhs*Q)V%UNoCp$=d zW(4+KE{E~>rj)n+&etFL;b`+Kn;q<~+p+36s%4W}r-smglj+z$ zQ%cL~{qVhaEB17PZ*`akKr=$;|GvYK@>e+Kdad%4b)VhJd3XgruVwqZKi5wK--qZ# zlY~|ghiRCO1V*t`)_7cu_KhXg8Y61c+XyNh7#0!3?@eS@yl;dk^i>YU3{?UPGCQ0 zl1}U~^6}JvlBLb%xy^~_$;YXEJEFTzpJqS-GIzU|>`j2A4`C6NF{Em;7%ow-=R22j zu-ci8(D}N1`{jrVXx{_;D7WA$XV76rQ1pGzd7u$$`r)S$>MRODXv?v=ilV zqK$+m3s3i^_gi$#e7fZj;YrcYiG;N{OGXti)PHzst_opqudj@Gd{dUAhzd0kd|s^+ zL2%UMfmgp>Jfkl&)U*Jn&M5L^NQAQ_o6}d@Sd;USQG5NRL`$gpRLe;D(a0U zD2y;g7I5?g@DGcbs7DQ4l4k$Gi72X01Pm=e=A;;Qv6jDNJUYh35B z%^H4S(+n3rtn)%sA6T-IRiQ4@vXZ8yhH;CAFUha4gU>EzO=W_CowOLqiavcT$t!91 zSI@7xZCMfw3cWi&Yr8Ajx8PI0t}7xx1GGIS*07fmtL=6$TuajmcZ!plTKD{*3hIp8 zTQt2tb9B43nMxbw59KG)?1q4txA@oHgePJu# zW{rzwT6RonKQOAbr)9jZP}ezF72mW5qRf!yOKS~}X)F%778|?MsibH9y{WOflQxaD z*%q{8hQgzw(AwpydQ3K{&$Ec<0d}?u)bz5Z`RkiW(w8>~WOX{}IO%YQ7aJNV{n=dl zS~{;q-<%z2K;3k2*l+cz6Uuced}IL1XWtM!wjiB zU;Czj(D)J&3j9iY!7ZcfgN9c|{dG4*5*LrFgkeFty!>C3YWD#2=h|xBU>6?sp7bEL!Ffdhc$# zRJ9@5MLQ#(ga1l+5~C}Nb_`b=-$MOBr9_~jO1wZ43l`^>+!xsU7b~!rnz5H~heBlh z$=CaDsuJMkTO~ALq)fSKYT8j2J%Bm)vQp&@vQmsDZy%m--9_?mE2I5V%{Es4gH&Cb z*yF#H01#KcD8H7`#HHz$N8Fn8yY=0(}aV#366GMh&}L~lR8se zbEVrd-Fc%osfI^tw>V`PX569ar`ixa*t#RsuG7c6i@#bCd&3s;a)3meupX@D#REFcfh*qAkTv zl8nUs6{+50Z{pfr{GvSAk7a&-zLe!Xus57*d4MeFYJbpzb>RKAFY)qR^bL4@8u&0s zwll5M!-0snJ%ly03+_nITDnajudSt*Z=3hM_pZY@Izyg&cDlk-R|mzxvG*#4s|eMm zbvW9NQh!^_iNGFaAYr|@QOCcu-GCF|3$i?^t%$`dA57#0UH&__*u&cwT`jkX;MavN zf!!O2oUy^d>FJMTOSm(>wk^TXFA^ z&z`h-jUXsD3dX2sF~r;WdZ(hEu}ajt8S-U8r6=^h_k92A)!t(W#Ag3rf!F62w^$7fALFJRkb_$8zJ_v%!2`syJ=lU* zDd69%s}{(|Z@$N2!wZcv#)pN)i4Ok4pzk(6GaCs-jh2?yO_r!1fi98ER*S0A+02y%QVzR2;ku<>WP%IOzApMimQP^qk(#WfQ*vIX4=WZp{V8WH(mmms z?B|Ywx5LzY)${ps8l@wSO01_n+ul$L>-ha^t@A7IJ@DP;Fjj1gkBhhLSR<&$pG*W?(e|6wGG9R^TW<&iEu7^l{+axIUEp-V&GIkS8~e`0Pa^#di-RDmix z?FJ@&ddpLu0#{X=YRjG2B+t}`IjfKd`K!i`x}!(F$nHL-S;x{(Y6_g>A}w}~E5+`H z$8QB*uULp;1KEX(2y>YS54v#_!w=}??)GoDNYSrh4oZ81+F4_Vvh zeoX(250^BzH;rHU*>GlTjOZt~pOg-euWp?vD4bPZ?_*oBzLi{JtyWfyP~FiFv*G+L z&pRVHgmdZJ|J};#hk{eA2b_jY;&GOg!3Zrls{?+IMlDH|{ln1LGgVfJdUhP1$#KO@ zPj;^~Uj90psEV#nj=HGbJ3-xXwd3!_ggh*ZB_%W9`t0WjrK9F2m8_sN*PSOTUx5v4 zJgA_5b~*RwrhswI4350tzY<!j&!QE?s-F%f!%*oZ;>9P=$bkCo}Rcd$tRw zQL9caQQ>)xdcYDk_aJ-1S#*;z>r%;663t}R%{*2AvbF%wK~3P`nZ_u+XaG+nhv|3H zq6B`Sp(IcJGFxo~kpbc9ch#bT7Hx)7siByr@6mNJ(h)+Kkq~2m}Gp0KN0L; zc!ovT55L9e{&Hh#mw5j(uong>-iomCNXo?0M;`jRulZUeSCPmhF z%#Q?2xHGWyiCCy;JQ&e_5#c{6S!Wh+u09O7ooM|myOSL~%`npV5zP4P44;y}A5C{= z1$ZMHU9~bd^!ZJMPb4HDrBr!AXYxRq>-Dq5U|e1KM{!Qq+ii`=E|e7UW+FSUmxOIu z-p(LdXH3VPHpFTZ(?hWeFfAW2Lvry5*bH}Xf;d^510rML&&HF;Cig~nXnRhtYj8FT0kNxLfJ4yNZ$>Ns^|FIEeh@+9JT z2&M{hpwhLXn5)I3ePpC`t*%aNbpL_PVWP*__gB0q>IUBHzU>kXWe0ph(*TD!vDdlj zS($IO2Kl?ieQ(Dc&yv1oyU6*^C%IA0u}(ir&&A&U_Vx}MFIxmUFqhF6F+B_@1&^f*A6z=>Xb=esDnZaU{#{>pF?h0yzi=iTU^ zv7>bvM7i7pXB3RV|Juv}_5dc>S*wI^{&+d{oJLbPH>%Bu?4#uPf)vqlzQ(D%E-!P4 zoEDLNAs`}PVHWC3d_I3R_`#t0)yj*`n9dwR+Qy9IB|hI}Nv|Fh##26enh5rmSCccG zBJTU{wS?~@a7<$l8Q%^P&k9HsZe|br-(OB@`>7$|Xl1_ybEk{El=Le7-&z2WjRCHH zI0Q+nLs#N{Yam^n7x7^1Oixt9Y)Y{U`sQnsm;sJ|)wGf?ZrpB(1}luQL@%XALZ^Aw zq;XTif{@f#FDsPbm%}cJ30QV6mX{5nBjWd2N?)S;n&~@9G?5{2$A(@=ZLwbt2f@3Z zXBfJ1fV->iZ}t4x(_h+)R@b25t^V(D67X#y?(*wFA@@uVAU-;^T;|~poQdU4kqN%O zM+Z@x{W_pf*j2ix-jy^3FU*1~KxA8ZKFf!XF57hsgvL-^;I;daw2`HUR+oKN9HcV2 z2)wN}DXl7lpk1C>%7B0<@QP#^&4{Od&6I@>-hkxI8RHWfQ_-%tXL`V{f=Ob|J^6 zTa>jwht6`t?ND@U{P7V?bGBEB4F%$X$y`iQLP%#U5y5R z@sNf^!?jVo#>GnEmQ?jn?^8Ze2@GZBO4{s*cw0F(Ei&mocMnbbW62g=EWJy_ z#pDaplAzS=ta+NHz^3Oxc2vk!9z1023L;UBDr>H(`|7pE=Z%lNAs?1Lydw--$b9lb z+xqOF1{5koJ+)%Y=g+A3NCcep1GzfJBei&WSW+QQ=UPnXHl@ayGhD0n<2&&6z6e=; zc9-?atBb3S565&kL}gWxSjQA_kwiYiEe{<>ZJPeHDRxfbLsvWl7d)F0d>3Z_^jDPC zqZUs~B9%CN;Aoa}H0=!|-NDx3U;5KxLzEfBb$l6c;##)B)i5{BT<=x)D;GRG3-x06 zaRb6MYMZGoGy?;vYzTXn%4Yc26jG>$0PWI&Qkvi7tGZ6+-?rugau6Hl2JDdxV-s}# zB*iGq@f^7l?=zW&H2v{5YT6DjHf8R~e(n2uf3_=`LW1Gcv4=Y{GJ^VhQ-d_< zf%ontkQ?y?!Ps|}CoVCcH*uE@kHzROzb4gsqYRw5{Y7WU`<024m6sE?7KPF%A$1gu z+ecpYV^2ZMa@_q6zO1g%Z(*cOCf%YU^W~$ewaxjwvvg6V2t*0XGAz&aBJ>V?p7|-S zeqV#ow&6w8cP-R6Z=JvQz_=Xn`C;p2UKFlp3kz^_d*=99*JE+1r*4;1Hi+Uh%#456 z4n}yOeRt;1ELc0zaZr?>aHn?8T)ahg^2<8$YBc{*u^UYs(*LgA7i9&!m!_8---#WY z)K~+?*ZA-L-rjZNkBFNkv|3J)v;{|&^A0;LeJ-CDkVGw|fJzxunGovYOX^N*ZT_DCnerKD6HG--- zZXqnAoVB)PyWKA@163!y;0M~?#rYp{@x@3PBz=|xy@YQ}TD6=Q=ENKB(gIp$g~;?W zBr$OIHL#e+Px`HC;Cg}FQ5Z}bspHgt&)4Dk<(p>AQ0f{WkidyP+b3e@8imJXst5HL z_#K$>iSg9%`i0oX<>JBStRh`N&4PYU{c1Qut}2$M(C^aHX++6K^sKz>H>8cdV>+5} zQN^^|1x)GU!Kzla((O--8AnOF^#Gm8ugHIzsAVPvZVT!fRUDZwvF!U7L8;x9VRpgc z)j=qlE*!mT1mcVd14WnyE4)mdEl23P&iIo^;N*mTZ4<2_5n|pXre3>gk#Z5cw~eNz znb!9gA8OK2_rl_7mylG^(Yk)-LK$N5UlZ```htzFUwNufb<=d5j$_KIa}%R+b}RDP z{-ICHXECJlo`93{JJ?ieXJpzwUF?xjZnJN&LNI1r4F&b21o8eM*X3>I%g2a+l;w7A zurvuHJnb(_fq{Q&?b&p4&GtM`2c)r#u@(|T#XdzAbyo5IT;XBj5j`#@U#NF%{id_x zJ^LH#wzyz)>!PHlzr!J^XFO@YO25zRGw5nxH&fzpXiSLj{E?QpXp)HJ#-Y6+jI}a| z*mX3_-SE#q@X`f2cR0E$g&TX*bYlcQrLP78U1nb+zB9OlOy)Z{J-`05=U=j-%M1l2 zcyGq|pLJNvpW2dj+f?WTfvoGTCf%9Tv@qM!fG^YeRu0JB_i^#@*ME0`qtCml^};=e zwwpI)e7Ea&Y%c?B7ng~_!gvFtWWfQJ05QJ@<-CoTd$liduCvnTI}bejvfqUUEHhq( zH&vhzCg7?0)E%jfJ*zWQrrAM{7a!#UBN%Km+=8wN5JqI%AhwMuaJ(>tzmf+ED@>rS z_X)GCyc`_|lW(B!^&y|i33k(eCV2MorC#h=VNYnUw(kXpZ42%F`1x~4?@O)0`94wb zt=wC5@FSbp#QE!HF!Hlo0wD0r-7w&Ccov>uwlMCsDcIbLm4zg*OpJa% zKbX|y{fW;lQa86g@YMcg#Q8<$?B-GB{ZPym9B&toQ-1CNz6OHvI9|?E83xR%h(Gyh zisa$G1FPzxRjdOseYU>C5s;C=-BL$adtl^dZBA?eUpdE+6%?$U@Z_Eg>XS$6h9u}u z-KV=k0%2m@6Z+mafn2=23Gmcs_?upfk*b-4#ljt~NXosRt9riR68MNc*9e3$z5^nI zGG$3|G)0u}E(+0%#-r&zts!3uUu=1+DSryjTp?iXzL3bqMlKEmAL|N!kydzxvn}IV zX^~_>{O^IoF*wlos(2_izAdV7U-nA%t)S4}59E0rW7cA-9g{uLarmE4@SsK`&-bcb z=i0M(6GRf+m+Ao`QMX#nnJcv?!1vGVBZz{B*^1yim)S6Z>x1D#VKMo<;t9D+;K;;) zUtF<`SF-+Gi@@ebO$(L!TAfTY$ywQN0_&XL;5hcY>K6aOLN>3jRasNtTL11hsYvZ5 zY;ylTJL|amiyS{e-@`QX#M+}e172s-YY@vLaDHBc#-B2}vkjM_Oc3!4U*5#uzx?}o z?#e2Cv$o}V`q4G3;QYIg*U8xXW|z0h;RDvlhziw$IMSV3wWou$tnW4wz{<=Fs#$ZD z_U$4lIq;bbdL@*OMhzfm6)crq?Hyn^oiO1U6g{}*^Dil0xTg}nUva*f-;2OJU=itR zSFfWe+%J+_Gm9t=G@C%VZNuvn5chnZKknG%<#A6Jw%|=Hq2A8`e zm|X(j&*v7hq{U?S(+0yiZClK6!{y?kccY0I zKktQEEd;u#BMnJdC-c8I*3fo!wx*RIkq}h&ZP1vOR3(GX)9jlp2|KU_Kffs?MZ|u} zP_Hh@r)$x-2A7nEA~O53@M17N1!^JHk(gVx%dNB)RGrvw;1}2xTO8)65juu-+I%($ zl)sox?tpH&b9b!%TV#}RSF zFDPZLG3e8aoEEBnG3!JE0GAG;r>OI<$L&$}QZFkpsVxXDWK<_qJuak+O^uKn3^5*2 z;O)~{zIIaxvpu*NdiqaEzY1c%cylTh*^2M7HM z^=AFRGqu$VajYFuei^kqQb`J*J3Wx^zcSf1wJ+Xm#JQtw&56BgPfEly0(Cuif^UPt z4?kwpnz7iQ`ca!crS%Q4_q-!#G1Yzq=RGA2ZOY zxXe#4?^=Aq*e<~`nXzZ>!o`Qw(wA4yX<$qgxg#MBx^WKqtd)4wEAdd!{cH^!xbGuN zh1J?Cgd_V0%f`H_cfQ&|g*V5=#g*eT5G`W4v9{^OWAZqmgTAjyEWhIxS&-g~W1$wC z(f8um{bH#zbc$rseJC67o`23>9_Fe^AYe2S^I?G!%Qx2GngvpiH%?W0ENba&=M2Ka z{)8j-`_Tjt70QY2zjIvc0`OP`QOifEO}@knd5O-vKc3-z)6<8q()e7i3v+Ip1haXS z-nNx9d4InZiWX)bz1ct_HmiDe&bMxDMZm zSH5L|>OmL)H++3iEcAH){>Ivf$k+Gu8;#xfYp`)b*`nQ|VAP$>dRfniiod=!T`u?U zX_HP;Q%qH5!9yHFzr$w?0;DCK>c#7>n2Sc8?)Z$p6VFTMQG?C9_Z^<5Y2jijN6hF} z0v2k$_@M{_^I{UG-p6J1l|%@Dqx|D7@Ig40iVVd(6YLZe=Y-0~!FxhRxKC92b)!{_ zVPRoawC*6L-mLhBw3}ljOqV}KM^{N~UFGpEUc*i_x{>9Z2AE-c#MLot zd|rvX&%VO>#mu6lrKVO^Mw^+HHGO~1B~L85Lbvm~QbASu2|jLV44z55j(Ig12?ows z$#q=Cif3#B--gU?BvY6(p-y1z=+}QPdONdF2wpVjtGkm^-M@ku=~MR>P2CR4dGBK& zU9iY;M6_~B8Q=#y${eF+P||lw6RDrMxzoWqT65e|gO;854-YX;bOzJCE2U%oVhKLLQ%Z%yoMHE3+J;7P8T zc_&q=>NM1DF6R(c2H}&5ectgJh<0h46$l?jI6LSKW)zA`7A3M0~89dn|tE8&}!={_rN&7$D}$X`9$6MFz? zTFS~aRAqT??C?;K#xYGf-IV1}wQbT8*dZy^wOoAlL{^8s)qaMpG00y;SN5n2jeRT2 z+&B3V^McvncgLR6@zTn%pEGHdFPkTcXsjNW|9SXWc$QIp@YtgXP#}ZQ!t`tq)r;AY zL-35)H!x_gn9LHewkYUGE{8_)F;i-}vh#O>rj}mGh;Xe;ZJSjmq{;L%<{zduHl|2T zbM<|0gHfRmDc%dvlE`gwQIQK)l$*F$yZ*Z$`yj_6t;0CVnw%DRl1mE|3`*;S-fQ5!)Vz|fdyalGKh{c_khT~ z;=W-`NjJ%^gFr@qo+DZ2@=$b$iIVhOf1x+H=?_ZjdFz=KoY1r-5rsJhY<=X|T1r||e zPEK4xZ|dwU?~WBxxjr#cJ;N`eLaet>nxmOnB(f#pRF+(C%rwRpeYGHMV$}imA+^V=S9c4xZx7RVc&!ATM_b)`_hHw6P7@p`PTSP348Op`7 z2)s*`j<(pvm>ky$*V5Z|LjNqzVYCCqHmrYSFo#A(?>I#78WQ({P2Q9E+=uO#;g%D8 zq{`6uB_bz#4KT~ftaMwBej0Y?_?BJ3<#&@>VkY56Pi=XiUeG771s=Mu`|Q?-QI_^U z*9Fp?w}aAJt5o~l_K134Q4={ zCcan=Cmhk)!^2~(QkbIe!}ZsEz?=!4;47#KHPA=R%~>M$cec$J`?z?rehfjRjh9lB zXejNKC&LL8vbTUZDD2+L<>2XwG^Q&nIcMKvzo1KJ#8aYaCMF$y$j!1xeo4#zRR*-AX!Dy%{ZLzU}F+X>;oYWlGeE6g5h< z$B}}>(DticcYf6(0gESl?J;a4S(1_X5K{ZkrF7|MEh4qWqWg7jWiN_mSAAu}sA~C! zKTJCV6PdAV9tEp1Lpcs3*WtJf>~=@V;4sxg9Bp_U9;&0>r`n&nycw)XLzE_aQyvPI z++>^@X4N;O0DXnl)N!n*klzqdz)w_0J$!GSVHAsQF?;2<8x4 z4zP)LIht#oCT%YDJ+i_z7v-hA$bAX2e<9~bNy`7g&&73sF(JuNm;5UnuX9JyNV33{ z48Q4*sqd>^Et5@U2!xJupet0`Mr2x(`#gN+vVl;)qv#z!d7kHI>1hA$D?YfI*bZUf z^L4%!2UQ2u9E(Q?#F8QUD4QRJnz&)j#-}Li-=^4-qm!uR30vu7bG2x6GU+mrc$4DWG9QV3>N~hxQ}XY zg}PU!aU7x?3OgUZl#(`uF=OH>EtXw;bF&S?!+XQ>EKLQNohvPutk+bomy<7uFK!ijDo?aZO3rf0skY>I9u)0UuX2ow`p)2 zotRPI<6QHnJ)NngnQEZ|#;W6JItyVo$;H!0WOT< z4bBiafCQ3K8^Q2bIDLoB?!S#Gj5>w-?2g`TpXNrjt!v!uHPAluC4#EU~BbnwwlbsKF8_CDptJhgl zZE1G9X^i{j4uLaVg2}0^=zY6}`Orwq=72hJq=)JK)Q+51XN{qFZemmQ*Mx!A+T6M$ z%SHEevrp(?i)0iuUHkWiS-jMxKf?@39Om5Augnmko-nMy*w@KDhBEHouKGgDW^e6@ zS!iGimGHAeh^Bg|C+WTO-0q!g+JTzNMC)ngw0(p;l>^3UMQ>-v16!$7`moXB_xY|; z_0~>Jy>bSDlGKTH;9|5@!zoBX#=t9R#b4bZGkF!Iz9-CdmD!OO9I`=Z`c|fz5a4z^T^S#NmA^gU6gi39lbz_3-m4fUBFb z;4uv*ud^>cPWE<`lk9Pi0LJp3WW}Isu_ihM*xCDOYKJvZyiN>|x=99iE_bYkJm_xL z_l~0=^dWV2pVZ>+Jpi)VF~iP=0W!nr&C59OF#BqDqxTfy?sS7#S}NVEZQ-o`hgX~Z zKGn(JM;F%YydeFR;4PO7t+|T*`GVv7msT(5y8INSqo-Q0HA2hD<_<29%V?Q?{S9+` zt5OrF&O*yJq9X*8afN%Rbm@+vr#6AMs@JEIQ24TZAtAKWals{5suR`vx3`_j^VkB9 zd8Rprqb!#`N)a2a@L`e=Fu1h50*QQBstSc=z9kR4;o(V!qtZKHF{5Bv+_bye7Y?@BA?p<4%e%IeR z+Zkta>2M;P$3rY%BWIHjJzI9?vs@|HTdk^>Vb<<-X`iUs%(@V-%oEg=*l(3oOAaOa7#MX z=)sv?QVD&$vtew;vx*E2K6i#?+|Hlik#b)&TiQ_FqNcYW^CrzmGwf>5LYH#UGp*g) zEgOtaDVQTheMF~lfA6)3R@tIJlf~JQU|>A8urUGXFm+(C^R<}yz*aJLqg`F^zktiR zNq!SU!_rpuoZToMW2FH$L#t?{QYCJdMY|Ly>+G9(!^pIRR1cxVIflhnH`%*eV)gBX zJU>b2z*gY#2JRORGdmGN#|ucizKX^|4>FRk+o=vri?p?U=S$bc3uR9$O1ty+$C)X$ z0npKV!fGrHqd;>fZ}8+ zq+YvL%+U7O-aJXwr@RK8EEr` zG)XaE&UV=I|FRfgUfK zAp-~JO_Em_{&r%_I(Q*fMoN=Rl$x`8je1PH6oK2s8Yy%7s3!6BJKNjAQ{ z`U=e{f4nDwEjN@Q`1uGUD_V8gJ94(aG?L^O{!A-rX9#LFf#PAMpkZB@7mCuooIKiE zH9oK^@Xxc;F^(*~Z|eTeM(G>?7pG->TDKKkltpjFu!PZkF16Y0$DL1E5=$PH@fBur z$MK+vzfKNGn?clpl_#{9JlQMqv62OX=pokCMbxxozy;(BTe|p z^Skl(lxwMY66@rtOuF3M_&4KYrOG%s%UU^FsXWG)hu7xS86n+(XhhRajSK$&cflI- zEFuM&_lHlbHJ+k=pI$dBD^}Lv`u-(6SLSaTe{b1o;f-2jD$BUAjSjSjnlO+EL_%hK zT$f-E)`?H<$#D*GipCjOwp_4|ZU~_0ocKCu|7?GhpDkJDAc07rL_(F|(pfjflTk_p(^Lwi(s7pwa+T8i?u!)#%K@P4`u~@32#a< z$eAjfaVDcilMR_#@?iFB>fGx_qHMG@B!+f(-%z{d@Q92pIgVEnN0Z}ZFrLPyhWexO zuR?5OLBekCAV0bcpGR4B&jG})t#_kLg)C|mvlcZd&Sz2#Hqzv7`d3Mqn$SnY`ko$i zFYbFPe8m3}Y5GN{h&L3;SxzBykla;drqI?%BY@dV$FH$-i zCpA?dM)1tT&mq=V;6aOQtlG;fStsqh(lxq|#`ecJbrxIXuMQEkG^)CNr#Z*FMoNPZ zEaXY5J`=02`Jr!~i-;S^zV1BKj#s3g^jTXbr7j&6rfZvz1V!@_re?6~aVyM-NB*zd zS%tF#=OZo^1NXN&Qnk61&yKN&iPv9vWscLJjlxt00mE&aaRa~ZtxB(Yy3AIbC~Cjm znHnZ5BM=lSmusuR|AGCwXD@;f`)|hi zfv=YExdWQ+r$?Pt60KTj1mX)UN?Sq3w1wgEr>qiWCCS){+83M5c`FmY)6~?(AO_jExIGyIgZ|vk$5|tj+laKyZp5YTB7OErmTP`A;RZuxPfz zT_`-`YLVL$*h>4MeAw_Ki{?kI$lC~o&SU<9!)&mM`8062*@ysIn1xF_D7`*to1jM{Fa_6ff2(B{91?OK>SA)pWB@J-$L z`kT&`i?5Z>RmZF_AfI`$13}-ygGgCf(2=Cx^iHak9gZ6vX6-(hLqX6RQ^H<>-{nT+ zYfLztGl8^8{P9M8vj3ewQA!%=Mu=!efV(K)Ms2dr(q@LEQ1~Xig0OMR^(peWb!z^! z!q*cQk^1SqrsLV}(QO{!$jeF zh7&jbFEWFtt0me=KYJ*2Jp19vcpnX$t9}+BCbMAbX0W$YV>#w33Vk#?XZ*mgS%$TX1pVW7I=S(T0qABl zKFs}>r2UsB4Hr|nA8jF6sN@a{C$D7>kV5!||95tF2HMH6QD7#AI34`!y3onZoaOPy zWo6M0A^SkLO!>LukrFqxti^R1qsVKK`d>+oM)Pd?oTo$cZ}NtbmL3|ujDVrF)MzP;)oVu)RX7RWqPad_ed^U+eIr&3&y;` z9|13tlrh*3a1Q*BXF*;dLFihv0OpzxPI9=U0N7?v6P^?~CPXeavPuG7H|E$3mW%!z zT~(`18l+d28tuH;h^~F4ERqB*PR+O|Xdog%Ktdw<(1yBcp>J;tK^8z!u8B{>jr(e@ zI%3J{N~h9^&*UFA@Qu*^*)#Fg;+!7^WW9n%EWc8gP5Wll-^SM=|D;v>e?*L+MF z>~K=+KDQ5#H;k&E2->1I@>Jh}aekV(M*fECgb1>^y}?=55}9}+xseQ;9py2_b|J*v zcKe%$KO)p$(#$@NblfVAx{ID@w#i)@AwbVld1wBc7-!SEZCB==C zZh=M{&Lz?IcdjO8joxeM?~a~Jx{e`P8gPE$$q`Sd$|857cL_QCLC$P}wQ~gEl&3oc zB#nEi9&b^rT5oCoTX_DnXM%FA<|>EqMOV!VZ><}`?lQQbm#4w|({|;==*kZgzl@2I z#%W?06>VXq$>`5-UFGa{NgUk|%`BPu1tnW(_BASA+HM$Sy!}yu3!-DLXPI^7@P_J! z{}J>2kEF$LNGa&-Ehlu_3HFDsH}xR-9mHQ8n`u>V>ufJ8-C-xuJXB5@i#8E6PtdjZ zbX8q?iW!4nD)S)`TBd;RcygFaaKh^~|2lGEn9hfm&KJV`6W(pKaCkpu_rksul*f!y zn2)0P`j=PKXE?~nOlfyl753?Ji!q@Iu5Diu6&!IemPxbXX$$rh9WP!k;_HUP>a*Nj z*A^8V`%JGm7|Mg4Ix3h$cu=4ZOfU*3>LmV!UsY}C+T(f_H70x2Kr({=KOOc`uWCJ^ z$MmIk%+@oJ?e4)$D~gcG1M1OGuN#2gYxn#T+hR{POeq$2~VB5j)YGx{5dyVCcy3VDTH~R8v11%CT3&p9|dH8p6?Gu-m%y;?Ak5nta6|n&2#i%no+TVqH*i(FY z!o7Gexi|Y&p>I@I6pEap|KGGv;(ZW@aF4$JBRnz0G3Ao*PnMMQzp!S`7tFCsV&?GSFT@ zx#y?~_1j^-a`TBH(ho{yS*V}zAJLKhdybcXnI!&An_tLe- z@|eFckLIEr9?kJHIrH=Xgh>lOFUQ2h{l)`8Ml-w5UQfl{>$D^*N}Ndl09zL=^^fjE zC!ob33a7;_yJBoE9~6XOek-m{U1*$x^(O?*wGez?RSosyv7V$IchS;o{E=#`$tP zt{+RJ7INy8NH(nz&Iu=7)P#3l*b0-bNpQj@4L+A=CV98wi5!VnaDc{0)Rh5<Vr5+>7NaleF2oflQ3%{X^mSul~nRe>f*AXF=E3Ghw`VcvY3qTS{v zp;E`(=u`+bu4O7TmDA?>Dd7{H{3vqX^p~#YW^C&siWwTtU>Sc8%0@CW`U3yQx9oOT ztcLBDBhSvx*{2KvQ>pwoBVlIs_#Np;>qVH(N}Po})J{7C2-hb};w>`RAiSoylg;?j zug=|w;%@k{cJeAV?1%v;0D{4pLfD1m2pXeMn4_fy2P;b}^MW|3t1Pa)X4k5-7sHBE z9cWXw=NXsdu9s-(q=4z!>Z+w{mCjLP$BQTbea)}p5PK8>PIg^HjPo|sF7+IBCu)zD zSq1RJr6)VZfPtH((zPEqKvv1=ipr7L15iU%*_`)Y+?jbe5-(3eYHH^_mP9m5?8in|kjD?i&(nzI^kQa3z4D5q zcB{&$3^^fiNDlV}qz9Av6hvdT2uf2h(9%0jRaGEr`%)fKrVSiuDQ?0gNS<|b*smpP zGyHIzvo{c1?0Cby{Gfs*reVZGc*eq+8}Dhb&3bv6BAqPph5R-SCd+QDcN%o4$=(>1 zZo3xjNF*L;1W_V%nujr#P7tkXH^EbE-I?^@XAt)H&FsD91vlH&M>^+q3+U~BGMQC^ zz?sXGVgHkVmUhz|(u5L0QNKYq~HKV>@JcLZuocN{%cW|D67Cd6~|%DM_Az zGxmWahGlrWA?S`~G8=6w-sKmAA9JXdQE^lnjYkznpvz;ydl>KQ?~ZWi zO$z=|fx)kz<^f&Z`AAq)MM#kK@HR&p37#XtagENPcYZFw z=b3M7O)Lqihg+L`R%p0S^wL7YL)Ml9?`EsF_!WTTK z&>k%IuE1I_=I-r*ZOiTKEcek&3_3q-F^KC6yu`(DXhzxnM97@|BgBItvVFVohoSia zuq*Zn();D&=nu}i^jZVzIVnIOn%wce%J*90I+(ldF#X4Q5~Uk!hIQ{m@6^<1z}_bh zH$H2`WDPQ|lT1!+*NabnVDhRbBKdb%KW@7goGj(S5nRMh&(qC}cL7bPqQB z!fen#tp=V+=EltFSZDpdHwOh%<({DC5I?G39i9JLBahln?#(ubxb!J06Ah-USV3OK z#LY^JjI#F{vrN97{q=nzwKg^d_w?c}_PqoCKp(EA3&g%so8x-WX6Pe&mqP5qH(pV> znK5RV-vCcG-zY4p@RYb4+nMgFbJ2?AZ`X&};OduYb6>~R-rL%l@N4ZET%H*r(lXVGO|2-5zqe3(UO=D z44lKFIfX|&z}}7B`#p(V z-B?VJ9Id(HEAs;9VhLv6JDdyoRE_$X=8^+Kfps*o*20q7Z6cG$bYUmYJiMM z$b-wZBDRJwOhMEtQmMwhTMiVT8q*ljG~_V-Y|e~zR_^U?rIvgd9MGH1i%_(JruXy zq0++Wi00`G6(%zLS^ElpBK0YAZvnzWF*=q?_eeh^FCZ2g3Jd+#4V)gum~k#uzyqgg zx@ocFmufmhRB-k`y{BNZlQUQjloP1c`Yvyr56N0-Id4!_;FjI*;$RZpA}KvOwjl{& z2fK}Ifbr02ab^F+rbghwuX~$mKGOanToDxrW!1bK*sUM7&*rpFA!KM*^1 zH%mCxv=D1#eB$bAuJxuS^P{rjkWR|=Qtha7!bDOnHT84HJa*Cy$LvdPXsqgf92ngv z-k1UInYP| z!*&YmHDZiw{whc@lfOr;2G0@hA@3Q05fzC)oQuGjpNojIiNdcNs9CDN=$G4IgKY{P%@U?gygbJ+;-(%*9Us%LxhR# zZdWjQTMsBAKCKdZ>G;(i#cXYkF%SzSIrP?MzsdpCg>mWV_%lQrgKTX6kc_2+Jvi%S z)B{)tOB`bi@?9|j;8lT;`n8sp+66co1=3%m8#>l3Eu;y3-5LR?pum*cAf8E~-|l8E znu3|<7+uRg+XrqtOjrQ9_F3<8?zNx|iTYR>Lekt}6Cx;=%NK&o-m>#YD7#rRyau#B z{0Y7g4QCosd`oLZF`;rxE5Kl8>i~8@}e8f!>*Yfpf!KDw@*B@##r|>G! z>%u|~oLJXGeEYhu7})tU1Uw0R_8KWsgYePa*17ty+HRD0?tESviC-TyvHXscC~Uc< z6hCYpxB|PTv)ASxl0RPSEx_y>m{}tEUX@^2*%DFsSYD7lOg+gzrY)Swn6(gG(FzHOOyoec#C*t@= z`x{{>B31|m=lz0C?TnL}mLN*bo=}TA5&VR+fWruna+s_QV~pIzPeMgMrwkeDv1LM` zF)eh4d3Pc)WRCG>=D^y-D@YF1y_gK{yWiSx{yIH>?zpvrVzlT&Q_dc>^oOy#RG+fT z0oy(G*WYTC-c+-Oy+1XU%T);K9(*2obqDRD0CB!LKME5;yGpciU_pR>y9c(~@h~21 zCElf&5lsN%u7&V#liR?0oCJjL2^GXX$CeLc$dqM?Nic;wAIdJxj(P;Jwo3N(8wp-W z%JFx+;t}cDICq*hJqM~)uP#<>^Fv=t{*r038x^{0FU-Z>iCM(0ege^Co~xCxNu+ghWqrt?4YK4`%8t-v)+i3w<^HTt&MGba0oJ~jYdanj-Y zjYvd>l_Y8Orn&NlPsjc*IB&$9#ioA$Ks7&OtEeY8h!3PMKWdhG(5xaN!{9GsnRStc zs#s|Acr78}sR*kSxvOnF%K#st3pP*pS=NMMA%NqLkdbbh_uQ`}lV~t6&yazS(&XHm zcX4spb{#IDhO?1-nN8jyIwE4j-&;Sh8FMO8(h)=dGbaDha*TAGH^dXETKBltZJLy` zYY$9q26+VjVxQpHHjDUM8!V1LiHg)pE1dzhpge;8!O+3;&2*+uXxBI{*p8K%#PSf6 zP!%-2QMgLnaHAhjP>g_kzw~PiWhGf4y|+xRo^wndXan4%%4*S5Z}Wehh06ywzO)hPXDf zGcOValeKcyd|2YAUbsG+s(^YP@n~agOiN2yK!YrdwKYrc#a1KCe)oQLO-(QyQo~m_ zQS|apR^=7@r7i>p8qqp@OhFarD5B?)aa~IYA<~qdj2R;vfb#NkLuX^dWA^c3<%x-r zIh<5$1uZ(-7vJO=i4)(pMM`R54C*eU11eFD=q=R z#`~1Sqm1ebxb^l87Tab<)oI8@^SC>d+rXNxf&-t43UJ?rl+}0kYE!TJSa}9-(>ZDp zgtWAT2kbX+`z^42&fhuLefE4y=Wsd$U$)uw_R;~oTXn6o>gV%;JHJex59+6;b#W?b z-af|tQ%)8UIVWiNZLq#!)%$Mmenioy5F-2%-s+d!;Z;X(;=wAG^?|CE`mdIot?3?> z&r##J|4Nu<1%tu&e=gBU07^_H?(#0zq5Zamp4{v0=)ol7aSU3c`fzNN$OS|S=t*RX z7DOI80#T!&izNUF425znDv>_Xfns<;ePXo9k23uO&$g|0&$07E_zFCY>Wor2!IHjQ zBqH=F3FYG+r|CzBF_9$V$Pt6!wDREb8*>dpfEEH+Wn_f&lriTID~tx@6JiRQ`)nkr zZW5iPAEneY%XoyJNT^>l$L|_doH5}1{xpGGGbAdJlHQBxhXiY45R(=&MXrnz=o{1l1plVR!LKBK zzZhTas6S{b_ZgdLxN{PqM+v z&4~Xi=-Z5G+e1o$g)0qrWkG8khOE~F+@6@MJP%0Y5v}Yxq(orVE64OR{{`)dykn`l zir!DFH=Cd$^@AW&KR$S8;*Mt#$)~5ZY27X!hGB~^Q{6bYs$v&*AtYL~r?K#H1zw99 z`>I$(HQu=!0s~P{?n$^&tmucdu{mrRM;6nGr}Cmwy0LHtXD<$-AnYLNnt zgvh-EH|uN|{ief0@Q(;C%J}iFoyqhlHNj5&zO`-|`O(S`o(%yZlm~U@W?bsl@OB%| z4kQHZOT%0l!s#1vApswEy?AHA?KBWzi=cXTLy^7wu`6vI3MkNwSQ;_yoeyE_gI0X3 zJ6)g}u%iL{29Y0@1f!Wn)oOa7;eGja@LD|$FL|>efgFcNFp{6*iYv?X32Kh4g6*aOI?YC?f6>G&HLtwzgMVz;)qh-a!e)Ci(xh0O`d30pGai%GID!V!2O(u06@fW)}~oDD~ZCxBU(N{Sh;39MR2-Npv>QWT4B@ zMER`htSag2=67PIhN01ceF|VL76ulG)nPDIegY)zbH}l%kkDYSu-7IOarh*54a${g zop1VL+j@vPQDceH#& z=)PiTy?FVjug6xnc@)}M^P*q^JZe9?$9|QgCv6B)`eRH zkNACG^;3c*#;txuXU|+S;mOCyOrXKJG>kX!e3_R*=D0H8KC3J#(21v3s~Cm)!}cDKONitcYOA_{xtB8uZL}ABQUc$W_(xl zPf)D&ezGTG8+~SWFCW`8%0L0_inQQVwqj;IGdd6hSyU&OdP<%v>%<_oBv^pGanbOt z7*TB9+F{#R5PQ?fil**P-!&WG+=Z}V{o}WJCJNM&watS0xQGtvsTusbG-0mW$tm{C zd!uSr9ebIlC&>owxLm6X0@NkXC%thI&@CG7QlHo8pp=TuoE((+&ZzD@Bezv3*jK~b zBPooI^=9hbuRN>>WL}B@g}dzzZyMe_a-do?$jG=%w zwL~#Djr1?wXwHWQ4$r19x}wW*^8m>%ynZz%z*P*)!ge^3tK(Tn6C zQ3s}&8*#uc9->yZY^geJ{;4-CQCbm+7C?L})eVtnXJ2uf z&QwzujxW}Bxw%#9_UYHGIVfS zx~2Ih3+thG)8LBtz3aS`14G2%VkGNn2w@<--3x|pz>)2JnjJ<(_|Q%00Q#I*=C>_%E)&YYo6-gEvKJfZg$H&j+*7z<4EI( z9UCgs!fkB9!G}nhmIdUe%Cg3kX`GOQXk(H{Zv5C^+P<$Vmq#Vr8l6 ze4ZLt8~FxZY@%NvuRTiA8SZLe)AO0~uSxbD*gWG4_PdsE#=$!85(&j1M5g+*M-v~3 z9h!{1>^ZzPKLYe?+R{n$Svd2C?D1PWRRuW{EP-YG6yLx1)-V}#}fnYf^n zKoGXUUz-1v#NSPQ&m&mY%1mZFF)Ym{h_yVp73L2wMypdb*H7j|VRE_8ZTJfM>5O zB%U9vQRJq8IX;SBu=E>Z(1golU1bQiflCHSi+gZ=l+Is3-Smf#QR8F8*&8lItAmQU zN3HJ%*pm@&h7F9p}DO@ovfoY|I=WU2K;ijU>;r?w3N{ z>(~i8ms#Sp(NF@hW6a=z(s36y+ySyKDFta3@gQtRl4WA17A@*Fl1P1tD~`89hq_V! zGM}mbhrBFH+nL_X4ZMxeBNYEg{Vqb@sabq6WSD;^Bj^|tbh~CP8<48fkMes7iJlZ~ zxK8rI4FoF#J;MgPk)Qlf43-NA58@#wa-`b)`7HBu2Emh?dq$Z9h~vhi50Rx$@1R!h zstqcfe@J9xAATJ4(*X8AE&A_bAK-nJ1@sbL&V&+JP))-|TM;3s$$*F^zsJ&qLAD%N;%%>U%)DEgrLg%S9Q3ZdkN-VO?p0R1wPY+7P(%=z_GNNy9Rn2B zKH*i@6nUPlOXdbU<;hMheC?bh!+KS zsgerPBTBbRa{$BNuNaCk)qN|Q#?b6eZLV+-2xs%tp~-8KzdD3sPnj&{PlyA2d2qvg(RJoFi=S{{0>v7`@c11tBps zp7}ml>f>r+C&llv9h9uCk4l2*xv~fxQzv;yx(`#A9plg0RX61dNs8oMixi=FS2xJK z8=HU(V!gX4K0t^KKjEmGf%JF+!A)SN&YLzq=vX(*RtIPHS+nakRl7xTzrRn*H9Y#H zVI}SdjE>G^1i}kKHwAhK(YH8WSE{G^6TA`FsrX{hPL;|{6=DAv2nP;3T0RCe;}AxL zxYJScrJUuM^34%$l4t7^Fr6d!<9)-i`+D@};<(_ay!g8u9Prn`tA#%GjDvC*lRuNa z{e^?oKW~URhl5R1<$V(&Sc2B|wo*p>EzZXaEj>hiS(-DkVaaKVI+p6iGiUKseR1%@ zzuu&vyJf|DnPK(b!$RSoz<8kviJFU8RvQ?v&A2!JPLJ*JPl2&AF+h=Bsam6mMLA3| z@T0Nr2vF7T2D#502!p4d{Uz(_<%U?Re~fn$VI8;iK&2-#orS z&@UWNa2R2-AJJn$rU1kwAAvI0;#kdlD5e>V>D55+TJwI^JtVq$?v`b0#e!5*aBRYP z4;;3Yt0wX7YY%DOhE3%iONl#Uoc!^-v^BUO(`A0p1W!!EYN!(w1hYp_l;H}si~_`V z2*~wYK!-#B1WhW|lWV-~`YGDyN>kQ5v5v6kTxCL>4k*_yzh)=yuE$K(#hV zBMxc@=2@*OIl?2$#!Rh_c#Z9yI zEm86xj{{xjTGc@B4Wud~Q8s!~5Mq@ajBv-l%`t|UPTMT$DrrL1J8|GN19q~%*a|26 z_qT9gmT#Awgu!ue8(*3$%5C1~e-2F|LsB^y?_bM=x zY%Ep&AfpF{X%QK~+>!@im_~gLsnwqCXl6N}+xRMR3HRPwEzAy@ZT|ajbKDOoD)0@N zJ@u0NfM8l(EoCgTY2?6#Dp#xf4QU^v744|2=IW*kdl!s}}$*v3QbD;e|paErvX)CWKvQ za6xoQN=RXXHkxl{jvNj5=wH5l2MrS(y!3^wgJ%7oU1Y7eCQuc9sxGV)xRMA!=}z^Q zZTm+g23&`6IPmeb?^`;cE7pas7;2@#UhiK=HpRO0DQ@eU&KbG<&Z*P|re>2kpy#%K z-{4~r-8S4>ypkyV!i4#v)jeF7VAdG70g$TgIM(s|ly6p%?T%sF8=PSbk`u_HUpf|`5wk26GHgKQNV{mod7tQPW+D6%A~8dXIp2sjx7Q!obE!;My*&yPRD&;IQ- z;4g|pqcmpz%NoQh-SgA$Fm}vE0SY^2_oZ_6dnM+3}8c2a>zNkI5zrif4NwTwx9BLytU zXU%DKQCrc-8O`+>n(0BSQa@3Cs=qSTX46PwQpa3@aELyjTxXyI2DqpSfpum3Vm7g-;EbfT_%z^r!cU~$P5%i|*dBSy`SM@}Zf>S_g zzCSkNR7MU8_mPef38Yteu4j$LHfXpA>YD0S5!w+L55)QNahpS$EPVSGO~ZsOwcpCB zTXsG{O+zGT@a@~YF3FF#QCqIEJW(69fX~p!az4>8b9;-)qH2}ZAv4c5TyH#rcyFP;7`reeL<1kRm1nNWY zl?C%^X$;Krt%jn%58|O51a{`24wCqlL)Hia8-Rg=z8Dnqg{f(=yR5zS(x+X{^FnYG z0%rOh2J$&-YG}i6>BfGpO^2n109M7^6O}3-g42$t(_G20rOtQu*SYykcWbVF%EQ~R zkVY2f{%mf2N$szm`~O|{9KpxS?`t6Hyzg(?%ko6@Im7b3@BK#~HBc(HeBfj^E_@fr z+i-l=@#dq8zXSwqx{rHOoVrg7=Q{H)3P?xHs$4hmS{~ z%x12jaSXA7#_}`?3~2_*rP1eXmg9ZUxkvzr;)5F+nT3IWidP5fY$=R1<%@TDeCJXi z0cM-TOCDB82oqtadP(kfa(m;r8?`2cPr3&}s-tpsRBdeyc6&j5sGk*8NBNww;hDau zM!oE|SIy`krq}b@)i+iHdKuGbWDs8KQcW0wJTBRY->sS{J7me#I|mVm{ijV{%CHBU zxKFTF)wJwXrVxpGR1T28K12j!b46R3u;ZFY%H)<^{vOr*WJdOP_{PG`=@Xrfi@{?v zDqyrXU3$lz{9WU~&O45{A4*5J2Nxq2$dPC6T4N8l{$Ia&S8cl1Ox|Gt98zw+BCy-s z6vnLkk$7c1nSIqfMAcN;hBej%^b%^FJYtD%zqgMwlIKs8mndbd51|z7^7EOeKPA!M zAMoMchcUb$h2dvs-N8UqTdT?ddo@}~FeIr{JRRi>pORyO$_9Abg&OXi8t)%Li!ni$9#+ZgsPGhVZ;( zql&${Rk40iV_4?&U>vmUC#KLa2BNRuIi5TCa7AC;Y)@)Lt)NEwRc%bKS9po8>GhK? z%^a&x^p)ix3~W{(-e9NKVpeJPeOPuE2H(op*B2;w6Z`A^4{LXtcLX0WKPqgyuFWNB+H{Zvh3GN3ldNB%-EY6Vk?hWW z0;H22xy>tfQq*AwdSCC8LT|~=Sdw#tzG*DVwQY-h$JKX4bIl*hNan_fbI`C{*`xR} zBO0tu1h6~YHoU*JeN*k?>_WHoxk|s#{(GZc`0~9m%j14!?-hS<%o|4)SxcsUGoHdC zu-Vdne*O&)5CQm+Ox`5Z3mHJ|{RPM7w!D1A=IU|jS7HZ2Dw91wxxvO$UmL75`<-aQ~B#1PrLMPY~z)&lQH1O%}Q(0Ud=k?_uS}%w1ylIz->_g(E}c2H*#zwX>I> zmqg~Ebn!wuS{4WgeqkUXvJBeRrWSRC(GHY!bI~U+ec{X3HvQPx8ax`60MNo`@qVXR zFz*zsY&{Cp6mg@~;eRvE`0#+xg-qQ$r|PqqRi@~3nNZ*9`;hG0%+tc_#%R5{osjKc z%e7L$-kGU;4|gS?ps9~^c2hUm}Ac8X;+zZrYbYPm4^ z*@}be$kk3a5L|WO_Bh?#R-Cp@?gwm}(7I3=t40pZ$c|z*p!dKH=NvHtlhq{eDBFNt zwO@LkL;=-(-JUUoz87 z6BJ`nkY`bdaIo}J=g7@L{UhZK&E$$3Fr#kHZsO#++D^EmoTI=HYO3`dCsi;QtX^-g zFkd%HHm2#dswPsfY3$oDaFfODM_60;;oc34fI`VCH9?^9Xt9k;nS^BtO*QaS3j-Ep z9cD4FH;v~tIDMbhdW2z+WBI!T4*S3quIlenjOYLnj{<@+`s{_T_H49zmAf5XO#x|M z$^9a7u;)sBj(!4u>d$IQ_dOcH5aLqnA^RZgchIn-kY?ydjpkr&lA*Z9)c(54s0|f z5DOUn9_Bdz`xfQuY8v^BYl0l!{LklYh~`)V%R&w77j5PMEreK@&w>NvB)s=rpZ?bL zHIK!&rYJXfpe~P~n~*qE0vgcAzkckm{o%0|k2G2SQ3YR!So>otmEd_BX-gf-mv{ zBVfIRuVN#__x|)NLL-Z>4MSSc(M`GOO3<>+3)0S!=o8l)lFuC+CF7AIu=$Wi?NE#I zazr2Q?D=me()?ewlZ#}Z1Bz{T?6n40%uVt$H(#M&H43QzAVH|ZUb`X4=4g?`%mj^~ zBslrNUrt5F@M+AcQllb5yWOZg+usRY%A7Bil{!*CSSK$bP@j=^fej^Z)+bPFJtRjz zmq%YNNh{qpEJNGJ+PYUZt?}l@OyfQnl7wKcMF)grDF=J`+?0`9*|=<(wz$FTvfkO< zs>0oWscWuP_fQg#7iMPB4k;|O6fb^{essjQ87Ey<<$*pJw_y)o)eJNp_l99YYTco* zZ7ErLn^O&@j-NFeM}dI?g$t|ZWGb8r%33Md$ycjsyDTKQWD|$)zK*yX7{{-6jNkT$ zIBZ`)5y!*+a+ooo8KU%q)-h69?H^MFGEk3Q7bdBr`qh8Q2bt+7Nt0wA`mD)i+7N6Cz>DlpvcY9-z=v!HcvKg~)?CdFl% zz*oH2=$G!58CcCN?K=sT1vC6 z)wLq4_SPSZS8--0u@?)KT*oln=D_t#?IA03u1vG*I70hX$MggtA*_Z@0H^DpzpIU-1#5j^XSGm`X+rzoqS#< zMST$LKTt#=v<;nfl9Ok|P%89c`Jf-~`;Q>?NW&aNTV93O1vO~sY?iJ_di4igIlTE1 zD5cY_W1Z~&-l~{l1-Sya1v{U8L-0*AI!iP_#|M23eR~$rn5Ty?)PYH%WnHhlOCmzL zw4-;5>{kY(b`6weYIXrT=#j5w2a~L8sRl<8dOLUu`pk1?IJK4!w>G!WdeEUJVKM7B z7jvWLOdk~R0b1G*pXSHPpf3RmGJ~3mg?eLGffACB_g`6x&yA!mudNnzOwy*&vsmSf zc2)?kNT5&+>vzOMo#2%B{2X1wGMU?VpQG4Ulf^<2m?wmVBQqwmusw&LGkRTK9EtPi zTLdT{m&1dI%c3v+S7-^M#6x8qK1 zll!mC!y^1<8>*%J&T@o_q7f)Wu9diEyoI00dNXoNvssd}Tx(OtqdMMyh7qPB$P{_o-qN(SfWpzIO3nlYmO}dOi{PdwLa$IIuecZ zy#u=@4EQcJ)=o7Q4yI$6aQyfUW9T86d$@Tok_bir7fbnJkCkd1Q>FV{q9 zEm2n2K@{4@3YmNV?7+Sd{xuICHeP|T|B?!!7nAu*!);gT? zHr`b?FZ};n0KT;+9VnG<8=nqp@16IV>^b@03+nCjYUbV%oonx=mj+vj4VF7k%IEea zo=V_y4_gQp<-ox1ZdTiX*fF%qpbz>gBR_Yyqr7D6S?`*`6)(bs;$qV8ziKcA6T!57 z5=W>Cc7~@e44lyD#YEa{cpk`mu)?Eg=p>q9xRjL)Qr~G`5Uyg`1+ezmp(0W_O6$#K zDvLC|vgzBvjVbhMMguXcSyCKT(Cy->S(&p-+2Tlg1l!Y_+z zG|53X1gjJs<0Til7==Wc#`c?b`Py4S3yKkwd8(o6;!K9wCj)ULVg}K%9JYGBUEAzX zYxB}%@(~d!4Kew}GJR?Nq5NUdM3iB-RU(kRNki<>+nIIr(b|t1p6j)d=&UX;+*Ci1 z-!$rOaZS`jG?yK~8oU%6sQ_y|XkVYvG%BL0X_y+ci8Jtu^tRQLN_WEA*_1N=3x$WZ7<-+T1d3nn7H+-Q;$~-6gq{}1G531W zp`n7Ky#oLcf`!+(s_OlE&5%QA+$7D;1r+wcXJTS9HiB?vT|`b&FVC#@0-DrHR-sM2 zG-?{BezX~x@)E3Ad%%ucOItCvP8>jWdwUd=+)Z}JRQ1P?i;O*?qW!LOWJ`8KY?rYf%mZMPZ(fxN7+QgH1X;H}YC+1spz zzfc3;>Noi2c-?#N+j)tDjfi{x=hJ;~LBsx@&dui@gJq!x6?*AVd)r(FEBb)XdQf-3 z-W{T^ybmb(e?)zCSe4Hc?~5p)2&i;}NOw0#NOyNjcQ;5XE!{|WbLf(m&O>+Sq5Hr+ ze1G>omp{+L^X~5K?9OLrJ~MNp_Pf%ugGVeCR}XrizDY)Whm>wrsdO#7ddatBzo|XA zz2kQuP2BJIV25VR$Zw1M=B+<=`&w{zEsnjt3nn(B9)WQ=^YY;5p2x=9Tfy!_y&Ek) zN4nNsf>STz+g%Oaw|d`8OG|HCMX1x{@p-l)V*}aE$gki z;Jsa3;Iq)Nm-hy`-w*ZH^8_ExC|o$}&1Jy{<3E0{2)V57^nxK?7af%zu7g?s5rTf{ z`om&IYgij1@k3o*H5PRAq}MZ+c@m?{dFBuPMS+($EYqeX2{Ew8q=ktwI}`WG%F&qC zWG;mqwkc5}VQB~Vh|FXS+IHa9;w;NUtS2|vv*VlYAHN}fV@ZrETaBz=@<7;I#j z{JwMN{U>d%v%jDIeH~p5uv;e6^o#p;pEgmL6p|Pe!y!i7#a{iTSnkx;>Nad*&8Nf? z*8uv<-MZ5pV{})FFUcCZ)CrO3q6@7>an}yNTh!U)wX`7*f<+2wop^Tn$wqB;&EI`6 zn#(ZyE4qpE%WU#P*pDpYU069{;?1zeGWDDmRV3y^;nc6Fol~LS-&*)8wwMK#ep7;( zb>jyseM_tsK2tI29jaAFqK|$j5J(CR?LXW5rZqA2sHsR@i*fw@Ij~CD^)0s+Zd=)4 zoQgDrok_7n`hnd_Ko8VfnO|ri@`w5x4VwQc7_JOnoLGQ2y48K(t)U%LTImDnv0d4( zG8<^MnsjFwT@RSShnag zPP)Lt^|ikwLW_wuSjx|_YEN%QZ`$nb1_Q#-E3Y(+# zqxsPB3v2`K^%{CE+Z+j0qhGC*K}i2$@)z6qlQuc)p((2y)~T8B$soa`;vEH@C_`Xv zw|g(`N`MrJlY2M*6ML9tQa&dp{)!xxXulzmQs-FvE`Q@gTKijntDa|$9!4ko`I*KC z{8fSl;f=kC%7Djz_x%+7xH1iO794a9wu6x?fEnrR0;eA=YiHE0mD_u4ZHUKY75BvZ z?;+4yvb3tZdMo1JwCBAGcLAKcLwS%4oBn1THCyrCaz5{PtslJ_cJw}h%-Y{9uj-sY z`(dDLUETM1cCe%4p``9OkSlUpy#f6X5 z|MO7dM1bZxM(Wt(dhBFkA{KT&;(ZSKfb`oO7cSK?$3j4)l=qWay8ZRE_xJru8{s(k z>Ls4z7SUh}6#U2>g-<5$3Yu1cPJ&?_+#Gx|9K__;sGUDPs97Kwcob~zXYY@x!*m>x^6F^1?2G*%w12BZX;xsg1i*SP=j{)Ve+G)}cT$ML`3ZH9Mv^HE3wsQ4LZlQjm(@~qvKXjl@%8PUj*X~5i)}>1bp5YZ;<`^ z2%3~+-%b3FXR%y&3Nvm<)kEG##>Cym(fnA!sWz~vPi9nLDRT=#k zvoXRqKF(*BiJuWqtD~QBVcs~l6DxfEn;hOMPbRbnNG$mw4+U#9vDcW`axUs+DEO0GjgtBbShk)+ny?offp?3(+%zkx)I zkIjqgWr(@`y;q@d)(JK~d)bWZ($5z+UrkQ|+ZHIzlIKJAy{3&?gtVN0wdMg?`szVj z*l`A>!5p<9e(Sq#NKkhmnlBvj<67uoXcDY-3R1(0FO}yyN-4`bjru^_8#W0NPJh0i z9j(u`15lxu9=RJjzBK-C6j}e7OPDacWIph~fg<^Kr;z7v z!CrdKUHb|6aJBG)Aks?N6@d^URP+T$fG9|Wh_3~BJyEO&+E}ip|0Xo5e}`t*HNv^O z*ZFOtkjs^SI*pm_LjN(mCD1JeF6&_lPvELpkZ7&|&37;Gb}dvu;|baUJ#0CZbrYn& zBdX@vmB`}HSv=mzYTK8sJ`T~gBZJjh_5H8|UK?JE=^jTm`+*8;e&_JGP}y3z)ScFT z%Y|_O><}O8s;+_LLVd-ElKA z){oz@DaWPZk-y;O;aqURkAsAUI2L$&rTr`aWm@pA15oaOQW_fuZ3N( zsxc6sPUDfLk1{BE_q3~Ok1g@c#(_I^OGRsL-1IyC0TEqOVxDqWk;*vEg{WZ+y4saj zN#R`CY~AhtOhzxH%A#fxIF(#H5jm zbA+$KQ4JH=)4JY6zdi0jh3B?&i`@ z{u?i^46b%vWJkj98;{Y?4(|))vjUlEgEjEhM*?;Jrm)J^n%QUZI3e5)_}x9PExC@! zQJ)tQLeKBC76mR+&`+^OUHGl1yC2`9vD2dwxnPZkb8h_GT(0|;5P)-c1WzeNz2M-U zfWRT2BluI#&II=j={5U*+r@^(NLq_HaoMw0L7)pGLIqyY6NP`RTk^O5(Tyg=RrB64 zmxyS1-BN3#-!#b=tGdle_U&3Ou}>9W^fzLg74nNlDduE0f_}QUDC`(S88GkMSi*Jx zULvKdH#9(;pSoz?!|=fN>OqFsz&wRgGc+~=5bzps~bM932Sn~ zYIfFXHu|10WruYX6H|)*Eiy_Z6|C#mb*~Kx?YXjKLc3jE8H$9#-sUKsqr@U-D`8 zz&+$r3MDC{%um|!G0~}tWT^e2X)L<*O6SeTa&j`fv@Y^cJ=C0GF?=wMKOAU{p=-lt?BqjO9Xu4C>8EKya`N={277J*_6> zu0k1{;8?UyFo6#YC*l=w$;6vOEIR{NNxdryH}AbN`IK(vwH-0TKeyB7hI)r={L7PI z{#r$%rnO05)=e)YHU_w_ZIo3B2M9GApi{0;9YS>o|3>e)%wvTpYhNAG~K}6Oab_*#thn zIoc!`^gl4P$*QkBsP_M``5(F!&BC2Iu2_G(6E{elO{4`^p@;679zPIWdxZb{C#iuK zMFxxBy+i)`!8FlRyK~_!7mM0XEz8r8Y?XZU;NM?;pRc@*q47C#Sq(W|#WZwd>s_x% zIO^rrkD78xBc_#bduPeG#fR0Q7jZM3He8*UchGw`-@DxkJo=ttBhe~GQK!~Sfr>ef zMGdBVFEn#{M39|E%i@XC=<`ZLqr9AByySxgt68OE-CO4O|M|SNjwIme++&#~GZ$^; z$;TuLVMXcZEk}*0RF}|-gNtBXQOUm&T(`-I%V-*casu{|f_$pPbR`cS@OrRj8D`7F-w_3&}(}v=cwS(J;#Ue>{$z^EXxRMG;v3 zSt=pff)G=n6l7-AMLywBvw!%olk|~WZK7AM{TJ%b2<<(4&LcTX!Oq8n59+(x2D3WFROuh)?G}-^Gqk25vGeYJ2TkCt`_{c%9$zFfU zU!(I=d-0mivZ(JBO_|+Td(6~CS5j%DwDtad;E>4eT%sqYGHDOfe`nkrN2lj%RuCEX z61mBx&yvu;yPv+?w9lQtH*!M2FZ@=Y!RWUi&gHPdXtMN{R4hvGsye81KlgUa@6p4V z9{JayTXW|f=)hPc93)foj+oN6z52O)3h*3XG^C3b^Y7eu#x;FJ7p7L%R9*P9=f|X>s8R^9Di&i z^BK7tm-o<68gX0xF&1&h`Lwn zEm*eE{XH5!t#n=*bT;}Q^WbAX>khY&s{&;Un9BmF{;{; zWn2RKh0?_JfWR-2BqDl> ztb-mxGHeR3D_MxWY$sPEKm#dDiT)n!+?j3C>itPz>M`7s6i9lQ`2ik#<=b7{`_5Pwzzk>BtPt&W`lEA}y6S1$a0KF(sI`a=~?vjIP>t_n&YVc7=_ zxu`u6$m||vPs{_%js_K_SmWdG+FxJh8vJI>tFJ0LpRkW&&Ojhx?Y`fkhVdt`fg!k8 zl$m^m;h%$Wtd2?;IYOoFHj$h*&dHHp!x&M~hCWbdjMoH%ACy?_HRdPU3AOZBahB!- z&?e?+(HG zG~_SX{-kgRU+^>@9K1&``e(NpdcyrP(i?ZhUsfmD@1~t`)2L!X`KQo-&+UoOv4-ke zCkFR=cC`ZVut#@x{r3$+CY@wffS>iqn*YaDI9h_dMngTu%flc?jyY}X4y$?j!oPt^ zgYDe#>K~!agVE}!&47-Yb!{7ziUZuCBoH*%@`Lh%JEx^7XL|Hw|AOXL>p3*)1cR0I2#JZxl zebh4A^sC(Gv09SmY{cCb_l0#LN9!(p`ybL8aQ2ExDnEbxZz+}~zmMgw(u_1^-9CGF-V;8R%%#;P!0&yvqj#yE*!|6KEl~2-qOTdAK)W7v zAkHkkUmb%pt&ICj%bUpV8PDD}6~az?b%AU7{V1c|aM5#P`oT5IXBp^6`Dg=6C*jbP4JQOpZ24{3CCT zQ2&He^}Qtp{=ajjSi#Vmnc?~B3B~&;E{*8}A-wO}RY;QG2p_KkCdz4waVM|e{*e8I z)$wTQk0gG63`u+7T<YvVxACcYW4$ok0*&w|0qx!faodMI z!#BeyyEd4_&?DR;=Y$s;?h7do2BwTYVc=3yn1pw#IVU*-9OvY7>9jLgzBasotM~Sy zYc}$wCn_$#H=i%8@s2?^9ClafUZ-(?qR5OJvWt?)_tg*0sdohG@TYfJNTEvvJZH(m zNIeg*6o4R{o}vvz>n&^6PMzne`3+W%tqH$7T#aG+oGC`wuyRu=GrO3ca{Ng%`n0Y> zzwC_a6cb#Vn@&JPqOj125iYLojxsBvmdx}={fkHeos(`PE;M7rOCf8g-lyR;;{(nP ze+YSxp4fl%$-nmQ`QyLS3O7=MSYeAwvgFyk6xQHD!$ONNO~DUNW=wpvHg!WbS`6V# z-+Y%&w&t(8*hPv>6OZ?UOuEwr7tW5$pfk({v3edU3*H2wKM?TOvwMS@Hu#UC=fnSe zK{hQj<@INr(U=usg?R-g95CrIgx*;XHp#q7OD1A4uMC!7TnkeX=lp4$wMwNAHCtUf zEvXb#uzfaah{!Al?EN8z^W|{u$bqCzL+)#p_0_(L$b=eycn!_l#JQW-^AWHjD09nY+n}#$ zb6|#=$ogqmRn=SU`?7DwwB7;1={iR-2;wA%L7R8Ci8uaE4AoP8cVOWowExL&$RwUO z^+n%$@~bQ6?$^)W(Glpq%5)`pTK6v`2-9Y=?x<~hy~+w`)~KSgBHqK)$yAd!d+7ac zbBJx?58aK=yaS|_axeMLFP!!N6xS40* z()yf;F5j7XhXS_uVtoww(|$9mz*8@Mx)`Ua#iE+wYf`-bNn$d@n7+yoLF`K|LuuCp zf*lO>3d`_5Umkuk9t+;~)v8xbg;#_yEY0NRXVe@wp!&meq3n9Oghzc-b?>S+^z9uH zDJoL+S;YmzSvM>hHKPO}6@C2)6Xb*y@=3r1&T$VLqTKHqm^V2nIk@ga7aL|(8uwyA ziNW6~%Hj(89`c&$h2#7jtb|@7>+Iwon9&N6fFmHJ5GX!h$p7y5NHD#`I6!Q}_IX56 zW0?#@U@v^6yfpzM-WWC52)&%&F=JDvIL@@j$Hsf$v^CA2{Po|%lv#0`WCXIRw)wwc zQ;aq9xrKwSANj(rdq`ES@7-UMR-BHWy-p-pCn1&nY51QUs>4fT`&PBmVbh$;eT`i#{$=ggk^b zoq5Cew3+Va2TAl`<*hw`Ag!UjV1Rf*9#)2&MlUii(#i0Z^w@;%0!pdL4-wk4)%n#u zP5GBW2L*NvO&nGb6(do6RE|$J0PK$`fLRz3U64$7_3YEW*^0A_DZV3$^6aAWXIhl^ z_$A3)TJoU$nXmbWdi5mH6$Xkme->?P`BI0SJulug@3xDBQfjS^P0|u7OPiKAnALT^ zHy_YYduGaM5#On4|EhEMY?3O@>us!nc>JoXPAq851m$|3mJTgg6Ce&%218I#DPZ&P z&tR2)2rJG3{F_2Z>>hcioca@_PCt@HaD{-)KF^8C74=&3rQOVaxAN}E84{LZpUA6A z7LvOYLwnb*S@C+|X>Z2QtrUx=$j^6Z)~MQ3F?Ko?^%&&fnV4mcWg=n`bHi@Gs#J^C z82{TG9yrPnE7nwu4+?|>=7_SUF07S~Xoy6QgpZB4Vjr0NyoDi@CR>Gl0bA8F1DGIi znBRBm5j8u88F3J%Tk5QMlSg^Z;?`IPXJ4TtX{81pe{@7`4uVb*IrJ74`1Fh8MXJEg z0e-Z!->HYiA1dm-=FJLhg3=S>jB`iHw6+E*3ycLRzQS2O@^Wn{&x}Qay~ACOAv1n& zwfkFs5&A9a^jU&m%}-5A$%fD<$1mo3_jOYD!sNCi{Sl_6tl*M z@II`R3p$g=(v#13O(>XgDjgRr`zVa4S_(2AtdWvGW6!KI3UsY1kl zy9Ps}HZgrwH{m-w4s190auVVRVXQ?uTz4DQ$g9CU_c$5{Yglyk?mQI+W<9P{)Y*x)AU*=4P z-mocJ48Q?eABO^lC*!$h#1Za-CgH(&7n{azQ%h=5sui-|qFHiz@C&x4K%oU~m6leR z`}Mzx*H)4YmFs5ew$%{`biOPAfNXU#20D2uWm`ef_CyJpktr+x3g&iuif!Q*3Sb~p zHYiOZh4mdz%0o&=(Yc*SqKx&vEa7u9`HlwO7{%i79Zy(?@eyJ{B^+D-@k27b)@H*A znT+pq>0)S^wyqwql zQNvXa`V9^Fd}|^wfJVdKe=ll>CBcNxMuhL9bkGT^c4*gHo(}7}h&UJ0x9TsjmA3hQ zn7iPt_aNs!M&^1@J9sNc;QX?tNMDJVx~8VQ{QUleurs+;9ZNKWv}yTjwPnk(hj({P zBgYfg@PNvhR4jOQbbq6_zeLe=JNBmlhXANl^DepnrjJFri9opeBLlKwomnAtlAzbL z+-v7-L2UL*-b_ef1WbAVeEQ-xBuhbi2+IH5vZ4X@G1)bi={I#$9^oAW@kxE<@bClt z1OE#9e9=@iEaAr}Zbp_0)k?x?F>CvR?qvo#Sp!TlYdT`0jnfD?~)tweod_ ziN+K>N7@uM;8SGqO@wY)v*RGkH)EA;jOL&E<#(BhP{;Bq*rld&)<@16=dE~723l#8 z-f=|MKDqN+-Sk2L@>RaY(^831ThcQQ0!RPUGV34n0Ved98(2)fi(L2NG3l^^*f(@3 zl;wiiCo);xmGgtoVn)XpQGrx9gV zs`hGm-1 zascqMm-M7=Bu1MdIlB`l+iWd_iJ2|=a0<2r?}o{T(<=WMYp=I(v$Q+9&aam_$!)-TLo^xKz*pA$ z#`B+tpY=2|S5lImFEg~^>$F(RsphL%k-lr~P6NP;8aERrBV&*5T^5IGmo{$STa>e~ z3lwfN_u>`O4i!3YOfU}74Q}%|c?K)L0?hj&_yX>yi^W~mAC;v*XficLI@~t>*Mt~s zOjE_IXt1x?Vc- zxXmi)g{$ws5$(bRzP@aL4>ZZ<>TCV2^@Zhf;OO;U8vyk66EHr5;zJkr#|;$)q8nkE z19?ouh%e>}0FvRwV8a$D>r||cE+BG1okq?t6+_;_%q4C&4iyS8C-m~w+z0a9SgR7- zto)GFVu9ZN!UaJO4xqDsQ`3fBq4szd;|*Yd@uEt)BK-(J6+}}}iA@DN4*&*U7Uc?Z zn*6$g0#vQKfTvIGEcUjDyYraV}b(uKl1q(2IS`xNj$h83w~Q~hhPv3Kyx zfq}nsSfUDcvHrKEK1g=g9M-8({cIUY)M8aBGAG4K?1VT#ry8cq{NTN{fCycUSYpNR zB34@4@CevF@BA7S?q!XaR*WkmO^QG#*UMS}z~HqN%D(9(%uW&%M7;v;Jux-g3S>{L zr_H8@4NJ_;`YBSFbpW7>zgL27wQG&)7yYF;^agnkCoX2RHwlo$Y9k=S8tYU#HU^tpipm}aXsErRe~_Lt#ppW`&( zpD1Qs#VqJEHsY3CwlvzPp&Z3RLI4NTkfG@?q1+9P!_P4-g&*fKeL3gn=913czgz&o zzgJ{P0AS2Z!>SYGaHE2uHL@|DTM&`>i%WC9S~3G)n%s2{b~x$>hDg{b59`z4`jl0jB7+I*^9i`P&KB<$Wih#>oU4pVMz8iIKyA-Q z_UZwEqCa{=e~27eCRQ6$+VZS-q@QxJZ<^pZG9dHGxTz{Fp*oya6aZeN-o6qt)687! zDpzo9=I&RB0Qg&oNcfOi;bSIK^i)eCUt=JXIm5b+H8o1S2B?@^Fo5y>X!jQEnnPVm zVNAflbEk##bF*^Sh|1}2upYf51%;k&;tIy97<9vG@+x(EI_AT=d>d5 zmAn8|rkM{?L(;Tc%&fiNHJvrdSzC~)TxOEIsscLAnJVd?6acYjzQw||4 zO%XCM2pO;O$P#a%T#>w~uJ=d+1>XJ2m2DI%G z>S;80muqrOiz|xfpDJd-a@6@By+K1Z+g_znAgwVESN#r<0Cn$fFCJv{kaDJFtZJt) z*DU;M*X!_X#k~S@IzfFi$I!n%bqqQS8%aHG>$^K)Ncctpx7Yuo1IjUZ-Ld)<)PqC9 zT*n0)jR`3Ai@{RQ^8`{8p|M~tM!=uum8O=>^ih#XKRKGkmpYw_WF-+>siQcKL#-!@-1 z%1sVGta#AB)%MzChA4WglM!W20xXJlee#c?3TuCAF-Xm?hzg}!;g`oNDiFm@& z=~xU`L52Yp)LOVJ?mt_Gbno{dekyj)DGf;t&tNZKX^7`+3kDt4mQM>`G}sL~`#hq3 zCH{ODEta`inCU6iA4RB)q_$W~>p2Z0q?W07-1BNx*5ZAXJ3YD7QVGeU4L~f!xQIV5;(*~4fv}y4&);o70HN`m(Cid87T%H z_x!uJv)z6zH2a%>v69F8wknE zaFVa?WzK|iH>g`-+#h`~SDnlM?MX*Gnka#XY;ThCFF7HPvqXOsG5$n*O66vM6z57` zm%*Rh{-`K^#`@4HJ@Kj{4q)mD%O4SP??Ffxk*#f=8)nT zzz*;%{0;!9Y^*7}3x?b8NB``c^Yda-TXkrc(XVXIOtiHQ@Wj^!0X@R7BnL+AC`~_YN=Q2fxT|w4T3a;jWTaWW3y$diHvL2#? ze{Lvs@6jre3R^;$Aot%y;}AB9w~1Q+0?YeQ((cL2tcoJ*Tw|mw68Le;hh+{A54H64 z7@h@SA{Is>D;Te!MNRrrUU;)9e{gE#Vi2OZu!Q5XwSc|d{GbODv;h-NHAJ@8UFgKB zJVfl~eSSajL}#Y*3Kl{sPpbyMnPC;n&TCXqV`E-#x2mtRaa)_h)E;$u0XsbyVZ67` zf!PK%R7+Fyw^yoVb7NysZ*T9%tfrzpY>Z7Qd9qR$(UC1*3FP(jr3}uL^2333n#zVM zd&MDIX(hc#Hzpi&!!rdr0ax)|51rVDny_FoEbJ_ru_jtVY;GH7gG8>bn>PSzBfgy{ zXyq|6D*z!`4svXh3A1>!_c>Fv%Czj(Iz z;mbkw$3|PP?dW*zhQ4<2rCHVkn&#PKWGSz5*-h+ugVRs`Lh2O|!&T+O)Efj&4#^d?wwg=ZFYvsSA zLq`#*zv}q}dUXC8dxQk&#(NhzE>x&};ib1=<^K|aJ5i7wkoet|f`5i{Dgc=BeeEM_ zx_ch|&xU&S#8~ia+gq_{7G0GpR?x+a@9Iq5o)8zEg~-eXavI*8N3cC9aZwL-Czmk15FHScInx*7Tt15RQ$QO z%<>6-o|>x_X%aljID6P<{hq5=ikYhxV8s7RRX^T%I<#n6t%gmUHS(WKKCh9$_(`vK zV37gEO)_M}_|ox-o@(TM%uIimD*BuZ-i01r;nn2RQVESs)8yjOD%l#KP4W)`z*$~f z){B_43@l@A>n?^Rhv0lHT&kOzu}4*M%+5%3B(^k$#$@u;lFpM#7Sx(~n}NLs{!MKp zB&P^-6>Rp~sZ^2S-7DbZTk)K&H8>JY*R|K@Ve{So)CqS?$rSKX3UoaCVX=C)CuJR1 z@S%Y?Zh?_Qo1c|dpxYv-APu9gXtUo|0f$fgL?lHL&HKWtI4@L&)Os%)v7LUF)iH$w zELSAc-ZULYNaXao%(XptDNrseQcGKYcQLoF9cxR5;7vgv3|n-fy8bxobcKPlBni6O zoiM>9N4iq*`tW8N-NA4QJ9Ls)u)>fI?W>+$4r2-kn*SRw_tUW>G|BFJ)=;K(2o~aU3T<v*qct>? zw*y){enjEA=@Rhbo8zEKYS~dFhX?ygR=}aZV4mRyW!A#$q=vx?*rU!^VPSb!mj!X; z`!<^p?UdR`t5(U}cPBn)k8>kE9O`m!%^hE@+~P3B5WP}CwWVV$@}ZQ!KCH{kYSP5h zrhcijV5ovBVujS}!ppqfZv>l~%b7#k}vz24v zq1g;+FC>TEQh!2;v=+K(S}KeNtK2BM9Ou!KkMaNWbblRNh+la~&Tv$z^jg+V5YyZq zG|RLK{pYfcS)dP{X&5}p$t!v|X({r%8+(=z$ye0Vf84O}Ia`r0RJWM)T75uR_Sk*e zOYDGQ!`RF2yW5=`-6%>OFe7v_4RdodHg9HTchBbec9q(5uqT6e^PbrEVsnOV>It25 zCtY3R97yix#DhhaSG?jl!Cz|28ya@GS6>7E<$btm46mbn9?9KcO-UB7em9Jo?S@)C zNX#sh1V9xFTvF7&@4PddgEh|(WS8aDC1}f9u*~|tSm0*t1DSTBZ@FgtQJX z*4zuVAA@yBu&F>zQ+@i{r}|bdma5RObyrbUnpDu}pxGeqTAw^Mtl2~|X6XDWpk#(e zoA+AMCe?`e>Z3Qu2K-iJy~3UGaEyb+e4} zbDqszBb%iFS*E2Z3irewPKefR{1Trb+q>bfnfmMNDo`kpaf1&{fW zE^}Wv=ub5eLus2BX!@v-lMV~;+qEs?7hPB7f8VCEvDZWS4)0nrY81R1T@BX zD^8qcF=b4dMSPbEzG>4Tt!bwgG)MF}kIYywG>$GC))i8mFxG=id%-;9qc@k4F-vnr ztJ5VDraNS-CI7UhcpE&Zmf26UjeHm!snse$4*GId+l#KAJiA5Ixq9)~a3sH8f+nbu z6`uXZ6h4F)ZvV<#!%bj6(YBV6aTt?S7jtCBv3#TBb7%IOwgIEBnsMJ2{Q1F{XK7tO z&b32S3T3A$)qIvpQ*Fr;h%M)+83`A)mUyT?IP3LYDqTH@7Y|AOOi$#>30_><+_J>BzubKSe~RfO-E$ zz4}r?7IZ#K*01?6iuc5vZ)=;eL)gx1ap*!He%WbE#B=ROLb2P2?elaleA}|ahSeKE z>iY!8CR9d&e0*D~1*}PFJ;&M=uNyB6=+7_v()#zY0_VSB(Cy*KOiYQSFl9A z{ruXyn{?T^+sfrQfm0H1u&zk|`D)Smsf%QpeyXc0`yin={COUaqYhGFwxEB8~+<$3Yu4+G)?IWMVF z^os;p_tS-!#qvHS-Wjv{Zf$yZgX-I}?SEj^f1miA1*9mJy%z=J>Da`ow{`zPbK0NW zh~wt>!S)Ya%W0|ZCQ}cIDSXYa8a6F18xziUO#9u$PaY@;7Rt!T@X%WQd&n1l^^*!j zBKNNl*okl}v$;X;I7Rk^?MuPELRg8^Z8nX ztR37?9mq zdmt#IsQ*X`j z-k;TRvZ4K#7)MQ^*{B1n8zfl%0lamhrKdR_`n(xE;G@Vgg{*%Y&kR##mEgoOC171j@ z;$(=-^PYw4JCGTarnyGo#-AD2cMkdIv2x{}awaJZQpm6LC4Q`kc2EO>J4kN*N0BPTiNa z^UU?#Iez|Pwvq?)##}pME=dhkS_{GQu9GVKCQ8pwu&N6kG|G2duqP(kTxPbM@xxs3CtZz|rTg^*qJK7ojc(c&daTYRra6nrr zz1xX>nR)Y=JenE4?Eq`AX*JdEi4}jPLy^b+g!aD!024M=69l@S`%=U0N<{EdWO-IE z%~6wyw*p_^+icUJVFl)(G|AY-xKVrVV|yh%dE@td=-eIY#*U^UAIoS1t*Yoa14T;a zbGB_E;(NXA?I5sb`hQ%2Ha+R{tz$i}V?*dTennCpsU(lnPV32CIOWiUPS)M9*=5R@ zcfk@ERwYV)A69Zojs0l2yZbRD! zrstuyU$aYD8*_GTv(F!^iZGv7+@dx*{AITeZvwn!CO23+*_^ zAuaP|V=J=grkR71lm$r3oUx~_DXAdG&VJdTla4mos1{1u<$ViibE*76_3XOo#7}?b zC-3U zUUf@F9CGw;_n14bjff9pdmSR?wH50%5JxpO9kg` zoLGY9ZFWvcbqr4O?CW%DO?5^r&?7=YFA^nL3e#s)OCKx>iq??5o9V?uXNt`DMCj(o>oEa)kQdY z3ELufOse9Q?XM2|1+E?V9|v;Z#UjM|u8m?JKIpp{dy;==sghvfC@m%Z^nx0ksXsbII*vS zJK~LW#?n>?0xL+>r`*=;C4cSe}a(1+O&+I3lV6G+7oySlV!6V642p4@lt2YjWc1!zgtvF*f|87#1D*xouSi;OuRrLH@q@BZ ztN!Cxp6YV7uEA}Kud!bA9W{cl)cl}R?eRY&-yXnP%~MNqC}eg~Z5Jn5u#YAq-?rpi zWX^CgxX=VyER|M^Vb&j9c}ZcVU>dLFgkTN*OYLpK2L;vzn0g-XfP!o?sR<6I^us@z zbN@X=#}gLWrp|DDgEpdWr-t8vjYrIR*g8D^#5L%n;EO*vR>ZEHzYpq5SHr)I(0@Ng z1YLm`dJ(F|Y3(bm8IZydD$cyowPZ%91uv%ZjA`w(t1Vh4*%S=8k`B+gZp<|HyV_P3 z(4AD?GBiaE@>A)`SgFk|$VGBrSOyqNL0>naN~@^`h5L%0{2!*iI-rUs>U-&ul9X1E zPK8USbcb|@m+lVf?ogz=q&p;)k`n0#=?4$LtI3E zw`!YSWdL)W^l4kdp?a@%hrQ;9+iH1=BIu%VD&^?)SLLOqrQ=NRGFts*;fGJbkI1W> zDI@2WhR8#mR*fM=w-0?4o{|$_YD^rdN&;&igPy;6JCFE1L3*Ss%cWB}<5R_B&(Sc~ zt9Tu*&MnX2KadgE8yA*U({U7`lWdoR5}KwKFa^ApF)-qKu{&3Z*0J3rR1TiZ?kIyW zsNhoC7;_dzc^i3uHrkA0YbH1^(XnY$2* z;$r)=GNxcnT0iX{;)Fy303?`im^9~VKhwGmt0{YrgI?OwhFF%rR`$0RjNgO(8fFXY zM4@_op5xAe1tjkwqjj(BR?HRU%;&j(T)(aoDw{sV8Y_$#7SY zW;Z4M{qUl9pJ}6L%zEjtqB4a-u*NxiuYREKLgpU;`k-D)LS zW7O$#e*CQ5014QM)5?NRJYwP zJM-3^w$j_aoj3caIXXpX;i`?3Q22zQabAAD&~~J%$ndhx#`MP^E17flW(vZmA|%N= zI6$`Me(1|E#QB@GH8tWqN8fNCWFoJX*=7*F1`S{rGSL3c^me|qAcYmH(1N;-{W;WD z@JDn1+pCpn{KywKF`)a)9XeN63n@J>nsYNBoj7#nj*SwOJHDH{64$UU9M9_h+5?;8 z6g&Fv+TRd!#l7^EE1L?-#)h)9IvNnrSJa9OvANL7D9Zx?*R_b${fz+>P*7x-so8b) zS9$Z?e;W;+)i8VNldmONS3ci3qlO`rOHyj@Jy;Ppw~ysVOWU@6Nd8I2JM}1Kwvy~L zldKieX^N#c2-A0rH9N`W);xL%+QXR%*Yc+)XfNdhRhNE>mIzNcb$%-o7fmWGR6JmK<(`@BbeCz00K-_l%Jywu0wUQ@7@{y|ES@vftiPVUSp(X= z7#aDh*GJ!3Z_Qd&A;U5~i#By&xW2lY#xi1y49JPZWgQ3-Yu1YjtE@<*+N^I`BIf^a zVGfDMH*?b%=PO3R)8!rg#$B}9LaV>OG?P5-<1WZBQ!93@r(@J*Ls)ts0Q~CW23V%1 zJjS97a`!8}Eh#YB%hN3Ix_cq~kaL3d+S~h!s`IP$2OA^qm z&kjTz>pAU8uJLTL=31qgN&Mp3uF?bD`%U}%z@#9-!eiZ&ZYh+Tgfgc!@ZcH}&>dX~C=^;mH_^Fkf)rXJ^ ztqf_k4DyWSbq*yAsa4ENxc+6$p~0`_R>RcU$=NP>DSdGU1oD-e==_-9V@PuX{Ij&y z=NI)p_PY|4;Wig0%od4K`zQ=XJz0ndcN`r#|7D`59&OFbu{yuV)BpKvmdnS2^tbxh z?*rtP%JO%Egd8Rs5)JY|`1N@ZwGsX=R-`L1_s;rRZ?-7@A$ShMQdW&Suo{wLQ+pxD?SZ3XTO|+)XxwxzHDl{x)&x zHU1D{G8aENT}w~?C)ZAxRsR`^le2zx&co%mB8n=7)XEN`5jLU-E-IK#vl!4vrYu?+ z`polL^c`L{zb$oH>&rT-I6c>MjO2;7sc2X}wq@bAOB%*}PXKWFN!6;&xGk zrXsn9k(em7nwvagoS=+U=VWc&<`2#|kMgI*KzR-YhL~}a53$$EgoPI4xobfy2A}Kc zu95v=`)Z=4S|k0loBj&jxwuWAKswjOtLQhq0wf@SY#^YgR9an+v@}nfiHV(+Lg9LL zjdiGVbEho8 z%lC~jqde6Tu&|5w=O#oD!6PP{>TCMjip_DMw>@5IffDgTgIY3U~Ya_E%mE(g7ii z4aqHjs&hFG;v%V4jK6a+0)Pdmx0@*#jwM%ak3$#DpyQGj7OU)vU&`QT?x zM-od-e~@EzT5xB*$9t@zTE{98{vq#wXJW>rS{aQnQyNBl38mYUdh~ z(>0dv%0+YLsVUAyW$-{>R0E)Jvo;?`J=Vr8VGK&sE3Q_03Rx%*y9CYh0!)AR4Fp%KrgEn{MQO9+&nOM(eC zve8Doq50h5(@-?c**BE#r#iWdJDUQkNyRtT6a~gNCe0EIZ@Cr=7z<89a_8ubpupdFdLSMLIKv!%=*MuU7JCOvFZus7B~lw#DEQlMa1AXYhwu1H_MV*byA zSXQVkPq%@SK~X1zaJpsBCWfHJPp!lzUi3;|-2@w^;+W|n&*oh#XZ=GJyGrV>eEIG+ zQo2c9%g|Ra-RngnWl$EQK_Iu#7?EVn?5ejx8w!#}vOkGNhjr`dP#vQYy>OGY?+Oa&QJ1HB4e6MA&uk#mV8qeQ%ShF*}m3S8N*zU}w#kl-kl_Nz-yh>fr^E@aG5zu0x_;Ue)o) z0YA>)kh(s9!a3O&d`$09{Rm35A|{AxAqtwa85$|P2oD(e-DjBw2S6h@=XXD7x}WKe z>Zz;;;#}-$_MH7H+Mm33mDwGCm$JS!W2;7L#rjH24h!W`s_{b`iPtOq9?LUcQYEF8 z(H%)LGZjU>nkTs}U}=PoTWw>QoL={NG;jfE)Sx&RIf~kznp{(U?y^0{NywRkFU+U; zw|{XZ?R?$fV;BOEgIDFGFlu6D!LIq13o<_xc(gnap)J_q$&@u$;IZXND*9ZjastZW z5AUsf<5AqB^U>gx_qdyMMWL^8l`0g32|`~je9)d!*^MHsqlLYoaT@(J2uJmpLH6rTDacnCiSqkS zx@BK3lW8joJzl@Z-`yDNL*#K++QcN`PBiK=uwOc~VR-`(Sb-iw+Fh`K!lamDu&2s? z-ykkR#gX)!*}@O{`boM-Mr361<5&<|oRe|`C4A|e>KUtFJ7ChLrKs=bOz}hckVH zmGX~cj-H#Qe79$Y4o<)?f73^I#8qZO3dIT24C+~~Gqp{hXj$>cYQlD@cy~tYTI-Vj zQXg-94iVk4YW5nk@II7Nfw; zM6_fBI2Ck&3YKB6=O;=GO)r~Y)cQdlW5ul7+U(LEKH|nt+?#BoH?_CdB7h|Q#p7Nj z!3->IGF6YnASIO}!P2C`ZW#`E`$5n`z*lUHYhU-T(KqL5ZZ<2^b+Koi8k&kuCjUw{ z^qH8pUG_E*K9ARHW|CO5HEfY|0^hFkY%oQO0#aF7g>$78eN-%2V_mkGWk9>1sZH8R zB9s3zm{re1EHXLCw&hoV_Gn~yeiPR&#M-{UP|f^akxuDO-$457Sr6WR2Jyej4s7>< zFov<|BeQxh#>^1$rW7J)nKv>=`>$*VxyUAv-~qB7c6BzRUlLp@Y(dJ~Ut&iH74}A} zias|snvhks&88|NjfO-$l8u%re? zkd(Zz8oAr=!V+PjAjOO* zsjD@;`07b!JuiVSw>R;R$fETdsfyO}Xu}>>0Dx&QZ!`=lF#6oPeI{Un2N)p-jM+TW z6X?3&fJJ4yPw|P%?vVi_d&zRL%1Et`glx0B0Tz%|d!Xy9IILhHJWEct(Q|R6GN^%% zNpc1(0iZ1U^DrKgyVhTNvg*7rhMHTK?|DQBR0}`$I2x$}-#TGi{VG?|1Wdp5b~TiV zCPs+@{*{k`tvx+f?$LF4d4r-ZA1VG<2DgPINxv$v@HsGG5y($mCQ0g|W?`2CRy_@P zWnlc{QOY&p8MojWHym{z>9t(q`b{{39%v6!Q-_oYx zW80AP3NiIkwM&YcTGm{s%u*(GlcwzCx!EarBSy|f1v?F*k>LPC2t^8`MQ{LnmW#>vhj~a zBvB^1RR!+Mp@)fOkoMc?_4SW(=pRE}U6@%K zr{?Y8eSzr2d4I8@9HY7Nhk1^oIMDR-V3x(Qk)DtLsK(46$XagJx@qF3XHPAY>NTG!jJl?Jwvh92 z002XbMID&{v*8m}W>&P#E-qSBS0-}K*VfC=eouEKzKznRl`tRX0v=-wB{!HHuB=#; z9Xd05rvCQCYNO8q3)in|6Gzd%pU5eEqv{{{Ld#W1fyOng-{p z`mW~+45x%ny_VW(X%k0Q9u>5)uBB_klXbtHz9KQ%!+;p)74~v`8I(u&fM8<7SIi%0JQ6f^J1{$E+53I<>3q$6q! zI=7Rvwzo=?$ui& zP9Cu6?+g#*T*3o8`)wY!^YXSY8#P{3)HSBrx_fPxyd=_c1byUaA(e5lcoUZa1-l9^ z;ce~Nx;mCogoSaT2@i)g5M>BT`J5@o_=1?2mlYL4Nw);&WmhAeQL&P@*SZOG#a7PE z<%NZ>8Iwf>owwllzMONKqf>)%@rRqv-r->UqQ0?S_UHE02mHy zlApCy5}{c&thY1-ZCw3!3y-9ssENaMa)e;>b4r7ROpP8aFw_yQ-a|LFuGw)?XZpKa ze@sv1%>d*hAp&}Bm-fGoT)wdOmiU_(gPCNef15*^7C$&X6nL33xAE=!$2 zYQ&~7N?hGDd_0emxC;XL*>NLWKErQydO>3A=ykbvNi(0vELQ z$eb{9js*7QGOPHQx3uP7nPNz~((ML|tI)Xo6sMj)120(7U*}~}8aUBonGe~uv+m;~ z8UQyjOqu5ld3%v->>EXUrQJp^kdJlePQU}RMR}~c+Swz;aS|G>;}fyAn=WJXTc}`+ zM2rXB0Mv_AfQ-24TXvXE1OSn$3nbbvN}z7~)~K-vBaFXXN*O;$8h;g-%H5%_oF-DU z6(CR?x{S$EoS5KGb?mf^WX!LC0~XY2zWZP>9lNZhH<=VLp-4$KgQh-A$Mv17rajE3 z^n)dgM5eQf0;7`mZ~)IQc<}vEkJqda45jT-5Ou9_OAh$AkLWxcDV8GwIhFxneE={L z8}!lD+-&;u&0;KROqy*=(ZY8{<^46qrmp%z)6 zMy$uOzp>s@j$@p;UxMxw8Hj(>8urCv6_rEbijl5K?@Xm+7n-p~OSUBxQ2`?tj}ykg zX4TpX3f0B;Hm__=$WB#$4}S+|_E_%OSlEufJ3F=Hb>!r(@eBz#kdIDO&~yDjj0mI@ zBQb^XBLM8Jj%qUeaWTtBa12@{MVmioK>OvjRp;ukIr^ptom&#vjw~0#qljn^uxv9<9W+i(j~DUV(`uq9&)n_g zX)%UhAOSW?Ex@;OxI*()&;5q$pgv4B6xX6u&`2pWX^HMPkj4=o@Jv8b5E`1%p zAaMW>d?3uo0b88v!0jGJ``EFC{yahePo?ak2D)6n_F8o|pkOsmW;>_98b14Rwfp4a zV%nJpbcupty!BD*j5ah;Ph+wpL4BY}hWsh%99vq^alu=eerxQw46wZ0{wI5CAW}=7 z7?a6WGtY+U+H5e-W`*O5#D>XVct8Xeoh-pf>@8W{icJ1QVNW|>bnxfQRe*WcCG%O5 z#!Oc7073aqU45$}XehH4(bskOipmYe6-uBj4(&_ystUaSJwV4?yu^RS7xvAhLFKaK z{M`AKDJQs{ANTaMqfC1UDAD;G*Q(665+$Z?h%1)+A^WS7MNviP3-ZuPDs<3O=nIbf zdkmg+x7QQleQL|fM)>o0Ny3}q`9kzX9oFQ&Bt(Z{6Vji?j8Y7A+J~fL*;y+OnZd(JgI+ z;nM);+gO7U^Lk0689Zy~VF>0to>Kaa$w%$;?zgG{&n!$-q=|tQWur4O-1o{A=^P5~ zreR)$7tL8dd%Cc-V9}6KDPpQ@)3$r3(&&@`f4RFK^@HM5B#WlpipXUBXuS|6)Vnti%2WfG z8^os{PBjp@$IURdnSaCJ7)!nILLHP26AWxf+A3-zgT3f(&sCE<2_DKYK_i~b=&^*H z51Z7Qr~$0o*I4iCcX8U9_{<^WG_$pp_`}dcR`o;c*#=%*IOqp}`V(xWVQo!BgZ*wS zz{%yp&2$`bh{?RZ-l>R8#{FnF3{~@=OIX=(j=|@BVLXsOygh5b!;CP~#w4wy<10_s z@xH2n9z=zOp27jj-2N%BbHtXyJx9Y`y0DepO~_;1m%)3 z0hlUf&wCixlC}Nh_DtcOJ`d>PGO><6F%$ak*uzyGR>48!PHX37GVIi1GI;r{4vhN} zJgU9>kk$dGW8<>SFhv!M>`BLjMXM;b<_(Is(RN@!E>eT`l!`%`f`xnO$tWMB z)orZUJs^mzn)%@e*9$=8QDML;>P^eRl5%8N!Up!2c?bf~_2kiMG&DDxJWI1R;==-r zlOT%%>d^lO_?MNHt=T)0#ZCC~*yv;zJ7$7_M8gcr=%N%_G3 z^4EriFWJQsEb9JTc97UKPQ9>#n7D&3dw0EKUte$+M;!O-)IwF15Bvat5973=g9?wP zQ)6mEkrb&gRKQ=5L`_oP?A5cf5Aa=Zu;6<2NQeB?T#hpsVYcQN+{BYoz#9U6=vpJ_ zDQOG<@Hp(mgNA}nbSdUAZ8GRoK7}chh4E)dMkBX+%ffkGTy9oM+Lxhko&dA5KChTe zM`i<%mPr@+YRZ4(U;b6=Eq{dt4~lFUsckRJbTSik%!{(j_A7MPtppnP+T?ctu-sns zSsXNmdKybzkdz7nh67OLNs)kIUEaYFDR;e_tRxKwxw7VGQPg0uJYH0JB-4VJJBV69 zfCcupio)r7Q6V8N3!F7YDLQ*&n=0jEprbA)KPUnX|uc?D$DAC}NDmHY4BkL{g9K$rJpK8!S3*}$1pzDKlIoyM=xBWEJkdxtc1wqSb z3j*7~kSLjW!V3x64;#})&7>L&PEBJXq`n+a5GaB}$Jqaxb!B(0{5Nf~LdJEnbYb4AxXQcaL4&8Oc7y5MsZ1ak_9=zk3Xl8R`p2d#>^E0YRD2v?p zvCVK{bz|WBH?MERX%stGT~T+?YHD@_CDg%ag$uwqgAcNL@q2^;r2xp zsA9VvK?>URDI8P6KSvaSGdcr6a$45>go0!;5gw4h<7!CulSX#KeO|^ULulv_Y?C4`E%z9Lg7m zu7)J|8vH(b_kt;W=bHrAY!U56nf~ch&fAaeb6}TGsvn4d;mJPli zYF5pU!dVOF0K4vsK71XFFVeRh`X+cl!JH^lVJ-_lI?Div(710R+FS}(56~a|gMzWxzja$ZH(InuK1p};X*}tw&p`DRCwGFrv z6tfG*ObTehWkdZ-&*mM}k2OJlT6v^Lk$F8L@QFz2mrZA!50&CfpsAH~Wo*%7`;Dd;B+uMy!4J){t~O${9Azb=)A zp~3i~k`@l1e2Eam@M`#Il(}Te=eW(mnWoeThD7pWabn5h*>qA?xF0ds;^H9D*Js&* zG(6%aoCn*u1iP3U@OE;}(WJ$Ob;=pOjZh<8Nb}n#QG~U@mSL8g8v|a@irzd>PM z#!D&9O1?GUmj?UaGBAa)gS>hDhVsbh`-}9espALpppEewK`UN`likLiZzeKgD@uMh zuYIn@vb`L@$KiWp`s18TRo#=f-QT1uug~?RxPSKVRv(x)S}6q(+j#CCPS(!IvEw-U z7M&kCd^a8Y>9WT<>D2-03ZfWhu4EFEt?SrN^60dmG$}`&{k@x%@QaMU;uE0}|;KM~eH&@oZL3ttZud)(T>IO~5G#GVh)Uq#yp?yj@1<6UrYSowRX;ejrWI6N6##!hSp^P|aw{XyMSb&dHIR|lYj0AgKfntkAn=dn zhM%G-C^XoNm?5muZm_~DE6;Aj)T`d#q#^@II5SU z{{2cj0pPgmn(c9#zj#Fwjjp^=`UZ49@Ab}MY}{ft zAGPn`E)9Wz`)z08xm7tX*K?kh!1QS@jppCa(wnO40w#%-MbrYn_Q6P0BNz$dU$_mGW#BQ1`)e;!*@&`cPf$IZ~gM$rEu*za~X&97UF}Ql6^Wdk7wgmXoaBK;S z0gFAXl^uRqENnmL3||kc{{Zkl&e6tAoy^j5r&quf1OJifh;QO0i9(30)c|QZUl#L) z+_64*-_yr!RBl{(*4s2aO-#a?KL1STue$1fgwY60SV9V32kaDXK*qT3Lq!am1;^pZd)anJ zZU=7jm$b9#;IEIgobGkZ2fI&RH$eqI{v&VS0o;GIb9AGP(yaPl<`t{iSLb{H!VL@|pQj$}Lp|$A~E)~3Vu{liG_V?m|eaTbR&!;tdapf>);V#>Q zUo8HxqqWVHu2^hM1h(5@%jcwMa|S+FJ8EmI1LBExDreT(1AcvYY&>hiMxBJhJ&EpC zKLaDh%YRcUP*)|vBhjHX4V841uIuD{&ZA>FH|qrB=K{~pmdxcN=uip z5vb=9KrA?90H=nw4bua!!50`#afwBi71oGF=(?>#D@gg?k#M8yGm??Afh0ioR{EXv zZm<3w)@0hUQmFUbPKSC2%EZG5HR@`c58ybw6{@*mD`<(gMLTNA)C-O_t!K?0v!{ua zr~PNRS7R|a(SHo_e*|q^|EU6wV2A;OikTs_I>G!5d7wn5OK}E#AXaVmVeO}wUw4C` zd9*!EE&@i$5dea;q|CPU)u>t1Fa`16;E=F-7G4E_$#Vm zd)7Qo^b1>l#`~Bh+zNe?96ekR1E1GdeGayU8j0YDSXk99Rr9X?A_d!} zcdd1HXXZX!&Wc_@VXD<(rv#iKQgBMQ5K&0gt3~K@PM5zU(?a{9Y@mJP%(h#=#<+QI z)}equcSsXMmnNB5g84>D^T?dhPu=Z`@H@V!7ZcVm@PV;UAI0=dUs!zIe9x=&5ntx; z&!zb`!=G%3(WKqt+Y!N4WnOuUO0{8ys(_n4rjw&nOKG{*qH@C>I$LVfp#tv3zDGL@ zsZOG~!z-39?t#RNt}#MB3n8gUO7Hiw@gEPk6n)_kWg@p&Jb7?B&*s$f9K3EgCvpU~*Q(faJtx(x@ceN||KyR2q#TxC%NMWE{3bQ zw(1)r8@~}>yeW0#b1Db7L6SQ*JFW5l*#WiB-vPrSGFuy;d4RsM^C4Pjv~bkjdreCl zkx&X;x0bknhS4+ZLrl*`G;W9$(to-X%6b=@<5Dp~K141qH1H66{2 z9Ml$OSy15^0m=mKXKJXiT>&b1s}z(o#dgIGj;wf%TG6Fc*kYPbl<0F+O!}0*3DoqiUd~-z%jXKfPOs2#}!A! zfeYCvYFEE~IC)_QpO@de}XNZ>GNifv8M=w9DeLgEhY+cD!4bd^(1u z{n53JwX?>*p6@sUDn#Ilh_UuwB!Tm*`DjR089*RbvUn8!z5qSG{S<7^oSC2jG5m2 zpC@_K&fhFlVYj;*+GtYuSI_i0e7KuE zUl0i63te+ApCDSOYSp`mCY)YVS6Iay;oFQ?enBOTs*pgD>>Onqr4!eP#vc|GtA(1w z(jaN9M{W1C&T+*QbY8YtP2*rPFe-T5Iau(u=Wy4&=G){CBOX(qy)ppH&;^-qw$Z6= z1}ZlRz0AaLsy`CAN~ayles!NCC`Ei>*?B=fDK%yICeee2{!(_YqUZr~7=Wp`n3dg2 zlI`s9$|Y;~kaW@Khy!wq_2vf(qllQYylA<6>r(>nx|)Ztbo1p5*!%b15kW5Y?Q@IL z-#IRmK*@C*`zY`+iuyLMUki9&i2h+Fou$B7#__IgPdQ!G(--{OpWQY^l6KtrfW}<= z%$b?_<_*__CgI}NDUrv;6$?-o(ol$|WcZ``Nk3OIfSVH0oPy^3(tb7@Y1u)eW;cuJ z&9R!jdWmuX=+;oK_C{rkmeeSH4Lyi*kwLXBp+eR(L>SHP1YF?ZJx)jLe}SiavXUem z{{8zVBh!z4nYEW?wB2BVjWxu)__eYO>ffv1Oh5Y{%3VCyDiB`YU(7xv(;&Fdqf3hGbbOFF7s;?E{f`w z_yuPK4%=~QLS>>NEZy{F|ror9R6x|8A|p zJg-YGH){EA6dhM}m1CM-Zqe~B9j7}{S;L)oaNe7)=FsdpZLd|14w{~O-@#A&U~%@+ zCQYi6pKuXBBwG7I^w@gBa%qu=Xo#Y`#*M^t9~K#Y``(wj0T2RkcB zjBvjmj;P!vrrT;B^Q%V7ktExiPanVJUowGnfB86h& z6x4Fs7nq=p1hPTO!W}Ki-9}pvHMg}y7Gq#Xbx=oexsGrG1X)y6xRB#``XF7 z)03Re?yhX;;zN>O>uro@3;o)gbVJkMlk;qpH0IX6nEod6tAPf>2BC+=cQWu8yP~JQ zVnyh6OhZQ4mvvuB?P@=LJT3E1W$pmg9Ne!*+lwhheoatAEH)|><7>#aM#m>G$Pv`O zVn9J$d>Mm@G*x&zm*)K}9;rED4R!e&I9$$*M=N0vtzQT36pm*p6sNkLNl+pjShe_qzG$Q6!BEzuN4mQ7|2r3+{zD%dw-R!0UH3#9Vw6{n2Yo}G>v0hwRec@O$d^8d`u`~Wl7@L%lYrUrb;1R~+Rquz{- z3I!F53%92I&O3yo@2z9(xQo@jHJ#%xS;4V921NgN^`LBz6WWOgtH!_M8D4zdd)I*N8r{-E_ZQL1W!az(Z|ln{&b*>6-%-M-ve84+A=f> zZu7fm|EoS?gnzRbep?8D<|jtbmU5{0id-NyZ5D!4G_M^WHa#N%{{m&kooAzE=rvlj zOm?Ju$pbxnbP$1~cLEFVOXorx;vh*zyynVwV zr;BUU&_8AR*(6zTSL}I%_!IGVYD3}gf7uAd6k_zn8!wWo@aF&q|B@&J^t&wT=jdL{aR%((pYi-<}{E0s_1qBq<(1mLvo)e&ngm&wOup-c^zT6iz91qU+(e-xapcVUzs zd<4r8a+y7Yxcn(q(0e)jTdt5X zVuIuw^bj}P1Lf(YRCw2vdQXy4)#d$B()HY*Bo26$J5kkEI&nP%ae-TxEPW(LXRN>% zTU;pH(Yy_CUGxHe_d-H9CrY(aM(#QF9e&zrub6@xmwjLQ_e@RE*;2YCX170@#Qz0Z z1$xZr^$a_^l8S|&c~n(Ew8BCoHJb51^qU(J2Osc1=i(#BQ2UM?&CxH1?pLn8YJxne z?j>)=RDbwXU_K-{4%aE11|xx`eQP$PF{L+mE`vwVcKXfo&+tuOlT2By4-^hI(+;aQ zU;1_}hj;lgcq90k1kTGKBjk%>B~)J31fA7a++7&eX46n?G{h;iYBD z43xzze+K!l>(QSbuSPXRmi|LeT9-uR%E>~^sAHFSnPt28orUaE%qO!m&*6&Gn@?KlH#~e4qk?DD=vQ?5_0w$ecSo&rxLpK z)BA+DN~c*LJ0W`qpQ*obcjwK{GHY*G?CecAHzim6;e|>t;r3^2h6*cpd97j0T@`GAv zxn#5uZ33nTf(QYUOa&D#J{9WQ5NWFV>$yfY{oldSIe&hDD?_97J%>Kad-5CA?XPWSd8H`nI|3{fs85{4b69u2$D0^CN{JtM4>cXil zC0R+*gDs7juGHwJsUAYNtEv6(LJqo+@;_bm?XUD#`5jT79VlS-5L4(`?8_mFb-B96 z3S;^CXRo3-K=RM8$^1Jnud9CX&vcdgACMkSq-@2Ts4fx1Q|u7<+&5Gey$n&XX$8*$hkCR_q0zN0QO~gc$$41nQtTfbe*L`Wk8e*s( zn;%Dit#;4+)Bx05=E*dW-@fj^&kNQxd{2I8`%L3%*t{clwBz12oI^8p^Xp85s)d|N z6G6B?=et(Co- zD~@W;tAx|s&421f_08UAJ~Rzg73l(eru6J?<&Nh297}ZzbF61JKPYSQpH2|JW~B3$ z-^Ook2%10l+A_Mc6mqh2zGzIeqPb=i!#Al^_NQ?B4YG3XmO2r6$si^YktiKuBm5hrwf2K1tau*zp~*L&CJ zUgP6$hld+X{V$u>88yI%8SULU z^qK+VU+p&;)GyR)3qlWd8}6CC-G&>OHk1lCdL=j6-xMiNdnNahQ=F@(A$~?(W%Mg5 z^@};2{hLMSbj>r=AL$$#-z`nb>l+K1A@`g`GMyFD_{^eUyBBg<67Vy;>@E;lU{-y$ zM+L*|$8*_DA<#1TF9ikt8qCA*9Y`0qA_I*?=Yallqdj8CL zJC!qz=xU=~7{vjtoF1q8J7cbG9v7k&E?XrlhZOK!;HUWsQ|I4HWi;?r!RO8bH@~EC zs)&B{SA+@3{}4+6sJEyT*;rM|0*OzO_6mMi@ZUIgG+z-bW#19gW8j6$Sj_KM?C-{! zhYZQB?oK$}w1=E*40!Vhzpwu_(#XDV>bD$*9dBm;OQo{c=2Nd$x_hIXi}~noUyQbi|FekV-G>@w52WK zIjRsUO^IlVAY@nmbut5T+b+H_MCT0jIaub+wV&3e^V7s%!j*EQ3&MTt4GbNR`hj75 z;-mMZ^FX*gaZaYok{moB#(#zm~E8WN(d=g*OivDSPD zIjC`DGN!QO3{Ssvv*%T^XA6&*^3d~j9nO4p6?(8i&MS z`#gyWxdQXnMo3EUc6RG(|Cs}bk0*FxH4OcvKW7E+`aWXrz|zi+D6bNM)d3Ng*unK2 zj`hci*jpX7eLk;LkcIxEiH}7z!B%|P3wB$N;;fy`NZIEB3xsb(upU&E*c~XU-p7AS z8yuEsOPHJ7kdIsX`&=-uHQF?NpZT%|zUMQU4fVHqMi!&gq9nuqFX_TB)JQ6d#;M`M z(cc0eM)f0Y__MtKMts0y755Kle^XpaOF2;bxCp9%Z21tlY10So527AYkl7e_5H zG(9oS7Yq)t?rh}c-gRug)dJ7X_F!Dpeyh>ZK6LwRL3Zv*aGu)%;ijKH_gn$Y{O5Vh z-lf;ilBzj}Q_0phurlzE)Y#LH%!h zGUbrLQZvIo&6o8U4(RCql`Z~l`-n5!QdRxtKJx8cpj5gVIXKz?=SMt9o#-#^Lo%$>RSV4rjMUTf`r z?vcW3-wLU!Dr>jUf|6udj?7sPg`zBhJJ?h1%10d1OKUq@unq9I;K2m%Uet`*x1UxG z81Zd|XnYp~Bnd3DK~b|zU;bYv2PJ}#1QbdiGF%!rrGi>%<0mQ!Vt)ve?)$|-|I;Ye zhex3Q0i|Ym|NMQK8N5$Q*pn;19&_6>#W3G|e22F{f|}idy&deeAJhGkny(!Wc#=9B zHQCb+w~*`Sf@^TbhIzX@QRo&x>~3BU(Z$pP&eB^H|zOj4r<~5Pcabj zf3Oqi3g|N+b_Aw$8(;ea*c|;C(g++UibV(nrZ2bp>|C%y1IiVP~!Fy^D{)L1J%WH z^}PF;Eib;iEbHURLILQZJpaNo&4w=v8Yqy)s@Aw)mAM;X;ccFc5F>9>614B~)^}Pl zUp{RMiS)kt%W~2bZ$>-Z?*B2I9Hv~YnGNoqa(BT3u8O^ALPZcXoS;nkd+Ui73gr~~ z&@P>Fs2}!CV(rL9*bv;0F6gNeE3@N#K2H8SlmFwHP)r$CB1dtnQ9TWNc`pts3u^%A z0jVzMoI#`oyS)+Tg!50Jf#F8U`o{%NYFXlIST}1WOySl8zsQiT*X3GjJUot{yjh`} zFLTqX*MT4pr=S0^tObU{zbp9yMr$nlppt#`4<{vx_W-aPsENzx`8ea&mYSROFJg4O zdpQ-e%|A@9V{{h}dmf7Y~`_SDl6kz5sGO_qu2! zvFt5j89(i;zS#__7PFuEgL>9V@*i69eqJrYWnB?-=&HreI&_|>gIe&?pZ3M3)Q|pL zUF*67WFN`~|AbEOHh9Pv@1K4W+k(h?2YKXoriYi7mBZv=oAy-t_KA*M5MG%6-#WoQ zhZo^yjb<{HXBylCQ68h_94{n_enLn>3RB!aSk0@xUk8E(fX)**m4gz40wdG?rFt>W z&*fYxBPtHgY^j}urhT+%rgq>=K{9Y>vc};M>Xcf|C}=Gc+g#mk3@sHN&g32q#iwL2 z8$TDJ^0DAEIAjcmqsy$>aMfIlrT*Y^_t3&2xpswI=u+L0%e)RUw-m#FohWFWO{38= zy^B$C(q3MfSpm}55t;F&d!_=0@`uzG!Q1(OebJ@Mng!NhusFwGlA$n3F%-^kHt!dT z0IJP3jxygq=Z6--T8mBG=*w$bXJRZ_Eb~Cf>F{-+U5mwp(YeAVM;JOd)OBAy5bu^EjRhuHY9moBxI-hTg6rmVh@A5NvFiS4DGZIC=aJ9fIxHgwIA0yXpSZIo6Ha+7{ zd>d&Y5%oRwhH8Sytb>_yAx<7tpdp>gqmk~!>hWlHk{31%7u9LG?W4rJ? zs-){S%wY*TS7_3@S((L`Ssl>7_vN2z&&J?R2_^`PU!4tZY5dCTb(qd0oSWA8y#8I#PWs>Kgf`BH z_fHQD6VJ%Ij;o%FA;Okaur97u_y3WeyqL|4J=#kYQ(sTUNLDwL-j8#>u^BPzxx(p3o~XLO6h0d%cS`UuMZ&Xi>gTcnuVdXA1yEO1b|+n{AA3y@GFz zL$r>!+HGx78By@L{A!ENKMv^ck1_7qIFe9NV= zT>kJB?cuCCawfLMEBGyXD)_MR+i|4bHxX=? zC6MG3@;_5(LPT@$d4Rg?xE&@vGx0Yu_PL*YcGwH{H{9+azxCIh6LJis?gLMyCjPkNL zeoY!ui%64p^TAS-Hu&n;C=7O*J>Bg^jNq&%DFs$LN&!pgtvMp3qbttm!*_{*yN{aX z(@tDbsq>PtFE(MzN=F^6ry3=%yhKi#z{Q)pmni8_&ahrFbUh=Dk+w{?RQaw0hmqs+ zjI!zbv4xo&wTJ)4vRFLGUxN3EV^Iif02k!zo~n`6g7 z7QCEu90QGO{CZ-=9#iCr((}b7E+L2DnWjAr*nrb1ZcvT+fg(pHBbBMMrkI}Ep{NVX zD=WD)ErpHW!9T0Q$HyD=ToxsrG{eIKW^{1jt8@~_-_Jik@>KUuzCg3RgGxJmvsw#D zy%f<_>uBK<0E5$Uq0KA5a`(PRDk-@vv1%C%RgD@iVwbyA9RJ>wb>3loE*W)3)-CBNNvUpVK%XOsZ0IxHA5*LA~FMY@pH}FJTBeq>8`U*ueSM zF@Znr5i){}Y`=s}en#z5?`kF==zuKwU*;n3soGiy9CINbU-aZQ9VWDYTelw^DHiBhjOwQg80RV}MOPN`BVS*|HyN3HU#RSrWb8vm%2 zo%cqX4J(~19Ls2L#M($QBd)z z-2^9+Haj^#ol^hFE%z$)AY8Bxgqy~NA(r8_ZOQ;P4duQDh+mByVN7r?ok9QhuYVps zD^pugwfFDT>p|Z;L6g0Rab!R;I7hpd4kTGLHTM&2wGZB9$Aj|%P*j@V4x9n`8mQBu zvDkJV_{D6;?+Nx5{oUtjysrk~_(d-%mc(sk^;R`cZ?K-k_mlc%*(C73GWUbBtE##Y zOAz${p35rouvxLFJ_!`QxCniL`K5Nv? zo=4w5u5%OB8g2ttjeg9|W+r`-bH9X z-!J9B2Rq_w0kuD+M?%nPZ(-h^iC&+Em3e@%G?4!R)pgYM`ZK--h6}S(WL=Rtc!Z~? zo;b+t!O2XXbaWf*^D4{dnXbvw2D&?~itECfR>K$uJlWDni?Nug4(8rPf ztJCXMsIN=IS?FJ?ZD&8Np919Ltq06fpk4*Q1{3W15@tZZ=!Np;ozbdewQ5APDnHbgW!_&+%)>u9B2%K)7s|T<447!jl+6e#Ty(M_IUw<)#zjX> z8j}3tRiBnqV|mKBwT{zdr2Bj<;5FwaBApPA7{+{bMI%Un9etN0xIkL#vAx|0X&95{ z)^#3zo>{;)Hmt|0fRV;OI6|q@&~Dali|`iLU2}FiQ;OKniBO8CcwfdolvPjP0KeCG z1Z2;mJ`suD2n*p`jL`!fNt2U42jWY=_O`3TER7`#-WRl0L`*^hVbtrjMWv#n`VGSI zfTY+#+_G+a$+xo94cfA=_EMXJ)Fnn*)<(9kbv$F4@YY!mj%wKN8Aiyp6QKGSpit+6 z_kWxdI=TTrlNTG!__=)9D0j0)V23U1R!y6j5JoG6GWi=Nrw%{U)L3bYMrL`NQq9~eM+Y8)nIRth4sjpgGEJ*G{bChIP|Yk!D@7LW zTiLCDBV~v}p_0YGfkPxMl$#tiz1~(o^}8c=#5Dqv)sgp+<~)wDiqFNDY}ayETx z3bH9MoueA@3qTcplh;*il7Qs~5`TE>p)0wu8bWPWV47)g&7TS~&ln z;fKh{@5;~VYie#M3?4%a4EF1#s4Y)g@Ji5EpFeV{-C9>i}wX+AxJcLOc8W6ZN=xCvZmh+k^QDLR*0#5KP;cCzjITUdwOyO?hJu`z5fc? z(c%H&SLzm^{~m0&hgj{y(ag6!yN9SF$xwaA!)-QrMD{X`N`yEbC67^H#V6p2ic@b6 z2Ub;9;wB)-Yirl^`r+;!g1Y$*?svOO5uygA`F-76>K$J`one% z8Yk=w$m(^onG3PgMr$yG%@D@x^KdfFCE62N&V9)47x8pQW5d79}4xu^Wvkbemw_^Cv0&*kWC{l>{@s35_sMlTJITS>lE; z@>1WlJi=kyyr9y=F0r(&}n4jC|pD_LJOvD(T6(B^Ia%@9x%H? zh&ZBEl532~Hcc$!40ehi_G>zEUDa@E2ETw)k-BkI+qEbRQO_T#beGVXF_lP>!XT#X ze!vPNiZSSAXcG)_-9W@jktxEzMJYPcKA6-X1n-o0`y;7rbSZAi&)#3nONqgU2@c2f zR}~DH1Klg@H-@u;RD=N}_Nwxvs2q_SBjw;yIER=KCnmjSyR}1?`Iw_asjoKI3ekRu z;tN!kD7wD{d`1H&0U_Ll<365)p9KKq3N$va>0k1y+FL*E<=s?Ok#<^Sv_G^eOWDyq zq%gJWm8v^)b*f`33^Ran4QV3G+Z23f7mYeaRT>fVo^sjs-0c2J2@29~LIp@gfIqf5PHvsj5mWTgG@GXYxzD>YFMRmw?`-sf!a52VQ}=u z3_{}N_i5jD;UGfEDT|OZGy{Thvn2ac`P%gDLo=U`XWcBSk9V;`cU<)IOauiYV48Yf z>ze#yL$#E?I!)tMNAzlp2eubebP|E|)`In5`)u%Lu(y>%uz3kB6Sq!Xd4fOTXkvCt z!>l4+k>~%l04SHXw8(6+M~mMKAKmLsWXT-iu4v*l4ZU(IH6YWVSskMZIHd)S`jEpk z-jkP5qg`m%?zJ;1+i8~}=`Y|XC&>?T`F^yNflQJ&@E`Q3X_)bww86$|#jL;hno@{V zjItUpX|$NvspPOrt9{I10RtS6<_qA1%%F@K{?_kQ!eLxU&q>Si*^(Yi(jve`&SBs< zkLWrXc*taf^xlNDTZWo#E=0&JK zy+AmFo9~B~d799isZc)qIY#z_A~-E^iT%(04>mxil!pn&=`e?^!Gg;t99;QC(BH|G z*2f<{gDZu+XM#QxoGFXCsIrT)M7x+8#C>`u%)ArB?Ul58Yu!_N)*y7G>0gQ-u9KKc zt}HL8>7b7;82)MDhX}E&+@?kw{zEq1x&M=~z~g&MDZSWHJ<_7Q-%rgi1avIs{mk~P zer-N&@y*%690+ju@1dbb9~bRW!H`6I8eSo1)SeRDSlH3%Li4lJ zrM*g{tK`WXM>p`!V*OrD|Fm|jxEmAhjX@rk-&0gm_|X^9oyyO_F^D#cz3%5%V^}qy zJJ8f}0YB#-;U4V)zim);@zF^l)TZYFMeTR3hJb&*ZW-#~$?`#|_23~vNIp6~wE@vr?uCI9}2Xz6;!GrXpzi2!gv zBiaH9vIS;}e}RCLlUhDZw^On|)ByT;7!(bGOw&L#c!K&Gf5dFd@1j`ArJR!{jDXc# z0_U=2RnX70|4V+f4Uvg2k;$YiOOFc2_*@`Uo8%*}S$d|8xhC@HzdVa*H*ks-_Z+E4 zIw>tjs1qg&cOp&GM6`%CDO_<8e`I4lzz$fA#QvMCc;2rDYOJwKQ}2oj;_rprL)W>K zNM^Ix^+ZCzA>7Q9-rpR)sQ*B`cN=u%4pAh3Hj1G4|Ra|#4(;{9iq zF-vu1i>hjDd;2S@5I+rnS{Z9Yn?rP)R}jc?^V!}w8+HS<9@iZPw+?$Ekzxd7erVRa zf{-OtO|-R+F3`Yy`)^$Mh9Yz*Sspcpo&6W5(Oo9$q zv+iIraxmuZ8b`aT<>{d)_t47BK>BqfZS`zkBth+tY>v!S(iR&uOc`cr;VUA zrU`oQY!DCQG#sZica&XT0kH9d!w~jFka4A5T=m8OMJSvCX+V2bMCJIZtw;Deu-(cp z5o~*p%w}`VqfWpMtE&xsx5fb);eBa6qx_rm_~n0jyn8Y)LmfJFZoycwkk&zln+h=; z71te34e8O3^+Yu>AWJoq{SV_b)Id5lvmzJ|^sM`#$Iw1WIEvX_4p`oW-R7xx{MuCc zNy4L)&!Rl2yVj{14f^El4lI%=RgaNp`@ZPcDC31ot)~%aJh{>SdA1dXhZesY=OVun z;R_(TTek#%TjbXZ%!4BrqSkPozV}ui~H3v%CE(N|2MnglUfhl5UC*9X*X%9&$k}st#NTSSG990Yvvomtyeh~`55H1^~ zPue)oN7+1*J%_}$6o5jW{P;0isLP0)zH96DS9@5!gO_GeazCExsQj(A9iHw&03ol- zydL$I_C#P-oon&+krc^ANj3J|7bG5x+Tt=yIP;yFi&RrGfLX*E7azL(q4mvRi&)$v$pWO&579PNP*3ZsUtdV3- z@GH7Ahc~ZU#<+omEbDmukOsb}6kj>EPO*aRxK);ao|11pi1~M+%0!yBx{m6Z$Mhim zTJXC9%;#{6o$#NgqoG?v*4A4uJ@y$5$-rlGS7%LnCIoBPg=js{< zBL3z|4Saj<-TLGg92{l4^_bU|J=M8e z@<;D<73p_1nqMp`xr2?%84Clx9IPXP1w`DFH=|1FIfUX*A8N%6w)MKbQmi#- z0HO~ZYwS*WpKV4>;NI?w&_3K)BS)SD^2;jj<^(o%$9dcc7A~NR7^?t()2DjEAOFsR zXni%CCJ*XQi7M_W#ld(EIsxtPu{Vm@q|U{h5lB>14b7$Jw@O> zGzLWLHcEC~V4n@X2ke55B3n~D*fl7H2V%k|C`BJ)kMQi(>QXFzz(b5(Bkf$h@0GM5ht-@D%e|p2ZxX5Zg^GBjoFmT*Xv$ zmhH_XY<5HyiVIeiYLAR6dU?#oOw?E$axaKBn&1SVSt3XeJlJNN zI+O^Zl}q^XebJwu03yyxioJ&+D5+9BCE62x)UX3C`%`L!DTJ`q&m^HYDgu%Sd~q|H zABGu1Nu&#LMjM7(CH=&&SXxzJB45BL{!F<(vpOf9K&!;{{kFqJm!jbjPDC#hT>5WHcQ-c~yl3NI1%I6n zraw+e>~&)EJJxfb?D|T@94Xl+JWAv)9*7w%5xMT4O}K$dQ&eE}N3nUXBtI|yXqlik z6HT_N9$HjXJ5iB&-VJ0y(}xpH=jv|HO0wggMMsdu&8y0&1LjRwRVc8izkC(r<9|bo zWt>_CoASdvEYUSOYv{wBWG+~4vk=tZd!v#VfE!>MPQi1MqU$#aDj z)u$%3t%_7j8~silqirU=wPr@Dr?XTlf2x8AexN~E_r@xA*by8IRtQE)IcNy816<_u z^t09aQ=dcl``4eL6>&_CExSuG;@xw1XLt7gVG{j)d^TfoS(G1EU=iw?eBJs^LR3TXBXfokoS3X zVTAgv2(t{haAcgiBIw=$I2x>pF(h8V`5_uisT0)Ah0f*mt|{?ev`RjqJ+Y1Mjyu=o7sbgXiiB+E`ne^kwJe%zzak@ zSEpH+g~E3vE8=xd82SVbN9IZV3rj-cN7!VWjks;^;Yr$yEFfn9PK+$*b4=k_X;9z0 zh^Dw)D!u8ECWw>Bja`#uX0MXih-5km7voe=5l=f}|H7KT`j@iD{YR|JSaLBP{hp)s z6UcPBW;3(0af+|R)@@?d#{a;fh8Hyv!qS_5jz^PPAR~og2vcUhbBg(r&yYf5Yl#?s z;$@o6DYy40DJeazlfLJ3Lp)a8#|56d7>5?B(eLP2;yey&9}EH!-5r18ZLv{{3-zNl z;hNRbK4M|<C-RgfLrEf}9$ zbqOeK8xdWyZ)tk4Lr_t`ZVCfafiQ-;!XM$h(OX>i8Z9_aG0I%xF91eJ`C;|k?&Dd^ zgA%$gnq{Hwp#NeIf0h{EVcyF>p1WZcOrdu%<}~Hz1V`#Q@Wz&H=-1#$I_asY*4|H! zx+A+^`ySEJ{e*&WdwGgl)uwN46}B7|9AdOCF0pIMHYUP962Gb*HNERSLB1Zns)t1n z=aL%{O7Hk;aKd7*x!>%%V+sCg{y-!7*mqsk5n){_b&}(T@^^S7R4~%SQ=#}x)9(qs zC{&TN>$DQN{hJ5T5?WONsJL=W+J5W#vjvP%syebYSbCWsTOLZY(7Qk^{Ton6r^gt{9$;14Y6;!Szb4@ zgdGk905mpDH;^M5SfJv-KOR5seRrqd&$m`O{))q6{>hwv3{W~P(UR(a3qUfO)=6j6 zA+N3EUvay-o5FxUzBk!CI)!707l2Dc^Z2cR`+6|ftGd#Z|KVk zaC}nAJc@__2tyAqIR{1Wm{xJ8uDWLbwENbU>#uI+3NGEog2<>N0es-%fa zOK_jh7lor07_%Bg1Vh~x9zv$W6TW?cP}SaHJBm;?UvbtWGnEwE?jcslmraOGqCyE+ z2+}_ks)>t%14`J4XhRcEI7A(%vZ=nXHXZWT_huD1gzkM(m*KcrYGMKME`-R@N^=OY z7M11G)V0RUH8i0IQW+~7UnG_t7sYj8)gM`W(%J}v&;Sw?U3XVy(|NI|J^!OQ$< z7ipu!Qgut@_}2Mw99zVcpk5R2qa*mfuPL$tudyFpK_XIxGI^0D+juOOWWtp&ZJ8Nj zY7FGaJrew@D^YKtRp`afT^%3RW!JxBQ}+4C0$Nq#dVbi*w7DoZmQ);s71RP`KR43( zuMBTjTDfy)>L2#4WSdY9{88)yW>Srk6g@mty7GWj1~T318hlj497!ou(rlAoWRV0m z=e?ha#Pem>H+~G0LH4FO0TF)Q#Fz^I zkYyJZAbvi@FD=V@loH;%2R7Xc3|n_E{0a}Pj3MRQB&jH*I+fVK+ZDRDu1(V-5at3%~O z7M$`3(Ppm6v=+9K+%MupiV+F=bqpU_l+59q~z27&+f^??-tP^r7NspK$Qnt*S>Uc9`kUCV(;dE=>1j|lefw7*uV5<<6s;w-nc&^Xz!4;>pGC175I zsx4&c@rMK`>s{-2kT=WSiv@4 zKWd3N-6ac766DP_PLVvWmLpyln%sYb`+j6j0xCQts6RIc^sfpG4a2Nrq#zdTni5wD ztM)n2e!5_MQyb%l0Tsh?y}kqLS6*}P>QyU(bMZn7csnLXkb${9R0P~*v+bh^xZk1Z z@x7ppZnfOy@voRbysO>q?*t z5WTgTOqSsFGaF{W(EQCw_4E1&$_0fEfb8dHK+yF4|UwCV#@f z^X1^12S4{vhbF@y^swjIpp{=B62g{|e+?o0Uan)weqbt%uE*_52pL3k&@4J3LTV>5 zV-)QUt)t3H3dTHvuIXw#fWMCgQLV)sSxOf2#p*kE>6&kVkM5Mat^BfiINUbI_PvSf z>SFecTVHS%ZB0(q2CWNvu`IjP0D~ACT3CQv`XQka&MXch11y=(y!K_qBBejTLAZC$lqpGM6)+Om;T+GV#N!Sug&g^Vs}OxlExs zwH;l_lZ~&*hFxP%5c`o2Z509kz<~b(O9RV>(d)IiSvk3RCQj6GsDn|kxi$wa#6kEu z_kx9N<`PRpQY)|t_hFiYnVDY@FrI8R@^$$o#Q{cZ$hu()G!eLnu_y&3O3p1av!mwW zdSI%FiIw*(Jb5UyAtf{i-@l`)I*{ej(j2<5$8wB9G?HYx(TZeh(gibz>UnbT_{JY8 zoJCPWZ-W~pCR#o5Pz!$2w&NgmPeq3a9r9T*|EiBq-01#(!>4`@!-p1Os$p((v@1ux zhG0%z#0$=y)Rykb^L%atRqtwErX>?XkbMSVPI&06h_p5+xLrnTbG4+!YFneC$XSS- zhS&Y^&=ajhi$AdcXKgLI!Goua4puSf5R`k~y7iwz6rW`%iL8;4BxH|0R2ilD;4mfP zYSoy*){QqPUgldrafaQxdH>x5?Byc?0`z7nDlX2P$*(Knwsb_}SDp)A-ryY4mg!bi zz0mQcPn*}Y$3sWY2^@s85q!8Omeh6B_I0PCHUw6u_nROj3C;7bZW|b$ITj8w_<8Hu zjZ%iTKSwO>GZ)gdnBOl=LA%&P@V~b!ucilL&TLkL+jF-(p)`a`TNmT}nMtK(343n4 zyXfXR!}W*x<)p{w(K*)6N8UqpP0jSdvZ{;I&hqv}e%3;;1bihwC_@R1AJiuj&N**+np$1yXLQUP3fpC|nG@A`^7S2oVerpCO(qu^ZYZ%=pWTx=X}H~J}V zyb%=RcQT21GygoUHt%ygv?P4xY@5uP@TynNe97v{(x|etncu~e9AKX>z!8C~CBj$@ zLK8uXPGk{bcw;vDVe`4J(z1f-iy5swWo`_w;L%XAD4y*Ks9MduOVv3@SAfHCT6~1L zPOk-S_S5NX|4^%zA^IXj-1blmu&;qe&D+hn8n=YcGA~O;7KE0#r3Sit7=LyRB{hrXXt&&1~#uwAsqNVSHP16 zt$+o`Adm%Vd>`>l464gy6^i1s6dm^Pb_ocC%W^Atn)Ug40V}60dnQYKwxRU9+ZOop z3SK-zIehUh+8k+TQ=4Nn9Q(v@QHNK(!Z4a9$#wwCwjqNd0yj(k@41Dwz0{tAZDjeM zAB)+NIi#Hs+s2UF%`OYpI0E#1vIP$;)#RIXZDO{roWE_Ket3`kaY`J_>gxX==B482+)}KV+n1$pX zMc_a~bBXU0X%{;Dv~=v^r59!_^2-MFamdKezhj{^m;Ed!CdbHT#=t@B8&khMI;{HI5u02I~nFaJkNg-kYsB6WwCo_sB?i@B#C6@IFgH!Naa7>${8=xlQ60C0jiQPsFj)W|H>7WBIn{xR1e ziZY|aq`rN#1ZF~xV=sopmh`upj<4~MFmW$G?DI<^8WHSiTRQ5kP9qIWK9W|}qu4l3 zIW#NNqpoe0hQ$BL&{9)3`;yD(fD<0Nrdhi2DkF=$nS*uhwOroIz z3@}-=YU$w<2=M`uZ4Il({;%KeHUJ+k@yH&lv(rUuOIeF<-7CXgjZUT-)lu8~y59zUVU8_*U6};uw&h>`ZK-y;KEcgv% zDA^L0bbrn5`a+l_+BlrW6)&9RuXS;8$Zh_$rvXc0jk^hSSji19&d}uS-MGIO(IiE= z|1NqNjq+>GS~ln@2~HaqDT(YkBh{|SYg0}WVcrW)&exhuH}hgtz!{k!Jnm7;K!!IR zA)(6-TLTy0}jQea5#X* zA*{Q982ge>-Va-_u=5Vpw_8gm-4hrOg815i*>Q;cPmlbn{4w>M)3_?+(upJ z>wdlTX&)dOT8)<@w&puDY^(&?)iLwWE+0_Oj|q_II?IvR$T!mRiM|IJJG@o_mB1|~ zME3LXCcF&41JR!cV1~3))>nQc4gzI0A64burMxi**nbAPLlUh?y046k%+2@fpsYe5 z<+ zZrVqR3Wju>l%++bT{cpJw4Ksx9+ym_z&Ig3&LD^FlRxZ9rNxkw=TAU&+jhcpNwj$|mRdJ$op^2di;*y79*-9zw(-cQ+JpKY#^R{6z7g*oRw9OcCT>T#3!&R- z`tB3TN~=$^bJN)iZ+SrAFdS5ErE8~Atx`oxP1kH64z)4yBomHkyJY=hS zHKUcWs#DF~M7@PiWztusi1KP(FHg*T^@;h1xSiYvq@aSQOHJMl%S-0|LxUt|QDy%5 z0cKuqB%3zngxW1X)IKi`R(}{u_L8~5l6$xmw*t>+Da@gU526myd@?v>mcXh;7#Gto-vO z3D~PatAg$$g!`)<;i`rCFdXZ!>$%#8L-^+j5BbT8ZfYzEK-9u3(4q2NevRVu6DfMNE@lXsYM%bzw!iXYQ1N0A8G+JB>VYZEsV zL7^&H)4|!sh$}c^u5`e9pXna$a@VNDz3r2w?3Enl_xhhd$ZC2XsC55kVf@zIOmFy< zr%%ixgIaU?yPa8`Y)|J4c-zLaeSeMO;g+lb;}+r)8$J?NmMWQ@;VP_CPE!)U{jvWN zJkbF@XUG@peBe&NYBPg%iUh+2$s9_x zQ;{h{V!*Us3)X?!>&@+}!}9ad{8&X?nSy_$M%}XU(gWU>4VA*V#ZxFZ*Xo(# zcW>07*g@Dab&fNVVn!wnb}sm{jxKnzF}Ac;(l%$U!yUs?yaLhf#y z8%{b5iv#j5rN;R%x1Ctq#^fI6773V>N)}G`iJ2j}E<$x!BGHi8$hRXal^6oJh`Lz^ z+;Q(jQ^vLTZA|5Dw}w~G#q}kw54uhc-CWZ-S!-4E0LlJy!tH`)RE#TXn{JqMeC)`e zl>~Rb^h5yLLEin4dCj$vN(IObyq;oBtCGhdeok4l3 zyoA5DPS%^9PaB3J5D7IAXd-VStt!cH1y)@h9_$VUTle5xBqd|4{;8s^@6vumtui0x zN5~MkJkiQ%LVl z1EBTGbjcMpYhZaFWKPlNM5GevnT}|l;NHxG0yqLol1G9ASA9afrhI4uM# zX8LQch!M@1$r*h@SQm<7Bq7bt)InzrA@*udj?mR1Z=S)mAt0*fH{ll^xD%QE{LN=? zJnqj7P`_%R)&KF_Ckbb5vu$aSL4UYxApbxc%heXB$&Titjb#ZVPu$2Gh~+o_Gt^`d0rEc-^9cXC4m?9N#;uA@*iOEU z#AIeV7<&Er`E}8T;(>N>!%GS%e|G)wiLt`)(%8xEg7Xpj0Q5^0%l7IzjGda(QnJY> zt4%Lo>soT+Q=m&eC$wJ2tzvrcevylEx77F^07Jr;r<9p>8N6zQHrJXi#I(`0{bdif z(R}S1B|o5$fcXZM-WDte1w`sQGK5&QndI=QIPj0R*LNkTYEhuc14wGEOKAbgO=}j;1 zbs0^z0fSrClFA{DGDP&S$fAOGT*3N4(!4KzD9pyB#w@Wi%na+Osy9$GFMLsFb*G`- z$ex;Sc|%N~r>vsF5t#0ubA2f8z@HI;-Kd%hmFeSqIk)3IH~f1DVfwF|i2R=k4uG|y z)>nh64SShXy21gm+$$(nWioM;|D@w0-^H!BfsmqdAH*pntN`YNas(d^Yu$4tW+<6Z z4ciK<3gNvrAA2*bbsLzupUkMzFjS$VXPj1=C#r^LUMY}M0+6rzF!B9io{zpUuwK~7 zq1y*Apb}JFfMHGfW2fvZ8j{X*I`w0U*K=-~$K3^CDIk2}8;` zUdyQNe#qq6Vn1T36P*FSnm!q2exnZ7fe6X{SbDs^6nG`WNBq+@Wl&lmwO2RUUH+x5 zP0T;CSp9qB?{BY&FUqf+uYv`n76uxY*1(#^pk7I;;Nqwl<5zRXr>jTj}=9@+=EFNPjyx8^L#NueU+ho*{$e&?AUwYfvGFH_g;(htF8sDNjaA z4R2R4xw|Jo-w0_h3|P|{&&4Ry?i%Weh$%>-n+rr$vp>mV8&$qt4oGKToJ_^aMBVGs5K>)O-P%D6)AIQD+TiJ8yWNhMy;JY2{T+el^53{}S$j1hwxt5ARf4E& zFh^6{ubP|-PNW9yPhOaqmbBtsC&j!b)wL_cP9)n2`n`ka1bOD?vWVhhfJkqnZzVF4 z+yu{>u#-sa`h9Eses=QiYxE*w+*i+ax+O#}t#?DVF#B9yX~Cy86PKn);Xla462%v{ zH)SCMXdS^E2btFJtvNUrTDO5ip0iFdd9)FB=W~`ikESV)8)!y8Rn;JGiY)0L43Wk^ zk55yH%q6p8o4NMIdL;yHlJE{CSw6J1`4^ss-OW~WE~sZSUNV22mbYYM}W1e7F`E!>Kpo>hZ z;VLgsV#4ERew)&hLcuPlhq-183JS?4-^Y|`9SV@KJAYvPJ>#t&b3(uBXxt?-!_#h? zY3`8AZ}Ley>)=F?XL__sVSF-Rm-F!enJmwAafPXgp(3zwS1Yr$=F++KOlOR~xpyuK zg2}KL?Jt%^&l>(Wq=qS$WtE)QkrXYEV&E@UE%d$6kzvA5?vl{e2e&XFJ$&2>tiLoM z)^EN->8j1z@BE&guBJ1kGfi)j+_S(>5?Hb`p`ZW#4Z_bnQe&2R1XtDx{Q2zM+o{6p zM`5TPjhp@;8EaN{XDINTF4E=~z>Jtoy#tXuCT8aQmWqLm_HS%1JF{M;BFLFMTLz)2 zSglUR36juam%P~gk*S@|N8-K-Xz+19?8s%mMcfD%F7~fPnNIt1?`4DSvJCWT0x&xiiTAR*_uTAU(;kF#FfSQ_a@ zKw#+xMY_9FI%LUR8l=19eem~x_XE2ZyU)a#GiT=9_n9-oD5o%Ku8}v}KOuNNX!N$lz`jj-ZLK>3H4j7j#AzuWlN}Ew6B=wn`mL76e}tDq+wX6$ z=6Ty9kttm$m8tIWp~>xEIk7@t0aGK^Iyb+Yb<*VIed2rB$E)@(uS?31E{eJid&+wqu>NH}Wyp0S zw?#&A!Ac9!Qr*N#BpWM7R_KcpgVsBG%4vg_(#!~8{Wae9D?jn((biVd_S3NEVv3>3 z8r+-g5X7=YZjrnDN5Zqtw9<=^{;~2v**!{0JCfd&f(UdW>*lU4WnW|iy>3fzKvrk> z+V?436fEV?dV8w1xhX25!LzXl4HmbnJ0Ly!mcNIt9*%O>@LC;TH?;i%bhnJ+_w9Cn zb;JUEeN)t!Elnn{N58=1y5~CcgLm~L0Q_M4>^+2qO}`cE1m*J?Ql{`NH>~9bJv(rs z_qdOG(=B0F?Uit5dMsV0&^4Km=N%p&(M&)5ap!{pEikhq(QW&zT3RTpaOzFnP~}k1{-(_6RD^$@xv3l} z!YSWqfBHI_rtAQa>#s^SDmGZWtUgm_ZMZqhlLtz(3q_8(4cHy~$D~G(E zeV@{&9j9GF%cy=sB<6IiMqKgfI@Iv($d+(QR!_wPn-=vFYBZWU*t6PGOJk0Zd9THY z?6&V-W58~W>p62Tl%kl>{UZOsdv_Q_OMg)=e#IQ$w+E@zwsXfQf0dFhaIy2T=eb$K zyHEWt$hN2bTB#5Wu~^kEBj&6u5acjh$H>d{H;Lf^u9|B`cdg>Hd|-C`L(gCIVPqM0 zpS6g|{3c3rzel0bUfdg?2~{~0Vs~>=R6`(TH)tr{PG5E(#cvb&iLvo_LtQ&*@i-so{N=fiSDlF4KziF51@nsl{Ib4Jx*YhiVHEBr zg$%pfl_bs!{GrrY>YY=uizdCJ7^%H(o5{gAU+kI}!+&_v&f)gjGD~M43LnVoLy6Xs z%5+fdqtvJb$p%Ec%<}H^m&W%gPIxKJC=1~;5v@YSJL&eRvq0u7=q#C=@Oi<$amk%L z@*zK3qSlD+=aY^N-&2*&!9_DgR(MS`--+UraVUyWAp-WW5cXE3ZAeQ&>%i+XC7et( z;_Y9*erEo*{h@5P2)5D4g+COS_}xCN`N~sCb3}$xCaQ^k_~4gJLScl_064sOk`jR5 z3m~Ph^Ue=SWdr0a}z8)RBZ(PK&U1;~&f2%Ii z=T#aGWf%|+m%l8gGAQ$eTdm^PqZt+nxsurv|4RSGTT=S^37wI6sv>NEj?N;jv~1OF zvWQfcvp*bI>>IFT3ZL-WXNII6hbn$-UUAFY(oBE5qLcB z=*{uVxs`T)&;Fwn52cWcHqc^__`h;D1}w2x52_et$#?)FLC{k;k2M775Rce@-S^3hp) zGlC(Nor<)SBhKqR{kuiHhQh1a2Fq!`%nF+^)`^8I!Atl;QE8&PSDV3_IR`j27$G4%z0UW-3q7c$Gb>qfh&#Af1>-6&f7l|3OYb<`g+f;5$- zS_jc}Xm+a{?JnHu=t~6yI`7kb_*ax1J})g27oA&(AH#g1TbSk1{4#^bLbm2)x^IKA zB^EutU#LI1J>u0mz#A~^j<^5uHR;4bUwPL{-2r9?5FIdPb$I|;0q|Er+j7QizUnH2 zKkQTL*8GXtY942Wo5izu5=57pqk(&yn?I1Kv`Bxq7@Y%hP^+6_kHZ*(uxbCJv`7h& zzps0A*)16G_UlUV8dfZ=SN6umLFi$F_f4hbsi&p;P1i)%XD7l8KwXDI|Huz=ED{(=POiha~W-#uZ4kBGl19L#372Wp3$z&@7> z3RC((k;}zE(xDPU z&vE;jttR(=X6{DU`OU#^LI76@4@$dl{-m=VaZN-|Q|65N*uOy2UjT|+<1NZ3> zkKFj{gcgFZ-7h@p;M_oAWZA~3z$2Je86~mN8*PovnYI#(#J;PJZwPSmy!f9T@bQE1 z2Dwo$7q`DRUeu1DBb1^R0i%1&`uZR5_t$XO=D9=|i)LcAqVYn8Y2dUeljs_KjE_ll zW&;lftAW!RfdnefKj}-Pe_QIuvs<7)+_<^;`D8ADh9DD20-rdPwoW<{tUmdCEFen* zFYa~xw5SVo9XPp~3R9kd%-%fq8gneBSQ{k&Wt}BraJ_WV8-ET`(@f}=m z<&%!ms-E%X*$H1NYpDTcBN!I`>zWLP3NCwqELY(v_aLgIrNqFZIXfo+)}SGOH!~TJ zfxczRx=e7(Hu6v#H5;3oM>Ywx8#Z9)W+CfeJaKkEd_yW|K{mhMX~q#TykY(pv!>r$ zE^y4+xJ`_RN>of}gLIcMY8f5i#OCPU;5&~zo2k*8x!(4zPR{6?FWU0wbE?bT$l%5< z>R^@Mz^HSXoSe@qG^wGz_~{3Yb_UsE&uF3Tj@ELEPX` zE1qUG;HF~*@UmkbhMQVoz8*~w82Wkp2Ve)^@bus&T=_R8DGII0n*B^Gncz?A3r#Sd zFFsfpt&R>p=C@UzljX5eg&X!qhd*c1w?^SSNbuh{IlhD`3r0X^cBv3MUR7*%l-c^K z{bG(NUg{rt&FC-4s-a}3p_N@Pjo=4 z{BNF}?Y$)Ib2_PcC#;GIGGrzi``|SiZ{$ZOIWHh0^dfIZjMKl2$d|8XEXF)(uFCe3 zSG2mRb=gi|mzeyu2a>w1$icK}xrtZ2Mn*5J`lx{yOeFoMc$}_KsuENbjJPapiAni_ z!U*QiC^tM98`W!7sIo!%B7#BqE&0&0kRi;l?98@0;o|-8-sitD7q$duE!z?U^;qll3Ss0M5UaG(j$!jDnyZOgrR zloS-*i=T2Z{FpH=OiW*X=s%jN{#Z)&+s%4|bm$wGyYYV2QqWg2Eqvbd3@Zg;XLcuS zC696Xn{chZ1DH6mr8+8BOI*$hhF7ay_361EW*e^6Hi{S~^yluY0EDn&NLpz6RG1Z|GZ-!Ro&x za-IIx*g+LT*gFH>h1B&D_2i##!j&VzcE# zNP1Dy&_*73)UmF?Q}8ops{!smD>@n>DvXY9oW{wA2Ky9*R9?^Zx-m=DB%x>%K>&VJ z<Q0y3jBz8^p$e#bH5Lo4Q& z3Cfwagj?Ff6EX+Of6SM5q0808+G|?wWiR*EF%r)JV|~c@Ps3bT)DR3YZX%ET&wJ}$ z0;sxS=;uMrwOZ5nc4gu>S9L31W0f6RQ`sD=`mYS%XA4v)u8oNKMbCRthojpDYRGro zzJh7^yGl3s@Rs6lc2#1)*v796zuoM~n>PNCvRu%@pPMg72;Jj(9|FydfhoQwc=mVs z-s|W=q{EZlC&`z`EkVPlWL#dqo0)hZa3`}8?r*e!DF7vYL3Dz9hV0zte$bg-K3LM} zGWmEb*%RHTT#k;0HGa$9tea}(sA^@YnOFC58i85l@ItjdNM)qR2&UVWH<&vC3UJBJ z`U-K~Tqtw)DW=k1fr;^CkRgF4Fi?)rj}E_T`per2I)rhzB32;ypl0xp=^J-F@SxnM z)h!*%3@b_K9BM-!OLPso%0AENSJb+(J(*JqH44DqPNWiGI#7b|cW@qd${z?q%Gg4i z&9VL8epd6Hj(khub-em=$KU~(Yqd%4!hniq;I}49Y(~0!b1BycV&Iy*z_(d{X#D;J znS3^oT3pv&gi`r%-NX|nD4L>`eT;&cC}*|w{VI~I96(%ZXjijc#!w>^L@(MlcJ0V5 zHNRSo1VZ{+$bYd`wh(v4bSskTo)Mp(iGSyELHeM2b$1HwWEwkdcov4t#dW2LOczy% zR)z94m6EO8nmPRtYOh)O<(^gXch-`Ny2ba0;qE}qBhuK+2l84Et|KGln{@)$j&myl zONP6?1!Bq#H0TqTmRkzQ)&`kFpZw+bRZhHC1)=?vc z^3n{yDB|DcJn%Q2LDQF6uL(43CTXiPUp;MCRYK-d*W}FjDiT9 zV1xeg;kCG~<@7|4iw{P9eQv*2ajmsPsm}lsS+R~l8V{xQ4Hq$dwI59_R ze0y z))dQL6TxN*Aj+uW<8Z)4 z_AM*0Oaz#9#v~f8l=-5^?RgP{81qYWtG>TxB~~j%ksKH^KU=|!Lln9!X;E07t*%2q zlB1PiznE8DBp6N|H&6iWzp{~U z&wJM0I!htWreq8`0UIJ5$P-l~(wl*^W@K_qf@HNA+U+HUSDJ8j9@D^RB*XuY1(<-! zGLIYfx?uXu1bJ(Yc6-GZ7fUF$&AyDnn@;W4z6tuOSaLR_P- z5QMtYO%VZDV&QNx8<<(b1uVch*d_n~FyzcIs z`s2=D_PyjXQa+AUx-G<@exvr6-&v`of|=4qZ?KM5h6DQZxv%>2sFrVTKr~$BO%lay zzD2qqan*+_s6lV57gjE_ocC^|AL^-yQ%3-;Vn#D=4|C_v#(~}d1 zhQKw0fXv6Idnz7(piCIa0ok%RT)V%;S~U@xym=wv|1l0P26GF5fkHgGbc3r;FC8Br zx@UR(Onjty_?o@h4E&b;t521Wj#jV&EZ7$U*a__;Y?~|HZ0UAG+cK!d-j$!NaqiQ=fLLH?)0y_AJOAp*5k(xjft1PzBVEa&^J5J;YOCL@J}=D8LuSM z$PJF(%>^DV%ThgJ*H)9WYIlC(P^V#G>IQiTS7XelS5g*{-0!5g|Y4eso0LXvvkmf(P0!J)TB4wlr(5lE86Cb_JOv43`5Yo3$8i6 zd)mXe-+W+uLl`hhvm`;B{_x5`Lh|#UZ#0X4h^zDX@mFeTRNb}ApyxM82|p*dnZ119 z`1LdSl8qf3EWd&Wb_QHVd2r2aa@D-~y2;0NulIH~6+2|PNceX~g%;tO6>Hx;-AY4ferU#?xXmbnd#|l8}d|CErRdSv! zqf;NXKMp=dh&~Sal`;d&<8ch(j95e2P1w&r zoO0tcRUoF0mJtYx5r^MWUJ(kF{EVoKaHa84rmgD!&Dl6h_Y4xcMz(0+jX`y-ETQx! z>5r|?t$l2uK;3nvqg+m>)ip5j~ zNx5QHs*|}z=|=5`JSY7lf}uDnxA8cZmWCX*F^H`{5?l$5mDefQ^Ez4Bs2r!lGoqJo9t{3 zP_zZNm_5QXsDOk9FHg&Fz%EpeFgv-fe zx=XJhQknaz9=1LktvnL>=qkDve}Q6cQ7kWMvsHVV?L7xfU9x$Ef^Q?0v-Z+#fGb91ejvZpP3K2|%OD26uCOF$C6%(zTeqsgLiBCC#b*in zRn_*K(Ejr?2lioDGm6(CC2@5XckN1(e3$I-z1eP~xtid4`m;#1h8tWNtixx19+*eI zHAxhkwX$oGP$C%RgIM?j1%^vcu191@GCOBFZ}L@sYJEu4ABaJ_|D}c_rX$!6C1pqa zDkbm-#^>4h-bW9~4t=LW?d9c}h>$#%7;NaE-Os*4IAvD)Yx7a35Z(q9m`$YRH%usZ zV92^|(i_aI=;7tkPPKbvDT;_>aA+*0FTyEd>pVH@u4?U{n<7_3t3pgrHMM8U4+A6K zxSb=-Vez?X`%2B07voHhgG)m1idxnJsLd=%Lwj>Gpnx4+y}xZw)G*X*7uRcNZ8?Zp z?j^*U;36PIOFq5RGw{iSC{jlDBBkC`S$+mK?r9}&D}QNXB1GOikL*p`ZnbJ?@qY0S zarp-am# zmG^aM2$`bKv;2_#+3l}l+1k)7uz-CLseXIr8siV136$|Fje$fDI+(VP2NMGygAts{+sP(>TzsGvSYuI^B%*dy^ z_IJws1 z7rUCmT#v+PZt{(=Q1(MN=BN_2gL^6|s=jU+aq!Vv8^oj9N?NfoxW^a0>!ezz*xc_W z0-xXX)AVGHINrPoK`_uo7|W{l+8KfxARh_l7{3pH%jO%%TP_k>`VP1#4x#=d*sf__ z$m0eIzPl=#_?QT6$b)lb09fxIggC)X`6b?aJXC7WTskDP?3ocF$cA^97L~;r!ZNPj zY2&253rS&&q;z9HE{3yprp35e5=RMU6?>;5Int!+hv0vh_VhVJ^FHLf|B~no)l$kA zG5HZdN=rkM+n)Y$uFbfOXE*9*MPMxF*(Wli&55f|;qQl;x;ndWx0^RUe4V74!5Qpb zR3%txMe3x9w37Dz@n@y~GQMvi2WpHee8GV}o5!$wAAo)mCySLJ0H%uD(lMndWd zKF!A2f|%jFMi%Kx3$)ms=zGm4=D|MdjMaqgqJO)2)_u)P2=_;+AbwXM=)UwSZnW~i z7ETDYaMvi;`N{$ptmrOoYGmOr79_MWEHc9k>aB}Y$@&>Zjz5m!?sQ}tYzD`Xb@ zv4!^;r?oBofgp#@K*>$%*Q>8AYG^fx7dr0)*RI~v4cS-U7AS#?dwbK-L>LkJQw}%4 zB#aO>2ZU{7Ad|>I&M&1shfnS5-C|TH7QsA(FHhFkvQ7sm&^GTJtadbigOcO7ZmDfJ z%tnhSJPsL|=wdaFh4_u#ox&Y@^Um~|nKa|@%9cLgRZ#RpyrQIeA?|V8gX-XQr;$%~ z2VV9jEpg&kHHjad;)S36izUW1uiAg)%n)#YTYYr3>9u+hoi3WPxxfPXSTrp(`ck&12Sns4o0?|3t7 z6$}3?-|JMANu)EzPecEA7zyd(3F4BBJqDQ(fz7VfZ4xh zE;{}D5iz@5hIy|^-QP6cp5L*w(d`Em;7G7>v#uJeUGJnc|P67~03aews(-ywN?Ml2)?HfXqXuwZnKJ37 zIRNlAM#KkL3T$IBG7dZmE+essbS)C4saho-K1+|gnG*S&l(NelH{7nYP7(o+`5;X+ z(c=cYz=+pz_1h+kgW1hrPffNiWe)cZ;Q6&rRpI@siXJ_hxFJQYoZ%zLy`(6U#5P-& zuiO!*KHUrn+xu9@IBj-IU!+!VD)5Kh=4`*fu}P~kAZ&!?@6N!k?tfz;5w_&`n;&Sa zG5JzhDum@u+%R#VW8O(VS+&tvVaM4p+;A@_8dm`utxT4TiE6dB4EJtZ`cBmzexFQz3?<-4=`N(NolA4VPD>S<+Mq*a96b zEQblMv9(r~)dqEh2+l#Jae_m{Uu-i1^1s)|@S()&^#q;uzN!%CyRljm*M2LzQK+-0 z{%e{%{dzFtaHdLWt5i_B54o3F*=InldpX^4CNKtOXZyk1Z~yZ3S9&Vi0cK6$D=CYs=Z1C0!F_5{-V{L|XmiEM_J zmQ}wr&3HQd6lge~9(mQgt7XP=Hfts zys@rxvJtkvt(w5iz*96nEtA#!(@mlFV;Bpc%l39gD^IV@tIh*g^Zs23Fi|RB{RefB zx&?>M(ZkFKHEE0?F}0L7c{#k6rwd2xc#8uqONC>Qt%>&^`H&f#Sv>h7wOe0Uk*!-U zq)2+y@gv^vN-Q?p*pyAKQBE%KX5#)RWSXGS&Wax?Mko z1fT9nY4!f=gY(%brdt>v?Spyi;DxvC8+Eu5IV{|#5n}6d zI9ka|ts9a1O|(GMd9bzwY^(qV%ok0PFU9BCCycyx!HD?fRGc2|MfBl)cb1q=dWdvb zLIb82cF%B~1DaY(+0z964+4H|>#p~%bq*c5;qUmw`Fv-tup93EDlwy%KwZ;s20i&D zxpm#6Z&i!)CKve|kD>Y}@A4Y7>t#rXPO7H%IM`C-vTJ?TjCY;buBZL%a&h92TSGt* zhpJe$h$T}#u^>`eH(ga_qt?v%-;0IU$h*nkxskC-SJ6Sm>w9{?qO9&6YsZC6fn<@* z-37a&#n!XT0#od(otsV23Ku<2#rZXYNMLs5_t4J-( zoD1J4;bt0v4~%R0tzr4)f)wd2a6ef9b>C`t*Q{Bmla)$**Oq7WTyN<}=eGFsP2xUu z6BUMY6TDb=A?GYPeuB9<4go<_lQd(Li`v}4+GtAFB44`g1Aoph?Q524@*6d+y|f9) z6-_I%=6y$h#Y<^UA!d32QU&d!)248TDMkW;a6ZS?d%vbd z+O&hm-qoV>o(wj+_T>@cRlZ^BFsv|hMcpgBj9sX>&-|iuZ&e>RWI+>C>MpMfXv>7; zzvviGp1`l_H+U4)Ux1r3tJZ%_c_vI~=ZV~JPAq5yVHXs;NozH~r#s;TF(FY`KoG{{x$Hd+6B0mhaX#-s+R%il044WBVa~8TIJojbcS1hcI`)EkK3_gO5$Ml56!d_#A=gF&#}IXe(}XJ*)F_Kogb~dgpaor~z{`=t zn(WrSX=YF4&$x^d3~9b_9>It=>L(ZKrbX=R{_$*C4t&$Ku)?RVu}vxiFIvr|-0m}x;$@nXg~^TBLA4NPWC9il~TesVv;{FS#DA z*1m{8&v!V=mYI<~>h0ld1zEb9gF4MRlWF-9$`WV4(2s?hUMy?NQ!2qK{OrI| z`kzQ-W8%YyT3*tRBK2R=&U-{e8jQ|Db%c5OC7u;`70MAjz+ z;@RP?y|5xul3W|>H%1>J*dhM`B{fj1p|Ut97~OK!ARcTQHJZkf%|;dzrK&V@=6*HZ z(nCPnupYCqR&D1sCfMyVTEC8^?hNCM*U=gPBG-G;DW%k0v{1itkG;8Q+Q&EA84mX6 zizFa(9$-A>unGxAY2hCy6N?QIze5WVuG`}^_SmH$?yn(v8Ew;gM5=rI9E16+l*v5Z zpiSwL2C*Wg+xgiqnTr>sq%)9PUQ2X`!P|jD$?eq$3yf&J?pxA5{QCxsRTOc99DUUZ zGMkwcw(A4Qy*0%M8cn~rm>-FX`^jNiD-F@*>;~8!b7cud&4CJ~uX)({uFR2*Me1H6 zoc-9Xhp=S^0EbvALD%P2W{FvG-5jru2W_Ujn0I*DJu08bH<~9#Tk0b|F|5TmAnS2& z%y7oE`8CYZ|=c%x4Xjx|q_#S@*?uXnlo-brsmxcrP9EzUmF(D2e zys?oyny2CLyC#kt%QDnNI&(5OE>XODVbyVj9XUCB==d@IOY{Ask7U^&dXRM&h#i4TjHlS|zqw$81l zo0zwuZ*8kx%0y8R?GhK-BtvbClx;1Li{p;H5*vb(tbrzULp#be8hP)maQ64Ga#xsl z(nMOMdYu3{UnrOXzO>7WCgdq)sBn}@TA2v4N^0j(PT`j$LK>(6ht=$hdYrgl^|C!2 zUUXfFB0A^!611qCLiICOk402|=SE3|&UC=^B-Lm8Dx;I{l6 zPMIWpV#%uRQ==O}OSYIUxAO>dR@X!W7rnOX>wZ z3-UFUV4aflZ);^ygVBEuekNq$_OQ-2)UC!zhpOz10>*0KC9G%s_);%dzplkokc046to_0A?+C6DeO;RVN8~8 z)fU{j+A91kJjz2*);VAC@UhOQ{LXtPI!-Du6qF8%Wz&|S;^ z^jqLOZCKtv_39C8^f+vdJsy1Phdt8n`k`dMzsYb+tqbu7r8l0lfGVK`-QiMu>|DQr z#M$FE&wYnXOzdi2WYXW(+so5NUiNfRFx5yu6~;W*08;ywo7b*yNM4|q6LOnf=~d_@ zWzP$k_@1`W2TIg#eqS``A4%`eKKwkOuR~aEQP-=A__1H8I3sk2O0Il*thLZ@ky>7- zXXX8*eBL1A(2=&%ec2v!+Ypf%j(k{$hs~pd(#B+b^$P74f$Ag~!s`sdL7RbOJ>o{& zjMqGKv?=Y+kZQ;englpHlrgpyk+KN~La14AER{Dt8m+AZ?6qSE&%}|dl+R5p=7Cz% zZ52BoUwRh2UzW<3B}O{_f-DF;D;Xc7*1O5q;`i7+%5<;<)Z}|6&x~S-lJU5-g1J(% zKfyRmgmp|y$b8{&wG6NtzYJ~zX-(T$DykL)bT?|}#|*5&ckA{?HzOZ?VgV_;lmByw zki9pU;Qen6u9e38exUN}c3Z;xN(LvDIR_pZIx`pqB zESGV@(SHTk4U*ur(pJAP4@ris#I^SXd!fdI$CkQ9ug*~B$bLs)A12Pr&JppHG;4=| zo4=mS;EADw+5OON1>MnvqR|a*ws)_t^6aHzdkd?Z+!^jWYu#~&TFDvL@)toJZ?n9~ zvb<0BTLeH$JX&zQDP>X7_ZmaH*w2po;XPXh`P!%1HMaqg(WhYlRltX{LdLLfSG4)5O)LZ$?g zWp89rG_H)%1K zaMlQEEB!`qySgFftrLB6!Vf>}7AO)ns{i>09|I_XX7=}wC)e*rs#S4c@h`uUJDMgV_Rz~1REbF zo+9A?6BY??(#fJG7i~Pln4?M$N>PvxF;S;i*DIQLYTppInWsA<8Kn~<5b#BWYijrN zqlM-_m{c$dz4zuJ)JnZP7M4mZo+`~_vkhm9YW==7N)r@MZ^90i3MO|~N99-yuDn;kQBKSxb^JkTym?V`)GolRVO zxX93#;cELknycgbXV{q`yGRbg?iqxtN7)oSv7a#v>l*y^S_ds|VcjTvURTtF66`IF z$eXSVPJ8vC^b2nH$UJ^%uQ|%U_V)UG!^)+xY)Hk61Tw%d`$utuG(J`y?rb^g85pjx zImh2&!5^^?8@(oU`W(0~e^><@nwIYSQA<#Pby@UHI+;|}SH$nh4Vpz!N|czM5!q7) z^02}FLam|2#y@pevrR?h7cez!jXN;{t2w>zI-wlZ-%z4|I3C~dA`k}V&6C=Ar@0ly z9KHP!iGV=-TwY3C^^{AOn-YWK6MsvW@KrgFxF1LMF{XX_++b2-OF>Dpg?)mhq39p& zkNrB!`{ztxG*??HsInyAN_aB~3A*b&DHN*Q?CaIH0-0cESXw|H+#41nP3?2=$v8>n zUu=Iwpz=O)%-oIa{Wwg-w(98B58E#5Rb9|6e<$8>!Auyk>rqU-b9}LyFsjhj;vL@9 zCdL?FQ7-IGk@aWynn_nrG#2SoIJDfalw&7byXl?dXx*CYo%hoXTNNVs6vm%1&CkzCl!-E>%kyUELI2Ii8d^=*$J z@BNkA&JND*p-5yRq9M~rNv)d+Y`o-I?G);{`^Y)4lJ6)5G?s^JS#3owVh0|}P%CcHbe z{q)W7ss`^^J^113ZMI%+^jV}xV(Xn_2FJ-%)4&Iu3I1tzp=LV=itZtJ6L9mBb%HY? zj|=$0ItIB6FNJ&)(_Is9>Xc<57J9WeS3a)!&hj|j&ks>rl!wR#z2okNKU4J9ePZgA z4@56PlYFffT%^1?_jM0oSM;&xnfWU8^hLUcpneq2{TS}`ZhjT8#kGk09(se12==W0 zTVvildire-_!B<32*)?)MBvjKD0QGE9|2G61Ei!a6+%KOdjGNwd^a`g*1g6i^n`@| zTQmDg+1wKzg0P z$Q9_qO$cv#QA9I{1I#zYJb@$g+Gd|^mntT>qd>eTUqU75IIq&t*w?dmE~?RdcKYTw znzOV*KC92N#LRhhsAa$X2f4%tO3bkC=_x(v`hSZgNXnOlhIy?pPx+8ahr#5X)vtZ7}_Eb7XvSv%r zmE`xMlC15#?4U#s$UW+82NG`BS~~@8q$+qYajPQbjlg~< z&t}|eZjn5`Qt3$FOew}}ISV+-&gNGYE`qQQ=Mp2BNR0vh3d?5XyK7M%)%8LFz9#E> zeihwL-Q3@g6I?`_?-sVzYb><5u9_MNS!$SqBB~M_O`4s)cCkLO`xATd-PLgcC#% zf%=zs_hb9&EbfAm{YMt(#Qg>q=Rs$o?BTw&7Ota#!zaY`Obtkv@~=t2pRoJ?786{h z_A7r17SzuN8WIXCwF*73dOd3*Aa>l5>3<>273}uLnQ3w4wmF|Vd1R*JyLl;xjk+1LN4c@OW zsLVq2$us^g{5wS&e!pNQo|;y4=yZFnKC2a6B#{45H_QoNl8@RSn^te+9j)+2Q!bvw zaHTJ0dQrwuH}`eRv|(@^)0Z5OS3TB06%WErTh+K@>B;;yHf=Ziw?P*Op##hQLwf); zzTE!Hjrp64;iidLIP^X9ETPZ>WBNJD2v;C+h2tgO7Y zlPZ%eJ*6F1RS|l;DlvAEx>b5G1v0OM^rGD^l3_ACs7Kw7)lUaNU`Y!yqx`*mKv}cU z!~%<;iu-y7lbVK)0||;@c)TuM=Wp-p@$h(qpY-A2ynosB>glIMgOUOswJ9X1UDStG zjRg@cK(}pbS1kI6^bas|s!!uV zb(hWFRjF{Gvwdyz{GlmMPZ8SK61D^HmFryF)z5z=vHmh{igjy>tGgzMjjok8APGi2 zlj4z%2gYx}0BDn-prFbV7Y00Z4%&&S{b-!}Z`xiMbfyQ|q#|$O0!J|(T>XhtH!X`Dy~fSuf`>wC~zo7msmI6Bd zu8efR{cruzhw3W>+{K^<5(z-W2-xiOXn6#SePuX! z$ zZEXQ10ACvRdmoYrqEwZ?QXvltl>TeEqrE<|9J3lO>=tD{eo{8zD%76Y*JS>_dav?# z`&vh)Z6JfxxT_C-Xq#KULP}nx9n6yPqWS=Se=_E#;$v-8mDb+rh`}2K(a_d%R1E;=#TF)2 zwZvTtW6eEB350$3vKKdR7)NTl1_)&jIt<;!Lc&!pVu>fN8?u&P@R4 z*h^5p>_+sJ~)OU~a@ z4E5Ks_tXb$NiqCES|{}YE{uSCYekp-3AO4(0a6!$tLeYun7D*w`f>d!T8$_z9UTbD*YI^s$ z$cG(9M}RW9M$GvfleSXw#Rr){lANl-S_JtUif~g@lh)!jiN@|OaT07v`sdmH+6p3c zG+ZT~?asv~4{1JY$d%NHJDrp5$8`C!)CH40(l_c}jMCvUBzkbq^(=DIKGDnwtHDnj z%<(w!Upl|-;JcA`3xKpG2AlO5AYMDLeR5A7i^>nY{-isRFY|n5JY6T~CO_=q2bqXe zGa#T4=X|Fwu6B_AaJH%1DHmyf^z*Hj{pWTDgdd-#EVT0W+uFRWmlb+(<6NRBl~=W4 zX%UJTlX5Wr&Z%5Fhfu?V1Uq*>)tFQ_9K@g0LZ;o3vbPji`#rqmyAQ#frp#5Pzk=Vb zhUR5Z(Y^;>zGs>Yq*UL{UgC5*HA+lzk!3N27@WXN8g*sN7G4wwm=g@trOTwo#l;;5 zzCL*c670A2joZl=79ghK4d+SBh(oi|WM&oDW@Cq=b4I!$Yp@yWlFCe1TSsn%%dW&2b1)2)=Kc2IFpE#pMhvZN zN6zN4HjN;Pq{!y>UY0scl={}oVb{ui&ENEc!JyXHQsZ0(9OZ1Z zdSkUt7jn7wmyI^;*uOS8YslC{WKnjao?b-~MaH{wJe=K5jS7?~KKKj19bU1$#$T(L zKPMrjqwy4g7P^MtuOvN*KMy8m{PpsB?^U_qBZ}vQBb$_Ohp^!f`dO3DR_N9gTQ>oP z*mODyqBiDP%WN=-cb5DPP$9;SxYnpqMz;?n)}l!6vVl??FYH-6FR4}-+`r?Szy#i$ z+bHtCOTpEB*$tgT(fwoah4fdXeB%mZ-%DA7uN^0=;?k<7!5k(owcOm^DXra5H%zK2 zEeeE6!*mCD`pL7Ji8|llRFzlGin;H(u02SpU*Ux=B=*1G5O%BmzW_4^%=y274OiZW zhv|D&ZCy#Mq>{0qCL$iwNtX_S2jiy+q z@wqrre!Nm+AOUfNm$pZC_bbXhR|GYy}tf0UjOmWR1dQ6bvqu>7Rc_ROH&?QO`4IBk&*Gu;-g5g zb@kX(6;0D%wOXZ`Y(o_H-avZnTs5gsmL;5X@ZN_XBiAaoXf>Y+xQvX9jPDif1qoae zNF1aX4$k?#sh|(3V2`d~7 z4wj-)g@-c_a;-GmM_fF~13*k)4XI2aQI2Auc!X3}g1Um&WzQVJM(>?ZGPX_kN0Sze zsvXk9q=CmWWn&V7Fxk~R9?|!thhZeo7=m`+qh$bt3DByd6@^g4nD)kQciZ~-RoJmo z@i%CMMh&8+YY?<>Xw`!~j+y(JmE+c$)1cTBgmwWK6dne`$35SGu7O4JCS-ArZ4J`( zDUziO4iX`iG&*NKOCrPp;$r_wfJLbONIB8zktP<9cHJJ!SRbxC`m2rtzq1kd+Q{6K zzR5lJhtQ8A9wBW^lMeerG`x5T?}9uKbj7`IN{Ua$x@T&#ZubK8n2$$=;)$*NMF=iXwG@B#hxP>*3$`VtjNqO|LEWoOQksHpReR^lySrs@a3OY

EG_`6@;3O(`gCS0;&~2 z57ifN#n}ksasOp+?yxE?aOcjTTEy=PRL@YjSKvd+2^^5R*-ury`+tFSRE8C&@Gt?2 zVuGr4P&$xARZUP%$~$MZizY0m*$)6jaDLXJZ8l$BWn^SzWPBg!V&FuyX?)>HgZGA} zX%03FEe>tedk^n@cx$c#=Nz0jEu)_ZrMs6-zQ0Z%63!ON!(%W%^Qp! zB073sGETMFHe=uA@)p1T`Y(utz~%Gx8_aL3Ey1xn0b#Vs8nka-!+!ZIOhORPtIsGe zKVwLBL|cMKwYcw!7^$f{cjMJ>s6YNGq#laEy}QEX%gx4bY6|Q9IX3ZQip);vJ#^fCsQ&$yh0}BPI z!W${zISm3XeTq>z^x#;xBXnrw1~0Dvj+TFhsGznb-mEU7LUHJbJ3f`wr}u905n6hb zTEJl6@W_p$AVGcV0DQWt@bk~VLu3h1aC3W$4lW0?P{N3s9S0dpBOj$~(=a`=~ zGBPqUzB@2xZF=t$vG$nw_t84~OsZvMJmCZQfLt5?{Z!&Rjsh(CQDdv z)K0b5Hpi(yI?uxhVW{hqEo*A5) + { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // U+003F (?) + { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // U+0040 (@) + { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // U+0041 (A) + { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // U+0042 (B) + { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // U+0043 (C) + { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // U+0044 (D) + { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // U+0045 (E) + { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // U+0046 (F) + { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // U+0047 (G) + { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // U+0048 (H) + { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0049 (I) + { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // U+004A (J) + { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // U+004B (K) + { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // U+004C (L) + { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // U+004D (M) + { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // U+004E (N) + { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // U+004F (O) + { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // U+0050 (P) + { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // U+0051 (Q) + { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // U+0052 (R) + { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // U+0053 (S) + { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0054 (T) + { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // U+0055 (U) + { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0056 (V) + { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // U+0057 (W) + { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // U+0058 (X) + { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // U+0059 (Y) + { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // U+005A (Z) + { 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // U+005B ([) + { 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // U+005C (\) + { 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // U+005D (]) + { 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // U+005E (^) + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // U+005F (_) + { 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0060 (`) + { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // U+0061 (a) + { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // U+0062 (b) + { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // U+0063 (c) + { 0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00}, // U+0064 (d) + { 0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00}, // U+0065 (e) + { 0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00}, // U+0066 (f) + { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0067 (g) + { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // U+0068 (h) + { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0069 (i) + { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // U+006A (j) + { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // U+006B (k) + { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+006C (l) + { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // U+006D (m) + { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // U+006E (n) + { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // U+006F (o) + { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // U+0070 (p) + { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // U+0071 (q) + { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // U+0072 (r) + { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // U+0073 (s) + { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // U+0074 (t) + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // U+0075 (u) + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0076 (v) + { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // U+0077 (w) + { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // U+0078 (x) + { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0079 (y) + { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // U+007A (z) + { 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // U+007B ({) + { 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // U+007C (|) + { 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // U+007D (}) + { 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+007E (~) + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // U+007F +}; + +void font_char(char c, size_t x, size_t y, u8 color) { + assert(c >= 0, "INVALID CHARACTER"); + + const u8 *glyph = FONT[(size_t) c]; + + for (size_t yy = 0; yy < 8; yy++) { + for (size_t xx = 0; xx < 8; xx++) { + if (glyph[yy] & (1 << xx)) { + screen_set(color, x + xx, y + yy); + } + } + } +} + +void font_str(const char *s, size_t x, size_t y, u8 color) { + char c; + + while ((c = *s++) != 0) { + font_char(c, x, y, color); + x += 8; + } +} diff --git a/src/font.h b/src/font.h new file mode 100644 index 0000000..9e695e4 --- /dev/null +++ b/src/font.h @@ -0,0 +1,21 @@ +#ifndef FONT_H +#define FONT_H + +#include "util.h" +#include "screen.h" + +#define font_width(_s) (strlen((_s)) * 8) +#define font_height() (8) +#define font_str_doubled(_s, _x, _y, _c) do {\ + const char *__s = (_s);\ + __typeof__(_x) __x = (_x);\ + __typeof__(_y) __y = (_y);\ + __typeof__(_c) __c = (_c);\ + font_str(__s, __x + 1, __y + 1, COLOR_ADD(__c, -2));\ + font_str(__s, __x, __y, __c);\ + } while (0); + +void font_char(char c, size_t x, size_t y, u8 color); +void font_str(const char *s, size_t x, size_t y, u8 color); + +#endif diff --git a/src/fpu.c b/src/fpu.c new file mode 100644 index 0000000..b743580 --- /dev/null +++ b/src/fpu.c @@ -0,0 +1,15 @@ +#include "fpu.h" + +void fpu_init() { + size_t t; + + asm("clts"); + asm("mov %%cr0, %0" : "=r"(t)); + t &= ~(1 << 2); + t |= (1 << 1); + asm("mov %0, %%cr0" :: "r"(t)); + asm("mov %%cr4, %0" : "=r"(t)); + t |= 3 << 9; + asm("mov %0, %%cr4" :: "r"(t)); + asm("fninit"); +} diff --git a/src/fpu.h b/src/fpu.h new file mode 100644 index 0000000..5b1fa7b --- /dev/null +++ b/src/fpu.h @@ -0,0 +1,8 @@ +#ifndef FPU_H +#define FPU_H + +#include "util.h" + +void fpu_init(); + +#endif diff --git a/src/idt.c b/src/idt.c new file mode 100644 index 0000000..f2197ec --- /dev/null +++ b/src/idt.c @@ -0,0 +1,39 @@ +#include "idt.h" + +struct IDTEntry { + u16 offset_low; + u16 selector; + u8 __ignored; + u8 type; + u16 offset_high; +} PACKED; + +struct IDTPointer { + u16 limit; + uintptr_t base; +} PACKED; + +static struct { + struct IDTEntry entries[256]; + struct IDTPointer pointer; +} idt; + +// in start.S +extern void idt_load(); + +void idt_set(u8 index, void (*base)(struct Registers*), u16 selector, u8 flags) { + idt.entries[index] = (struct IDTEntry) { + .offset_low = ((uintptr_t) base) & 0xFFFF, + .offset_high = (((uintptr_t) base) >> 16) & 0xFFFF, + .selector = selector, + .type = flags | 0x60, + .__ignored = 0 + }; +} + +void idt_init() { + idt.pointer.limit = sizeof(idt.entries) - 1; + idt.pointer.base = (uintptr_t) &idt.entries[0]; + memset(&idt.entries[0], 0, sizeof(idt.entries)); + idt_load((uintptr_t) &idt.pointer); +} diff --git a/src/idt.h b/src/idt.h new file mode 100644 index 0000000..9f4ca2e --- /dev/null +++ b/src/idt.h @@ -0,0 +1,10 @@ +#ifndef IDT_H +#define IDT_H + +#include "util.h" +#include "isr.h" + +void idt_set(u8 index, void (*base)(struct Registers*), u16 selector, u8 flags); +void idt_init(); + +#endif diff --git a/src/irq.c b/src/irq.c new file mode 100644 index 0000000..4a02786 --- /dev/null +++ b/src/irq.c @@ -0,0 +1,82 @@ +#include "irq.h" +#include "idt.h" +#include "isr.h" + +// PIC constants +#define PIC1 0x20 +#define PIC1_OFFSET 0x20 +#define PIC1_DATA (PIC1 + 1) + +#define PIC2 0xA0 +#define PIC2_OFFSET 0x28 +#define PIC2_DATA (PIC2 + 1) + +#define PIC_EOI 0x20 +#define PIC_MODE_8086 0x01 +#define ICW1_ICW4 0x01 +#define ICW1_INIT 0x10 + +#define PIC_WAIT() do { \ + asm ("jmp 1f\n\t" \ + "1:\n\t" \ + " jmp 2f\n\t"\ + "2:"); \ + } while (0) + +static void (*handlers[32])(struct Registers *regs) = { 0 }; + +static void stub(struct Registers *regs) { + if (regs->int_no <= 47 && regs->int_no >= 32) { + if (handlers[regs->int_no - 32]) { + handlers[regs->int_no - 32](regs); + } + } + + // send EOI + if (regs->int_no >= 0x40) { + outportb(PIC2, PIC_EOI); + } + + outportb(PIC1, PIC_EOI); +} + +static void irq_remap() { + u8 mask1 = inportb(PIC1_DATA), mask2 = inportb(PIC2_DATA); + outportb(PIC1, ICW1_INIT | ICW1_ICW4); + outportb(PIC2, ICW1_INIT | ICW1_ICW4); + outportb(PIC1_DATA, PIC1_OFFSET); + outportb(PIC2_DATA, PIC2_OFFSET); + outportb(PIC1_DATA, 0x04); // PIC2 at IRQ2 + outportb(PIC2_DATA, 0x02); // Cascade indentity + outportb(PIC1_DATA, PIC_MODE_8086); + outportb(PIC1_DATA, PIC_MODE_8086); + outportb(PIC1_DATA, mask1); + outportb(PIC2_DATA, mask2); +} + +static void irq_set_mask(size_t i) { + u16 port = i < 8 ? PIC1_DATA : PIC2_DATA; + u8 value = inportb(port) | (1 << i); + outportb(port, value); +} + +static void irq_clear_mask(size_t i) { + u16 port = i < 8 ? PIC1_DATA : PIC2_DATA; + u8 value = inportb(port) & ~(1 << i); + outportb(port, value); +} + +void irq_install(size_t i, void (*handler)(struct Registers *)) { + CLI(); + handlers[i] = handler; + irq_clear_mask(i); + STI(); +} + +void irq_init() { + irq_remap(); + + for (size_t i = 0; i < 16; i++) { + isr_install(32 + i, stub); + } +} diff --git a/src/irq.h b/src/irq.h new file mode 100644 index 0000000..27a23b6 --- /dev/null +++ b/src/irq.h @@ -0,0 +1,10 @@ +#ifndef IRQ_H +#define IRQ_H + +#include "util.h" +#include "isr.h" + +void irq_install(size_t i, void (*handler)(struct Registers*)); +void irq_init(); + +#endif diff --git a/src/isr.c b/src/isr.c new file mode 100644 index 0000000..a3884aa --- /dev/null +++ b/src/isr.c @@ -0,0 +1,172 @@ +#include "isr.h" +#include "idt.h" +#include "system.h" + +#define NUM_ISRS 48 + +extern void _isr0(struct Registers*); +extern void _isr1(struct Registers*); +extern void _isr2(struct Registers*); +extern void _isr3(struct Registers*); +extern void _isr4(struct Registers*); +extern void _isr5(struct Registers*); +extern void _isr6(struct Registers*); +extern void _isr7(struct Registers*); +extern void _isr8(struct Registers*); +extern void _isr9(struct Registers*); +extern void _isr10(struct Registers*); +extern void _isr11(struct Registers*); +extern void _isr12(struct Registers*); +extern void _isr13(struct Registers*); +extern void _isr14(struct Registers*); +extern void _isr15(struct Registers*); +extern void _isr16(struct Registers*); +extern void _isr17(struct Registers*); +extern void _isr18(struct Registers*); +extern void _isr19(struct Registers*); +extern void _isr20(struct Registers*); +extern void _isr21(struct Registers*); +extern void _isr22(struct Registers*); +extern void _isr23(struct Registers*); +extern void _isr24(struct Registers*); +extern void _isr25(struct Registers*); +extern void _isr26(struct Registers*); +extern void _isr27(struct Registers*); +extern void _isr28(struct Registers*); +extern void _isr29(struct Registers*); +extern void _isr30(struct Registers*); +extern void _isr31(struct Registers*); +extern void _isr32(struct Registers*); +extern void _isr33(struct Registers*); +extern void _isr34(struct Registers*); +extern void _isr35(struct Registers*); +extern void _isr36(struct Registers*); +extern void _isr37(struct Registers*); +extern void _isr38(struct Registers*); +extern void _isr39(struct Registers*); +extern void _isr40(struct Registers*); +extern void _isr41(struct Registers*); +extern void _isr42(struct Registers*); +extern void _isr43(struct Registers*); +extern void _isr44(struct Registers*); +extern void _isr45(struct Registers*); +extern void _isr46(struct Registers*); +extern void _isr47(struct Registers*); + +static void (*stubs[NUM_ISRS])(struct Registers*) = { + _isr0, + _isr1, + _isr2, + _isr3, + _isr4, + _isr5, + _isr6, + _isr7, + _isr8, + _isr9, + _isr10, + _isr11, + _isr12, + _isr13, + _isr14, + _isr15, + _isr16, + _isr17, + _isr18, + _isr19, + _isr20, + _isr21, + _isr22, + _isr23, + _isr24, + _isr25, + _isr26, + _isr27, + _isr28, + _isr29, + _isr30, + _isr31, + _isr32, + _isr33, + _isr34, + _isr35, + _isr36, + _isr37, + _isr38, + _isr39, + _isr40, + _isr41, + _isr42, + _isr43, + _isr44, + _isr45, + _isr46, + _isr47, +}; + +static const char *exceptions[32] = { + "Divide by zero", + "Debug", + "NMI", + "Breakpoint", + "Overflow", + "OOB", + "Invalid opcode", + "No coprocessor", + "Double fault", + "Coprocessor segment overrun", + "Bad TSS", + "Segment not present", + "Stack fault", + "General protection fault", + "Page fault", + "Unrecognized interrupt", + "Coprocessor fault", + "Alignment check", + "Machine check", + "RESERVED", + "RESERVED", + "RESERVED", + "RESERVED", + "RESERVED", + "RESERVED", + "RESERVED", + "RESERVED", + "RESERVED", + "RESERVED", + "RESERVED" +}; + +static struct { + size_t index; + void (*stub)(struct Registers*); +} isrs[NUM_ISRS]; + +static void (*handlers[NUM_ISRS])(struct Registers*) = { 0 }; + +void isr_install(size_t i, void (*handler)(struct Registers*)) { + handlers[i] = handler; +} + +// referenced from start.S +void isr_handler(struct Registers *regs) { + if (handlers[regs->int_no]) { + handlers[regs->int_no](regs); + } +} + +static void exception_handler(struct Registers *regs) { + panic(exceptions[regs->int_no]); +} + +void isr_init() { + for (size_t i = 0; i < NUM_ISRS; i++) { + isrs[i].index = i; + isrs[i].stub = stubs[i]; + idt_set(isrs[i].index, isrs[i].stub, 0x08, 0x8E); + } + + for (size_t i = 0; i < 32; i++) { + isr_install(i, exception_handler); + } +} diff --git a/src/isr.h b/src/isr.h new file mode 100644 index 0000000..0507ee8 --- /dev/null +++ b/src/isr.h @@ -0,0 +1,16 @@ +#ifndef ISR_H +#define ISR_H + +#include "util.h" + +struct Registers { + u32 __ignored, fs, es, ds; + u32 edi, esi, ebp, esp, ebx, edx, ecx, eax; + u32 int_no, err_no; + u32 eip, cs, efl, useresp, ss; +}; + +void isr_install(size_t i, void (*handler)(struct Registers*)); +void isr_init(); + +#endif diff --git a/src/keyboard.c b/src/keyboard.c new file mode 100644 index 0000000..6ec3cec --- /dev/null +++ b/src/keyboard.c @@ -0,0 +1,66 @@ +#include "keyboard.h" +#include "irq.h" +#include "system.h" +#include "timer.h" + +u8 keyboard_layout_us[2][128] = { + { + KEY_NULL, KEY_ESC, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', + '-', '=', KEY_BACKSPACE, KEY_TAB, 'q', 'w', 'e', 'r', 't', 'y', 'u', + 'i', 'o', 'p', '[', ']', KEY_ENTER, 0, 'a', 's', 'd', 'f', 'g', 'h', 'j', + 'k', 'l', ';', '\'', '`', 0, '\\', 'z', 'x', 'c', 'v', 'b', 'n', 'm', + ',', '.', '/', 0, 0, 0, ' ', 0, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, + KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, 0, 0, KEY_HOME, KEY_UP, + KEY_PAGE_UP, '-', KEY_LEFT, '5', KEY_RIGHT, '+', KEY_END, KEY_DOWN, + KEY_PAGE_DOWN, KEY_INSERT, KEY_DELETE, 0, 0, 0, KEY_F11, KEY_F12 + }, { + KEY_NULL, KEY_ESC, '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', + '_', '+', KEY_BACKSPACE, KEY_TAB, 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', + 'I', 'O', 'P', '{', '}', KEY_ENTER, 0, 'A', 'S', 'D', 'F', 'G', 'H', + 'J', 'K', 'L', ':', '\"', '~', 0, '|', 'Z', 'X', 'C', 'V', 'B', 'N', + 'M', '<', '>', '?', 0, 0, 0, ' ', 0, KEY_F1, KEY_F2, KEY_F3, KEY_F4, + KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, 0, 0, KEY_HOME, KEY_UP, + KEY_PAGE_UP, '-', KEY_LEFT, '5', KEY_RIGHT, '+', KEY_END, KEY_DOWN, + KEY_PAGE_DOWN, KEY_INSERT, KEY_DELETE, 0, 0, 0, KEY_F11, KEY_F12 + } +}; + +struct Keyboard keyboard; + +// bad hack! for a better RNG +static bool seeded = false; + +static void keyboard_handler(struct Registers *regs) { + u16 scancode = (u16) inportb(0x60); + + if (!seeded) { + seed(((u32) scancode) * 17 + timer_get()); + seeded = true; + } + + if (KEY_SCANCODE(scancode) == KEY_LALT || + KEY_SCANCODE(scancode) == KEY_RALT) { + keyboard.mods = BIT_SET(keyboard.mods, HIBIT(KEY_MOD_ALT), KEY_IS_PRESS(scancode)); + } else if ( + KEY_SCANCODE(scancode) == KEY_LCTRL || + KEY_SCANCODE(scancode) == KEY_RCTRL) { + keyboard.mods = BIT_SET(keyboard.mods, HIBIT(KEY_MOD_CTRL), KEY_IS_PRESS(scancode)); + } else if ( + KEY_SCANCODE(scancode) == KEY_LSHIFT || + KEY_SCANCODE(scancode) == KEY_RSHIFT) { + keyboard.mods = BIT_SET(keyboard.mods, HIBIT(KEY_MOD_SHIFT), KEY_IS_PRESS(scancode)); + } else if (KEY_SCANCODE(scancode) == KEY_CAPS_LOCK) { + keyboard.mods = BIT_SET(keyboard.mods, HIBIT(KEY_MOD_CAPS_LOCK), KEY_IS_PRESS(scancode)); + } else if (KEY_SCANCODE(scancode) == KEY_NUM_LOCK) { + keyboard.mods = BIT_SET(keyboard.mods, HIBIT(KEY_MOD_NUM_LOCK), KEY_IS_PRESS(scancode)); + } else if (KEY_SCANCODE(scancode) == KEY_SCROLL_LOCK) { + keyboard.mods = BIT_SET(keyboard.mods, HIBIT(KEY_MOD_SCROLL_LOCK), KEY_IS_PRESS(scancode)); + } + + keyboard.keys[(u8) (scancode & 0x7F)] = KEY_IS_PRESS(scancode); + keyboard.chars[KEY_CHAR(scancode)] = KEY_IS_PRESS(scancode); +} + +void keyboard_init() { + irq_install(1, keyboard_handler); +} diff --git a/src/keyboard.h b/src/keyboard.h new file mode 100644 index 0000000..2beceb2 --- /dev/null +++ b/src/keyboard.h @@ -0,0 +1,87 @@ +#ifndef KEYBOARD_H +#define KEYBOARD_H + +#include "util.h" + +// TODO: some of this it 100% wrong lmao +#define KEY_NULL 0 +#define KEY_ESC 27 +#define KEY_BACKSPACE '\b' +#define KEY_TAB '\t' +#define KEY_ENTER '\n' +#define KEY_RETURN '\r' + +#define KEY_INSERT 0x90 +#define KEY_DELETE 0x91 +#define KEY_HOME 0x92 +#define KEY_END 0x93 +#define KEY_PAGE_UP 0x94 +#define KEY_PAGE_DOWN 0x95 +#define KEY_LEFT 0x4B +#define KEY_UP 0x48 +#define KEY_RIGHT 0x4D +#define KEY_DOWN 0x50 + +#define KEY_F1 0x80 +#define KEY_F2 (KEY_F1 + 1) +#define KEY_F3 (KEY_F1 + 2) +#define KEY_F4 (KEY_F1 + 3) +#define KEY_F5 (KEY_F1 + 4) +#define KEY_F6 (KEY_F1 + 5) +#define KEY_F7 (KEY_F1 + 6) +#define KEY_F8 (KEY_F1 + 7) +#define KEY_F9 (KEY_F1 + 8) +#define KEY_F10 (KEY_F1 + 9) +#define KEY_F11 (KEY_F1 + 10) +#define KEY_F12 (KEY_F1 + 11) + +#define KEY_LCTRL 0x1D +#define KEY_RCTRL 0x1D + +#define KEY_LALT 0x38 +#define KEY_RALT 0x38 + +#define KEY_LSHIFT 0x2A +#define KEY_RSHIFT 0x36 + +#define KEY_CAPS_LOCK 0x3A +#define KEY_SCROLL_LOCK 0x46 +#define KEY_NUM_LOCK 0x45 + +#define KEY_MOD_ALT 0x0200 +#define KEY_MOD_CTRL 0x0400 +#define KEY_MOD_SHIFT 0x0800 +#define KEY_MOD_CAPS_LOCK 0x1000 +#define KEY_MOD_NUM_LOCK 0x2000 +#define KEY_MOD_SCROLL_LOCK 0x4000 + +#define KEYBOARD_RELEASE 0x80 + +#define KEYBOARD_BUFFER_SIZE 256 + +#define KEY_IS_PRESS(_s) (!((_s) & KEYBOARD_RELEASE)) +#define KEY_IS_RELEASE(_s) (!!((_s) & KEYBOARD_RELEASE)) +#define KEY_SCANCODE(_s) ((_s) & 0x7F) +#define KEY_MOD(_s, _m) (!!((_s) & (_m))) +#define KEY_CHAR(_s) __extension__({\ + __typeof__(_s) __s = (_s);\ + KEY_SCANCODE(__s) < 128 ?\ + keyboard_layout_us[KEY_MOD(__s, KEY_MOD_SHIFT) ? 1 : 0][KEY_SCANCODE(__s)] :\ + 0;\ + }) + +struct Keyboard { + u16 mods; + bool keys[128]; + bool chars[128]; +}; + +extern u8 keyboard_layout_us[2][128]; +extern struct Keyboard keyboard; + +#define keyboard_key(_s) (keyboard.keys[(_s)]) +#define keyboard_char(_c) (keyboard.chars[(u8) (_c)]) + +void keyboard_init(); + +#endif diff --git a/src/link.ld b/src/link.ld new file mode 100644 index 0000000..a723260 --- /dev/null +++ b/src/link.ld @@ -0,0 +1,29 @@ +OUTPUT_FORMAT("binary") +ENTRY(_start) +SECTIONS +{ + . = 0x10000; + + .text BLOCK(4K) : ALIGN(4K) + { + *(.text.prologue) + *(.text) + } + + .rodata BLOCK(4K) : ALIGN(4K) + { + *(.rodata) + } + + .data BLOCK(4K) : ALIGN(4K) + { + *(.data) + } + + .bss BLOCK(4K) : ALIGN(4K) + { + *(.bss) + } + + end = .; +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..acd9211 --- /dev/null +++ b/src/main.c @@ -0,0 +1,756 @@ +// remove to disable music, useful when building for hardware without an SB16 +#define ENABLE_MUSIC + +#include "util.h" +#include "screen.h" +#include "idt.h" +#include "isr.h" +#include "irq.h" +#include "timer.h" +#include "font.h" +#include "system.h" +#include "keyboard.h" +#include "speaker.h" +#include "fpu.h" + +#ifdef ENABLE_MUSIC +#include "sound.h" +#include "music.h" +#endif + +#define FPS 30 +#define LEVELS 30 + +#define TILE_SIZE 10 + +#define LOGO_HEIGHT 5 +static const char *LOGO[LOGO_HEIGHT] = { + "AAA BBB CCC DD EEE FFF", + " A B C D D E F ", + " A BBB C DD E FFF", + " A B C D D E F", + " A BBB C D D EEE FFF", +}; + +#define NUM_TILES (BORDER + 1) +enum Tile { + NONE = 0, + GREEN, + ORANGE, + YELLOW, + PURPLE, + PINK, + BLUE, + CYAN, + RED, + BORDER +}; + +#define TILE_MASK 0x0F +#define TILE_FLAG_FLASH 0x10 +#define TILE_FLAG_DESTROY 0x20 + +static const u8 TILE_COLORS[NUM_TILES] = { + [NONE] = COLOR(7, 0, 3), + [GREEN] = COLOR(0, 5, 0), + [ORANGE] = COLOR(5, 3, 0), + [YELLOW] = COLOR(5, 5, 0), + [PURPLE] = COLOR(3, 0, 3), + [PINK] = COLOR(5, 0, 5), + [BLUE] = COLOR(0, 0, 2), + [CYAN] = COLOR(0, 3, 3), + [RED] = COLOR(5, 0, 0), + [BORDER] = COLOR(2, 2, 1), +}; + +u8 TILE_SPRITES[NUM_TILES][TILE_SIZE * TILE_SIZE] = { 0 }; + +#define BOARD_WIDTH 10 +#define BOARD_HEIGHT 20 +#define BOARD_SIZE (BOARD_WIDTH * BOARD_HEIGHT) + +#define BOARD_WIDTH_PX (BOARD_WIDTH * TILE_SIZE) +#define BOARD_HEIGHT_PX (BOARD_HEIGHT * TILE_SIZE) +#define BOARD_X ((SCREEN_WIDTH - BOARD_WIDTH_PX) / 2) +#define BOARD_Y 0 + +#define IN_BOARD(_x, _y) ((_x) >= 0 && (_y) >= 0 && (_x) < BOARD_WIDTH && (_y) < BOARD_HEIGHT) + +// max size of 4x4 to account for all rotations +#define TTM_SIZE 4 + +#define TTM_BLOCK(_t, _i, _j) (((_t) & (1 << (((_j) * 4) + (_i)))) != 0) + +#define TTM_OFFSET_X(_t)\ + MIN(_t & 0x000F ? LOBIT((_t >> 0) & 0xF) : 3,\ + MIN(_t & 0x00F0 ? LOBIT((_t >> 4) & 0xF) : 3,\ + MIN(_t & 0x0F00 ? LOBIT((_t >> 8) & 0xF) : 3,\ + _t & 0xF000 ? LOBIT((_t >> 12) & 0xF) : 3))) + +#define TTM_WIDTH(_t)\ + 1 + MAX(HIBIT((_t >> 0) & 0xF),\ + MAX(HIBIT((_t >> 4) & 0xF),\ + MAX(HIBIT((_t >> 8) & 0xF), HIBIT((_t >> 12) & 0xF)))) -\ + TTM_OFFSET_X(_t) + +#define TTM_HEIGHT(_t) ((HIBIT(_t) / 4) - (LOBIT(_t) / 4) + 1) +#define TTM_OFFSET_Y(_t) (LOBIT(_t) / 4) + +#define TTM_FOREACH(_xname, _yname, _xxname, _yyname, _xbase, _ybase)\ + for (i32 _yname = 0, _yyname = (_ybase); _yname < TTM_SIZE; _yname++,_yyname++)\ + for (i32 _xname = 0, _xxname = (_xbase); _xname < TTM_SIZE; _xname++,_xxname++)\ + +struct Tetromino { + enum Tile color; + u16 rotations[4]; +}; + +#define NUM_TETROMINOS 7 +static const struct Tetromino TETROMINOS[NUM_TETROMINOS] = { + { + // line + .color = CYAN, + .rotations = { + 0x00F0, + 0x2222, + 0x0F00, + 0x4444 + } + }, { + // left L + .color = BLUE, + .rotations = { + 0x8E00, + 0x6440, + 0x0E20, + 0x44C0 + } + }, { + // right L + .color = ORANGE, + .rotations = { + 0x2E00, + 0x4460, + 0x0E80, + 0xC440 + } + }, { + // cube + .color = YELLOW, + .rotations = { + 0xCC00, + 0xCC00, + 0xCC00, + 0xCC00 + } + }, { + // right skew + .color = GREEN, + .rotations = { + 0x6C00, + 0x4620, + 0x06C0, + 0x8C40 + } + }, { + // left skew + .color = RED, + .rotations = { + 0xC600, + 0x2640, + 0x0C60, + 0x4C80 + } + }, { + // T + .color = PURPLE, + .rotations = { + 0x4E00, + 0x4640, + 0x0E40, + 0x4C40 + } + } +}; + +#define NUM_LEVELS 30 + +// from listfist.com/list-of-tetris-levels-by-speed-nes-ntsc-vs-pal +static u8 FRAMES_PER_STEP[NUM_LEVELS] = { + 48, 43, 38, 33, 28, 23, 18, 13, 8, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 1 +}; + +static u32 LINE_MULTIPLIERS[4] = { + 40, 100, 300, 1200 +}; + +#define NUM_CONTROLS 7 +struct Control { + bool down; + bool last; + bool pressed; + u32 pressed_frames; +}; + +static struct { + u8 board[BOARD_HEIGHT][BOARD_WIDTH]; + + u32 frames, steps, frames_since_step; + u32 score, lines, level; + i32 lines_left; + bool menu, pause, stopped, destroy, game_over, music; + + const struct Tetromino *next; + + struct { + const struct Tetromino *ttm; + u8 r; + i32 x, y; + bool done; + } curr; + + union { + struct { + struct Control rotate_left; + struct Control rotate_right; + struct Control rotate; + struct Control left; + struct Control right; + struct Control down; + struct Control fast_down; + }; + struct Control raw[NUM_CONTROLS]; + } controls; +} state; + +static void done() { + // flash tetromino which was just placed + TTM_FOREACH(x, y, xx, yy, state.curr.x, state.curr.y) { + if (IN_BOARD(xx, yy) && + TTM_BLOCK(state.curr.ttm->rotations[state.curr.r], x, y)) { + state.board[yy][xx] |= TILE_FLAG_FLASH; + } + } + + // check for lines + u32 lines = 0; + + for (size_t y = 0; y < BOARD_HEIGHT; y++) { + bool line = true; + + for (size_t x = 0; x < BOARD_WIDTH; x++) { + if ((state.board[y][x] & TILE_MASK) == NONE) { + line = false; + break; + } + } + + if (line) { + lines++; + + for (size_t x = 0; x < BOARD_WIDTH; x++) { + state.board[y][x] |= TILE_FLAG_FLASH | TILE_FLAG_DESTROY; + } + + state.destroy = true; + } + } + + if (lines > 0) { + state.lines += lines; + state.score += LINE_MULTIPLIERS[lines - 1] * (state.level + 1); + + // check for leveling up + if (state.level != NUM_LEVELS - 1) { + state.lines_left -= lines; + if (state.lines_left <= 0) { + state.level++; + state.lines_left = 10; + } + } + } + + // new tetromino is spawned in update() after destroy + state.curr.done = true; +} + +static bool try_modify( + const struct Tetromino *ttm, u16 tc, i32 xc, i32 yc, u16 tn, i32 xn, i32 yn) { + u8 board[BOARD_HEIGHT][BOARD_WIDTH]; + memcpy(&board, &state.board, sizeof(board)); + + // clear current tiles + if (tc != 0) { + TTM_FOREACH(x, y, xx, yy, xc, yc) { + if (IN_BOARD(xx, yy) && TTM_BLOCK(tc, x, y)) { + state.board[yy][xx] = NONE; + } + } + } + + TTM_FOREACH(x, y, xx, yy, xn, yn) { + if (yy < 0) { + if (TTM_BLOCK(tn, x, y) && + (xx < 0 || xx >= BOARD_WIDTH)) { + goto fail; + } + + continue; + } else if (!TTM_BLOCK(tn, x, y)) { + continue; + } else if (!IN_BOARD(xx, yy) || state.board[yy][xx] != NONE || + xx < 0 || xx > (BOARD_WIDTH - 1)) { + goto fail; + } + + state.board[yy][xx] = ttm->color; + } + + return true; +fail: + memcpy(&state.board, &board, sizeof(board)); + return false; +} + +static bool spawn() { + if (state.next == NULL) { + state.next = &TETROMINOS[rand() % NUM_TETROMINOS]; + } + + state.curr.ttm = state.next; + state.curr.r = 0; + state.curr.x = (BOARD_WIDTH / 2) - 2; + state.curr.y = -TTM_OFFSET_Y(state.curr.ttm->rotations[state.curr.r]) - 1; + state.curr.done = false; + + if (!try_modify( + state.curr.ttm, + 0, 0, 0, + state.curr.ttm->rotations[state.curr.r], + state.curr.x, state.curr.y)) { + return false; + } + + state.next = &TETROMINOS[rand() % NUM_TETROMINOS]; + return true; +} + +static bool move(i32 dx, i32 dy) { + if (try_modify( + state.curr.ttm, + state.curr.ttm->rotations[state.curr.r], + state.curr.x, state.curr.y, + state.curr.ttm->rotations[state.curr.r], + state.curr.x + dx, state.curr.y + dy)) { + state.curr.x += dx; + state.curr.y += dy; + return true; + } + + return false; +} + +static bool rotate(bool right) { + u8 r = (state.curr.r + (right ? 1 : -1) + 4) % 4; + + if (try_modify( + state.curr.ttm, + state.curr.ttm->rotations[state.curr.r], + state.curr.x, state.curr.y, + state.curr.ttm->rotations[r], + state.curr.x, state.curr.y)) { + state.curr.r = r; + return true; + } + + return false; +} + +static void generate_sprites() { + for (enum Tile t = 0; t < NUM_TILES; t++) { + if (t == NONE) { + continue; + } + + u8 color = TILE_COLORS[t]; + u8 *pixels = TILE_SPRITES[t]; + + for (size_t y = 0; y < TILE_SIZE; y++) { + for (size_t x = 0; x < TILE_SIZE; x++) { + u8 c = color; + + if (y == 0 || x == 0) { + c = COLOR_ADD(color, 1); + } else if (y == TILE_SIZE - 1 || x == TILE_SIZE - 1) { + c = COLOR_ADD(color, -1); + } + + pixels[y * TILE_SIZE + x] = c; + } + } + } +} + +static void render_tile(enum Tile tile, size_t x, size_t y) { + u8 *pixels = TILE_SPRITES[tile]; + for (size_t j = 0; j < TILE_SIZE; j++) { + memcpy(&screen_offset(x, y + j), pixels + (j * TILE_SIZE), TILE_SIZE); + } +} + +static void render_border() { + for (size_t y = 0; y < (SCREEN_HEIGHT / TILE_SIZE); y++) { + size_t yy = BOARD_Y + (y * TILE_SIZE); + + render_tile( + BORDER, + BOARD_X - TILE_SIZE, + yy + ); + + render_tile( + BORDER, + BOARD_X + (BOARD_WIDTH * TILE_SIZE), + yy + ); + } +} + +static void render_board() { + for (size_t y = 0; y < BOARD_HEIGHT; y++) { + for (size_t x = 0; x < BOARD_WIDTH; x++) { + u8 data = state.board[y][x]; + enum Tile tile = data & TILE_MASK; + + size_t xs = BOARD_X + (x * TILE_SIZE), + ys = BOARD_Y + (y * TILE_SIZE); + + if (data & TILE_FLAG_FLASH) { + screen_fill(COLOR(4, 4, 1), xs, ys, TILE_SIZE, TILE_SIZE); + } else if (tile != NONE) { + render_tile(tile, xs, ys); + } + } + } +} + +static void render_ui() { +#define X_OFFSET_RIGHT (BOARD_X + BOARD_WIDTH_PX + (TILE_SIZE * 2)) + +#define RENDER_STAT(_title, _value, _color, _x, _y, _w) do {\ + char buf[32];\ + itoa((_value), buf, 32);\ + font_str_doubled((_title), (_x), (_y), COLOR(7, 7, 3));\ + font_str_doubled(buf, (_x) + (_w) - font_width(buf), (_y) + TILE_SIZE, (_color));\ + } while (0); + + size_t w = font_width("SCORE"); + RENDER_STAT("SCORE", state.score, COLOR(5, 5, 0), X_OFFSET_RIGHT, TILE_SIZE * 1, w); + RENDER_STAT("LINES", state.lines, COLOR(5, 3, 0), X_OFFSET_RIGHT, TILE_SIZE * 4, w); + RENDER_STAT("LEVEL", state.level, COLOR(5, 0, 0), X_OFFSET_RIGHT, TILE_SIZE * 7, w); + +#define X_OFFSET_LEFT (BOARD_X - (TILE_SIZE * 8)) +#define Y_OFFSET_LEFT TILE_SIZE + + font_str_doubled("NEXT", X_OFFSET_LEFT, TILE_SIZE, COLOR(7, 7, 3)); + + for (size_t j = 0; j < TTM_SIZE; j++) { + for (size_t i = 0; i < TTM_SIZE; i++) { + u16 tiles = state.next->rotations[0]; + + if (TTM_BLOCK(tiles, i, j)) { + render_tile( + state.next->color, + X_OFFSET_LEFT + ((i - TTM_OFFSET_X(tiles)) * TILE_SIZE), + Y_OFFSET_LEFT + (TILE_SIZE / 2) + ((j - TTM_OFFSET_Y(tiles) + 1) * TILE_SIZE) + ); + } + } + } +} + +static void render_game_over() { + const size_t w = SCREEN_WIDTH / 3, h = SCREEN_HEIGHT / 3; + screen_fill( + COLOR(4, 4, 2), + (SCREEN_WIDTH - w) / 2, + (SCREEN_HEIGHT - h) / 2, + w, + h + ); + + screen_fill( + COLOR(2, 2, 1), + (SCREEN_WIDTH - (w - 8)) / 2, + (SCREEN_HEIGHT - (h - 8)) / 2, + w - 8, + h - 8 + ); + + font_str_doubled( + "GAME OVER", + (SCREEN_WIDTH - font_width("GAME OVER")) / 2, + (SCREEN_HEIGHT / 2) - TILE_SIZE, + (state.frames / 5) % 2 == 0 ? + COLOR(6, 2, 1) : + COLOR(7, 4, 2) + ); + + char buf_score[64]; + itoa(state.score, buf_score, 64); + + font_str_doubled( + "SCORE:", + (SCREEN_WIDTH - font_width("SCORE:")) / 2, + (SCREEN_HEIGHT / 2), + COLOR(6, 6, 0) + ); + + font_str_doubled( + buf_score, + (SCREEN_WIDTH - font_width(buf_score)) / 2, + (SCREEN_HEIGHT / 2) + TILE_SIZE, + COLOR(7, 7, 3) + ); +} + +static void step() { + bool stopped = !move(0, 1); + + if (stopped && state.stopped) { + // twice stop = end for this tetromino + done(); + } + + state.stopped = stopped; +} + +void reset(u32 level) { + // initialize game state + memset(&state, 0, sizeof(state)); + state.frames_since_step = FRAMES_PER_STEP[0]; + state.level = 0; + state.lines_left = state.level * 10 + 10; + spawn(); +} + +static void update() { + if (state.game_over) { + if (keyboard_char('\n')) { + reset(0); + } + + return; + } + + // un-flash flashing tiles, remove destroy tiles + for (size_t y = 0; y < BOARD_HEIGHT; y++) { + bool destroy = false; + + for (size_t x = 0; x < BOARD_WIDTH; x++) { + u8 data = state.board[y][x]; + + if (data & TILE_FLAG_DESTROY) { + state.board[y][x] = NONE; + destroy = true; + } else { + state.board[y][x] &= ~TILE_FLAG_FLASH; + } + } + + if (destroy) { + if (y != 0) { + memmove( + &state.board[1], + &state.board[0], + sizeof(state.board[0]) * y + ); + } + + memset(&state.board[0], NONE, sizeof(state.board[0])); + } + } + + // spawn a new tetromino if the current one is done + if (state.curr.done && !spawn()) { + state.game_over = true; + return; + } + + if (state.destroy) { + state.destroy = false; + return; + } + + const bool control_states[NUM_CONTROLS] = { + keyboard_char('a'), + keyboard_char('d'), + keyboard_char('r'), + keyboard_key(KEY_LEFT), + keyboard_key(KEY_RIGHT), + keyboard_key(KEY_DOWN), + keyboard_char(' ') + }; + + for (size_t i = 0; i < NUM_CONTROLS; i++) { + struct Control *c = &state.controls.raw[i]; + c->last = c->down; + c->down = control_states[i]; + c->pressed = !c->last && c->down; + + if (c->pressed) { + c->pressed_frames = state.frames; + } + } + + if (state.controls.rotate_left.pressed) { + rotate(false); + } else if (state.controls.rotate_right.pressed || + state.controls.rotate.pressed) { + rotate(true); + } + + if (state.controls.left.down && + (state.frames - state.controls.left.pressed_frames) % 2 == 0) { + move(-1, 0); + } else if (state.controls.right.down && + (state.frames - state.controls.right.pressed_frames) % 2 == 0) { + move(1, 0); + } else if (state.controls.down.down && + (state.frames - state.controls.down.pressed_frames) % 2 == 0) { + if (!move(0, 1)) { + done(); + } + } else if (state.controls.fast_down.pressed) { + while (move(0, 1)); + done(); + } + + if (--state.frames_since_step == 0) { + step(); + state.steps++; + state.frames_since_step = FRAMES_PER_STEP[state.level]; + } +} + +static void render() { + screen_clear(COLOR(0, 0, 0)); + render_border(); + render_board(); + render_ui(); + + if (state.game_over) { + render_game_over(); + } +} + +void update_menu() { + if (keyboard_char('\n')) { + reset(0); + state.menu = false; + } +} + +void render_menu() { + screen_clear(COLOR(0, 0, 0)); + + // render logo + size_t logo_width = strlen(LOGO[0]), + logo_x = (SCREEN_WIDTH - (logo_width * TILE_SIZE)) / 2, + logo_y = TILE_SIZE * 3; + + for (i32 x = -1; x < (i32) logo_width + 1; x++) { + render_tile(BORDER, logo_x + (x * TILE_SIZE), logo_y - (TILE_SIZE * 2)); + render_tile(BORDER, logo_x + (x * TILE_SIZE), logo_y + (TILE_SIZE * (1 + LOGO_HEIGHT))); + } + + for (size_t y = 0; y < LOGO_HEIGHT; y++) { + for (size_t x = 0; x < logo_width; x++) { + char c = LOGO[y][x]; + + if (c == ' ' || c == '\t' || c == '\n') { + continue; + } + + render_tile( + GREEN + ((((state.frames / 10) + (6 - (c - 'A'))) / 6) % 8), + logo_x + (x * TILE_SIZE), + logo_y + (y * TILE_SIZE) + ); + } + } + + const char *play = "PRESS ENTER TO PLAY"; + font_str_doubled( + play, + (SCREEN_WIDTH - font_width(play)) / 2, + logo_y + ((LOGO_HEIGHT + 6) * TILE_SIZE), + (state.frames / 6) % 2 == 0 ? + COLOR(6, 6, 2) : + COLOR(7, 7, 3) + ); +} + +void _main(u32 magic) { + idt_init(); + isr_init(); + fpu_init(); + irq_init(); + screen_init(); + timer_init(); + keyboard_init(); + generate_sprites(); + +#ifdef ENABLE_MUSIC + sound_init(); + music_init(); + state.music = true; + sound_master(255); +#endif + + state.menu = true; + + + bool last_music_toggle = false; + u32 last_frame = 0, last = 0; + + while (true) { + const u32 now = (u32) timer_get(); + +#ifdef ENABLE_MUSIC + if (now != last) { + music_tick(); + last = now; + } +#endif + + if ((now - last_frame) > (TIMER_TPS / FPS)) { + last_frame = now; + + if (state.menu) { + update_menu(); + render_menu(); + } else { + update(); + render(); + } + +#ifdef ENABLE_MUSIC + if (keyboard_char('m')) { + if (!last_music_toggle) { + state.music = !state.music; + sound_master(state.music ? 255 : 0); + } + + last_music_toggle = true; + } else { + last_music_toggle = false; + } +#endif + + screen_swap(); + state.frames++; + } + } +} diff --git a/src/math.c b/src/math.c new file mode 100644 index 0000000..8b4b7e3 --- /dev/null +++ b/src/math.c @@ -0,0 +1,46 @@ +#include "math.h" + +f64 fabs(f64 x) { + return x < 0.0 ? -x : x; +} + +f64 fmod(f64 x, f64 m) { + f64 result; + asm("1: fprem\n\t" + "fnstsw %%ax\n\t" + "sahf\n\t" + "jp 1b" + : "=t"(result) : "0"(x), "u"(m) : "ax", "cc"); + return result; +} + +f64 sin(f64 x) { + f64 result; + asm("fsin" : "=t"(result) : "0"(x)); + return result; +} + +f64 cos(f64 x) { + return sin(x + PI / 2.0); +} + +// black magic +f64 pow(f64 x, f64 y) { + f64 out; + asm( + "fyl2x;" + "fld %%st;" + "frndint;" + "fsub %%st,%%st(1);" + "fxch;" + "fchs;" + "f2xm1;" + "fld1;" + "faddp;" + "fxch;" + "fld1;" + "fscale;" + "fstp %%st(1);" + "fmulp;" : "=t"(out) : "0"(x),"u"(y) : "st(1)" ); + return out; +} diff --git a/src/math.h b/src/math.h new file mode 100644 index 0000000..dbf1a6f --- /dev/null +++ b/src/math.h @@ -0,0 +1,15 @@ +#ifndef MATH_H +#define MATH_H + +#include "util.h" + +#define E 2.71828 +#define PI 3.14159265358979323846264338327950 + +f64 fmod(f64 x, f64 m); +f64 fabs(f64 x); +f64 sin(f64 x); +f64 cos(f64 x); +f64 pow(f64 x, f64 y); + +#endif diff --git a/src/music.c b/src/music.c new file mode 100644 index 0000000..64c7a65 --- /dev/null +++ b/src/music.c @@ -0,0 +1,361 @@ +#include "music.h" +#include "timer.h" +#include "sound.h" +#include "math.h" + +struct Note { + u8 octave; + u8 note; + u16 duration; +}; + +struct NoteActive { + struct Note note; + double ticks; +}; + +#define TRACK_BPM 150 +#define TRACK_BPS (TRACK_BPM / 60.0) +#define TICKS_PER_BEAT (TIMER_TPS / TRACK_BPS) +#define TICKS_PER_SIXTEENTH (TICKS_PER_BEAT / 16.0) + +#define CHORUS_MELODY_LENGTH (sizeof(CHORUS_MELODY) / sizeof(CHORUS_MELODY[0])) +static const struct Note CHORUS_MELODY[] = { + // CHORUS MELODY + { OCTAVE_5, NOTE_E, 16 }, + { OCTAVE_4, NOTE_B, 8 }, + { OCTAVE_5, NOTE_C, 8 }, + { OCTAVE_5, NOTE_D, 16 }, + + { OCTAVE_5, NOTE_C, 8 }, + { OCTAVE_4, NOTE_B, 8 }, + { OCTAVE_4, NOTE_A, 16 }, + + { OCTAVE_4, NOTE_A, 8 }, + { OCTAVE_5, NOTE_C, 8 }, + { OCTAVE_5, NOTE_E, 16 }, + + { OCTAVE_5, NOTE_D, 8 }, + { OCTAVE_5, NOTE_C, 8 }, + { OCTAVE_4, NOTE_B, 16 }, + + { OCTAVE_4, NOTE_B, 8 }, + { OCTAVE_5, NOTE_C, 8 }, + { OCTAVE_5, NOTE_D, 16 }, + { OCTAVE_5, NOTE_E, 16 }, + { OCTAVE_5, NOTE_C, 16 }, + { OCTAVE_4, NOTE_A, 16 }, + { OCTAVE_4, NOTE_A, 16 }, + { OCTAVE_4, NOTE_NONE, 24 }, + + { OCTAVE_5, NOTE_D, 16 }, + { OCTAVE_5, NOTE_F, 8 }, + { OCTAVE_5, NOTE_A, 8 }, + { OCTAVE_5, NOTE_A, 4 }, + { OCTAVE_5, NOTE_A, 4 }, + { OCTAVE_5, NOTE_G, 8 }, + { OCTAVE_5, NOTE_F, 8 }, + { OCTAVE_5, NOTE_E, 16 }, + { OCTAVE_5, NOTE_NONE, 8 }, + + { OCTAVE_5, NOTE_C, 8 }, + { OCTAVE_5, NOTE_E, 8 }, + { OCTAVE_5, NOTE_E, 4 }, + { OCTAVE_5, NOTE_E, 4 }, + { OCTAVE_5, NOTE_D, 8 }, + { OCTAVE_5, NOTE_C, 8 }, + { OCTAVE_4, NOTE_B, 16 }, + + { OCTAVE_4, NOTE_B, 8 }, + { OCTAVE_5, NOTE_C, 8 }, + { OCTAVE_5, NOTE_D, 16 }, + { OCTAVE_5, NOTE_E, 16 }, + { OCTAVE_5, NOTE_C, 16 }, + { OCTAVE_4, NOTE_A, 16 }, + { OCTAVE_4, NOTE_A, 16 }, + { OCTAVE_4, NOTE_NONE, 16 }, +}; + +#define BRIDGE_MELODY_LENGTH (sizeof(BRIDGE_MELODY) / sizeof(BRIDGE_MELODY[0])) +static const struct Note BRIDGE_MELODY[] = { + // BRIDGE + { OCTAVE_4, NOTE_E, 32 }, + { OCTAVE_4, NOTE_C, 32 }, + { OCTAVE_4, NOTE_D, 32 }, + { OCTAVE_3, NOTE_B, 32 }, + { OCTAVE_4, NOTE_C, 32 }, + { OCTAVE_3, NOTE_A, 32 }, + { OCTAVE_3, NOTE_AF, 48 }, + { OCTAVE_3, NOTE_NONE, 16 }, + + { OCTAVE_4, NOTE_E, 32 }, + { OCTAVE_4, NOTE_C, 32 }, + { OCTAVE_4, NOTE_D, 32 }, + { OCTAVE_3, NOTE_B, 32 }, + { OCTAVE_3, NOTE_A, 16 }, + { OCTAVE_4, NOTE_E, 16 }, + { OCTAVE_4, NOTE_A, 32 }, + { OCTAVE_4, NOTE_AF, 48 }, + { OCTAVE_4, NOTE_NONE, 16 }, +}; + +#define BASS_NOTE(_octave, _note)\ + { _octave, _note, 8 },\ + { (_octave + 1), _note, 8 } + +#define BASS_HALF_MEASURE(_octave, _note)\ + BASS_NOTE(_octave, _note),\ + BASS_NOTE(_octave, _note) + +#define BASS_MEASURE(_octave, _note)\ + BASS_HALF_MEASURE(_octave, _note),\ + BASS_HALF_MEASURE(_octave, _note) + +#define CHORUS_BASS_LENGTH (sizeof(CHORUS_BASS) / sizeof(CHORUS_BASS[0])) +static const struct Note CHORUS_BASS[] = { + // CHORUS BASS + BASS_MEASURE(OCTAVE_2, NOTE_E), + BASS_MEASURE(OCTAVE_2, NOTE_A), + BASS_HALF_MEASURE(OCTAVE_2, NOTE_AF), + BASS_HALF_MEASURE(OCTAVE_2, NOTE_E), + BASS_HALF_MEASURE(OCTAVE_2, NOTE_A), + { OCTAVE_2, NOTE_A, 8 }, + { OCTAVE_2, NOTE_A, 8 }, + { OCTAVE_2, NOTE_B, 8 }, + { OCTAVE_3, NOTE_C, 8 }, + + BASS_MEASURE(OCTAVE_2, NOTE_D), + BASS_MEASURE(OCTAVE_2, NOTE_C), + BASS_HALF_MEASURE(OCTAVE_2, NOTE_AF), + BASS_HALF_MEASURE(OCTAVE_2, NOTE_E), + BASS_MEASURE(OCTAVE_2, NOTE_A), +}; + +#define BRIDGE_BASS_NOTE(_octave_0, _note_0, _octave_1, _note_1)\ + { _octave_0, _note_0, 8 },\ + { _octave_1, _note_1, 8 } + +#define BRIDGE_BASS_HALF_MEASURE(_octave_0, _note_0, _octave_1, _note_1)\ + BRIDGE_BASS_NOTE(_octave_0, _note_0, _octave_1, _note_1),\ + BRIDGE_BASS_NOTE(_octave_0, _note_0, _octave_1, _note_1) + +#define BRIDGE_BASS_MEASURE(_octave_0, _note_0, _octave_1, _note_1)\ + BRIDGE_BASS_HALF_MEASURE(_octave_0, _note_0, _octave_1, _note_1),\ + BRIDGE_BASS_HALF_MEASURE(_octave_0, _note_0, _octave_1, _note_1) + +#define BRIDGE_BASS_LENGTH (sizeof(BRIDGE_BASS) / sizeof(BRIDGE_BASS[0])) +static const struct Note BRIDGE_BASS[] = { + BRIDGE_BASS_MEASURE(OCTAVE_2, NOTE_A, OCTAVE_3, NOTE_E), + BRIDGE_BASS_MEASURE(OCTAVE_2, NOTE_AF, OCTAVE_3, NOTE_E), + + BRIDGE_BASS_HALF_MEASURE(OCTAVE_2, NOTE_A, OCTAVE_3, NOTE_E), + BRIDGE_BASS_HALF_MEASURE(OCTAVE_2, NOTE_E, OCTAVE_2, NOTE_A), + + BRIDGE_BASS_HALF_MEASURE(OCTAVE_2, NOTE_E, OCTAVE_2, NOTE_AF), + BRIDGE_BASS_NOTE(OCTAVE_2, NOTE_E, OCTAVE_2, NOTE_AF), + { OCTAVE_2, NOTE_E, 8 }, + { OCTAVE_2, NOTE_NONE, 8 }, + + BRIDGE_BASS_MEASURE(OCTAVE_2, NOTE_A, OCTAVE_3, NOTE_E), + BRIDGE_BASS_MEASURE(OCTAVE_2, NOTE_AF, OCTAVE_3, NOTE_E), + + BRIDGE_BASS_HALF_MEASURE(OCTAVE_2, NOTE_A, OCTAVE_3, NOTE_E), + BRIDGE_BASS_HALF_MEASURE(OCTAVE_2, NOTE_E, OCTAVE_2, NOTE_A), + + BRIDGE_BASS_HALF_MEASURE(OCTAVE_2, NOTE_E, OCTAVE_2, NOTE_AF), + BRIDGE_BASS_NOTE(OCTAVE_2, NOTE_E, OCTAVE_2, NOTE_AF), + { OCTAVE_2, NOTE_E, 8 }, + { OCTAVE_2, NOTE_NONE, 8 } +}; + +#define CHORUS_HARMONY_LENGTH (sizeof(CHORUS_HARMONY) / sizeof(CHORUS_HARMONY[0])) +static const struct Note CHORUS_HARMONY[] = { + // CHORUS HARMONY + { OCTAVE_5, NOTE_NONE, 16 }, + { OCTAVE_4, NOTE_AF, 8 }, + { OCTAVE_4, NOTE_A, 8 }, + { OCTAVE_4, NOTE_B, 16 }, + + { OCTAVE_4, NOTE_A, 8 }, + { OCTAVE_4, NOTE_AF, 8 }, + { OCTAVE_4, NOTE_E, 16 }, + + { OCTAVE_4, NOTE_E, 8 }, + { OCTAVE_4, NOTE_A, 8 }, + { OCTAVE_4, NOTE_A, 16 }, + + { OCTAVE_4, NOTE_B, 8 }, + { OCTAVE_4, NOTE_A, 8 }, + { OCTAVE_4, NOTE_AF, 16 }, + + { OCTAVE_4, NOTE_AF, 8 }, + { OCTAVE_4, NOTE_A, 8 }, + { OCTAVE_4, NOTE_B, 16 }, + { OCTAVE_5, NOTE_C, 16 }, + { OCTAVE_4, NOTE_A, 16 }, + { OCTAVE_4, NOTE_E, 16 }, + { OCTAVE_4, NOTE_E, 16 }, + { OCTAVE_4, NOTE_NONE, 24 }, + + { OCTAVE_4, NOTE_F, 16 }, + { OCTAVE_4, NOTE_A, 8 }, + { OCTAVE_5, NOTE_C, 16 }, + { OCTAVE_4, NOTE_B, 8 }, + { OCTAVE_4, NOTE_A, 8 }, + { OCTAVE_4, NOTE_G, 16 }, + { OCTAVE_4, NOTE_NONE, 8 }, + + { OCTAVE_4, NOTE_E, 8 }, + { OCTAVE_4, NOTE_G, 16 }, + { OCTAVE_4, NOTE_F, 8 }, + { OCTAVE_4, NOTE_E, 8 }, + { OCTAVE_4, NOTE_AF, 16 }, + + { OCTAVE_4, NOTE_AF, 8 }, + { OCTAVE_4, NOTE_A, 8 }, + { OCTAVE_4, NOTE_B, 16 }, + { OCTAVE_5, NOTE_C, 16 }, + { OCTAVE_4, NOTE_A, 16 }, + { OCTAVE_4, NOTE_E, 16 }, + { OCTAVE_4, NOTE_E, 16 }, + { OCTAVE_4, NOTE_NONE, 16 }, +}; + +#define BRIDGE_HARMONY_LENGTH (sizeof(BRIDGE_HARMONY) / sizeof(BRIDGE_HARMONY[0])) +static const struct Note BRIDGE_HARMONY[] = { + { OCTAVE_4, NOTE_C, 32 }, + { OCTAVE_3, NOTE_A, 32 }, + { OCTAVE_3, NOTE_B, 32 }, + { OCTAVE_3, NOTE_AF, 32 }, + { OCTAVE_3, NOTE_A, 32 }, + { OCTAVE_3, NOTE_E, 32 }, + { OCTAVE_3, NOTE_E, 48 }, + { OCTAVE_3, NOTE_NONE, 16 }, + + { OCTAVE_4, NOTE_C, 32 }, + { OCTAVE_3, NOTE_A, 32 }, + { OCTAVE_3, NOTE_B, 32 }, + { OCTAVE_3, NOTE_AF, 32 }, + { OCTAVE_3, NOTE_E, 16 }, + { OCTAVE_3, NOTE_A, 16 }, + { OCTAVE_4, NOTE_E, 32 }, + { OCTAVE_4, NOTE_E, 48 }, + { OCTAVE_4, NOTE_NONE, 16 }, +}; + +#define SNARE_OCTAVE OCTAVE_4 +#define SNARE_NOTE NOTE_E +#define SNARE_AND\ + { SNARE_OCTAVE, NOTE_NONE, 8},\ + { SNARE_OCTAVE, SNARE_NOTE, 2},\ + { SNARE_OCTAVE, NOTE_NONE, 6} +#define SNARE_EIGTH\ + { SNARE_OCTAVE, SNARE_NOTE, 2},\ + { SNARE_OCTAVE, NOTE_NONE, 6} + +#define SNARE_MEASURE\ + SNARE_AND,\ + SNARE_AND,\ + SNARE_AND,\ + SNARE_AND + +#define CHORUS_SNARE_LENGTH (sizeof(CHORUS_SNARE) / sizeof(CHORUS_SNARE[0])) +static const struct Note CHORUS_SNARE[] = { + SNARE_MEASURE, + SNARE_MEASURE, + SNARE_MEASURE, + SNARE_AND, + SNARE_AND, + SNARE_AND, + SNARE_EIGTH, + SNARE_EIGTH, + SNARE_MEASURE, + SNARE_MEASURE, + SNARE_MEASURE, + SNARE_AND, + SNARE_AND, + SNARE_AND, + SNARE_EIGTH, + SNARE_EIGTH, +}; + +#define BRIDGE_SNARE_LENGTH (sizeof(BRIDGE_SNARE) / sizeof(BRIDGE_SNARE[0])) +static const struct Note BRIDGE_SNARE[] = { + SNARE_MEASURE, + SNARE_MEASURE, + SNARE_MEASURE, + SNARE_MEASURE, + SNARE_MEASURE, + SNARE_MEASURE, + SNARE_MEASURE, + SNARE_AND, + SNARE_AND, + SNARE_AND, + SNARE_EIGTH, + SNARE_EIGTH, +}; + +#define TRACK_MAX_LENGTH (4 * (CHORUS_MELODY_LENGTH * 3 + BRIDGE_MELODY_LENGTH + 1)) +#define TRACK_PARTS 4 +static struct Note TRACK[TRACK_PARTS][TRACK_MAX_LENGTH]; +static size_t PART_LENGTHS[TRACK_PARTS]; + +static i32 indices[TRACK_PARTS]; +static struct NoteActive current[NUM_NOTES]; + +void music_tick() { + for (size_t i = 0; i < TRACK_PARTS; i++) { + if (indices[i] == -1 || (current[i].ticks -= 1) <= 0) { + indices[i] = (indices[i] + 1) % PART_LENGTHS[i]; + + double remainder = fabs(current[i].ticks); + + struct Note note = TRACK[i][indices[i]]; + current[i].note = note; + current[i].ticks = TICKS_PER_SIXTEENTH * note.duration - remainder; + + sound_note(i, note.octave, note.note); + } + + // remove last tick to give each note an attack + if (current[i].ticks <= 1) { + sound_note(i, OCTAVE_1, NOTE_NONE); + } + } +} + +void music_init() { + sound_wave(0, WAVE_TRIANGLE); + sound_volume(0, 255); + + sound_wave(1, WAVE_NOISE); + sound_volume(1, 128); + + sound_wave(2, WAVE_TRIANGLE); + sound_volume(2, 196); + + sound_wave(3, WAVE_TRIANGLE); + sound_volume(3, 196); + + // AABA part +#define PART(_i, _c, _b) do { \ + size_t cs = sizeof(_c) / sizeof(_c[0]), \ + bs = sizeof(_b) / sizeof(_b[0]), \ + n = 0; \ + memcpy(&TRACK[_i][n], _c, sizeof(_c)); n += cs; \ + memcpy(&TRACK[_i][n], _c, sizeof(_c)); n += cs; \ + memcpy(&TRACK[_i][n], _b, sizeof(_b)); n += bs; \ + memcpy(&TRACK[_i][n], _c, sizeof(_c)); \ + PART_LENGTHS[_i] = cs * 3 + bs; \ + } while (0); + + PART(0, CHORUS_MELODY, BRIDGE_MELODY); + PART(1, CHORUS_SNARE, BRIDGE_SNARE); + PART(2, CHORUS_BASS, BRIDGE_BASS); + PART(3, CHORUS_HARMONY, BRIDGE_HARMONY); + + + for (size_t i = 0; i < TRACK_PARTS; i++) { + indices[i] = -1; + } +} diff --git a/src/music.h b/src/music.h new file mode 100644 index 0000000..fd28a90 --- /dev/null +++ b/src/music.h @@ -0,0 +1,9 @@ +#ifndef MUSIC_H +#define MUSIC_H + +#include "util.h" + +void music_tick(); +void music_init(); + +#endif diff --git a/src/screen.c b/src/screen.c new file mode 100644 index 0000000..1a3ec26 --- /dev/null +++ b/src/screen.c @@ -0,0 +1,41 @@ +#include "screen.h" + +static u8 *BUFFER = (u8 *) 0xA0000; + +// double buffers +u8 _sbuffers[2][SCREEN_SIZE]; +u8 _sback = 0; + +#define CURRENT (_sbuffers[_sback]) +#define SWAP() (_sback = 1 - _sback) + +// VGA control port addresses +#define PALETTE_MASK 0x3C6 +#define PALETTE_READ 0x3C7 +#define PALETTE_WRITE 0x3C8 +#define PALETTE_DATA 0x3C9 + +void screen_swap() { + memcpy(BUFFER, &CURRENT, SCREEN_SIZE); + SWAP(); +} + +void screen_clear(u8 color) { + memset(&CURRENT, color, SCREEN_SIZE); +} + +void screen_init() { + // configure palette with 8-bit RRRGGGBB color + outportb(PALETTE_MASK, 0xFF); + outportb(PALETTE_WRITE, 0); + for (u8 i = 0; i < 255; i++) { + outportb(PALETTE_DATA, (((i >> 5) & 0x7) * (256 / 8)) / 4); + outportb(PALETTE_DATA, (((i >> 2) & 0x7) * (256 / 8)) / 4); + outportb(PALETTE_DATA, (((i >> 0) & 0x3) * (256 / 4)) / 4); + } + + // set color 255 = white + outportb(PALETTE_DATA, 0x3F); + outportb(PALETTE_DATA, 0x3F); + outportb(PALETTE_DATA, 0x3F); +} diff --git a/src/screen.h b/src/screen.h new file mode 100644 index 0000000..ddee993 --- /dev/null +++ b/src/screen.h @@ -0,0 +1,53 @@ +#ifndef SCREEN_H +#define SCREEN_H + +#include "util.h" + +#define SCREEN_WIDTH 320 +#define SCREEN_HEIGHT 200 +#define SCREEN_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT) + +#define COLOR(_r, _g, _b)((u8)( \ + (((_r) & 0x7) << 5) | \ + (((_g) & 0x7) << 2) | \ + (((_b) & 0x3) << 0))) + +#define COLOR_R(_index) (((_index) >> 5) & 0x7) +#define COLOR_G(_index) (((_index) >> 2) & 0x7) +#define COLOR_B(_index) (((_index) >> 0) & 0x3) + +#define COLOR_ADD(_index, _d) __extension__({ \ + __typeof__(_index) _c = (_index); \ + __typeof__(_d) __d = (_d); \ + COLOR( \ + CLAMP(COLOR_R(_c) + __d, 0, 7), \ + CLAMP(COLOR_G(_c) + __d, 0, 7), \ + CLAMP(COLOR_B(_c) + __d, 0, 3) \ + );}) + +extern u8 _sbuffers[2][SCREEN_SIZE]; +extern u8 _sback; + +#define screen_buffer() (_sbuffers[_sback]) + +#define screen_set(_p, _x, _y)\ + (_sbuffers[_sback][((_y) * SCREEN_WIDTH + (_x))]=(_p)) + +#define screen_offset(_x, _y) (screen_buffer()[(_y) * SCREEN_WIDTH + (_x)]) + +#define screen_fill(_c, _x, _y, _w, _h) do {\ + __typeof__(_x) __x = (_x);\ + __typeof__(_y) __y = (_y);\ + __typeof__(_w) __w = (_w);\ + __typeof__(_y) __ymax = __y + (_h);\ + __typeof__(_c) __c = (_c);\ + for (; __y < __ymax; __y++) {\ + memset(&screen_buffer()[__y * SCREEN_WIDTH + __x], __c, __w);\ + }\ + } while (0) + +void screen_swap(); +void screen_clear(u8 color); +void screen_init(); + +#endif diff --git a/src/sound.c b/src/sound.c new file mode 100644 index 0000000..1ee76d4 --- /dev/null +++ b/src/sound.c @@ -0,0 +1,356 @@ +#include "sound.h" +#include "system.h" +#include "irq.h" +#include "math.h" + +static const f64 NOTES[NUM_OCTAVES * OCTAVE_SIZE] = { + // O1 + 32.703195662574764, + 34.647828872108946, + 36.708095989675876, + 38.890872965260044, + 41.203444614108669, + 43.653528929125407, + 46.249302838954222, + 48.99942949771858, + 51.913087197493056, + 54.999999999999915, + 58.270470189761156, + 61.735412657015416, + + // O2 + 65.406391325149571, + 69.295657744217934, + 73.416191979351794, + 77.781745930520117, + 82.406889228217381, + 87.307057858250872, + 92.4986056779085, + 97.998858995437217, + 103.82617439498618, + 109.99999999999989, + 116.54094037952237, + 123.4708253140309, + + // O3 + 130.8127826502992, + 138.59131548843592, + 146.83238395870364, + 155.56349186104035, + 164.81377845643485, + 174.61411571650183, + 184.99721135581709, + 195.99771799087452, + 207.65234878997245, + 219.99999999999989, + 233.08188075904488, + 246.94165062806198, + + // O4 + 261.62556530059851, + 277.18263097687202, + 293.66476791740746, + 311.12698372208081, + 329.62755691286986, + 349.22823143300383, + 369.99442271163434, + 391.99543598174927, + 415.30469757994513, + 440, + 466.16376151808993, + 493.88330125612413, + + // O5 + 523.25113060119736, + 554.36526195374427, + 587.32953583481526, + 622.25396744416196, + 659.25511382574007, + 698.456462866008, + 739.98884542326903, + 783.99087196349899, + 830.60939515989071, + 880.00000000000034, + 932.32752303618031, + 987.76660251224882, + + // O6 + 1046.5022612023952, + 1108.7305239074892, + 1174.659071669631, + 1244.5079348883246, + 1318.5102276514808, + 1396.9129257320169, + 1479.977690846539, + 1567.9817439269987, + 1661.2187903197821, + 1760.000000000002, + 1864.6550460723618, + 1975.5332050244986, + + // O7 + 2093.0045224047913, + 2217.4610478149793, + 2349.3181433392633, + 2489.0158697766506, + 2637.020455302963, + 2793.8258514640347, + 2959.9553816930793, + 3135.9634878539991, + 3322.437580639566, + 3520.0000000000055, + 3729.3100921447249, + 3951.0664100489994, +}; + +#define MIXER_IRQ 0x5 +#define MIXER_IRQ_DATA 0x2 + +// SB16 ports +#define DSP_MIXER 0x224 +#define DSP_MIXER_DATA 0x225 +#define DSP_RESET 0x226 +#define DSP_READ 0x22A +#define DSP_WRITE 0x22C +#define DSP_READ_STATUS 0x22E +#define DSP_ACK_8 DSP_READ_STATUS +#define DSP_ACK_16 0x22F + +// TODO: ??? +#define DSP_PROG_16 0xB0 +#define DSP_PROG_8 0xC0 +#define DSP_AUTO_INIT 0x06 +#define DSP_PLAY 0x00 +#define DSP_RECORD 0x08 +#define DSP_MONO 0x00 +#define DSP_STEREO 0x20 +#define DSP_UNSIGNED 0x00 +#define DSP_SIGNED 0x10 + +#define DMA_CHANNEL_16 5 +#define DMA_FLIP_FLOP 0xD8 +#define DMA_BASE_ADDR 0xC4 +#define DMA_COUNT 0xC6 + +// commands for DSP_WRITE +#define DSP_SET_TIME 0x40 +#define DSP_SET_RATE 0x41 +#define DSP_ON 0xD1 +#define DSP_OFF 0xD3 +#define DSP_OFF_8 0xD0 +#define DSP_ON_8 0xD4 +#define DSP_OFF_16 0xD5 +#define DSP_ON_16 0xD6 +#define DSP_VERSION 0xE1 + +// commands for DSP_MIXER +#define DSP_VOLUME 0x22 +#define DSP_IRQ 0x80 + +#define SAMPLE_RATE 48000 +#define BUFFER_MS 40 + +#define BUFFER_SIZE ((size_t) (SAMPLE_RATE * (BUFFER_MS / 1000.0))) + +static i16 buffer[BUFFER_SIZE]; +static bool buffer_flip = false; + +static u64 sample = 0; + +static u8 volume_master; +static u8 volumes[NUM_NOTES]; +static u8 notes[NUM_NOTES]; +static u8 waves[NUM_NOTES]; + +void sound_note(u8 index, u8 octave, u8 note) { + notes[index] = (octave << 4) | note; +} + +void sound_volume(u8 index, u8 v) { + volumes[index] = v; +} + +void sound_master(u8 v) { + volume_master = v; +} + +void sound_wave(u8 index, u8 wave) { + waves[index] = wave; +} + +static void fill(i16 *buf, size_t len) { + for (size_t i = 0; i < len; i++) { + double f = 0.0; + + for (size_t j = 0; j < NUM_NOTES; j++) { + u8 octave = (notes[j] >> 4) & 0xF, + note = notes[j] & 0xF; + + if (note == NOTE_NONE) { + continue; + } + + double note_freq = NOTES[octave * OCTAVE_SIZE + note], + freq = note_freq / (double) SAMPLE_RATE, + d = 0.0, + offset = 0.0; + + switch (waves[j]) { + case WAVE_SIN: + d = sin(2.0 * PI * sample * freq); + break; + case WAVE_SQUARE: + d = sin(2.0 * PI * sample * freq) >= 0.0 ? 1.0 : -1.0; + break; + case WAVE_TRIANGLE: + d = fabs(fmod(4 * (sample * freq) + 1.0, 4.0) - 2.0) - 1; + break; + case WAVE_NOISE: + offset = (freq * 128.0) * ((rand() / 4294967295.0) - 0.5); + d = fabs(fmod(4 * (sample * freq + offset) + 1.0, 4.0) - 2.0) - 1; + break; + } + + d *= (volumes[j] / 255.0); + f += d; + } + + buf[i] = (i16) (((volume_master / 255.0) * 4096.0) * f); + + sample++; + + // avoid double overflow errors, instead just mess up one note every + // few minutes + sample %= (1 << 24); + } +} + +static void dsp_write(u8 b) { + while (inportb(DSP_WRITE) & 0x80); + outportb(DSP_WRITE, b); +} + +static void dsp_read(u8 b) { + while (inportb(DSP_READ_STATUS) & 0x80); + outportb(DSP_READ, b); +} + +static void reset() { + char buf0[128], buf1[128]; + + outportb(DSP_RESET, 1); + + // TODO: maybe not necessary + // ~3 microseconds? + for (size_t i = 0; i < 1000000; i++); + + outportb(DSP_RESET, 0); + + u8 status = inportb(DSP_READ_STATUS); + if (~status & 128) { + goto fail; + } + + status = inportb(DSP_READ); + if (status != 0xAA) { + goto fail; + } + + outportb(DSP_WRITE, DSP_VERSION); + u8 major = inportb(DSP_READ), + minor = inportb(DSP_READ); + + if (major < 4) { + status = (major << 4) | minor; + goto fail; + } + + return; +fail: + strlcpy(buf0, "FAILED TO RESET SB16: ", 128); + itoa(status, buf1, 128); + strlcat(buf0, buf1, 128); + panic(buf0); +} + +static void set_sample_rate(u16 hz) { + dsp_write(DSP_SET_RATE); + dsp_write((u8) ((hz >> 8) & 0xFF)); + dsp_write((u8) (hz & 0xFF)); +} + +static void transfer(void *buf, u32 len) { + u8 mode = 0x48; + + // disable DMA channel + outportb(DSP_ON_8, 4 + (DMA_CHANNEL_16 % 4)); + + // clear byte-poiner flip-flop + outportb(DMA_FLIP_FLOP, 1); + + // write DMA mode for transfer + outportb(DSP_ON_16, (DMA_CHANNEL_16 % 4) | mode | (1 << 4)); + + // write buffer offset (div 2 for 16-bit) + u16 offset = (((uintptr_t) buf) / 2) % 65536; + outportb(DMA_BASE_ADDR, (u8) ((offset >> 0) & 0xFF)); + outportb(DMA_BASE_ADDR, (u8) ((offset >> 8) & 0xFF)); + + // write transfer length + outportb(DMA_COUNT, (u8) (((len - 1) >> 0) & 0xFF)); + outportb(DMA_COUNT, (u8) (((len - 1) >> 8) & 0xFF)); + + // write buffer + outportb(0x8B, ((uintptr_t) buf) >> 16); + + // enable DMA channel + outportb(0xD4, DMA_CHANNEL_16 % 4); +} + +static void sb16_irq_handler(struct Registers *regs) { + buffer_flip = !buffer_flip; + + fill( + &buffer[buffer_flip ? 0 : (BUFFER_SIZE / 2)], + (BUFFER_SIZE / 2) + ); + + inportb(DSP_READ_STATUS); + inportb(DSP_ACK_16); +} + +static void configure() { + irq_install(MIXER_IRQ, sb16_irq_handler); + outportb(DSP_MIXER, DSP_IRQ); + outportb(DSP_MIXER_DATA, MIXER_IRQ_DATA); + + u8 v = MIXER_IRQ; + if (v != MIXER_IRQ) { + char buf0[128], buf1[128]; + itoa(v, buf0, 128); + strlcpy(buf1, "SB16 HAS INCORRECT IRQ: ", 128); + strlcat(buf1, buf0, 128); + panic(buf1); + } +} + +void sound_init() { + irq_install(MIXER_IRQ, sb16_irq_handler); + reset(); + configure(); + + transfer(buffer, BUFFER_SIZE); + set_sample_rate(SAMPLE_RATE); + + u16 sample_count = (BUFFER_SIZE / 2) - 1; + dsp_write(DSP_PLAY | DSP_PROG_16 | DSP_AUTO_INIT); + dsp_write(DSP_SIGNED | DSP_MONO); + dsp_write((u8) ((sample_count >> 0) & 0xFF)); + dsp_write((u8) ((sample_count >> 8) & 0xFF)); + + dsp_write(DSP_ON); + dsp_write(DSP_ON_16); + + memset(¬es, NOTE_NONE, sizeof(notes)); + memset(&waves, WAVE_SIN, sizeof(waves)); +} diff --git a/src/sound.h b/src/sound.h new file mode 100644 index 0000000..d1f3e95 --- /dev/null +++ b/src/sound.h @@ -0,0 +1,50 @@ +#ifndef SOUND_H +#define SOUND_H + +#include "util.h" + +#define NUM_NOTES 8 + +#define NUM_OCTAVES 7 +#define OCTAVE_SIZE 12 + +#define OCTAVE_1 0 +#define OCTAVE_2 1 +#define OCTAVE_3 2 +#define OCTAVE_4 3 +#define OCTAVE_5 4 +#define OCTAVE_6 5 +#define OCTAVE_7 6 + +#define NOTE_C 0 +#define NOTE_CS 1 +#define NOTE_DF NOTE_CS +#define NOTE_D 2 +#define NOTE_DS 3 +#define NOTE_EF NOTE_DS +#define NOTE_E 4 +#define NOTE_F 5 +#define NOTE_FS 6 +#define NOTE_GF NOTE_FS +#define NOTE_G 7 +#define NOTE_GS 8 +#define NOTE_AF NOTE_GS +#define NOTE_A 9 +#define NOTE_AS 10 +#define NOTE_BF NOTE_AS +#define NOTE_B 11 + +#define NOTE_NONE 12 + +#define WAVE_SIN 0 +#define WAVE_SQUARE 1 +#define WAVE_NOISE 2 +#define WAVE_TRIANGLE 3 + +void sound_init(); +void sound_note(u8 index, u8 octave, u8 note); +void sound_master(u8 v); +void sound_volume(u8 index, u8 v); +void sound_wave(u8 index, u8 wave); + +#endif diff --git a/src/speaker.c b/src/speaker.c new file mode 100644 index 0000000..ab5efc1 --- /dev/null +++ b/src/speaker.c @@ -0,0 +1,44 @@ +#include "speaker.h" +#include "fpu.h" + +// SEE: https://wiki.osdev.org/PC_Speaker +// SEE ALSO: https://web.archive.org/web/20171115162742/http://guideme.itgo.com/atozofc/ch23.pdf + +static float notes[7][12] = { + { 130.81, 138.59, 146.83, 155.56, 164.81, 174.61, 185.0, + 196.0, 207.65, 220.0, 227.31, 246.96 }, + { 261.63, 277.18, 293.66, 311.13, 329.63, 349.23, 369.63, + 392.0, 415.3, 440.0, 454.62, 493.92 }, + { 523.25, 554.37, 587.33, 622.25, 659.26, 698.46, 739.99, + 783.99, 830.61, 880.0, 909.24, 987.84 }, + { 1046.5, 1108.73, 1174.66, 1244.51, 1328.51, 1396.91, 1479.98, + 1567.98, 1661.22, 1760.0, 1818.48, 1975.68 }, + { 2093.0, 2217.46, 2349.32, 2489.02, 2637.02, 2793.83, 2959.96, + 3135.96, 3322.44, 3520.0, 3636.96, 3951.36 }, + { 4186.0, 4434.92, 4698.64, 4978.04, 5274.04, 5587.86, 5919.92, + 6271.92, 6644.88, 7040.0, 7273.92, 7902.72 }, + { 8372.0, 8869.89, 9397.28,9956.08,10548.08,11175.32, 11839.84, + 12543.84, 13289.76, 14080.0, 14547.84, 15805.44 } +}; + +void speaker_note(u8 octave, u8 note) { + speaker_play((u32) notes[octave][note]); +} + +void speaker_play(u32 hz) { + u32 d = 1193180 / hz; + outportb(0x43, 0xB6); + //outportb(0x42, (u8) (d & 0xFF)); + //outportb(0x42, (u8) ((d >> 8) & 0xFF)); + outportb(0x42, 140); + outportb(0x42, 140); + + u8 t = inportb(0x61); + if (t != (t | 0x3)) { + outportb(0x61, t | 0x3); + } +} + +void speaker_pause() { + outportb(0x61, inportb(0x61) & 0xFC); +} diff --git a/src/speaker.h b/src/speaker.h new file mode 100644 index 0000000..464c370 --- /dev/null +++ b/src/speaker.h @@ -0,0 +1,10 @@ +#ifndef SPEAKER_H +#define SPEAKER_H + +#include "util.h" + +void speaker_note(u8 octave, u8 note); +void speaker_play(u32 hz); +void speaker_pause(); + +#endif diff --git a/src/stage0.S b/src/stage0.S new file mode 100644 index 0000000..9cb289e --- /dev/null +++ b/src/stage0.S @@ -0,0 +1,203 @@ +.code16 +.org 0 + +.text + +.global _start +_start: + cli + + /* segment setup */ + mov %cs, %ax + mov %ax, %ds + mov %ax, %es + mov %ax, %fs + mov %ax, %gs + mov %ax, %ss + + /* place stack pointer in middle of free memory area */ + movw $0x3000, %sp + + /* save drive number to read kernel later */ + mov %dl, drive_num + + sti + + /* should print TETRIS TIME */ + movw $welcome_str, %si + call print + + /* read kernel into memory at 0x10000 (segment 0x1000). + kernel binary has been placed on the disk directly after the first sector + reading $20 * num_sectors sectors after (value in %cx) + */ + movw $20, %cx + movb drive_num, %dl + movw $disk_packet, %si + movw $0x1000, segment + movw $1, sector +sector_loop: + movb $0x42, %ah + int $0x13 + jc disk_error + + addw $64, sector + addw $0x8000, offset + jnc sector_same_segment + + /* increment segment, reset offset if on different segment */ + addw $0x1000, segment + movw $0x0000, offset +sector_same_segment: + /* decrements %cx and loops if nonzero */ + loop sector_loop + + /* video mode: 320x200 @ 16 colors */ + movb $0x00, %ah + movb $0x13, %al + int $0x10 + + /* enable A20 line */ + cli + + /* read and save state */ + call enable_a20_wait0 + movb $0xD0, %al + outb $0x64 + call enable_a20_wait1 + xorw %ax, %ax + inb $0x60 + + /* write new state with A20 bit set (0x2) */ + pushw %ax + call enable_a20_wait0 + movb $0xD1, %al + outb $0x64 + call enable_a20_wait0 + popw %ax + orw $0x2, %ax + outb $0x60 + + /* enable PE flag */ + movl %cr0, %eax + orl $0x1, %eax + movl %eax, %cr0 + + /* jmp to flush prefetch queue */ + jmp flush +flush: + lidt idt + lgdt gdtp + + movw $(gdt_data_segment - gdt_start), %ax + movw %ax, %ds + movw %ax, %es + movw %ax, %es + movw %ax, %fs + movw %ax, %gs + movw %ax, %ss + movl $0x3000, %esp + ljmp $0x8, $entry32 + +.code32 +entry32: + /* jump to kernel loaded at 0x10000 */ + movl $0x10000, %eax + jmpl *%eax + +_loop: + jmp _loop + +.code16 +enable_a20_wait0: + xorw %ax, %ax + inb $0x64 + btw $1, %ax + jc enable_a20_wait0 + ret + +enable_a20_wait1: + xorw %ax, %ax + inb $0x64 + btw $0, %ax + jnc enable_a20_wait1 + ret + +disk_error: + movw $disk_error_str, %si + call print + +/* prints string in %ds:si */ +print: + xorb %bh, %bh + movb $0x0E, %ah + + lodsb + + /* NULL check */ + cmpb $0, %al + je 1f + + /* print %al to screen */ + int $0x10 + jmp print + +1: ret + +welcome_str: + .asciz "TETRIS TIME\n" +disk_error_str: + .asciz "DISK ERROR\n" + +/* SAVED DRIVE NUMBER TO READ FROM */ +drive_num: + .word 0x0000 + +/* INT 13H PACKET */ +disk_packet: + .byte 0x10 + .byte 0x00 +num_sectors: + .word 0x0040 +offset: + .word 0x0000 +segment: + .word 0x0000 +sector: + .quad 0x00000000 + +/* GDT */ +.align 16 +gdtp: + .word gdt_end - gdt_start - 1 + /* .long (0x07C0 << 4) + gdt */ + .long gdt_start + +.align 16 +gdt_start: +gdt_null: + .quad 0 +gdt_code_segment: + .word 0xffff + .word 0x0000 + .byte 0x00 + .byte 0b10011010 + .byte 0b11001111 + .byte 0x00 +gdt_data_segment: + .word 0xffff + .word 0x0000 + .byte 0x00 + .byte 0b10010010 + .byte 0b11001111 + .byte 0x00 +gdt_end: + +/* IDT */ +idt: + .word 0 + .long 0 + +/* MBR BOOT SIGNATURE */ +.fill 510-(.-_start), 1, 0 +.word 0xAA55 diff --git a/src/start.S b/src/start.S new file mode 100644 index 0000000..3c8aa4b --- /dev/null +++ b/src/start.S @@ -0,0 +1,126 @@ +.code32 +.section .text.prologue + +.global _start +_start: + movl $stack, %esp + andl $-16, %esp + movl $0xDEADBEEF, %eax + pushl %esp + pushl %eax + cli + call _main + +.section .text +.align 4 + +.global idt_load +.type idt_load, @function +idt_load: + mov 4(%esp), %eax + lidt (%eax) + ret + +.macro ISR_NO_ERR index + .global _isr\index + _isr\index: + cli + push $0 + push $\index + jmp isr_common +.endm + +.macro ISR_ERR index + .global _isr\index + _isr\index: + cli + push $\index + jmp isr_common +.endm + +ISR_NO_ERR 0 +ISR_NO_ERR 1 +ISR_NO_ERR 2 +ISR_NO_ERR 3 +ISR_NO_ERR 4 +ISR_NO_ERR 5 +ISR_NO_ERR 6 +ISR_NO_ERR 7 +ISR_ERR 8 +ISR_NO_ERR 9 +ISR_ERR 10 +ISR_ERR 11 +ISR_ERR 12 +ISR_ERR 13 +ISR_ERR 14 +ISR_NO_ERR 15 +ISR_NO_ERR 16 +ISR_NO_ERR 17 +ISR_NO_ERR 18 +ISR_NO_ERR 19 +ISR_NO_ERR 20 +ISR_NO_ERR 21 +ISR_NO_ERR 22 +ISR_NO_ERR 23 +ISR_NO_ERR 24 +ISR_NO_ERR 25 +ISR_NO_ERR 26 +ISR_NO_ERR 27 +ISR_NO_ERR 28 +ISR_NO_ERR 29 +ISR_NO_ERR 30 +ISR_NO_ERR 31 +ISR_NO_ERR 32 +ISR_NO_ERR 33 +ISR_NO_ERR 34 +ISR_NO_ERR 35 +ISR_NO_ERR 36 +ISR_NO_ERR 37 +ISR_NO_ERR 38 +ISR_NO_ERR 39 +ISR_NO_ERR 40 +ISR_NO_ERR 41 +ISR_NO_ERR 42 +ISR_NO_ERR 43 +ISR_NO_ERR 44 +ISR_NO_ERR 45 +ISR_NO_ERR 46 +ISR_NO_ERR 47 + +/* defined in isr.c */ +.extern isr_handler +.type isr_handler, @function + +isr_common: + pusha + push %ds + push %es + push %fs + push %gs + + mov $0x10, %ax + mov %ax, %ds + mov %ax, %es + mov %ax, %fs + mov %ax, %gs + cld + + push %esp + call isr_handler + add $4, %esp + + pop %gs + pop %fs + pop %es + pop %ds + + popa + + add $8, %esp + iret + +.section .data +.align 32 +stack_begin: + .fill 0x4000 +stack: diff --git a/src/system.c b/src/system.c new file mode 100644 index 0000000..f579e85 --- /dev/null +++ b/src/system.c @@ -0,0 +1,35 @@ +#include "system.h" +#include "screen.h" +#include "font.h" + +static u32 rseed = 1; + +void seed(u32 s) { + rseed = s; +} + +u32 rand() { + static u32 x = 123456789; + static u32 y = 362436069; + static u32 z = 521288629; + static u32 w = 88675123; + + x *= 23786259 - rseed; + + u32 t; + + t = x ^ (x << 11); + x = y; y = z; z = w; + return w = w ^ (w >> 19) ^ t ^ (t >> 8); +} + +void panic(const char *err) { + screen_clear(COLOR(7, 0, 0)); + + if (err != NULL) { + font_str(err, (SCREEN_WIDTH - font_width(err)) / 2, SCREEN_HEIGHT / 2 - 4, COLOR(7, 7, 3)); + } + + screen_swap(); + for (;;) {} +} diff --git a/src/system.h b/src/system.h new file mode 100644 index 0000000..d7eed35 --- /dev/null +++ b/src/system.h @@ -0,0 +1,21 @@ +#ifndef SYSTEM_H +#define SYSTEM_H + +#include "util.h" + +#define _assert_0() __error_illegal_macro__ +#define _assert_1(_e) do { if (!(_e)) panic(NULL); } while (0) +#define _assert_2(_e, _m) do { if (!(_e)) panic((_m)); } while (0) + +#define _assert(x, _e, _m, _f, ...) _f + +#define assert(...) _assert(,##__VA_ARGS__,\ + _assert_2(__VA_ARGS__),\ + _assert_1(__VA_ARGS__),\ + _assert_0(__VA_ARGS__)) + +void panic(const char *err); +u32 rand(); +void seed(u32 s); + +#endif diff --git a/src/timer.c b/src/timer.c new file mode 100644 index 0000000..549fe83 --- /dev/null +++ b/src/timer.c @@ -0,0 +1,48 @@ +#include "timer.h" +#include "isr.h" +#include "irq.h" + +#define PIT_A 0x40 +#define PIT_B 0x41 +#define PIT_C 0x42 +#define PIT_CONTROL 0x43 + +#define PIT_MASK 0xFF +#define PIT_SET 0x36 + +#define PIT_HZ 1193181 +#define DIV_OF_FREQ(_f) (PIT_HZ / (_f)) +#define FREQ_OF_DIV(_d) (PIT_HZ / (_d)) +#define REAL_FREQ_OF_FREQ(_f) (FREQ_OF_DIV(DIV_OF_FREQ((_f)))) + +static struct { + u64 frequency; + u64 divisor; + u64 ticks; +} state; + +static void timer_set(int hz) { + outportb(PIT_CONTROL, PIT_SET); + + u16 d = (u16) (1193131.666 / hz); + outportb(PIT_A, d & PIT_MASK); + outportb(PIT_A, (d >> 8) & PIT_MASK); +} + +u64 timer_get() { + return state.ticks; +} + +static void timer_handler(struct Registers *regs) { + state.ticks++; +} + +void timer_init() { + const u64 freq = REAL_FREQ_OF_FREQ(TIMER_TPS); + state.frequency = freq; + state.divisor = DIV_OF_FREQ(freq); + state.ticks = 0; + //timer_set(state.divisor); + timer_set(TIMER_TPS); + irq_install(0, timer_handler); +} diff --git a/src/timer.h b/src/timer.h new file mode 100644 index 0000000..0980c0e --- /dev/null +++ b/src/timer.h @@ -0,0 +1,12 @@ +#ifndef TIMER_H +#define TIMER_H + +#include "util.h" + +// number chosen to be integer divisor of PIC frequency +#define TIMER_TPS 363 + +u64 timer_get(); +void timer_init(); + +#endif diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..87373c4 --- /dev/null +++ b/src/util.h @@ -0,0 +1,202 @@ +#ifndef UTIL_H +#define UTIL_H + +// fixed width integer types +typedef unsigned char u8; +typedef unsigned short u16; +typedef unsigned int u32; +typedef unsigned long long u64; +typedef char i8; +typedef short i16; +typedef int i32; +typedef long long i64; +typedef u32 size_t; +typedef u32 uintptr_t; +typedef float f32; +typedef double f64; + +typedef u8 bool; +#define true (1) +#define false (0) + +#define NULL (0) + +#define CONCAT_IMPL(x, y) x##y +#define CONCAT(x, y) CONCAT_IMPL(x, y) + +#define __MIN_IMPL(_x, _y, _xn, _yn) __extension__({\ + __typeof__(_x) _xn = (_x);\ + __typeof__(_y) _yn = (_y);\ + (_xn < _yn ? _xn : _yn);\ + }) +#define MIN(_x, _y) __MIN_IMPL(_x, _y, CONCAT(__x, __COUNTER__), CONCAT(__y, __COUNTER__)) + +#define __MAX_IMPL(_x, _y, _xn, _yn) __extension__({\ + __typeof__(_x) _xn = (_x);\ + __typeof__(_y) _yn = (_y);\ + (_xn > _yn ? _xn : _yn);\ + }) +#define MAX(_x, _y) __MAX_IMPL(_x, _y, CONCAT(__x, __COUNTER__), CONCAT(__y, __COUNTER__)) + +#define CLAMP(_x, _mi, _ma) (MAX(_mi, MIN(_x, _ma))) + +// returns the highest set bit of x +// i.e. if x == 0xF, HIBIT(x) == 3 (4th index) +// WARNING: currently only works for up to 32-bit types +#define HIBIT(_x) (31 - __builtin_clz((_x))) + +// returns the lowest set bit of x +#define LOBIT(_x)\ + __extension__({ __typeof__(_x) __x = (_x); HIBIT(__x & -__x); }) + +// returns _v with _n-th bit = _x +#define BIT_SET(_v, _n, _x) __extension__({\ + __typeof__(_v) __v = (_v);\ + (__v ^ ((-(_x) ^ __v) & (1 << (_n))));\ + }) + +#define PACKED __attribute__((packed)) + +#ifndef asm +#define asm __asm__ volatile +#endif + +#define CLI() asm ("cli") +#define STI() asm ("sti") + +static inline u16 inports(u16 port) { + u16 r; + asm("inw %1, %0" : "=a" (r) : "dN" (port)); + return r; +} + +static inline void outports(u16 port, u16 data) { + asm("outw %1, %0" : : "dN" (port), "a" (data)); +} + +static inline u8 inportb(u16 port) { + u8 r; + asm("inb %1, %0" : "=a" (r) : "dN" (port)); + return r; +} + +static inline void outportb(u16 port, u8 data) { + asm("outb %1, %0" : : "dN" (port), "a" (data)); +} + +static inline size_t strlen(const char *str) { + size_t l = 0; + while (*str++ != 0) { + l++; + } + return l; +} + +static inline char *itoa(i32 x, char *s, size_t sz) { + // TODO: holy god this is bad code we need some error handling here + if (sz < 20) { + extern void panic(const char *); + panic("ITOA BUFFER TOO SMALL"); + } + + u32 tmp; + i32 i, j; + + tmp = x; + i = 0; + + do { + tmp = x % 10; + s[i++] = (tmp < 10) ? (tmp + '0') : (tmp + 'a' - 10); + } while (x /= 10); + s[i--] = 0; + + for (j = 0; j < i; j++, i--) { + tmp = s[j]; + s[j] = s[i]; + s[i] = tmp; + } + + return s; +} + +static inline void memset(void *dst, u8 value, size_t n) { + u8 *d = dst; + + while (n-- > 0) { + *d++ = value; + } +} + +static inline void *memcpy(void *dst, const void *src, size_t n) { + u8 *d = dst; + const u8 *s = src; + + while (n-- > 0) { + *d++ = *s++; + } + + return d; +} + +static inline void *memmove(void *dst, const void *src, size_t n) { + // OK since we know that memcpy copies forwards + if (dst < src) { + return memcpy(dst, src, n); + } + + u8 *d = dst; + const u8 *s = src; + + for (size_t i = n; i > 0; i--) { + d[i - 1] = s[i - 1]; + } + + return dst; +} + +// SEE: https://opensource.apple.com/source/Libc/Libc-1158.30.7/string/strlcat.c.auto.html +static inline size_t strlcat(char *dst, const char *src, size_t size) { + const size_t sl = strlen(src), + dl = strlen(dst); + + if (dl == size) { + return size + sl; + } + + if (sl < (size - dl)) { + memcpy(dst + dl, src, sl + 1); + } else { + memcpy(dst + dl, src, size - dl - 1); + dst[size - 1] = '\0'; + } + + return sl + dl; +} + +static inline size_t strlcpy(char *dst, const char *src, size_t n) { + // copy as many bytes as can fit + char *d = dst; + const char *s = src; + size_t size = n; + + while (--n > 0) { + if ((*d++ = *s++) == 0) { + break; + } + } + + // if we ran out of space, null terminate + if (n == 0) { + if (size != 0) { + *d = 0; + } + + // traverse the rest of s + while (*s++); + } + + return s - src - 1; +} + +#endif