├── .github └── workflows │ └── ci.yml ├── Readme.md ├── build.sh ├── example └── demomode.sh └── mbc.c /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | raspberry-build: 8 | runs-on: ubuntu-22.04 9 | env: 10 | BUILD_MODE: build 11 | TARGET_ARCH: armhf-linux 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | submodules: recursive 18 | fetch-depth: 0 19 | 20 | - name: Build 21 | run: | 22 | chmod u+x ./build.sh 23 | ./build.sh 24 | 25 | - name: Store artifact 26 | uses: actions/upload-artifact@v3 27 | with: 28 | name: "out-raspberry" #destination 29 | path: build/release/* #source 30 | 31 | raspberry-test: 32 | needs: [raspberry-build] 33 | runs-on: ubuntu-22.04 34 | env: 35 | BUILD_MODE: test 36 | TARGET_ARCH: armhf-linux 37 | 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v3 41 | with: 42 | submodules: recursive 43 | fetch-depth: 0 44 | 45 | - name: Download Raspberry artifact 46 | uses: actions/download-artifact@v3 47 | with: 48 | name: out-raspberry #source 49 | path: build/release #destination 50 | 51 | - name: Test 52 | uses: pguyot/arm-runner-action@v2 53 | with: 54 | commands: | 55 | chmod u+x ./build.sh 56 | ./build.sh 57 | 58 | ### Release 59 | 60 | release: 61 | if: github.event_name == 'push' 62 | needs: [raspberry-test] 63 | runs-on: ubuntu-22.04 64 | 65 | steps: 66 | 67 | - name: Download Raspberry artifact 68 | uses: actions/download-artifact@v3 69 | with: 70 | name: out-raspberry #source 71 | path: release #destination 72 | 73 | - name: Release 74 | uses: softprops/action-gh-release@v1 75 | with: 76 | body: Automatic Release (${{ github.event.head_commit.timestamp }}) 77 | tag_name: release.${{ github.sha }} 78 | files: release/* 79 | 80 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # MiSTer Batch control 3 | 4 | This is a simple command line utility to control the [MiSTer 5 | fpga](https://github.com/MiSTer-devel). Its main purpose is to load ROM files 6 | from the command line since this features is not supported by the MiSTer out of 7 | the box. 8 | 9 | This software is in the public domain. IT IS PROVIDED "AS IS", WITHOUT 10 | WARRANTY OF ANY KIND. 11 | 12 | # Installation 13 | 14 | A simple script to install this utility, among with others, is in 15 | [MiSTer_Misc](https://github.com/pocomane/MiSTer_misc/) repository. Alternatively 16 | you can find pre-compiled binaries in the [release 17 | page](https://github.com/pocomane/MiSTer_Batch_Control/releases/latest). 18 | 19 | If you make some changes to the code, you can use the `build.sh` script to 20 | download the [gcc/musl crosscompile toolchain](http://musl.cc), and to compile 21 | a static version of the application. However, also a simple command like: 22 | 23 | ``` 24 | arm-linux-gnueabihf-gcc -std=c99 -static -D_XOPEN_SOURCE=700 -o mbc mbc.c 25 | ``` 26 | 27 | should be able to compile the utility, since it does not have any dependency 28 | (except standard C library and linux interface). 29 | 30 | # Support 31 | 32 | Some functionalities need specific support for each system. To handle 33 | not-supported or non-standard cases, the `CUSTOM` system is provided. It let 34 | you, for example, to freely set the game directory, the startup delay, 35 | etc. It is configured with the environment variables specified in the 36 | `list_core` command section. If you make a system work in this way, please 37 | open a github issue specifying the value of the variables, so its support can 38 | be easly added. 39 | 40 | Please note that these informations could change from one release of the MiSTer 41 | to another, so please, make sure you are referring to the last release. If the 42 | utility stops to work after an update for some specific core, you can open an 43 | issue with the same information: probably I just did not update them yet. 44 | 45 | If you want to make the change by yourself (e.g. to support an old release of 46 | the MiSTer), a single line of code for each core must be added/changed in the 47 | definition of the `system_list` variable. It just contains the same 48 | informations described in `list_core`. 49 | 50 | # Usage 51 | 52 | Running the `mbc` without arguments will give minimal help. To actually perform 53 | some action on the MiSTer, `mbc` must be run like: 54 | 55 | ``` 56 | mbc COMMAND [ARG1 [ARG2 [...]]] 57 | ``` 58 | 59 | The main commands are `raw_seq` and `load_rom`, but other usefull ones are 60 | provided too. Please refer to the documentation of the single commands in this 61 | Readme. 62 | 63 | Some commands need specific support for each cores. A special system `CUSTOM` 64 | is provided to try to launch unsupported core. See documentation of the `list_core` 65 | for more details. 66 | 67 | 68 | ## Command raw_seq 69 | 70 | ``` 71 | mbc raw_seq KEY_SEQUENCE 72 | ``` 73 | 74 | This command will emulate the key press and release as described in the 75 | `KEY_SEQUENCE`. It is a sequence of the following codes with the following 76 | meanings: 77 | 78 | - U - press and release up arrow 79 | - D - press and release down arrow 80 | - L - press and release left arrow 81 | - R - press and release right arrow 82 | - O - press and release enter (Open) 83 | - E - press and release esc 84 | - H - press and release home 85 | - F - press and release end (Finish) 86 | - M - press and release F12 (Menu) 87 | - Lowercase letters (a-z) or digits (0-9) - press and release of the corresponding key 88 | - :XX - press and release of the key with the hex code XX 89 | - {XX - press of the key with the hex code XX 90 | - }XX - release of the key with the hex code XX 91 | - !s - wait for 1 second 92 | - !m - wait that some mount point changes (the first time it is called, it does 93 | not wait since the initial detected mount table is empty; so it must be called 94 | multiple time in the sequence, e.g. `EEMDD!mO!m`) 95 | 96 | The `XX` value is the hex representition of the coded in the 97 | [uapi/input-event-code](https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h) 98 | kernel header. 99 | 100 | For example, the commands 101 | 102 | ``` 103 | mbc raw_seq EEMDDO 104 | mbc raw_seq :01:01:58:6C:6C:1C 105 | ``` 106 | 107 | do the same thing: they send Esc, Esc, F12, down, dowm, enter, so they select 108 | the third item of the menu. 109 | 110 | The timing can be configured by means of the following environment variables: 111 | 112 | - `MBC_CORE_WAIT` sets the number of milliseconds to wait for the core to be loaded; 113 | the default is 3000. 114 | 115 | - `MBC_KEY_WAIT` sets the number of milliseconds to wait between each key-press; 116 | the default is 40. 117 | 118 | - `MBC_SEQUENCE_WAIT` sets the number of milliseconds to wait before and after a key 119 | sequence; the default is 1000. 120 | 121 | 122 | ## Command load_rom 123 | 124 | ``` 125 | mbc load_rom SYSTEM /PATH/TO/ROM 126 | ``` 127 | 128 | This will load the default core associated to the `SYSTEM`, then it will load 129 | the rom passed as argument. The supported systems can be retrieved with the 130 | `list_core` command. 131 | 132 | The games can be loaded from any directory, but the default one MUST exist 133 | (e.g. /media/fat/games/NES). It can be empty. 134 | 135 | 136 | ## Command list_core 137 | 138 | ``` 139 | mbc list_core 140 | ``` 141 | 142 | The `list_core` command lists the systems. The list will contain the `SYSTEM` id and 143 | the path of the default core. The special system `CUSTOM` can be by mean of the 144 | following environment variables: 145 | 146 | - `MBC_CUSTOM_CORE` is the fixed suffix of the path of the core file; the special 147 | value "!direct" can be used to load the rom directly, like in the ARCADE system; 148 | e.g. `MBC_CUSTOM_CORE=/media/fat/_Console/NES_` 149 | 150 | - `MBC_CUSTOM_FOLDER` is the name of the folders that are related to the system, 151 | e.g. `MBC_CUSTOM_FOLDER=NES` 152 | 153 | - `MBC_CUSTOM_ROM_EXT` is the extension of the rom files, e.g. 154 | `MBC_CUSTOM_ROM_EXT=nes` 155 | 156 | - `MBC_CUSTOM_DELAY` number of seconds to wait before load/mount the rom, more 157 | details at [MGL doc](https://mister-devel.github.io/MkDocs_MiSTer/advanced/mgl/#mgl-format); 158 | e.g. `MBC_CUSTOM_DELAY=2` 159 | 160 | - `MBC_CUSTOM_MODE` type and index for the MGL load, more details at [MGL doc](https://mister-devel.github.io/MkDocs_MiSTer/advanced/mgl/#mgl-format); 161 | e.g. `MBC_CUSTOM_MODE=f0` 162 | 163 | ## Command list_content 164 | 165 | ``` 166 | mbc list_content 167 | ``` 168 | 169 | It will list all the games in the default directory. Each game is preceded by 170 | the system name and a space. The system name is upper case and has no spaces. 171 | 172 | Please note that the directory is not recursivelly searched; linux has better 173 | tools for such purpose. This command is meant just to give a quick feedback 174 | of the `mbc` functionalities. 175 | 176 | 177 | ## Command load_all_as 178 | 179 | ``` 180 | mbc load_all_as SYSTEM /PATH/TO/CORE /PATH/TO/ROM 181 | ``` 182 | 183 | This is similar to `load_rom` but it will use the core provaided as argument 184 | instead of the default one. 185 | 186 | 187 | ## Command load_all 188 | 189 | ``` 190 | mbc load_all /PATH/TO/CORE /PATH/TO/ROM 191 | ``` 192 | 193 | This is similar to the `load_all_as` command, but it tries to match the system 194 | from the core file name. 195 | 196 | 197 | ## Command catch_input 198 | 199 | ``` 200 | mbc catch_input TIMEOUT_MS 201 | ``` 202 | 203 | Each time a button is pressed a `event catched` will be printed, plus an event 204 | counter. If no event was found within a certain time, `timeout` will be 205 | printed. The timeout time is specified in milliseconds and if it is negative, 206 | no timeout is set and `timeout` will be never printed. 207 | 208 | Please note that the `MiSTer` main app opens all the inputs in exclusive mode. 209 | This means that while it is running, `mbc` will not see any event. To use this 210 | command the `MiSTer` main app must be stopped (and then restarted after the exit of 211 | `mbc`). An example is in the `example/demomode.sh` script. 212 | 213 | ## Command wait_input 214 | 215 | ``` 216 | mbc wait_input TIMEOUT_MS 217 | ``` 218 | 219 | This is similar to `catch_input` command, but it exits after the first event or 220 | timeout. Same attention of `catch_input` command should be paid when operating 221 | with the `MiSTer` app. 222 | 223 | ## Command stream 224 | 225 | ``` 226 | mbc stream 227 | ``` 228 | 229 | It will open the standard input, and will execute each line as a single command. 230 | 231 | ## Command mgl_gen 232 | 233 | ``` 234 | mbc mgl_gen SYSTEM /PATH/TO/CORE /PATH/TO/ROM 235 | ``` 236 | 237 | This will generate an MGL able to run the CORE and load the ROM. The SYSTEM must 238 | be provided in order to generate a compatible MGL script. The output script will 239 | be printed on the standard output. 240 | 241 | ## Command mgl_gen_auto 242 | 243 | ``` 244 | mbc mgl_gen_auto /PATH/TO/CORE /PATH/TO/ROM 245 | ``` 246 | 247 | This is similar to the `mgl_gen` command, but it tries to match the system 248 | from the core file name. 249 | 250 | ## Command list_rom_for 251 | 252 | TODO : describe 253 | 254 | ## Command load_core 255 | 256 | TODO : describe 257 | 258 | ## Command load_core_as 259 | 260 | TODO : describe 261 | 262 | ## Command done 263 | 264 | TODO : describe 265 | 266 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -x 4 | 5 | if [ "$BUILD_MODE" = "test" ] ; then 6 | set -e 7 | chmod ugo+x ./build/release/mbc 8 | ./build/release/mbc 9 | exit 0 10 | fi 11 | 12 | die(){ 13 | echo "ERROR" 14 | exit -1 15 | } 16 | 17 | export DOWNLOAD_GCC_TOOLCHAIN="http://musl.cc/arm-linux-musleabihf-cross.tgz" 18 | export PATH="$PWD/build/arm-linux-musleabihf-cross/bin:$PATH" 19 | 20 | if [ "$(ls build/arm-linux-musle*)" = "" ] ; then 21 | echo "downloading $DOWNLOAD_GCC_TOOLCHAIN" 22 | mkdir -p build ||die 23 | cd build 24 | curl "$DOWNLOAD_GCC_TOOLCHAIN" --output cc_toolchain.tar.gz ||die 25 | tar -xzf cc_toolchain.tar.gz ||die 26 | cd .. 27 | fi 28 | 29 | BFLAG=" -std=c99 -Wall -D_XOPEN_SOURCE=700 -static -O2 " 30 | COMMIT=$(git rev-parse HEAD) 31 | BFLAG=" $BFLAG -DMBC_BUILD_COMMIT=\"$COMMIT\" " 32 | DATE=$(date --rfc-3339=seconds | tr ' ' '/') 33 | BFLAG=" $BFLAG -DMBC_BUILD_DATE=\"$DATE\" " 34 | 35 | echo "building..." 36 | cd build 37 | arm-linux-musleabihf-gcc $BFLAG -o mbc ../mbc.c ||die 38 | arm-linux-musleabihf-strip mbc ||die 39 | mkdir -p hook/expose ||die 40 | cd hook/expose ||die 41 | ln -s ../../mbc __unnamed__ ||die 42 | cd - 43 | tar -czf mbc.tar.gz mbc hook/expose/__unnamed__ 44 | mkdir release 45 | cp mbc release/ 46 | cp mbc.tar.gz release/ 47 | cd .. 48 | 49 | -------------------------------------------------------------------------------- /example/demomode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is a very simple example script of a MiSTer "Demo mode". 4 | # 5 | # It starts a random rom every 5 minutes. During the rom execution, the MiSTer 6 | # menu is disabled and no input are passed to the core. If you want to quit 7 | # the script and return to the normal MiSTer exection mode, press any gamepad 8 | # button 3 times within 5 seconds. The sceen will flash, but the current core 9 | # and rom will be kept, so you can play the last game. 10 | # 11 | # This script is ment to be run in a ssh connection, not by the Script menu. If 12 | # you want launch it from such menu, a wrapper should be added in the Script 13 | # folder, e.g.: 14 | # #!/bin/bash 15 | # /path/to/this/script.sh &; disown 16 | 17 | MBC="./mbc_mnt" 18 | 19 | start_mister(){ 20 | cd /media/fat 21 | /media/fat/MiSTer & 22 | disown 23 | cd - 24 | sleep 5 25 | } 26 | 27 | stop_mister(){ 28 | sleep 1 29 | killall MiSTer 30 | } 31 | 32 | main(){ 33 | 34 | stop_mister 35 | 36 | PREVIOUS_TIME=0 37 | EVENT_COUNT=0 38 | CHANGE_ROM="true" 39 | QUIT="false" 40 | while true ; do 41 | 42 | if [ "$CHANGE_ROM" = "true" ]; then 43 | RANDOM_CMD=$($MBC list_content | shuf -n 1) 44 | RANDOM_SYS=$(echo "$RANDOM_CMD" | sed 's: .*$::') 45 | RANDOM_ROM=$(echo "$RANDOM_CMD" | sed 's:^[^ ]* ::') 46 | start_mister 47 | $MBC load_rom "$RANDOM_SYS" "$RANDOM_ROM" 48 | stop_mister 49 | fi 50 | 51 | EVENT_FOUND=$($MBC wait_input 300000) 52 | CURRENT_TIME=$(date -u +%s) 53 | 54 | if [ "$EVENT_FOUND" = "timeout" ]; then 55 | CHANGE_ROM="true" 56 | EVENT_COUNT=1 57 | else 58 | # Check 3 keypress in 5 seconds. 59 | # Note: 3 keypresses + 3 keyreleases = 6 events 60 | CHANGE_ROM="false" 61 | if [ $((CURRENT_TIME - PREVIOUS_TIME)) -gt 5 ]; then 62 | EVENT_COUNT=1 63 | else 64 | EVENT_COUNT=$((EVENT_COUNT + 1)) 65 | fi 66 | PREVIOUS_TIME=$CURRENT_TIME 67 | if [ $EVENT_COUNT -gt 6 ]; then 68 | QUIT="true" 69 | fi 70 | fi 71 | 72 | if [ "$QUIT" = "true" ]; then 73 | start_mister 74 | exit 0 75 | fi 76 | done 77 | } 78 | 79 | ###### 80 | main 81 | 82 | -------------------------------------------------------------------------------- /mbc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #define MBC_BUILD_REVISION 34 20 | 21 | #define DEVICE_NAME "Fake device" 22 | #define DEVICE_PATH "/dev/uinput" 23 | #define MISTER_COMMAND_DEVICE "/dev/MiSTer_cmd" 24 | #define MGL_PATH "/run/mbc.mgl" 25 | #define CORE_EXT "rbf" 26 | 27 | #define HAS_PREFIX(P, N) (!strncmp(P, N, sizeof(P)-1)) // P must be a string literal 28 | 29 | #define ARRSIZ(A) (sizeof(A)/sizeof(A[0])) 30 | #define LOG(F,...) printf("%d - " F, __LINE__, __VA_ARGS__ ) 31 | #define PRINTERR(F,...) LOG("error - %s - " F, strerror(errno ? errno : EPERM), __VA_ARGS__ ) 32 | #define SBSEARCH(T, SA, C) (bsearch(T, SA, sizeof(SA)/sizeof(SA[0]), sizeof(SA[0]), (C))) 33 | 34 | #ifdef FAKE_OS_OPERATION 35 | #define nanosleep(m,n) (LOG("nanosleep %ld %ld\n", (m)->tv_sec, (m)->tv_nsec), 0) 36 | #define open(p,b) (LOG("open '%s'\n", p), fake_fd(p)) 37 | #define ioctl(f,...) (0) 38 | #define write(f,d,s) (LOG("write '%s' <-",f),hex_write(d,s),printf("\n"), 0) 39 | #define close(f) (LOG("close '%s'\n",fake_fd_name(f)), 0) 40 | #define mkdir(p,m) (LOG("mkdir '%s'\n",p), 0) 41 | #define poll(p,n,m) (LOG("%s\n","poll"), 0) 42 | #define read(f,d,s) (LOG("read '%s'\n",fake_fd_name(f)), 0) 43 | #define inotify_init() (LOG("%s\n","inotifyinit"), 0) 44 | #define inotify_add_watch(f,d,o) (LOG("inotifyadd '%s'\n",fake_fd_name(f)), 0) 45 | //#define opendir(p) (LOG("opendir '%s'\n",p), fake_fd(p)) 46 | //#define readdir(f) (LOG("readdir '%s'\n",fake_fd_name(f)), 0) 47 | #define closedir(f) (LOG("closedir '%s'\n",fake_fd_name(f)), 0) 48 | #define inotify_rm_watch(f,w) (LOG("inotifyrm '%s'\n",fake_fd_name(f)), 0) 49 | #define fopen(p,o) (LOG("fopen '%s'\n",p), fake_file(p)) 50 | #define fread(d,s,n,o) (LOG("fread '%s'\n",fake_file_name(o)), 0) 51 | #define fwrite(d,s,n,o) (LOG("fwrite '%s'\n",fake_file_name(o)), 0) 52 | #define fprintf(f,m,d) (LOG("fprintf '%s' <- '%s'\n",fake_file_name(f),m), 0) 53 | #define fclose(f) (LOG("fclose '%s'\n",fake_file_name(f)), 0) 54 | #define mount(s,t,x,o, y) (LOG("mount '%s' -> '%s'\n",s,t), 0) 55 | #define umount(p) (LOG("umount '%s'\n",p), 0) 56 | #define umount2(p,...) (LOG("umount2 '%s'\n",p), 0) 57 | #define remove(p) (LOG("remove '%s'\n",p), 0) 58 | #define rmdir(p) (LOG("rmdir '%s'\n",p), 0) 59 | //#define stat(p,s) (LOG("stat '%s'\n",p), stat(p,s)) 60 | //#define getenv(k) (LOG("getenv '%s'\n",k),getenv(k)) 61 | #undef S_ISREG 62 | #define S_ISREG(s) (1) 63 | static int fake_fd(const char* name) { return (int)(char*)strdup(name); } 64 | static char* fake_fd_name(int fd) { return (char*)fd; } 65 | static FILE* fake_file(const char* name) { return (FILE*)(char*)strdup(name); } 66 | static char* fake_file_name(FILE* f) { return (char*)f; } 67 | static void hex_write(const char*b,int s){ for(int i=0;ipool, monitor->file_n, ms); 179 | if (0> result) return -4; // Poll error 180 | return result; 181 | } 182 | 183 | int user_input_clear(input_monitor*monitor){ 184 | struct input_event ev; 185 | int count = 0; 186 | while (0< user_input_poll(monitor, 1)) 187 | for (int f = 0; f < monitor->file_n; f += 1) 188 | if (monitor->pool[f].revents & POLLIN) 189 | if (0< read(monitor->pool[f].fd, &ev, sizeof(ev))) 190 | count += 1; 191 | return count; 192 | } 193 | 194 | int user_input_open(input_monitor*monitor){ 195 | const char folder[] = "/dev/input"; 196 | 197 | monitor->watcher = -1; 198 | monitor->file_n = 0; 199 | 200 | int inotify_fd = inotify_init(); 201 | if (inotify_fd == -1) return -1; // Inotify error 202 | 203 | monitor->pool[monitor->file_n].fd = inotify_fd; 204 | monitor->pool[monitor->file_n].events = POLLIN; 205 | monitor->file_n += 1; 206 | 207 | monitor->watcher = inotify_add_watch( inotify_fd, folder, IN_CREATE | IN_DELETE ); 208 | if (monitor->watcher == -1) return -2; // Watcher error 209 | 210 | struct dirent* dir = NULL; 211 | DIR* dirinfo = opendir(folder); 212 | if (dirinfo != NULL) { 213 | while (0 != (dir = readdir (dirinfo))){ 214 | if (HAS_PREFIX("event", dir->d_name)){ 215 | 216 | char out[sizeof(folder)+strlen(dir->d_name)+9]; 217 | snprintf(out, sizeof(out)-1, "%s/%s", folder,dir->d_name); 218 | struct stat fileinfo; 219 | if (stat(out, &fileinfo) == 0 && !S_ISDIR(fileinfo.st_mode) && !S_ISREG(fileinfo.st_mode)) { 220 | 221 | if (monitor->file_n >= sizeof(monitor->pool)/sizeof(*(monitor->pool))) break; 222 | int fd = open(out,O_RDONLY|O_NONBLOCK); 223 | if (fd <= 0) return -3; // Open error 224 | 225 | monitor->pool[monitor->file_n].fd = fd; 226 | monitor->pool[monitor->file_n].events = POLLIN; 227 | monitor->file_n += 1; 228 | } 229 | } 230 | } 231 | closedir(dirinfo); 232 | } 233 | 234 | while(0< user_input_clear(monitor)); // drop old events 235 | 236 | return 0; 237 | } 238 | 239 | int is_user_input_event(int code){ return (code>0); } 240 | int is_user_input_timeout(int code){ return (code==0); } 241 | 242 | void user_input_close(input_monitor*monitor){ 243 | 244 | if (monitor->watcher >= 0) 245 | inotify_rm_watch(monitor->pool[0].fd, monitor->watcher); 246 | 247 | for (int f = 0; f < monitor->file_n; f += 1) 248 | if (monitor->pool[f].fd > 0) close(monitor->pool[f].fd); 249 | monitor->file_n = 0; 250 | } 251 | 252 | static size_t updatehash( size_t hash, char c){ 253 | return hash ^( c + (hash<<5) + (hash>>2)); 254 | } 255 | 256 | size_t contenthash( const char* path){ 257 | FILE *mnt = fopen( path, "r"); 258 | if( !mnt) return 0; 259 | int c; 260 | size_t hash = 0; 261 | while( EOF != ( c = fgetc( mnt))) 262 | hash = updatehash( hash, (char) c); 263 | fclose(mnt); 264 | if( 0 == hash) hash = 1; // 0 is considered invalid hash, so it can be used as error or initialization value 265 | return hash; 266 | } 267 | 268 | typedef struct { 269 | char *id; // This must match the filename before the last _ . Otherwise it can be given explicitly at the command line. It must be UPPERCASE without any space. 270 | char *core; // Path prefix to the core; searched in the internal DB 271 | char *fsid; // Name used in the filesystem to identify the folders of the system; if it starts with something different than '/', it will identify a subfolder of a default path 272 | char *romext; // Valid extension for rom filename; searched in the internal DB 273 | char *loadmode;// Load mode for the MGL, more detail a thttps://mister-devel.github.io/MkDocs_MiSTer/advanced/mgl/#mgl-format 274 | char *delay; // Delay to be used during the MGL load, more detail at https://mister-devel.github.io/MkDocs_MiSTer/advanced/mgl/#mgl-format 275 | } system_t; 276 | 277 | #define ROM_IS_THE_CORE "!direct" 278 | 279 | static system_t system_list[] = { 280 | // The first field can not contain '\0'. 281 | // The array must be lexicographically sorted wrt the first field (e.g. 282 | // :sort vim command, but mind '!' and escaped chars at end of similar names). 283 | 284 | { "ALICEMC10", "/media/fat/_Computer/AliceMC10_", "AliceMC10", "c10", "f0", "1", }, 285 | { "AMIGA.ADF", "/media/fat/_Computer/Minimig_", "Amiga", "adf", "f0", "1", }, 286 | { "AMIGA.HDF", "Amiga", "Amiga", "hdf", "f0", "1", }, 287 | { "AMSTRAD", "/media/fat/_Computer/Amstrad_", "Amstrad", "dsk", "f0", "1", }, 288 | { "AMSTRAD-PCW", "/media/fat/_Computer/Amstrad-PCW_", "Amstrad PCW", "dsk", "f0", "1", }, 289 | { "AMSTRAD-PCW.B", "/media/fat/_Computer/Amstrad-PCW_", "Amstrad PCW", "dsk", "f0", "1", }, 290 | { "AMSTRAD.B", "/media/fat/_Computer/Amstrad_", "Amstrad", "dsk", "f0", "1", }, 291 | { "AMSTRAD.TAP", "/media/fat/_Computer/Amstrad_", "Amstrad", "cdt", "f0", "1", }, 292 | { "AO486", "/media/fat/_Computer/ao486_", "AO486", "img", "f0", "1", }, 293 | { "AO486.B", "/media/fat/_Computer/ao486_", "AO486", "img", "f0", "1", }, 294 | { "AO486.C", "/media/fat/_Computer/ao486_", "AO486", "vhd", "f0", "1", }, 295 | { "AO486.D", "/media/fat/_Computer/ao486_", "AO486", "vhd", "f0", "1", }, 296 | { "APOGEE", "/media/fat/_Computer/Apogee_", "APOGEE", "rka", "f0", "1", }, 297 | { "APPLE-I", "/media/fat/_Computer/Apple-I_", "Apple-I", "txt", "f0", "1", }, 298 | { "APPLE-II", "/media/fat/_Computer/Apple-II_", "Apple-II", "dsk", "f0", "1", }, 299 | { "AQUARIUS.BIN", "/media/fat/_Computer/Aquarius_", "AQUARIUS", "bin", "f0", "1", }, 300 | { "AQUARIUS.CAQ", "/media/fat/_Computer/Aquarius_", "AQUARIUS", "caq", "f0", "1", }, 301 | { "ARCADE", ROM_IS_THE_CORE, "/media/fat/_Arcade", "mra", NULL, "0", }, 302 | { "ARCADIA", "/media/fat/_Console/Arcadia_", "Arcadia", "bin", "f0", "2", }, 303 | { "ARCHIE.D1", "/media/fat/_Computer/Archie_", "ARCHIE", "vhd", "f0", "1", }, 304 | { "ARCHIE.F0", "/media/fat/_Computer/Archie_", "ARCHIE", "img", "f0", "1", }, 305 | { "ARCHIE.F1", "/media/fat/_Computer/Archie_", "ARCHIE", "img", "f0", "1", }, 306 | { "ASTROCADE", "/media/fat/_Console/Astrocade_", "Astrocade", "bin", "f0", "1", }, 307 | { "ATARI2600", "/media/fat/_Console/Atari2600_", "ATARI2600", "rom", "f0", "1", }, 308 | { "ASTROCADE", "/media/fat/_Console/Astrocade_", "Astrocade", "rom", "f0", "1", }, 309 | { "ATARI5200", "/media/fat/_Console/Atari5200_", "ATARI5200", "rom", "f1", "1", }, 310 | { "ATARI7800", "/media/fat/_Console/Atari7800_", "ATARI7800", "a78", "f1", "1", }, 311 | { "ATARI800.CART", "/media/fat/_Computer/Atari800_", "ATARI800", "car", "f0", "1", }, 312 | { "ATARI800.D1", "/media/fat/_Computer/Atari800_", "ATARI800", "atr", "f0", "1", }, 313 | { "ATARI800.D2", "/media/fat/_Computer/Atari800_", "ATARI800", "atr", "f0", "1", }, 314 | { "ATARILYNX", "/media/fat/_Console/AtariLynx_", "AtariLynx", "lnx", "f1", "1", }, 315 | { "BBCMICRO", "/media/fat/_Computer/BBCMicro_", "BBCMicro", "vhd", "f0", "1", }, 316 | { "BK0011M", "/media/fat/_Computer/BK0011M_", "BK0011M", "bin", "f0", "1", }, 317 | { "BK0011M.A", "/media/fat/_Computer/BK0011M_", "BK0011M", "dsk", "f0", "1", }, 318 | { "BK0011M.B", "/media/fat/_Computer/BK0011M_", "BK0011M", "dsk", "f0", "1", }, 319 | { "BK0011M.HD", "/media/fat/_Computer/BK0011M_", "BK0011M", "vhd", "f0", "1", }, 320 | { "C16.CART", "/media/fat/_Computer/C16_", "C16", "bin", "f0", "1", }, 321 | { "C16.DISK", "/media/fat/_Computer/C16_", "C16", "d64", "f0", "1", }, 322 | { "C16.TAPE", "/media/fat/_Computer/C16_", "C16", "tap", "f0", "1", }, 323 | { "C64.CART", "/media/fat/_Computer/C64_", "C65", "crt", "f1", "1", }, 324 | { "C64.DISK", "/media/fat/_Computer/C64_", "C64", "rom", "f1", "1", }, 325 | { "C64.PRG", "/media/fat/_Computer/C64_", "C64", "prg", "f1", "1", }, 326 | { "C64.TAPE", "/media/fat/_Computer/C64_", "C64", "rom", "f1", "1", }, 327 | { "CHANNELF", "/media/fat/_Console/ChannelF_", "ChannelF", "bin", "f0", "2", }, 328 | { "COCO_2", "/media/fat/_Computer/CoCo2_", "CoCo2", "rom", "f0", "1", }, 329 | { "COCO_2.CAS", "/media/fat/_Computer/CoCo2_", "CoCo2", "cas", "f0", "1", }, 330 | { "COCO_2.CCC", "/media/fat/_Computer/CoCo2_", "CoCo2", "ccc", "f0", "1", }, 331 | { "COLECO", "/media/fat/_Console/ColecoVision_", "Coleco", "col", "f0", "1", }, 332 | { "COLECO.SG", "/media/fat/_Console/ColecoVision_", "Coleco", "sg", "f0", "1", }, 333 | { "CUSTOM", "/media/fat/_Console/NES_", "NES", "nes", "f0", "1", }, 334 | { "EDSAC", "/media/fat/_Computer/EDSAC_", "EDSAC", "tap", "f0", "1", }, 335 | { "GALAKSIJA", "/media/fat/_Computer/Galaksija_", "Galaksija", "tap", "f0", "1", }, 336 | { "GAMEBOY", "/media/fat/_Console/Gameboy_", "GameBoy", "gb", "f0", "2", }, 337 | { "GAMEBOY.COL", "/media/fat/_Console/Gameboy_", "GameBoy", "gbc", "f0", "2", }, 338 | { "GBA", "/media/fat/_Console/GBA_", "GBA", "gba", "f0", "2", }, 339 | { "GENESIS", "/media/fat/_Console/Genesis_", "Genesis", "gen", "f0", "1", }, 340 | { "INTELLIVISION", "/media/fat/_Console/Intellivision_", "Intellivision","bin", "f0", "1", }, 341 | { "JUPITER", "/media/fat/_Computer/Jupiter_", "Jupiter_", "ace", "f0", "1", }, 342 | { "LASER310", "/media/fat/_Computer/Laser310_", "Laser310_", "vz", "f0", "1", }, 343 | { "MACPLUS.2", "/media/fat/_Computer/MacPlus_", "MACPLUS", "dsk", "f0", "1", }, 344 | { "MACPLUS.VHD", "/media/fat/_Computer/MacPlus_", "MACPLUS", "dsk", "f0", "1", }, 345 | { "MEGACD", "/media/fat/_Console/MegaCD_", "MegaCD", "chd", "s0", "1", }, 346 | { "MEGACD.CUE", "/media/fat/_Console/MegaCD_", "MegaCD", "cue", "s0", "1", }, 347 | { "MEGADRIVE", "/media/fat/_Console/Genesis_", "Genesis", "md", "f0", "1", }, 348 | { "MEGADRIVE.BIN", "/media/fat/_Console/Genesis_", "Genesis", "bin", "f0", "1", }, 349 | { "MSX", "/media/fat/_Computer/MSX_", "MSX", "vhd", "f0", "1", }, 350 | { "NEOGEO", "/media/fat/_Console/NeoGeo_", "NeoGeo", "neo", "f1", "1", }, 351 | { "NES", "/media/fat/_Console/NES_", "NES", "nes", "f0", "2", }, 352 | { "NES.FDS", "/media/fat/_Console/NES_", "NES", "fds", "f0", "2", }, 353 | { "ODYSSEY2", "/media/fat/_Console/Odyssey2_", "ODYSSEY2", "bin", "f0", "1", }, 354 | { "ORAO", "/media/fat/_Computer/ORAO_", "ORAO", "tap", "f0", "1", }, 355 | { "ORIC", "/media/fat/_Computer/Oric_", "Oric_", "dsk", "f0", "1", }, 356 | { "PDP1", "/media/fat/_Computer/PDP1_", "PDP1", "bin", "f0", "1", }, 357 | { "PET2001", "/media/fat/_Computer/PET2001_", "PET2001", "prg", "f0", "1", }, 358 | { "PET2001.TAP", "/media/fat/_Computer/PET2001_", "PET2001", "tap", "f0", "1", }, 359 | { "PSX", "/media/fat/_Console/PSX_", "PSX", "cue", "s1", "1", }, 360 | { "QL", "/media/fat/_Computer/QL_", "QL_", "mdv", "f0", "1", }, 361 | { "S32X", "/media/fat/_Console/S32X_", "S32X", "32x", "f0", "1", }, 362 | { "SAMCOUPE.1", "/media/fat/_Computer/SAMCoupe_", "SAMCOUPE", "img", "f0", "1", }, 363 | { "SAMCOUPE.2", "/media/fat/_Computer/SAMCoupe_", "SAMCOUPE", "img", "f0", "1", }, 364 | { "SATURN", "/media/fat/_Console/Saturn", "Saturn", "cue", "s0", "1", }, 365 | { "SMS", "/media/fat/_Console/SMS_", "SMS", "sms", "f1", "1", }, 366 | { "SMS.GG", "/media/fat/_Console/SMS_", "SMS", "gg", "f2", "1", }, 367 | { "SNES", "/media/fat/_Console/SNES_", "SNES", "sfc", "f0", "2", }, 368 | { "SPECIALIST", "/media/fat/_Computer/Specialist_", "Specialist_", "rsk", "f0", "1", }, 369 | { "SPECIALIST.ODI", "/media/fat/_Computer/Specialist_", "Specialist_", "odi", "f0", "1", }, 370 | { "SPECTRUM", "/media/fat/_Computer/ZX-Spectrum_", "Spectrum", "tap", "f0", "1", }, 371 | { "SPECTRUM.DSK", "/media/fat/_Computer/ZX-Spectrum_", "Spectrum", "dsk", "f0", "1", }, 372 | { "SPECTRUM.SNAP", "/media/fat/_Computer/ZX-Spectrum_", "Spectrum", "z80", "f0", "1", }, 373 | { "SUPERGRAFX", "/media/fat/_Console/TurboGrafx16_", "TGFX16", "sgx", "f0", "1", }, 374 | { "TGFX16", "/media/fat/_Console/TurboGrafx16_", "TGFX16", "pce", "f0", "1", }, 375 | { "TGFX16-CD", "/media/fat/_Console/TurboGrafx16_", "TGFX16-CD", "chd", "s0", "1", }, 376 | { "TGFX16-CD.CUE", "/media/fat/_Console/TurboGrafx16_", "TGFX16-CD", "cue", "s0", "1", }, 377 | { "TI-99_4A", "/media/fat/_Computer/Ti994a_", "TI-99_4A", "bin", "f0", "1", }, 378 | { "TI-99_4A.D", "/media/fat/_Computer/Ti994a_", "TI-99_4A", "bin", "f0", "1", }, 379 | { "TI-99_4A.G", "/media/fat/_Computer/Ti994a_", "TI-99_4A", "bin", "f0", "1", }, 380 | { "TRS-80", "/media/fat/_Computer/TRS-80_", "TRS-80_", "dsk", "f0", "1", }, 381 | { "TRS-80.1", "/media/fat/_Computer/TRS-80_", "TRS-80_", "dsk", "f0", "1", }, 382 | { "TSCONF", "/media/fat/_Computer/TSConf_", "TSConf_", "vhd", "f0", "1", }, 383 | { "VC4000", "/media/fat/_Console/VC4000_", "VC4000", "bin", "f0", "1", }, 384 | { "VECTOR06", "/media/fat/_Computer/Vector-06C_""/core", "VECTOR06", "rom", "f0", "1", }, 385 | { "VECTOR06.A", "/media/fat/_Computer/Vector-06C_""/core", "VECTOR06", "fdd", "f0", "1", }, 386 | { "VECTOR06.B", "/media/fat/_Computer/Vector-06C_""/core", "VECTOR06", "fdd", "f0", "1", }, 387 | { "VECTREX", "/media/fat/_Console/Vectrex_", "VECTREX", "vec", "f0", "1", }, 388 | { "VECTREX.OVR", "/media/fat/_Console/Vectrex_", "VECTREX", "ovr", "f0", "1", }, 389 | { "VIC20", "/media/fat/_Computer/VIC20_", "VIC20", "prg", "f0", "1", }, 390 | { "VIC20.CART", "/media/fat/_Computer/VIC20_", "VIC20", "crt", "f0", "1", }, 391 | { "VIC20.CT", "/media/fat/_Computer/VIC20_", "VIC20", "ct", "f0", "1", }, 392 | { "VIC20.DISK", "/media/fat/_Computer/VIC20_", "VIC20", "d64", "f0", "1", }, 393 | { "WONDERSWAN", "/media/fat/_Console/WonderSwan_", "WonderSwan", "ws", "f0", "1", }, 394 | { "WONDERSWAN.COL", "/media/fat/_Console/WonderSwan_", "WonderSwan", "wsc", "f0", "1", }, 395 | { "ZX81", "/media/fat/_Computer/ZX81_", "ZX81", "0", "f0", "1", }, 396 | { "ZX81.P", "/media/fat/_Computer/ZX81_", "ZX81", "p", "f0", "1", }, 397 | { "ZXNEXT", "/media/fat/_Computer/ZXNext_", "ZXNext", "vhd", "f0", "1", }, 398 | 399 | // unsupported 400 | //{ "AMIGA", "/media/fat/_Computer/Minimig_", "/media/fat/games/Amiga", "", "", ""}, 401 | //{ "ATARIST", "/media/fat/_Computer/AtariST_", "/media/fat/games/AtariST", "", "", ""}, 402 | //{ "AY-3-8500", "/media/fat/_Console/AY-3-8500_", "/media/fat/games/AY-3-8500", "", "", ""}, 403 | //{ "Altair8800" 404 | //{ "MULTICOMP", "/media/fat/_Computer/MultiComp_", "/media/fat/games/MultiComp", "", "", ""}, 405 | //{ "MultiComp" 406 | //{ "SHARPMZ", "/media/fat/_Computer/SharpMZ_", "/media/fat/games/SharpMZ_", "", "", ""}, 407 | //{ "X68000" 408 | }; 409 | 410 | int core_wait = 3000; // ms 411 | int inter_key_wait = 40; // ms 412 | int sequence_wait = 1000; // ms 413 | 414 | static void emulate_key_press(int fd, int key) { 415 | msleep(inter_key_wait); 416 | ev_emit(fd, EV_KEY, key, 1); 417 | ev_emit(fd, EV_SYN, SYN_REPORT, 0); 418 | } 419 | 420 | static void emulate_key_release(int fd, int key) { 421 | msleep(inter_key_wait); 422 | ev_emit(fd, EV_KEY, key, 0); 423 | ev_emit(fd, EV_SYN, SYN_REPORT, 0); 424 | } 425 | 426 | static void emulate_key(int fd, int key) { 427 | emulate_key_press(fd, key); 428 | emulate_key_release(fd, key); 429 | } 430 | 431 | static void key_emulator_wait_mount(){ 432 | static size_t mnthash = 0; 433 | LOG("%s\n", "waiting for some change in the mount table"); 434 | size_t newhash = mnthash; 435 | for(int retry = 0; retry < 20; retry += 1){ 436 | newhash = contenthash("/proc/mounts"); 437 | if (newhash != mnthash) break; 438 | msleep(500); 439 | } 440 | if (newhash != mnthash) LOG("%s (%zu)\n", "detected a change in the mounting points", newhash); 441 | else LOG("%s (%zu)\n", "no changes in the mounting points (timeout)", newhash); 442 | mnthash = newhash; 443 | } 444 | 445 | static void key_emulator_function(int fd, int code){ 446 | switch (code){ 447 | default: return; 448 | break; case KEY_M: key_emulator_wait_mount(); 449 | break; case KEY_S: msleep(1000); 450 | } 451 | } 452 | 453 | #define TAG_KEY_NOTHING '\0' 454 | #define TAG_KEY_PRESS '{' 455 | #define TAG_KEY_RELEASE '}' 456 | #define TAG_KEY_FULL ':' 457 | #define TAG_KEY_FUNCT '!' 458 | 459 | static char* parse_hex_byte(char* seq, int* code, int* tag){ 460 | int c, n; 461 | if (0> sscanf(seq, "%2x%n", &c, &n) || 2!= n) return 0; 462 | if (code) *code = c; 463 | if (tag) *tag = TAG_KEY_NOTHING; 464 | return seq+n; 465 | } 466 | 467 | static char* parse_tagged_byte(char* seq, int* code, int* tag){ 468 | if (seq[1] == '\0') return 0; 469 | char* result = parse_hex_byte(seq+1, code, 0); 470 | if( !result) return 0; 471 | switch( *seq){ 472 | default: return 0; 473 | // TODO : use different char and tag definition 474 | break; case TAG_KEY_FULL: if (tag) *tag = TAG_KEY_FULL; 475 | break; case TAG_KEY_PRESS: if (tag) *tag = TAG_KEY_PRESS; 476 | break; case TAG_KEY_RELEASE: if (tag) *tag = TAG_KEY_RELEASE; 477 | } 478 | return result; 479 | } 480 | 481 | static char* parse_alphanumeric_key(char* seq, int* code, int* tag){ 482 | int i = 0; if (!code) code = &i; 483 | switch (*seq) { 484 | default: return 0; 485 | 486 | break;case '0': *code = KEY_0; 487 | break;case '1': *code = KEY_1; 488 | break;case '2': *code = KEY_2; 489 | break;case '3': *code = KEY_3; 490 | break;case '4': *code = KEY_4; 491 | break;case '5': *code = KEY_5; 492 | break;case '6': *code = KEY_6; 493 | break;case '7': *code = KEY_7; 494 | break;case '8': *code = KEY_8; 495 | break;case '9': *code = KEY_9; 496 | break;case 'a': *code = KEY_A; 497 | break;case 'b': *code = KEY_B; 498 | break;case 'c': *code = KEY_C; 499 | break;case 'd': *code = KEY_D; 500 | break;case 'e': *code = KEY_E; 501 | break;case 'f': *code = KEY_F; 502 | break;case 'g': *code = KEY_G; 503 | break;case 'h': *code = KEY_H; 504 | break;case 'i': *code = KEY_I; 505 | break;case 'j': *code = KEY_J; 506 | break;case 'k': *code = KEY_K; 507 | break;case 'l': *code = KEY_L; 508 | break;case 'm': *code = KEY_M; 509 | break;case 'n': *code = KEY_N; 510 | break;case 'o': *code = KEY_O; 511 | break;case 'p': *code = KEY_P; 512 | break;case 'q': *code = KEY_Q; 513 | break;case 'r': *code = KEY_R; 514 | break;case 's': *code = KEY_S; 515 | break;case 't': *code = KEY_T; 516 | break;case 'u': *code = KEY_U; 517 | break;case 'v': *code = KEY_V; 518 | break;case 'w': *code = KEY_W; 519 | break;case 'x': *code = KEY_X; 520 | break;case 'y': *code = KEY_Y; 521 | break;case 'z': *code = KEY_Z; 522 | } 523 | if (tag) *tag = TAG_KEY_FULL; 524 | return seq+1; 525 | } 526 | 527 | static char* parse_tagged_alphanumeric_key(char* seq, int* code, int* tag){ 528 | char* result = parse_alphanumeric_key(seq+1, code, tag); 529 | if (!result) return 0; 530 | if (TAG_KEY_FUNCT != *seq) return 0; // TODO : use different char and tag definition 531 | if (tag) *tag = TAG_KEY_FUNCT; 532 | return result; 533 | } 534 | 535 | static char* parse_special_key(char* seq, int* code, int* tag){ 536 | int i = 0; if (!code) code = &i; 537 | switch (*seq) { 538 | default: return 0; 539 | 540 | break;case 'U': *code = KEY_UP; // 103 0x67 up 541 | break;case 'D': *code = KEY_DOWN; // 108 0x6c down 542 | break;case 'L': *code = KEY_LEFT; // 105 0x69 left 543 | break;case 'R': *code = KEY_RIGHT; // 106 0x6a right 544 | break;case 'O': *code = KEY_ENTER; // 28 0x1c enter (Open) 545 | break;case 'E': *code = KEY_ESC; // 1 0x01 esc 546 | break;case 'H': *code = KEY_HOME; // 102 0x66 home 547 | break;case 'F': *code = KEY_END; // 107 0x6b end (Finish) 548 | break;case 'M': *code = KEY_F12; // 88 0x58 f12 (Menu) 549 | } 550 | if (tag) *tag = TAG_KEY_FULL; 551 | return seq+1; 552 | } 553 | 554 | static char* parse_key_sequence(char* seq, int* code, int* tag){ 555 | char *next; 556 | if (0!=( next = parse_alphanumeric_key(seq, code, tag) )) return next; 557 | if (0!=( next = parse_tagged_alphanumeric_key(seq, code, tag) )) return next; 558 | if (0!=( next = parse_special_key(seq, code, tag) )) return next; 559 | if (0!=( next = parse_tagged_byte(seq, code, tag) )) return next; 560 | return 0; 561 | } 562 | 563 | static int emulate_sequence(char* seq) { 564 | 565 | int fd = ev_open(); 566 | if (fd < 0) { 567 | return -1; 568 | } 569 | 570 | // Wait that userspace detects the new device 571 | msleep(sequence_wait); 572 | 573 | while (seq && '\0' != *seq) { 574 | int code = 0, tag = 0; 575 | 576 | // Parse the sequence 577 | char* newseq = parse_key_sequence(seq, &code, &tag); 578 | if (0 == newseq || seq == newseq) goto err; // can not parse 579 | seq = newseq; 580 | 581 | // Emulate the keyboard event 582 | switch (tag) { 583 | default: goto err; // unsupported action 584 | 585 | break;case TAG_KEY_FULL: emulate_key(fd, code); 586 | break;case TAG_KEY_PRESS: emulate_key_press(fd, code); 587 | break;case TAG_KEY_RELEASE: emulate_key_release(fd, code); 588 | break;case TAG_KEY_FUNCT: key_emulator_function(fd, code); 589 | } 590 | } 591 | 592 | // Wait that userspace detects all the events 593 | msleep(sequence_wait); 594 | 595 | ev_close(fd); 596 | return 0; 597 | 598 | err: 599 | ev_close(fd); 600 | return -1; 601 | } 602 | 603 | static char* after_string(char* str, char delim) { 604 | for (char* curr = str; '\0' != *curr; curr += 1){ 605 | if (delim == *curr) { 606 | str = curr+1; 607 | } 608 | } 609 | return str; 610 | } 611 | 612 | static void get_core_name(char* corepath, char* out, int size) { 613 | 614 | char* start = after_string(corepath, '/'); 615 | if (NULL == start) { 616 | return; 617 | } 618 | 619 | char* end = after_string(start, '_'); 620 | end -= 1; 621 | int len = end - start; 622 | if (len <= 0) { 623 | return; 624 | } 625 | 626 | size -= 1; 627 | if (size > len) size = len; 628 | strncpy(out, start, size); 629 | out[size] = '\0'; 630 | 631 | for (int i = 0; i < len; i++){ 632 | out[i] = toupper(out[i]); 633 | } 634 | } 635 | 636 | static int cmp_char_ptr_field(const void * a, const void * b) { 637 | // This accesses the "id" field of the "system_t" struct 638 | return strcmp(*(const char**)a, *(const char**)b); 639 | } 640 | 641 | static system_t* get_system(char* corepath, char * name) { 642 | 643 | char system[64] = {0, }; 644 | 645 | if (NULL == name) { 646 | get_core_name(corepath, system, ARRSIZ(system)); 647 | if ('\0' == system[0]) { 648 | return NULL; 649 | } 650 | name = system; 651 | } 652 | 653 | system_t target = {name, 0}; 654 | return (system_t*) SBSEARCH(&target, system_list, cmp_char_ptr_field); 655 | } 656 | 657 | static int load_core(system_t* sys, char* corepath) { 658 | 659 | if (NULL == sys) { // TODO : remove this check ? sys is not used ! 660 | PRINTERR("%s\n", "invalid system"); 661 | return -1; 662 | } 663 | 664 | while (1) { 665 | int tmp = open(MISTER_COMMAND_DEVICE, O_WRONLY|O_NONBLOCK); 666 | if (0<= tmp) { 667 | close(tmp); 668 | break; 669 | } 670 | LOG("%s\n", "can not access the MiSTer command fifo; retrying"); 671 | msleep(1000); 672 | } 673 | 674 | FILE* f = fopen(MISTER_COMMAND_DEVICE, "wb"); 675 | if (0 == f) { 676 | PRINTERR("%s\n", MISTER_COMMAND_DEVICE); 677 | return -1; 678 | } 679 | 680 | // TODO : check that a file exists at corepath ? 681 | 682 | int ret = fprintf(f, "load_core %s\n", corepath); 683 | if (0 > ret){ 684 | return -1; 685 | } 686 | 687 | fclose(f); 688 | return 0; 689 | } 690 | 691 | // This MUST BE KEPT IN SYNC with the findPrefixDir function of the Main_MiSTer (file_io.cpp) 692 | // 693 | int findPrefixDirAux(const char* path, char *dir, size_t dir_len){ 694 | char temp_dir[dir_len+1]; 695 | if (0> snprintf(temp_dir, dir_len, "%s/%s", path, dir)) return 0; 696 | struct stat sb; 697 | if (stat(temp_dir, &sb)) return 0; 698 | if (!S_ISDIR(sb.st_mode)) return 0; 699 | if (0> snprintf(dir, dir_len, "%s", temp_dir)) return 0; 700 | return 1; 701 | } 702 | int findPrefixDir(char *dir, size_t dir_len){ 703 | 704 | if (findPrefixDirAux("/media/usb0", dir, dir_len)) return 1; 705 | if (findPrefixDirAux("/media/usb0/games", dir, dir_len)) return 1; 706 | if (findPrefixDirAux("/media/usb1", dir, dir_len)) return 1; 707 | if (findPrefixDirAux("/media/usb1/games", dir, dir_len)) return 1; 708 | if (findPrefixDirAux("/media/usb2", dir, dir_len)) return 1; 709 | if (findPrefixDirAux("/media/usb2/games", dir, dir_len)) return 1; 710 | if (findPrefixDirAux("/media/usb3", dir, dir_len)) return 1; 711 | if (findPrefixDirAux("/media/usb3/games", dir, dir_len)) return 1; 712 | if (findPrefixDirAux("/media/usb4", dir, dir_len)) return 1; 713 | if (findPrefixDirAux("/media/usb4/games", dir, dir_len)) return 1; 714 | if (findPrefixDirAux("/media/usb5", dir, dir_len)) return 1; 715 | if (findPrefixDirAux("/media/usb5/games", dir, dir_len)) return 1; 716 | if (findPrefixDirAux("/media/usb6", dir, dir_len)) return 1; 717 | if (findPrefixDirAux("/media/usb6/games", dir, dir_len)) return 1; 718 | if (findPrefixDirAux("/media/fat/cifs", dir, dir_len)) return 1; 719 | if (findPrefixDirAux("/media/fat/cifs/games", dir, dir_len)) return 1; 720 | if (findPrefixDirAux("/media/fat", dir, dir_len)) return 1; 721 | if (findPrefixDirAux("/media/fat/games", dir, dir_len)) return 1; 722 | 723 | return 0; 724 | } 725 | 726 | static void get_base_path(system_t* sys, char* out, int size) { 727 | if ('/' == sys->fsid[0]) { 728 | snprintf(out, size-1, "%s", sys->fsid); 729 | } else { 730 | snprintf(out, size-1, "%s", sys->fsid); 731 | if (!findPrefixDir(out, size)) 732 | snprintf(out, size-1, "/media/fat/games/%s", sys->fsid); // fallback 733 | } 734 | } 735 | 736 | char* search_in_string(const char* pattern_start, const char* data, size_t *size){ 737 | char *pattern, *candidate; 738 | 739 | #define MATCH_RESET() do{ \ 740 | pattern = (char*) pattern_start; \ 741 | candidate = NULL; \ 742 | }while(0) 743 | 744 | MATCH_RESET(); 745 | while( 1){ 746 | 747 | if('\0' == *pattern) goto matched; 748 | if('\0' == *data) goto not_matched; 749 | if(pattern_start == pattern) candidate = (char*) data; 750 | 751 | // if('%' != *pattern){ // simple char vs wildcard delimiter 752 | if(*pattern == *data) pattern += 1; // simple char match 753 | else MATCH_RESET(); // simple char do not match 754 | // }else{ 755 | // 756 | // pattern += 1; 757 | // switch(*pattern){ 758 | // break; default: // wrong wildcard 759 | // goto not_matched; 760 | // 761 | // break; case '%': // match a '%' (escaped wildcard delimiter) 762 | // if('%' == *data) pattern += 1; 763 | // else MATCH_RESET(); 764 | // 765 | // break; case 'W': // match zero or more whitespace 766 | // while(' ' == *data || '\t' == *data || '\r' == *data || '\n' == *data) 767 | // data += 1; 768 | // data -= 1; 769 | // pattern += 1; 770 | // } 771 | // } 772 | data += 1; 773 | } 774 | not_matched: 775 | candidate = NULL; 776 | 777 | matched: 778 | if(size){ 779 | if(!candidate) *size = 0; 780 | else *size = data - candidate + 1; 781 | } 782 | return candidate; 783 | 784 | #undef MATCH_RESET 785 | } 786 | 787 | 788 | int get_absolute_dir_name(const char* source, char* out, size_t len){ 789 | char dirpath[PATH_MAX]; 790 | realpath(source, dirpath); 791 | if( -1 == snprintf(out, len, "%s", dirpath)) 792 | return -1; 793 | return 0; 794 | } 795 | 796 | int get_relative_path_to_root(int skip, const char* path, char* out, size_t len){ 797 | char dirpath[PATH_MAX]; 798 | dirpath[0] = '\0'; 799 | get_absolute_dir_name(path, dirpath, sizeof(dirpath)); 800 | char* cur = out; 801 | snprintf(cur, len, "./"); 802 | len -= 2; 803 | cur += 2; 804 | for(int i = 0, count = 0; dirpath[i] != '\0'; i += 1){ 805 | if(dirpath[i] == '/'){ 806 | count += 1; 807 | if(count > skip){ 808 | snprintf(cur, len, "../"); 809 | len -= 3; 810 | cur += 3; 811 | } 812 | } 813 | } 814 | return 0; 815 | } 816 | 817 | int stricmp(const char* a, const char* b) { 818 | for (; tolower(*a) == tolower(*b); a += 1, b += 1) 819 | if (*a == '\0') 820 | return 0; 821 | return tolower(*a) - tolower(*b); 822 | } 823 | 824 | static int has_ext(char* name, char* ext){ 825 | char* name_ext = after_string(name, '.'); 826 | if (name_ext == name) return 0; 827 | while (1) { 828 | if (tolower(*ext) != tolower(*name_ext)) return 0; 829 | if (*name_ext != '\0') break; 830 | ext += 1; 831 | name_ext += 1; 832 | } 833 | return 1; 834 | } 835 | 836 | static int resolve_core_path(char* path, char* out, int len){ 837 | strncpy(out, path, len); 838 | char* name = after_string(out, '/'); 839 | if (*name == '\0' || name - out < 3) { 840 | return -1; 841 | } 842 | name[-1] = '\0'; 843 | int matched = 0; 844 | struct dirent* ep = NULL; 845 | DIR* dp = opendir(out); 846 | if (dp != NULL) { 847 | int nlen = strlen(name); 848 | while (0 != (ep = readdir (dp))){ 849 | if (!strncmp(name, ep->d_name, nlen) && has_ext(ep->d_name, CORE_EXT)){ 850 | matched = 1; 851 | snprintf(out+strlen(out), len-strlen(out)-1, "/%s", ep->d_name); 852 | break; 853 | } 854 | } 855 | } 856 | if (!matched) { 857 | return -1; 858 | } 859 | return 0; 860 | } 861 | 862 | static int list_core(){ 863 | for (int i=0; icore)) 877 | return 0; 878 | return 1; 879 | } 880 | 881 | static int rebase_canon_path(const char* base, const char* path, char* buf, size_t len) { 882 | 883 | size_t blen = strlen(base); 884 | 885 | // Special case: path is inside base 886 | if (!strncmp(base, path, blen)){ 887 | return 0> snprintf(buf, len, "%s", path+blen+1); 888 | } 889 | 890 | // Find common prefix 891 | size_t prefix = 0; 892 | for (size_t c = 0; base[c] == path[c] && '\0' != base[c] && '\0' != path[c]; c += 1) 893 | if (base[c] == '/') prefix = c + 1; 894 | 895 | // Expand the right number of ../ 896 | for (int c = 0; c < blen - prefix; c+=1){ 897 | if ('/' == base[prefix + c -1]) { 898 | if (0> snprintf(buf, len, "../")){ 899 | return -1; 900 | } 901 | buf += 3; 902 | len -= 3; 903 | } 904 | } 905 | 906 | // Final path part 907 | return 0> snprintf(buf, len, "%s", path+prefix); 908 | } 909 | 910 | // Note: path and out may point to the same memory. 911 | static int reduce_path(const char* path, char* out, size_t len) { 912 | size_t d = 0; 913 | 914 | for (size_t s = 0; '\0' != path[s]; s += 1){ 915 | if (d >= len) break; 916 | out[d] = path[s]; 917 | if (0){ 918 | 919 | // simplify '//' occurences 920 | }else if (0 921 | ||(d >= 1 && '/' == out[d-1] && '/' == out[d]) 922 | ){ 923 | d -= 1; 924 | 925 | // simplify './' occurences 926 | }else if (0 927 | ||(d == 1 && '.' == out[d-1] && '/' == out[d]) 928 | ||(d > 1 && '/' == out[d-2] && '.' == out[d-1] && '/' == out[d]) 929 | ){ 930 | d -= 2; 931 | 932 | // simplify '../' occurences 933 | }else if (0 934 | ||(d == 2 && '.' == out[d-2] && '.' == out[d-1] && '/' == out[d]) 935 | ||(d > 2 && '/' == out[d-3] && '.' == out[d-2] && '.' == out[d-1] && '/' == out[d]) 936 | ){ 937 | d -= 4; 938 | if (d < 0) d = 0; 939 | for (; d > 0 && '/' != out[d]; d -= 1); 940 | } 941 | 942 | d += 1; 943 | } 944 | out[d] = '\0'; 945 | if (d-1 < len && '/' == out[d-1]) out[d-1] = '\0'; 946 | return 0; 947 | } 948 | 949 | static int rebase_path(const char* base, const char* path, char* buf, size_t len) { 950 | 951 | char* pwd = getenv("PWD"); 952 | if (!pwd) return -1; 953 | size_t pwdl = strlen(pwd); 954 | 955 | size_t basel = strlen(base)+pwdl+2; 956 | char rbase[basel]; 957 | if (0> snprintf(rbase, basel, "%s%s%s", 958 | '/' != base[0] ? pwd : "", 959 | '/' != base[0] ? "/" : "", 960 | base) 961 | ) return -1; 962 | 963 | size_t pathl = strlen(path)+pwdl+2; 964 | char rpath[pathl]; 965 | if (0> snprintf(rpath, pathl, "%s%s%s", 966 | '/' != path[0] ? pwd : "", 967 | '/' != path[0] ? "/" : "", 968 | path) 969 | ) return -1; 970 | 971 | int res; 972 | res = reduce_path(rbase, rbase, sizeof(rbase)); 973 | if (res) return res; 974 | res = reduce_path(rpath, rpath, sizeof(rpath)); 975 | if (res) return res; 976 | 977 | return rebase_canon_path(rbase, rpath, buf, len); 978 | } 979 | 980 | static int write_mgl_wrapper(FILE* out, char* corepath, char* rom, char* delay, char typ, char* index) { 981 | 982 | if (0> fprintf( out, "\n %s", corepath)){ 983 | PRINTERR("%s", "error while writing mgl file\n"); 984 | return -1; 985 | } 986 | 987 | if (0> fprintf( out, "\n fprintf( out, 993 | "\" delay=\"%s\" type=\"%c\" index=\"%s\" />\n\n", 994 | delay, typ, index)){ 995 | PRINTERR("%s", "error while writing mgl file\n"); 996 | return -1; 997 | } 998 | 999 | return 0; 1000 | } 1001 | 1002 | static int generate_mgl(system_t* sys, char* corepath, char* rompath, FILE* out) { 1003 | char base[PATH_MAX]; 1004 | char rcore[PATH_MAX]; 1005 | char rrom[PATH_MAX]; 1006 | 1007 | if (NULL == sys) { 1008 | PRINTERR("%s\n", "invalid system"); 1009 | return -1; 1010 | } 1011 | 1012 | char *opt = sys->loadmode; 1013 | if (!has_mgl_support(sys)) { 1014 | PRINTERR("can not generate mgl for '%s' system - plese load the core directly\n", sys->id); 1015 | return -1; 1016 | } 1017 | 1018 | if (NULL == corepath) 1019 | corepath = sys->core; 1020 | 1021 | if (rebase_path("/media/fat", corepath, rcore, sizeof(rcore))){ 1022 | PRINTERR("can not handle folder %s\n", corepath); 1023 | return -1; 1024 | } 1025 | 1026 | // remove extension from core path 1027 | for (int c = strlen(rcore)-1; c >= 0; c -= 1){ 1028 | if ('/' == rcore[c]) break; 1029 | if ('.' == rcore[c]) { 1030 | rcore[c] = '\0'; 1031 | break; 1032 | } 1033 | } 1034 | 1035 | get_base_path(sys, base, sizeof(base)); 1036 | 1037 | if (rebase_path(base, rompath, rrom, sizeof(rrom))){ 1038 | PRINTERR("can not handle folder %s\n", rompath); 1039 | return -1; 1040 | } 1041 | 1042 | return write_mgl_wrapper(out, rcore, rrom, 1043 | sys->delay ? sys->delay : "0", 1044 | opt[0], 1045 | '\0' == opt[0] ? opt : opt+1); 1046 | } 1047 | 1048 | static int generate_mgl_in_path(system_t* sys, char* corepath, char* rom, const char* outpath) { 1049 | 1050 | int result = 0; 1051 | 1052 | if (mkparent(outpath, 0777)){ 1053 | PRINTERR("can not create mgl folder at %s\n", outpath); 1054 | return -1; 1055 | } 1056 | 1057 | FILE* mgl = fopen(outpath, "wb"); 1058 | if (0 != mgl) { 1059 | result = generate_mgl(sys, corepath, rom, mgl); 1060 | fclose(mgl); 1061 | } 1062 | 1063 | if (result) 1064 | PRINTERR("can not generate mgl file at %s\n", outpath); 1065 | return result; 1066 | } 1067 | 1068 | static int load_core_directly(system_t* sys, char* path){ 1069 | char rpath[PATH_MAX]; 1070 | if (NULL == sys) 1071 | return -1; 1072 | if (rebase_path(sys->core, path, rpath, sizeof(rpath))) 1073 | return -1; 1074 | return load_core(sys, rpath); 1075 | } 1076 | 1077 | static int load_core_and_rom(system_t* sys, char* corepath, char* rom) { 1078 | 1079 | if (!has_mgl_support(sys)) 1080 | return load_core_directly(sys, rom); 1081 | 1082 | if (generate_mgl_in_path(sys, corepath, rom, MGL_PATH)) 1083 | return -1; 1084 | return load_core(sys, MGL_PATH); 1085 | } 1086 | 1087 | static int load_rom_autocore(system_t* sys, char* rom) { 1088 | 1089 | if (NULL == sys) { 1090 | return -1; 1091 | } 1092 | 1093 | if (!has_mgl_support(sys)) 1094 | return load_core_directly(sys, rom); 1095 | 1096 | int plen = 64 + strlen(sys->core); 1097 | char corepath[plen]; 1098 | 1099 | if (resolve_core_path(sys->core, corepath, plen)){ 1100 | PRINTERR("Can not find the core at %s\n", sys->core); 1101 | return -1; 1102 | } 1103 | 1104 | return load_core_and_rom(sys, corepath, rom); 1105 | } 1106 | 1107 | struct cmdentry { 1108 | const char * name; 1109 | void (*cmd)(int argc, char** argv); 1110 | }; 1111 | 1112 | int checkarg(int min, int val){ 1113 | if (val >= min+1) return 1; 1114 | PRINTERR("At least %d arguments are needed\n", min); 1115 | return 0; 1116 | } 1117 | 1118 | int list_content_for(system_t* sys){ 1119 | DIR *dp; 1120 | struct dirent *ep; 1121 | int something_found = 0; 1122 | 1123 | char romdir[PATH_MAX] = {0}; 1124 | get_base_path(sys, romdir, sizeof(romdir)); 1125 | 1126 | dp = opendir(romdir); 1127 | if (dp != NULL) { 1128 | while (0 != (ep = readdir (dp))){ 1129 | 1130 | if (has_ext(ep->d_name, sys->romext)){ 1131 | something_found = 1; 1132 | printf("%s %s/%s\n", sys->id, romdir, ep->d_name); 1133 | } 1134 | } 1135 | closedir(dp); 1136 | } 1137 | if (!something_found) { 1138 | //printf("#%s no '.%s' files found in %s\n", sys->id, sys->romext, romdir); 1139 | } 1140 | return something_found; 1141 | } 1142 | 1143 | int list_content(){ 1144 | for (int i=0; i sscanf(ms, "%d", &timeout)){ 1154 | printf("error\n"); 1155 | return -1; 1156 | } 1157 | input_monitor monitor; 1158 | int result = user_input_open(&monitor); 1159 | if (result) goto end; 1160 | 1161 | for (int c = 1; 1;){ 1162 | 1163 | result = user_input_poll(&monitor, timeout); 1164 | if (!single) c += user_input_clear(&monitor); 1165 | 1166 | if (is_user_input_timeout(result)) printf("timeout\n"); 1167 | else if (is_user_input_event(result)) printf("event catched %d\n", c); 1168 | 1169 | if (single) break; 1170 | } 1171 | goto end; 1172 | 1173 | end: 1174 | user_input_close(&monitor); 1175 | if (!is_user_input_timeout(result) && ! is_user_input_event(result)) 1176 | PRINTERR("input monitor error %d\n", result); 1177 | 1178 | return 0; 1179 | } 1180 | 1181 | static int stream_mode(); 1182 | 1183 | // command list 1184 | static void cmd_exit(int argc, char** argv) { exit(0); } 1185 | static void cmd_stream_mode(int argc, char** argv) { stream_mode(); } 1186 | static void cmd_load_core(int argc, char** argv) { if(checkarg(1,argc))load_core(get_system(argv[1],NULL),argv[1]); } 1187 | static void cmd_load_core_as(int argc, char** argv) { if(checkarg(2,argc))load_core(get_system(NULL,argv[1]),argv[2]); } 1188 | static void cmd_list_core(int argc, char** argv) { list_core(); } 1189 | static void cmd_rom_autocore(int argc, char** argv) { if(checkarg(2,argc))load_rom_autocore(get_system(NULL,argv[1]),argv[2]); } 1190 | static void cmd_load_all(int argc, char** argv) { if(checkarg(2,argc))load_core_and_rom(get_system(argv[1],NULL),argv[1],argv[2]); } 1191 | static void cmd_load_all_as(int argc, char** argv) { if(checkarg(3,argc))load_core_and_rom(get_system(NULL,argv[1]),argv[2],argv[3]); } 1192 | static void cmd_raw_seq(int argc, char** argv) { if(checkarg(1,argc))emulate_sequence(argv[1]); } 1193 | static void cmd_list_content(int argc, char** argv) { list_content(); } 1194 | static void cmd_list_rom_for(int argc, char** argv) { if(checkarg(1,argc))list_content_for(get_system(NULL,argv[1])); } 1195 | static void cmd_wait_input(int argc, char** argv) { if(checkarg(1,argc))monitor_user_input(1,argv[1]); } 1196 | static void cmd_catch_input(int argc, char** argv) { if(checkarg(1,argc))monitor_user_input(0,argv[1]); } 1197 | static void cmd_mgl_gen(int argc, char** argv) { if(checkarg(3,argc))generate_mgl(get_system(NULL,argv[1]),argv[2],argv[3],stdout); } 1198 | static void cmd_mgl_gen_auto(int argc, char** argv) { if(checkarg(2,argc))generate_mgl(get_system(NULL,argv[1]),NULL,argv[2],stdout); } 1199 | // 1200 | struct cmdentry cmdlist[] = { 1201 | // 1202 | // The "name" field can not contain ' ' or '\0'. 1203 | // The array must be lexicographically sorted wrt "name" field (e.g. 1204 | // :sort vim command, but mind '!' and escaped chars at end of similar names). 1205 | // 1206 | {"catch_input" , cmd_catch_input , } , 1207 | {"done" , cmd_exit , } , 1208 | {"list_content" , cmd_list_content , } , 1209 | {"list_core" , cmd_list_core , } , 1210 | {"list_rom_for" , cmd_list_rom_for , } , 1211 | {"load_all" , cmd_load_all , } , 1212 | {"load_all_as" , cmd_load_all_as , } , 1213 | {"load_core" , cmd_load_core , } , 1214 | {"load_core_as" , cmd_load_core_as , } , 1215 | {"load_rom" , cmd_rom_autocore , } , 1216 | {"mgl_gen" , cmd_mgl_gen , } , 1217 | {"mgl_gen_auto" , cmd_mgl_gen_auto , } , 1218 | {"raw_seq" , cmd_raw_seq , } , 1219 | {"stream" , cmd_stream_mode , } , 1220 | {"wait_input" , cmd_wait_input , } , 1221 | }; 1222 | 1223 | static int run_command(int narg, char** args) { 1224 | 1225 | // match the command 1226 | struct cmdentry target = {args[0], 0}; 1227 | struct cmdentry * command = (struct cmdentry*) 1228 | SBSEARCH(&target, cmdlist, cmp_char_ptr_field); 1229 | 1230 | // call the command 1231 | if (NULL == command || NULL == command->cmd) { 1232 | PRINTERR("%s\n", "unknown command"); 1233 | return -1; 1234 | } 1235 | command->cmd(narg, args); 1236 | return 0; 1237 | } 1238 | 1239 | static int stream_mode() { 1240 | char* line = NULL; 1241 | size_t size = 0; 1242 | while (1) { 1243 | 1244 | // read line 1245 | int len = getline(&line, &size, stdin); 1246 | if (-1 == len) { 1247 | break; 1248 | } 1249 | len = len-1; 1250 | line[len] = '\0'; 1251 | 1252 | // Split command in arguments 1253 | int narg = 0; 1254 | char *args[5] = {0,}; 1255 | int matchnew = 1; 1256 | for (int i=0; icore = strdup(val); 1294 | val = getenv("MBC_CUSTOM_FOLDER"); 1295 | if (NULL != val && val[0] != '\0') custom_system->fsid = strdup(val); 1296 | val = getenv("MBC_CUSTOM_ROM_EXT"); 1297 | if (NULL != val && val[0] != '\0') custom_system->romext = strdup(val); 1298 | val = getenv("MBC_CUSTOM_DELAY"); 1299 | if (NULL != val && val[0] != '\0') custom_system->delay = strdup(val); 1300 | val = getenv("MBC_CUSTOM_MODE"); 1301 | if (NULL != val && val[0] != '\0') custom_system->loadmode = strdup(val); 1302 | } 1303 | 1304 | val = getenv("MBC_CORE_WAIT"); 1305 | if (NULL != val && val[0] != '\0') { 1306 | int i; 1307 | if (1 == sscanf(val, "%d", &i)) { 1308 | core_wait = i; 1309 | } else { 1310 | printf("invalid core wait option from environment; fallling back to %d ms\n", core_wait); 1311 | } 1312 | } 1313 | 1314 | val = getenv("MBC_KEY_WAIT"); 1315 | if (NULL != val && val[0] != '\0') { 1316 | int i; 1317 | if (1 == sscanf(val, "%d", &i)) { 1318 | inter_key_wait = i; 1319 | } else { 1320 | printf("invalid key wait option from environment; fallling back to %d ms\n", inter_key_wait); 1321 | } 1322 | } 1323 | 1324 | val = getenv("MBC_SEQUENCE_WAIT"); 1325 | if (NULL != val && val[0] != '\0') { 1326 | int i; 1327 | if (1 == sscanf(val, "%d", &i)) { 1328 | sequence_wait = i; 1329 | } else { 1330 | printf("invalid sequence wait option from environment; fallling back to %d ms\n", sequence_wait); 1331 | } 1332 | } 1333 | } 1334 | 1335 | static void print_help(char* name) { 1336 | printf("MBC (Mister Batch Control) Revision %d\n", MBC_BUILD_REVISION); 1337 | #ifdef MBC_BUILD_DATE 1338 | printf("Build timestamp: %s\n", MBC_BUILD_DATE); 1339 | #endif // MBC_BUILD_DATE 1340 | #ifdef MBC_BUILD_COMMIT 1341 | printf("Build commit: %s\n", MBC_BUILD_COMMIT); 1342 | #endif // MBC_BUILD_COMMIT 1343 | printf("Usage:\n"); 1344 | printf(" %s COMMAND [ARGS]\n", name); 1345 | printf("\n"); 1346 | printf("E.g.:\n"); 1347 | printf(" %s load_rom NES /media/fat/NES/*.nes\n", name); 1348 | printf("\n"); 1349 | printf("Supported COMMAND:"); 1350 | for (int i=0; i argc) { 1362 | print_help(argv[0]); 1363 | return 0; 1364 | } 1365 | 1366 | read_options(argc, argv); 1367 | 1368 | return run_command(argc-1, argv+1); 1369 | } 1370 | 1371 | --------------------------------------------------------------------------------