From 57870298585bb843ab31b5384dd6e98e09987faf Mon Sep 17 00:00:00 2001 From: Jaidyn Ann Date: Mon, 20 Jul 2020 04:01:38 -0500 Subject: [PATCH] Init --- Makefile | 2 + nes_small_v.ini | 41 ++++ sbp.mfk | 546 ++++++++++++++++++++++++++++++++++++++++++++++++ sbp.nes | Bin 0 -> 40976 bytes tileset.chr | Bin 0 -> 8192 bytes 5 files changed, 589 insertions(+) create mode 100644 Makefile create mode 100644 nes_small_v.ini create mode 100644 sbp.mfk create mode 100644 sbp.nes create mode 100644 tileset.chr diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5635260 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +all: + millfork -t nes_small sbp.mfk diff --git a/nes_small_v.ini b/nes_small_v.ini new file mode 100644 index 0000000..fd4744b --- /dev/null +++ b/nes_small_v.ini @@ -0,0 +1,41 @@ +; a very simple NES cartridge format +; uses mapper 0 and no bankswitching, so it's only good for very simple games +; assumes CHRROM is at chrrom:$0000-$1fff and PRGROM is at prgrom:$8000-$ffff +; same as nes_small.ini, but uses vertical mirroring +; output file size: 40976 bytes + +[compilation] +arch=ricoh +modules=nes_hardware,nes_routines,default_panic,stdlib + +[allocation] +zp_bytes=all + +segments=default,prgrom,chrrom +default_code_segment=prgrom +ram_init_segment=prgrom + +segment_default_start=$200 +segment_default_end=$7ff +segment_default_bank=$ff + +segment_prgrom_start=$8000 +segment_prgrom_end=$ffff + +segment_chrrom_start=$0000 +segment_chrrom_end=$1fff + +[define] +NES=1 +WIDESCREEN=0 +KEYBOARD=0 +JOYSTICKS=2 +HAS_BITMAP_MODE=0 + +[output] +style=single +format=$4E,$45,$53,$1A, 2,1,1,0, 0,0,0,0, 0,0,0,0, prgrom:$8000:$ffff, chrrom:$0000:$1fff +extension=nes +labels=nesasm + + diff --git a/sbp.mfk b/sbp.mfk new file mode 100644 index 0000000..0dbfcb5 --- /dev/null +++ b/sbp.mfk @@ -0,0 +1,546 @@ +// SUPER BREAD PAN +// SUPERPANSKATOL' +// Based on Garydos'es Pong example + +import random +import nes_joy + +segment(chrrom) const array graphics @ $0000 = file("tileset.chr") + +// ============================================================================ +// STRUCTS +// ============================================================================ +// sprite layout in oam +struct Sprite { + byte y, + byte tile, + byte attrs, + byte x +} + +// abstraction for 16x16 animated monstrosities +struct Entity { + Sprite top0, // left side, for the record + Sprite bottom0, + Sprite top1, // right side, please kill me + Sprite bottom1, + + byte movement, // 0-3; idle, walk, jump/fall + byte direction, + byte frame // internal count used for animation +} + +// ============================================================================ +// VARIABLES +// ============================================================================ +byte i +byte score1 +array oam_buffer [256] @$200 // sprite buffer +word framecounter +Entity sam @$204 // player character +volatile Gamestate gamestate // the current Gamestate + +// ============================================================================ +// CONSTANTS +// ============================================================================ +const array pallete = [ + $22,$29,$1A,$0F, $22,$36,$17,$0F, $22,$30,$21,$0F, $22,$27,$17,$0F, + $22,$1C,$15,$14, $0F,$18,$28,$0F, $22,$1C,$15,$14, $22,$02,$38,$3C +] + +const array attribute = [ + %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, + %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, + %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, %00000101, %00000101 +] + +enum Gamestate { + STATETITLE, + STATEPLAYING, + STATEGAMEOVER +} + +const array scorebackground = "P1 Score- " ascii +const array gameover_msg = "G A M E O V E R" ascii +const array title_msg = "Press Start" ascii + +// ------------------------------------- +// LOCATIONS +// ------------------------------------- +// vram locations declared as constants for readability/convenience +// *note that these do not correlate to CPU ram locations* +const word ppu_pallete_ram = $3F00 +const word ppu_nametable_ram = $2000 +const word ppu_nametable_0_attr_ram = $23C0 + +// ------------------------------------- +// LEVEL +// ------------------------------------- +const byte RIGHTWALL = $F4 +const byte TOPWALL = $18 +const byte BOTTOMWALL = $B0 +const byte LEFTWALL = $04 + +// ------------------------------------- +// SAM +// ------------------------------------- +// i don't actually remember if the super crate box guy is named sam or not +// or if he even has a name? +const byte SAM_HEIGHT = $20 // height of each paddle in pixels +const byte SAM_ATTR = %00000001 +const byte SAM_ATTR_HFLIP = %01000001 +const byte SAM_ATTR_VFLIP = %10000001 +const byte SAM_ATTR_HVFLIP = %11000001 + +// ------------------------------------- +// ANIMATIONS +// ------------------------------------- +// all Entity (16x16)'s animations are assumed by update_entity() to consist of +// 5 frames. An animation array just contains 20 locations in CHR, four per +// frame, in this totally logical order: +// top-left, bottom-left, top-right, bottom-right +const array SAM_IDLE = [ $44, $54, $45, $55, + $46, $56, $47, $57, + $48, $58, $49, $59, + $4A, $5A, $4B, $5B, + $4C, $5C, $4D, $5D ] + +const array SAM_WALK = [ $64, $74, $65, $75, + $66, $76, $67, $77, + $68, $78, $69, $79, + $6A, $7A, $6B, $7B, + $6C, $7C, $6D, $7D ] + + +// ============================================================================ +// CORE +// ============================================================================ +// quite important uwu + +void main() { + gamestate = STATETITLE + title_init() + while(true){} // all work is done in nmi + // thnx nmi <3 +} + +// run at each non-maskable interrupt +// generated during each vertical blanking interval. cool +void nmi() { + // push all sprite info to the ppu (picture processing unit) + // through dma (direct memory access) transfer + ppu_oam_dma_write( oam_buffer.addr.hi ) + main_game_logic() +} + +// run at each interrupt request +// ``an irq temporarily stops a program, running an interupt handler instead'' +// thanks wikipedia +// do I know when these are called on the nes? absolutely not +// at reset? maybe? +void irq() { + +} + +// pretty self-explanatory +inline void main_game_logic() { + // use a return dispatch here + // to use different logic for each screen/gamestate + return [gamestate] { + STATETITLE @ title_logic + STATEPLAYING @ ingame_logic + STATEGAMEOVER @ gameover_logic + } +} + +// uwu owo uwu owo uwu owo +inline void init_graphics() { + init_sprites() + load_palletes() +} + +// loads palletes onto the ppu +macro void load_palletes() { + byte i + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr( ppu_pallete_ram ) + + for i,0,until,$20 { + ppu_write_data( pallete[i] ) + } +} + +// cleans up oam (object attribute memory) +// each sprite gets 4 bytes in oam: +// 1: ypos, 2: tile index, 3: attr table, 4: xpos +void init_sprites() { + byte i + for i,0,to,255 { + if (i & %00000011) == 0 { + //each sprite takes up 4 bytes, and we want to edit + //the y position of each sprite (0th byte) + //so we use the %00000011 mask to write every 4th byte + //(every 0th sprite byte) + oam_buffer[i] = $ef // move the sprite off screen + } + else { + oam_buffer[i] = 0 + } + } +} + + +// ============================================================================ +// TITLE-SCREEN +// ============================================================================ +void title_init() { + byte i + //for now, turn off the screen and nmi + ppu_ctrl = 0 + ppu_mask = 0 + + //initialize the sprites and palletes + init_graphics() + + //write a full screen of background data + + load_sky_background() + + //write the title screen message + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$018B) // point the PPU to the message's start + for i,0,until,$0B { + ppu_write_data(title_msg[i]) + } + + //write the border + //top border + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$0020) + for i,0,until,$20 { + ppu_write_data($01) + } + //bottom border + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$0380) + for i,0,until,$20 { + ppu_write_data($01) + } + + //set ppu address increment to 32 so we can draw the left and right borders + //(allows us to draw to the nametable in vertical strips rather than horizontal) + ppu_ctrl = %00000100 + + //left border + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram) + for i,0,until,$20 { + ppu_write_data($01) + } + //right border + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$1F) + for i,0,until,$20 { + ppu_write_data($01) + } + + + framecounter = 0 + ppu_set_scroll(0,0) + ppu_wait_vblank() //wait for next vblank before re-enabling NMI + //so that we don't get messed up scroll registers + //re-enable the screen and nmi + ppu_ctrl = %10010000 // enable NMI, sprites from Pattern Table 0, background from Pattern Table 1 + ppu_mask = %00011110 // enable sprites, enable background, no clipping on left side +} + +void title_logic() { + read_joy1() + if input_start != 0 { + rand_seed = framecounter //seed the random number generator with the amount of frames + //that have passed since the title screen was shown + gamestate = STATEPLAYING + ingame_init() + return + } + framecounter += 1 +} + + +// ============================================================================ +// IN-GAME +// ============================================================================ +void ingame_init() { + //for now, turn off the screen and nmi + ppu_ctrl = 0 + ppu_mask = 0 + + //write a full screen of data + load_sky_background() + draw_score_text_background() + draw_boundaries_background() + load_ingame_attr_table() + + ppu_set_scroll(0,0) + ppu_wait_vblank() //wait for next vblank before re-enabling NMI + //so that we don't get messed up scroll registers + //re-enable the screen and nmi + ppu_ctrl = %10010000 // enable NMI, sprites from Pattern Table 0, background from Pattern Table 1 + ppu_mask = %00011110 // enable sprites, enable background, no clipping on left side +} + +void ingame_logic() { + draw_score() + //update scroll last because writes to vram also + //overwrite the scroll register + ppu_set_scroll(0,0) // tell the ppu there is no background scrolling + + ingame_input() + update_ingame_sprites() + if score1 >= 15 { + //Someone's reached 15 points, the game is over, + //so set the state to game over and reset the + //framecounter + gamestate = STATEGAMEOVER + + //move all the sprites off screen + //in preperation for the gameover screen + sam.top0.x = $ef + + gameover_init() + } +} + +void ingame_input ( ) { + // Player 1 controls + read_joy1() + sam.movement = 0 + + // up + if input_dy < 0 { + if (sam.top0.y > TOPWALL) { + sam.top0.y -= 2 + } + } + // down + else if input_dy > 0 { + if (sam.top0.y + SAM_HEIGHT) < BOTTOMWALL { + sam.top0.y += 2 + } + } + + // left + if input_dx < 0 { + sam.direction = 0 + sam.top0.x -= 2 + sam.movement = 1 + } + // right + else if input_dx > 0 { + sam.direction = 1 + sam.top0.x += 2 + sam.movement = 1 + } +} + +void update_ingame_sprites ( ) +{ + // player. sam is the player. + update_entity( pointer.Entity( sam.addr ), + sam.top0.x, sam.top0.y, SAM_IDLE, SAM_WALK) +} + +macro void load_ingame_attr_table() { + byte i + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_0_attr_ram) // point the PPU to nametable 0's attribute table + for i,0,until,$10 { + ppu_write_data(attribute[i]) + } +} + + +// ============================================================================ +// GAME-OVER +// ============================================================================ +void gameover_init() { + //for now, turn off nmi and sprites + ppu_ctrl = 0 + ppu_mask = 0 + + draw_score() //draw the final score + draw_gameover() //draw the game over message + + framecounter = 0 + ppu_set_scroll(0,0) // tell the ppu there is no background scrolling + ppu_wait_vblank() //wait for next vblank before re-enabling NMI + //so that we don't get messed up scroll registers + //re-enable the screen and nmi + ppu_ctrl = %10010000 // enable NMI, sprites from Pattern Table 0, background from Pattern Table 1 + ppu_mask = %00011110 // enable sprites, enable background, no clipping on left side +} + +void gameover_logic() { + if framecounter >= 240{ + //3 seconds have passed, + //reset the game + simulate_reset() + } + framecounter += 1 +} + +void draw_gameover() { + byte i + + //draw the static game over message + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$0107) // point the PPU to the message's start + for i,0,until,$12 { + ppu_write_data(gameover_msg[i]) + } + //draw the win message + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$01AC) // point the PPU to the message's start +} + + +// ============================================================================ +// MISC GRAPHICS +// ============================================================================ +// ------------------------------------- +// BACKGROUND +// ------------------------------------- +inline void load_sky_background() { + word xx + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram) // point the PPU to palette ram + for xx,0,until,$0060 { + ppu_write_data($92) // $00 = sky + } + for xx,0,until,$0300 { + ppu_write_data($00) // $00 = sky + } +} + +macro void draw_score_text_background() { + byte i + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$20) // point the PPU to score text's start + for i,0,until,$1C { + ppu_write_data(scorebackground[i]) + } +} + +macro void draw_boundaries_background() { + byte i + + //draw top boundary + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$40) // point the PPU to the top boundary's start + for i,0,until,$20 { + ppu_write_data($81) //write the top boundary tile + } + + //draw bottom boundary + read_ppu_status() // read PPU status to reset the high/low latch + ppu_set_addr(ppu_nametable_ram+$02C0) // point the PPU to the top boundary's start + for i,0,until,$20 { + ppu_write_data($80) //write the bottom boundary tile + } +} + +// ------------------------------------- +// SPRITES +// ------------------------------------- +// sets the current frame of ent's animation and determines which animation to use +void update_entity ( pointer.Entity ent, byte xx, byte yy, pointer idle, pointer walk ) { + if ( ent[0].direction == 0 ) { + ent[0].top0.attrs = SAM_ATTR_HFLIP + ent[0].bottom0.attrs = SAM_ATTR_HFLIP + ent[0].top1.attrs = SAM_ATTR_HFLIP + ent[0].bottom1.attrs = SAM_ATTR_HFLIP + + ent[0].top1.x = xx - 8 + ent[0].bottom1.x = xx - 8 + } + else { + ent[0].top0.attrs = SAM_ATTR + ent[0].bottom0.attrs = SAM_ATTR + ent[0].top1.attrs = SAM_ATTR + ent[0].bottom1.attrs = SAM_ATTR + + ent[0].top1.x = xx + 8 + ent[0].bottom1.x = xx + 8 + } + + ent[0].top0.x = xx + ent[0].bottom0.x = xx + + ent[0].top0.y = yy + ent[0].bottom0.y = yy + 8 + ent[0].top1.y = yy + ent[0].bottom1.y = yy + 8 + + byte ani + ani = 0 + if ( ent[0].frame < 4 ) { + ani = 0 + } else if ( ent[0].frame < 8 ) { + ani = 4 + } else if (ent[0].frame < 12 ) { + ani = 8 + } else if (ent[0].frame < 16 ) { + ani = 12 + } else if (ent[0].frame < 20 ) { + ani = 16 + } else { + ent[0].frame = 0 + ani = 0 + } + + if ( ent[0].movement == 1 ) { + ent[0].top0.tile = walk[ani + 0] + ent[0].bottom0.tile = walk[ani + 1] + ent[0].top1.tile = walk[ani + 2] + ent[0].bottom1.tile = walk[ani + 3] + } else { + ent[0].top0.tile = idle[ani + 0] + ent[0].bottom0.tile = idle[ani + 1] + ent[0].top1.tile = idle[ani + 2] + ent[0].bottom1.tile = idle[ani + 3] + } + + ent[0].frame += 1 +} + +// ------------------------------------- +// ETC +// ------------------------------------- +inline void draw_score() { + byte digit01 + byte digit10 + read_ppu_status() // read PPU status to reset the high/low latch + + //display player1's score + digit01 = score1 %% 10 //get the ones digit + digit10 = score1 / 10 //get the tens digit + digit10 %%= 10 + + ppu_set_addr(ppu_nametable_ram+$29) // point the PPU to player1's score number + if digit10 > 0 { + ppu_write_data(digit10 + '0') + } + ppu_write_data(digit01 + '0') +} + + +// ============================================================================ +// UTIL +// ============================================================================ + +inline asm void ppu_wait_vblank() { + vblankwait: + BIT $2002 + ! BPL vblankwait + ? RTS +} diff --git a/sbp.nes b/sbp.nes new file mode 100644 index 0000000000000000000000000000000000000000..cde35529aa023954d4cc740b474631c6fd5c96bb GIT binary patch literal 40976 zcmeH~e{36P8OL8=9NS48$8Gb&O^tI&rI8{g9Sp%qM6ILI%^TTPQCK^EY&UIn6Yx~4 zEy;B~Y&XrNXpl68KiH6>T2W4_Hwx{4(DeTZXs_?~dm^ z-^FKlRRMK?%Ja5&&->i>dEe)GKks|C^Yre$os5HQv<#WpMA)ZK40uuk`{-zf%ne0!$3r4ZNkm4zfB1cPguA)Shkcp&#)PGhGiF186r2&HqWh(Pq^9T zW~DhB%oV8`w$%2_2`aTK?O8UrPNn&op7Ox}W;bQH@Jvz!3$DpcixO(m~idI3a0zDAekogg<#FJx|8x&7g$gL?2 zS&~~dBqBqIAuFETUm>6&TbElEe!huZ`u%w1(z)@7U@ze6Sb6$g<&|uk-8L6DRdmlv*}U}TdCoR;(yvzmU0WcPsG1GX1zFW6SFtza9$HaT1PS&$RdecQu?%_0(=|AI1yNr9Iare=?(#o%T z(oI&U<&0U}`Q^{$#9?SKfp=W+e zdh57MQ558 z3VjFd*O&VY?KhPBb+q3M>-^Q2ImBff;<62K*@n325Lz%yoZfuK1Mi>QrnK#-%Qa@! zX4~E%%)lyq2pGL(5OVJtht+Mpa~-oCbJvV_u0!d_ZZ5xbKQmMT*6+zMa>IEyBe$M+ zGV=+EPG;bT zveH>>jYgZA5P2=~dgL92 zL=;CvVIoRhgxw_%%5{S>GblR-Wiohf@Xf)np!8(PR8!8SbZ3iGb+M9e9?A3V;T?`5 zv)pKPnyil3>IBWsVyE@Q>MVw=C!vojWaT3Y>DkfQ()0O_mL9IVrRS4igKe!XfnLM* zFnnMAU0Q$B%9p;n`)iLq`}n@d{wJP$^6Ovz#=tk9`qs-w4}3Q|d~jq?96I*xSHE*S zCcW~#lPURewx4~9-OWOKhJ6+~U)gi`z>y>D-lH!bKDy^K?B16S9X{|dl{dZ!v4aGV z01`j~NB{{S0VIF~kN^@u0!RP}AOR$R1dsp{Kmter2_OL^fCP{L5G$9Qx2f8OX5znHk9FN`j%udiRev@mKGzEPm1UorLC3UxC;Hv|4{eWjdWBe7LT z?SvnX{{?ee(^~sxS!>@dTU({|+_JSrI?VmIY^_de^4HFPNBn(zkW4mZ@#}# z^Zkvc%$Y5l{LPIQ`m^sQlb8PdVp2bwr29gQ|7Dx(-~a0Vq<;MQ#KD6H`}O1HeWM{p zBl_xxAJXyqpB5I9dMOD4v0wMfZ*{^RnJYZgw%z) zx(xe?ErwM+YLWV?8YmKuS9Mj@MXpypefsp<@D1YB9S+?|Ei|zIePH5EJ^mSsq@q(&;AnZ`h*^jb71uwd#HuhCkY$Y)!5p~^KrrD3kG|8 z;~<3I|FQNw=pk+GeXwkDB*06O#FNVRQY;p! zE+?kJzse)Omeblb+t1dk*ZJY)ica{LbgPiqc);`J?@a3fAE&L7FV52zw`8y}-j)u2 zm+%_>`mAakC)j<-?ihWcqWeO9VliKdkP6UjBX|+=+kD9<OM{8}Lxv@bv^Kn3=Eg+jvDSDBBNZY$sD3&Q+HKoAJ>!3~!Xn9DXD z0TDtfx>N|-_m|N1=WPS1uN5lsr2^Dns`yeNpSRbm-hu%(UCS5qc^fY+Z~l%lrvRF; z!peL!Tfvr3qWf>F#|O=xuTY{HE92ouTGjq67BrZ!qF<%H?LgA?+2@1R6-xO^Jk@X6 z>N9lPd{SPjss~OZw90roy?uW0V!ZABfc0vHMO(bYLrR+uqwVu)uzPl%mB{SHs%yr1 z5|>KzBI!%S)p%Tmj{%`52u0A7^nk;G8D3hFylOF?NREvB4d#yxN2AN|$s+Q3h zfu$Qv&zA@;jn_s-phiNjN23xg(5Mgo&@t)@fu0EDx-bMJJqxC|(!3Tx| zAIJ|Ms2V&kz0SQ{A|akH@_dK{IWEs}K@tp>uRtI`=tV^cAykAAlB7eS4b!coEdbe8 zbVB@)ZBZjoj3|b>7_d~W2OA2BYS{GC^x*^FV6pD5kSLmqrcm;#7z%;a7ov1i4LX77 Wf&uA)=Yb1E0!RP}Ab|&-z&`+7%4!1u literal 0 HcmV?d00001 diff --git a/tileset.chr b/tileset.chr new file mode 100644 index 0000000000000000000000000000000000000000..48bb95ef0aa423ac904d17733598c6e27a76b0e2 GIT binary patch literal 8192 zcmeHK&ub(_6n^8zO^~4pxVUp@dlvSRW21rvJH+H7-t^{r*=`meJtg=Lc-6y_2?G&v*wxE|5KN1ZtPnc0n}Z$bt@6E|>gk#evLIQN*e{uU^Xhx` z-m9;we*~biN`Xp&|EmI1uYB~uxs{;Bc$%g@fVlI?ov(iSbSl?FmMkBiL`RPN94A?T z8;Z{nmQ_TKHn}>}u zPxW0-MpXa3x7VXl`Ce~?3%zpuGxE#VXSqyG4dhtnUsVG+miYxWkYkyDTK#hTsrl;q zb=CFjF00HrhCl3HU;O^JFueQg`=MM9XF|F})J=HIZe$LTAVFB2v+Y-1y$ z)g@7c2;t&k(jyGlzd<}&m7ZrK%H4Me(OTRY%AS;*(-QpZGI33;{nD-&&+&)d1OUBkCAyfjo4;dIeaf+f*zj}X4{VLbKS|l zI|Xl8Eq@pl5j;lIW-uS?Yd$x46pJE0HCl#|7XEw?$HFN5G)fIUACJoRS6JkuYOz+2)BbDy7-)LEIHiike0)ip*q`lK zV8VjGN4|C-X?=P=TwR<-MLh8bT7Ma@<>L{b=m$?DS}~u}>-o`ZGwu4o^@?~~i{}hW zYCg`^^9kHNUC$!&RK78Fc5J($@^K@GL;UX7`Je+s&kMR;0g>Zy*WJN!JOJ7H{6TNv zgJobi#+!8v$9UpJcAoq}+Vi0mc!}pFcpGqrjx$6)qyvrzrZX7udNTBau-p9u^S3*$ zyN7okALZm=gJ;{e4+hZw$Gi^pdVMhPhh8_}0mZv*zrB}5Q#?geJV1>9cI+b#gm^$Y z(*}~h$p8(JHki(rk1oZFZWnt5*l}G>4QBn}KRQ`|7V=@2XxQnvS^ohZ0Bj$4&#x!Y zU;$(ECd}I0wqI-DIF%#)s2u4*<=DvgZNz=-kIvWj1CiZG_k&hI{chupj{3#;Odp(% z`e1#$p$Z&Nzpy_{!D4L4m<6V7N49N(X^uXPMg!&mj`~Jfda!$%67XXz(Ill>%p8fj