1// MIT License
2
3// Copyright (c) 2017 Vadim Grigoruk @nesbox // grigoruk@gmail.com
4
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22
23#include "console.h"
24#include "start.h"
25#include "tools.h"
26#include "studio/fs.h"
27#include "studio/net.h"
28#include "studio/config.h"
29#include "ext/png.h"
30#include "zip.h"
31#include "studio/demos.h"
32
33#if defined(TIC80_PRO)
34#include "studio/project.h"
35#else
36#include "cart.h"
37#endif
38
39#include <ctype.h>
40#include <string.h>
41
42#if !defined(__TIC_MACOSX__)
43#include <malloc.h>
44#endif
45
46#if defined (TIC_BUILD_WITH_LUA)
47#include <lua.h>
48#include <lauxlib.h>
49#include <lualib.h>
50#endif
51
52#include <sys/stat.h>
53
54#if defined(__EMSCRIPTEN__)
55#include <emscripten.h>
56#endif
57
58#define CONSOLE_CURSOR_COLOR tic_color_red
59#define CONSOLE_INPUT_COLOR tic_color_white
60#define CONSOLE_BACK_TEXT_COLOR tic_color_grey
61#define CONSOLE_FRONT_TEXT_COLOR tic_color_light_grey
62#define CONSOLE_ERROR_TEXT_COLOR tic_color_red
63#define CONSOLE_LINK_TEXT_COLOR tic_color_blue
64#define CONSOLE_CURSOR_BLINK_PERIOD TIC80_FRAMERATE
65#define CONSOLE_CURSOR_DELAY (TIC80_FRAMERATE / 2)
66#define CONSOLE_BUFFER_WIDTH (STUDIO_TEXT_BUFFER_WIDTH)
67#define CONSOLE_BUFFER_HEIGHT (STUDIO_TEXT_BUFFER_HEIGHT)
68#define CONSOLE_BUFFER_SCREENS 64
69#define CONSOLE_BUFFER_SCREEN (CONSOLE_BUFFER_WIDTH * CONSOLE_BUFFER_HEIGHT)
70#define CONSOLE_BUFFER_SIZE (CONSOLE_BUFFER_SCREEN * CONSOLE_BUFFER_SCREENS)
71#define CONSOLE_BUFFER_ROWS (CONSOLE_BUFFER_SIZE / CONSOLE_BUFFER_WIDTH)
72#define DEFAULT_CHMOD 0755
73
74#define HELP_CMD_LIST(macro) \
75 macro(version) \
76 macro(welcome) \
77 macro(spec) \
78 macro(ram) \
79 macro(vram) \
80 macro(commands) \
81 macro(api) \
82 macro(keys) \
83 macro(buttons) \
84 macro(startup) \
85 macro(terms) \
86 macro(license)
87
88#define IMPORT_CMD_LIST(macro) \
89 macro(binary) \
90 macro(tiles) \
91 macro(sprites) \
92 macro(map) \
93 macro(code) \
94 macro(screen)
95
96#define IMPORT_KEYS_LIST(macro) \
97 macro(bank) \
98 macro(x) \
99 macro(y) \
100 macro(w) \
101 macro(h) \
102 macro(vbank)
103
104#define EXPORT_CMD_LIST(macro) \
105 macro(win) \
106 macro(winxp) \
107 macro(linux) \
108 macro(rpi) \
109 macro(mac) \
110 macro(html) \
111 macro(binary) \
112 macro(tiles) \
113 macro(sprites) \
114 macro(map) \
115 macro(mapimg) \
116 macro(sfx) \
117 macro(music) \
118 macro(screen) \
119 macro(help)
120
121#if defined(TIC80_PRO)
122# define ALONE_KEY(macro) macro(alone)
123#else
124# define ALONE_KEY(macro)
125#endif
126
127#define EXPORT_KEYS_LIST(macro) \
128 macro(bank) \
129 macro(vbank) \
130 macro(id) \
131 ALONE_KEY(macro)
132
133static const char* WelcomeText =
134 "TIC-80 is a fantasy computer for making, playing and sharing tiny games.\n\n"
135 "It has built-in tools for development: code, sprites, maps, sound editors and the command line, "
136 "which is enough to create a mini retro game.\n"
137 "In the end, you will get a cartridge file, which can be stored and played on the website.\n\n"
138 "Also, the game can be packed into a player that works on all popular platforms and distributed as you wish.\n"
139 "To make a retro-style game, the whole creation process takes place under some technical limitations: "
140 "240x136 pixels display, 16 color palette, 256 8x8 color sprites, 4 channel sound, etc.";
141
142static const struct SpecRow {const char* section; const char* info;} SpecText1[] =
143{
144 {"DISPLAY", "240x136 pixels, 16 colors palette."},
145 {"INPUT", "4 gamepads with 8 buttons / mouse / keyboard."},
146 {"SPRITES", "256 8x8 tiles and 256 8x8 sprites."},
147 {"MAP", "240x136 cells, 1920x1088 pixels."},
148 {"SOUND", "4 channels with configurable waveforms."},
149 {"CODE", "64KB of $LANG_NAMES$.",
150 },
151};
152
153static const char* TermsText =
154 "## Terms of Use\n"
155 "- All cartridges posted on the " TIC_WEBSITE " website are the property of their authors.\n"
156 "- Do not redistribute the cartridge without permission, directly from the author.\n"
157 "- By uploading cartridges to the site, you grant Nesbox the right to freely use and distribute them. "
158 "All other rights by default remain with the author.\n"
159 "- Do not post material that violates copyright, obscenity or any other laws.\n"
160 "- Nesbox reserves the right to remove or filter any material without prior notice.\n\n"
161 "## Privacy Policy\n"
162 "We store only the user's email and password in encrypted form and will not transfer any personal "
163 "information to third parties without explicit permission.";
164
165static const char* LicenseText =
166 "## MIT License\n"
167 "\n"
168 "Copyright (c) 2017-" TIC_VERSION_YEAR " Vadim Grigoruk @nesbox // grigoruk@gmail.com\n"
169 "\n"
170 "Permission is hereby granted, free of charge, to any person obtaining a copy "
171 "of this software and associated documentation files (the 'Software'), to deal "
172 "in the Software without restriction, including without limitation the rights "
173 "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell "
174 "copies of the Software, and to permit persons to whom the Software is "
175 "furnished to do so, subject to the following conditions: "
176 "The above copyright notice and this permission notice shall be included in all "
177 "copies or substantial portions of the Software.\n"
178 "\n"
179 "THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR "
180 "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, "
181 "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE "
182 "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER "
183 "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, "
184 "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE "
185 "SOFTWARE.";
186
187static const struct StartupOption {const char* name; const char* help;} StartupOptions[] =
188{
189#define CMD_PARAMS_DEF(name, ctype, type, post, help) {#name post, help},
190 CMD_PARAMS_LIST(CMD_PARAMS_DEF)
191#undef CMD_PARAMS_DEF
192};
193
194struct CommandDesc
195{
196 char* command;
197
198 struct Param
199 {
200 char* key;
201 char* val;
202 }* params;
203
204 s32 count;
205
206 char* src;
207};
208
209static const char* PngExt = PNG_EXT;
210
211#if defined(__EMSCRIPTEN__)
212#define CAN_ADDGET_FILE 1
213#endif
214
215
216// You must free the result if result is non-NULL. TODO: find a better place for this function?
217char *str_replace(const char *orig, char *rep, char *with) {
218 char *result; // the return string
219 const char *ins; // the next insert point
220 char *tmp; // varies
221 s32 len_rep; // length of rep (the string to remove)
222 s32 len_with; // length of with (the string to replace rep with)
223 s32 len_front; // distance between rep and end of last rep
224 s32 count; // number of replacements
225
226 // sanity checks and initialization
227 if (!orig || !rep)
228 return NULL;
229 len_rep = strlen(rep);
230 if (len_rep == 0)
231 return NULL; // empty rep causes infinite loop during count
232 if (!with)
233 with = "";
234 len_with = strlen(with);
235
236 // count the number of replacements needed
237 ins = orig;
238 for (count = 0; tmp = strstr(ins, rep); ++count) {
239 ins = tmp + len_rep;
240 }
241
242 tmp = result = malloc(strlen(orig) + (len_with - len_rep) * count + 1);
243
244 if (!result)
245 return NULL;
246
247 // first time through the loop, all the variable are set correctly
248 // from here on,
249 // tmp points to the end of the result string
250 // ins points to the next occurrence of rep in orig
251 // orig points to the remainder of orig after "end of rep"
252 while (count--) {
253 ins = strstr(orig, rep);
254 len_front = ins - orig;
255 tmp = strncpy(tmp, orig, len_front) + len_front;
256 tmp = strcpy(tmp, with) + len_with;
257 orig += len_front + len_rep; // move to next "end of rep"
258 }
259 strcpy(tmp, orig);
260 return result;
261}
262
263static char* replaceHelpTokens(const char* text)
264{
265 char langnames[10240] = {0};
266 char langextensions[10240] = {0};
267 char langnamespipe[10240] = {0};
268
269 FOR_EACH_LANG(ln)
270 bool isLast = *(conf+1) == NULL;
271 bool isSecondToLast = *(conf+2) == NULL;
272
273 strcat(langnames, ln->name);
274 if (!isLast)
275 strcat(langnames, isSecondToLast ? " or " : ", ");
276
277 strcat(langextensions, ln->fileExtension);
278 strcat(langextensions, " ");
279
280 strcat(langnamespipe, ln->name);
281 if (!isLast)
282 strcat(langnamespipe, "|");
283 FOR_EACH_LANG_END
284
285 char* replaced1 = str_replace(text, "$LANG_NAMES$", langnames);
286 char* replaced2 = str_replace(replaced1, "$LANG_EXTENSIONS$", langextensions);
287 char* replaced3 = str_replace(replaced2, "$LANG_NAMES_PIPE$", langnamespipe);
288 free(replaced2);
289 free(replaced1);
290 return replaced3;
291}
292
293
294static const char* getName(const char* name, const char* ext)
295{
296 static char path[TICNAME_MAX];
297
298 strcpy(path, name);
299
300 size_t ps = strlen(path);
301 size_t es = strlen(ext);
302
303 if(!(ps > es && strstr(path, ext) + es == path + ps))
304 strcat(path, ext);
305
306 return path;
307}
308
309static const char* getCartName(const char* name)
310{
311 return getName(name, CART_EXT);
312}
313
314static void scrollBuffer(char* buffer)
315{
316 memmove(buffer, buffer + CONSOLE_BUFFER_WIDTH, CONSOLE_BUFFER_SIZE - CONSOLE_BUFFER_WIDTH);
317 memset(buffer + CONSOLE_BUFFER_SIZE - CONSOLE_BUFFER_WIDTH, 0, CONSOLE_BUFFER_WIDTH);
318}
319
320static void scrollConsole(Console* console)
321{
322 while(console->cursor.pos.y >= CONSOLE_BUFFER_HEIGHT * CONSOLE_BUFFER_SCREENS)
323 {
324 scrollBuffer(console->text);
325 scrollBuffer((char*)console->color);
326
327 console->cursor.pos.y--;
328 }
329
330 size_t inputLines = (console->cursor.pos.x + console->input.pos) / CONSOLE_BUFFER_WIDTH;
331 s32 minScroll = console->cursor.pos.y + inputLines - CONSOLE_BUFFER_HEIGHT + 1;
332 if(console->scroll.pos < minScroll)
333 console->scroll.pos = minScroll;
334}
335
336static void setSymbol(Console* console, char sym, u8 color, s32 offset)
337{
338 console->text[offset] = sym;
339 console->color[offset] = color;
340}
341
342static s32 cursorOffset(Console* console)
343{
344 return console->cursor.pos.x + console->cursor.pos.y * CONSOLE_BUFFER_WIDTH;
345}
346
347static tic_point cursorPos(Console* console)
348{
349 s32 offset = cursorOffset(console) + console->input.pos;
350 return (tic_point)
351 {
352 offset % CONSOLE_BUFFER_WIDTH,
353 offset / CONSOLE_BUFFER_WIDTH
354 };
355}
356
357static void nextLine(Console* console)
358{
359 console->cursor.pos.x = 0;
360 console->cursor.pos.y++;
361}
362
363static bool iswrap(char sym)
364{
365 switch(sym)
366 {
367 case '|': return true;
368 }
369
370 return isspace(sym);
371}
372
373static void consolePrintOffset(Console* console, const char* text, u8 color, s32 wrapLineOffset)
374{
375#ifndef BAREMETALPI
376 printf("%s", text);
377#endif
378
379 console->cursor.pos = cursorPos(console);
380
381 for(const char* ptr = text, *next = ptr; *ptr; ptr++)
382 {
383 char symbol = *ptr;
384
385 scrollConsole(console);
386
387 if (symbol == '\n')
388 nextLine(console);
389 else
390 {
391 if(!iswrap(symbol))
392 {
393 const char* cur = ptr;
394 s32 len = CONSOLE_BUFFER_WIDTH;
395
396 while(*cur && !iswrap(*cur++)) len--;
397
398 if(len > 0 && len <= console->cursor.pos.x)
399 {
400 nextLine(console);
401 console->cursor.pos.x = wrapLineOffset;
402 }
403 }
404
405 setSymbol(console, symbol, iswrap(symbol) ? tic_color_dark_grey : color, cursorOffset(console));
406
407 console->cursor.pos.x++;
408
409 if (console->cursor.pos.x >= CONSOLE_BUFFER_WIDTH)
410 nextLine(console);
411 }
412 }
413
414 console->input.text = console->text + cursorOffset(console);
415 console->input.pos = 0;
416}
417
418static void consolePrint(Console* console, const char* text, u8 color)
419{
420 consolePrintOffset(console, text, color, 0);
421}
422
423static void printBack(Console* console, const char* text)
424{
425 consolePrint(console, text, CONSOLE_BACK_TEXT_COLOR);
426}
427
428static void printFront(Console* console, const char* text)
429{
430 consolePrint(console, text, CONSOLE_FRONT_TEXT_COLOR);
431}
432
433static void printLink(Console* console, const char* text)
434{
435 consolePrint(console, text, CONSOLE_LINK_TEXT_COLOR);
436}
437
438static void printError(Console* console, const char* text)
439{
440 consolePrint(console, text, CONSOLE_ERROR_TEXT_COLOR);
441}
442
443static void printLine(Console* console)
444{
445 consolePrint(console, "\n", 0);
446}
447
448static void clearSelection(Console* console)
449{
450 ZEROMEM(console->select);
451}
452
453static void commandDoneLine(Console* console, bool newLine)
454{
455 if(!console->args.cli)
456 {
457 if(newLine)
458 printLine(console);
459
460 char dir[TICNAME_MAX];
461 tic_fs_dir(console->fs, dir);
462 if(strlen(dir))
463 printBack(console, dir);
464
465 printFront(console, ">");
466 }
467
468 console->active = true;
469
470 clearSelection(console);
471
472 FREE(console->desc->src);
473 FREE(console->desc->command);
474 FREE(console->desc->params);
475
476 memset(console->desc, 0, sizeof(CommandDesc));
477}
478
479static void commandDone(Console* console)
480{
481 commandDoneLine(console, true);
482}
483
484static inline void drawChar(tic_mem* tic, char symbol, s32 x, s32 y, u8 color, bool alt)
485{
486 tic_api_print(tic, (char[]){symbol, '\0'}, x, y, color, true, 1, alt);
487}
488
489static void drawCursor(Console* console)
490{
491 if(!console->active)
492 return;
493
494 tic_point pos = cursorPos(console);
495 pos.x *= STUDIO_TEXT_WIDTH;
496 pos.y -= console->scroll.pos;
497 pos.y *= STUDIO_TEXT_HEIGHT;
498
499 u8 symbol = console->input.text[console->input.pos];
500
501 bool inverse = console->cursor.delay || console->tickCounter % CONSOLE_CURSOR_BLINK_PERIOD < CONSOLE_CURSOR_BLINK_PERIOD / 2;
502
503 if(inverse)
504 tic_api_rect(console->tic, pos.x - 1, pos.y - 1, TIC_FONT_WIDTH + 1, TIC_FONT_HEIGHT + 1, CONSOLE_CURSOR_COLOR);
505
506 drawChar(console->tic, symbol, pos.x, pos.y, inverse ? TIC_COLOR_BG : CONSOLE_INPUT_COLOR, false);
507}
508
509static void drawConsoleText(Console* console)
510{
511 tic_mem* tic = console->tic;
512 const char* ptr = console->text + console->scroll.pos * CONSOLE_BUFFER_WIDTH;
513 const u8* colorPointer = console->color + console->scroll.pos * CONSOLE_BUFFER_WIDTH;
514
515 const char* end = ptr + CONSOLE_BUFFER_SCREEN;
516 tic_point pos = {0};
517
518 struct
519 {
520 const char* start;
521 const char* end;
522 } select =
523 {
524 console->select.start,
525 console->select.end
526 };
527
528 if(select.start > select.end)
529 SWAP(select.start, select.end, const char*);
530
531 while(ptr < end)
532 {
533 char symbol = *ptr++;
534 u8 color = *colorPointer++;
535 bool hasSymbol = symbol && symbol != ' ';
536 bool drawSelection = ptr > select.start && ptr <= select.end;
537 s32 x = pos.x * STUDIO_TEXT_WIDTH;
538 s32 y = pos.y * STUDIO_TEXT_HEIGHT;
539
540 if(drawSelection)
541 tic_api_rect(tic, x, y - 1, STUDIO_TEXT_WIDTH, STUDIO_TEXT_HEIGHT, hasSymbol ? color : CONSOLE_INPUT_COLOR);
542
543 if(hasSymbol)
544 drawChar(console->tic, symbol, x, y, drawSelection ? TIC_COLOR_BG : color, false);
545
546 if(++pos.x == CONSOLE_BUFFER_WIDTH)
547 {
548 pos.y++;
549 pos.x = 0;
550 }
551 }
552}
553
554static void processConsoleHome(Console* console)
555{
556 console->input.pos = 0;
557}
558
559static void processConsoleEnd(Console* console)
560{
561 console->input.pos = strlen(console->input.text);
562}
563
564static s32 getInputOffset(Console* console)
565{
566 return (console->input.text - console->text) + console->input.pos;
567}
568
569static void deleteText(Console* console, s32 start, s32 end)
570{
571 s32 offset = console->input.text - console->text;
572 s32 size = CONSOLE_BUFFER_SIZE - offset - end;
573 memmove(console->input.text + start, console->input.text + end, size);
574
575 u8* color = console->color + offset;
576 memmove(color + start, color + end, size);
577}
578
579static void processConsoleDel(Console* console)
580{
581 deleteText(console, console->input.pos, console->input.pos + 1);
582}
583
584static void processConsoleBackspace(Console* console)
585{
586 if(console->input.pos > 0)
587 {
588 console->input.pos--;
589
590 processConsoleDel(console);
591 }
592}
593
594static void onHelpCommand(Console* console);
595
596static void onExitCommand(Console* console)
597{
598 exitStudio(console->studio);
599 commandDone(console);
600}
601
602static void onEditCommand(Console* console)
603{
604 gotoCode(console->studio);
605 commandDone(console);
606}
607
608static void loadCartSection(Console* console, const tic_cartridge* cart, const char* section)
609{
610 tic_mem* tic = console->tic;
611
612 static const struct Section
613 {
614 const char* name;
615 s32 offset;
616 s32 size;
617 } Sections[] =
618 {
619#define SECTION_DEF(name, ...) {#name, offsetof(tic_bank, name), sizeof(tic_ ## name)},
620 TIC_SYNC_LIST(SECTION_DEF)
621#undef SECTION_DEF
622 };
623
624 if(section)
625 {
626 if(strcmp(section, "code") == 0)
627 memcpy(&tic->cart.code, &cart->code, sizeof(tic_code));
628 else
629 FOR(const struct Section*, it, Sections)
630 if(strcmp(section, it->name) == 0)
631 {
632 memcpy((u8*)&tic->cart.bank0 + it->offset, (const u8*)&cart->bank0 + it->offset, it->size);
633 break;
634 }
635 }
636 else
637 memcpy(&tic->cart, cart, sizeof(tic_cartridge));
638}
639
640static const char* getDemoCartPath(char* path, tic_script_config* script)
641{
642 //printf("Demo cart path of %s: ", script->name);
643 strcpy(path, TIC_LOCAL_VERSION "default_");
644 strcat(path, script->name);
645 strcat(path, ".tic");
646
647 //printf("%s\n", path);
648
649 return path;
650}
651
652static void* getDemoCart(Console* console, tic_script_config* script, s32* size)
653{
654 char path[1024];
655 getDemoCartPath(path, script);
656
657 {
658 void* data = tic_fs_loadroot(console->fs, path, size);
659
660 if(data && *size)
661 return data;
662 }
663 tic_script_config_extra* ex = getConfigExtra(script);
664
665 const u8* demo = ex->demoRom;
666 s32 romSize = ex->demoRomSize;
667
668 u8* data = calloc(1, sizeof(tic_cartridge));
669
670 if(data)
671 {
672 *size = tic_tool_unzip(data, sizeof(tic_cartridge), demo, romSize);
673
674 if(*size)
675 tic_fs_saveroot(console->fs, path, data, *size, false);
676 }
677
678 return data;
679}
680
681static void setCartName(Console* console, const char* name, const char* path)
682{
683 if(console->rom.name != name)
684 strcpy(console->rom.name, name);
685
686 if(console->rom.path != path)
687 strcpy(console->rom.path, path);
688}
689
690static void onLoadDemoCommandConfirmed(Console* console, tic_script_config* script)
691{
692 void* data = NULL;
693 s32 size = 0;
694
695 {
696 char path[1024];
697 getDemoCartPath(path, script);
698 const char* name = getCartName(path);
699 setCartName(console, name, tic_fs_path(console->fs, name));
700 }
701
702 data = getDemoCart(console, script, &size);
703 tic_cart_load(&console->tic->cart, data, size);
704 tic_api_reset(console->tic);
705
706 studioRomLoaded(console->studio);
707
708 printBack(console, "\ncart ");
709 printFront(console, console->rom.name);
710 printBack(console, " loaded!\n");
711
712 free(data);
713}
714
715static void onCartLoaded(Console* console, const char* name, const char* section)
716{
717 tic_api_reset(console->tic);
718
719 if(!section)
720 setCartName(console, name, tic_fs_path(console->fs, name));
721
722 studioRomLoaded(console->studio);
723
724 printBack(console, "\ncart ");
725 printFront(console, console->rom.name);
726 printBack(console, " loaded!\nuse ");
727 printFront(console, "RUN");
728 printBack(console, " command to run it\n");
729
730}
731
732static inline tic_cartridge* newCart()
733{
734 return malloc(sizeof(tic_cartridge));
735}
736
737static void updateProject(Console* console)
738{
739 tic_mem* tic = console->tic;
740 const char* path = console->rom.path;
741
742 if(*path)
743 {
744 s32 size = 0;
745 void* data = fs_read(path, &size);
746
747 if(data) SCOPE(free(data))
748 {
749#if defined(TIC80_PRO)
750 if(tic_project_ext(path))
751 tic_project_load(console->rom.name, data, size, &tic->cart);
752 else
753#endif
754 tic_cart_load(&tic->cart, data, size);
755
756 studioRomLoaded(console->studio);
757 }
758 }
759}
760
761typedef struct
762{
763 Console* console;
764 char* name;
765 char* section;
766 fs_done_callback callback;
767 void* calldata;
768} LoadByHashData;
769
770static void loadByHashDone(const u8* buffer, s32 size, void* data)
771{
772 LoadByHashData* loadByHashData = data;
773 Console* console = loadByHashData->console;
774
775 tic_cartridge* cart = newCart();
776
777 SCOPE(free(cart))
778 {
779 tic_cart_load(cart, buffer, size);
780 loadCartSection(console, cart, loadByHashData->section);
781 onCartLoaded(console, loadByHashData->name, loadByHashData->section);
782 }
783
784 if (loadByHashData->callback)
785 loadByHashData->callback(loadByHashData->calldata);
786
787 FREE(loadByHashData->name);
788 FREE(loadByHashData->section);
789 FREE(loadByHashData);
790
791 commandDone(console);
792}
793
794static void loadByHash(Console* console, const char* name, const char* hash, const char* section, fs_done_callback callback, void* data)
795{
796 console->active = false;
797
798 LoadByHashData loadByHashData = { console, strdup(name), section ? strdup(section) : NULL, callback, data};
799 tic_fs_hashload(console->fs, name, hash, loadByHashDone, MOVE(loadByHashData));
800}
801
802typedef struct
803{
804 Console* console;
805 char* name;
806 char* hash;
807 char* section;
808} LoadPublicCartData;
809
810static bool compareFilename(const char* name, const char* title, const char* hash, s32 id, void* data, bool dir)
811{
812 LoadPublicCartData* loadPublicCartData = data;
813 Console* console = loadPublicCartData->console;
814
815 if (strcmp(name, loadPublicCartData->name) == 0 && hash && strlen(hash))
816 {
817 loadPublicCartData->hash = strdup(hash);
818 return false;
819 }
820
821 return true;
822}
823
824static void fileFound(void* data)
825{
826 LoadPublicCartData* loadPublicCartData = data;
827 Console* console = loadPublicCartData->console;
828
829 if (loadPublicCartData->hash)
830 loadByHash(console, loadPublicCartData->name, loadPublicCartData->hash, loadPublicCartData->section, NULL, NULL);
831 else
832 {
833 char msg[TICNAME_MAX];
834 sprintf(msg, "\nerror: `%s` file not loaded", loadPublicCartData->name);
835 printError(console, msg);
836 commandDone(console);
837 }
838
839 FREE(loadPublicCartData->name);
840 FREE(loadPublicCartData->hash);
841 FREE(loadPublicCartData->section);
842 FREE(loadPublicCartData);
843}
844
845static bool printUsage(Console* console, const char* command);
846
847static void onLoadCommandConfirmed(Console* console)
848{
849 if(console->desc->count > 0)
850 {
851 tic_mem* tic = console->tic;
852
853 const char* param = console->desc->params->key;
854 const char* name = getCartName(param);
855 const char* section = console->desc->count > 1 ? console->desc->params[1].key : NULL;
856
857 if(section)
858 {
859 static const char* Sections[] =
860 {
861 "code",
862#define SECTION_DEF(name, ...) #name,
863 TIC_SYNC_LIST(SECTION_DEF)
864#undef SECTION_DEF
865 };
866
867 bool found = false;
868 for(const char** it = Sections, **end = it + COUNT_OF(Sections); it != end; ++it)
869 if(strcmp(*it, section) == 0)
870 {
871 found = true;
872 break;
873 }
874
875 if(!found)
876 {
877 printError(console, "\nunknown section: ");
878 printError(console, section);
879 printLine(console);
880 printUsage(console, console->desc->command);
881 commandDone(console);
882 return;
883 }
884 }
885
886 if (tic_fs_ispubdir(console->fs))
887 {
888 LoadPublicCartData loadPublicCartData = { console, strdup(name), NULL, section ? strdup(section) : NULL };
889 tic_fs_enum(console->fs, compareFilename, fileFound, MOVE(loadPublicCartData));
890
891 return;
892 }
893 else
894 {
895 s32 size = 0;
896 void* data = strcmp(name, CONFIG_TIC_PATH) == 0
897 ? tic_fs_loadroot(console->fs, name, &size)
898 : tic_fs_load(console->fs, name, &size);
899
900 if(data) SCOPE(free(data))
901 {
902 tic_cartridge* cart = newCart();
903
904 SCOPE(free(cart))
905 {
906 tic_cart_load(cart, data, size);
907 loadCartSection(console, cart, section);
908 onCartLoaded(console, name, section);
909 }
910 }
911 else if(tic_tool_has_ext(param, PngExt) && tic_fs_exists(console->fs, param))
912 {
913 png_buffer buffer;
914 buffer.data = tic_fs_load(console->fs, param, &buffer.size);
915
916 SCOPE(free(buffer.data))
917 {
918 tic_cartridge* cart = loadPngCart(buffer);
919
920 if(cart) SCOPE(free(cart))
921 {
922 loadCartSection(console, cart, section);
923 onCartLoaded(console, param, section);
924 }
925 else printError(console, "\npng cart loading error");
926 }
927 }
928 else
929 {
930 const char* name = param;
931
932#if defined(TIC80_PRO)
933 if(tic_project_ext(name))
934 {
935 void* data = tic_fs_load(console->fs, name, &size);
936
937 if(data) SCOPE(free(data))
938 {
939 tic_cartridge* cart = newCart();
940
941 SCOPE(free(cart))
942 {
943 tic_project_load(name, data, size, cart);
944 loadCartSection(console, cart, section);
945 onCartLoaded(console, name, section);
946 }
947 }
948 else printError(console, "\nproject loading error");
949
950 }
951 else printError(console, "\nfile not found");
952#else
953 if(tic_project_ext(name)) {
954 printError(console, "\nproject loading error");
955 printFront(console, "\nThis version only supports binary .png or .tic cartridges.");
956 printLine(console);
957 printFront(console, "\nTIC-80 ");
958 consolePrint(console,"PRO",tic_color_light_blue);
959 printFront(console, " is needed for text files.");
960 printLine(console);
961 printFront(console, "\nLearn more:\n");
962 printLink(console, "https://tic80.com/pro");
963 } else {
964 printError(console, "\ncart loading error");
965 }
966
967#endif
968 }
969 }
970 }
971 else
972 printUsage(console, console->desc->command);
973
974 commandDone(console);
975}
976
977typedef void(*ConsoleConfirmCallback)(Console* console);
978
979typedef struct
980{
981 Console* console;
982 ConsoleConfirmCallback callback;
983} CommandConfirmData;
984
985static void onConfirm(Studio* studio, bool yes, void* data)
986{
987 CommandConfirmData* confirmData = (CommandConfirmData*)data;
988
989 if(yes)
990 {
991 confirmData->callback(confirmData->console);
992 }
993 else commandDone(confirmData->console);
994
995 free(confirmData);
996}
997
998static void confirmCommand(Console* console, const char** text, s32 rows, ConsoleConfirmCallback callback)
999{
1000 if(console->args.cli)
1001 {
1002 for(s32 i = 0; i < rows; i++)
1003 {
1004 printError(console, text[i]);
1005 printLine(console);
1006 }
1007
1008 commandDone(console);
1009 }
1010 else
1011 {
1012 CommandConfirmData data = {console, callback};
1013 confirmDialog(console->studio, text, rows, onConfirm, MOVE(data));
1014 }
1015}
1016
1017typedef void(*LoadDemoConfirmCallback)(Console* console, tic_script_config* script);
1018
1019typedef struct
1020{
1021 Console* console;
1022 LoadDemoConfirmCallback callback;
1023 tic_script_config* script;
1024} LoadDemoConfirmData;
1025
1026static void onLoadDemoConfirm(Studio* studio, bool yes, void* data)
1027{
1028 LoadDemoConfirmData* demoData = (LoadDemoConfirmData*)data;
1029
1030 if(yes)
1031 {
1032 demoData->callback(demoData->console, demoData->script);
1033 }
1034 else commandDone(demoData->console);
1035
1036 free(demoData);
1037}
1038
1039static const char* LoadWarningRows[] =
1040{
1041 "WARNING!",
1042 "You have unsaved changes",
1043 "Do you really want to load cart?",
1044};
1045
1046static void onLoadDemoCommand(Console* console, tic_script_config* script)
1047{
1048 if(studioCartChanged(console->studio))
1049 {
1050 LoadDemoConfirmData data = {console, onLoadDemoCommandConfirmed, script};
1051 confirmDialog(console->studio, LoadWarningRows, COUNT_OF(LoadWarningRows), onLoadDemoConfirm, MOVE(data));
1052 }
1053 else
1054 {
1055 onLoadDemoCommandConfirmed(console, script);
1056 }
1057}
1058
1059static void onLoadCommand(Console* console)
1060{
1061 if(studioCartChanged(console->studio))
1062 {
1063 confirmCommand(console, LoadWarningRows, COUNT_OF(LoadWarningRows), onLoadCommandConfirmed);
1064 }
1065 else
1066 {
1067 onLoadCommandConfirmed(console);
1068 }
1069}
1070
1071static void loadDemo(Console* console, tic_script_config* script)
1072{
1073 if (script == NULL) script = Languages[0]; // can be called with null, meaning first language (Lua)
1074 s32 size = 0;
1075 u8* data = getDemoCart(console, script, &size);
1076
1077 if(data)
1078 {
1079 tic_cart_load(&console->tic->cart, data, size);
1080 tic_api_reset(console->tic);
1081
1082 free(data);
1083 }
1084
1085 memset(console->rom.name, 0, sizeof console->rom.name);
1086
1087 studioRomLoaded(console->studio);
1088}
1089
1090static void onNewCommandConfirmed(Console* console)
1091{
1092 bool done = false;
1093
1094 if(console->desc->count)
1095 {
1096 const char* param = console->desc->params->key;
1097
1098 FOR_EACH_LANG(ln)
1099 if(strcmp(param, ln->name) == 0)
1100 {
1101 loadDemo(console, ln);
1102 done = true;
1103 }
1104 FOR_EACH_LANG_END
1105
1106 if(!done)
1107 {
1108 printError(console, "\nunknown parameter: ");
1109 printError(console, param);
1110 commandDone(console);
1111 return;
1112 }
1113 }
1114 else
1115 {
1116 printError(console, "\nerror: choose a language for the new cart.");
1117 printUsage(console, console->desc->command);
1118 }
1119
1120 if(done) printBack(console, "\nnew cart has been created");
1121 else printError(console, "\ncart not created");
1122
1123 commandDone(console);
1124}
1125
1126static void onNewCommand(Console* console)
1127{
1128 if(studioCartChanged(console->studio))
1129 {
1130 static const char* Rows[] =
1131 {
1132 "WARNING!",
1133 "You have unsaved changes",
1134 "Do you really want to create new cart?",
1135 };
1136
1137 confirmCommand(console, Rows, COUNT_OF(Rows), onNewCommandConfirmed);
1138 }
1139 else
1140 {
1141 onNewCommandConfirmed(console);
1142 }
1143}
1144
1145static void insertInputText(Console* console, const char* text)
1146{
1147 s32 size = strlen(text);
1148 s32 offset = getInputOffset(console);
1149
1150 if(size < CONSOLE_BUFFER_SIZE - offset)
1151 {
1152 char* pos = console->text + offset;
1153 u8* color = console->color + offset;
1154
1155 {
1156 s32 len = strlen(pos);
1157 memmove(pos + size, pos, len);
1158 memmove(color + size, color, len);
1159 }
1160
1161 memcpy(pos, text, size);
1162 memset(color, CONSOLE_INPUT_COLOR, size);
1163
1164 console->input.pos += size;
1165 }
1166
1167 clearSelection(console);
1168}
1169
1170typedef struct
1171{
1172 Console* console;
1173 char* incompleteWord; // Original word that's being completed.
1174 char* options; // Options to show to the user.
1175 char* commonPrefix; // Common prefix of all options.
1176} TabCompleteData;
1177
1178static void addTabCompleteOption(TabCompleteData* data, const char* option)
1179{
1180 if (strstr(option, data->incompleteWord) == option)
1181 {
1182 // Possibly reduce the common prefix of all possible options.
1183 if (strlen(data->options) == 0)
1184 {
1185 // This is the first option to be added. Initialize the prefix.
1186 strncpy(data->commonPrefix, option, CONSOLE_BUFFER_SCREEN);
1187 }
1188 else
1189 {
1190 // Only leave the longest common prefix.
1191 char* tmpCommonPrefix = data->commonPrefix;
1192 char* tmpOption = (char*) option;
1193
1194 while (*tmpCommonPrefix && *tmpOption && *tmpCommonPrefix == *tmpOption) {
1195 tmpCommonPrefix++;
1196 tmpOption++;
1197 }
1198
1199 *tmpCommonPrefix = 0;
1200 }
1201
1202 // The option matches the incomplete word, add it to the list.
1203 strncat(data->options, option, CONSOLE_BUFFER_SCREEN);
1204 strncat(data->options, " ", CONSOLE_BUFFER_SCREEN);
1205 }
1206}
1207
1208// Used to show tab-complete options, for example.
1209static void provideHint(Console* console, const char* hint)
1210{
1211 char* input = malloc(CONSOLE_BUFFER_SCREEN);
1212 strncpy(input, console->input.text, CONSOLE_BUFFER_SCREEN);
1213
1214 printLine(console);
1215 printBack(console, hint);
1216 commandDone(console);
1217 insertInputText(console, input);
1218
1219 free(input);
1220}
1221
1222static void finishTabComplete(const TabCompleteData* data)
1223{
1224 bool anyOptions = strlen(data->options) > 0;
1225 if (anyOptions) {
1226 // Adding one at the right because all options end with a space.
1227 bool justOneOptionLeft = strlen(data->options) == strlen(data->commonPrefix)+1;
1228
1229 if (strlen(data->commonPrefix) == strlen(data->incompleteWord) && !justOneOptionLeft)
1230 {
1231 provideHint(data->console, data->options);
1232 }
1233 processConsoleEnd(data->console);
1234 insertInputText(data->console, data->commonPrefix+strlen(data->incompleteWord));
1235
1236 if (justOneOptionLeft)
1237 {
1238 insertInputText(data->console, " ");
1239 }
1240 }
1241
1242 free(data->options);
1243 free(data->commonPrefix);
1244}
1245
1246static void tabCompleteLanguages(TabCompleteData* data)
1247{
1248 FOR_EACH_LANG(ln)
1249 addTabCompleteOption(data, ln->name);
1250 FOR_EACH_LANG_END
1251 finishTabComplete(data);
1252}
1253
1254static void tabCompleteExport(TabCompleteData* data)
1255{
1256#define EXPORT_CMD_DEF(name) addTabCompleteOption(data, #name);
1257 EXPORT_CMD_LIST(EXPORT_CMD_DEF)
1258#undef EXPORT_CMD_DEF
1259 finishTabComplete(data);
1260}
1261
1262static void tabCompleteImport(TabCompleteData* data)
1263{
1264#define IMPORT_CMD_DEF(name) addTabCompleteOption(data, #name);
1265 IMPORT_CMD_LIST(IMPORT_CMD_DEF)
1266#undef IMPORT_CMD_DEF
1267 finishTabComplete(data);
1268}
1269
1270static bool addFileAndDirToTabComplete(const char* name, const char* title, const char* hash, s32 id, void* data, bool dir)
1271{
1272 addTabCompleteOption(data, name);
1273
1274 return true;
1275}
1276
1277static bool addFilenameToTabComplete(const char* name, const char* title, const char* hash, s32 id, void* data, bool dir)
1278{
1279 if (!dir)
1280 addTabCompleteOption(data, name);
1281
1282 return true;
1283}
1284
1285static bool addDirToTabComplete(const char* name, const char* title, const char* hash, s32 id, void* data, bool dir)
1286{
1287 if (dir)
1288 addTabCompleteOption(data, name);
1289
1290 return true;
1291}
1292
1293static void finishTabCompleteAndFreeData(void* data) {
1294 finishTabComplete((const TabCompleteData *) data);
1295 free(data);
1296}
1297
1298static void tabCompleteFiles(TabCompleteData* data)
1299{
1300 tic_fs_enum(data->console->fs, addFilenameToTabComplete, finishTabCompleteAndFreeData, MOVE(*data));
1301}
1302
1303static void tabCompleteDirs(TabCompleteData* data)
1304{
1305 tic_fs_enum(data->console->fs, addDirToTabComplete, finishTabCompleteAndFreeData, MOVE(*data));
1306}
1307
1308static void tabCompleteFilesAndDirs(TabCompleteData* data)
1309{
1310 tic_fs_enum(data->console->fs, addFileAndDirToTabComplete, finishTabCompleteAndFreeData, MOVE(*data));
1311}
1312
1313static void tabCompleteConfig(TabCompleteData* data)
1314{
1315 addTabCompleteOption(data, "reset");
1316 addTabCompleteOption(data, "default");
1317 finishTabComplete(data);
1318}
1319
1320typedef struct
1321{
1322 const char* name;
1323 bool dir;
1324} FileItem;
1325
1326typedef struct
1327{
1328 Console* console;
1329 FileItem* items;
1330 s32 count;
1331} PrintFileNameData;
1332
1333static bool printFilename(const char* name, const char* title, const char* hash, s32 id, void* ctx, bool dir)
1334{
1335 PrintFileNameData* data = ctx;
1336
1337 data->items = realloc(data->items, (data->count + 1) * sizeof *data->items);
1338 data->items[data->count++] = (FileItem){strdup(name), dir};
1339
1340 return true;
1341}
1342
1343static s32 casecmp(const char *str1, const char *str2)
1344{
1345 while (*str1 && *str2)
1346 {
1347 if (tolower((u8) *str1) != tolower((u8) *str2))
1348 break;
1349
1350 ++str1;
1351 ++str2;
1352 }
1353
1354 return (s32) ((u8) tolower(*str1) - (u8) tolower(*str2));
1355}
1356
1357static inline s32 itemcmp(const void* a, const void* b)
1358{
1359 const FileItem* item1 = a;
1360 const FileItem* item2 = b;
1361
1362 if(item1->dir != item2->dir)
1363 return item1->dir ? -1 : 1;
1364
1365 return casecmp(item1->name, item2->name);
1366}
1367
1368static void onDirDone(void* ctx)
1369{
1370 PrintFileNameData* data = ctx;
1371 Console* console = data->console;
1372
1373 qsort(data->items, data->count, sizeof *data->items, itemcmp);
1374
1375 for(const FileItem *item = data->items, *end = item + data->count; item < end; item++)
1376 {
1377 printLine(console);
1378
1379 if(item->dir)
1380 {
1381 printBack(console, "[");
1382 printBack(console, item->name);
1383 printBack(console, "]");
1384 }
1385 else printFront(console, item->name);
1386
1387 free((void*)item->name);
1388 }
1389
1390 if (data->count == 0)
1391 {
1392 printBack(console, "\n\nuse ");
1393 printFront(console, "DEMO");
1394 printBack(console, " command to install demo carts");
1395 }
1396 else free(data->items);
1397
1398 printLine(console);
1399 commandDone(console);
1400
1401 free(ctx);
1402}
1403
1404typedef struct
1405{
1406 Console* console;
1407 char* name;
1408} ChangeDirData;
1409
1410static void onChangeDirectoryDone(bool dir, void* data)
1411{
1412 ChangeDirData* changeDirData = data;
1413 Console* console = changeDirData->console;
1414
1415 if (dir)
1416 {
1417 tic_fs_changedir(console->fs, changeDirData->name);
1418 }
1419 else printBack(console, "\ndir doesn't exist");
1420
1421 free(changeDirData->name);
1422 free(changeDirData);
1423
1424 commandDone(console);
1425}
1426
1427static void onChangeDirectory(Console* console)
1428{
1429 if(console->desc->count)
1430 {
1431 const char* param = console->desc->params->key;
1432
1433 if(strcmp(param, "/") == 0)
1434 {
1435 tic_fs_homedir(console->fs);
1436 }
1437 else if(strcmp(param, "..") == 0)
1438 {
1439 tic_fs_dirback(console->fs);
1440 }
1441 else
1442 {
1443 ChangeDirData data = { console, strdup(param) };
1444 tic_fs_isdir_async(console->fs, param, onChangeDirectoryDone, MOVE(data));
1445 return;
1446 }
1447 } else {
1448 tic_fs_homedir(console->fs);
1449 }
1450
1451 commandDone(console);
1452}
1453
1454static void onMakeDirectory(Console* console)
1455{
1456 if(console->desc->count)
1457 {
1458 char msg[TICNAME_MAX];
1459 const char* param = console->desc->params->key;
1460
1461 if (tic_fs_exists(console->fs, param)) {
1462 sprintf(msg, "\nerror, [%s] already exists :(", param);
1463 printError(console, msg);
1464 commandDone(console);
1465 return;
1466 }
1467
1468 sprintf(msg, "\ncreated [%s] folder :)", param);
1469
1470 printBack(console, tic_fs_makedir(console->fs, param)
1471 ? "\nerror, dir not created :("
1472 : msg);
1473
1474 }
1475 else printError(console, "\ninvalid dir name");
1476
1477 commandDone(console);
1478}
1479
1480static void onDirCommand(Console* console)
1481{
1482 printLine(console);
1483
1484 PrintFileNameData data = {console};
1485 tic_fs_enum(console->fs, printFilename, onDirDone, MOVE(data));
1486}
1487
1488static void onFolderCommand(Console* console)
1489{
1490
1491 printBack(console, "\nStorage path:\n");
1492 printFront(console, tic_fs_pathroot(console->fs, ""));
1493
1494 tic_fs_openfolder(console->fs);
1495
1496 commandDone(console);
1497}
1498
1499static void onClsCommand(Console* console)
1500{
1501 memset(console->text, 0, CONSOLE_BUFFER_SIZE);
1502 memset(console->color, TIC_COLOR_BG, CONSOLE_BUFFER_SIZE);
1503
1504 ZEROMEM(console->scroll);
1505 ZEROMEM(console->cursor);
1506 ZEROMEM(console->input);
1507
1508 printf("\r");
1509
1510 commandDoneLine(console, false);
1511}
1512
1513static void onInstallDemosCommand(Console* console)
1514{
1515 tic_fs* fs = console->fs;
1516 u8* data = (u8*)newCart();
1517
1518 SCOPE(free(data))
1519 {
1520 printBack(console, "\nadded carts:\n\n");
1521
1522#if defined(TIC_BUILD_WITH_LUA)
1523
1524 static const u8 demofire[] =
1525 {
1526 #include "../build/assets/fire.tic.dat"
1527 };
1528
1529 static const u8 demop3d[] =
1530 {
1531 #include "../build/assets/p3d.tic.dat"
1532 };
1533
1534 static const u8 demosfx[] =
1535 {
1536 #include "../build/assets/sfx.tic.dat"
1537 };
1538
1539 static const u8 demopalette[] =
1540 {
1541 #include "../build/assets/palette.tic.dat"
1542 };
1543
1544 static const u8 demofont[] =
1545 {
1546 #include "../build/assets/font.tic.dat"
1547 };
1548
1549 static const u8 demomusic[] =
1550 {
1551 #include "../build/assets/music.tic.dat"
1552 };
1553
1554 static const u8 demoquest[] =
1555 {
1556 #include "../build/assets/quest.tic.dat"
1557 };
1558
1559 static const u8 demotetris[] =
1560 {
1561 #include "../build/assets/tetris.tic.dat"
1562 };
1563
1564 static const u8 demobenchmark[] =
1565 {
1566 #include "../build/assets/benchmark.tic.dat"
1567 };
1568
1569 static const u8 demobpp[] =
1570 {
1571 #include "../build/assets/bpp.tic.dat"
1572 };
1573
1574 static const u8 democar[] =
1575 {
1576 #include "../build/assets/car.tic.dat"
1577 };
1578
1579#define DEMOS_LIST(macro) \
1580 macro(fire) \
1581 macro(font) \
1582 macro(music) \
1583 macro(p3d) \
1584 macro(palette) \
1585 macro(quest) \
1586 macro(sfx) \
1587 macro(tetris) \
1588 macro(benchmark) \
1589 macro(bpp) \
1590 macro(car)
1591
1592 static const struct Demo {const char* name; const u8* data; s32 size;} Demos[] =
1593 {
1594#define DEMOS_DEF(name) {#name ".tic", demo ## name, sizeof demo ## name},
1595 DEMOS_LIST(DEMOS_DEF)
1596#undef DEMOS_DEF
1597 };
1598
1599#undef DEMOS_LIST
1600
1601 FOR(const struct Demo*, demo, Demos)
1602 {
1603 tic_fs_save(fs, demo->name, data, tic_tool_unzip(data, sizeof(tic_cartridge), demo->data, demo->size), true);
1604 printFront(console, demo->name);
1605 printLine(console);
1606 }
1607#endif
1608
1609 static const char* Bunny = "bunny";
1610
1611 tic_fs_makedir(fs, Bunny);
1612 tic_fs_changedir(fs, Bunny);
1613
1614 FOR_EACH_LANG(ln)
1615 {
1616 tic_script_config_extra* ex = getConfigExtra(ln);
1617 if (ex->markRom != NULL) { // having a Mark is not mandatory
1618 char cartname[1024];
1619 strcpy(cartname, ln->name);
1620 strcat(cartname, "mark.tic");
1621
1622 tic_fs_save(fs, cartname, data, tic_tool_unzip(data, sizeof(tic_cartridge), ex->markRom, ex->markRomSize), true);
1623 printFront(console, Bunny);
1624 printFront(console, "/");
1625 printFront(console, cartname);
1626 printLine(console);
1627 }
1628 }
1629 FOR_EACH_LANG_END
1630
1631 tic_fs_dirback(fs);
1632 }
1633
1634 commandDone(console);
1635}
1636
1637static void onGameMenuCommand(Console* console)
1638{
1639 gotoMenu(console->studio);
1640 commandDone(console);
1641}
1642
1643static void onSurfCommand(Console* console)
1644{
1645 gotoSurf(console->studio);
1646}
1647
1648static void loadExternal(Console* console, const char* path)
1649{
1650 CommandDesc desc =
1651 {
1652 .params = malloc(sizeof *desc.params),
1653 .count = 1,
1654 };
1655
1656 *desc.params = (struct Param){.key = strdup(path)};
1657 *console->desc = desc;
1658
1659 onLoadCommandConfirmed(console);
1660}
1661
1662static void onConfigCommand(Console* console)
1663{
1664 if(console->desc->count)
1665 {
1666 if(strcmp(console->desc->params->key, "reset") == 0)
1667 {
1668 console->config->reset(console->config);
1669 printBack(console, "\nconfiguration reset :)");
1670 }
1671 else if(strcmp(console->desc->params->key, "default") == 0)
1672 {
1673 if (console->desc->count == 1)
1674 onLoadDemoCommand(console, 0);
1675 else
1676 {
1677 FOR_EACH_LANG(script)
1678 if (strcmp(console->desc->params[1].key, script->name) == 0)
1679 onLoadDemoCommand(console, script);
1680 FOR_EACH_LANG_END
1681 }
1682 }
1683 else
1684 {
1685 printError(console, "\nunknown parameter:\n");
1686 printError(console, console->desc->params->key);
1687 }
1688 }
1689 else
1690 {
1691 CommandDesc desc =
1692 {
1693 .params = malloc(sizeof *desc.params),
1694 .count = 1,
1695 };
1696
1697 *desc.params = (struct Param){.key = strdup(CONFIG_TIC_PATH)};
1698 *console->desc = desc;
1699
1700 onLoadCommand(console);
1701
1702 return;
1703 }
1704
1705 commandDone(console);
1706}
1707
1708typedef struct
1709{
1710#define IMPORT_KEYS_DEF(key) s32 key;
1711 IMPORT_KEYS_LIST(IMPORT_KEYS_DEF)
1712#undef IMPORT_KEYS_DEF
1713} ImportParams;
1714
1715static void onFileImported(Console* console, const char* filename, bool result)
1716{
1717 if(result)
1718 {
1719 printLine(console);
1720 printBack(console, filename);
1721 printBack(console, " imported :)");
1722 }
1723 else
1724 {
1725 char buf[TICNAME_MAX];
1726 sprintf(buf, "\nerror: %s not imported :(", filename);
1727 printError(console, buf);
1728 }
1729
1730 commandDone(console);
1731}
1732
1733static inline tic_bank* getBank(Console* console, s32 bank)
1734{
1735 return &console->tic->cart.banks[bank];
1736}
1737
1738static inline const tic_palette* getPalette(Console* console, s32 bank, s32 vbank)
1739{
1740 return vbank
1741 ? &getBank(console, bank)->palette.vbank1
1742 : &getBank(console, bank)->palette.vbank0;
1743}
1744
1745static void onImportTilesBase(Console* console, const char* name, const void* buffer, s32 size, tic_tile* base, ImportParams params)
1746{
1747 png_buffer png = {(u8*)buffer, size};
1748 bool error = true;
1749
1750 png_img img = png_read(png);
1751
1752 if(img.data) SCOPE(free(img.data))
1753 {
1754 const tic_palette* pal = getPalette(console, params.bank, params.vbank);
1755
1756 for(s32 j = 0, y = params.y, h = y + (params.h ? params.h : img.height); y < h; ++y, ++j)
1757 for(s32 i = 0, x = params.x, w = x + (params.w ? params.w : img.width); x < w; ++x, ++i)
1758 if(x >= 0 && x < TIC_SPRITESHEET_SIZE && y >= 0 && y < TIC_SPRITESHEET_SIZE)
1759 setSpritePixel(base, x, y, tic_nearest_color(pal->colors,
1760 (tic_rgb*)(img.pixels + i + j * img.width), TIC_PALETTE_SIZE));
1761
1762 error = false;
1763 }
1764
1765 onFileImported(console, name, !error);
1766}
1767
1768static void onImport_tiles(Console* console, const char* name, const void* buffer, s32 size, ImportParams params)
1769{
1770 onImportTilesBase(console, name, buffer, size, getBank(console, params.bank)->tiles.data, params);
1771}
1772
1773static void onImport_binary(Console* console, const char* name, const void* buffer, s32 size, ImportParams params)
1774{
1775 bool ok = name && buffer && size <= TIC_BINARY_SIZE;
1776
1777 if(ok)
1778 {
1779 tic_binary* binary = &console->tic->cart.binary;
1780 binary->size = size;
1781 memcpy(binary->data, buffer, size);
1782 }
1783
1784 onFileImported(console, name, ok);
1785}
1786
1787static void onImport_sprites(Console* console, const char* name, const void* buffer, s32 size, ImportParams params)
1788{
1789 onImportTilesBase(console, name, buffer, size, getBank(console, params.bank)->sprites.data, params);
1790}
1791
1792static void onImport_map(Console* console, const char* name, const void* buffer, s32 size, ImportParams params)
1793{
1794 bool ok = name && buffer && size <= sizeof(tic_map);
1795
1796 if(ok)
1797 {
1798 enum {Size = sizeof(tic_map)};
1799
1800 tic_map* map = &getBank(console, params.bank)->map;
1801 memset(map, 0, Size);
1802 memcpy(map, buffer, MIN(size, Size));
1803 }
1804
1805 onFileImported(console, name, ok);
1806}
1807
1808static void onImport_code(Console* console, const char* name, const void* buffer, s32 size, ImportParams params)
1809{
1810 tic_mem* tic = console->tic;
1811 bool error = false;
1812
1813 if(name && buffer && size <= sizeof(tic_code))
1814 {
1815 enum {Size = sizeof(tic_code)};
1816
1817 memset(tic->cart.code.data, 0, Size);
1818 memcpy(tic->cart.code.data, buffer, MIN(size, Size));
1819
1820 studioRomLoaded(console->studio);
1821 }
1822 else error = true;
1823
1824 onFileImported(console, name, !error);
1825}
1826
1827static void onImport_screen(Console* console, const char* name, const void* buffer, s32 size, ImportParams params)
1828{
1829 png_buffer png = {(u8*)buffer, size};
1830 bool error = true;
1831
1832 png_img img = png_read(png);
1833
1834 if(img.data) SCOPE(free(img.data))
1835 {
1836 if(img.width == TIC80_WIDTH && img.height == TIC80_HEIGHT)
1837 {
1838 tic_bank* bank = getBank(console, params.bank);
1839 const tic_palette* pal = getPalette(console, params.bank, params.vbank);
1840
1841 s32 i = 0;
1842 for(const png_rgba *pix = img.pixels, *end = pix + (TIC80_WIDTH * TIC80_HEIGHT); pix < end; pix++)
1843 tic_tool_poke4(bank->screen.data, i++, tic_nearest_color(pal->colors, (tic_rgb*)pix, TIC_PALETTE_SIZE));
1844
1845 error = false;
1846 }
1847 }
1848
1849 onFileImported(console, name, !error);
1850}
1851
1852static void onImportCommand(Console* console)
1853{
1854 bool error = true;
1855
1856 if(console->desc->count > 1)
1857 {
1858 ImportParams params = {0};
1859
1860 for(const struct Param* it = console->desc->params, *end = it + console->desc->count; it < end; ++it)
1861 {
1862#define IMPORT_KEYS_DEF(name) if(it->val && strcmp(it->key, #name) == 0) params.name = atoi(it->val);
1863 IMPORT_KEYS_LIST(IMPORT_KEYS_DEF)
1864#undef IMPORT_KEYS_DEF
1865 }
1866
1867 const char* filename = console->desc->params[1].key;
1868 s32 size = 0;
1869 const void* data = tic_fs_load(console->fs, filename, &size);
1870
1871 if(data) SCOPE(free((void*)data))
1872 {
1873 static const struct Handler
1874 {
1875 const char* section;
1876 void (*handler)(Console*, const char*, const void*, s32, ImportParams);
1877 } Handlers[] =
1878 {
1879#define IMPORT_CMD_DEF(name) {#name, onImport_##name},
1880 IMPORT_CMD_LIST(IMPORT_CMD_DEF)
1881#undef IMPORT_CMD_DEF
1882 };
1883
1884 const char* section = console->desc->params[0].key;
1885 FOR(const struct Handler*, ptr, Handlers)
1886 if(strcmp(section, ptr->section) == 0)
1887 {
1888 ptr->handler(console, filename, data, size, params);
1889 error = false;
1890 break;
1891 }
1892 }
1893 else
1894 {
1895 char msg[TICNAME_MAX];
1896 sprintf(msg, "\nerror, %s file not loaded", filename);
1897 printError(console, msg);
1898 commandDone(console);
1899 return;
1900 }
1901 }
1902
1903 if(error)
1904 {
1905 printError(console, "\nerror: invalid parameters.");
1906 printUsage(console, console->desc->command);
1907
1908 commandDone(console);
1909 }
1910}
1911
1912static void onFileExported(Console* console, const char* filename, bool result)
1913{
1914 if(result)
1915 {
1916 printLine(console);
1917 printBack(console, filename);
1918 printBack(console, " exported :)");
1919 }
1920 else
1921 {
1922 char buf[TICNAME_MAX];
1923 sprintf(buf, "\nerror: %s not exported :(", filename);
1924 printError(console, buf);
1925 }
1926
1927 commandDone(console);
1928}
1929
1930typedef struct
1931{
1932#define EXPORT_KEYS_DEF(key) s32 key;
1933 EXPORT_KEYS_LIST(EXPORT_KEYS_DEF)
1934#undef EXPORT_KEYS_DEF
1935} ExportParams;
1936
1937static void exportSprites(Console* console, const char* filename, tic_tile* base, ExportParams params)
1938{
1939 tic_mem* tic = console->tic;
1940 const tic_cartridge* cart = &tic->cart;
1941
1942 png_img img = {TIC_SPRITESHEET_SIZE, TIC_SPRITESHEET_SIZE, malloc(TIC_SPRITESHEET_SIZE * TIC_SPRITESHEET_SIZE * sizeof(png_rgba))};
1943
1944 SCOPE(free(img.data))
1945 {
1946 const tic_palette* pal = getPalette(console, params.bank, params.vbank);
1947
1948 for(s32 i = 0; i < TIC_SPRITESHEET_SIZE * TIC_SPRITESHEET_SIZE; i++)
1949 img.values[i] = tic_rgba(&pal->colors[getSpritePixel(base, i % TIC_SPRITESHEET_SIZE, i / TIC_SPRITESHEET_SIZE)]);
1950
1951 png_buffer png = png_write(img);
1952
1953 SCOPE(free(png.data))
1954 {
1955 onFileExported(console, filename, tic_fs_save(console->fs, filename, png.data, png.size, true));
1956 }
1957 }
1958}
1959
1960static void* embedCart(Console* console, u8* app, s32* size)
1961{
1962 tic_mem* tic = console->tic;
1963 u8* data = NULL;
1964 void* cart = newCart();
1965
1966 SCOPE(free(cart))
1967 {
1968 s32 cartSize = tic_cart_save(&tic->cart, cart);
1969
1970 s32 zipSize = sizeof(tic_cartridge);
1971 u8* zipData = (u8*)malloc(zipSize);
1972
1973 SCOPE(free(zipData))
1974 {
1975 if((zipSize = tic_tool_zip(zipData, zipSize, cart, cartSize)))
1976 {
1977 s32 appSize = *size;
1978
1979 EmbedHeader header =
1980 {
1981 .appSize = appSize,
1982 .cartSize = zipSize,
1983 };
1984
1985 memcpy(header.sig, CART_SIG, STRLEN(CART_SIG));
1986
1987 s32 finalSize = appSize + sizeof header + header.cartSize;
1988 data = malloc(finalSize);
1989
1990 if (data)
1991 {
1992 memcpy(data, app, appSize);
1993 memcpy(data + appSize, &header, sizeof header);
1994 memcpy(data + appSize + sizeof header, zipData, header.cartSize);
1995
1996 *size = finalSize;
1997 }
1998 }
1999 }
2000 }
2001
2002 return data;
2003}
2004
2005typedef struct
2006{
2007 Console* console;
2008 char filename[TICNAME_MAX];
2009} GameExportData;
2010
2011static void onExportGet(const net_get_data* data)
2012{
2013 GameExportData* exportData = (GameExportData*)data->calldata;
2014 Console* console = exportData->console;
2015
2016 switch(data->type)
2017 {
2018 case net_get_progress:
2019 {
2020 console->cursor.pos.x = 0;
2021 printf("\r");
2022 printBack(console, "GET ");
2023 printFront(console, data->url);
2024
2025 char buf[8];
2026 sprintf(buf, " [%i%%]", data->progress.size * 100 / data->progress.total);
2027 printBack(console, buf);
2028 }
2029 break;
2030 case net_get_error:
2031 printError(console, "file downloading error :(");
2032 commandDone(console);
2033 free(exportData);
2034 break;
2035 default:
2036 break;
2037 }
2038}
2039
2040static void onNativeExportGet(const net_get_data* data)
2041{
2042 switch(data->type)
2043 {
2044 case net_get_done:
2045 {
2046 GameExportData* exportData = (GameExportData*)data->calldata;
2047 Console* console = exportData->console;
2048
2049 tic_mem* tic = console->tic;
2050
2051 char filename[TICNAME_MAX];
2052 strcpy(filename, exportData->filename);
2053 free(exportData);
2054
2055 s32 size = data->done.size;
2056
2057 printLine(console);
2058
2059 const char* path = tic_fs_path(console->fs, filename);
2060 void* buf = NULL;
2061
2062 onFileExported(console, filename, (buf = embedCart(console, data->done.data, &size)) && fs_write(path, buf, size));
2063 chmod(path, DEFAULT_CHMOD);
2064
2065 if (buf)
2066 free(buf);
2067 }
2068 break;
2069 default:
2070 onExportGet(data);
2071 }
2072}
2073
2074static void exportGame(Console* console, const char* name, const char* system, net_get_callback callback, ExportParams params)
2075{
2076 tic_mem* tic = console->tic;
2077 printLine(console);
2078 GameExportData data = {console};
2079 strcpy(data.filename, name);
2080
2081 char url[TICNAME_MAX] = "/export/" DEF2STR(TIC_VERSION_MAJOR) "." DEF2STR(TIC_VERSION_MINOR) TIC_VERSION_STATUS "/";
2082 strcat(url, system);
2083
2084#if defined(TIC80_PRO)
2085 if (params.alone)
2086 strcat(url, tic_core_script_config(console->tic)->name);
2087#endif
2088
2089 tic_net_get(console->net, url, callback, MOVE(data));
2090}
2091
2092static inline void exportNativeGame(Console* console, const char* name, const char* system, ExportParams params)
2093{
2094 exportGame(console, name, system, onNativeExportGet, params);
2095}
2096
2097static void onHtmlExportGet(const net_get_data* data)
2098{
2099 switch(data->type)
2100 {
2101 case net_get_done:
2102 {
2103 GameExportData* exportData = (GameExportData*)data->calldata;
2104 Console* console = exportData->console;
2105
2106 tic_mem* tic = console->tic;
2107
2108 char filename[TICNAME_MAX];
2109 strcpy(filename, exportData->filename);
2110 free(exportData);
2111
2112 const char* zipPath = tic_fs_path(console->fs, filename);
2113 bool errorOccured = !fs_write(zipPath, data->done.data, data->done.size);
2114
2115 if(!errorOccured)
2116 {
2117 struct zip_t *zip = zip_open(zipPath, ZIP_DEFAULT_COMPRESSION_LEVEL, 'a');
2118
2119 if(zip) SCOPE(zip_close(zip))
2120 {
2121 void* cart = newCart();
2122
2123 SCOPE(free(cart))
2124 {
2125 s32 cartSize = tic_cart_save(&tic->cart, cart);
2126
2127 if(cartSize)
2128 {
2129 zip_entry_open(zip, "cart.tic");
2130 zip_entry_write(zip, cart, cartSize);
2131 zip_entry_close(zip);
2132 }
2133 else errorOccured = true;
2134 }
2135 }
2136 else errorOccured = true;
2137 }
2138
2139 onFileExported(console, filename, !errorOccured);
2140 }
2141 break;
2142 default:
2143 onExportGet(data);
2144 }
2145}
2146
2147static const char* getFilename(const char* filename, const char* ext)
2148{
2149 if(strcmp(filename + strlen(filename) - strlen(ext), ext) == 0)
2150 return filename;
2151
2152 static char Name[TICNAME_MAX];
2153 strcpy(Name, filename);
2154 strcat(Name, ext);
2155
2156 return Name;
2157}
2158
2159static void onExport_win(Console* console, const char* param, const char* filename, ExportParams params)
2160{
2161 exportNativeGame(console, getFilename(filename, ".exe"), param, params);
2162}
2163
2164static void onExport_winxp(Console* console, const char* param, const char* filename, ExportParams params)
2165{
2166 exportNativeGame(console, getFilename(filename, ".exe"), param, params);
2167}
2168
2169static void onExport_linux(Console* console, const char* param, const char* filename, ExportParams params)
2170{
2171 exportNativeGame(console, filename, param, params);
2172}
2173
2174static void onExport_rpi(Console* console, const char* param, const char* filename, ExportParams params)
2175{
2176 exportNativeGame(console, filename, param, params);
2177}
2178
2179static void onExport_mac(Console* console, const char* param, const char* filename, ExportParams params)
2180{
2181 exportNativeGame(console, filename, param, params);
2182}
2183
2184static void onExport_html(Console* console, const char* param, const char* filename, ExportParams params)
2185{
2186 exportGame(console, getFilename(filename, ".zip"), param, onHtmlExportGet, params);
2187}
2188
2189static void onExport_tiles(Console* console, const char* param, const char* filename, ExportParams params)
2190{
2191 exportSprites(console, getFilename(filename, PngExt), getBank(console, params.bank)->tiles.data, params);
2192}
2193
2194static void onExport_binary(Console* console, const char* param, const char* path, ExportParams params)
2195{
2196 const char* filename = getFilename(path, ".binary");
2197
2198 tic_binary *binary = &console->tic->cart.binary;
2199 // TODO: do we need this buffer at all, could we just handle `binary.data` directly to `tic_fs_save`?
2200 void* buffer = malloc(binary->size);
2201
2202 SCOPE(free(buffer))
2203 {
2204 memcpy(buffer, binary->data, binary->size);
2205
2206 onFileExported(console, filename, tic_fs_save(console->fs, filename, buffer, binary->size, true));
2207 }
2208}
2209
2210static void onExport_sprites(Console* console, const char* param, const char* filename, ExportParams params)
2211{
2212 exportSprites(console, getFilename(filename, PngExt), getBank(console, params.bank)->sprites.data, params);
2213}
2214
2215static void onExport_map(Console* console, const char* param, const char* path, ExportParams params)
2216{
2217 enum{Size = sizeof(tic_map)};
2218 const char* filename = getFilename(path, ".map");
2219
2220 void* buffer = malloc(Size);
2221
2222 SCOPE(free(buffer))
2223 {
2224 tic_map* map = &getBank(console, params.bank)->map;
2225 memcpy(buffer, map->data, Size);
2226
2227 onFileExported(console, filename, tic_fs_save(console->fs, filename, buffer, Size, true));
2228 }
2229}
2230
2231static void onExport_mapimg(Console* console, const char* param, const char* path, ExportParams params)
2232{
2233 const char* filename = getFilename(path, ".png");
2234
2235 enum{Width = TIC_MAP_WIDTH * TIC_SPRITESIZE, Height = TIC_MAP_HEIGHT * TIC_SPRITESIZE};
2236
2237 png_img img = {Width, Height, malloc(Width * Height * sizeof(png_rgba))};
2238
2239 SCOPE(free(img.data))
2240 {
2241 {
2242 tic_mem* tic = console->tic;
2243
2244 tic_api_sync(tic, -1, params.bank, false);
2245
2246 for(s32 r = 0; r < TIC_MAP_ROWS; r++)
2247 for(s32 c = 0; c < TIC_MAP_COLS; c++)
2248 {
2249 tic_api_map(tic, c * TIC_MAP_SCREEN_WIDTH, r * TIC_MAP_SCREEN_HEIGHT,
2250 TIC_MAP_SCREEN_WIDTH, TIC_MAP_SCREEN_HEIGHT, 0, 0, NULL, 0, 1, NULL, NULL);
2251
2252 tic_core_blit(tic);
2253
2254 for(s32 j = 0; j < TIC80_HEIGHT; j++)
2255 for(s32 i = 0; i < TIC80_WIDTH; i++)
2256 img.values[(i + c * TIC80_WIDTH) + (j + r * TIC80_HEIGHT) * Width] =
2257 tic->product.screen[(i + TIC80_MARGIN_LEFT) + (j + TIC80_MARGIN_TOP) * TIC80_FULLWIDTH];
2258 }
2259 }
2260
2261 png_buffer png = png_write(img);
2262
2263 SCOPE(free(png.data))
2264 {
2265 onFileExported(console, filename, tic_fs_save(console->fs, filename, png.data, png.size, true));
2266 }
2267 }
2268}
2269
2270static void onExport_sfx(Console* console, const char* param, const char* name, ExportParams params)
2271{
2272 const char* filename = getFilename(name, ".wav");
2273 bool error = true;
2274
2275 if(params.id >= 0 && params.id < SFX_COUNT)
2276 error = studioExportSfx(console->studio, params.id, filename) == NULL;
2277
2278 onFileExported(console, filename, !error);
2279}
2280
2281static void onExport_music(Console* console, const char* type, const char* name, ExportParams params)
2282{
2283 const char* filename = getFilename(name, ".wav");
2284 bool error = true;
2285
2286 if(params.id >= 0 && params.id < MUSIC_TRACKS)
2287 error = studioExportMusic(console->studio, params.id, filename) == NULL;
2288
2289 onFileExported(console, filename, !error);
2290}
2291
2292static void onExport_screen(Console* console, const char* param, const char* name, ExportParams params)
2293{
2294 const char* filename = getFilename(name, ".png");
2295
2296 tic_mem* tic = console->tic;
2297 const tic_cartridge* cart = &tic->cart;
2298
2299 png_img img = {TIC80_WIDTH, TIC80_HEIGHT, malloc(TIC80_WIDTH * TIC80_HEIGHT * sizeof(png_rgba))};
2300
2301 SCOPE(free(img.data))
2302 {
2303 const tic_palette* pal = getPalette(console, params.bank, params.vbank);
2304
2305 tic_bank* bank = getBank(console, params.bank);
2306 for(s32 i = 0; i < TIC80_WIDTH * TIC80_HEIGHT; i++)
2307 img.values[i] = tic_rgba(&pal->colors[tic_tool_peek4(bank->screen.data, i)]);
2308
2309 png_buffer png = png_write(img);
2310
2311 SCOPE(free(png.data))
2312 {
2313 onFileExported(console, filename, tic_fs_save(console->fs, filename, png.data, png.size, true));
2314 }
2315 }
2316}
2317
2318static void onExport_help(Console* console, const char* param, const char* name, ExportParams params);
2319
2320static void onExportCommand(Console* console)
2321{
2322 if(console->desc->count > 1)
2323 {
2324 ExportParams params = {0};
2325
2326 for(const struct Param* it = console->desc->params, *end = it + console->desc->count; it < end; ++it)
2327 {
2328#define EXPORT_KEYS_DEF(name) if(it->val && strcmp(it->key, #name) == 0) params.name = atoi(it->val);
2329 EXPORT_KEYS_LIST(EXPORT_KEYS_DEF)
2330#undef EXPORT_KEYS_DEF
2331 }
2332
2333 const char* filename = console->desc->params[1].key;
2334
2335 static const struct Handler
2336 {
2337 const char* type;
2338 void(*handler)(Console*, const char*, const char*, ExportParams);
2339 } Handlers[] =
2340 {
2341#define EXPORT_CMD_DEF(name) {#name, onExport_##name},
2342 EXPORT_CMD_LIST(EXPORT_CMD_DEF)
2343#undef EXPORT_CMD_DEF
2344 };
2345
2346 const char* type = console->desc->params[0].key;
2347
2348 FOR(const struct Handler*, ptr, Handlers)
2349 if(strcmp(type, ptr->type) == 0)
2350 {
2351 ptr->handler(console, type, filename, params);
2352 return;
2353 }
2354 }
2355
2356 {
2357 printError(console, "\nerror: invalid parameters.");
2358 printUsage(console, console->desc->command);
2359 commandDone(console);
2360 }
2361}
2362
2363static void drawShadowText(tic_mem* tic, const char* text, s32 x, s32 y, tic_color color, s32 scale)
2364{
2365 tic_api_print(tic, text, x, y + scale, tic_color_black, false, scale, false);
2366 tic_api_print(tic, text, x, y, color, false, scale, false);
2367}
2368
2369const char* readMetatag(const char* code, const char* tag, const char* comment);
2370
2371static CartSaveResult saveCartName(Console* console, const char* name)
2372{
2373 tic_mem* tic = console->tic;
2374
2375 bool success = false;
2376
2377 if(name && strlen(name))
2378 {
2379 u8* buffer = (u8*)malloc(sizeof(tic_cartridge) * 3);
2380
2381 if(buffer)
2382 {
2383 if(strcmp(name, CONFIG_TIC_PATH) == 0)
2384 {
2385 console->config->save(console->config);
2386 studioRomSaved(console->studio);
2387 free(buffer);
2388 return CART_SAVE_OK;
2389 }
2390 else
2391 {
2392 s32 size = 0;
2393
2394 if(tic_tool_has_ext(name, PngExt))
2395 {
2396 png_buffer cover;
2397
2398 {
2399 enum{CoverWidth = 256};
2400
2401 static const u8 Cartridge[] =
2402 {
2403 #include "../build/assets/cart.png.dat"
2404 };
2405
2406 png_buffer template = {(u8*)Cartridge, sizeof Cartridge};
2407 png_img img = png_read(template);
2408
2409 // draw screen
2410 {
2411 enum{PaddingLeft = 8, PaddingTop = 8};
2412
2413 const tic_bank* bank = &tic->cart.bank0;
2414 const tic_rgb* pal = bank->palette.vbank0.colors;
2415 const u8* screen = bank->screen.data;
2416 u32* ptr = img.values + PaddingTop * CoverWidth + PaddingLeft;
2417
2418 for(s32 i = 0; i < TIC80_WIDTH * TIC80_HEIGHT; i++)
2419 ptr[i / TIC80_WIDTH * CoverWidth + i % TIC80_WIDTH] = tic_rgba(pal + tic_tool_peek4(screen, i));
2420 }
2421
2422 // draw title/author/desc
2423 {
2424 enum{Width = 224, Height = 40, PaddingTop = 162, PaddingLeft = 16, Scale = 2, Row = TIC_FONT_HEIGHT * 2 * Scale};
2425
2426 tic_api_cls(tic, tic_color_dark_grey);
2427
2428 const char* comment = tic_core_script_config(tic)->singleComment;
2429
2430 char* title = tic_tool_metatag(tic->cart.code.data, "title", comment);
2431 if(title)
2432 {
2433 drawShadowText(tic, title, 0, 0, tic_color_white, Scale);
2434 free(title);
2435 }
2436
2437 char* author = tic_tool_metatag(tic->cart.code.data, "author", comment);
2438 if(author)
2439 {
2440 char buf[TICNAME_MAX];
2441 snprintf(buf, sizeof buf, "by %s", author);
2442 drawShadowText(tic, buf, 0, Row, tic_color_grey, Scale);
2443 free(author);
2444 }
2445
2446 u32* ptr = img.values + PaddingTop * CoverWidth + PaddingLeft;
2447 const u8* screen = tic->ram->vram.screen.data;
2448 const tic_rgb* pal = getConfig(console->studio)->cart->bank0.palette.vbank0.colors;
2449
2450 for(s32 y = 0; y < Height; y++)
2451 for(s32 x = 0; x < Width; x++)
2452 ptr[CoverWidth * y + x] = tic_rgba(pal + tic_tool_peek4(screen, y * TIC80_WIDTH + x));
2453 }
2454
2455 cover = png_write(img);
2456
2457 free(img.data);
2458 }
2459
2460 png_buffer zip = png_create(sizeof(tic_cartridge));
2461
2462 {
2463 png_buffer cart = png_create(sizeof(tic_cartridge));
2464 cart.size = tic_cart_save(&tic->cart, cart.data);
2465 zip.size = tic_tool_zip(zip.data, zip.size, cart.data, cart.size);
2466 free(cart.data);
2467 }
2468
2469 png_buffer result = png_encode(cover, zip);
2470 free(zip.data);
2471 free(cover.data);
2472
2473 buffer = result.data;
2474 size = result.size;
2475 }
2476#if defined(TIC80_PRO)
2477 else if(tic_project_ext(name))
2478 {
2479 size = tic_project_save(name, buffer, &tic->cart);
2480 }
2481#endif
2482 else
2483 {
2484 name = getCartName(name);
2485 size = tic_cart_save(&tic->cart, buffer);
2486 }
2487
2488 if(size && tic_fs_save(console->fs, name, buffer, size, true))
2489 {
2490 setCartName(console, name, tic_fs_path(console->fs, name));
2491 success = true;
2492 studioRomSaved(console->studio);
2493 }
2494 }
2495
2496 free(buffer);
2497 }
2498 }
2499 else if (strlen(console->rom.name))
2500 {
2501 return saveCartName(console, console->rom.name);
2502 }
2503 else return CART_SAVE_MISSING_NAME;
2504
2505 return success ? CART_SAVE_OK : CART_SAVE_ERROR;
2506}
2507
2508static CartSaveResult saveCart(Console* console)
2509{
2510 return saveCartName(console, NULL);
2511}
2512
2513static void onSaveCommandConfirmed(Console* console)
2514{
2515 CartSaveResult rom = saveCartName(console, console->desc->count ? console->desc->params->key : NULL);
2516
2517 if(rom == CART_SAVE_OK)
2518 {
2519 printBack(console, "\ncart ");
2520 printFront(console, console->rom.name);
2521 printBack(console, " saved!\n");
2522 }
2523 else if(rom == CART_SAVE_MISSING_NAME)
2524 printBack(console, "\ncart name is missing\n");
2525 else
2526 printBack(console, "\ncart saving error");
2527
2528 commandDone(console);
2529}
2530
2531static void onSaveCommand(Console* console)
2532{
2533 const char* param = console->desc->count ? console->desc->params->key : NULL;
2534
2535 if(param && strlen(param) &&
2536 (tic_fs_exists(console->fs, param) ||
2537 tic_fs_exists(console->fs, getCartName(param))))
2538 {
2539 static const char* Rows[] =
2540 {
2541 "WARNING!",
2542 "The cart already exists",
2543 "Do you want to overwrite it?",
2544 };
2545
2546 confirmCommand(console, Rows, COUNT_OF(Rows), onSaveCommandConfirmed);
2547 }
2548 else
2549 {
2550 onSaveCommandConfirmed(console);
2551 }
2552}
2553
2554static void onRunCommand(Console* console)
2555{
2556 commandDone(console);
2557
2558 runGame(console->studio);
2559}
2560
2561static void onResumeCommand(Console* console)
2562{
2563 commandDone(console);
2564
2565 resumeGame(console->studio);
2566}
2567
2568static void onEvalCommand(Console* console)
2569{
2570 printLine(console);
2571
2572 const tic_script_config* script_config = tic_core_script_config(console->tic);
2573
2574 if (script_config->eval)
2575 {
2576 if(console->desc->count)
2577 script_config->eval(console->tic,
2578 console->desc->src+strlen(console->desc->command));
2579 else printError(console, "nothing to eval");
2580 }
2581 else
2582 {
2583 printError(console, "'eval' not implemented for the script");
2584 }
2585
2586 commandDone(console);
2587}
2588
2589static void onDelCommandConfirmed(Console* console)
2590{
2591 if(console->desc->count)
2592 {
2593 if (tic_fs_ispubdir(console->fs))
2594 {
2595 printError(console, "\naccess denied");
2596 }
2597 else
2598 {
2599 const char* param = console->desc->params->key;
2600 if(tic_fs_isdir(console->fs, param))
2601 {
2602 printBack(console, tic_fs_deldir(console->fs, param)
2603 ? "\ndir not deleted"
2604 : "\ndir successfully deleted");
2605 }
2606 else
2607 {
2608 printBack(console, tic_fs_delfile(console->fs, param)
2609 ? "\nfile not deleted"
2610 : "\nfile successfully deleted");
2611 }
2612 }
2613 }
2614 else printBack(console, "\nname is missing");
2615
2616 commandDone(console);
2617}
2618
2619static void onDelCommand(Console* console)
2620{
2621 static const char* Rows[] =
2622 {
2623 "WARNING!",
2624 "Do you really want to delete file?",
2625 };
2626
2627 confirmCommand(console, Rows, COUNT_OF(Rows), onDelCommandConfirmed);
2628}
2629
2630#if defined(CAN_ADDGET_FILE)
2631
2632static void onAddFile(Console* console, const char* name, const u8* buffer, s32 size)
2633{
2634 if(name)
2635 {
2636 const char* path = tic_fs_path(console->fs, name);
2637
2638 if(!fs_exists(path))
2639 {
2640 if(fs_write(path, buffer, size))
2641 {
2642 printLine(console);
2643 printFront(console, name);
2644 printBack(console, " successfully added :)");
2645 }
2646 else printError(console, "\nerror: file not added :(");
2647 }
2648 else
2649 {
2650 printError(console, "\nerror: ");
2651 printError(console, name);
2652 printError(console, " already exists :(");
2653 }
2654 }
2655
2656 commandDone(console);
2657}
2658
2659static void onAddCommand(Console* console)
2660{
2661 void* data = NULL;
2662
2663 EM_ASM_
2664 ({
2665 Module.showAddPopup(function(filename, rom)
2666 {
2667 if(filename == null || rom == null)
2668 {
2669 dynCall('viiii', $0, [$1, 0, 0, 0]);
2670 }
2671 else
2672 {
2673 var filePtr = Module._malloc(filename.length + 1);
2674 stringToUTF8(filename, filePtr, filename.length + 1);
2675
2676 var dataPtr = Module._malloc(rom.length);
2677 writeArrayToMemory(rom, dataPtr);
2678
2679 dynCall('viiii', $0, [$1, filePtr, dataPtr, rom.length]);
2680
2681 Module._free(filePtr);
2682 Module._free(dataPtr);
2683 }
2684 });
2685 }, onAddFile, console);
2686}
2687
2688static void onGetCommand(Console* console)
2689{
2690 if(console->desc->count)
2691 {
2692 const char* name = console->desc->params->key;
2693 const char* path = tic_fs_path(console->fs, name);
2694
2695 if(fs_exists(path))
2696 {
2697 s32 size = 0;
2698 void* buffer = fs_read(path, &size);
2699
2700 EM_ASM_
2701 ({
2702 var name = UTF8ToString($0);
2703 var blob = new Blob([HEAPU8.subarray($1, $1 + $2)], {type: "application/octet-stream"});
2704
2705 Module.saveAs(blob, name);
2706 }, name, buffer, size);
2707 }
2708 else
2709 {
2710 printError(console, "\nerror: ");
2711 printError(console, name);
2712 printError(console, " doesn't exist :(");
2713 }
2714 }
2715 else printBack(console, "\nusage: get <file>");
2716
2717 commandDone(console);
2718}
2719
2720#endif
2721
2722// Declare this here to resolve a cyclic dependency with COMMANDS_LIST.
2723static void tabCompleteHelp(TabCompleteData* data);
2724
2725static const char HelpUsage[] = "help [<text>"
2726#define HELP_CMD_DEF(name) "|" #name
2727 HELP_CMD_LIST(HELP_CMD_DEF)
2728#undef HELP_CMD_DEF
2729 "]";
2730
2731#define SECTION_DEF(NAME, ...) "|" #NAME
2732#define EXPORT_CMD_DEF(name) #name "|"
2733#define EXPORT_KEYS_DEF(name) #name "=0 "
2734#define IMPORT_CMD_DEF(name) #name "|"
2735#define IMPORT_KEYS_DEF(key) #key"=0 "
2736
2737#if defined(CAN_ADDGET_FILE)
2738#define ADDGET_FILE(macro) \
2739 macro("add", \
2740 NULL, \
2741 "upload file to the browser local storage.", \
2742 NULL, \
2743 onAddCommand, \
2744 NULL, \
2745 NULL) \
2746 \
2747 macro("get", \
2748 NULL, \
2749 "download file from the browser local storage.", \
2750 "get <file>", \
2751 onGetCommand, \
2752 tabCompleteFiles, \
2753 NULL) \
2754
2755#else
2756#define ADDGET_FILE(macro)
2757#endif
2758
2759// macro(name, alt, help, usage, handler, tab-complete for first param, for second param)
2760#define COMMANDS_LIST(macro) \
2761 macro("help", \
2762 NULL, \
2763 "show help info about commands/api/...", \
2764 HelpUsage, \
2765 onHelpCommand, \
2766 tabCompleteHelp, \
2767 NULL) \
2768 \
2769 macro("exit", \
2770 "quit", \
2771 "exit the application.", \
2772 NULL, \
2773 onExitCommand, \
2774 NULL, \
2775 NULL) \
2776 \
2777 macro("edit", \
2778 NULL, \
2779 "open cart editors.", \
2780 NULL, \
2781 onEditCommand, \
2782 NULL, \
2783 NULL) \
2784 \
2785 macro("new", \
2786 NULL, \
2787 "creates a new `Hello World` cartridge.", \
2788 "new <$LANG_NAMES_PIPE$>", \
2789 onNewCommand, \
2790 tabCompleteLanguages, \
2791 NULL) \
2792 \
2793 macro("load", \
2794 NULL, \
2795 "load cartridge from the local filesystem" \
2796 "(there's no need to type the .tic extension).\n" \
2797 "you can also load just the section (sprites, map etc) from another cart.", \
2798 "load <cart> [code" TIC_SYNC_LIST(SECTION_DEF) "]", \
2799 onLoadCommand, \
2800 tabCompleteFiles, \
2801 NULL) \
2802 \
2803 macro("save", \
2804 NULL, \
2805 "save cartridge to the local filesystem, use $LANG_EXTENSIONS$" \
2806 "cart extension to save it in text format (PRO feature).", \
2807 "save <cart>", \
2808 onSaveCommand, \
2809 tabCompleteFiles, \
2810 NULL) \
2811 \
2812 macro("run", \
2813 NULL, \
2814 "run current cart / project.", \
2815 NULL, \
2816 onRunCommand, \
2817 NULL, \
2818 NULL) \
2819 \
2820 macro("resume", \
2821 NULL, \
2822 "resume last run cart / project.", \
2823 NULL, \
2824 onResumeCommand, \
2825 NULL, \
2826 NULL) \
2827 \
2828 macro("eval", \
2829 "=", \
2830 "run code provided code.", \
2831 NULL, \
2832 onEvalCommand, \
2833 NULL, \
2834 NULL) \
2835 \
2836 macro("dir", \
2837 "ls", \
2838 "show list of local files.", \
2839 NULL, \
2840 onDirCommand, \
2841 NULL, \
2842 NULL) \
2843 \
2844 macro("cd", \
2845 NULL, \
2846 "change directory.", \
2847 "\ncd <path>\ncd /\ncd ..", \
2848 onChangeDirectory, \
2849 tabCompleteDirs, \
2850 NULL) \
2851 \
2852 macro("mkdir", \
2853 NULL, \
2854 "make a directory.", \
2855 "mkdir <name>", \
2856 onMakeDirectory, \
2857 NULL, \
2858 NULL) \
2859 \
2860 macro("folder", \
2861 NULL, \
2862 "open working directory in OS.", \
2863 NULL, \
2864 onFolderCommand, \
2865 NULL, \
2866 NULL) \
2867 \
2868 macro("export", \
2869 NULL, \
2870 "export cart to HTML,\n" \
2871 "native build (win linux rpi mac),\n" \
2872 "export sprites/map/... as a .png image " \
2873 "or export sfx and music to .wav files.", \
2874 "\nexport [" EXPORT_CMD_LIST(EXPORT_CMD_DEF) "...] " \
2875 "<file> [" EXPORT_KEYS_LIST(EXPORT_KEYS_DEF) "...]" , \
2876 onExportCommand, \
2877 tabCompleteExport, \
2878 tabCompleteFiles) \
2879 \
2880 macro("import", \
2881 NULL, \
2882 "import code/sprites/map/... from an external file.", \
2883 "\nimport [" IMPORT_CMD_LIST(IMPORT_CMD_DEF) "...] " \
2884 "<file> [" IMPORT_KEYS_LIST(IMPORT_KEYS_DEF) "...]", \
2885 onImportCommand, \
2886 tabCompleteImport, \
2887 tabCompleteFiles) \
2888 \
2889 macro("del", \
2890 NULL, \
2891 "delete from the filesystem.", \
2892 "del <file|folder>", \
2893 onDelCommand, \
2894 tabCompleteFilesAndDirs, \
2895 NULL) \
2896 \
2897 macro("cls", \
2898 "clear", \
2899 "clear console screen.", \
2900 NULL, \
2901 onClsCommand, \
2902 NULL, \
2903 NULL) \
2904 \
2905 macro("demo", \
2906 NULL, \
2907 "install demo carts to the current directory.", \
2908 NULL, \
2909 onInstallDemosCommand, \
2910 NULL, \
2911 NULL) \
2912 \
2913 macro("config", \
2914 NULL, \
2915 "edit system configuration cartridge,\n" \
2916 "use `reset` param to reset current configuration,\n" \
2917 "use `default` to edit default cart template.", \
2918 "config [reset|default]", \
2919 onConfigCommand, \
2920 tabCompleteConfig, \
2921 NULL) \
2922 \
2923 macro("surf", \
2924 NULL, \
2925 "open carts browser.", \
2926 NULL, \
2927 onSurfCommand, \
2928 NULL, \
2929 NULL) \
2930 \
2931 macro("menu", \
2932 NULL, \
2933 "show game menu where you can setup video, sound and input options.", \
2934 NULL, \
2935 onGameMenuCommand, \
2936 NULL, \
2937 NULL) \
2938 ADDGET_FILE(macro)
2939
2940static struct Command
2941{
2942 const char* name;
2943 const char* alt;
2944 const char* help;
2945 const char* usage;
2946 void(*handler)(Console*);
2947 void(*tabComplete1)(TabCompleteData*);
2948 void(*tabComplete2)(TabCompleteData*);
2949
2950} Commands[] =
2951{
2952#define COMMANDS_DEF(name, alt, help, usage, handler, tabComplete1, tabComplete2) \
2953 {name, alt, help, usage, handler, tabComplete1, tabComplete2},
2954 COMMANDS_LIST(COMMANDS_DEF)
2955#undef COMMANDS_DEF
2956};
2957
2958#undef SECTION_DEF
2959#undef EXPORT_CMD_DEF
2960#undef EXPORT_KEYS_DEF
2961#undef IMPORT_CMD_DEF
2962#undef IMPORT_KEYS_DEF
2963
2964typedef struct Command Command;
2965
2966#define API_LIST(macro) \
2967 TIC_CALLBACK_LIST(macro) \
2968 TIC_API_LIST(macro)
2969
2970static struct ApiItem {const char* name; const char* def; const char* help;} Api[] =
2971{
2972#define TIC_API_DEF(name, def, help, ...) {#name, def, help},
2973 API_LIST(TIC_API_DEF)
2974#undef TIC_API_DEF
2975};
2976
2977typedef struct ApiItem ApiItem;
2978
2979static void tabCompleteHelp(TabCompleteData* data)
2980{
2981#define HELP_CMD_DEF(name) addTabCompleteOption(data, #name);
2982 HELP_CMD_LIST(HELP_CMD_DEF)
2983#undef HELP_CMD_DEF
2984
2985 for(s32 i = 0; i < COUNT_OF(Commands); i++)
2986 {
2987 addTabCompleteOption(data, Commands[i].name);
2988 }
2989
2990#define TIC_API_DEF(name, def, help, ...) addTabCompleteOption(data, #name);
2991 API_LIST(TIC_API_DEF)
2992#undef TIC_API_DEF
2993
2994 finishTabComplete(data);
2995}
2996
2997
2998static s32 createRamTable(char* buf)
2999{
3000 char* ptr = buf;
3001 ptr += sprintf(ptr, "\n+-----------------------------------+"
3002 "\n| 96KB RAM LAYOUT |"
3003 "\n+-------+-------------------+-------+"
3004 "\n| ADDR | INFO | BYTES |"
3005 "\n+-------+-------------------+-------+");
3006
3007 static const struct Row {s32 addr; const char* info;} Rows[] =
3008 {
3009 {0, "<VRAM>"},
3010 {offsetof(tic_ram, tiles), "TILES"},
3011 {offsetof(tic_ram, sprites), "SPRITES"},
3012 {offsetof(tic_ram, map), "MAP"},
3013 {offsetof(tic_ram, input.gamepads), "GAMEPADS"},
3014 {offsetof(tic_ram, input.mouse), "MOUSE"},
3015 {offsetof(tic_ram, input.keyboard), "KEYBOARD"},
3016 {offsetof(tic_ram, sfxpos), "SFX STATE"},
3017 {offsetof(tic_ram, registers), "SOUND REGISTERS"},
3018 {offsetof(tic_ram, sfx.waveforms), "WAVEFORMS"},
3019 {offsetof(tic_ram, sfx.samples), "SFX"},
3020 {offsetof(tic_ram, music.patterns.data), "MUSIC PATTERNS"},
3021 {offsetof(tic_ram, music.tracks.data), "MUSIC TRACKS"},
3022 {offsetof(tic_ram, music_state), "MUSIC STATE"},
3023 {offsetof(tic_ram, stereo), "STEREO VOLUME"},
3024 {offsetof(tic_ram, persistent), "PERSISTENT MEMORY"},
3025 {offsetof(tic_ram, flags), "SPRITE FLAGS"},
3026 {offsetof(tic_ram, font.regular), "FONT"},
3027 {offsetof(tic_ram, font.regular.params), "FONT PARAMS"},
3028 {offsetof(tic_ram, font.alt), "ALT FONT"},
3029 {offsetof(tic_ram, font.alt.params), "ALT FONT PARAMS"},
3030 {offsetof(tic_ram, mapping), "BUTTONS MAPPING"},
3031 {offsetof(tic_ram, free), "... (free)"},
3032 {TIC_RAM_SIZE, ""},
3033 };
3034
3035 for(const struct Row* row = Rows, *end = row + COUNT_OF(Rows) - 1; row < end; row++)
3036 ptr += sprintf(ptr, "\n| %05X | %-17s | %-5i |", row->addr, row->info, (row + 1)->addr - row->addr);
3037
3038 ptr += sprintf(ptr, "\n+-------+-------------------+-------+\n");
3039
3040 return strlen(buf);
3041}
3042
3043static s32 createVRamTable(char* buf)
3044{
3045 char* ptr = buf;
3046 ptr += sprintf(ptr, "\n+-----------------------------------+"
3047 "\n| 16KB VRAM LAYOUT |"
3048 "\n+-------+-------------------+-------+"
3049 "\n| ADDR | INFO | BYTES |"
3050 "\n+-------+-------------------+-------+");
3051
3052 static const struct Row {s32 addr; const char* info;} Rows[] =
3053 {
3054 {offsetof(tic_ram, vram.screen), "SCREEN"},
3055 {offsetof(tic_ram, vram.palette), "PALETTE"},
3056 {offsetof(tic_ram, vram.mapping), "PALETTE MAP"},
3057 {offsetof(tic_ram, vram.vars), "BORDER COLOR"},
3058 {offsetof(tic_ram, vram.vars.offset), "SCREEN OFFSET"},
3059 {offsetof(tic_ram, vram.vars.cursor), "MOUSE CURSOR"},
3060 {offsetof(tic_ram, vram.blit), "BLIT SEGMENT"},
3061 {offsetof(tic_ram, vram.reserved), "... (reserved) "},
3062 {TIC_VRAM_SIZE, ""},
3063 };
3064
3065 for(const struct Row* row = Rows, *end = row + COUNT_OF(Rows) - 1; row < end; row++)
3066 ptr += sprintf(ptr, "\n| %05X | %-17s | %-5i |", row->addr, row->info, (row + 1)->addr - row->addr);
3067
3068 ptr += sprintf(ptr, "\n+-------+-------------------+-------+\n");
3069
3070 return strlen(buf);
3071}
3072
3073static s32 createKeysTable(char* buf)
3074{
3075 char* ptr = buf;
3076 ptr += sprintf(ptr, "\n+------+-----+ +------+--------------+"
3077 "\n| CODE | KEY | | CODE | KEY |"
3078 "\n+------+-----+ +------+--------------+");
3079
3080 static const struct Row {s32 code; const char* key;} Rows[] =
3081 {
3082 {1, "A"},
3083 {2, "B"},
3084 {3, "C"},
3085 {4, "D"},
3086 {5, "E"},
3087 {6, "F"},
3088 {7, "G"},
3089 {8, "H"},
3090 {9, "I"},
3091 {10, "J"},
3092 {11, "K"},
3093 {12, "L"},
3094 {13, "M"},
3095 {14, "N"},
3096 {15, "O"},
3097 {16, "P"},
3098 {17, "Q"},
3099 {18, "R"},
3100 {19, "S"},
3101 {20, "T"},
3102 {21, "U"},
3103 {22, "V"},
3104 {23, "W"},
3105 {24, "X"},
3106 {25, "Y"},
3107 {26, "Z"},
3108 {27, "0"},
3109 {28, "1"},
3110 {29, "2"},
3111 {30, "3"},
3112 {31, "4"},
3113 {32, "5"},
3114 {33, "6"},
3115 {34, "7"},
3116 {35, "8"},
3117 {36, "9"},
3118 {37, "MINUS"},
3119 {38, "EQUALS"},
3120 {39, "LEFTBRACKET"},
3121 {40, "RIGHTBRACKET"},
3122 {41, "BACKSLASH"},
3123 {42, "SEMICOLON"},
3124 {43, "APOSTROPHE"},
3125 {44, "GRAVE"},
3126 {45, "COMMA"},
3127 {46, "PERIOD"},
3128 {47, "SLASH"},
3129 {48, "SPACE"},
3130 {49, "TAB"},
3131 {50, "RETURN"},
3132 {51, "BACKSPACE"},
3133 {52, "DELETE"},
3134 {53, "INSERT"},
3135 {54, "PAGEUP"},
3136 {55, "PAGEDOWN"},
3137 {56, "HOME"},
3138 {57, "END"},
3139 {58, "UP"},
3140 {59, "DOWN"},
3141 {60, "LEFT"},
3142 {61, "RIGHT"},
3143 {62, "CAPSLOCK"},
3144 {63, "CTRL"},
3145 {64, "SHIFT"},
3146 {65, "ALT"},
3147 };
3148
3149 int lastAlphaNumeric = 36;
3150 for(const struct Row* row = Rows, *end = row + lastAlphaNumeric; row < end; row++)
3151 {
3152 const struct Row* otherRow = row + lastAlphaNumeric;
3153 ptr += sprintf(ptr, "\n| ");
3154 ptr += sprintf(ptr, "%4d | %-3s |", row->code, row->key);
3155 if (otherRow < Rows + COUNT_OF(Rows))
3156 ptr += sprintf(ptr, " | %4d | %-12s |", otherRow->code, otherRow->key);
3157 else
3158 ptr += sprintf(ptr, " | %4s | %12s |", "", "");
3159 }
3160
3161 ptr += sprintf(ptr, "\n+------+-----+ +------+--------------+\n");
3162
3163 return strlen(buf);
3164}
3165
3166static s32 createButtonsTable(char* buf)
3167{
3168 char* ptr = buf;
3169 ptr += sprintf(ptr, "\n+--------+----+----+----+----+"
3170 "\n| ACTION | P1 | P2 | P3 | P4 |"
3171 "\n+--------+----+----+----+----+");
3172
3173 static const struct Row {const char* action;} Rows[] =
3174 {
3175 {"UP"},
3176 {"DOWN"},
3177 {"LEFT"},
3178 {"RIGHT"},
3179 {"A"},
3180 {"B"},
3181 {"X"},
3182 {"Y"},
3183 };
3184
3185 int id = 0;
3186 for(const struct Row* row = Rows, *end = row + COUNT_OF(Rows); row < end; row++) {
3187 ptr += sprintf(ptr, "\n| %6s | %2d | %2d | %2d | %2d |", row->action, id, id + 8, id + 16, id + 24);
3188 id++;
3189 }
3190
3191 ptr += sprintf(ptr, "\n+--------+----+----+----+----+");
3192
3193 return strlen(buf);
3194}
3195
3196static void onExport_help(Console* console, const char* param, const char* name, ExportParams params)
3197{
3198 const char* filename = getFilename(name, ".md");
3199
3200 char* buf = malloc(TIC_BANK_SIZE), *ptr = buf;
3201
3202 SCOPE(free(buf))
3203 {
3204 ptr += sprintf(ptr, "# " TIC_NAME_FULL "\n" TIC_VERSION"\n" TIC_COPYRIGHT"\n");
3205 ptr += sprintf(ptr, "\n## Welcome\n%s\n", WelcomeText);
3206 ptr += sprintf(ptr, "\n## Specification\n```\n");
3207
3208 FOR(const struct SpecRow*, row, SpecText1)
3209 ptr += sprintf(ptr, "%-10s%s\n", row->section, row->info);
3210
3211 ptr += sprintf(ptr, "```\n```\n");
3212 ptr += createRamTable(ptr);
3213 ptr += sprintf(ptr, "```\n```");
3214 ptr += createVRamTable(ptr);
3215 ptr += sprintf(ptr, "```\n\n## Console commands\n");
3216
3217 FOR(const Command*, cmd, Commands)
3218 ptr += sprintf(ptr, "\n### %s\n%s\nusage: `%s`\n",
3219 cmd->name, cmd->help, cmd->usage ? cmd->usage : cmd->name);
3220
3221 ptr += sprintf(ptr, "\n## API functions\n");
3222
3223 FOR(const ApiItem*, api, Api)
3224 ptr += sprintf(ptr, "\n### %s\n`%s`\n%s\n", api->name, api->def, api->help);
3225
3226 ptr += sprintf(ptr, "\n## Button IDs\n");
3227 ptr += sprintf(ptr, "```");
3228 ptr += createButtonsTable(ptr);
3229 ptr += sprintf(ptr, "```\n");
3230
3231 ptr += sprintf(ptr, "\n## Key IDs\n");
3232 ptr += sprintf(ptr, "```");
3233 ptr += createKeysTable(ptr);
3234 ptr += sprintf(ptr, "```\n");
3235
3236 ptr += sprintf(ptr, "\n## Startup options\n```\n");
3237 FOR(const struct StartupOption*, opt, StartupOptions)
3238 ptr += sprintf(ptr, "--%-14s %s\n", opt->name, opt->help);
3239
3240 ptr += sprintf(ptr, "```\n\n%s\n\n%s", TermsText, LicenseText);
3241
3242 char* helpReplaced = replaceHelpTokens(buf);
3243
3244 SCOPE(free(helpReplaced))
3245 {
3246 onFileExported(console, filename, tic_fs_save(console->fs, filename, helpReplaced, strlen(helpReplaced), true));
3247 }
3248 }
3249}
3250
3251TabCompleteData newTabCompleteData(Console* console, char* incompleteWord) {
3252 TabCompleteData data = { console, .incompleteWord = incompleteWord };
3253 data.options = malloc(CONSOLE_BUFFER_SCREEN);
3254 data.commonPrefix = malloc(CONSOLE_BUFFER_SCREEN);
3255 data.options[0] = '\0';
3256 data.commonPrefix[0] = '\0';
3257
3258 return data;
3259}
3260
3261static void processConsoleTab(Console* console)
3262{
3263 char* input = console->input.text;
3264 char* param = strchr(input, ' ');
3265
3266 if(param)
3267 {
3268 // Tab-complete command's parameters.
3269 param++;
3270 char* secondParam = strchr(param, ' ');
3271 if (secondParam)
3272 secondParam++;
3273
3274 for(s32 i = 0; i < COUNT_OF(Commands); i++)
3275 {
3276 s32 commandLen = param-input-1;
3277 bool commandMatches = (strlen(Commands[i].name) == commandLen &&
3278 strncmp(Commands[i].name, input, commandLen) == 0) ||
3279 (Commands[i].alt &&
3280 strlen(Commands[i].name) == commandLen &&
3281 strncmp(Commands[i].alt, input, commandLen) == 0);
3282
3283 if (commandMatches)
3284 {
3285 if (secondParam) {
3286 if (Commands[i].tabComplete2) {
3287 TabCompleteData data = newTabCompleteData(console, secondParam);
3288 Commands[i].tabComplete2(&data);
3289 }
3290 } else {
3291 if (Commands[i].tabComplete1) {
3292 TabCompleteData data = newTabCompleteData(console, param);
3293 Commands[i].tabComplete1(&data);
3294 }
3295 }
3296 }
3297 }
3298 }
3299 else
3300 {
3301 // Tab-complete commands.
3302 TabCompleteData data = newTabCompleteData(console, input);
3303 for(s32 i = 0; i < COUNT_OF(Commands); i++)
3304 {
3305 addTabCompleteOption(&data, Commands[i].name);
3306 if (Commands[i].alt)
3307 addTabCompleteOption(&data, Commands[i].alt);
3308 }
3309 finishTabComplete(&data);
3310 }
3311 scrollConsole(console);
3312}
3313
3314static void toUpperStr(char* str)
3315{
3316 while(*str)
3317 {
3318 *str = toupper(*str);
3319 str++;
3320 }
3321}
3322
3323static bool printUsage(Console* console, const char* command)
3324{
3325 FOR(const Command*, cmd, Commands)
3326 {
3327 if(strcmp(command, cmd->name) == 0)
3328 {
3329 consolePrint(console, "\n---=== COMMAND ===---\n", tic_color_green);
3330 char* helpReplaced = replaceHelpTokens(cmd->help);
3331 printBack(console, helpReplaced);
3332 free(helpReplaced);
3333
3334 if(cmd->usage)
3335 {
3336 printFront(console, "\n\nusage: ");
3337 char* usageReplaced = replaceHelpTokens(cmd->usage);
3338 printBack(console, usageReplaced);
3339 free(usageReplaced);
3340 }
3341
3342 printLine(console);
3343 return true;
3344 }
3345 }
3346
3347 return false;
3348}
3349
3350static bool printApi(Console* console, const char* param)
3351{
3352 FOR(const ApiItem*, api, Api)
3353 {
3354 if(strcmp(param, api->name) == 0)
3355 {
3356 printLine(console);
3357 consolePrint(console, "---=== API ===---\n", tic_color_blue);
3358 consolePrint(console, api->def, tic_color_light_blue);
3359 printFront(console, "\n\n");
3360 printBack(console, api->help);
3361 printLine(console);
3362 return true;
3363 }
3364 }
3365
3366 return false;
3367}
3368
3369#define STRBUF_SIZE(name, ...) + STRLEN(#name) + STRLEN(Sep)
3370
3371static void onHelp_api(Console* console)
3372{
3373 consolePrint(console, "\nAPI functions:\n", tic_color_blue);
3374 {
3375 const char Sep[] = " ";
3376
3377 // calc buf size on compile time
3378 char buf[API_LIST(STRBUF_SIZE) + 1] = {[0] = 0};
3379
3380 FOR(const ApiItem*, api, Api)
3381 strcat(buf, api->name), strcat(buf, Sep);
3382
3383 printBack(console, buf);
3384 }
3385}
3386
3387static void onHelp_commands(Console* console)
3388{
3389 consolePrint(console, "\nConsole commands:\n", tic_color_green);
3390 {
3391 const char Sep[] = " ";
3392
3393 // calc buf size on compile time
3394 char buf[COMMANDS_LIST(STRBUF_SIZE) + 1] = {[0] = 0};
3395
3396 FOR(const Command*, cmd, Commands)
3397 strcat(buf, cmd->name), strcat(buf, Sep);
3398
3399 printBack(console, buf);
3400 }
3401}
3402
3403#undef STRBUF_SIZE
3404
3405static void printTable(Console* console, const char* text)
3406{
3407#ifndef BAREMETALPI
3408 printf("%s", text);
3409#endif
3410
3411 for(const char* textPointer = text, *endText = textPointer + strlen(text); textPointer != endText;)
3412 {
3413 char symbol = *textPointer++;
3414
3415 scrollConsole(console);
3416
3417 if(symbol == '\n')
3418 nextLine(console);
3419 else
3420 {
3421 u8 color = 0;
3422
3423 switch(symbol)
3424 {
3425 case '+':
3426 case '|':
3427 case '-':
3428 color = tic_color_dark_grey;
3429 break;
3430 default:
3431 color = CONSOLE_FRONT_TEXT_COLOR;
3432 }
3433
3434 setSymbol(console, symbol, color, cursorOffset(console));
3435
3436 console->cursor.pos.x++;
3437
3438 if(console->cursor.pos.x >= CONSOLE_BUFFER_WIDTH)
3439 nextLine(console);
3440 }
3441 }
3442}
3443
3444static void onHelp_ram(Console* console)
3445{
3446 char buf[2048];
3447 createRamTable(buf);
3448 printTable(console, buf);
3449}
3450
3451static void onHelp_vram(Console* console)
3452{
3453 char buf[1024];
3454 createVRamTable(buf);
3455 printTable(console, buf);
3456}
3457
3458static void onHelp_keys(Console* console)
3459{
3460 char buf[4096];
3461 createKeysTable(buf);
3462 printTable(console, buf);
3463}
3464
3465static void onHelp_buttons(Console* console)
3466{
3467 char buf[1024];
3468 createButtonsTable(buf);
3469 printTable(console, buf);
3470}
3471
3472static void onHelp_version(Console* console)
3473{
3474 consolePrint(console, "\n"TIC_VERSION, CONSOLE_BACK_TEXT_COLOR);
3475}
3476
3477static void onHelp_spec(Console* console)
3478{
3479 printLine(console);
3480
3481 char buf[TICNAME_MAX];
3482
3483 FOR(const struct SpecRow*, row, SpecText1)
3484 {
3485#define OFFSET 8
3486 char* rowReplaced = replaceHelpTokens(row->info);
3487 sprintf(buf, "%-" DEF2STR(OFFSET) "s%s\n", row->section, rowReplaced);
3488 consolePrintOffset(console, buf, tic_color_grey, OFFSET);
3489 free(rowReplaced);
3490#undef OFFSET
3491 }
3492}
3493
3494static void onHelp_welcome(Console* console)
3495{
3496 printLine(console);
3497 printBack(console, WelcomeText);
3498}
3499
3500static void onHelp_startup(Console* console)
3501{
3502 char buf[TICNAME_MAX];
3503 printFront(console, "\nStartup options:\n");
3504 FOR(const struct StartupOption*, opt, StartupOptions)
3505 {
3506#define OFFSET 12
3507#define PREFIX "--"
3508 sprintf(buf, PREFIX "%-" DEF2STR(OFFSET) "s%s\n", opt->name, opt->help);
3509 consolePrintOffset(console, buf, tic_color_grey, OFFSET + STRLEN(PREFIX));
3510#undef PREFIX
3511#undef OFFSET
3512 }
3513}
3514
3515static void onHelp_terms(Console* console)
3516{
3517 printLine(console);
3518 printBack(console, TermsText);
3519}
3520
3521static void onHelp_license(Console* console)
3522{
3523 printLine(console);
3524 printBack(console, LicenseText);
3525}
3526
3527static void onHelpCommand(Console* console)
3528{
3529 if(console->desc->count)
3530 {
3531 const char* param = console->desc->params->key;
3532
3533 if(printUsage(console, param)) return commandDone(console);
3534 else if(printApi(console, param)) return commandDone(console);
3535 else
3536 {
3537
3538 static const struct Handler {const char* cmd; void(*handler)(Console*);} Handlers[] =
3539 {
3540#define HELP_CMD_DEF(name) {#name, onHelp_##name},
3541 HELP_CMD_LIST(HELP_CMD_DEF)
3542#undef HELP_CMD_DEF
3543 };
3544
3545 FOR(const struct Handler*, ptr, Handlers)
3546 if(strcmp(ptr->cmd, param) == 0)
3547 {
3548 ptr->handler(console);
3549 return commandDone(console);
3550 }
3551 }
3552
3553 printError(console, "\nunknown topic: ");
3554 printError(console, param);
3555 }
3556 else
3557 {
3558 printFront(console, "\n\nusage: ");
3559 printBack(console, HelpUsage);
3560
3561 printBack(console, "\n\ntype ");
3562 printFront(console, "help commands");
3563 printBack(console, " to show commands");
3564
3565 printBack(console, "\n\npress ");
3566 printFront(console, "ESC");
3567 printBack(console, " to switch editor/console\n");
3568 }
3569
3570 commandDone(console);
3571}
3572
3573static CommandDesc parseCommand(const char* input)
3574{
3575 CommandDesc desc = {.src = strdup(input),
3576 .command = strdup(input)};
3577
3578 char* token = strtok(desc.command, " ");
3579
3580 while((token = strtok(NULL, " ")))
3581 {
3582 desc.params = realloc(desc.params, ++desc.count * sizeof *desc.params);
3583 desc.params[desc.count - 1].key = token;
3584 }
3585
3586 for(struct Param* it = desc.params, *end = it + desc.count; it < end; it++)
3587 {
3588 it->key = strtok(it->key, "=");
3589 it->val = strtok(NULL, "=");
3590 }
3591
3592 return desc;
3593}
3594
3595static void processCommand(Console* console, const char* text)
3596{
3597 console->active = false;
3598
3599 *console->desc = parseCommand(text);
3600
3601 if (console->desc->command)
3602 {
3603 const char* command = console->desc->command;
3604
3605 FOR(const Command*, cmd, Commands)
3606 if(casecmp(console->desc->command, cmd->name) == 0 ||
3607 (cmd->alt && casecmp(console->desc->command, cmd->alt) == 0))
3608 {
3609 cmd->handler(console);
3610 command = NULL;
3611 break;
3612 }
3613
3614 if(command)
3615 {
3616 printLine(console);
3617 printError(console, "unknown command: ");
3618 printError(console, command);
3619 commandDone(console);
3620 }
3621 }
3622 else commandDone(console);
3623}
3624
3625static void fillHistory(Console* console)
3626{
3627 if(console->history.size)
3628 {
3629 console->input.pos = 0;
3630 memset(console->input.text, '\0', strlen(console->input.text));
3631
3632 const char* item = console->history.items[console->history.index];
3633 strcpy(console->input.text, item);
3634 memset(console->color + getInputOffset(console), CONSOLE_INPUT_COLOR, strlen(item));
3635 processConsoleEnd(console);
3636 }
3637}
3638
3639static void onHistoryUp(Console* console)
3640{
3641 fillHistory(console);
3642
3643 if(console->history.index > 0)
3644 console->history.index--;
3645}
3646
3647static void onHistoryDown(Console* console)
3648{
3649 if(console->history.index < console->history.size - 1)
3650 {
3651 console->history.index++;
3652 fillHistory(console);
3653 }
3654 else
3655 {
3656 memset(console->input.text, '\0', strlen(console->input.text));
3657 processConsoleEnd(console);
3658 }
3659}
3660
3661static void appendHistory(Console* console, const char* value)
3662{
3663 if(console->history.size)
3664 if(strcmp(console->history.items[console->history.index = console->history.size - 1], value) == 0)
3665 return;
3666
3667 console->history.index = console->history.size++;
3668 console->history.items = realloc(console->history.items, sizeof(char*) * console->history.size);
3669 console->history.items[console->history.index] = strdup(value);
3670}
3671
3672static void processConsoleCommand(Console* console)
3673{
3674 size_t commandSize = strlen(console->input.text);
3675
3676 if(commandSize)
3677 {
3678 printf("%s", console->input.text);
3679 appendHistory(console, console->input.text);
3680 processCommand(console, console->input.text);
3681 }
3682 else commandDone(console);
3683}
3684
3685static void error(Console* console, const char* info)
3686{
3687 consolePrint(console, info ? info : "unknown error", CONSOLE_ERROR_TEXT_COLOR);
3688 commandDone(console);
3689}
3690
3691static void trace(Console* console, const char* text, u8 color)
3692{
3693 consolePrint(console, text, color);
3694 commandDone(console);
3695}
3696
3697static void setScroll(Console* console, s32 val)
3698{
3699 if(console->scroll.pos != val)
3700 {
3701 console->scroll.pos = MIN(CLAMP(val, 0, console->cursor.pos.y), CONSOLE_BUFFER_ROWS - CONSOLE_BUFFER_HEIGHT);
3702 }
3703}
3704
3705#if defined (TIC_BUILD_WITH_LUA)
3706
3707static lua_State* netLuaInit(u8* buffer, s32 size)
3708{
3709 if (buffer && size)
3710 {
3711 char* script = calloc(1, size + 1);
3712 memcpy(script, buffer, size);
3713 lua_State* lua = luaL_newstate();
3714
3715 if(lua)
3716 {
3717 if(luaL_loadstring(lua, (char*)script) == LUA_OK && lua_pcall(lua, 0, LUA_MULTRET, 0) == LUA_OK)
3718 return lua;
3719
3720 else lua_close(lua);
3721 }
3722
3723 free(script);
3724 }
3725
3726 return NULL;
3727}
3728
3729static void onHttpVesrsionGet(const net_get_data* data)
3730{
3731 Console* console = (Console*)data->calldata;
3732
3733 switch(data->type)
3734 {
3735 case net_get_done:
3736 {
3737 lua_State* lua = netLuaInit(data->done.data, data->done.size);
3738
3739 union
3740 {
3741 struct
3742 {
3743 s32 major;
3744 s32 minor;
3745 s32 patch;
3746 };
3747
3748 s32 data[3];
3749 } version =
3750 {
3751 {
3752 .major = TIC_VERSION_MAJOR,
3753 .minor = TIC_VERSION_MINOR,
3754 .patch = TIC_VERSION_REVISION,
3755 },
3756 };
3757
3758 if(lua)
3759 {
3760 static const char* Fields[] = {"major", "minor", "patch"};
3761
3762 for(s32 i = 0; i < COUNT_OF(Fields); i++)
3763 {
3764 lua_getglobal(lua, Fields[i]);
3765
3766 if(lua_isinteger(lua, -1))
3767 version.data[i] = (s32)lua_tointeger(lua, -1);
3768
3769 lua_pop(lua, 1);
3770 }
3771
3772 lua_close(lua);
3773 }
3774
3775 if((version.major > TIC_VERSION_MAJOR) ||
3776 (version.major == TIC_VERSION_MAJOR && version.minor > TIC_VERSION_MINOR) ||
3777 (version.major == TIC_VERSION_MAJOR && version.minor == TIC_VERSION_MINOR && version.patch > TIC_VERSION_REVISION))
3778 {
3779 char msg[TICNAME_MAX];
3780 sprintf(msg, " new version %i.%i.%i available", version.major, version.minor, version.patch);
3781
3782 enum{Offset = (2 * STUDIO_TEXT_BUFFER_WIDTH)};
3783
3784 memset(console->text + Offset, ' ', STUDIO_TEXT_BUFFER_WIDTH);
3785 strcpy(console->text + Offset, msg);
3786 memset(console->color + Offset, tic_color_red, strlen(msg));
3787 }
3788 }
3789 break;
3790 default:
3791 break;
3792 }
3793}
3794
3795#endif
3796
3797static char* getSelectionText(Console* console)
3798{
3799 const char* start = console->select.start;
3800 const char* end = console->select.end;
3801
3802 if (start > end)
3803 SWAP(start, end, const char*);
3804
3805 s32 size = end - start;
3806 if (size)
3807 {
3808 size += size / CONSOLE_BUFFER_WIDTH + 1;
3809 char* clipboard = malloc(size);
3810 memset(clipboard, 0, size);
3811 char* dst = clipboard;
3812
3813 s32 index = (start - console->text) % CONSOLE_BUFFER_WIDTH;
3814
3815 for (const char* ptr = start; ptr < end; ptr++, index++)
3816 {
3817 if (index && (index % CONSOLE_BUFFER_WIDTH) == 0)
3818 *dst++ = '\n';
3819
3820 if (*ptr)
3821 *dst++ = *ptr;
3822 }
3823
3824 return clipboard;
3825 }
3826
3827 return NULL;
3828}
3829
3830static void copyToClipboard(Console* console)
3831{
3832 char* text = getSelectionText(console);
3833
3834 if (text)
3835 {
3836 tic_sys_clipboard_set(text);
3837 free(text);
3838 clearSelection(console);
3839 }
3840}
3841
3842static void copyFromClipboard(Console* console)
3843{
3844 if(tic_sys_clipboard_has())
3845 {
3846 const char* clipboard = tic_sys_clipboard_get();
3847
3848 if(clipboard)
3849 {
3850 char* text = strdup(clipboard);
3851
3852 char* dst = text;
3853 for(const char* src = clipboard; *src; src++)
3854 if(isprint(*src))
3855 *dst++ = *src;
3856
3857 insertInputText(console, text);
3858 free(text);
3859
3860 tic_sys_clipboard_free(clipboard);
3861 }
3862 }
3863}
3864
3865static void processMouse(Console* console)
3866{
3867 tic_mem* tic = console->tic;
3868 // process scroll
3869 {
3870 tic80_input* input = &console->tic->ram->input;
3871
3872 if(input->mouse.scrolly)
3873 {
3874 enum{Scroll = 3};
3875 s32 delta = input->mouse.scrolly > 0 ? -Scroll : Scroll;
3876 setScroll(console, console->scroll.pos + delta);
3877 }
3878 }
3879
3880 tic_rect rect = {0, 0, TIC80_WIDTH, TIC80_HEIGHT};
3881
3882 if(checkMousePos(console->studio, &rect))
3883 setCursor(console->studio, tic_cursor_ibeam);
3884
3885#if defined(__TIC_ANDROID__)
3886
3887 if(checkMouseDown(console->studio, &rect, tic_mouse_left))
3888 {
3889 setCursor(console->studio, tic_cursor_hand);
3890
3891 if(console->scroll.active)
3892 {
3893 setScroll(console, (console->scroll.start - tic_api_mouse(tic).y) / STUDIO_TEXT_HEIGHT);
3894 }
3895 else
3896 {
3897 console->scroll.active = true;
3898 console->scroll.start = tic_api_mouse(tic).y + console->scroll.pos * STUDIO_TEXT_HEIGHT;
3899 }
3900 }
3901 else console->scroll.active = false;
3902
3903#else
3904
3905 if(checkMouseDown(console->studio, &rect, tic_mouse_left))
3906 {
3907 tic_point m = tic_api_mouse(tic);
3908
3909 console->select.end = console->text
3910 + m.x / STUDIO_TEXT_WIDTH
3911 + (m.y / STUDIO_TEXT_HEIGHT + console->scroll.pos) * CONSOLE_BUFFER_WIDTH;
3912
3913 if(!console->select.active)
3914 {
3915 console->select.active = true;
3916 console->select.start = console->select.end;
3917 }
3918 }
3919 else console->select.active = false;
3920
3921#endif
3922
3923 if(checkMouseClick(console->studio, &rect, tic_mouse_middle))
3924 {
3925 char* text = getSelectionText(console);
3926
3927 if (text)
3928 {
3929 insertInputText(console, text);
3930 tic_sys_clipboard_set(text);
3931 free(text);
3932 }
3933 else
3934 copyFromClipboard(console);
3935 }
3936}
3937
3938static void processConsolePgUp(Console* console)
3939{
3940 setScroll(console, console->scroll.pos - STUDIO_TEXT_BUFFER_HEIGHT/2);
3941}
3942
3943static void processConsolePgDown(Console* console)
3944{
3945 setScroll(console, console->scroll.pos + STUDIO_TEXT_BUFFER_HEIGHT/2);
3946}
3947
3948static inline bool isalnum_(char c) {return isalnum(c) || c == '_';}
3949
3950static s32 leftWordPos(Console* console)
3951{
3952 const char* start = console->input.text;
3953 const char* pos = console->input.text + console->input.pos - 1;
3954
3955 if(pos > start)
3956 {
3957 if(isalnum_(*pos)) while(pos > start && isalnum_(*(pos-1))) pos--;
3958 else while(pos > start && !isalnum_(*(pos-1))) pos--;
3959 return pos - console->input.text;
3960 }
3961
3962 return console->input.pos;
3963}
3964
3965static s32 rightWordPos(Console* console)
3966{
3967 const char* end = console->input.text + strlen(console->input.text);
3968 const char* pos = console->input.text + console->input.pos;
3969
3970 if(pos < end)
3971 {
3972 if(isalnum_(*pos)) while(pos < end && isalnum_(*pos)) pos++;
3973 else while(pos < end && !isalnum_(*pos)) pos++;
3974 return pos - console->input.text;
3975 }
3976
3977 return console->input.pos;
3978}
3979
3980static void leftWord(Console* console)
3981{
3982 console->input.pos = leftWordPos(console);
3983}
3984
3985static void rightWord(Console* console)
3986{
3987 console->input.pos = rightWordPos(console);
3988}
3989
3990static void deleteWord(Console* console)
3991{
3992 s32 pos = rightWordPos(console);
3993 deleteText(console, console->input.pos, pos);
3994}
3995
3996static void backspaceWord(Console* console)
3997{
3998 s32 pos = leftWordPos(console);
3999 deleteText(console, pos, console->input.pos);
4000 console->input.pos = pos;
4001}
4002
4003static void processKeyboard(Console* console)
4004{
4005 tic_mem* tic = console->tic;
4006
4007 if(!console->active)
4008 return;
4009
4010 if(tic->ram->input.keyboard.data != 0)
4011 {
4012 switch(getClipboardEvent(console->studio))
4013 {
4014 case TIC_CLIPBOARD_COPY: copyToClipboard(console); break;
4015 case TIC_CLIPBOARD_PASTE: copyFromClipboard(console); break;
4016 default: break;
4017 }
4018
4019 console->cursor.delay = CONSOLE_CURSOR_DELAY;
4020
4021 bool ctrl = tic_api_key(tic, tic_key_ctrl);
4022 bool alt = tic_api_key(tic, tic_key_alt);
4023
4024 if (ctrl || alt)
4025 {
4026 if (ctrl)
4027 {
4028#if defined(__TIC_LINUX__)
4029 tic_keycode clearKey = tic_key_l;
4030#else
4031 tic_keycode clearKey = tic_key_k;
4032#endif
4033
4034 if (keyWasPressed(console->studio, tic_key_a)) processConsoleHome(console);
4035 else if (keyWasPressed(console->studio, tic_key_e)) processConsoleEnd(console);
4036 else if (keyWasPressed(console->studio, clearKey))
4037 {
4038 onClsCommand(console);
4039 return;
4040 }
4041 }
4042
4043 if (keyWasPressed(console->studio, tic_key_left)) leftWord(console);
4044 else if(keyWasPressed(console->studio, tic_key_right)) rightWord(console);
4045 else if(keyWasPressed(console->studio, tic_key_delete)) deleteWord(console);
4046 else if(keyWasPressed(console->studio, tic_key_backspace)) backspaceWord(console);
4047 }
4048 else
4049 {
4050 if(keyWasPressed(console->studio, tic_key_up)) onHistoryUp(console);
4051 else if(keyWasPressed(console->studio, tic_key_down)) onHistoryDown(console);
4052 else if(keyWasPressed(console->studio, tic_key_left))
4053 {
4054 if(console->input.pos > 0)
4055 console->input.pos--;
4056 }
4057 else if(keyWasPressed(console->studio, tic_key_right))
4058 {
4059 console->input.pos++;
4060 size_t len = strlen(console->input.text);
4061 if(console->input.pos > len)
4062 console->input.pos = len;
4063 }
4064 else if(keyWasPressed(console->studio, tic_key_return)) processConsoleCommand(console);
4065 else if(keyWasPressed(console->studio, tic_key_backspace)) processConsoleBackspace(console);
4066 else if(keyWasPressed(console->studio, tic_key_delete)) processConsoleDel(console);
4067 else if(keyWasPressed(console->studio, tic_key_home)) processConsoleHome(console);
4068 else if(keyWasPressed(console->studio, tic_key_end)) processConsoleEnd(console);
4069 else if(keyWasPressed(console->studio, tic_key_tab)) processConsoleTab(console);
4070 else if(keyWasPressed(console->studio, tic_key_pageup)) processConsolePgUp(console);
4071 else if(keyWasPressed(console->studio, tic_key_pagedown)) processConsolePgDown(console);
4072 }
4073 }
4074
4075 char sym = getKeyboardText(console->studio);
4076
4077 if(sym)
4078 {
4079 insertInputText(console, (char[]){sym, '\0'});
4080 scrollConsole(console);
4081
4082 console->cursor.delay = CONSOLE_CURSOR_DELAY;
4083 }
4084
4085}
4086
4087static void tick(Console* console)
4088{
4089 tic_mem* tic = console->tic;
4090
4091 processMouse(console);
4092 processKeyboard(console);
4093
4094 Start* start = getStartScreen(console->studio);
4095
4096 if(console->tickCounter == 0)
4097 {
4098 if(!start->embed)
4099 {
4100 loadDemo(console, 0);
4101
4102 if(!console->args.cli)
4103 {
4104 printBack(console, "\n hello! type ");
4105 printFront(console, "help");
4106 printBack(console, " for help\n");
4107
4108#if defined (TIC_BUILD_WITH_LUA)
4109 if(getConfig(console->studio)->checkNewVersion)
4110 tic_net_get(console->net, "/api?fn=version", onHttpVesrsionGet, console);
4111#endif
4112 }
4113
4114 commandDone(console);
4115 }
4116 else printBack(console, "\n loading cart...");
4117 }
4118
4119 if (getStudioMode(console->studio) != TIC_CONSOLE_MODE) return;
4120
4121 tic_api_cls(tic, TIC_COLOR_BG);
4122 drawConsoleText(console);
4123
4124 if(start->embed)
4125 {
4126 if(console->tickCounter >= (u32)(console->args.skip ? 1 : TIC80_FRAMERATE))
4127 {
4128 runGame(console->studio);
4129
4130 start->embed = false;
4131 studioRomLoaded(console->studio);
4132
4133 printLine(console);
4134 commandDone(console);
4135 console->active = true;
4136
4137 return;
4138 }
4139 }
4140 else
4141 {
4142 if(console->cursor.delay)
4143 console->cursor.delay--;
4144
4145 drawCursor(console);
4146
4147 if(console->active)
4148 {
4149 if(console->commands.current < console->commands.count)
4150 {
4151 const char* command = console->commands.items[console->commands.current];
4152 if(!console->args.cli)
4153 printFront(console, command);
4154
4155 processCommand(console, command);
4156
4157 console->commands.current++;
4158 }
4159 else if(getConfig(console->studio)->cli)
4160 exitStudio(console->studio);
4161 }
4162 }
4163
4164 console->tickCounter++;
4165}
4166
4167static inline bool isslash(char c)
4168{
4169 return c == '/' || c == '\\';
4170}
4171
4172static bool cmdLoadCart(Console* console, const char* path)
4173{
4174 bool done = false;
4175
4176 s32 size = 0;
4177 void* data = fs_read(path, &size);
4178
4179 if(data)
4180 {
4181 const char* cartName = NULL;
4182
4183 {
4184 const char* ptr = path + strlen(path);
4185 while(ptr > path && !isslash(*ptr))--ptr;
4186 cartName = ptr + isslash(*ptr);
4187 }
4188
4189 setCartName(console, cartName, path);
4190 tic_mem* tic = console->tic;
4191
4192 if(tic_tool_has_ext(cartName, PngExt))
4193 {
4194 tic_cartridge* cart = loadPngCart((png_buffer){data, size});
4195
4196 if(cart)
4197 {
4198 memcpy(&tic->cart, cart, sizeof(tic_cartridge));
4199 free(cart);
4200 done = true;
4201 }
4202 }
4203 else if(tic_tool_has_ext(cartName, CART_EXT))
4204 {
4205 tic_cart_load(&tic->cart, data, size);
4206 done = true;
4207 }
4208#if defined(TIC80_PRO)
4209 else if(tic_project_ext(cartName))
4210 {
4211 if(tic_project_load(cartName, data, size, &tic->cart))
4212 done = true;
4213 }
4214#endif
4215
4216 free(data);
4217 }
4218
4219 if(done)
4220 studioRomLoaded(console->studio);
4221
4222 return done;
4223}
4224
4225static s32 cmdcmp(const void* a, const void* b)
4226{
4227 return strcmp(((const Command*)a)->name, ((const Command*)b)->name);
4228}
4229
4230static s32 apicmp(const void* a, const void* b)
4231{
4232 return strcmp(((const ApiItem*)a)->name, ((const ApiItem*)b)->name);
4233}
4234
4235void initConsole(Console* console, Studio* studio, tic_fs* fs, tic_net* net, Config* config, StartArgs args)
4236{
4237 if(!console->text) console->text = malloc(CONSOLE_BUFFER_SIZE);
4238 if(!console->color) console->color = malloc(CONSOLE_BUFFER_SIZE);
4239 if(!console->desc) console->desc = malloc(sizeof(CommandDesc));
4240
4241 *console = (Console)
4242 {
4243 .studio = studio,
4244 .tic = getMemory(studio),
4245 .config = config,
4246 .loadByHash = loadByHash,
4247 .load = loadExternal,
4248 .loadCart = cmdLoadCart,
4249 .updateProject = updateProject,
4250 .error = error,
4251 .trace = trace,
4252 .tick = tick,
4253 .save = saveCart,
4254 .done = commandDone,
4255 .cursor = {.pos.x = 1, .pos.y = 3, .delay = 0},
4256 .input = console->text,
4257 .tickCounter = 0,
4258 .active = false,
4259 .text = console->text,
4260 .color = console->color,
4261 .fs = fs,
4262 .net = net,
4263 .args = args,
4264 .desc = console->desc,
4265 };
4266
4267 // parse --cmd param
4268 {
4269 char* command = args.cmd;
4270 while(command)
4271 {
4272 console->commands.items = realloc(console->commands.items, sizeof(char*) * (console->commands.count + 1));
4273 console->commands.items[console->commands.count++] = command;
4274
4275 static const char Sep[] = " & ";
4276 command = strstr(command, Sep);
4277
4278 if(command)
4279 {
4280 *command = '\0';
4281 command += STRLEN(Sep);
4282 }
4283 }
4284 }
4285
4286 qsort(Commands, COUNT_OF(Commands), sizeof Commands[0], cmdcmp);
4287 qsort(Api, COUNT_OF(Api), sizeof Api[0], apicmp);
4288
4289 memset(console->text, 0, CONSOLE_BUFFER_SIZE);
4290 memset(console->color, TIC_COLOR_BG, CONSOLE_BUFFER_SIZE);
4291 memset(console->desc, 0, sizeof(CommandDesc));
4292
4293 Start* start = getStartScreen(console->studio);
4294
4295 if(!console->args.cli)
4296 {
4297 memcpy(console->text, start->text, STUDIO_TEXT_BUFFER_SIZE);
4298 memcpy(console->color, start->color, STUDIO_TEXT_BUFFER_SIZE);
4299
4300 printLine(console);
4301 for(const char* ptr = console->text, *end = ptr + STUDIO_TEXT_BUFFER_SIZE;
4302 ptr < end; ptr += CONSOLE_BUFFER_WIDTH)
4303 if(*ptr)
4304 puts(ptr);
4305 }
4306
4307 if (args.cart)
4308 if (!cmdLoadCart(console, args.cart))
4309 {
4310 printf("error: cart `%s` not loaded\n", args.cart);
4311 exit(1);
4312 }
4313 else
4314 getStartScreen(console->studio)->embed = true;
4315
4316 console->active = !start->embed;
4317}
4318
4319void freeConsole(Console* console)
4320{
4321 free(console->text);
4322 free(console->color);
4323
4324 if(console->history.items)
4325 {
4326 for(char **ptr = console->history.items, **end = ptr + console->history.size; ptr < end; ptr++)
4327 free(*ptr);
4328
4329 free(console->history.items);
4330 }
4331
4332 FREE(console->commands.items);
4333 free(console->desc);
4334 free(console);
4335}
4336