├── .gitignore ├── novel ├── music │ └── Put your vgm or mp3 music files here.txt ├── script │ ├── inventory.scr │ └── main.scr ├── wav │ └── Put your sound fx wav files here.txt ├── background │ └── black.jpg └── foreground │ └── character.png ├── Mega_Drive_Palette.png ├── run.bat ├── src ├── novel_images.c ├── novel_scripts.c ├── novel_variables.c ├── novel_sounds.c ├── main.c ├── novel_external_functions.c ├── novel_functions.c └── novel_player.c ├── avoid.txt ├── inc ├── novel_scripts.h ├── novel_player.h ├── novel_sounds.h ├── novel_external_functions.h ├── novel_images.h ├── novel_variables.h └── novel_functions.h ├── novel.ini ├── make_all.bat ├── rebuild.bat ├── create_external_functions.py ├── create_image_res.py ├── script_format.txt ├── create_sound_res.py ├── tutorial.md ├── readme.md ├── convert_images.py └── convert_scripts.py /.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | /.vscode 3 | /res 4 | /src/boot -------------------------------------------------------------------------------- /novel/music/Put your vgm or mp3 music files here.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /novel/script/inventory.scr: -------------------------------------------------------------------------------- 1 | text You have nothing. 2 | jump $retfile $retlabel -------------------------------------------------------------------------------- /novel/wav/Put your sound fx wav files here.txt: -------------------------------------------------------------------------------- 1 | Put your sound fx .wav files here -------------------------------------------------------------------------------- /Mega_Drive_Palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakoeimon/VNMD/HEAD/Mega_Drive_Palette.png -------------------------------------------------------------------------------- /novel/background/black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakoeimon/VNMD/HEAD/novel/background/black.jpg -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | c:\RetroArch-Win64\retroarch.exe -L c:\RetroArch-Win64\cores\blastem_libretro.dll %cd%\out\rom.bin -------------------------------------------------------------------------------- /novel/foreground/character.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakoeimon/VNMD/HEAD/novel/foreground/character.png -------------------------------------------------------------------------------- /src/novel_images.c: -------------------------------------------------------------------------------- 1 | #include "novel_images.h" 2 | 3 | const Image *NOVEL_BACKGROUND[] = { 4 | &bg_1, 5 | }; 6 | 7 | const Image *NOVEL_FOREGROUND[] = { 8 | &fg_1, 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /avoid.txt: -------------------------------------------------------------------------------- 1 | Symbols to avoid. 2 | When converting the scripts keep in mind that those letters(symbols) 3 | counts one in python and more in C 4 | If a jump does not work see for those symbols in your sripts .scr files. 5 | 6 | ß -------------------------------------------------------------------------------- /src/novel_scripts.c: -------------------------------------------------------------------------------- 1 | #include "novel_scripts.h" 2 | 3 | const u8* NOVEL_SCRIPTS[] = { 4 | script_main, 5 | script_inventory, 6 | }; 7 | 8 | const s32 NOVEL_SCRIPTS_BYTES_COUNT[] = { 9 | 218, 10 | 26, 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /inc/novel_scripts.h: -------------------------------------------------------------------------------- 1 | #ifndef H_NOVEL_SCRIPTS 2 | #define H_NOVEL_SCRIPTS 3 | 4 | #include "genesis.h" 5 | #include "scripts_res.h" 6 | 7 | extern const u8* NOVEL_SCRIPTS[]; 8 | 9 | extern const s32 NOVEL_SCRIPTS_BYTES_COUNT[]; 10 | 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /inc/novel_player.h: -------------------------------------------------------------------------------- 1 | #ifndef H_NOVEL_PLAYER 2 | #define H_NOVEL_PLAYER 3 | //#define GR_LANG 4 | void novel_reset(); 5 | 6 | void novel_update(); 7 | 8 | typedef struct ifchoice_{ 9 | int display; 10 | int pos; 11 | char *text; 12 | }ifchoice; 13 | 14 | #endif -------------------------------------------------------------------------------- /novel.ini: -------------------------------------------------------------------------------- 1 | MAGICK_CONVERT_PATH C:/Program Files/ImageMagick-7.1.0-Q16-HDRI/convert.exe 2 | BG_WIDTH 32 3 | BG_HEIGHT 24 4 | BG_TOP 0 5 | TEXT_TOP 24 6 | TEXT_BOTTOM 27 7 | TEXT_LEFT 2 8 | TEXT_RIGHT 38 9 | COMPRESSION NONE 10 | SAVE_CHECK 1234 11 | SOUND_DRV XGM 12 | DITHERING TRUE 13 | BAD_FILENAMES FALSE -------------------------------------------------------------------------------- /inc/novel_sounds.h: -------------------------------------------------------------------------------- 1 | #ifndef H_NOVEL_SOUNDS 2 | #define H_NOVEL_SOUNDS 3 | 4 | #include "genesis.h" 5 | #include "novel_sounds_res.h" 6 | 7 | extern void (*NOVEL_PLAY_SOUND[])(int v); 8 | extern void (*NOVEL_PLAY_MUSIC[])(); 9 | 10 | void novel_stop_sound(); 11 | 12 | void novel_stop_music(); 13 | 14 | #endif -------------------------------------------------------------------------------- /inc/novel_external_functions.h: -------------------------------------------------------------------------------- 1 | #ifndef H_NV_EXTERNAL_FUNCTIONS 2 | #define H_NV_EXTERNAL_FUNCTIONS 3 | 4 | #define NV_EXTERNAL_FUNCTIONS_NUM 5 5 | 6 | extern void (*nv_external_functions[])(); 7 | 8 | void reset(); 9 | void fade_out(); 10 | void ret_jump_store(); 11 | void ret_return(); 12 | void fr_clear(); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /make_all.bat: -------------------------------------------------------------------------------- 1 | python convert_images.py 2 | 3 | python create_image_res.py 4 | python create_sound_res.py 5 | 6 | python create_external_functions.py 7 | python convert_scripts.py 8 | 9 | c:\sgdk\bin\make -f c:\sgdk\makefile.gen 10 | 11 | c:\RetroArch-Win64\retroarch.exe -L c:\RetroArch-Win64\cores\blastem_libretro.dll out\rom.bin 12 | -------------------------------------------------------------------------------- /src/novel_variables.c: -------------------------------------------------------------------------------- 1 | #include "novel_variables.h" 2 | #include "novel_external_functions.h" 3 | 4 | const int NOVEL_SAVE_CHECK_NUM = 1234; 5 | 6 | const int NOVEL_NUM_VARIABLES = 3; 7 | const int NOVEL_NUM_GLOBAL_VARIABLES = 0; 8 | 9 | int NOVEL_VARIABLES[3]; 10 | void (*nv_external_functions[])() = { 11 | reset, 12 | fade_out, 13 | ret_jump_store, 14 | ret_return, 15 | fr_clear, 16 | }; -------------------------------------------------------------------------------- /src/novel_sounds.c: -------------------------------------------------------------------------------- 1 | #include "novel_sounds.h" 2 | 3 | void novel_stop_sound() { 4 | XGM_stopPlayPCM(SOUND_PCM_CH2); 5 | } 6 | 7 | void novel_stop_music() { 8 | XGM_stopPlay(); 9 | } 10 | 11 | void play_null_music() {} 12 | void (*NOVEL_PLAY_MUSIC[])() = { 13 | play_null_music 14 | }; 15 | 16 | void play_null_sound(int v) {} 17 | void (*NOVEL_PLAY_SOUND[])() = { 18 | play_null_sound 19 | }; 20 | 21 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "novel_player.h" 4 | 5 | #ifdef GR_LANG 6 | #include "grres.h" 7 | #endif 8 | 9 | int main(bool hard) 10 | { 11 | #ifdef GR_LANG 12 | VDP_loadFont(&greek_font, DMA); 13 | #endif 14 | 15 | XGM_setMusicTempo(60); 16 | novel_reset(); 17 | while(TRUE) { 18 | 19 | novel_update(); 20 | 21 | } 22 | return 0; 23 | } 24 | 25 | -------------------------------------------------------------------------------- /novel/script/main.scr: -------------------------------------------------------------------------------- 1 | bgload black 60 2 | text It was too dark to see anything. 3 | label make_a_choice 4 | choice Look around.|Inventory 5 | if selected == 1: 6 | text He tried to see, but darness prevailed. 7 | goto make_a_choice 8 | fi 9 | if selected == 2: 10 | ret_jump_store 11 | jump inventory 12 | text So you have nothing. 13 | fi 14 | 15 | setimg character 128 0 16 | text Ha... you have nothing! 17 | fr_clear 18 | text and it is dark again. -------------------------------------------------------------------------------- /inc/novel_images.h: -------------------------------------------------------------------------------- 1 | #ifndef H_NOVEL_IMAGES 2 | #define H_NOVEL_IMAGES 3 | 4 | #include "genesis.h" 5 | #include "novel_images_res.h" 6 | 7 | #define NOVEL_TEXT_TOP 24 8 | #define NOVEL_TEXT_BOTTOM 27 9 | #define NOVEL_TEXT_LEFT 2 10 | #define NOVEL_TEXT_RIGHT 38 11 | #define NOVEL_TEXT_WIDTH 36 12 | 13 | #define NOVEL_BG_WIDTH 32 14 | #define NOVEL_BG_HEIGHT 24 15 | #define NOVEL_BG_TOP 0 16 | #define NOVEL_BG_LEFT 4 17 | 18 | extern const Image *NOVEL_BACKGROUND[]; 19 | extern const Image *NOVEL_FOREGROUND[]; 20 | 21 | #endif -------------------------------------------------------------------------------- /rebuild.bat: -------------------------------------------------------------------------------- 1 | del %cd%\out\rom.bin 2 | 3 | python create_external_functions.py 4 | python convert_scripts.py 5 | c:\sgdk\bin\make -f c:\sgdk\makefile.gen 6 | 7 | c:\RetroArch-Win64\retroarch.exe -L c:\RetroArch-Win64\cores\blastem_libretro.dll out\rom.bin 8 | REM c:\sgdk\emu\blastem\blastem.exe %cd%\out\rom.bin 9 | 10 | REM C:\Users\kakoeimon\AppData\Roaming\RetroArch\RetroArch.exe -L C:\Users\kakoeimon\AppData\Roaming\RetroArch\cores\genesis_plus_gx_libretro.dll %cd%\out\rom.bin 11 | REM C:\Users\kakoeimon\AppData\Roaming\RetroArch\RetroArch.exe -L C:\Users\kakoeimon\AppData\Roaming\RetroArch\cores\blastem_libretro.dll %cd%\out\rom.bin 12 | -------------------------------------------------------------------------------- /create_external_functions.py: -------------------------------------------------------------------------------- 1 | h_out = open("inc\\novel_external_functions.h", "w") 2 | 3 | c_in = open("src\\novel_external_functions.c", "r") 4 | 5 | funcs = [] 6 | for line in c_in.readlines(): 7 | if line[:5] == "void ": 8 | f = line[5:].replace("(", "").replace(")", "").replace("{", "").strip() 9 | funcs.append(f) 10 | 11 | print(funcs) 12 | 13 | h_out.write("#ifndef H_NV_EXTERNAL_FUNCTIONS\n") 14 | h_out.write("#define H_NV_EXTERNAL_FUNCTIONS\n\n") 15 | 16 | h_out.write("#define NV_EXTERNAL_FUNCTIONS_NUM " + str(len(funcs)) + "\n\n") 17 | 18 | h_out.write("extern void (*nv_external_functions[])();\n\n") 19 | 20 | for f in funcs: 21 | h_out.write("void " + f + "();\n") 22 | 23 | h_out.write("\n#endif\n") -------------------------------------------------------------------------------- /inc/novel_variables.h: -------------------------------------------------------------------------------- 1 | #ifndef H_NOVEL_VARIABLES 2 | #define H_NOVEL_VARIABLES 3 | 4 | extern const int NOVEL_SAVE_CHECK_NUM; 5 | 6 | extern const int NOVEL_NUM_VARIABLES; 7 | extern const int NOVEL_NUM_GLOBAL_VARIABLES; 8 | extern int NOVEL_VARIABLES[]; 9 | 10 | 11 | 12 | //VARABLES 13 | #define NVAR_selected NOVEL_VARIABLES[0] 14 | #define NVAR_retfile NOVEL_VARIABLES[1] 15 | #define NVAR_retlabel NOVEL_VARIABLES[2] 16 | 17 | 18 | //FILES 19 | #define NFILE_main 0 20 | #define NFILE_inventory 1 21 | 22 | 23 | //LABELS 24 | #define NLABEL_main_make_a_choice 39 25 | 26 | 27 | 28 | 29 | //BACKGROUNDS 30 | #define NBG_black 0 31 | 32 | 33 | 34 | 35 | //FOREGROUNDS 36 | #define NFG_character 0 37 | 38 | 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /inc/novel_functions.h: -------------------------------------------------------------------------------- 1 | #ifndef H_NOVEL_FUNCTIONS 2 | #define H_NOVEL_FUNCTIONS 3 | 4 | #include "genesis.h" 5 | 6 | #define NOVEL_NO_MUSIC 1600 7 | 8 | typedef struct Novel_ { 9 | s32 position; 10 | u16 script_index; 11 | s16 back_index; 12 | s16 fore_index; 13 | s16 fore_pal; 14 | s16 music; 15 | s16 selected; 16 | s16 advance; 17 | s16 pause_menu; 18 | s16 pause_menu_pos; 19 | s16 pause_menu_selected; 20 | s16 fore_imgs[3]; 21 | s16 fore_pos[3][2]; 22 | } Novel; 23 | 24 | extern Novel NOVEL; 25 | 26 | void draw_int(int value, int x, int y); 27 | 28 | void novel_draw_background(int index, int fade_time); 29 | void novel_draw_foreground(int index, int x, int y); 30 | int draw_text(char *str); 31 | void clear_text(); 32 | 33 | #endif -------------------------------------------------------------------------------- /src/novel_external_functions.c: -------------------------------------------------------------------------------- 1 | #include "genesis.h" 2 | #include "novel_player.h" 3 | #include "novel_functions.h" 4 | #include "novel_scripts.h" 5 | #include "novel_images.h" 6 | #include "novel_sounds.h" 7 | #include "novel_variables.h" 8 | #include "novel_external_functions.h" 9 | 10 | void reset() { 11 | novel_reset(); 12 | } 13 | 14 | 15 | void fade_out() { 16 | PAL_fadeOutAll(60, FALSE); 17 | PAL_setColor(31, RGB24_TO_VDPCOLOR(0xffffff)); //Set the 31 color to white cause this is the color of the text 18 | } 19 | 20 | 21 | void ret_jump_store() { 22 | NVAR_retfile = NOVEL.script_index; 23 | NVAR_retlabel = NOVEL.position + 7; //Seven cause that's how long a jump is in bytes. 24 | } 25 | 26 | void ret_return() { 27 | NOVEL.script_index = NVAR_retfile; 28 | NOVEL.position = NVAR_retlabel; 29 | } 30 | 31 | void fr_clear() { 32 | novel_draw_background(NOVEL.back_index, 0); 33 | } -------------------------------------------------------------------------------- /create_image_res.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import glob 3 | import shutil 4 | import os 5 | 6 | if os.path.exists('out'): 7 | shutil.rmtree("out") 8 | 9 | ########################################## 10 | ini = open("novel.ini") 11 | magick_convert = ini.readline().replace("MAGICK_CONVERT_PATH ", "").replace("\n", "") 12 | bg_width = int(ini.readline().replace("BG_WIDTH ", "")) 13 | bg_height = int(ini.readline().replace("BG_HEIGHT ", "")) 14 | bg_top = int(ini.readline().replace("BG_TOP ", "")) 15 | text_top = int(ini.readline().replace("TEXT_TOP ", "")) 16 | text_bottom = int(ini.readline().replace("TEXT_BOTTOM ", "")) 17 | text_left = int(ini.readline().replace("TEXT_LEFT ", "")) 18 | text_right = int(ini.readline().replace("TEXT_RIGHT ", "")) 19 | compression = ini.readline().replace("COMPRESSION ", "").replace("\n", "") 20 | 21 | tmp_back = glob.glob('novel\\background\\**\\*.png', recursive=True) 22 | if (len(tmp_back) == 0): 23 | tmp_back = glob.glob('novel\\background\\**\\*.jpg', recursive=True) 24 | if (len(tmp_back) == 0): 25 | print("The project does not have backgrounds.") 26 | print("This way even the foregrounds will be ignored") 27 | exit() 28 | 29 | tmp_back = tmp_back[0] 30 | tmp_back = Image.open(tmp_back) 31 | img_scale = (bg_width * 8) / tmp_back.size[0] 32 | 33 | bg_left = int((40 - bg_width) / 2) 34 | 35 | ############################################# 36 | 37 | 38 | novel_res = open('res/novel_images_res.res', 'w') 39 | images_h = open('inc/novel_images.h', 'w') 40 | images_c = open('src/novel_images.c', 'w') 41 | 42 | images_h.write("#ifndef H_NOVEL_IMAGES\n") 43 | images_h.write("#define H_NOVEL_IMAGES\n\n") 44 | images_h.write("#include \"genesis.h\"\n") 45 | images_h.write("#include \"novel_images_res.h\"\n\n") 46 | 47 | images_h.write("#define NOVEL_TEXT_TOP " + str(text_top) + "\n") 48 | images_h.write("#define NOVEL_TEXT_BOTTOM " + str(text_bottom) + "\n") 49 | images_h.write("#define NOVEL_TEXT_LEFT " + str(text_left) + "\n") 50 | images_h.write("#define NOVEL_TEXT_RIGHT " + str(text_right) + "\n") 51 | images_h.write("#define NOVEL_TEXT_WIDTH " + str(text_right - text_left) + "\n\n") 52 | 53 | images_h.write("#define NOVEL_BG_WIDTH " + str(bg_width) + "\n") 54 | images_h.write("#define NOVEL_BG_HEIGHT " + str(bg_height) + "\n") 55 | images_h.write("#define NOVEL_BG_TOP " + str(bg_top) + "\n") 56 | images_h.write("#define NOVEL_BG_LEFT " + str(int(bg_left)) + "\n\n") 57 | 58 | 59 | images_h.write("extern const Image *NOVEL_BACKGROUND[];\n") 60 | images_h.write("extern const Image *NOVEL_FOREGROUND[];\n") 61 | 62 | images_h.write("\n#endif") 63 | 64 | 65 | images_c.write("#include \"novel_images.h\"\n\n") 66 | images_c.write("const Image *NOVEL_BACKGROUND[] = {\n") 67 | 68 | bg_num = 0 69 | for background in glob.glob('res\\background\\**\\*.png', recursive=True): 70 | bg_num +=1 71 | _id = "bg_" + str(bg_num) 72 | novel_res.write("IMAGE " + _id + " " + background.replace('res\\', "") + " " + compression + "\n") 73 | images_c.write("\t&" + _id + ",\n") 74 | images_c.write("};\n\n") 75 | novel_res.write("\n\n") 76 | 77 | images_c.write("const Image *NOVEL_FOREGROUND[] = {\n") 78 | foreground_size = [] 79 | fg_num = 0 80 | for foreground in glob.glob('res\\foreground\\**\\*.png', recursive=True): 81 | fg_num +=1 82 | _id = "fg_" + str(fg_num) 83 | novel_res.write("IMAGE " + _id + " " + foreground.replace('res\\', "") + " " + compression + "\n") 84 | images_c.write("\t&" + _id + ",\n") 85 | foreground_size.append(Image.open(foreground).size) 86 | images_c.write("};\n\n") 87 | 88 | novel_res.close() 89 | -------------------------------------------------------------------------------- /script_format.txt: -------------------------------------------------------------------------------- 1 | VNDS .scr file format: 2 | 3 | for any clarification on the usage of these, look at a .scr from another VN 4 | 5 | bgload: 6 | usage: bgload file [delay] 7 | looks in background/ for the image and draws it as the background 8 | control length of the delay in frames with background will be displayed before proceeding to the script (default 0) 9 | 10 | setimg: 11 | usage: setimg file x y 12 | looks in foreground/ for the image and draws it at the point (x,y) 13 | the x y values must be the MegaDrive values not the scaled ones. 14 | 15 | sound: 16 | usage: sound file times 17 | looks in sound/ for the file, loads it into memory(don`t do this with 18 | anything over a meg in size) and plays it X times. -1 for infinite 19 | looping. 20 | if file is ~, it stops any currently playing sound. 21 | 22 | music: 23 | usage: music file 24 | looks in sound/ for the file, 25 | 26 | music is expected to be in mp3 format 27 | if file is ~, it stops the music. 28 | 29 | text: 30 | usage: text string 31 | displays text to the screen. 32 | 33 | if string is !, it`ll make a blank text with an ! at the bottom right and require clicking to advance 34 | 35 | choice: 36 | usage: choice option1|option2|etc... 37 | displays choices on the screen 38 | 39 | when a choice is clicked, selected is set to the value of what was 40 | selected, starting at 1. 41 | use if selected == 1, etc to go off what was selected. 42 | 43 | ifchoice: 44 | usage: choice var1 == 1:option1|option2|etc... 45 | displays choices on the screen but if the condition is true. 46 | in this case if the variable var1 is 1 then option1 will be displayed. 47 | Condition and choice text are separated by a : 48 | 49 | when a choice is clicked, selected is set to the value of what was 50 | selected, starting at 1. 51 | use if selected == 1, etc to go off what was selected. 52 | 53 | setvar/gsetvar: 54 | usage: setvar modifier value 55 | sets a variable 56 | 57 | modifier: =, +. - 58 | setvar puts values into local save memory, to be kept in normal save files 59 | for things like character flags and such 60 | gsetvar sets variables in global.sav 61 | for things like cleared path flags 62 | prefix a variable with $ to use it in other commands: `text var is $var` 63 | {$var} can be used if needed to separate the variable from other text 64 | 65 | if/fi: 66 | usage: 67 | if x == 1 68 | commands 69 | fi 70 | conditional jump 71 | 72 | if true, it keeps reading. if false, it skips till it encounters a fi 73 | Note: left operand must be a variable, right may be either 74 | This is due to either redoing every script to put $ in front of the 75 | variables, or just making left not default to string if the 76 | variable doesnt exist. 77 | 78 | jump: 79 | usage: jump file.scr [label] 80 | looks in script/ for the .scr and starts reading off that. 81 | if label is specified, jumps to that label in the script 82 | 83 | delay: 84 | usage: delay X 85 | X being number of frames to hold, DS runs at 60 frames per second. 86 | 87 | random: // NOT IMPLEMENTED 88 | usage: random var low high 89 | set var to a number between low and high (inclusive) 90 | 91 | label/goto: 92 | usage: 93 | label name 94 | goto name 95 | 96 | a goto command will search the current script for a label with the same 97 | name and start the script from that part 98 | 99 | cleartext: 100 | usage: cleartext [type] 101 | clears text from the screen. 102 | 103 | if no type is given, it`ll make enough blank lines to fill the display 104 | if type is !, it`ll completely clear the text buffer (including history) 105 | 106 | -------------------------------------------------------------------------------- /src/novel_functions.c: -------------------------------------------------------------------------------- 1 | #include "novel_functions.h" 2 | #include "novel_scripts.h" 3 | #include "novel_images.h" 4 | #include "novel_variables.h" 5 | 6 | #define NOVEL_FORE_PAL_START 1 7 | 8 | Novel NOVEL; 9 | 10 | void draw_int(int value, int x, int y) { 11 | char str[4]; 12 | intToStr(value, str, 4); 13 | VDP_drawText(str, x, y); 14 | } 15 | 16 | void clear_text() { 17 | //static s16 novel_text_height = NOVEL_TEXT_BOTTOM - NOVEL_TEXT_TOP; 18 | VDP_clearPlane(WINDOW, TRUE); 19 | //VDP_clearTextArea(NOVEL_TEXT_LEFT, NOVEL_TEXT_TOP, NOVEL_TEXT_WIDTH, novel_text_height); 20 | } 21 | 22 | void novel_draw_background(int index, int fade_time) { 23 | const Image *img = NOVEL_BACKGROUND[index]; 24 | 25 | //VDP_clearPlane(BG_B, FALSE); 26 | 27 | 28 | NOVEL.fore_pal = NOVEL_FORE_PAL_START; 29 | NOVEL.fore_index = TILE_USER_INDEX + img->tileset->numTile; 30 | for (int i = 0; i < 3; i++) { 31 | NOVEL.fore_imgs[i] = MAX_S16; 32 | } 33 | VDP_clearPlane(BG_A, TRUE); 34 | SYS_doVBlankProcess(); 35 | 36 | SYS_doVBlankProcess(); 37 | if (index != NOVEL.back_index) { 38 | 39 | SYS_doVBlankProcess(); 40 | SYS_doVBlankProcess(); 41 | //PAL_setPalette(PAL0, palette_black, CPU); 42 | 43 | VDP_loadTileSet(img->tileset,TILE_USER_INDEX, DMA_QUEUE); 44 | 45 | //for (int i =0; i < 60; i++) SYS_doVBlankProcess(); 46 | 47 | 48 | NOVEL.back_index = index; 49 | VDP_setTileMapEx(BG_B, img->tilemap, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, TILE_USER_INDEX), NOVEL_BG_LEFT, NOVEL_BG_TOP, 0, 0, NOVEL_BG_WIDTH, NOVEL_BG_HEIGHT, DMA_QUEUE); 50 | PAL_setPalette(PAL0, img->palette->data, DMA_QUEUE); 51 | SYS_doVBlankProcess(); 52 | } 53 | 54 | for (int i = 0; i < fade_time; i++) { 55 | 56 | SYS_doVBlankProcess(); 57 | if (NOVEL.advance) { 58 | break; 59 | } 60 | } 61 | SYS_doVBlankProcess(); 62 | SYS_doVBlankProcess(); 63 | 64 | } 65 | 66 | void novel_draw_foreground(int index, int x, int y) { 67 | const Image *img = NOVEL_FOREGROUND[index]; 68 | int s_x = img->tilemap->w; 69 | int s_y = img->tilemap->h; 70 | SYS_doVBlankProcess(); 71 | VDP_loadTileSet(img->tileset,NOVEL.fore_index, DMA_QUEUE); 72 | SYS_doVBlankProcess(); 73 | PAL_setPalette(NOVEL.fore_pal, img->palette->data, DMA_QUEUE); 74 | SYS_doVBlankProcess(); 75 | VDP_setTileMapEx(BG_A, img->tilemap, TILE_ATTR_FULL(NOVEL.fore_pal, FALSE, FALSE, FALSE, NOVEL.fore_index), x, y, 0, 0, s_x, s_y, DMA_QUEUE); 76 | //those 3 FORE IMGS are for the save function 77 | NOVEL.fore_imgs[NOVEL.fore_pal-1] = index; 78 | NOVEL.fore_pos[NOVEL.fore_pal-1][0] = x; 79 | NOVEL.fore_pos[NOVEL.fore_pal-1][1] = y; 80 | NOVEL.fore_pal++; 81 | if (NOVEL.fore_pal > 3) { 82 | NOVEL.fore_pal = NOVEL_FORE_PAL_START; 83 | } 84 | NOVEL.fore_index += img->tileset->numTile; 85 | //VDP_loadFont(&font_default, DMA); 86 | SYS_doVBlankProcess(); 87 | } 88 | 89 | 90 | int draw_line(char *str, int draw_pos) { 91 | u8 line[NOVEL_TEXT_WIDTH]; 92 | int wanted_length = NOVEL_TEXT_WIDTH; 93 | if (strlen(str) > NOVEL_TEXT_WIDTH) { 94 | while( str[wanted_length] != ' ' && wanted_length != 0) { 95 | wanted_length--; 96 | } 97 | if (wanted_length == 0) { 98 | wanted_length = NOVEL_TEXT_WIDTH; 99 | } 100 | } 101 | strncpy(line, str, wanted_length++); 102 | 103 | VDP_drawText(line, NOVEL_TEXT_LEFT, draw_pos); 104 | 105 | 106 | if (draw_pos >= NOVEL_TEXT_BOTTOM - 1 || strlen(str) <= NOVEL_TEXT_WIDTH) { 107 | draw_pos = NOVEL_TEXT_TOP - 1; 108 | SYS_doVBlankProcess(); 109 | if (NOVEL.pause_menu) return 0; 110 | 111 | while (NOVEL.advance != TRUE) 112 | { 113 | 114 | SYS_doVBlankProcess(); 115 | if (NOVEL.pause_menu) return 0; 116 | u16 joy = JOY_readJoypad(JOY_1); 117 | if (joy & BUTTON_C) { 118 | NOVEL.advance = TRUE; 119 | } 120 | } 121 | NOVEL.advance = FALSE; 122 | NOVEL.selected = FALSE; 123 | clear_text(); 124 | } 125 | if (strlen(str) > NOVEL_TEXT_WIDTH) { 126 | return draw_line(&str[wanted_length], draw_pos+1); 127 | } 128 | return 1; 129 | } 130 | 131 | int draw_text(char *str) { 132 | if (str[0] == '@' || str[0] == '!' || str[0] == '~') { 133 | int draw_pos = NOVEL_TEXT_TOP - 1; 134 | if (str[0] == '!') { 135 | char tmp_str[NOVEL_TEXT_WIDTH]; 136 | for (int i = 0; i < NOVEL_TEXT_WIDTH; i++) { 137 | tmp_str[i] = ' '; 138 | } 139 | tmp_str[NOVEL_TEXT_WIDTH-2] = '!'; 140 | if (!draw_line(tmp_str, NOVEL_TEXT_BOTTOM - 2)) { 141 | return 0; 142 | } 143 | } else { 144 | if (!draw_line(str+1, NOVEL_TEXT_TOP)) { 145 | return 0; 146 | } 147 | } 148 | 149 | } else if (!draw_line(str, NOVEL_TEXT_TOP)) { 150 | return 0; 151 | } 152 | return strlen(str) + 2; 153 | 154 | } 155 | -------------------------------------------------------------------------------- /create_sound_res.py: -------------------------------------------------------------------------------- 1 | from ctypes import sizeof 2 | import glob 3 | import shutil 4 | from pydub import AudioSegment 5 | from pydub.utils import mediainfo 6 | import pathlib 7 | import soundfile as sf 8 | 9 | import os 10 | 11 | ############################################### 12 | ini = open("novel.ini") 13 | magick_convert = ini.readline().replace("MAGICK_CONVERT_PATH ", "").replace("\n", "") 14 | bg_width = int(ini.readline().replace("BG_WIDTH ", "")) 15 | bg_height = int(ini.readline().replace("BG_HEIGHT ", "")) 16 | bg_top = int(ini.readline().replace("BG_TOP ", "")) 17 | text_top = int(ini.readline().replace("TEXT_TOP ", "")) 18 | text_bottom = int(ini.readline().replace("TEXT_BOTTOM ", "")) 19 | text_left = int(ini.readline().replace("TEXT_LEFT ", "")) 20 | text_right = int(ini.readline().replace("TEXT_RIGHT ", "")) 21 | compression = ini.readline().replace("COMPRESSION ", "").replace("\n", "") 22 | save_check = ini.readline().replace("SAVE_CHECK ", "").replace("\n", "") 23 | sound_drv = ini.readline().replace("SOUND_DRV ", "").replace("\n", "").strip() 24 | ################################################ 25 | 26 | exec('if os.path.isdir("res\wav"):\n\tshutil.rmtree("res\wav")\nos.mkdir("res\wav")') 27 | 28 | 29 | 30 | sound_res = open("res\\novel_sounds_res.res", 'w') 31 | 32 | 33 | sound_res.write("NEAR\n") 34 | 35 | sounds_c = open("src/novel_sounds.c", 'w') 36 | sounds_c.write("#include \"novel_sounds.h\"\n\n") 37 | 38 | sounds_c.write("void novel_stop_sound() {\n") 39 | if sound_drv == "XGM": 40 | sounds_c.write("\tXGM_stopPlayPCM(SOUND_PCM_CH2);\n") 41 | elif sound_drv == "2ADPCM": 42 | sounds_c.write("\tSND_stopPlay_2ADPCM(SOUND_PCM_CH2);\n") 43 | sounds_c.write("\tSND_stopPlay_2ADPCM(SOUND_PCM_CH1);\n") 44 | elif sound_drv == "4PCM": 45 | sounds_c.write("\tSND_stopPlay_4PCM(SOUND_PCM_CH2);\n") 46 | 47 | sounds_c.write("}\n\n") 48 | 49 | sounds_c.write("void novel_stop_music() {\n") 50 | if sound_drv == "XGM": 51 | sounds_c.write("\tXGM_stopPlay();\n") 52 | elif sound_drv == "2ADPCM": 53 | sounds_c.write("\tSND_stopPlay_2ADPCM(SOUND_PCM_CH1);\n") 54 | elif sound_drv == "4PCM": 55 | sounds_c.write("\tSND_stopPlay_4PCM(SOUND_PCM_CH1);\n") 56 | 57 | sounds_c.write("}\n\n") 58 | 59 | 60 | ############### MUSIC 61 | 62 | if os.path.isdir("res\\music"): 63 | shutil.rmtree("res\\music") 64 | os.mkdir("res\\music") 65 | 66 | 67 | have_music = False 68 | if os.path.isdir("novel\\music"): 69 | 70 | if sound_drv == "XGM": 71 | for music in glob.glob("novel\\music\\**\\*.vgm", recursive=True): 72 | have_music = True 73 | music = os.path.basename(music) 74 | alias = music.replace(".", "_") 75 | sounds_c.write("void play_music_" + alias + "() {\n") 76 | sounds_c.write("\tXGM_startPlay_FAR(" + alias + ", sizeof(" + alias + "));\n") 77 | sounds_c.write("}\n\n") 78 | elif sound_drv == "2ADPCM" or sound_drv == "4PCM": 79 | music_num = 0 80 | for music in glob.glob("novel\\music\\**\\*.wav", recursive=True): 81 | have_music = True 82 | music = os.path.basename(music) 83 | music_num +=1 84 | alias = "music_" + str(music_num) 85 | sounds_c.write("void play_" + alias + "() {\n") 86 | if sound_drv == "2ADPCM": 87 | sounds_c.write("\tSND_startPlay_2ADPCM(" + alias + ", sizeof(" + alias + "), SOUND_PCM_CH1, 1);\n") 88 | else: 89 | sounds_c.write("\tSND_startPlay_4PCM(" + alias + ", sizeof(" + alias + "), SOUND_PCM_CH1, 1);\n") 90 | sounds_c.write("}\n\n") 91 | 92 | if not have_music: 93 | sounds_c.write("void play_null_music() {}\n") 94 | 95 | sounds_c.write("void (*NOVEL_PLAY_MUSIC[])() = {\n") 96 | 97 | if os.path.isdir("novel\\music"): 98 | 99 | if sound_drv == "XGM": 100 | 101 | for music in glob.glob("novel\\music\\**\\*.vgm", recursive=True): 102 | print(music) 103 | shutil.copyfile(music, music.replace("novel\\music", "res\\music")) 104 | music = os.path.basename(music) 105 | alias = music.replace(".", "_") 106 | sound_res.write("XGM " + alias + " music/" + music + "\n" ) 107 | sounds_c.write("play_music_" + alias + ",\n") 108 | elif sound_drv == "2ADPCM" or sound_drv == "4PCM": 109 | music_num = 0 110 | for music in glob.glob("novel\\music\\**\\*.wav", recursive=True): 111 | print(music) 112 | outmusic = music.replace("novel\\music", "res\\music") 113 | pathlib.Path(os.path.dirname(outmusic)).mkdir(parents=True, exist_ok=True) 114 | shutil.copyfile(music, outmusic) 115 | music = outmusic.replace("res\\music\\", "", 1) 116 | music_num +=1 117 | alias = "music_" + str(music_num) 118 | sound_res.write("WAV " + alias + " music/" + music + " " + sound_drv + "\n" ) 119 | sounds_c.write("play_" + alias + ",\n") 120 | 121 | if not have_music: 122 | sounds_c.write("play_null_music\n") 123 | else: 124 | sound_res.write("\n\n\n") 125 | sounds_c.write("};\n\n") 126 | 127 | 128 | 129 | ######################## WAV 130 | wav_files = {} 131 | for wav in glob.glob("novel\\wav\\**\\*.wav", recursive=True): 132 | wav_files[wav.replace("novel\\", "", 1)] = "wav_" + str(len(wav_files)) 133 | #continue 134 | outwav = wav.replace("novel\\wav", "res\\wav") 135 | pathlib.Path(os.path.dirname(outwav)).mkdir(parents=True, exist_ok=True) 136 | shutil.copyfile(wav, outwav) 137 | 138 | 139 | have_sounds = False 140 | for wav in wav_files.keys(): 141 | have_sounds = True 142 | alias = wav_files[wav] 143 | sounds_c.write("void play_" + alias + "(int v) {\n") 144 | 145 | sound_res.write("WAV " + alias + " " + wav + " " + sound_drv + " \n" ) 146 | 147 | if sound_drv == "XGM": 148 | sounds_c.write("\tnovel_stop_sound();\n") 149 | sounds_c.write("\tSYS_doVBlankProcess();\n") 150 | sounds_c.write("\tXGM_setPCM(68, FAR_SAFE(&" + alias + ", sizeof("+ alias + "))," + "sizeof("+ alias + "));\n") 151 | sounds_c.write("\tSYS_doVBlankProcess();\n") 152 | sounds_c.write("\tXGM_startPlayPCM(68,1,SOUND_PCM_CH2);\n") 153 | 154 | elif sound_drv == "2ADPCM": 155 | #sounds_c.write("\tSND_startPlay_2ADPCM(" + alias + ", sizeof(" + alias + "), SOUND_PCM_CH2, 0);\n") 156 | #sounds_c.write("\tnovel_stop_sound();\n") 157 | sounds_c.write("\tSYS_doVBlankProcess();\n") 158 | sounds_c.write("\tif (v != 1) SND_startPlay_2ADPCM( FAR_SAFE(" + alias + ", sizeof(" + alias + ")), sizeof(" + alias + "), SOUND_PCM_CH2, 0);\n") 159 | sounds_c.write("\tif (v == 1) SND_startPlay_2ADPCM( FAR_SAFE(" + alias + ", sizeof(" + alias + ")), sizeof(" + alias + "), SOUND_PCM_CH1, 1);\n") 160 | #sounds_c.write("\tSND_startPlay_2ADPCM( FAR_SAFE(" + alias + ", sizeof(" + alias + ")), sizeof(" + alias + "), SOUND_PCM_CH2, v);\n") 161 | #sounds_c.write("\tSYS_doVBlankProcess();\n") 162 | elif sound_drv == "4PCM": 163 | #sounds_c.write("\tSND_startPlay_4PCM(" + alias + ", sizeof(" + alias + "), SOUND_PCM_CH2, 0);\n") 164 | sounds_c.write("\tnovel_stop_sound();\n") 165 | sounds_c.write("\tSYS_doVBlankProcess();\n") 166 | sounds_c.write("\tSND_startPlay_4PCM( FAR_SAFE(" + alias + ", sizeof(" + alias + ")), sizeof(" + alias + "), SOUND_PCM_CH2, v);\n") 167 | sounds_c.write("\tSYS_doVBlankProcess();\n") 168 | else: 169 | print("---------------------- ERROR --------------------") 170 | print(" The sound driver is not right in the novel.ini file") 171 | print(" Use 2ADPCM or XGM") 172 | print("--------------------------------------------------") 173 | sounds_c.write("}\n") 174 | 175 | if not have_sounds: 176 | sounds_c.write("void play_null_sound(int v) {}\n") 177 | 178 | sounds_c.write("void (*NOVEL_PLAY_SOUND[])() = {\n") 179 | for wav in wav_files.keys(): 180 | alias = "play_" + wav_files[wav] 181 | sounds_c.write(alias + ",\n") 182 | if not have_sounds: 183 | sounds_c.write("play_null_sound\n") 184 | else: 185 | sound_res.write("\n\n\n") 186 | sounds_c.write("};\n\n") 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /tutorial.md: -------------------------------------------------------------------------------- 1 | # VNMD TUTORIAL 2 | 3 | Let's create a vary simple VNMD game that will explore all the VNMD functionality. 4 | 5 | Open the file novel\script\main.scr 6 | 7 | This is your main script it is the script that VNMD requires and the first script your game will run. 8 | 9 | Delete everything and write. 10 | 11 | bgload black 12 | this will load and display the black.jpg located in novel\background 13 | 14 | novel\background is the place where you will get all your backgrounds. 15 | 16 | black.jpg is special, because it contains the size of the images and it is going to be used by the python scripts to know the scale of the foregrounds. The backgrounds will be scaled to fit the size defined in the novel.ini but for the foregrounds we need another way and this way is the size of the black.jpg 17 | 18 | Now write: 19 | 20 | text Hello VNMD. 21 | this is just a text that will be displayed on scree. 22 | 23 | Try it out. 24 | Type in the terminal 25 | 26 | make_all.bat 27 | 28 | This batch file will run the the python scripts in the right order to build the game. 29 | But you will have to edit it so it contains the paths to the emulator etc. 30 | For example 31 | change line 32 | 33 | c:\RetroArch-Win64\retroarch.exe -L c:\RetroArch-Win64\cores\blastem_libretro.dll out\rom.bin 34 | 35 | with the paths and the commands of the emulator of your choice to run the out\rom.bin 36 | 37 | If everything goes as expected then the emulator will run and you will see a black screen at the top and at the bottom text writing 38 | 39 | Hello VNMD 40 | 41 | Now create another script named inventory.scr in the directory novel\script 42 | 43 | write there : 44 | 45 | text You have nothing. 46 | 47 | return to the main file and write at the bottom. 48 | 49 | jump inventory.scr 50 | 51 | 52 | 53 | This will make the game to jump to the inventory script. 54 | 55 | You can also just write 56 | 57 | jump inventory 58 | 59 | VNMD even thought it started as a VNDS clone, it contains extra functionality that it is not VNDS compatible. 60 | 61 | In this case it does not requires the file extensions. 62 | 63 | Run make_all and see that it jumps to the inventory file as you will get after the Hello VNMD the text You have nothing. 64 | 65 | Let's make it a choice to jump to the inventory. 66 | 67 | Delete everything in the main script and write. 68 | 69 | bgload black 60 70 | text It was too dark to see anything. 71 | label make_a_choice 72 | choice Look around.|Inventory 73 | if selected == 1: 74 | text He tried to see, but darness prevailed. 75 | goto make_a_choice 76 | fi 77 | if selected == 2: 78 | jump inventory 79 | text So you have nothing. 80 | fi 81 | 82 | make_all 83 | 84 | Many new things were introduced here but let's examine them one by one. 85 | 86 | label is just that a label inside the script. The purpose of a label is to mark a position inside the script and use the label to jump there with the goto command or the jump command. 87 | 88 | choice is the way to create options in the game. The vertical column | separates the options from each other. 89 | 90 | if is used to make comparisons and to create roots and actions in the game. In this example it checks if the variable selected is equal to 1. 91 | selected is var that exists in every VNMD game and is the var that changes when choices are made. So 1 is for the first choice and 2 is for the second one. 92 | 93 | fi is where the if closes. Every if must contains a fi afterwards to know where the if branch is ending. You can have nested ifs. Also the : is ignored when it is at the end of the line. This is a VNMD only future cause it makes branching much easier as the vscode reacts like it is a python script and you can have easy tabbing this way. 94 | 95 | goto is used to jump to a label of the file. So when this line is reached the game will go to the label line and continue from there. In this case we will have to make the choice again. 96 | 97 | 98 | make_all and play the game. 99 | You will see that if you go to the inventory, that even without a jump command the game returns to the main. This is cause if a script comes to an end without a jump then the game resets. We can change that with a jump command. 100 | 101 | Go to the inventory.scr and write at the bottom 102 | 103 | jump main make_a_choice 104 | 105 | This will make the script to jump to the main.scr but to the label make_a_choice. So if you run this you will not see the text It was too dark to see anything. cause the game will not reset but will return to the main and to the label specified. 106 | 107 | We do not want to make the inventory to return to the main every time so we can change this by using string vars. 108 | String vars only function with labels and script files. 109 | so at the top of the main write 110 | 111 | setvar retfile = "main.scr" 112 | setvar retlabel = make_a_choice 113 | 114 | script vars like retfile in this case must contain a script file inside "" otherwise it will not work. 115 | labels can be putted inside a var but only the label in the same script. 116 | 117 | Now go to the inventory file and change the jump line to 118 | 119 | jump $retfile $retlabel 120 | 121 | this way the jump will jump to the file and label that the variables contain. 122 | 123 | This way you can return to any file from the inventory. 124 | 125 | make_all and see that it is like before. 126 | 127 | If you use this for an inventory soon you will realize that setting retfile and relabel and the labels is quite boring. 128 | For this reason the external function ret_jump_store can be use. 129 | 130 | In the main.scr write above the jump inventory this: 131 | 132 | ret_jump_store 133 | 134 | This is an external function that can be found in scr\novel_external_functions.c 135 | This will store the script file in retfile var and the position of the novel plus the size of the jump command to the retlabel. This means that when the jump in the inventory runs it will return immediately after the jump in the main. 136 | 137 | make_all and test. 138 | 139 | Now at the bottom of main.scr wite 140 | 141 | setimg character 128 0 142 | 143 | this will display the character.png located in the novel\foreground and it will have x = 180 and y = 0 144 | 145 | Then write 146 | 147 | text Ha... you have nothing! 148 | bgload black 149 | text and it is dark again. 150 | 151 | 152 | This will display the text and then display the black.png again and the other text. 153 | 154 | If you run this you will see that when the black.png is loaded and displayed again the character will disappear. 155 | 156 | This is cause this is the way VNDS functions. 157 | Every background removes every foreground. 158 | 159 | But because this is error prone VNMD have another external function for this. 160 | Replace bgload black with fr_clear 161 | 162 | This will just reload the background for you. 163 | 164 | I think that this along with the script_format.txt covers the functionality of the VNMD but there is something else that I believe can be very helpful. 165 | 166 | ## Create string vars for your locations. 167 | 168 | If you are going to make a visual novel like an choose you own adventure game then you are all ready good to go. 169 | 170 | But if you want to create something like a JAdventure (like Snatchers) then you will need and a little bit of a trick to make your life easier. 171 | 172 | In JAdventures many times you move from one place to another and as the progress goes on several events are happening in those places. 173 | 174 | One way to do this is by just using the if branching but soon becomes clear that writing a game like this is quite more difficult from what it seems. 175 | 176 | So one way of not getting to many nested ifs to accomplish this is by setting string variables that contain the the files for each location. 177 | 178 | For example let's say we have a place that it is supposed to be the office. 179 | At the main.scr write something like this. 180 | 181 | setvar office_file = "office.scr" 182 | 183 | Put inside the office.scr what ever you think is the default office location. 184 | 185 | Then when you need an event to be placed in there just change the office_file with another file that contains the event. 186 | 187 | Then use the jump when ever you want to go to the office like this 188 | 189 | jump $office_file 190 | 191 | You can also create a movement.scr and have the basic traveling around organized in there. 192 | 193 | For example: 194 | 195 | label corridor 196 | choice Enter the Office|Go outside 197 | if selected == 1: 198 | jump $office_file 199 | fi 200 | if selected == 2: 201 | jump $outside_file 202 | fi 203 | 204 | This way when you are writing the corridor file and you want the movement choices for the corridor to be displayed you can just write 205 | 206 | jump movement corridor 207 | 208 | Simple tricks but helped me a lot when I was making the Mansion of Sadness. 209 | 210 | Hope to hear back from you and play your games. 211 | Good luck. 212 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # VNMD 2 | 3 | **VNMD** is a tool for converting [VNDS](https://github.com/BASLQC/vnds) Visual Novels to a rom (.bin) to be used on **Sega Mega Drive** thanks to [SGDK](https://github.com/Stephane-D/SGDK). 4 | 5 | 6 | 7 | ## Setup 8 | 9 | This works only on Windows. 10 | 11 | First you will need to install [SGDK](https://github.com/Stephane-D/SGDK/releases/tag/v1.70) (version this repo used is 1.70 ) Probably it will be best if you instal SGDK at c:\sgdk 12 | 13 | Next you will need [Python3](https://www.python.org/downloads/). Probably you already have this so check it in your Command Prompt by typing: python --version. 14 | 15 | You will also need for Python [PILLOW](https://pillow.readthedocs.io/en/stable/installation.html) , [SoundFile](https://pypi.org/project/soundfile/) and [pydub](https://pypi.org/project/pydub/). 16 | Use pip commands to install them. 17 | 18 | 19 | 20 | Then Optionaly but for much better quality, install [ImageMagick](https://imagemagick.org/script/download.php) and when you install check the convert legacy. 21 | 22 | Last you will need a VNDS visual novel the list is [here](https://github.com/BASLQC/vnds/wiki/List-of-VNDS-Visual-Novels) 23 | * Prefer to download SD versions of the Novels. 24 | * Warning : Many Novels in this list are broken. Some contain Japanese characters or not standard character, others are just realy bad ports. For testing purposes I suggest to download the "The Best Eroge Ever" 25 | 26 | 27 | Extract this repo to a dir. inside this dir create a novel directory and extract the novel. 28 | Inside the novel dir you will have directories named background, foreground and scr among others. 29 | Some novels have those directories zipped unzip them. 30 | 31 | Note 32 | * all novel extractions must be made without creating a directory with the name of the zip. 33 | 34 | Last open the novel.ini 35 | ```` 36 | MAGICK_CONVERT_PATH C:/Program Files/ImageMagick-7.1.0-Q16-HDRI/convert.exe 37 | BG_WIDTH 32 38 | BG_HEIGHT 24 39 | BG_TOP 0 40 | TEXT_TOP 24 41 | TEXT_BOTTOM 28 42 | TEXT_LEFT 2 43 | TEXT_RIGHT 38 44 | COMPRESSION APLIB 45 | SAVE_CHECK 1123 46 | SOUND_DRV 2ADPCM 47 | ```` 48 | If you installed ImageMagick find the convert.exe that it was installed with your ImageMagick and replace the full file path at MAGICK_CONVERT_PATH with yours. Additionally you can leave the path empty or pointing not to a file to use just PILLOW for the convertion of the graphics. But Image Magick gives much better quality. 49 | 50 | Now you are ready to convert. 51 | 52 | ## Convert a novel 53 | Open a command prompt and navigate to the dir you unziped this repo. 54 | run 55 | ```` 56 | python convert_images.py 57 | ```` 58 | This will create a res dir and will convert the images of the Novel. 59 | This will take some time if the novel is big. 60 | 61 | Then run 62 | ```` 63 | python create_image_res.py 64 | ```` 65 | This will create the .res files for SGDK and some C files that will be needed for the rom. 66 | 67 | Then run. 68 | ```` 69 | python convert_scripts.py 70 | ```` 71 | This will convert the scripts to binaries and will create .res files and C files. 72 | Also with this command you can fins problems to a broken novel eaisier. 73 | 74 | If you got no errors till now. 75 | 76 | Run. 77 | ```` 78 | build.bat 79 | ```` 80 | This will build everything to the final rom 81 | 82 | 83 | ### Warning 84 | If the rom.bin is bigger than 4 MB then you will have to compile the SGDK to force it to use Bank Switching otherwise MD will crash. 85 | 86 | So if this is the case you will have to go to the directory you installed SGDK and inside the inc directory locate and open the file config.h 87 | in this file find this line 88 | ```` 89 | #define ENABLE_BANK_SWITCH 0 90 | ```` 91 | and change it to 92 | ```` 93 | #define ENABLE_BANK_SWITCH 1 94 | ```` 95 | then run from the SGDK's dir this file 96 | ```` 97 | build_lib.bat 98 | ```` 99 | The opposite must be made if Bank Switching is enabled and the rom is less than 4MB 100 | 101 | ## novel.ini 102 | The novel.ini is like this 103 | ```` 104 | MAGICK_CONVERT_PATH C:/Program Files/ImageMagick-7.1.0-Q16-HDRI/convert.exe 105 | BG_WIDTH 32 106 | BG_HEIGHT 24 107 | BG_TOP 0 108 | TEXT_TOP 24 109 | TEXT_BOTTOM 28 110 | TEXT_LEFT 2 111 | TEXT_RIGHT 38 112 | COMPRESSION APLIB 113 | SAVE_CHECK 1123 114 | SOUND_DRV 2ADPCM 115 | ```` 116 | * MAGICK_CONVERT_PATH is the path to the convert.exe if the path does not point to a file PILLOW will be used to convert the Images. 117 | * BG_WIDTH is the width you want the image to be in tiles (one tile is 8px) 118 | * BG_HEIGHT is the height you want the image to be in tiles (one tile is 8px) 119 | * BG_TOP where the image is going to be drawn and this is the top part (one tile is 8px) 120 | * TEXT_TOP from where your text is going to be drawn in tiles (one tile is 8px) 121 | * TEXT_BOTTOM the bottom of the text in tiles (one tile is 8px) 122 | * TEXT_LEFT the left side of the text in tiles (one tile is 8px) 123 | * TEXT_RIGHT the left side of the text in tiles (one tile is 8px) 124 | * COMPRESSION the compresion SGDK is going to use for the images. The options are. 125 | * * NONE no compression 126 | * * FAST fast but sometimes buggy. Avoid. 127 | * * APLIB slow but good compression. 128 | * SAVE_CHECK This is just a number to help the program with the saving option change it to what ever you want as long it is bigger than 0 and smaller than 32767 also it may be better to use a number with more than two digits. 129 | * SOUND_DRV as sound driver you want to use. Consider reading the SGDK info about this. Also you have to put the sound fx in a directory name wav and the music in a directory name music. The options are. 130 | * * 2ADPCM 131 | * * 4PCM 132 | * * XGM 133 | * 134 | 135 | # String Vars 136 | * VNDS can have string vars that can be used inside the text or as vars that hold script files or labels 137 | * VNMD can have string vars but you cannot use them inside the text. 138 | * String vars can be script files or labels (labels can be only labels of the working file) 139 | 140 | setvar retfile = "office.scr" (you can add any existing .scr file) 141 | jump $retfile (when you jump with string vars you can only use string vars for labels or nothing) 142 | 143 | or 144 | 145 | setvar retfile2 = "main.scr" 146 | setvar retlabel2 = "second_day" (this will work only if it is written in the main.scr and the main.scr have a label second_day) 147 | jump $retfile2 $retlabel2 148 | 149 | String vars can be used and inside an if but must be a script file and a right hand value 150 | if myfile == "sgdk_rocks.scr": 151 | text SGDK ROCKS! 152 | fi 153 | 154 | # Extra Commans 155 | ## ifcoice 156 | * ifchoice is a new command that exists only in VNMD and not in VNDS. 157 | it is similar to choice but it performs checks to make the choices visible. 158 | 159 | ifchoice knows: I know about the key| knows == 0: Where is the key|Nevermind 160 | explanation: 161 | 162 | "knows" is a variable that was declared with setvar 163 | ifchoice will check variables before the : and will display the choice only if the comparison is valid 164 | for example if "knows" is 0 165 | "I know about the key" is not going to be displayed (when checking without a symbol it is like you written != 0) 166 | "Where is the key" is going to be displayed 167 | "Nevermind" is going to be displayed every time cause there is no check for it. 168 | 169 | You can use all the comparisons. like == , != , > , < , >= , <= and you can use variables for the right hand too. 170 | 171 | String vars can be used here too also strings as a right hand value and a script file 172 | 173 | ifchoice refile == "cabinet.scr":Try the Key|Back 174 | 175 | Next you can use selected like it was a simple choice command. 176 | 177 | 178 | 179 | # External Functions 180 | * External functions are functions the user create and can be used in the scripts. 181 | 182 | The external functions are located in scr/novel_external_functions.c 183 | A simple example is the fade_out function : 184 | 185 | void fade_out() { 186 | PAL_fadeOutAll(60, FALSE); 187 | PAL_setColor(31, RGB24_TO_VDPCOLOR(0xffffff)); 188 | } 189 | This function located in scr/novel_external_functions.c can be used in the script by just writing fade_out in the script. 190 | External functions are just void functions and take no arguments. 191 | 192 | * The Novel variables, script files, label etc. are exposed to the user by macros. 193 | * * Variables. 194 | * Every variable created in the script by the command setvar can be found in C by using the name of the var with the prefix NVAR_. 195 | * e.g. the var selection is in C as NVAR_selection 196 | * e.g. a var created in script with the command setvar points = 0 can be found as NVAR_points 197 | * * Script Files 198 | * Every Script file can be found in C with the prefix NFILE_ 199 | * e.g. the main.scr is NFILE_main 200 | * * Labels 201 | * Labels in the script files can be found with the prefix NLABEL_ plus the name of the script the label is on and the label name. 202 | * e.g. a label named start in the script main.scr is NLABAL_main_start 203 | 204 | All those macros but and the header file for the novel_external_functions.c are created by running the python script create_external_functions.py 205 | Be sure to call this script every time you need access to a newly created var, script file or label. 206 | # So that's all... good luck in converting or creating new games. 207 | Many VNDS Novels require work to make them run. 208 | 209 | # Updating SGDK 210 | If you update SGDK be sure to delete the boot dir inside the scr dir, otherwise the project will not compile. 211 | 212 | # Other Stuff 213 | You can find more vnmd stuff at https://github.com/kakoeimon/VNMD_Stuff 214 | There you can find and a simple VNMD language extension for the VSCode. 215 | It will highlight basic VNDS commands and make writing a little bit easier. 216 | 217 | ## TODO 218 | * Add random command 219 | * Create a tutorial. 220 | 221 | -------------------------------------------------------------------------------- /convert_images.py: -------------------------------------------------------------------------------- 1 | from genericpath import isfile 2 | import os 3 | from sre_parse import FLAGS 4 | import subprocess 5 | import glob 6 | from PIL import Image 7 | import sys 8 | import pathlib 9 | 10 | dithering = True 11 | 12 | ########################################## 13 | ini = open("novel.ini") 14 | magick_convert = ini.readline().replace("MAGICK_CONVERT_PATH ", "").replace("\n", "") 15 | bg_width = int(ini.readline().replace("BG_WIDTH ", "")) 16 | bg_height = int(ini.readline().replace("BG_HEIGHT ", "")) 17 | bg_top = int(ini.readline().replace("BG_TOP ", "")) 18 | text_top = int(ini.readline().replace("TEXT_TOP ", "")) 19 | text_bottom = int(ini.readline().replace("TEXT_BOTTOM ", "")) 20 | text_left = int(ini.readline().replace("TEXT_LEFT ", "")) 21 | text_right = int(ini.readline().replace("TEXT_RIGHT ", "")) 22 | compression = ini.readline().replace("COMPRESSION ", "").replace("\n", "") 23 | save_check = ini.readline().replace("SAVE_CHECK ", "").replace("\n", "") 24 | sound_drv = ini.readline().replace("SOUND_DRV ", "").replace("\n", "").strip() 25 | try: 26 | dithering = ini.readline().replace("DITHERING ", "").replace("\n", "").strip() 27 | print("DITHERING is " + dithering) 28 | if dithering == "FALSE": 29 | dithering = False 30 | except: 31 | pass 32 | 33 | tmp_back = glob.glob('novel\\background\\**\\*.png', recursive=True) 34 | if os.path.isfile("novel\\background\\black.jpg"): 35 | tmp_back = ["novel\\background\\black.jpg"] 36 | if (len(tmp_back) == 0): 37 | tmp_back = glob.glob('novel\\background\\**\\*.jpg', recursive=True) 38 | if (len(tmp_back) == 0): 39 | print("The project does not have backgrounds.") 40 | print("This way even the foregrounds will be ignored") 41 | exit() 42 | 43 | tmp_back = tmp_back[0] 44 | tmp_back = Image.open(tmp_back) 45 | img_scale = (bg_width * 8) / tmp_back.size[0] 46 | 47 | bg_left = int((40 - bg_width) / 2) 48 | 49 | if os.path.isfile(magick_convert): 50 | print("Using ImageMagick") 51 | else: 52 | print("Using ony PILLOW") 53 | magick_convert = False 54 | ############################################# 55 | 56 | bg_width *= 8 57 | bg_height *= 8 58 | 59 | alpha_color = [255, 0, 255] 60 | 61 | def mk_dirtrees(file_name:str): 62 | pathlib.Path(os.path.dirname(file_name)).mkdir(parents=True, exist_ok=True) 63 | 64 | 65 | def scale_n_save_bg(image_file): 66 | img = Image.open(image_file) 67 | scaled = img.resize([bg_width, bg_height], resample=Image.HAMMING) 68 | mk_dirtrees(image_file) 69 | scaled.save(image_file) 70 | 71 | def scale_n_save_fg(image_file): 72 | img = Image.open(image_file) 73 | scaled = img.resize([int(img.size[0] * img_scale), int(img.size[1] * img_scale)], resample=Image.HAMMING) 74 | mk_dirtrees(image_file) 75 | scaled.save(image_file) 76 | 77 | 78 | def crop_n_save(image_file): 79 | img = Image.open(image_file) 80 | # 8 + 1 cause this way we will not cut the images 81 | if img.size[0] % 8 or img.size[1] %8: 82 | x = int(img.size[0] / 8 + 1) * 8 - 4 #this is -4 so to cut both sides 83 | y = int(img.size[1] / 8 + 1) * 8 #this is 8 + 1 to make the image taller and not cut it. 84 | if img.size[1] % 8 == 0: 85 | cropped = img.crop((4,0,x,y)) 86 | else: 87 | dif = img.size[1] - y 88 | cropped = img.crop((4,dif,x,y + dif)) 89 | mk_dirtrees(image_file) 90 | cropped.save(image_file) 91 | 92 | def reduce_colors_convert(img_file, num_colors): 93 | if dithering: 94 | subprocess.run([magick_convert, 95 | img_file, 96 | '-fx', 97 | '-1.28500726818617*u*u*u + 1.92751090227925*u*u + 0.357496365906917*u', 98 | '-ordered-dither', 99 | 'o8x8,8', 100 | '-colors', 101 | str(num_colors), 102 | '-ordered-dither', 103 | 'threshold,8,8,8', 104 | img_file]) 105 | else: 106 | subprocess.run([magick_convert, 107 | img_file, 108 | '-fx', 109 | '-1.28500726818617*u*u*u + 1.92751090227925*u*u + 0.357496365906917*u', 110 | #'-ordered-dither', 111 | #'o8x8,8', 112 | '-colors', 113 | str(num_colors), 114 | #'-ordered-dither', 115 | #'threshold,8,8,8', 116 | img_file]) 117 | 118 | img = Image.open(img_file) 119 | if img.mode != "P": 120 | img = img.convert("RGB").quantize(num_colors) 121 | img.save(img_file) 122 | 123 | def replace_alpha(img_file): 124 | if magick_convert == False: 125 | 126 | img = Image.open(img_file) 127 | bg = Image.new("RGB", img.size, (0, 0, 0)).convert("L") 128 | pink = Image.new("RGBA", img.size, (alpha_color[0], alpha_color[1], alpha_color[2])) 129 | bg = Image.composite(pink, img, bg) 130 | bg.save(img_file) 131 | else: 132 | img = Image.open(img_file) 133 | alpha = img.getchannel('A') 134 | bg = Image.new(img.mode, img.size, (0, 0, 0, 255)) 135 | bg.paste(alpha, mask=alpha) 136 | bg = bg.quantize(2).convert("L") 137 | pink = Image.new("RGBA", img.size, (alpha_color[0], alpha_color[1], alpha_color[2])) 138 | bg = Image.composite(img.convert("RGB"), pink, bg) 139 | bg.save(img_file) 140 | 141 | 142 | def check_colors(img_file: str): 143 | original_img = Image.open(img_file) 144 | #pal = Image.open("Mega_Drive_Palette_indexed.png") 145 | img = original_img.convert(mode="RGB") 146 | #print(len(img.getcolors())) 147 | if magick_convert == False: 148 | 149 | if "background" in img_file: 150 | img = original_img.quantize(16, dither=0) 151 | pal = img.getpalette() 152 | 153 | if (len(pal)) >= 16*3: 154 | black_index = -1 155 | pal = img.getpalette() 156 | for i in range(0, 16): 157 | if pal[i*3] == 0 and pal[i*3 + 1] == 0 and pal[i*3 + 2] == 0: 158 | black_index = i 159 | break 160 | if black_index > 0 and black_index < 16: 161 | remap = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] 162 | remap[0] = black_index 163 | remap[black_index] = 0 164 | img = img.remap_palette(remap) 165 | else: 166 | img = original_img.quantize(15, dither=0) 167 | if len(img.getcolors()) < 16: 168 | pal = img.getpalette() 169 | while len(pal) < 16*3: 170 | pal.append(0) 171 | pal.append(0) 172 | pal.append(0) 173 | 174 | pal[15*3] = 0 175 | pal[15*3+1] = 0 176 | pal[15*3+2] = 0 177 | img.putpalette(pal) 178 | img = img.remap_palette([15, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 0]) 179 | 180 | 181 | elif "foreground" in img_file: 182 | img = original_img.quantize(15, dither=0) 183 | pal = img.getpalette() 184 | while len(pal) < 16 * 3: 185 | pal.append(0) 186 | pal.append(0) 187 | pal.append(0) 188 | pal[15*3] = 255 189 | pal[15*3 + 1] = 255 190 | pal[15*3 + 2] = 255 191 | img.putpalette(pal) 192 | 193 | img.save(img_file) 194 | else: #MAGICK CONVERT 195 | img = original_img.convert("RGB") 196 | if "background" in img_file: 197 | img.close() 198 | reduce_colors_convert(img_file, 16) 199 | img = Image.open(img_file) 200 | pal = img.getpalette() 201 | 202 | if (len(pal)) >= 16*3: 203 | black_index = -1 204 | pal = img.getpalette() 205 | for i in range(0, 16): 206 | if pal[i*3] == 0 and pal[i*3 + 1] == 0 and pal[i*3 + 2] == 0: 207 | black_index = i 208 | break 209 | if black_index > 0 and black_index < 16: 210 | remap = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] 211 | remap[0] = black_index 212 | remap[black_index] = 0 213 | img = img.remap_palette(remap) 214 | else: 215 | original_img.save(img_file) 216 | img = Image.open(img_file) 217 | reduce_colors_convert(img_file, 15) 218 | img = Image.open(img_file) 219 | if len(img.getcolors()) < 16: 220 | pal = img.getpalette() 221 | while len(pal) < 16*3: 222 | pal.append(0) 223 | pal.append(0) 224 | pal.append(0) 225 | 226 | pal[15*3] = 0 227 | pal[15*3+1] = 0 228 | pal[15*3+2] = 0 229 | img.putpalette(pal) 230 | img = img.remap_palette([15, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 0]) 231 | 232 | 233 | elif "foreground" in img_file: 234 | reduce_colors_convert(img_file, 15) 235 | img = Image.open(img_file) 236 | pal = img.getpalette() 237 | while len(pal) < 16 * 3: 238 | pal.append(0) 239 | pal.append(0) 240 | pal.append(0) 241 | pal[15*3] = 255 242 | pal[15*3 + 1] = 255 243 | pal[15*3 + 2] = 255 244 | img.putpalette(pal) 245 | 246 | img.save(img_file) 247 | 248 | 249 | def check_back_size(img_file): 250 | img = Image.open(img_file) 251 | crop = img.crop((0,0, bg_width, bg_height)) 252 | crop.save(img_file) 253 | 254 | 255 | 256 | def convert_images(): 257 | 258 | for background in glob.glob('novel\\background\\**\\*.jpg', recursive=True): 259 | out = background.replace('novel\\', 'res\\').replace('.jpg', '.png').lower() 260 | print(out) 261 | img = Image.open(background).convert("RGBA") 262 | mk_dirtrees(out) 263 | img.save(out) 264 | 265 | if(img_scale != 1.0): 266 | scale_n_save_bg(out) 267 | check_back_size(out) 268 | check_colors(out) 269 | 270 | for background in glob.glob('novel\\background\\**\\*.png', recursive=True): 271 | out = background.replace('novel\\', 'res\\').lower() 272 | print(out) 273 | img = Image.open(background).convert("RGB") 274 | mk_dirtrees(out) 275 | img.save(out) 276 | 277 | if(img_scale != 1.0): 278 | scale_n_save_bg(out) 279 | check_back_size(out) 280 | check_colors(out) 281 | 282 | for foreground in glob.glob('novel\\foreground\\**\\*.png', recursive=True): 283 | out = foreground.replace('novel\\', 'res\\').lower() 284 | 285 | img = Image.open(foreground).convert("RGBA") 286 | mk_dirtrees(out) 287 | img.save(out) 288 | print(out) 289 | if(img_scale != 1.0): 290 | scale_n_save_fg(out) 291 | crop_n_save(out) 292 | replace_alpha(out) 293 | check_colors(out) 294 | 295 | 296 | 297 | if len(sys.argv) > 1: 298 | d = False 299 | if "background" in sys.argv[1]: 300 | d = False 301 | elif "foreground" in sys.argv[1]: 302 | d = True 303 | else: 304 | print("argument does not belong to an image") 305 | exit() 306 | out = sys.argv[1].replace('novel\\', 'res\\').replace('.jpg', '.png').lower() 307 | 308 | img = Image.open(sys.argv[1]).convert("RGB") 309 | img.save(out) 310 | 311 | if(img_scale != 1.0): 312 | scale_n_save_bg(out) 313 | if d: 314 | img = Image.open(sys.argv[1]).convert("RGBA") 315 | img.save(out) 316 | 317 | if(img_scale != 1.0): 318 | scale_n_save_fg(out) 319 | crop_n_save(out) 320 | replace_alpha(out) 321 | check_colors(out) 322 | else: 323 | img = Image.open(sys.argv[1]).convert("RGB") 324 | img.save(out) 325 | 326 | if(img_scale != 1.0): 327 | scale_n_save_bg(out) 328 | check_back_size(out) 329 | check_colors(out) 330 | print(out) 331 | subprocess.run(["python", "create_image_res.py"]) 332 | 333 | else: 334 | convert_images() 335 | 336 | 337 | -------------------------------------------------------------------------------- /src/novel_player.c: -------------------------------------------------------------------------------- 1 | #include "genesis.h" 2 | #include "novel_player.h" 3 | #include "novel_functions.h" 4 | #include "novel_scripts.h" 5 | #include "novel_images.h" 6 | #include "novel_sounds.h" 7 | #include "novel_variables.h" 8 | #include "novel_external_functions.h" 9 | 10 | char NOVEL_CHOICE_CHAR[1] = {'z' + 5}; 11 | 12 | int NOVEL_ACTUAL_TEXT_TOP = NOVEL_TEXT_TOP; 13 | 14 | int bytes_to_int(int byte1, int byte2) { 15 | return byte2 | byte1 << 8; 16 | } 17 | 18 | 19 | u8 *get_pos() { 20 | return FAR_SAFE(NOVEL_SCRIPTS[NOVEL.script_index]+NOVEL.position, NOVEL_SCRIPTS_BYTES_COUNT[NOVEL.script_index]); 21 | } 22 | 23 | u8 read_char() { 24 | u8 r = *get_pos(); 25 | NOVEL.position++; 26 | return r; 27 | } 28 | 29 | int read_int() { 30 | u8 a = *get_pos(); 31 | NOVEL.position++; 32 | u8 b = *get_pos(); 33 | NOVEL.position++; 34 | return b | a << 8; 35 | } 36 | 37 | 38 | u16 read_u16() { 39 | 40 | u8 a = *get_pos(); 41 | NOVEL.position++; 42 | u8 b = *get_pos(); 43 | NOVEL.position++; 44 | return b | a << 8; 45 | } 46 | 47 | 48 | s32 read_s32() { 49 | u8 a = *get_pos(); 50 | NOVEL.position++; 51 | u8 b = *get_pos(); 52 | NOVEL.position++; 53 | u8 c = *get_pos(); 54 | NOVEL.position++; 55 | u8 d = *get_pos(); 56 | NOVEL.position++; 57 | //long result = (((bytes[0] << 8 & bytes[1]) << 8 & bytes[2]) << 8) & bytes[3] 58 | return d | c << 8 | b << 16 | a << 24;; 59 | } 60 | 61 | 62 | void novel_event_handler(u16 joy, u16 changed, u16 state) { 63 | if (joy == JOY_1) { 64 | if (state & changed & BUTTON_A || state & changed & BUTTON_C) { 65 | NOVEL.advance = TRUE; 66 | } else if (state & changed & BUTTON_START) { 67 | NOVEL.pause_menu = TRUE; 68 | } 69 | } 70 | } 71 | 72 | void novel_choice_event_handler(u16 joy, u16 changed, u16 state) { 73 | if (state & changed & BUTTON_UP) { 74 | NOVEL_VARIABLES[0] -= 1; 75 | } else if (state & changed & BUTTON_DOWN) { 76 | NOVEL_VARIABLES[0] += 1; 77 | } else if (state & changed & BUTTON_LEFT) { 78 | NOVEL_VARIABLES[0] -= NOVEL_TEXT_BOTTOM - NOVEL_ACTUAL_TEXT_TOP; 79 | } else if (state & changed & BUTTON_RIGHT) { 80 | NOVEL_VARIABLES[0] += NOVEL_TEXT_BOTTOM - NOVEL_ACTUAL_TEXT_TOP; 81 | } else if (state & changed & BUTTON_B) { 82 | NOVEL.selected = TRUE; 83 | } else if (state & changed & BUTTON_START) { 84 | NOVEL.pause_menu = TRUE; 85 | } 86 | } 87 | 88 | 89 | void novel_set_bg_palette() { 90 | PAL_setPalette(PAL0, NOVEL_BACKGROUND[NOVEL.back_index]->palette->data, CPU); 91 | } 92 | 93 | 94 | void novel_pause_event_handler(u16 joy, u16 changed, u16 state) { 95 | if (state & changed & BUTTON_UP) { 96 | NOVEL.pause_menu_pos -= 1; 97 | } else if (state & changed & BUTTON_DOWN) { 98 | NOVEL.pause_menu_pos += 1; 99 | } else if (state & changed & BUTTON_B) { 100 | NOVEL.pause_menu_selected = TRUE; 101 | } else if (state & changed & BUTTON_START) { 102 | NOVEL.pause_menu = FALSE; 103 | } 104 | } 105 | 106 | void novel_pause_menu_set_cursor(int max_values) { 107 | for (int i = 0; i < 3; i++) { 108 | VDP_clearText(NOVEL_TEXT_LEFT, NOVEL_TEXT_TOP + i, 1); 109 | } 110 | if (NOVEL.pause_menu_pos < 0) NOVEL.pause_menu_pos = max_values - 1; 111 | if (NOVEL.pause_menu_pos >= max_values) NOVEL.pause_menu_pos = 0; 112 | VDP_drawText(NOVEL_CHOICE_CHAR, NOVEL_TEXT_LEFT, NOVEL_TEXT_TOP + NOVEL.pause_menu_pos); 113 | } 114 | 115 | int novel_get_save_index() { 116 | int base_length = 4 + NOVEL_NUM_GLOBAL_VARIABLES * 2; 117 | int save_length = (NOVEL_NUM_VARIABLES * 2 + 2 + 2 + 6 * 3 + 4 + 2 + 2) * (NOVEL.pause_menu_pos + 1); 118 | 119 | return base_length + save_length; 120 | } 121 | 122 | void novel_save() { 123 | int index = novel_get_save_index(); 124 | 125 | //draw_int(index, 2, 29); //I have no idea why, but if I delete this line the game crashes. 126 | clear_text(); 127 | SYS_doVBlankProcess(); 128 | #ifdef GR_LANG 129 | VDP_drawText("So paivm_di apohgjezsgje.", NOVEL_TEXT_LEFT, NOVEL_TEXT_TOP); 130 | #endif 131 | #ifndef GR_LANG 132 | VDP_drawText("Game Saved", NOVEL_TEXT_LEFT, NOVEL_TEXT_TOP); 133 | #endif 134 | SYS_doVBlankProcess(); 135 | SRAM_enable(); 136 | SRAM_writeWord(index, NOVEL_SAVE_CHECK_NUM); 137 | index +=2; 138 | SRAM_writeLong(index, NOVEL.position); 139 | index +=4; 140 | SRAM_writeWord(index, NOVEL.script_index); 141 | index += 2; 142 | SRAM_writeWord(index, NOVEL.back_index); 143 | index +=2; 144 | for (int i = 0; i < 3; i++) { 145 | SRAM_writeWord(index, NOVEL.fore_imgs[i]); 146 | index +=2; 147 | SRAM_writeWord(index, NOVEL.fore_pos[i][0]); 148 | index +=2; 149 | SRAM_writeWord(index, NOVEL.fore_pos[i][1]); 150 | index +=2; 151 | } 152 | SRAM_writeWord(index, NOVEL.music); 153 | index +=2; 154 | 155 | for (int i = 0; i < NOVEL_NUM_VARIABLES; i++) { 156 | SRAM_writeWord(index + i*2, NOVEL_VARIABLES[i]); 157 | } 158 | 159 | SRAM_disable(); 160 | 161 | for (int i = 0; i < 60; i++) { 162 | SYS_doVBlankProcess(); 163 | } 164 | 165 | } 166 | 167 | void novel_load() { 168 | novel_stop_music(); 169 | novel_stop_sound(); 170 | SYS_doVBlankProcess(); 171 | int index = novel_get_save_index(); 172 | 173 | //draw_int(index, 2, 29); //I have no idea why, but if I delete this line the game crashes. 174 | clear_text(); 175 | SYS_doVBlankProcess(); 176 | #ifdef GR_LANG 177 | VDP_drawText("Uyqsxrg...", NOVEL_TEXT_LEFT, NOVEL_TEXT_TOP); 178 | #endif 179 | #ifndef GR_LANG 180 | VDP_drawText("Loading...", NOVEL_TEXT_LEFT, NOVEL_TEXT_TOP); 181 | #endif 182 | SYS_doVBlankProcess(); 183 | SRAM_enable(); 184 | if (NOVEL_SAVE_CHECK_NUM == SRAM_readWord(index)) { 185 | int tmp_img[3]; 186 | index +=2; 187 | NOVEL.position = SRAM_readLong(index); 188 | index +=4; 189 | NOVEL.script_index = SRAM_readWord(index); 190 | index += 2; 191 | int tmp_bg = SRAM_readWord(index); 192 | index +=2; 193 | for (int i = 0; i < 3; i++) { 194 | tmp_img[i] = SRAM_readWord(index); 195 | index +=2; 196 | NOVEL.fore_pos[i][0] = SRAM_readWord(index); 197 | index +=2; 198 | NOVEL.fore_pos[i][1] = SRAM_readWord(index); 199 | index +=2; 200 | } 201 | NOVEL.music = SRAM_readWord(index); 202 | index +=2; 203 | 204 | for (int i = 0; i < NOVEL_NUM_VARIABLES; i++) { 205 | NOVEL_VARIABLES[i] = SRAM_readWord(index + i*2); 206 | } 207 | SRAM_disable(); 208 | 209 | novel_draw_background(tmp_bg, 16); 210 | for (int i = 0; i < 3; i++) { 211 | if (tmp_img[i] != MAX_S16) { 212 | novel_draw_foreground(tmp_img[i], NOVEL.fore_pos[i][0], NOVEL.fore_pos[i][1]); 213 | } 214 | } 215 | if (NOVEL.music != NOVEL_NO_MUSIC) { 216 | NOVEL_PLAY_MUSIC[NOVEL.music](); 217 | } else { 218 | novel_stop_music(); 219 | } 220 | NOVEL.pause_menu = FALSE; 221 | 222 | } else { 223 | SRAM_disable(); 224 | #ifdef GR_LANG 225 | VDP_drawText("Jem^ htq_da", NOVEL_TEXT_LEFT, NOVEL_TEXT_TOP); 226 | #endif 227 | #ifndef GR_LANG 228 | VDP_drawText("Empty Slot", NOVEL_TEXT_LEFT, NOVEL_TEXT_TOP); 229 | #endif 230 | for (int i = 0; i < 60; i++) { 231 | SYS_doVBlankProcess(); 232 | } 233 | } 234 | 235 | } 236 | 237 | 238 | void novel_pause_menu() { 239 | if (NOVEL.pause_menu) { 240 | //Stoping all sounds to protect z80 241 | novel_stop_music(); 242 | novel_stop_sound(); 243 | SYS_doVBlankProcess(); 244 | 245 | JOY_setEventHandler(novel_pause_event_handler); 246 | NOVEL.pause_menu_pos = 0; 247 | NOVEL.pause_menu_selected = FALSE; 248 | 249 | //VDP_clearPlane(WINDOW, TRUE); 250 | clear_text(); 251 | SYS_doVBlankProcess(); 252 | #ifdef GR_LANG 253 | VDP_drawText("Apoh^jetrg", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP); 254 | VDP_drawText("Uyqsxrg", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP+1); 255 | VDP_drawText("Epirsqou^ rsgm aqvij^ ohymg", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP+2); 256 | #endif 257 | #ifndef GR_LANG 258 | VDP_drawText("Save", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP); 259 | VDP_drawText("Load", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP+1); 260 | VDP_drawText("Return to title Screen", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP+2); 261 | #endif 262 | NOVEL.pause_menu_selected = FALSE; 263 | while (!NOVEL.pause_menu_selected && NOVEL.pause_menu) 264 | { 265 | 266 | novel_pause_menu_set_cursor(3); 267 | SYS_doVBlankProcess(); 268 | } 269 | SYS_doVBlankProcess(); 270 | if (NOVEL.pause_menu_selected) { 271 | NOVEL.pause_menu_selected = FALSE; 272 | if (NOVEL.pause_menu_pos == 0) { 273 | clear_text(); 274 | #ifdef GR_LANG 275 | VDP_drawText("Apoh^jetrg rsg htq_da 1", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP); 276 | VDP_drawText("Apoh^jetrg rsg htq_da 2", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP+1); 277 | VDP_drawText("Apoh^jetrg rsg htq_da 3", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP+2); 278 | #endif 279 | #ifndef GR_LANG 280 | VDP_drawText("Save to Slot 1", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP); 281 | VDP_drawText("Save to Slot 2", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP+1); 282 | VDP_drawText("Save to Slot 3", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP+2); 283 | #endif 284 | NOVEL.pause_menu_selected = FALSE; 285 | while (!NOVEL.pause_menu_selected && NOVEL.pause_menu) { 286 | novel_pause_menu_set_cursor(3); 287 | SYS_doVBlankProcess(); 288 | } 289 | if (NOVEL.pause_menu_selected) { 290 | SYS_doVBlankProcess(); 291 | novel_save(); 292 | } 293 | NOVEL.pause_menu = FALSE; 294 | if (NOVEL.music != NOVEL_NO_MUSIC) NOVEL_PLAY_MUSIC[NOVEL.music](); 295 | } else if (NOVEL.pause_menu_pos == 1) { 296 | NOVEL.pause_menu_pos = 0; 297 | clear_text(); 298 | 299 | #ifdef GR_LANG 300 | VDP_drawText("Uyqsxrg sg& htq_da& 1", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP); 301 | VDP_drawText("Uyqsxrg sg& htq_da& 2", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP+1); 302 | VDP_drawText("Uyqsxrg sg& htq_da& 3", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP+2); 303 | #endif 304 | #ifndef GR_LANG 305 | VDP_drawText("Load Slot 1", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP); 306 | VDP_drawText("Load Slot 2", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP+1); 307 | VDP_drawText("Load Slot 3", NOVEL_TEXT_LEFT+1, NOVEL_TEXT_TOP+2); 308 | #endif 309 | NOVEL.pause_menu_selected = FALSE; 310 | while (!NOVEL.pause_menu_selected && NOVEL.pause_menu) { 311 | novel_pause_menu_set_cursor(3); 312 | SYS_doVBlankProcess(); 313 | } 314 | if (NOVEL.pause_menu_selected) { 315 | novel_load(); 316 | } 317 | } else if (NOVEL.pause_menu_pos == 2) { 318 | NOVEL.pause_menu = FALSE; 319 | novel_reset(); //Just reset global save will be done only when th novel reach the end 320 | //novel_save_n_restart(); //Just for testing the global vars 321 | } 322 | } 323 | 324 | //VDP_clearTextArea(NOVEL_TEXT_LEFT, NOVEL_TEXT_TOP, NOVEL_TEXT_WIDTH, NOVEL_TEXT_BOTTOM - NOVEL_TEXT_TOP); 325 | NOVEL.pause_menu = FALSE; 326 | if (NOVEL.music != NOVEL_NO_MUSIC) NOVEL_PLAY_MUSIC[NOVEL.music](); 327 | clear_text(); 328 | JOY_setEventHandler(novel_event_handler); 329 | } 330 | 331 | } 332 | 333 | void novel_load_global() { 334 | // 335 | //First 2 bytes are the NOVEL_CHECK_NUM 336 | //NEXT 2 are the NOVEL_NUM_GLOBAL_VARIABLES 337 | //Those two are checked cause if NOVEL_NUM_GLOBAL_VARIABLES is changed then the global save will be wrong 338 | // and if the NOVEL_CHECK_NUM is changed the it is another novel. 339 | //NOVEL_CHECK_NUM is used to easily reset the global save when in development. 340 | //The first of NOVEL_VARIABLES is the selected 341 | // 342 | SYS_doVBlankProcess(); 343 | SRAM_enable(); 344 | if (SRAM_readWord(0) != NOVEL_SAVE_CHECK_NUM || SRAM_readWord(2) != NOVEL_NUM_GLOBAL_VARIABLES) { 345 | SRAM_writeWord(0, NOVEL_SAVE_CHECK_NUM); 346 | SRAM_writeWord(2, NOVEL_NUM_GLOBAL_VARIABLES); 347 | for (int i = 0; i < NOVEL_NUM_GLOBAL_VARIABLES; i++) { 348 | SRAM_writeWord(i * 2 + 4, 0); 349 | } 350 | } else { 351 | for (int i = 0; i < NOVEL_NUM_GLOBAL_VARIABLES; i++) { 352 | NOVEL_VARIABLES[i + 1] = SRAM_readWord(i*2+4); 353 | } 354 | } 355 | SRAM_disable(); 356 | SYS_doVBlankProcess(); 357 | 358 | } 359 | 360 | void novel_global_save() { 361 | SYS_doVBlankProcess(); 362 | SRAM_enable(); 363 | for (int i = 0; i < NOVEL_NUM_GLOBAL_VARIABLES; i++) { 364 | SRAM_writeWord(i*2 + 4, NOVEL_VARIABLES[i + 1]); 365 | } 366 | SRAM_disable(); 367 | SYS_doVBlankProcess(); 368 | } 369 | 370 | void novel_save_n_restart() { 371 | novel_global_save(); 372 | novel_reset(); 373 | } 374 | 375 | void novel_reset_vars() { 376 | for(int i = 0; i < NOVEL_NUM_VARIABLES; i++) { 377 | NOVEL_VARIABLES[i] = 0; 378 | } 379 | novel_load_global(); 380 | } 381 | 382 | void novel_reset() { 383 | NOVEL.position = 0; 384 | NOVEL.script_index = 0; 385 | NOVEL.back_index = 1; 386 | NOVEL.music = NOVEL_NO_MUSIC; 387 | 388 | NOVEL.fore_pal = 1; 389 | 390 | VDP_setTextPalette(PAL1); 391 | PAL_setColor(31, RGB24_TO_VDPCOLOR(0xffffff)); 392 | for (int i = 0; i < 3; i++) { 393 | NOVEL.fore_imgs[i] = MAX_S16; 394 | } 395 | 396 | NOVEL.selected = 0; 397 | //NOVEL.script_index = 0; 398 | NOVEL.advance = FALSE; 399 | NOVEL.pause_menu = FALSE; 400 | 401 | 402 | JOY_setEventHandler(novel_event_handler); 403 | 404 | VDP_setWindowVPos(TRUE, NOVEL_TEXT_TOP); 405 | 406 | VDP_setTextPlane(WINDOW); 407 | 408 | novel_reset_vars(); 409 | novel_stop_music(); 410 | clear_text(); 411 | VDP_clearPlane(BG_A, TRUE); 412 | SYS_doVBlankProcess(); 413 | VDP_clearPlane(BG_B, TRUE); 414 | SYS_doVBlankProcess(); 415 | } 416 | 417 | 418 | int make_choice(const char *bin) { 419 | int count = 2; 420 | NOVEL_VARIABLES[0] = 0; 421 | int number_of_choices = bin[0]; 422 | NOVEL_ACTUAL_TEXT_TOP = NOVEL_TEXT_TOP; 423 | int double_row_start = NOVEL_TEXT_BOTTOM - NOVEL_ACTUAL_TEXT_TOP; 424 | bin++; 425 | int new_lines = (number_of_choices - double_row_start * 2) / 2 + number_of_choices % 2; 426 | if (new_lines < 0) new_lines = 0; 427 | if (new_lines) { 428 | double_row_start += new_lines; 429 | NOVEL_ACTUAL_TEXT_TOP -= new_lines; 430 | VDP_setWindowVPos(TRUE, NOVEL_ACTUAL_TEXT_TOP); 431 | VDP_clearTileMapRect(BG_B, NOVEL_BG_LEFT, NOVEL_ACTUAL_TEXT_TOP, NOVEL_BG_WIDTH, NOVEL_BG_TOP + NOVEL_BG_HEIGHT - NOVEL_ACTUAL_TEXT_TOP); 432 | 433 | SYS_doVBlankProcess(); 434 | } 435 | 436 | for(int i = 0; i < number_of_choices; i++) { 437 | int len = strlen(bin) + 1; 438 | if (i < double_row_start ) { 439 | VDP_drawText(bin, NOVEL_TEXT_LEFT + 1, NOVEL_ACTUAL_TEXT_TOP + i); 440 | } else { 441 | VDP_drawText(bin, NOVEL_TEXT_LEFT + 18, NOVEL_ACTUAL_TEXT_TOP + i - double_row_start); 442 | } 443 | 444 | bin += len; 445 | count += len; 446 | } 447 | JOY_setEventHandler(novel_choice_event_handler); 448 | while (!NOVEL.selected) 449 | { 450 | if (NOVEL_VARIABLES[0] < 0) { 451 | NOVEL_VARIABLES[0] = number_of_choices - 1; 452 | } else if (NOVEL_VARIABLES[0] >= number_of_choices) { 453 | NOVEL_VARIABLES[0] = 0; 454 | } 455 | for (int i = 0; i < number_of_choices; i++) { 456 | if (i < double_row_start) { 457 | VDP_clearText(NOVEL_TEXT_LEFT, NOVEL_ACTUAL_TEXT_TOP + i, 1); 458 | } else { 459 | VDP_clearText(NOVEL_TEXT_LEFT + 17, NOVEL_ACTUAL_TEXT_TOP + i - double_row_start, 1); 460 | } 461 | 462 | } 463 | if (NOVEL_VARIABLES[0] < double_row_start) { 464 | VDP_drawText(NOVEL_CHOICE_CHAR, NOVEL_TEXT_LEFT, NOVEL_ACTUAL_TEXT_TOP + NOVEL_VARIABLES[0]); 465 | } else { 466 | VDP_drawText(NOVEL_CHOICE_CHAR, NOVEL_TEXT_LEFT + 17, NOVEL_ACTUAL_TEXT_TOP + NOVEL_VARIABLES[0] - double_row_start); 467 | } 468 | 469 | SYS_doVBlankProcess(); 470 | if (NOVEL.pause_menu) { 471 | JOY_setEventHandler(novel_event_handler); 472 | clear_text(); 473 | NOVEL.selected = FALSE; 474 | if (new_lines) { 475 | VDP_setTileMapEx(BG_B, NOVEL_BACKGROUND[NOVEL.back_index]->tilemap, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, TILE_USER_INDEX), NOVEL_BG_LEFT, NOVEL_ACTUAL_TEXT_TOP, 0, NOVEL_BG_HEIGHT - new_lines, NOVEL_BG_WIDTH, new_lines, DMA_QUEUE); 476 | NOVEL_ACTUAL_TEXT_TOP = NOVEL_TEXT_TOP; 477 | VDP_setWindowVPos(TRUE, NOVEL_TEXT_TOP); 478 | } 479 | return 0; 480 | } 481 | } 482 | JOY_setEventHandler(novel_event_handler); 483 | clear_text(); 484 | NOVEL_VARIABLES[0] +=1; 485 | NOVEL.selected = FALSE; 486 | if (new_lines) { 487 | VDP_setTileMapEx(BG_B, NOVEL_BACKGROUND[NOVEL.back_index]->tilemap, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, TILE_USER_INDEX), NOVEL_BG_LEFT, NOVEL_ACTUAL_TEXT_TOP, 0, NOVEL_BG_HEIGHT - new_lines, NOVEL_BG_WIDTH, new_lines, DMA_QUEUE); 488 | NOVEL_ACTUAL_TEXT_TOP = NOVEL_TEXT_TOP; 489 | VDP_setWindowVPos(TRUE, NOVEL_TEXT_TOP); 490 | } 491 | 492 | return count; 493 | } 494 | 495 | 496 | 497 | int make_ifchoice() { 498 | s32 current_novel_position = NOVEL.position; 499 | NOVEL_VARIABLES[0] = 0; 500 | read_char(); 501 | int number_of_choices = read_char(); 502 | ifchoice *choices = MEM_alloc(number_of_choices * sizeof(ifchoice)); 503 | NOVEL_ACTUAL_TEXT_TOP = NOVEL_TEXT_TOP; 504 | int double_row_start = NOVEL_TEXT_BOTTOM - NOVEL_ACTUAL_TEXT_TOP; 505 | //read_char(); 506 | int actual_number_of_choices = 0; 507 | for (int i = 0; i < number_of_choices; i++) { 508 | int type = read_char(); 509 | int right_hand_type = read_char(); 510 | int var_num = read_int(); 511 | int num = read_int(); 512 | int symbol = read_char(); 513 | choices[i].pos = i; 514 | choices[i].text = get_pos(); 515 | NOVEL.position += strlen(choices[i].text) + 1; 516 | if (type == 0) { 517 | actual_number_of_choices +=1; 518 | choices[i].display = TRUE; 519 | } else { 520 | int left_hand = NOVEL_VARIABLES[var_num]; 521 | int right_hand = num; 522 | if (right_hand_type == 1) { 523 | right_hand = NOVEL_VARIABLES[num]; 524 | } 525 | switch (symbol) 526 | { 527 | case 0: 528 | choices[i].display = left_hand == right_hand; 529 | break; 530 | case 1: 531 | choices[i].display = left_hand != right_hand; 532 | break; 533 | case 2: 534 | choices[i].display = left_hand > right_hand; 535 | break; 536 | case 3: 537 | choices[i].display = left_hand < right_hand; 538 | break; 539 | case 4: 540 | choices[i].display = left_hand >= right_hand; 541 | break; 542 | case 5: 543 | choices[i].display = left_hand <= right_hand; 544 | break; 545 | default: 546 | break; 547 | } 548 | 549 | if (choices[i].display) { 550 | actual_number_of_choices +=1; 551 | } 552 | 553 | } 554 | 555 | } 556 | int *actual_choices = MEM_alloc(actual_number_of_choices * sizeof(int)); 557 | int count = 0; 558 | for (int i = 0; i < number_of_choices; i++) { 559 | if (choices[i].display) { 560 | actual_choices[count] = choices[i].pos; 561 | count +=1; 562 | } 563 | } 564 | int new_lines = (actual_number_of_choices - double_row_start * 2) / 2 + actual_number_of_choices % 2; 565 | if (new_lines < 0) new_lines = 0; 566 | if (new_lines) { 567 | double_row_start += new_lines; 568 | NOVEL_ACTUAL_TEXT_TOP -= new_lines; 569 | VDP_setWindowVPos(TRUE, NOVEL_ACTUAL_TEXT_TOP); 570 | VDP_clearTileMapRect(BG_B, NOVEL_BG_LEFT, NOVEL_ACTUAL_TEXT_TOP, NOVEL_BG_WIDTH, NOVEL_BG_TOP + NOVEL_BG_HEIGHT - NOVEL_ACTUAL_TEXT_TOP); 571 | 572 | SYS_doVBlankProcess(); 573 | } 574 | 575 | int draw_choice_pos = 0; 576 | for(int i = 0; i < number_of_choices; i++) { 577 | if (choices[i].display) { 578 | if (draw_choice_pos < double_row_start ) { 579 | VDP_drawText(choices[i].text, NOVEL_TEXT_LEFT + 1, NOVEL_ACTUAL_TEXT_TOP + draw_choice_pos); 580 | } else { 581 | VDP_drawText(choices[i].text, NOVEL_TEXT_LEFT + 18, NOVEL_ACTUAL_TEXT_TOP + draw_choice_pos - double_row_start); 582 | } 583 | draw_choice_pos +=1; 584 | } 585 | } 586 | JOY_setEventHandler(novel_choice_event_handler); 587 | while (!NOVEL.selected) 588 | { 589 | if (NOVEL_VARIABLES[0] < 0) { 590 | NOVEL_VARIABLES[0] = actual_number_of_choices - 1; 591 | } else if (NOVEL_VARIABLES[0] >= actual_number_of_choices) { 592 | NOVEL_VARIABLES[0] = 0; 593 | } 594 | for (int i = 0; i < actual_number_of_choices; i++) { 595 | if (i < double_row_start) { 596 | VDP_clearText(NOVEL_TEXT_LEFT, NOVEL_ACTUAL_TEXT_TOP + i, 1); 597 | } else { 598 | VDP_clearText(NOVEL_TEXT_LEFT + 17, NOVEL_ACTUAL_TEXT_TOP + i - double_row_start, 1); 599 | } 600 | 601 | } 602 | if (NOVEL_VARIABLES[0] < double_row_start) { 603 | VDP_drawText(NOVEL_CHOICE_CHAR, NOVEL_TEXT_LEFT, NOVEL_ACTUAL_TEXT_TOP + NOVEL_VARIABLES[0]); 604 | } else { 605 | VDP_drawText(NOVEL_CHOICE_CHAR, NOVEL_TEXT_LEFT + 17, NOVEL_ACTUAL_TEXT_TOP + NOVEL_VARIABLES[0] - double_row_start); 606 | } 607 | 608 | SYS_doVBlankProcess(); 609 | if (NOVEL.pause_menu) { 610 | JOY_setEventHandler(novel_event_handler); 611 | clear_text(); 612 | NOVEL.selected = FALSE; 613 | if (new_lines) { 614 | VDP_setTileMapEx(BG_B, NOVEL_BACKGROUND[NOVEL.back_index]->tilemap, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, TILE_USER_INDEX), NOVEL_BG_LEFT, NOVEL_ACTUAL_TEXT_TOP, 0, NOVEL_BG_HEIGHT - new_lines, NOVEL_BG_WIDTH, new_lines, DMA_QUEUE); 615 | NOVEL_ACTUAL_TEXT_TOP = NOVEL_TEXT_TOP; 616 | VDP_setWindowVPos(TRUE, NOVEL_TEXT_TOP); 617 | } 618 | MEM_free(choices); 619 | MEM_free(actual_choices); 620 | return current_novel_position; 621 | } 622 | } 623 | JOY_setEventHandler(novel_event_handler); 624 | clear_text(); 625 | NOVEL_VARIABLES[0] = actual_choices[NOVEL_VARIABLES[0]] + 1; 626 | NOVEL.selected = FALSE; 627 | if (new_lines) { 628 | VDP_setTileMapEx(BG_B, NOVEL_BACKGROUND[NOVEL.back_index]->tilemap, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, TILE_USER_INDEX), NOVEL_BG_LEFT, NOVEL_ACTUAL_TEXT_TOP, 0, NOVEL_BG_HEIGHT - new_lines, NOVEL_BG_WIDTH, new_lines, DMA_QUEUE); 629 | NOVEL_ACTUAL_TEXT_TOP = NOVEL_TEXT_TOP; 630 | VDP_setWindowVPos(TRUE, NOVEL_TEXT_TOP); 631 | } 632 | MEM_free(choices); 633 | MEM_free(actual_choices); 634 | return NOVEL.position; 635 | } 636 | 637 | 638 | 639 | void novel_update() { 640 | int novel_function; 641 | NOVEL.advance = FALSE; 642 | NOVEL.selected = FALSE; 643 | u16 joy = JOY_readJoypad(JOY_1); 644 | if (joy & BUTTON_C) { 645 | NOVEL.advance = TRUE; 646 | } 647 | if (NOVEL.position >= NOVEL_SCRIPTS_BYTES_COUNT[NOVEL.script_index]) { 648 | novel_save_n_restart(); 649 | } 650 | novel_function = *get_pos(); 651 | switch (novel_function) 652 | { 653 | case 0: //bgload image.png fadetime 654 | NOVEL.position++; 655 | int back_index = read_int(); 656 | int fade_time = read_int(); 657 | novel_draw_background(back_index, fade_time); 658 | break; 659 | case 1: //setimg image.png x y 660 | NOVEL.position++; 661 | int fore_index = read_int(); 662 | int x = read_int(); 663 | int y = read_int(); 664 | novel_draw_foreground(fore_index, x , y); 665 | break; 666 | case 2: //SOUND 667 | { 668 | NOVEL.position++; 669 | int sound_pos = read_int(); 670 | int loop = read_char(); 671 | if (loop == 255) { 672 | loop = 1; 673 | } else { 674 | loop = 0; 675 | } 676 | if(sound_pos == 32700) { 677 | novel_stop_sound(); 678 | } else { 679 | NOVEL_PLAY_SOUND[sound_pos](loop); 680 | } 681 | } 682 | return; 683 | case 3: //MUSIC 684 | { 685 | NOVEL.position++; 686 | 687 | int music_pos = read_int(); 688 | 689 | if (music_pos == NOVEL_NO_MUSIC) { 690 | novel_stop_music(); 691 | } else { 692 | NOVEL_PLAY_MUSIC[music_pos](); 693 | } 694 | NOVEL.music = music_pos; 695 | 696 | } 697 | return; 698 | case 4: //TEXT 699 | { 700 | NOVEL.position += draw_text(get_pos()+1); 701 | break; 702 | } 703 | case 5: //CHOICE 704 | NOVEL.position += make_choice(get_pos()+1); 705 | //draw_int(NOVEL_VARIABLES[0], 2, 25); 706 | break; 707 | case 6: //SETVAR 708 | { 709 | NOVEL.position++; 710 | int var_index = read_int(); 711 | int symbol = read_char(); 712 | int num = read_int(); 713 | 714 | int type = read_char(); 715 | if (type == 1) { //IT is a Variable 716 | num = NOVEL_VARIABLES[num]; 717 | } 718 | 719 | switch (symbol) 720 | { 721 | case 0: 722 | NOVEL_VARIABLES[var_index] = num; 723 | break; 724 | case 1: 725 | NOVEL_VARIABLES[var_index] += num; 726 | break; 727 | case 2: 728 | NOVEL_VARIABLES[var_index] -= num; 729 | break; 730 | default: 731 | break; 732 | } 733 | 734 | 735 | } 736 | return; 737 | case 7: //GSETVAR 738 | break; 739 | case 8: //IF 740 | NOVEL.position++; 741 | int variable = read_int(); 742 | int eq = read_char(); 743 | int num = read_int(); 744 | int type = read_char(); 745 | if (type == 1) { //VARIABLE 746 | num = NOVEL_VARIABLES[num]; 747 | } 748 | 749 | int pass = FALSE; 750 | if (eq == 0) { // == 751 | if (NOVEL_VARIABLES[variable] == num) { 752 | pass = TRUE; 753 | } 754 | } else if (eq == 1) { // != 755 | if (NOVEL_VARIABLES[variable] != num) { 756 | pass = TRUE; 757 | } 758 | } else if (eq == 2) { // > 759 | if (NOVEL_VARIABLES[variable] > num) { 760 | pass = TRUE; 761 | } 762 | } else if (eq == 3) { // < 763 | if (NOVEL_VARIABLES[variable] < num) { 764 | pass = TRUE; 765 | } 766 | } else if (eq == 4) { // >= 767 | if (NOVEL_VARIABLES[variable] >= num) { 768 | pass = TRUE; 769 | } 770 | } else if (eq == 5) { // <= 771 | if (NOVEL_VARIABLES[variable] <= num) { 772 | pass = TRUE; 773 | } 774 | } 775 | if (pass) { 776 | NOVEL.position +=2; 777 | } else { 778 | NOVEL.position += read_int() - 2; 779 | } 780 | return; 781 | case 9: //FI 782 | NOVEL.position++; 783 | return; 784 | case 10: //JUMP 785 | { 786 | NOVEL.position++; 787 | int new_script_index = read_int(); 788 | NOVEL.position = read_s32(); 789 | NOVEL.script_index = new_script_index; 790 | return; 791 | } 792 | return; 793 | case 11: //DELAY 794 | { 795 | NOVEL.position++; 796 | int delay = read_int(); 797 | for (int i = 0; i < delay; i++) { 798 | if (NOVEL.advance) { 799 | i +=10; 800 | } 801 | SYS_doVBlankProcess(); 802 | } 803 | } 804 | return; 805 | case 12: //RANDOM 806 | break; 807 | case 13: //LABEL 808 | break; 809 | case 14: //GO_TO 810 | NOVEL.position++; 811 | NOVEL.position = read_s32(); 812 | 813 | return; 814 | case 15: //CLEAR_TEXT 815 | clear_text(); 816 | break; 817 | case 16: //RESET VARS Custom command made for setvar ~ ~ 818 | NOVEL.position++; 819 | novel_reset_vars(); 820 | return; 821 | case 17: //RETJUMP this is when a line is like this: jump $RETFILE $RETLABEL 822 | { 823 | NOVEL.position++; 824 | 825 | int type = read_char(); 826 | int retfile = read_int(); 827 | int retlabel = read_int(); 828 | 829 | NOVEL.script_index = NOVEL_VARIABLES[retfile]; 830 | if (type == 0) { 831 | 832 | NOVEL.position = NOVEL_VARIABLES[retlabel]; 833 | } else { 834 | NOVEL.position = 0; 835 | } 836 | } 837 | return; 838 | case 18: //ifchoice 839 | NOVEL.position = make_ifchoice(); 840 | break; 841 | case 19: //external functions 842 | { 843 | NOVEL.position++; 844 | int func = read_char(); 845 | nv_external_functions[func](); 846 | } 847 | 848 | break; 849 | default: 850 | 851 | break; 852 | } 853 | 854 | novel_pause_menu(); 855 | 856 | SYS_doVBlankProcess(); 857 | } -------------------------------------------------------------------------------- /convert_scripts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | from turtle import right 4 | from PIL import Image 5 | import unicodedata 6 | 7 | lines_count = 0 8 | 9 | novel_stop_music = 1600 10 | 11 | ########################################## 12 | ini = open("novel.ini") 13 | magick_convert = ini.readline().replace("MAGICK_CONVERT_PATH ", "").replace("\n", "") 14 | bg_width = int(ini.readline().replace("BG_WIDTH ", "")) 15 | bg_height = int(ini.readline().replace("BG_HEIGHT ", "")) 16 | bg_top = int(ini.readline().replace("BG_TOP ", "")) 17 | text_top = int(ini.readline().replace("TEXT_TOP ", "")) 18 | text_bottom = int(ini.readline().replace("TEXT_BOTTOM ", "")) 19 | text_left = int(ini.readline().replace("TEXT_LEFT ", "")) 20 | text_right = int(ini.readline().replace("TEXT_RIGHT ", "")) 21 | compression = ini.readline().replace("COMPRESSION ", "").replace("\n", "") 22 | save_check = ini.readline().replace("SAVE_CHECK ", "").replace("\n", "") 23 | sound_drv = ini.readline().replace("SOUND_DRV ", "").replace("\n", "") 24 | 25 | dithering = True 26 | try: 27 | dithering = ini.readline().replace("DITHERING ", "").replace("\n", "").strip() 28 | print("DITHERING is " + dithering) 29 | if dithering == "FALSE": 30 | dithering = False 31 | except: 32 | pass 33 | 34 | bad_filenames = False 35 | try: 36 | bad_filenames = ini.readline().replace("BAD_FILENAMES ", "").replace("\n", "").strip() 37 | print("BAD FILENAMES is " + bad_filenames) 38 | if bad_filenames == "TRUE": 39 | bad_filenames = True 40 | else: 41 | bad_filenames = False 42 | except: 43 | pass 44 | 45 | tmp_back = glob.glob('novel\\background\\**\\*.png', recursive=True) 46 | if (len(tmp_back) == 0): 47 | tmp_back = glob.glob('novel\\background\\**\\*.jpg', recursive=True) 48 | if (len(tmp_back) == 0): 49 | print("The project does not have backgrounds.") 50 | print("This way even the foregrounds will be ignored") 51 | exit() 52 | 53 | tmp_back = tmp_back[0] 54 | tmp_back = Image.open(tmp_back) 55 | img_scale = (bg_width * 8) / tmp_back.size[0] 56 | 57 | bg_left = int((40 - bg_width) / 2) 58 | 59 | external_funcs = {} 60 | external_funcs_file = open("inc\\novel_external_functions.h", "r") 61 | for func in external_funcs_file.readlines(): 62 | if func[:5] == "void ": 63 | external_funcs[func[5:].replace("(", "").replace(")", "").replace(";", "").strip()] = len(external_funcs) 64 | 65 | print(external_funcs) 66 | sounds_positions = {} 67 | 68 | for wav in glob.glob("res\\wav\\**\\*.wav", recursive=True): 69 | sounds_positions[os.path.splitext(wav.replace("res\\wav\\", "").replace("\\", "/"))[0]] = len(sounds_positions) 70 | 71 | music_positions = {} 72 | 73 | if sound_drv == "XGM": 74 | for xgm in glob.glob("res\\music\\**\\*.vgm", recursive=True): 75 | music_positions[xgm.replace("res\\music\\", "").replace(".vgm", "")] = len(music_positions) 76 | elif sound_drv == "2ADPCM" or sound_drv == "4PCM": 77 | for wav in glob.glob("res\\music\\**\\*.wav", recursive=True): 78 | music_positions[wav.replace("res\\music\\", "").replace(".wav", "").lower()] = len(music_positions) 79 | 80 | #print(music_positions) 81 | ############################################# 82 | 83 | backgrounds = {} 84 | for bg_file in glob.glob('res/background/**/*.png', recursive=True): 85 | backgrounds[bg_file.replace("\\", "/").replace("res/background/", "", 1)] = len(backgrounds) 86 | 87 | 88 | foregrounds = {} 89 | for fr_file in glob.glob('res/foreground/**/*.png', recursive=True): 90 | foregrounds[fr_file.replace("\\", "/").replace("res/foreground/", "", 1)] = len(foregrounds) 91 | 92 | ############### 93 | 94 | def count_line(lines, i): 95 | count = 0 96 | line = lines[i].lstrip() 97 | coms = line.split() 98 | c = line.split(" ")[0].replace("\n", "").replace("\t", "") 99 | if c == "bgload": 100 | count += 5 101 | elif c == "setimg": 102 | count += 7 103 | elif c == "sound": 104 | t = line.replace("sound ", "", 1).replace("\n", "").replace("\t", " ").strip().split(" ") 105 | t[0] = os.path.splitext(t[0].replace("\\", "/"))[0] 106 | if t[0] in sounds_positions.keys() or t[0] == "~": 107 | 108 | count += 4 109 | count += 0 110 | elif c == "music": 111 | #t = line.replace("music ", "", 1).replace("\n", "").replace("\t", " ").strip().split(" ") 112 | #if t[0] in music_positions.keys() or t[0] == "~": 113 | # count += 2 114 | count += 3 115 | elif c == "text": 116 | t = line.replace("text ", "", 1).replace("\n", "").replace("\t", " ").lstrip() 117 | if len(t) == 0 or t[0] == "~": 118 | return 0 119 | count += len(t) + 2 120 | elif c == "choice": 121 | count +=2 122 | choice = line.replace("choice ", "", 1).replace("\n", "").replace("\t", " ").split("|") 123 | for c in choice: 124 | count += len(c.strip()) + 1 125 | elif c == "ifchoice": 126 | count +=2 127 | choice = line.replace("ifchoice ", "", 1).replace("\n", "").replace("\t", " ").split("|") 128 | for c in choice: 129 | count +=7 130 | ifs = c.split(":") 131 | if len(ifs) == 2: 132 | c = ifs[1] 133 | count += len(c.strip()) + 1 134 | elif c == "setvar" or c == "gsetvar": 135 | if (coms[1] == "~"): 136 | count += 1 137 | else: 138 | count += 7 139 | elif c == "gsetvar": 140 | count += 0 141 | elif c == "if": 142 | count += 9 143 | elif c == "fi": 144 | count += 1 145 | elif c == "jump": 146 | t = line.strip().replace("\t", "").replace("jump ", "", 1).lower().replace("\n", "").replace("/", "_").replace(".scr", "").strip().split(" ") 147 | key = t[0].replace("-", "_") 148 | count += 7 149 | elif c == "delay": 150 | count += 3 151 | elif c == "random": 152 | count += 0 153 | elif c == "label": 154 | count += 0 155 | elif c == "goto": 156 | count += 5 157 | elif c == "cleartext": 158 | count += 0 159 | else: 160 | ############### EXTERNAL FUNCTIONS 161 | func = line.split(" ")[0].strip() 162 | if func in external_funcs: 163 | count +=2 164 | return count 165 | 166 | 167 | def count_to_fi(lines, i, ifs_count): 168 | 169 | line = lines[i].lstrip() 170 | count = count_line(lines, i) 171 | 172 | c = line.split(" ")[0].replace("\n", "").replace("\t", "") 173 | if c == "if": 174 | ifs_count +=1 175 | elif c == "fi": 176 | if ifs_count == 0: 177 | count += 1 178 | return count 179 | else: 180 | ifs_count -=1 181 | return count + count_to_fi(lines, i+1, ifs_count) 182 | 183 | functions = {"bgload":0, "setimg":1, "sound":2, "music":3, 184 | "text":4, "choice":5, "setvar":6, "gsetvar":7, 185 | "if":8, "fi":9, "jump":10, "delay":11, "random":12, 186 | "label":13, "goto": 14, "cleartext": 15 , "reset_vars": 16, 187 | "retjump": 17, "ifchoice": 18, "external_func": 19, 188 | 189 | } 190 | 191 | def get_novel_script_alias(script_file): 192 | f = script_file.replace("res\\", "", 1).replace(".scr", "") 193 | 194 | alias = f.replace("\\", "_").replace("-", "_").lower().replace("novel_script_", "", 1).replace("~", "_").replace("\"", "") 195 | return alias 196 | 197 | 198 | 199 | if not os.path.exists('res/script'): 200 | os.mkdir('res/script') 201 | 202 | 203 | 204 | 205 | scripts_c = open("src\\novel_scripts.c", "w") 206 | scripts_c.write("#include \"novel_scripts.h\"\n\n") 207 | scripts_c.write("const u8* NOVEL_SCRIPTS[] = {\n") 208 | scripts_c.write("\tscript_main,\n") 209 | 210 | 211 | script_res = open("res\\scripts_res.res", "w") 212 | #script_res.write("NEAR\n") 213 | 214 | scripts_dir = {"main":0} 215 | script_label = {"main": {}} 216 | script_var = {"selected":0, "retfile":1, "retlabel":2} 217 | script_retfile = ["retfile"] 218 | script_retlabel = ["retlabel"] 219 | script_byte_count = [] 220 | tmp_var = {} 221 | tmp_global_var = {} 222 | for d in glob.glob('novel\\script\\**\\*\\', recursive=True): 223 | d = d.replace("novel", "res", 1) 224 | if not os.path.exists(d): 225 | os.mkdir(d) 226 | 227 | for script_file in glob.glob('novel\\script\\**\\*.scr', recursive=True): 228 | 229 | alias = get_novel_script_alias(script_file) 230 | if alias != "main": 231 | script_label[alias] = {} 232 | try: 233 | utflines = open(script_file, 'r', encoding="UTF8").readlines() 234 | lines = [] 235 | for i in range(len(utflines)): 236 | lines.append(unicodedata.normalize('NFKD', utflines[i]).encode('ascii', 'ignore').decode('ascii')) 237 | except Exception as e: 238 | print("------------------ ERROR ---------------") 239 | print("at file " + script_file) 240 | print(e) 241 | exit() 242 | 243 | bytes_count = 0 244 | for i in range(len(lines)): 245 | line = lines[i].lstrip() 246 | l = line.replace("\t", " ").replace("\n", " ").replace(":", " ").lstrip().split() 247 | bytes_count += count_line(lines, i) 248 | 249 | 250 | if len(l) == 0: 251 | pass 252 | elif l[0] == "label": 253 | script_label[alias][l[1].lower()] = bytes_count 254 | elif l[0] == "gsetvar": 255 | if l[1].lower() not in tmp_global_var.keys() and l[1].lower() != "selected": 256 | tmp_global_var[l[1].lower()] = len(tmp_global_var) 257 | elif l[0] == "setvar": 258 | l[1] = l[1].lower() 259 | if l[1] != "~" and l[1] not in tmp_var.keys() and l[1] != "selected": 260 | l[3] = l[3].lower() 261 | tmp_var[l[1]] = len(tmp_var) 262 | 263 | 264 | pass 265 | pass 266 | 267 | tmp_global_var = sorted(tmp_global_var) 268 | tmp_var = sorted(tmp_var) 269 | num_global_var = len(tmp_global_var) 270 | 271 | for key in tmp_global_var: 272 | script_var[key] = len(script_var) 273 | for key in tmp_var: 274 | script_var[key] = len(script_var) 275 | 276 | 277 | 278 | 279 | scripts_h = open("inc\\novel_scripts.h", "w") 280 | scripts_h.write("#ifndef H_NOVEL_SCRIPTS\n") 281 | scripts_h.write("#define H_NOVEL_SCRIPTS\n\n") 282 | scripts_h.write("#include \"genesis.h\"\n") 283 | scripts_h.write("#include \"scripts_res.h\"\n\n") 284 | scripts_h.write("extern const u8* NOVEL_SCRIPTS[];\n\n") 285 | scripts_h.write("extern const s32 NOVEL_SCRIPTS_BYTES_COUNT[];\n\n") 286 | scripts_h.write("\n#endif\n") 287 | 288 | #print(script_label) 289 | 290 | for script_file in glob.glob('novel\\script\\**\\*.scr', recursive=True): 291 | f = script_file.replace("res\\", "", 1).replace(".scr", "") 292 | 293 | alias = f.replace("\\", "_").replace("novel_script_", "", 1).replace("-", "_").lower() 294 | 295 | if alias != "main": 296 | scripts_dir[alias] = len(scripts_dir) 297 | 298 | 299 | #print(scripts_dir) 300 | 301 | for script_file in glob.glob('novel\\script\\**\\*.scr', recursive=True): 302 | bytes_count = 0 303 | script = open(script_file, 'r', encoding="UTF8") 304 | out_file = script_file.replace("novel\\", "res\\").replace(".scr", ".bin").replace("-", "_") 305 | #print(out_file) 306 | out = open(out_file, 'wb') 307 | utflines = script.readlines() 308 | lines = [] 309 | for i in range(len(utflines)): 310 | lines.append(unicodedata.normalize('NFKD', utflines[i]).encode('ascii', 'ignore').decode('ascii')) 311 | 312 | for i in range(len(lines)): 313 | #lines[i] = lines[i].lstrip() 314 | line = lines[i].lstrip() 315 | coms = line.split() 316 | 317 | bytes_count += count_line(lines, i) 318 | c = line.split(" ")[0].replace("\n", "").replace("\t", "") 319 | if c == "bgload": 320 | out.write(functions["bgload"].to_bytes(1, "big")) 321 | t = line.replace("bgload ", "", 1).lower().replace("\\", "/").replace(".jpg", ".png").replace("\n", "").replace("\t", "").split(" ") 322 | key = t[0] 323 | if not ".png" in key: key += ".png" #To make it so we do not have to put the extension every time we use it 324 | try: 325 | out.write(backgrounds[key].to_bytes(2, "big")) 326 | except: 327 | print("\n\n\n------------------- ERROR------------------") 328 | print("\nThe background image " + key + " in script \"" + script_file + "\", line " + str(i+1) + " does not exists.") 329 | #print(backgrounds.keys()) 330 | #out.write(backgrounds[backgrounds.keys().0].to_bytes(1, "big")) 331 | exit() 332 | if len(t) == 2 and t[1].isnumeric(): 333 | out.write(int(t[1]).to_bytes(2, "big")) 334 | else: 335 | out.write((0).to_bytes(2, "big")) #16 in vnds 336 | 337 | 338 | elif c == "setimg": 339 | out.write(functions["setimg"].to_bytes(1, "big")) 340 | t = line.replace("setimg ", "", 1).lower().replace("\\", "/").replace(".jpg", ".png").replace("\n", "").replace("\t", "").split(" ") 341 | key = t[0] 342 | if not ".png" in key: key += ".png" #To make it so we do not have to put the extension every time we use it 343 | try: 344 | out.write(foregrounds[key].to_bytes(2, "big")) 345 | except: 346 | print("\n\n\n------------------- ERROR------------------") 347 | print("\nThe foreground image \"" + key + "\" in script \"" + script_file + "\", line " + str(i+1) + " does not exists.") 348 | exit() 349 | if len(t) > 1: 350 | #x = int((float(t[1]) * img_scale) / 8 + bg_left) 351 | x = int((float(t[1])) / 8 + bg_left) 352 | out.write(x.to_bytes(2, "big", signed=True)) 353 | else: 354 | out.write(int(0).to_bytes(2, "big", signed=True)) 355 | if len(t) > 2: 356 | #y = int((float(t[2]) * img_scale) / 8 + bg_top) 357 | y = int((float(t[2])) / 8 + bg_top) 358 | out.write(y.to_bytes(2, "big", signed=True)) 359 | else: 360 | out.write(int(0).to_bytes(2, "big", signed=True)) 361 | elif c == "sound": 362 | t = line.replace("sound ", "", 1).replace("\n", "").replace("\t", " ").strip().split(" ") 363 | t[0] = os.path.splitext(t[0].replace("\\", "/"))[0] 364 | if t[0] in sounds_positions.keys(): 365 | out.write(functions["sound"].to_bytes(1, "big")) 366 | out.write(sounds_positions[t[0]].to_bytes(2, "big")) 367 | if len(t) > 1: 368 | if int(t[1]) == -1: 369 | out.write(int(255).to_bytes(1, "big")) 370 | else: 371 | out.write(int(t[1]).to_bytes(1, "big")) 372 | else: 373 | out.write(int(1).to_bytes(1, "big")) 374 | elif t[0] == "~": 375 | out.write(functions["sound"].to_bytes(1, "big")) 376 | out.write(int(32700).to_bytes(2, "big")) 377 | out.write(int(1).to_bytes(1, "big")) 378 | pass 379 | elif c == "music": 380 | t = line.replace("music ", "", 1).replace("\n", "").replace("\t", " ").strip().split(" ") 381 | t[0] = os.path.splitext(t[0])[0] 382 | if t[0] in music_positions.keys(): 383 | out.write(functions["music"].to_bytes(1, "big")) 384 | out.write(int(music_positions[t[0]]).to_bytes(2, "big")) 385 | else: 386 | out.write(functions["music"].to_bytes(1, "big")) 387 | out.write(int(novel_stop_music).to_bytes(2, "big")) 388 | pass 389 | elif c == "text": 390 | lines_count +=1 391 | t = line.replace("text ", "", 1).replace("\n", "").replace("\t", " ").lstrip() 392 | if len(t) == 0 or t[0] == "~": 393 | continue 394 | out.write(functions["text"].to_bytes(1, "big")) 395 | out.write(t.encode()) 396 | out.write((0).to_bytes(1, "big")) 397 | elif c == "choice": 398 | out.write(functions["choice"].to_bytes(1, "big")) 399 | choice = line.replace("choice ", "", 1).replace("\n", "").replace("\t", " ").split("|") 400 | out.write(len(choice).to_bytes(1, "big")) 401 | for c in choice: 402 | out.write(c.strip().encode()) 403 | out.write((0).to_bytes(1, "big")) 404 | elif c == "setvar" or c == "gsetvar": 405 | coms[1] = coms[1].lower() 406 | if (coms[1] == "~"): 407 | out.write(functions["reset_vars"].to_bytes(1, "big")) 408 | continue 409 | out.write(functions["setvar"].to_bytes(1, "big")) 410 | try: 411 | out.write(script_var[coms[1]].to_bytes(2, "big")) 412 | except: 413 | print("\n\n\n------------------- ERROR------------------") 414 | print("Variable " + coms[1] + " in script \"" + script_file + "\", line " + str(i+1) + " does not exists\n") 415 | exit() 416 | if (coms[2] == "="): 417 | out.write(int(0).to_bytes(1, "big")) 418 | elif (coms[2] == "+"): 419 | out.write(int(1).to_bytes(1, "big")) 420 | elif (coms[2] == "-"): 421 | out.write(int(2).to_bytes(1, "big")) 422 | else: 423 | print("\n\n\n------------------- ERROR------------------") 424 | print("Unknown symbol " + coms[2] + " in script \"" + script_file + "\", line " + str(i+1) +"\n") 425 | exit() 426 | if coms[3].isnumeric(): 427 | out.write(int(coms[3]).to_bytes(2, "big")) 428 | # 0 it is a number to be added at the var 429 | out.write(int(0).to_bytes(1, "big")) 430 | else: 431 | 432 | coms[3] = coms[3].lower() 433 | if '"' in coms[3]: #It is a string so it is a .scr file or a label 434 | coms[3] = coms[3].replace('"', "") 435 | if ".scr" in coms[3]: #It is a .scr file 436 | try: 437 | key = coms[3].lower().replace(".scr", "") 438 | out.write(int(scripts_dir[key]).to_bytes(2, "big")) 439 | out.write(int(0).to_bytes(1, "big")) 440 | except: 441 | print(script_var) 442 | print("\n\n\n------------------- ERROR------------------") 443 | print("The .scr " + coms[3] + " in script \"" + script_file + "\", line " + str(i+1) + " does not exists") 444 | exit() 445 | else: 446 | try: 447 | alias = get_novel_script_alias(script_file) 448 | key = coms[3].lower() 449 | out.write(int(script_label[alias][key]).to_bytes(2, "big")) 450 | out.write(int(0).to_bytes(1, "big")) 451 | except: 452 | print(script_var) 453 | print("\n\n\n------------------- ERROR------------------") 454 | print("The label " + coms[3] + " in script \"" + script_file + "\", line " + str(i+1) + " does not exists") 455 | exit() 456 | else: 457 | try: 458 | out.write(int(script_var[coms[3]]).to_bytes(2, "big")) 459 | #1 it is another var to be added at the var 460 | out.write(int(1).to_bytes(1, "big")) 461 | except: 462 | print(script_var) 463 | print("\n\n\n------------------- ERROR------------------") 464 | print("The var " + coms[3] + " in script \"" + script_file + "\", line " + str(i+1) + " does not exists") 465 | exit() 466 | pass 467 | elif c == "gset_var": 468 | pass 469 | elif c == "if": 470 | out.write(functions["if"].to_bytes(1, "big")) 471 | #next is the type of the right hand value number or var 472 | t = line.replace("if ", "", 1).replace("\n", "").replace("\t", " ").split(" ") 473 | #out.write(int(script_var[t[0]]).to_bytes(2, "big")) 474 | try: 475 | out.write(int(script_var[coms[1].lower()]).to_bytes(2, "big")) 476 | except: 477 | print(script_var) 478 | print("\n\n\n------------------- ERROR------------------") 479 | print("The var " + coms[1] + " in script \"" + script_file + "\", line " + str(i+1) + " does not exists") 480 | exit() 481 | if coms[2] == "==": 482 | out.write((0).to_bytes(1, "big")) 483 | elif t[1] == "!=": 484 | out.write((1).to_bytes(1, "big")) 485 | elif t[1] == ">": 486 | out.write((2).to_bytes(1, "big")) 487 | elif t[1] == "<": 488 | out.write((3).to_bytes(1, "big")) 489 | elif t[1] == ">=": 490 | out.write((4).to_bytes(1, "big")) 491 | elif t[1] == "<=": 492 | out.write((5).to_bytes(1, "big")) 493 | else: 494 | print(line) 495 | exit() 496 | coms[3] = coms[3].replace(":", "") #Using : makes it easier to right if selections cause it behaves like python in the vscode 497 | #print(coms) 498 | if coms[3].isnumeric(): 499 | out.write(int(coms[3]).to_bytes(2, "big")) 500 | out.write((0).to_bytes(1, "big")) 501 | elif '"' in coms[3]: 502 | key = coms[3].replace('"', "").replace(".scr", "").strip() 503 | print(key) 504 | try: 505 | out.write(int(scripts_dir[key]).to_bytes(2, "big")) 506 | except: 507 | print(script_var) 508 | print("\n\n\n------------------- ERROR------------------") 509 | print("The .scr " + coms[3] + " in script \"" + script_file + "\", line " + str(i+1) + " does not exists") 510 | exit() 511 | out.write((0).to_bytes(1, "big")) 512 | else: 513 | try: 514 | out.write(int(script_var[coms[3]]).to_bytes(2, "big")) 515 | except: 516 | print(script_var) 517 | print("\n\n\n------------------- ERROR------------------") 518 | print("The var " + coms[3] + " in script \"" + script_file + "\", line " + str(i+1) + " does not exists") 519 | exit() 520 | out.write((1).to_bytes(1, "big")) 521 | try: 522 | out.write(count_to_fi(lines, i+1, 0).to_bytes(2, "big")) 523 | except: 524 | print(scripts_dir) 525 | print("\n\n\n------------------- ERROR------------------") 526 | print("\nThe if in script \"" + script_file + "\", line " + str(i+1) + " does not have fi.") 527 | exit() 528 | pass 529 | elif c == "fi": 530 | out.write(functions["fi"].to_bytes(1, "big")) 531 | pass 532 | elif c == "jump": 533 | t = line.replace("\t", "").replace("jump ", "", 1).lower().replace("\n", "").replace("/", "_").replace(".scr", "").split(" ") 534 | key = t[0].replace("-", "_").lower() 535 | if (key[0] == "$"): 536 | out.write(functions["retjump"].to_bytes(1, "big")) 537 | if len(t) > 1: 538 | if t[1][0] != "$": 539 | print(scripts_dir) 540 | print("\n\n\n------------------- ERROR------------------") 541 | print("\nThe jump ret can have only $ labels\"" + key + "\" in script \"" + script_file + "\", line " + str(i+1) + " does not exists.") 542 | exit() 543 | out.write((0).to_bytes(1, "big")) 544 | out.write(script_var[key.replace("$", "").strip()].to_bytes(2, "big")) 545 | out.write(int(script_var[t[1].replace("$", "").strip()]).to_bytes(2, "big")) 546 | out.write((0).to_bytes(1, "big")) #Extra one 0 to have the same size with regular jump 547 | else: 548 | out.write((1).to_bytes(1, "big")) 549 | out.write(script_var[key.replace("$", "").strip()].to_bytes(2, "big")) 550 | out.write((0).to_bytes(2, "big")) 551 | out.write((0).to_bytes(1, "big")) #Extra one 0 to have the same size with regular jump 552 | else: 553 | out.write(functions["jump"].to_bytes(1, "big")) 554 | try: 555 | out.write(scripts_dir[key].to_bytes(2, "big")) 556 | except: 557 | print(scripts_dir) 558 | print("\n\n\n------------------- ERROR------------------") 559 | print("\nThe jump \"" + key + "\" in script \"" + script_file + "\", line " + str(i+1) + " does not exists.") 560 | exit() 561 | if len(t) > 1: 562 | num = int(script_label[key][t[1].lower().strip()]) 563 | out.write(num.to_bytes(4, "big", signed=True)) 564 | else: 565 | out.write(int(0).to_bytes(4, "big", signed=True)) 566 | pass 567 | elif c == "delay": 568 | out.write(functions["delay"].to_bytes(1, "big")) 569 | try: 570 | out.write(int(coms[1]).to_bytes(2, "big", signed=True)) 571 | except: 572 | print("\n\n\n------------------- ERROR------------------") 573 | print("\nThe delay \"" + str(coms[1]) + "\" in script \"" + script_file + "\", line " + str(i+1) + " is too big.") 574 | exit() 575 | pass 576 | 577 | elif c == "random": 578 | print(line) 579 | pass 580 | elif c == "label": 581 | pass 582 | elif c == "goto": 583 | out.write(functions["goto"].to_bytes(1, "big")) 584 | t = line.replace("goto ", "", 1).lower().replace("\n", "", 1).replace("\t", "", 1).replace("/", "_").replace(".scr", "").split(" ") 585 | alias = get_novel_script_alias(script_file) 586 | try: 587 | pos = script_label[alias][t[0]] 588 | out.write(pos.to_bytes(4, "big", signed=True)) 589 | except: 590 | print("\n\n\n------------------- ERROR------------------") 591 | print("\nThe goto \"" + t[0] + "\" in script \"" + script_file + "\", line " + str(i+1) + " does not exists.") 592 | exit() 593 | 594 | 595 | pass 596 | elif c == "cleartext": 597 | print(line) 598 | pass 599 | elif c == "ifchoice": 600 | out.write(functions["ifchoice"].to_bytes(1, "big")) 601 | choice = line.replace("ifchoice ", "", 1).replace("\n", "").replace("\t", " ").split("|") 602 | out.write(len(choice).to_bytes(1, "big")) 603 | #types of is choice 0 no if 1 if 604 | #types of right hand is 0 for numbers 1 for var 605 | #the left hand number of var 606 | #the number of the var if it is var or the actual number 607 | #: separates ifs from the text of the choice 608 | #ifs 0 -> == | 1 -> != | 2 -> > | 3 -> < | 4 -> >= | 5 -> <= 609 | for c in choice: 610 | ifs = c.split(":") 611 | if len(ifs) == 1: 612 | 613 | out.write((0).to_bytes(1, "big")) 614 | out.write((0).to_bytes(1, "big")) 615 | out.write((0).to_bytes(2, "big")) 616 | out.write((0).to_bytes(2, "big")) 617 | out.write((0).to_bytes(1, "big")) 618 | 619 | out.write(c.strip().encode()) 620 | out.write((0).to_bytes(1, "big")) 621 | else: 622 | out.write((1).to_bytes(1, "big")) 623 | 624 | parts = ifs[0].strip().split(" ") 625 | #print(parts) 626 | if len(parts) < 3: #just a variable so it will return true if it is != 0 627 | out.write((0).to_bytes(1, "big")) 628 | try: 629 | out.write(script_var[parts[0].strip()].to_bytes(2, "big")) 630 | except: 631 | print("\n\n\n------------------- ERROR------------------") 632 | print("\nThe var in ifchoice \"" + parts[2] + "\" in script \"" + script_file + "\", line " + str(i+1) + " does not exists.") 633 | exit() 634 | out.write((1).to_bytes(1, "big")) 635 | else: 636 | if parts[2].isnumeric(): 637 | out.write((0).to_bytes(1, "big")) 638 | try: 639 | out.write(script_var[parts[0].strip()].to_bytes(2, "big")) 640 | except: 641 | print("\n\n\n------------------- ERROR------------------") 642 | print("\nThe var in ifchoice \"" + parts[2] + "\" in script \"" + script_file + "\", line " + str(i+1) + " does not exists.") 643 | exit() 644 | out.write(int(parts[2].strip()).to_bytes(2, "big")) 645 | else: 646 | 647 | if '"' in parts[2]: #It is a script file... srcipt labels are no accepted 648 | out.write((0).to_bytes(1, "big")) 649 | try: 650 | out.write(int(script_var[parts[0].strip()]).to_bytes(2, "big")) 651 | except: 652 | print("\n\n\n------------------- ERROR------------------") 653 | print("\nThe var in ifchoice \"" + parts[2] + "\" in script \"" + script_file + "\", line " + str(i+1) + " does not exists.") 654 | exit() 655 | scr = parts[2].lower().replace(".scr", "").replace('"', "").strip() 656 | print(scr) 657 | print(parts) 658 | print(scripts_dir[scr]) 659 | print(scripts_dir) 660 | 661 | try: 662 | out.write(int(scripts_dir[scr]).to_bytes(2, "big")) 663 | except: 664 | print(scripts_dir) 665 | print("\n\n\n------------------- ERROR------------------") 666 | print("The ifchoice .scr " + parts[2] + " in script \"" + script_file + "\", line " + str(i+1) + " does not exists") 667 | exit() 668 | else: 669 | out.write((1).to_bytes(1, "big")) 670 | try: 671 | out.write(int(script_var[parts[0].strip()]).to_bytes(2, "big")) 672 | except: 673 | print("\n\n\n------------------- ERROR------------------") 674 | print("\nThe var in ifchoice \"" + parts[2] + "\" in script \"" + script_file + "\", line " + str(i+1) + " does not exists.") 675 | exit() 676 | try: 677 | out.write(int(script_var[parts[2].strip()]).to_bytes(2, "big")) 678 | except: 679 | print("\n\n\n------------------- ERROR------------------") 680 | print("\nThe var in ifchoice \"" + parts[2] + "\" in script \"" + script_file + "\", line " + str(i+1) + " does not exists.") 681 | exit() 682 | comp = parts[1].strip() 683 | if comp == "==": 684 | out.write((0).to_bytes(1, "big")) 685 | elif comp == "!=": 686 | out.write((1).to_bytes(1, "big")) 687 | elif comp == ">": 688 | out.write((2).to_bytes(1, "big")) 689 | elif comp == "<": 690 | out.write((3).to_bytes(1, "big")) 691 | elif comp == ">=": 692 | out.write((4).to_bytes(1, "big")) 693 | elif comp == "<=": 694 | out.write((5).to_bytes(1, "big")) 695 | else: 696 | print("\n\n\n------------------- ERROR------------------") 697 | print("\nThe comparison in ifchoice \"" + parts[1] + "\" in script \"" + script_file + "\", line " + str(i+1) + " does not exists.") 698 | exit() 699 | 700 | #print(ifs[1]) 701 | out.write(ifs[1].strip().encode()) 702 | out.write((0).to_bytes(1, "big")) 703 | else: 704 | ############### EXTERNAL FUNCTIONS 705 | func = line.split(" ")[0].strip() 706 | if func in external_funcs: 707 | out.write(functions["external_func"].to_bytes(1, "big")) 708 | out.write(external_funcs[func].to_bytes(1, "big")) 709 | #print(script_file) 710 | #print(bytes_count) 711 | script.close() 712 | out.close() 713 | alias = out_file.replace("res\\", "", 1).replace("\\", "_").replace(".bin", "").replace(".", "_") 714 | #script_res.write("BIN " + alias + " " + out_file.replace("res\\", "", 1) + " 2 2 0 NONE FALSE\n") 715 | script_res.write("BIN " + alias + " " + out_file.replace("res\\", "", 1) + "\n") 716 | 717 | if alias != "script_main": 718 | scripts_c.write("\t" + alias + ",\n") 719 | script_byte_count.append(bytes_count) 720 | else: 721 | script_byte_count.insert(0,bytes_count) 722 | 723 | scripts_c.write("};\n\n") 724 | #scripts_h.write("extern const s32 NOVEL_SCRIPTS_BYTES_COUNT[];\n\n") 725 | scripts_c.write("const s32 NOVEL_SCRIPTS_BYTES_COUNT[] = {\n") 726 | for value in script_byte_count: 727 | scripts_c.write("\t" + str(value) + ",\n") 728 | scripts_c.write("};\n\n") 729 | scripts_c.close() 730 | 731 | 732 | 733 | 734 | variables_h = open("inc/novel_variables.h", "w") 735 | variables_h.write("#ifndef H_NOVEL_VARIABLES\n") 736 | variables_h.write("#define H_NOVEL_VARIABLES\n\n") 737 | variables_h.write("extern const int NOVEL_SAVE_CHECK_NUM;\n\n") 738 | variables_h.write("extern const int NOVEL_NUM_VARIABLES;\n") 739 | variables_h.write("extern const int NOVEL_NUM_GLOBAL_VARIABLES;\n") 740 | variables_h.write("extern int NOVEL_VARIABLES[];\n\n") 741 | 742 | variables_h.write("\n\n") 743 | variables_h.write("//VARABLES\n") 744 | for var in script_var: 745 | variables_h.write("#define NVAR_" + var + " NOVEL_VARIABLES[" + str(script_var[var])+ "]\n") 746 | 747 | variables_h.write("\n\n") 748 | variables_h.write("//FILES\n") 749 | for scr in scripts_dir: 750 | variables_h.write("#define NFILE_" + scr + " " + str(scripts_dir[scr])+ "\n") 751 | 752 | variables_h.write("\n\n") 753 | variables_h.write("//LABELS\n") 754 | for scr in script_label: 755 | for label in script_label[scr]: 756 | variables_h.write("#define NLABEL_" + scr + "_" + label + " " + str(script_label[scr][label])+ "\n") 757 | variables_h.write("\n\n") 758 | 759 | if not bad_filenames: 760 | variables_h.write("\n\n") 761 | variables_h.write("//BACKGROUNDS\n") 762 | for bg in backgrounds: 763 | variables_h.write("#define NBG_" + bg.replace(".png", "").replace(".jpg", "").strip() + " " + str(backgrounds[bg])+ "\n") 764 | variables_h.write("\n\n") 765 | 766 | variables_h.write("\n\n") 767 | variables_h.write("//FOREGROUNDS\n") 768 | for fg in foregrounds: 769 | variables_h.write("#define NFG_" + fg.replace(".png", "").replace(".jpg", "").strip() + " " + str(foregrounds[fg])+ "\n") 770 | variables_h.write("\n\n") 771 | else: 772 | variables_h.write("//BAD FILENAMES\n") 773 | variables_h.write("\n\n") 774 | 775 | variables_h.write("\n#endif\n") 776 | variables_h.close() 777 | 778 | var_c = open("src/novel_variables.c", "w") 779 | var_c.write("#include \"novel_variables.h\"\n") 780 | var_c.write("#include \"novel_external_functions.h\"\n\n") 781 | var_c.write("const int NOVEL_SAVE_CHECK_NUM = " + str(save_check) + ";\n\n") 782 | var_c.write("const int NOVEL_NUM_VARIABLES = " + str(len(script_var)) + ";\n") 783 | var_c.write("const int NOVEL_NUM_GLOBAL_VARIABLES = " + str(num_global_var) + ";\n\n") 784 | var_c.write("int NOVEL_VARIABLES[" + str(len(script_var)) + "];\n") 785 | var_c.write("void (*nv_external_functions[])() = {\n") 786 | for func in external_funcs: 787 | var_c.write("\t" + func + ",\n") 788 | var_c.write("};") 789 | var_c.close() 790 | 791 | 792 | if len(script_var) == 1: 793 | print("----------------------------") 794 | print("A Novel without Variables") 795 | 796 | #print(music_positions) 797 | print(script_var) 798 | 799 | print("Lines of text : " + str(lines_count)) --------------------------------------------------------------------------------