From 67f527328075c730a7aa5ac52ddc12775fe4fa91 Mon Sep 17 00:00:00 2001 From: Bananymous Date: Wed, 23 Apr 2025 13:13:08 +0300 Subject: [PATCH] ports: Add tinygb port This is awesome! --- ports/tinygb/build.sh | 17 + .../0001-Add-support-for-banan-os.patch | 578 ++++++++++++++++++ 2 files changed, 595 insertions(+) create mode 100755 ports/tinygb/build.sh create mode 100644 ports/tinygb/patches/0001-Add-support-for-banan-os.patch diff --git a/ports/tinygb/build.sh b/ports/tinygb/build.sh new file mode 100755 index 00000000..66c6ea7d --- /dev/null +++ b/ports/tinygb/build.sh @@ -0,0 +1,17 @@ +#!/bin/bash ../install.sh + +NAME='tinygb' +VERSION='git' +DOWNLOAD_URL="https://github.com/jewelcodes/tinygb.git#57fdaff675a6b5b963b2b6624868d9698eabe375" + +configure() { + : +} + +build() { + make -f Makefile.banan_os -j$(nproc) || exit 1 +} + +install() { + cp -v tinygb "$BANAN_SYSROOT"/usr/bin || exit 1 +} diff --git a/ports/tinygb/patches/0001-Add-support-for-banan-os.patch b/ports/tinygb/patches/0001-Add-support-for-banan-os.patch new file mode 100644 index 00000000..11782211 --- /dev/null +++ b/ports/tinygb/patches/0001-Add-support-for-banan-os.patch @@ -0,0 +1,578 @@ +From 3e565f7d35e842e246db3371776adae74d02ae62 Mon Sep 17 00:00:00 2001 +From: Bananymous +Date: Wed, 23 Apr 2025 13:10:38 +0300 +Subject: [PATCH] Add support for banan-os + +--- + Makefile.banan_os | 28 +++ + src/platform/banan-os/main.cpp | 365 +++++++++++++++++++++++++++++++ + src/platform/banan-os/main.cpp.o | Bin 0 -> 23536 bytes + 3 files changed, 393 insertions(+) + create mode 100644 Makefile.banan_os + create mode 100644 src/platform/banan-os/main.cpp + create mode 100644 src/platform/banan-os/main.cpp.o + +diff --git a/Makefile.banan_os b/Makefile.banan_os +new file mode 100644 +index 0000000..22e191e +--- /dev/null ++++ b/Makefile.banan_os +@@ -0,0 +1,28 @@ ++CC = $(BANAN_ARCH)-banan_os-gcc ++CXX = $(BANAN_ARCH)-banan_os-g++ ++LD = $(BANAN_ARCH)-banan_os-gcc ++ ++CFLAGS = -c -O2 -Isrc/include -Wall ++CXXFLAGS = --std=c++20 ++LDFLAGS = -O2 -lgui -linput ++ ++SRC := $(shell find src -name "*.c" -not -path 'src/platform/*') $(shell find src/platform/banan-os -name "*.c" -or -name "*.cpp") ++OBJ := $(addsuffix .o,$(SRC)) ++ ++all: tinygb ++ ++clean: ++ @rm -f $(OBJ) ++ @rm -f tinygb ++ ++%.c.o: %.c ++ @echo -e "\x1B[0;1;35m [ CC ]\x1B[0m $@" ++ $(CC) -o $@ $(CFLAGS) $< ++ ++%.cpp.o: %.cpp ++ @echo -e "\x1B[0;1;35m [ CC ]\x1B[0m $@" ++ $(CXX) -o $@ $(CFLAGS) $(CXXFLAGS) $< ++ ++tinygb: $(OBJ) ++ @echo -e "\x1B[0;1;36m [ LD ]\x1B[0m tinygb" ++ $(LD) $(OBJ) -o tinygb $(LDFLAGS) +diff --git a/src/platform/banan-os/main.cpp b/src/platform/banan-os/main.cpp +new file mode 100644 +index 0000000..e9c6a02 +--- /dev/null ++++ b/src/platform/banan-os/main.cpp +@@ -0,0 +1,365 @@ ++ ++/* tinygb - a tiny gameboy emulator ++ (c) 2022 by jewel */ ++ ++extern "C" { ++#include ++} ++ ++#include ++ ++#include ++#include ++#include ++ ++long rom_size; ++int scaling = 4; ++int frameskip = 0; // no skip ++ ++timing_t timing; ++char *rom_filename; ++ ++BAN::UniqPtr s_window; ++ ++// Key Config ++LibInput::Key key_a; ++LibInput::Key key_b; ++LibInput::Key key_start; ++LibInput::Key key_select; ++LibInput::Key key_up; ++LibInput::Key key_down; ++LibInput::Key key_left; ++LibInput::Key key_right; ++LibInput::Key key_throttle; ++ ++LibInput::Key get_key(const char* keyname) ++{ ++ if (keyname == nullptr); ++ else if (!strcmp("a", keyname)) return LibInput::Key::A; ++ else if (!strcmp("b", keyname)) return LibInput::Key::B; ++ else if (!strcmp("c", keyname)) return LibInput::Key::C; ++ else if (!strcmp("d", keyname)) return LibInput::Key::D; ++ else if (!strcmp("e", keyname)) return LibInput::Key::E; ++ else if (!strcmp("f", keyname)) return LibInput::Key::F; ++ else if (!strcmp("g", keyname)) return LibInput::Key::G; ++ else if (!strcmp("h", keyname)) return LibInput::Key::H; ++ else if (!strcmp("i", keyname)) return LibInput::Key::I; ++ else if (!strcmp("j", keyname)) return LibInput::Key::J; ++ else if (!strcmp("k", keyname)) return LibInput::Key::K; ++ else if (!strcmp("l", keyname)) return LibInput::Key::L; ++ else if (!strcmp("m", keyname)) return LibInput::Key::M; ++ else if (!strcmp("n", keyname)) return LibInput::Key::N; ++ else if (!strcmp("o", keyname)) return LibInput::Key::O; ++ else if (!strcmp("p", keyname)) return LibInput::Key::P; ++ else if (!strcmp("q", keyname)) return LibInput::Key::Q; ++ else if (!strcmp("r", keyname)) return LibInput::Key::R; ++ else if (!strcmp("s", keyname)) return LibInput::Key::S; ++ else if (!strcmp("t", keyname)) return LibInput::Key::T; ++ else if (!strcmp("u", keyname)) return LibInput::Key::U; ++ else if (!strcmp("v", keyname)) return LibInput::Key::V; ++ else if (!strcmp("w", keyname)) return LibInput::Key::W; ++ else if (!strcmp("x", keyname)) return LibInput::Key::X; ++ else if (!strcmp("y", keyname)) return LibInput::Key::Y; ++ else if (!strcmp("z", keyname)) return LibInput::Key::Z; ++ else if (!strcmp("0", keyname)) return LibInput::Key::_0; ++ else if (!strcmp("1", keyname)) return LibInput::Key::_1; ++ else if (!strcmp("2", keyname)) return LibInput::Key::_2; ++ else if (!strcmp("3", keyname)) return LibInput::Key::_3; ++ else if (!strcmp("4", keyname)) return LibInput::Key::_4; ++ else if (!strcmp("5", keyname)) return LibInput::Key::_5; ++ else if (!strcmp("6", keyname)) return LibInput::Key::_6; ++ else if (!strcmp("7", keyname)) return LibInput::Key::_7; ++ else if (!strcmp("8", keyname)) return LibInput::Key::_8; ++ else if (!strcmp("9", keyname)) return LibInput::Key::_9; ++ else if (!strcmp("up", keyname)) return LibInput::Key::ArrowUp; ++ else if (!strcmp("down", keyname)) return LibInput::Key::ArrowDown; ++ else if (!strcmp("left", keyname)) return LibInput::Key::ArrowLeft; ++ else if (!strcmp("right", keyname)) return LibInput::Key::ArrowRight; ++ else if (!strcmp("space", keyname)) return LibInput::Key::Space; ++ else if (!strcmp("rshift", keyname)) return LibInput::Key::RightShift; ++ else if (!strcmp("lshift", keyname)) return LibInput::Key::LeftShift; ++ else if (!strcmp("backspace", keyname)) return LibInput::Key::Backspace; ++ else if (!strcmp("delete", keyname)) return LibInput::Key::Delete; ++ else if (!strcmp("tab", keyname)) return LibInput::Key::Tab; ++ else if (!strcmp("escape", keyname)) return LibInput::Key::Escape; ++ else if (!strcmp("exclamation", keyname)) return LibInput::Key::ExclamationMark; ++ else if (!strcmp("at", keyname)) return LibInput::Key::AtSign; ++ else if (!strcmp("hash", keyname)) return LibInput::Key::Hashtag; ++ else if (!strcmp("dollar", keyname)) return LibInput::Key::Dollar; ++ else if (!strcmp("percent", keyname)) return LibInput::Key::Percent; ++ else if (!strcmp("caret", keyname)) return LibInput::Key::Caret; ++ else if (!strcmp("ampersand", keyname)) return LibInput::Key::Ampersand; ++ else if (!strcmp("asterisk", keyname)) return LibInput::Key::Asterix; ++ else if (!strcmp("leftparenthesis", keyname)) return LibInput::Key::OpenParenthesis; ++ else if (!strcmp("rightparenthesis", keyname)) return LibInput::Key::CloseParenthesis; ++ ++ return LibInput::Key::None; ++} ++ ++static void load_keys() ++{ ++ key_a = get_key(config_file.a); ++ if (key_a == LibInput::Key::None) ++ key_a = LibInput::Key::Z; ++ ++ key_b = get_key(config_file.b); ++ if (key_b == LibInput::Key::None) ++ key_b = LibInput::Key::X; ++ ++ key_start = get_key(config_file.start); ++ if (key_start == LibInput::Key::None) ++ key_start = LibInput::Key::Enter; ++ ++ key_select = get_key(config_file.select); ++ if (key_select == LibInput::Key::None) ++ key_select = LibInput::Key::RightShift; ++ ++ key_up = get_key(config_file.up); ++ if (key_up == LibInput::Key::None) ++ key_up = LibInput::Key::ArrowUp; ++ ++ key_down = get_key(config_file.down); ++ if (key_down == LibInput::Key::None) ++ key_down = LibInput::Key::ArrowDown; ++ ++ key_left = get_key(config_file.left); ++ if (key_left == LibInput::Key::None) ++ key_left = LibInput::Key::ArrowLeft; ++ ++ key_right = get_key(config_file.right); ++ if (key_right == LibInput::Key::None) ++ key_right = LibInput::Key::ArrowRight; ++ ++ key_throttle = get_key(config_file.throttle); ++ if (key_throttle == LibInput::Key::None) ++ key_throttle = LibInput::Key::Space; ++} ++ ++void delay(int ms) ++{ ++ const timespec ts { ++ .tv_sec = static_cast(ms / 1000), ++ .tv_nsec = (ms % 1000) * 1000000 ++ }; ++ nanosleep(&ts, nullptr); ++} ++ ++void destroy_window() ++{ ++ s_window.clear(); ++} ++ ++void update_window(uint32_t *framebuffer) ++{ ++ for (int i = 0; i < scaled_h; i++) ++ { ++ uint32_t* src = &framebuffer[i * scaled_w]; ++ uint32_t* dst = using_sgb_border ++ ? &s_window->pixels()[(i + gb_y) * s_window->width() + gb_x] ++ : &s_window->pixels()[i * s_window->width()]; ++ memcpy(dst, src, scaled_w * 4); ++ } ++ ++ if (framecount > frameskip) ++ { ++ s_window->invalidate(); ++ framecount = 0; ++ drawn_frames++; ++ } ++} ++ ++void update_border(uint32_t *framebuffer) ++{ ++ for (int i = 0; i < sgb_scaled_h; i++) ++ { ++ uint32_t* src = &framebuffer[i * sgb_scaled_w]; ++ uint32_t* dst = &s_window->pixels()[i * s_window->width()]; ++ memcpy(dst, src, sgb_scaled_w*4); ++ } ++} ++ ++void resize_sgb_window() ++{ ++ s_window->request_resize(SGB_WIDTH * scaling, SGB_HEIGHT * scaling); ++ ++ bool resized { false }; ++ s_window->set_resize_window_event_callback([&]() { resized = true; }); ++ while (!resized) ++ s_window->poll_events(); ++ s_window->set_resize_window_event_callback({}); ++ ++ ASSERT(s_window->width() == static_cast(SGB_WIDTH * scaling)); ++ ASSERT(s_window->height() == static_cast(SGB_HEIGHT * scaling)); ++} ++ ++int main(int argc, char **argv) ++{ ++ if(argc != 2) ++ { ++ fprintf(stdout, "usage: %s rom_name\n", argv[0]); ++ return 1; ++ } ++ ++ rom_filename = argv[1]; ++ ++ open_log(); ++ open_config(); ++ load_keys(); ++ ++ // open the rom ++ FILE* rom_file = fopen(rom_filename, "r"); ++ if (rom_file == nullptr) ++ { ++ write_log("unable to open %s for reading: %s\n", rom_filename, strerror(errno)); ++ return 1; ++ } ++ ++ fseek(rom_file, 0L, SEEK_END); ++ rom_size = ftell(rom_file); ++ fseek(rom_file, 0L, SEEK_SET); ++ ++ write_log("loading rom from file %s, %d KiB\n", rom_filename, rom_size / 1024); ++ ++ rom = malloc(rom_size); ++ if (rom == nullptr) ++ { ++ write_log("unable to allocate memory\n"); ++ fclose(rom_file); ++ return 1; ++ } ++ ++ if (fread(rom, 1, rom_size, rom_file) <= 0) ++ { ++ write_log("an error occured while reading from rom file: %s\n", strerror(errno)); ++ fclose(rom_file); ++ free(rom); ++ return 1; ++ } ++ ++ fclose(rom_file); ++ ++ if (auto ret = LibGUI::Window::create(GB_WIDTH * scaling, GB_HEIGHT * scaling, "tinygb"_sv); !ret.is_error()) ++ s_window = ret.release_value(); ++ else ++ { ++ write_log("couldn't create SDL window: %s\n", ret.error().get_message()); ++ free(rom); ++ return 1; ++ } ++ ++ s_window->set_key_event_callback([](LibGUI::EventPacket::KeyEvent::event_t event) { ++ int key = 0; ++ if (event.key == key_left) ++ key = JOYPAD_LEFT; ++ else if (event.key == key_right) ++ key = JOYPAD_RIGHT; ++ else if (event.key == key_up) ++ key = JOYPAD_UP; ++ else if (event.key == key_down) ++ key = JOYPAD_DOWN; ++ else if (event.key == key_a) ++ key = JOYPAD_A; ++ else if (event.key == key_b) ++ key = JOYPAD_B; ++ else if (event.key == key_start) ++ key = JOYPAD_START; ++ else if (event.key == key_select) ++ key = JOYPAD_SELECT; ++ else if (event.key == key_throttle) ++ throttle_enabled = event.released(); ++ else if (event.pressed() && (event.key == LibInput::Key::Plus || event.key == LibInput::Key::Equals)) ++ next_palette(); ++ else if (event.pressed() && (event.key == LibInput::Key::Hyphen)) ++ prev_palette(); ++ ++ if (key) ++ joypad_handle(event.pressed(), key); ++ }); ++ ++ // start emulation ++ memory_start(); ++ cpu_start(); ++ display_start(); ++ timer_start(); ++ sound_start(); ++ ++ time_t rawtime; ++ struct tm *timeinfo; ++ int sec = 500; // any invalid number ++ int percentage; ++ int throttle_underflow = 0; ++ int throttle_target = throttle_lo + SPEED_ALLOWANCE; ++ ++ for (;;) ++ { ++ s_window->poll_events(); ++ ++ for (timing.current_cycles = 0; timing.current_cycles < timing.main_cycles;) ++ { ++ cpu_cycle(); ++ display_cycle(); ++ timer_cycle(); ++ } ++ ++ time(&rawtime); ++ timeinfo = localtime(&rawtime); ++ ++ if (sec != timeinfo->tm_sec) ++ { ++ sec = timeinfo->tm_sec; ++ percentage = (drawn_frames * 1000) / 597; ++ ++ // adjust cpu throttle according to acceptable fps (98%-102%) ++ if (throttle_enabled){ ++ if(percentage < throttle_lo) { ++ // emulation is too slow ++ if(!throttle_time) { ++ // throttle_time--; ++ ++ if(!throttle_underflow) { ++ throttle_underflow = 1; ++ write_log("WARNING: CPU throttle interval has underflown, emulation may be too slow\n"); ++ } ++ } else { ++ //write_log("too slow; decreasing throttle time: %d\n", throttle_time); ++ ++ // this will speed up the speed adjustments for a more natural feel ++ if(percentage < (throttle_target/3)) throttle_time /= 3; ++ else if(percentage < (throttle_target/2)) throttle_time /= 2; ++ else throttle_time--; ++ } ++ ++ // prevent this from going too low ++ if(throttle_time <= (THROTTLE_THRESHOLD/3)) { ++ cycles_per_throttle += (cycles_per_throttle/5); // delay 20% less often ++ throttle_time = (THROTTLE_THRESHOLD/3); ++ } ++ } else if(percentage > throttle_hi) { ++ // emulation is too fast ++ //write_log("too fast; increasing throttle time: %d\n", throttle_time); ++ ++ if(throttle_time) { ++ // to make sure we're not multiplying zero ++ if(percentage > (throttle_target*3)) throttle_time *= 3; ++ else if(percentage > (throttle_target*2)) throttle_time *= 2; ++ else throttle_time++; ++ } ++ else { ++ throttle_time++; ++ } ++ ++ // prevent unnecessary lag ++ if(throttle_time > THROTTLE_THRESHOLD) { ++ cycles_per_throttle -= (cycles_per_throttle/5); // delay 20% more often ++ throttle_time = THROTTLE_THRESHOLD; ++ } ++ } ++ } ++ ++ drawn_frames = 0; ++ } ++ } ++ ++ die(0, ""); ++ return 0; ++} +diff --git a/src/platform/banan-os/main.cpp.o b/src/platform/banan-os/main.cpp.o +new file mode 100644 +index 0000000000000000000000000000000000000000..3774a9abb35f0e1c899e550571c8755c90296733 +GIT binary patch +literal 23536 +zcmb_k3wTu3wcbe(5Tpb6ew9H{KqX|5rw>dZ0}}~|K$NO#_3yJ*vT`!VS6v@x +z_S*kmd#$zK`<%VWwc+|@($y_|dS6jB)oMC2K*&`@Efga?WOwWF$9dCa;_cxYhvXg4d+;y#6sDTVJwWxsN7 +z59J$MGQAw>U+vjF3gIqRS8aAzP`vM|fq?Dtg=Jg|Rn +z&yH&egD-{O3TE5M%EFhzqr>oy{-!|Zv81G<-wAY{Zkgsl=RC`7DvY=JP~!YIJNjEV +zemLScb@V3!oztw~=0NA!me~^MTwc@|4E +zhOQRqoNBQQW0Nd4!D3q(!x#v3zWE+(WEsPl2z36@VmZc+ve*+A%QJSg#eQb70%ONm +z?E4nm#@MkI`-;W3Gj^QCuCrJhW5-*p)ne_8onW!gSnNv1FjfPdVT)BUhA|rG3=tM7 +z{^4PYn3wUhvp3K=o|RE%+*-=dHKo9_OMyp~ +z0zZ6-?knZeEv3NrQs8B!z;jE1CzS%NTPg6mQea~#aA_%US}E`~ +z>Qlq~?C38${>hFPmUlc-*7uXQ2L=LDs!j0#&7QT!*g=fBD$scgCH4J=(=Cn}G|;&N +z_&$we1`Tw!0KZS;<1D@!_~RNMZ}Ek|@7FjMx|bZaErlh1$P;^P2esEw*lM&aBIOefU5(y0o(=P;^5|iJ0IMc;I@F93T`vF +zW5FfBO#s&dt^(W)aBsdzYfMih{#fy?hR_?4MDJ588z=60YYHtPw0umY1^M)u2O{wU +ziQbp(C~G)0^RdK0^^$;z#A(90xsjq0^Z|42*Ia#ztk!F0MY>SASYPw2x-P1!?OHxJ^knD_o2A3blns^Q@|r6) +z>?IHKr`8sqhiTY_#q6$!pk-)=ru~Emp~b!vGKS038`y>Npf(5V!L5T5rH9Q09-F!O +z?^DX3T36lu7>o{i*5mQ3XYa7>UbShs9~qh>fz*DtqimEq>JC2)_&{?Y`3qFo=iHVI +z%v^a};^~8wY0pbzZN+hhec^{~#ja}(q*vb79QbQ*U}pFLJqPJIMA>=+lWyKk`2v$Z +z|F|1G;|94%G)^ua%Q(hqizj2Pv3w$(GO@gAiRD^IXEGVfnv9c;JE^>h$Ffe|#9Aqy +zi=~=OESGn(iQE=zHbaT2e2bGyoZxA>U^d+vO~qQBafZ-TtTE{X^XXtL +znM}uHc_-NFw5GG|<4iu0YTwi-HmF4Bty?@}=Ad?Jf!QtTR%doPwejpT%H;y9*tA2pI4w-JbO+wzj(&l +zsYYnYp1$Zm^+ +zPV_;BBjLK`k#!|4yLvy+{v20ss1ef{Cxs>^kAqn!)4iId%Kh%pT +z`K;h+O~Dn3B`juQ+-HL6c)XBxnu6O~Ajk^ctdd=+|Jdz#3H6^?Do;&0XZ~FBGoCIa +zn^M#B!8nf#8rx#YAa&JXfrdo3 +zIZ56Btf13cphl#T8ElQU2OH7b)4?32jpH5@U3JmA=$dfN(nz?L8jXy`TWEBB0_B-^ +zwyk*nq4)QRk&gcI{3Y(%W8Wrs_7;wfJhpUVISJXNL(QkO87!Px-!*;Z&KL8~)))U| +zCu9ok#AYMIa(d@YnGPkVwR&mIB{dB->(M^5P|4b)8U9Jf`X`O{Px?RXd0Yp) +zwqEp4ddffP5&xtg4<*@(QU)VM>`iD_P9pY&pZ0vk$K2}SB?(&_Y7bp~_hTWm+XGoR +zd4G0mf==FTJ-GomDXE)}jg0q|Tx!~*u9$_TeBE^+G7==emGuR<05h_A`aTa;R_3v(eB0HZ9 +zbjGPwLcO$|zx95+bI`lq>p!?3A5+4iH)!%^+cI*TV3kKjpu{+VY=C +z*q(#;m4_OrdT#yud+tuYxF@jd2V~vtVAR?>$55%lX!eVVao;N*LS}b6j~CVA7VwX7BC +z+eV!0_tl?wn+dISEBUmvySCLVdOXm5Cq=qPt#Li-=${dGKzA*9(LHK%-+2@c8*^ND_9eEi_^90% +zt2dD5o(IT;dU>(}0|RBJO#c~8GCL1Dtm_E#?qR1G^Sv>9sStlY{Dv|@o*GYU97&BD +zZ~F&S2(3Dsc6$q#6;g4mzIDsCZ3~A(bC%Lh6PwN@v78eY(URea{Hqq$CmQQgnL>Wy +z3a351jke6;kEXJu9Y64l5e}dLbEN?FvDU_>Slwlz1zFm+U+F}1n;LoR8E!~!3rEB6 +zUpzBXzfS>2n4F7EC>dEhC;Gu={EGL1co~r~%a<-aCpcsIs)m`t(45LSl~o4cZb4;N +zt}$h86U$CKV$9eclEk!z^9~TYD+pMX<%VOmjE&_Ga_gCweJHSG32X;rbJ@r=ihr3g +z`z}!Psfjm~FP$(t+)=S%965L4_%QvNYLa=7?jB3=2il|H#YJY +zMH*#Z8yJ~FP8#W9L#gOr&*a +zFq4J@9ILRWpCgkhd|I!<;ACew-NF7~I|Lf;&v7oOe&4qBGt@hz9S)=JSx)+c7~@1k +z67YYj3##9fNgnkeB$ncg2RUIWhu>$sh2tXs2ID&zzmUVFwu4h6dniG;_^gw*5||VY3EsuH^c2Cr%y^KE3jTG*WeykoHpZo#g8zVVsb9f=%D9wC +z@JEOdA0-Xp0U!Loeel2d;BOOtiW(R4U1%!pOiI~ZKzJ$n4TMjo+Q*s(vTFr}TYU6n +zS^fadT8QkrKw+DY{Pip!X6go#i(&VAl#li+@X2aE#2N}JqyALN|6_dc +zDL(jg!qF~Ou3D-om*)D&pYMa$`QQyc_~kx$%m>GbGyL4>@pF@p{8k^l%?JOS4}QH5 +zUL<^~IhDOJhPtbzyL{x|@xjZe!b`Q|WFP!QAAE)nKF0^I_QC6Y@bx};6X9r=YA(O5 +z4_kcX3qE*<5B?n={AM5gRv-L(KKQ@;;J@_2f9r$41f2F9UV7a}-q3tB)!>VkOVc=< +zVR5O&p}w)2@KXI`o0b2xsG(CUmws&VDTBD=ebq;gk)sO}y==s*?Z9wDTmXW@E +z+@e##M!NLzmD$oQ;3M3+RrBeK-13IH`Ip)w*KoLYp6#Md$<^Ch%@(IUY7f=HxCiUt +z=)4_)J#z;Wv(ZNTOXsxNyvxxcd)#H^jL&RNw`XEa(H1&CPdX-(b+$z_F*=LKVLYD$ +zMjd;k-DFZuTYfN=C%pIZn*@=+?!>J@R5%V*=QbgjTi#@Y=X +zil=i)$3dJD((U|}(8!4~-9TufgHC}9IXbtsUlSYCSvtow^j)zXKW&B%=i`}n)10OA +z``ng9#^vb1lr9)h8Halxs!F8jKr{i};Y4EdX4907ZBIqr+$29|PBcnq_R&}_=VbAX +zHtJq1pc1UDii%h41io(KjNGC6H!8B-jQIR%_$J#aT);@Ds9y5z +z(QQ=(=JHMHLf$lIvUG;rY%nD>(}6ecNEn=`N0aGIE)!3uniHGofEs}nGR=@M%{j-} +zVw&?#GHJGF6V#T-(`qkOKq0jg9mkvIcru;CT;px2g-uR#tdPt{WBGhG(OAekIn00_ +zvH9+SI8~Y*g`ujoB%IoGNy6EVnh4EtN8xI^sN&>9RW(W%*(bYEx(kv@lW%m>16Nh3 +zH@de&+)E!+6BvE*Oo6$kM2;>%v~w(lqa5pSo;H=@Fg +zxhh;9UL*kwja?hFsW~)LR0j2+qcnix(NtzAM@zy)8&bF(5MATs3ZyI=ZOWx(iik$z +zZ854;r!9d2Qeq~=y5u>N@LdW<%3ZQ5R0SJtvBt!&S{Q7VH;3NMRk37A8SbE(@zJeW)@*)I{ +zW!zid<2>?GM<@Hp&+^F27w9=Y^7vl~An`}uTbBFCU+R&^$t&mzANiz5-X4ULJie=f +z5a{;3&LfZiBLKR=NB*lGd3gi-hL8NedF0WjL3jDc_j=^dbj8ijedK@Rk>_h+=J!7G +z|Lc)o#QOj0BR}AgU&`{M_=56pk|RTX8ndII&b|<3V(`m +z)C0~4ME>_mUii+_Ia2E!2q{5+J +z>T?0(G7tQc(m>}a99PdG7NsL&#$mUb9vri?@>>5Bj7vR;|4%Di`~R%cU#q8H=xD}KKf1iL6dtBDshEf243- +zZ+A29?LT`N_m=C|3fJWtH=Ya^pp@v-9{iw1jXBjv +zkDOydzn;fX=O8csnhRTe2JzBfhqJ$s*Z5Q)yv7G#uke{l{|<$psc_kU!R{=DW3LYK +z`j4>#^2%T2!XxAx8Ap5RTMnBPK9P9@N#>!ynjWbi#6bu#Ckc+e2Lhg^M{xOqkAAg6 +z;ZunR!5`$6^IFt72zu)25j_|SAm~|1kKm92K^}V|!7m0#1RQlD_Djk829RVNXbKww9}&sKb?5dv!8LVp5G~X=$Xdx=M;{%7CkRA?$z@*B@aDkar`ZX +zpF+Io8DQM2C&1%N+6!|l=xBxGT}1Spz_?e>8A=}hV6OnGQh1Q!qGz6up0JYF{q{n} +zrT>rT_-ZAOu`l`?7>A$e+k$5lzDD6~3SX=68HD@E`9Y;{W|hUi-60$zP)6A6N31Djffn0R;UR9Vq=dJoRDTr4MWz1=rT^;+*Yo^)3dfu&{@^|t2zEb9kKjizj%>O=Ok!O8zmevYN +z!tYafqrx9lcwFI+Fphd|QutE}*Y)a$PDJA}(#;gAQH^{WgGPK2PD9?>&e +z;n-scK3?H13ZKL{{D<9%^nmcEK0tzt-Kk1Xg3?9*OoeY&_yVQp48rBTtXkn05H9a! +zYZTt3^lVi47KLwDcv9ilD;)Bo|4Rzj^50T8{E>3q&Nu=+?(S5$9(VUJF5~MN9$ybB +zc|E@NDO}H!|G_xwU(Xv)DtSF`{1@Y}i?)*YxaSovp_c;ac8dTXx#tWF +z1UyfV%rj#F5&_RzZurmp$rpx;+8E@w(^E=6){7uLAW4V3yi)+j+z9gOkvaqpJv-c-ezjar}@64|3cZBwqXxzhylX{USaQ0FmI;Hb!Of +z;Hw!AatI%}%UVSGjNsyb#3LW%IOZsj$V<60Eu?^Y0kVdJqf*WQXh5>ZL#>~7d1T$XNzRMN3@-c-`{ +zjF_ox%;ikwAX-Uh|G0MMf4RY4a;wayksJC$l_AQnq6-J3qU#^HKjF3TiK&E_QWb-R +z{s$b@>I$i3VvCb(*Oz&NwaZB_pclY?^BO +zlc5UQr|a=9TC17Co(zO<3n$ZqbrU3^U;cVmZ1B4k@?)-*d=mQQU(fk@I#nfwX!MfV6$Avyx9jzx+FpnMm8m_XWu(VURFP +z-Mpr|U(Bu!)RIfSc6NRV@IAV?!>pkqdzKgD>Kb>%P5tsZ(*W}HQV%qY1^0%Dt=Bnb%C_k#gTmDAE +zV1Euhn%-dlmm^V;L?zrxX{GX4^GnYo9>2uC`VW{eI#ptP@~HnWJ6wi +zkkwQW`x+#E;$j!5VjT95qQ`50myi8JYl?vp4O +zq-n>xtbQi~0$EKzA>dO4c*oy*&M)PaJQA1i4$5!aPXsvJ&6`DOC)D5Z^hiFz@uwo; +zQ@N*~Z)E$^Sf2zzVK~HJ`zV$FM$Yf8Z?P|G{z{ok^}hq`zi$7ND3jzD|EH1PrR?9v +z_7{jD4uhUJ_PM3jW=NMl9@mEf*eUD%EWs$HT5)}m%5B5&6 +z@4>59xWH=8kM^HRkJtW#VxK}yL39=({(Rs@=-1P7n|3>*s_5U69uEaq$gz}$8kK~vBgJe(9ZN|H>FcVetIx>TJe3$m- +m$7`K;>=I-S@r#t)KL7@ja=S5i?^i1S##OGwI8UNC|NjH~pc5