Index: compile ================================================================== --- compile +++ compile @@ -15,15 +15,14 @@ test instruc -nt instruc.h && node instruc.js > instruc.h test instruc.js -nt instruc.h && node instruc.js > instruc.h test names.js -nt names.h && node names.js > names.h test quarks -nt quarks.h && node quarks.js > quarks.h test quarks.js -nt quarks.h && node quarks.js > quarks.h -test heromesh.h -nt "$EXE" && rm bindings.o class.o picture.o function.o exec.o game.o edit.o picedit.o || true +test heromesh.h -nt "$EXE" && rm bindings.o class.o picture.o function.o exec.o game.o edit.o picedit.o sound.o || true test instruc.h -nt "$EXE" && rm class.o exec.o || true test pcfont.h -nt "$EXE" && rm picture.o || true test quarks.h -nt "$EXE" && rm bindings.o edit.o exec.o game.o picture.o picedit.o || true -test hash.h -nt "$EXE" && rm hash.o function.o || true echo '* smallxrm' test smallxrm.c -nt smallxrm.o && bash smallxrm.c echo '* bindings' test bindings.c -nt bindings.o && bash bindings.c echo '* class' @@ -38,10 +37,12 @@ test game.c -nt game.o && bash game.c echo '* edit' test edit.c -nt edit.o && bash edit.c echo '* picedit' test picedit.c -nt picedit.o && bash picedit.c +echo '* sound' +test sound.c -nt sound.o && bash sound.c echo '* hash' test hash.c -nt hash.o && bash hash.c echo '* main' bash main.c echo 'DONE' Index: config.doc ================================================================== --- config.doc +++ config.doc @@ -19,10 +19,31 @@ .altImage If the puzzle set contains multiple pictures with the same name and size then this resource controls which of those pictures is selected. The default setting is zero. +.audio.buffer + Buffer length for audio, as the number of samples. In order for audio to + work at all, this value must be set. + +.audio.mmlTempo + Define the number of quarter notes per minute. The default is 120. + +.audio.mmlTuning + The MML tuning of the A4 note, in Hz. The default is 440 Hz, but some + people do not like that tuning, so you can change it. + +.audio.mmlVolume + MML volume, from 0 to 1; fractions are allowed. The default value is 1. + +.audio.rate + Sample rate to use for audio, in hertz. In order for audio to work at + all, this value must be set. + +.audio.waveVolume + Wave volume, from 0 to 1; fractions are allowed. The default value is 1. + .autoSave If true, saves changes to the level and solution files when the program terminates. If false (default), they are only saved to the user cache database; to copy the changes to the level and solution files, you must use the .u command in SQL mode (-x), or use the -f switch. Index: heromesh.h ================================================================== --- heromesh.h +++ heromesh.h @@ -338,5 +338,13 @@ // == picedit == void run_picture_editor(void); +// == sound == + +void init_sound(void); +void set_sound_effect(Value v1,Value v2); +Uint16 find_user_sound(const char*name); +void set_sound_on(int on); +void sound_test(void); + Index: main.c ================================================================== --- main.c +++ main.c @@ -1,7 +1,7 @@ #if 0 -gcc ${CFLAGS:--s -O2} -o ${EXE:-~/bin/heromesh} -Wno-multichar main.c class.o picture.o bindings.o function.o exec.o game.o edit.o picedit.o smallxrm.o hash.o sqlite3.o `sdl-config --cflags --libs` -ldl -lpthread -lm +gcc ${CFLAGS:--s -O2} -o ${EXE:-~/bin/heromesh} -Wno-multichar main.c class.o picture.o bindings.o function.o exec.o game.o edit.o picedit.o sound.o smallxrm.o hash.o sqlite3.o `sdl-config --cflags --libs` -ldl -lpthread -lm exit #endif /* This program is part of Free Hero Mesh and is public domain. @@ -870,10 +870,21 @@ case SDLK_s: goto scrolltest; case SDLK_t: puts(screen_prompt("Testing screen_prompt()")?:"No output."); break; + case SDLK_u: + set_sound_effect(UVALUE(n,TY_USOUND),NVALUE(0)); + break; + case SDLK_w: + sound_test(); + SDL_FillRect(screen,0,0); + SDL_Flip(screen); + break; + case SDLK_z: + set_sound_effect(NVALUE(0),NVALUE(1)); + break; } break; case SDL_MOUSEBUTTONDOWN: draw_picture(ev.button.x,ev.button.y,n); SDL_Flip(screen); @@ -1105,15 +1116,17 @@ } if(main_options['z']) init_composite(); load_pictures(); if(main_options['T']) { printf("argv[0] = %s\n",argv[0]); + init_sound(); test_mode(); return 0; } if(!main_options['z']) init_usercache(); if(main_options['n']) return 0; + if(screen) init_sound(); load_classes(); load_key_bindings(); load_level_index(); optionquery[1]=Q_maxObjects; max_objects=strtoll(xrm_get_resource(resourcedb,optionquery,optionquery,2)?:"",0,0)?:0xFFFF0000L; Index: quarks ================================================================== --- quarks +++ quarks @@ -30,10 +30,12 @@ rate buffer mmlVolume waveVolume standardSounds +mmlTuning +mmlTempo ! Keyboard keyRepeat editKey gameKey Index: quarks.h ================================================================== --- quarks.h +++ quarks.h @@ -13,187 +13,189 @@ #define Q_rate 14 #define Q_buffer 15 #define Q_mmlVolume 16 #define Q_waveVolume 17 #define Q_standardSounds 18 -#define Q_keyRepeat 19 -#define Q_editKey 20 -#define Q_gameKey 21 -#define Q_backspace 22 -#define Q_tab 23 -#define Q_clear 24 -#define Q_return 25 -#define Q_pause 26 -#define Q_escape 27 -#define Q_space 28 -#define Q_exclaim 29 -#define Q_quotedbl 30 -#define Q_hash 31 -#define Q_dollar 32 -#define Q_ampersand 33 -#define Q_quote 34 -#define Q_leftparen 35 -#define Q_rightparen 36 -#define Q_asterisk 37 -#define Q_plus 38 -#define Q_comma 39 -#define Q_minus 40 -#define Q_period 41 -#define Q_slash 42 -#define Q_0 43 -#define Q_1 44 -#define Q_2 45 -#define Q_3 46 -#define Q_4 47 -#define Q_5 48 -#define Q_6 49 -#define Q_7 50 -#define Q_8 51 -#define Q_9 52 -#define Q_colon 53 -#define Q_semicolon 54 -#define Q_less 55 -#define Q_equals 56 -#define Q_greater 57 -#define Q_question 58 -#define Q_at 59 -#define Q_leftbracket 60 -#define Q_backslash 61 -#define Q_rightbracket 62 -#define Q_caret 63 -#define Q_underscore 64 -#define Q_backquote 65 -#define Q_A 66 -#define Q_B 67 -#define Q_C 68 -#define Q_D 69 -#define Q_E 70 -#define Q_F 71 -#define Q_G 72 -#define Q_H 73 -#define Q_I 74 -#define Q_J 75 -#define Q_K 76 -#define Q_L 77 -#define Q_M 78 -#define Q_N 79 -#define Q_O 80 -#define Q_P 81 -#define Q_Q 82 -#define Q_R 83 -#define Q_S 84 -#define Q_T 85 -#define Q_U 86 -#define Q_V 87 -#define Q_W 88 -#define Q_X 89 -#define Q_Y 90 -#define Q_Z 91 -#define Q_delete 92 -#define Q_kp0 93 -#define Q_kp1 94 -#define Q_kp2 95 -#define Q_kp3 96 -#define Q_kp4 97 -#define Q_kp5 98 -#define Q_kp6 99 -#define Q_kp7 100 -#define Q_kp8 101 -#define Q_kp9 102 -#define Q_kp_period 103 -#define Q_kp_divide 104 -#define Q_kp_multiply 105 -#define Q_kp_minus 106 -#define Q_kp_plus 107 -#define Q_kp_enter 108 -#define Q_kp_equals 109 -#define Q_up 110 -#define Q_down 111 -#define Q_right 112 -#define Q_left 113 -#define Q_insert 114 -#define Q_home 115 -#define Q_end 116 -#define Q_pageup 117 -#define Q_pagedown 118 -#define Q_f1 119 -#define Q_f2 120 -#define Q_f3 121 -#define Q_f4 122 -#define Q_f5 123 -#define Q_f6 124 -#define Q_f7 125 -#define Q_f8 126 -#define Q_f9 127 -#define Q_f10 128 -#define Q_f11 129 -#define Q_f12 130 -#define Q_f13 131 -#define Q_f14 132 -#define Q_f15 133 -#define Q_numlock 134 -#define Q_capslock 135 -#define Q_scrollock 136 -#define Q_rshift 137 -#define Q_lshift 138 -#define Q_rctrl 139 -#define Q_lctrl 140 -#define Q_ralt 141 -#define Q_lalt 142 -#define Q_rmeta 143 -#define Q_lmeta 144 -#define Q_lsuper 145 -#define Q_rsuper 146 -#define Q_mode 147 -#define Q_help 148 -#define Q_print 149 -#define Q_sysreq 150 -#define Q_break 151 -#define Q_menu 152 -#define Q_power 153 -#define Q_euro 154 -#define Q_compose 155 -#define Q_undo 156 -#define Q_shift 157 -#define Q_ctrl 158 -#define Q_alt 159 -#define Q_meta 160 -#define Q_numLock 161 -#define Q_editClick 162 -#define Q_gameClick 163 -#define Q_allowMouseWarp 164 -#define Q_middle 165 -#define Q_class 166 -#define Q_quiz 167 -#define Q_saveSolutions 168 -#define Q_solutionComment 169 -#define Q_solutionTimestamp 170 -#define Q_picedit 171 -#define Q_macro 172 -#define Q_sqlFile 173 -#define Q_sqlInit 174 -#define Q_sqlExtensions 175 -#define Q_sqlMemStatus 176 -#define Q_sqlSmallAllocations 177 -#define Q_sqlCoveringIndexScan 178 -#define Q_sqlPowerSafe 179 -#define Q_level 180 -#define Q_tracePrefix 181 -#define Q_stackProtection 182 -#define Q_maxObjects 183 -#define Q_traceAll 184 -#define Q_traceObject 185 -#define Q_showInventory 186 -#define Q_progress 187 -#define Q_autoSave 188 -#define Q_maxTrigger 189 -#define Q_pasteCommand 190 -#define Q_codepage 191 -#define Q_replaySpeed 192 -#define Q_autoWin 193 -#define Q_listMode 194 -#define Q_listColumns 195 +#define Q_mmlTuning 19 +#define Q_mmlTempo 20 +#define Q_keyRepeat 21 +#define Q_editKey 22 +#define Q_gameKey 23 +#define Q_backspace 24 +#define Q_tab 25 +#define Q_clear 26 +#define Q_return 27 +#define Q_pause 28 +#define Q_escape 29 +#define Q_space 30 +#define Q_exclaim 31 +#define Q_quotedbl 32 +#define Q_hash 33 +#define Q_dollar 34 +#define Q_ampersand 35 +#define Q_quote 36 +#define Q_leftparen 37 +#define Q_rightparen 38 +#define Q_asterisk 39 +#define Q_plus 40 +#define Q_comma 41 +#define Q_minus 42 +#define Q_period 43 +#define Q_slash 44 +#define Q_0 45 +#define Q_1 46 +#define Q_2 47 +#define Q_3 48 +#define Q_4 49 +#define Q_5 50 +#define Q_6 51 +#define Q_7 52 +#define Q_8 53 +#define Q_9 54 +#define Q_colon 55 +#define Q_semicolon 56 +#define Q_less 57 +#define Q_equals 58 +#define Q_greater 59 +#define Q_question 60 +#define Q_at 61 +#define Q_leftbracket 62 +#define Q_backslash 63 +#define Q_rightbracket 64 +#define Q_caret 65 +#define Q_underscore 66 +#define Q_backquote 67 +#define Q_A 68 +#define Q_B 69 +#define Q_C 70 +#define Q_D 71 +#define Q_E 72 +#define Q_F 73 +#define Q_G 74 +#define Q_H 75 +#define Q_I 76 +#define Q_J 77 +#define Q_K 78 +#define Q_L 79 +#define Q_M 80 +#define Q_N 81 +#define Q_O 82 +#define Q_P 83 +#define Q_Q 84 +#define Q_R 85 +#define Q_S 86 +#define Q_T 87 +#define Q_U 88 +#define Q_V 89 +#define Q_W 90 +#define Q_X 91 +#define Q_Y 92 +#define Q_Z 93 +#define Q_delete 94 +#define Q_kp0 95 +#define Q_kp1 96 +#define Q_kp2 97 +#define Q_kp3 98 +#define Q_kp4 99 +#define Q_kp5 100 +#define Q_kp6 101 +#define Q_kp7 102 +#define Q_kp8 103 +#define Q_kp9 104 +#define Q_kp_period 105 +#define Q_kp_divide 106 +#define Q_kp_multiply 107 +#define Q_kp_minus 108 +#define Q_kp_plus 109 +#define Q_kp_enter 110 +#define Q_kp_equals 111 +#define Q_up 112 +#define Q_down 113 +#define Q_right 114 +#define Q_left 115 +#define Q_insert 116 +#define Q_home 117 +#define Q_end 118 +#define Q_pageup 119 +#define Q_pagedown 120 +#define Q_f1 121 +#define Q_f2 122 +#define Q_f3 123 +#define Q_f4 124 +#define Q_f5 125 +#define Q_f6 126 +#define Q_f7 127 +#define Q_f8 128 +#define Q_f9 129 +#define Q_f10 130 +#define Q_f11 131 +#define Q_f12 132 +#define Q_f13 133 +#define Q_f14 134 +#define Q_f15 135 +#define Q_numlock 136 +#define Q_capslock 137 +#define Q_scrollock 138 +#define Q_rshift 139 +#define Q_lshift 140 +#define Q_rctrl 141 +#define Q_lctrl 142 +#define Q_ralt 143 +#define Q_lalt 144 +#define Q_rmeta 145 +#define Q_lmeta 146 +#define Q_lsuper 147 +#define Q_rsuper 148 +#define Q_mode 149 +#define Q_help 150 +#define Q_print 151 +#define Q_sysreq 152 +#define Q_break 153 +#define Q_menu 154 +#define Q_power 155 +#define Q_euro 156 +#define Q_compose 157 +#define Q_undo 158 +#define Q_shift 159 +#define Q_ctrl 160 +#define Q_alt 161 +#define Q_meta 162 +#define Q_numLock 163 +#define Q_editClick 164 +#define Q_gameClick 165 +#define Q_allowMouseWarp 166 +#define Q_middle 167 +#define Q_class 168 +#define Q_quiz 169 +#define Q_saveSolutions 170 +#define Q_solutionComment 171 +#define Q_solutionTimestamp 172 +#define Q_picedit 173 +#define Q_macro 174 +#define Q_sqlFile 175 +#define Q_sqlInit 176 +#define Q_sqlExtensions 177 +#define Q_sqlMemStatus 178 +#define Q_sqlSmallAllocations 179 +#define Q_sqlCoveringIndexScan 180 +#define Q_sqlPowerSafe 181 +#define Q_level 182 +#define Q_tracePrefix 183 +#define Q_stackProtection 184 +#define Q_maxObjects 185 +#define Q_traceAll 186 +#define Q_traceObject 187 +#define Q_showInventory 188 +#define Q_progress 189 +#define Q_autoSave 190 +#define Q_maxTrigger 191 +#define Q_pasteCommand 192 +#define Q_codepage 193 +#define Q_replaySpeed 194 +#define Q_autoWin 195 +#define Q_listMode 196 +#define Q_listColumns 197 static const char*const global_quarks[]={ "screenWidth", "screenHeight", "margin", "palette", @@ -208,10 +210,12 @@ "rate", "buffer", "mmlVolume", "waveVolume", "standardSounds", + "mmlTuning", + "mmlTempo", "keyRepeat", "editKey", "gameKey", "backspace", "tab", Index: sound.c ================================================================== --- sound.c +++ sound.c @@ -10,18 +10,19 @@ #include #include "sqlite3.h" #include "smallxrm.h" #include "quarks.h" #include "heromesh.h" +#include "cursorshapes.h" typedef struct { Uint8*data; Uint32 len; // length in bytes } WaveSound; static Uint8 sound_on; -static Sint16 mmlvolume=32767; +static Sint16 mmlvolume=9001; static SDL_AudioSpec spec; static WaveSound*standardsounds; static Uint16 nstandardsounds; static WaveSound*usersounds; static Uint16 nusersounds; @@ -28,30 +29,64 @@ static Uint8**user_sound_names; static FILE*l_fp; static long l_offset,l_size; static float wavevolume=1.0; static Uint8 needs_amplify=0; +static Uint32*mmltuning; +static Uint32 mmltempo; static Uint8*volatile wavesound; static volatile Uint32 wavelen; +static volatile Uint8 mmlsound[512]; +static volatile Uint16 mmlpos; +static volatile Uint32 mmltime; static void audio_callback(void*userdata,Uint8*stream,int len) { + static Uint32 phase; if(wavesound) { if(wavelen<=len) { memcpy(stream,wavesound,wavelen); memset(stream+wavelen,0,len-wavelen); wavesound=0; wavelen=0; } else { memcpy(stream,wavesound,len); wavesound+=len; - len-=len; + wavelen-=len; + } + } else if(mmlpos) { + Uint16*out=(Uint16*)stream; + Uint32 t=mmltime; + int re=len>>1; + int m=mmlpos; + int n=mmlsound[m-1]; + memset(stream,0,len); + while(re) { + if(n!=255) { + phase+=mmltuning[n]; + if(t>1) *out=phase&0x80000000U?-mmlvolume:mmlvolume; + } + if(!--t) { + m+=2; + if(m>=512) { + m=0; + break; + } + n=mmlsound[m-1]; + t=mmltempo*mmlsound[m]; + if(!t) { + m=0; + break; + } + } + out++; re--; } + mmlpos=m; + mmltime=t; } else { memset(stream,0,len); } - //TODO: MML sounds } static int my_seek(SDL_RWops*cxt,int o,int w) { switch(w) { case RW_SEEK_SET: fseek(l_fp,l_offset+o,SEEK_SET); break; @@ -105,11 +140,11 @@ if(!cvt.buf) goto fail; memcpy(cvt.buf,buf,len); if(SDL_ConvertAudio(&cvt)) goto fail; SDL_FreeWAV(buf); ws->data=cvt.buf; - ws->len=cvt.len; + ws->len=cvt.len_cvt; amplify_wave_sound(ws); return; fail: if(main_options['v']) fprintf(stderr,"[Failed to convert wave audio at %ld (%ld bytes)]\n",offset,size); SDL_FreeWAV(buf); @@ -118,61 +153,77 @@ static void load_sound_set(int is_user) { const char*v; char*nam; FILE*fp; Uint32 i,j; + WaveSound*ws; if(is_user) { if(main_options['z']) { fp=composite_slice(".xclass",0); - if(!fp) return; } else { nam=sqlite3_mprintf("%s.xclass",basefilename); if(!nam) return; fp=fopen(nam,"r"); sqlite3_free(nam); - if(!fp) return; } + if(!fp) return; } else { optionquery[2]=Q_standardSounds; v=xrm_get_resource(resourcedb,optionquery,optionquery,3); if(!v) return; fp=fopen(v,"r"); if(!fp) fatal("Cannot open standard sounds file (%m)\n"); nstandardsounds=50; - standardsounds=malloc(nstandardsounds*sizeof(WaveSoud)); + standardsounds=malloc(nstandardsounds*sizeof(WaveSound)); if(!standardsounds) fatal("Allocation failed\n"); for(i=0;i4 && nam[i-4]=='.' && nam[i-3]=='W' && nam[i-1]=='V' && (nam[i-2]=='A' || nam[i-2]=='Z')) { + j=nam[i-2]; nam[i-4]=0; if(is_user) { - + if(nusersounds>0x03FD) goto done; + i=nusersounds++; + usersounds=realloc(usersounds,nusersounds*sizeof(WaveSound)); + user_sound_names=realloc(user_sound_names,nusersounds*sizeof(Uint8*)); + if(!usersounds || !user_sound_names) fatal("Allocation failed\n"); + user_sound_names[i]=strdup(nam); + if(!user_sound_names[i]) fatal("Allocation failed\n"); + ws=usersounds+i; + ws->data=0; + ws->len=0; + } else { + //TODO: Implement standard sounds. + } + if(j=='A') { + load_sound(fp,l_offset,l_size,ws); } else { - + //TODO: Compressed sounds. } - } - fseek(fp,l_offset+l_size,SEEK_CUR); + fseek(fp,l_offset+l_size,SEEK_SET); } done: fclose(fp); free(nam); } void init_sound(void) { const char*v; + double f; + int i; optionquery[1]=Q_audio; optionquery[2]=Q_rate; v=xrm_get_resource(resourcedb,optionquery,optionquery,3); if(!v) return; spec.freq=strtol(v,0,10); @@ -199,24 +250,93 @@ wavevolume=strtod(v,0); } optionquery[2]=Q_mmlVolume; if(v=xrm_get_resource(resourcedb,optionquery,optionquery,3)) mmlvolume=fmin(strtod(v,0)*32767.0,32767.0); if(wavevolume>0.00001) { - load_sound_set(0); // Standard sounds + //load_sound_set(0); // Standard sounds load_sound_set(1); // User sounds } + if(mmlvolume) { + mmltuning=malloc(256*sizeof(Uint32)); + if(!mmltuning) fatal("Allocation failed\n"); + optionquery[2]=Q_mmlTuning; + if(v=xrm_get_resource(resourcedb,optionquery,optionquery,3)) f=strtod(v,0); else f=440.0; + f*=0x80000000U/(double)spec.freq; + for(i=0;i<256;i++) mmltuning[i]=f*pow(2.0,(i-96)/24.0); + optionquery[2]=Q_mmlTempo; + if(v=xrm_get_resource(resourcedb,optionquery,optionquery,3)) i=strtol(v,0,10); else i=120; + // Convert quarter notes per minute to samples per sixty-fourth note + mmltempo=(spec.freq*60)/(i*16); + } fprintf(stderr,"Done.\n"); wavesound=0; + mmlpos=0; SDL_PauseAudio(0); sound_on=1; } + +static void set_mml(const unsigned char*s) { + const Sint8 y[8]={2,0,4,-18,-14,-10,-8,-4}; + int m=0; + int o=96; + int t=2; + int n; + while(*s && m<512) { + switch(*s++) { + case '0' ... '8': o=24*(s[-1]-'0'); break; + case '-': if(o) o-=24; break; + case '+': o+=24; break; + case '.': t+=t/2; break; + case 'A' ... 'G': case 'a' ... 'g': + n=o+y[s[-1]&7]; + while(*s) switch(*s) { + case '!': n-=2; s++; break; + case '#': n+=2; s++; break; + case ',': n-=1; s++; break; + case '\'': n+=1; s++; break; + default: goto send; + } + goto send; + case 'H': case 'h': t=32; break; + case 'I': case 'i': t=8; break; + case 'L': case 'l': + t=0; + while(*s>='0' && *s<='9') t=10*t+*s++-'0'; + break; + case 'N': case 'n': + n=0; + while(*s>='0' && *s<='9') n=10*n+*s++-'0'; + send: + mmlsound[m++]=n; mmlsound[m++]=t; + break; + case 'Q': case 'q': t=16; break; + case 'S': case 's': t=4; break; + case 'T': case 't': t=2; break; + case 'W': case 'w': t=64; break; + case 'X': case 'x': mmlsound[m++]=255; mmlsound[m++]=t; break; + case 'Z': case 'z': t=1; break; + } + } + if(!m) return; + if(m<511) mmlsound[m+1]=0; + mmlpos=1; + mmltime=mmlsound[1]*mmltempo; +} void set_sound_effect(Value v1,Value v2) { + static const unsigned char*const builtin[4]={ + "s.g", + "scdefgab+c-bagfedc-c", + "i1c+c+c+c+c+c+c", + "-cc'c#d,dd'd#ee'ff'f#", + }; + const unsigned char*s; if(!sound_on) return; - if(!v2.t && !v2.u && wavesound) return; + if(!v2.t && !v2.u && (mmlpos || wavesound)) return; SDL_LockAudio(); wavesound=0; + mmlpos=0; switch(v1.t) { case TY_SOUND: if(v1.uw-16)/240?:1; + scrmax=(nitems+columns-1)/columns; + set_cursor(XC_arrow); + redraw: + SDL_FillRect(screen,0,0x02); + r.x=r.y=0; + r.w=screen->w; + r.h=8; + SDL_FillRect(screen,&r,0xF7); + SDL_LockSurface(screen); + draw_text(0,0," Mute Stop Status Cancel",0xF7,0xF0); + if(SDL_GetAudioStatus()==SDL_AUDIO_PLAYING) draw_text(0,0,"\x0E",0xF7,0xF1); + for(i=scroll*columns,x=0,y=8;iscreen->h-24) break; + if(ih-8; + scrollbar(&scroll,screen->h/8-1,scrmax,0,&r); + SDL_Flip(screen); + while(SDL_WaitEvent(&ev)) { + if(ev.type!=SDL_VIDEOEXPOSE && scrollbar(&scroll,screen->h/8-1,scrmax,&ev,&r)) goto redraw; + switch(ev.type) { + case SDL_MOUSEMOTION: + x=ev.button.x-16; y=ev.button.y-8; + if(x<0 || y<0) goto arrow; + i=x/240+columns*(y/24); x%=240; y%=24; + if(x<4 || x>236 || y<4 || y>20 || i<0 || i>=nitems) goto arrow; + set_cursor(XC_hand1); + break; + arrow: set_cursor(XC_arrow); + break; + case SDL_MOUSEBUTTONDOWN: + x=ev.button.x-16; y=ev.button.y-8; + if(x<0 || y<0) break; + i=x/240+columns*(y/24); x%=240; y%=24; + if(x<4 || x>236 || y<4 || y>20 || i<0 || i>=nitems) break; + if(i