NXEngine v1.0.0.2
[NXEngine.git] / tsc.cpp
blob4b252362d928b3e1ef45a2ed3941eb47ba01643e
2 // TSC script parser & executor
4 #include "nx.h"
5 #include "common/DBuffer.h"
6 #include "vararray.h"
7 #include "tsc.h"
8 #include "tsc.fdh"
10 #define TRACE_SCRIPT
12 // which textbox options are enabled by the "<TUR" script command.
13 #define TUR_PARAMS (TB_LINE_AT_ONCE | TB_VARIABLE_WIDTH_CHARS | TB_CURSOR_NEVER_SHOWN)
15 static ScriptInstance curscript;
16 static int lastammoinc = 0;
18 struct ScriptPage
20 // a variable-length array of pointers to compiled script code
21 // for each script in the page; their indexes in this array
22 // correspond to their script numbers.
23 VarArray<DBuffer *> scripts;
25 void Clear()
27 for(int i=0;i<scripts.nitems;i++)
28 delete scripts.get(i); // it's safe to delete NULL, so no check here
30 scripts.MakeEmpty();
34 ScriptPage script_pages[NUM_SCRIPT_PAGES];
37 void c------------------------------() {}
40 struct TSCCommandTable
42 const char *mnemonic;
43 int nparams;
45 #include "tsc_cmdtbl.cpp"
47 unsigned char codealphabet[] = { "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123+-" };
48 unsigned char letter_to_code[256];
49 unsigned char mnemonic_lookup[32*32*32];
52 static void GenLTC(void)
54 int i;
55 uchar ch;
57 memset(letter_to_code, 0xff, sizeof(letter_to_code));
58 for(i=0;;i++)
60 if (!(ch = codealphabet[i])) break;
61 letter_to_code[ch] = i;
64 memset(mnemonic_lookup, 0xff, sizeof(mnemonic_lookup));
65 for(i=0;i<OP_COUNT;i++)
67 mnemonic_lookup[MnemonicToIndex(cmd_table[i].mnemonic)] = i;
71 static int MnemonicToIndex(const char *str)
73 int l1, l2, l3;
75 l1 = letter_to_code[str[0]];
76 l2 = letter_to_code[str[1]];
77 l3 = letter_to_code[str[2]];
78 if (l1==0xff || l2==0xff || l3==0xff) return -1;
80 return (l1 << 10) | (l2 << 5) | l3;
83 static int MnemonicToOpcode(char *str)
85 int index = MnemonicToIndex(str);
86 if (index != -1)
88 index = mnemonic_lookup[index];
89 if (index != 0xff) return index;
92 staterr("MnemonicToOpcode: No such command '%s'", str);
93 return -1;
97 void c------------------------------() {}
100 bool tsc_init(void)
102 char fname[MAXPATHLEN];
104 GenLTC();
105 curscript.running = false;
106 for(int i=0;i<NUM_SCRIPT_PAGES;i++)
108 // load the "common" TSC scripts available to all maps
109 sprintf(fname, "%s/Head.tsc", data_dir);
110 if (tsc_load(fname, SP_HEAD)) return 1;
112 // load the inventory screen scripts
113 sprintf(fname, "%s/ArmsItem.tsc", data_dir);
114 if (tsc_load(fname, SP_ARMSITEM)) return 1;
116 // load stage select/teleporter scripts
117 sprintf(fname, "%s/StageSelect.tsc", data_dir);
118 if (tsc_load(fname, SP_STAGESELECT)) return 1;
120 return 0;
123 void tsc_close(void)
125 // free all loaded scripts
126 for(int i=0;i<NUM_SCRIPT_PAGES;i++)
127 script_pages[i].Clear();
130 // load a tsc file and return the highest script # in the file
131 bool tsc_load(const char *fname, int pageno)
133 ScriptPage *page = &script_pages[pageno];
134 int fsize;
135 char *buf;
136 bool result;
138 stat("tsc_load: loading '%s' to page %d", fname, pageno);
139 if (curscript.running && curscript.pageno == pageno)
140 StopScript(&curscript);
142 page->Clear();
144 // load the raw script text
145 buf = tsc_decrypt(fname, &fsize);
146 if (!buf)
148 staterr("tsc_load: failed to load file: '%s'", fname);
149 return 1;
152 // now "compile" all the scripts in the TSC
153 //int top_script = CompileScripts(buf, fsize, base);
154 result = tsc_compile(buf, fsize, pageno);
155 free(buf);
157 return result;
161 char *tsc_decrypt(const char *fname, int *fsize_out)
163 FILE *fp;
164 int fsize, i;
166 fp = fopen(fname, "rb");
167 if (!fp)
169 staterr("tsc_decrypt: no such file: '%s'!", fname);
170 return NULL;
173 fseek(fp, 0, SEEK_END);
174 fsize = ftell(fp);
175 fseek(fp, 0, SEEK_SET);
177 // load file
178 char *buf = (char *)malloc(fsize+1);
179 fread(buf, fsize, 1, fp);
180 buf[fsize] = 0;
181 fclose(fp);
183 // get decryption key, which is actually part of the text
184 int keypos = (fsize / 2);
185 int key = buf[keypos];
187 // everything EXCEPT the key is encrypted
188 for(i=0;i<keypos;i++) { buf[i] = (buf[i] - key); }
189 for(i++;i<fsize;i++) { buf[i] = (buf[i] - key); }
191 if (fsize_out) *fsize_out = fsize;
192 return buf;
196 void c------------------------------() {}
199 // compile a tsc file--a set of scripts in raw text format--into 'bytecode',
200 // and place the finished scripts into the given page.
201 bool tsc_compile(const char *buf, int bufsize, int pageno)
203 ScriptPage *page = &script_pages[pageno];
204 const char *buf_end = (buf + (bufsize - 1));
205 DBuffer *script = NULL;
206 char cmdbuf[4] = { 0 };
208 while(buf <= buf_end)
210 char ch = *(buf++);
212 if (ch == '#')
213 { // start of a scriptzz
214 if (script)
216 script->Append8(OP_END);
217 script = NULL;
220 int scriptno = ReadNumber(&buf, buf_end);
221 if (scriptno >= 10000 || scriptno < 0)
223 staterr("tsc_compile: invalid script number: %d", scriptno);
224 return 1;
227 // skip the CR after the script #
228 while(buf < buf_end)
230 if (*buf != '\r' && *buf != '\n') break;
231 buf++;
234 //stat("Parsing script #%04d", scriptno);
235 if (page->scripts.get(scriptno))
237 staterr("tsc_compile WARNING: duplicate script #%04d; ignoring", scriptno);
238 // because script is left null, we'll ignore everything until we see another #
240 else
242 script = new DBuffer;
243 page->scripts.put(scriptno, script);
246 else if (ch == '<' && script)
248 // read the command type
249 cmdbuf[0] = nextchar(&buf, buf_end);
250 cmdbuf[1] = nextchar(&buf, buf_end);
251 cmdbuf[2] = nextchar(&buf, buf_end);
253 int cmd = MnemonicToOpcode(cmdbuf);
254 if (cmd == -1) return 1;
256 //stat("Command '%s', parameters %d", cmdbuf, cmd_table[cmd].nparams);
257 script->Append8(cmd);
259 // read all parameters expected by that command
260 int nparams = cmd_table[cmd].nparams;
261 for(int i=0;i<nparams;i++)
263 int val = ReadNumber(&buf, buf_end);
265 script->Append8(val >> 8);
266 script->Append8(val & 0xff);
268 // colon between params
269 if (i < (nparams - 1))
270 buf++;
273 else if (script)
274 { // text for message boxes
276 buf--;
277 script->Append8(OP_TEXT);
278 ReadText(script, &buf, buf_end);
283 if (script)
284 script->Append8(OP_END);
286 return 0;
289 static char nextchar(const char **buf, const char *buf_end)
291 if (*buf <= buf_end)
292 return *(*buf)++;
294 return 0;
297 static int ReadNumber(const char **buf, const char *buf_end)
299 static char num[5] = { 0 };
300 int i = 0;
302 while(i < 4)
304 num[i] = nextchar(buf, buf_end);
305 if (!isdigit(num[i]))
307 (*buf)--;
308 break;
311 i++;
314 return atoi(num);
317 static void ReadText(DBuffer *script, const char **buf, const char *buf_end)
319 while(*buf <= buf_end)
321 char ch = nextchar(buf, buf_end);
322 if (ch == '<' || ch == '#')
324 (*buf)--;
325 break;
328 if (ch != 10)
329 script->Append8(ch);
332 script->Append8('\0');
336 void c------------------------------() {}
339 void RunScripts(void)
341 if (curscript.running)
342 ExecScript(&curscript);
345 void StopScripts(void)
347 if (curscript.running)
348 StopScript(&curscript);
351 int GetCurrentScript(void)
353 if (curscript.running)
354 return curscript.scriptno;
356 return -1;
359 ScriptInstance *GetCurrentScriptInstance()
361 if (curscript.running)
362 return &curscript;
364 return NULL;
368 void c------------------------------() {}
371 // returns a pointer to the executable data/bytecode of the given script.
372 // handles looking on head, etc.
373 const uint8_t *FindScriptData(int scriptno, int pageno, int *page_out)
375 ScriptPage *page = &script_pages[pageno];
376 DBuffer *script;
378 script = page->scripts.get(scriptno);
379 if (!script)
381 if (pageno != SP_HEAD)
382 { // try to find the script in head.tsc
383 return FindScriptData(scriptno, SP_HEAD, page_out);
385 else
387 return NULL;
391 if (page_out) *page_out = pageno;
392 return script->Data();
396 ScriptInstance *StartScript(int scriptno, int pageno)
398 const uint8_t *program;
399 int found_pageno;
401 program = FindScriptData(scriptno, pageno, &found_pageno);
402 if (!program)
404 staterr("StartScript: no script at position #%04d page %d!", scriptno, pageno);
405 return NULL;
408 // don't start regular map scripts (e.g. hvtrigger) if player is dead
409 if (player->dead && found_pageno != SP_HEAD)
411 stat("Not starting script %d; player is dead", scriptno);
412 return NULL;
415 // set the script
416 memset(&curscript, 0, sizeof(ScriptInstance));
418 curscript.program = program;
419 curscript.scriptno = scriptno;
420 curscript.pageno = found_pageno;
422 curscript.ynj_jump = -1;
423 curscript.running = true;
425 textbox.ResetState();
426 stat(" - Started script %04d", scriptno);
428 RunScripts();
429 return &curscript;
432 void StopScript(ScriptInstance *s)
434 if (!s->running)
435 return;
437 s->running = false;
438 stat(" - Stopped script %04d", s->scriptno);
440 // TRA is really supposed to be a jump, not a script restart--
441 // in that in maintains KEY/PRI across the stage transition.
442 // Emulate this by leaving the script state alone until the
443 // on-entry script starts.
444 player->inputs_locked = false;
445 game.frozen = false;
446 player->lookaway = false;
448 textbox.ResetState();
452 void c------------------------------() {}
455 bool JumpScript(int newscriptno, int pageno)
457 ScriptInstance *s = &curscript;
459 if (pageno == -1)
460 pageno = s->pageno;
462 stat("JumpScript: moving to script #%04d page %d", newscriptno, pageno);
464 s->program = FindScriptData(newscriptno, pageno, &s->pageno);
465 s->scriptno = newscriptno;
466 s->ip = 0;
468 if (!s->program)
470 staterr("JumpScript: missing script #%04d! Script terminated.", newscriptno);
471 StopScript(s);
472 return 1;
475 s->delaytimer = 0;
476 s->waitforkey = false;
477 s->wait_standing = false;
479 // <EVE doesn't clear textbox mode or the face etc
480 if (textbox.IsVisible())
482 textbox.ClearText();
484 // see entrance to Sacred Grounds when you have the Nikumaru Counter
485 // to witness that EVE clears TUR.
486 textbox.SetFlags(TB_LINE_AT_ONCE, false);
487 textbox.SetFlags(TB_VARIABLE_WIDTH_CHARS, false);
488 textbox.SetFlags(TB_CURSOR_NEVER_SHOWN, false);
491 return 0;
494 void ExecScript(ScriptInstance *s)
496 int cmd;
497 int val, parm[6];
498 int i;
499 Object *o;
500 char *mnemonic;
501 char *str;
502 int cmdip;
503 char debugbuffer[100];
505 #define JUMP_IF(cond) \
507 if (cond) \
509 if (JumpScript(parm[1])) return; \
513 // pause script while FAI/FAO still working
514 if (fade.getstate() == FS_FADING) return;
515 if (game.mode == GM_ISLAND) return;
517 // waiting for an answer from a Yes/No prompt?
518 if (s->ynj_jump != -1)
520 if (textbox.YesNoPrompt.ResultReady())
522 if (textbox.YesNoPrompt.GetResult() == NO)
523 JumpScript(s->ynj_jump);
525 textbox.YesNoPrompt.SetVisible(false);
526 s->ynj_jump = -1;
528 else
529 { // pause script until answer is receieved
530 return;
534 // pause script while text is still displaying
535 if (textbox.IsBusy()) return;
537 // pause while NOD is in effect
538 if (s->waitforkey)
540 if (s->nod_delay) // used to pause during <QUA without freezing textboxes in Hell
542 s->nod_delay--;
544 else if (buttondown())
546 if (!s->keysdown) // release nod
548 // hide the fact that the key was just pushed
549 // so player doesn't jump/fire stupidly when dismissing textboxes
550 lastinputs[JUMPKEY] = true;
551 lastinputs[FIREKEY] = true;
552 lastpinputs[JUMPKEY] = true;
553 lastpinputs[FIREKEY] = true;
555 s->waitforkey = false;
556 textbox.ShowCursor(false);
559 else
561 s->keysdown = false;
564 if (s->waitforkey) return;
567 // pause scripts while WAI is in effect.
568 // <WAI9999, used in inventory/stage-select screen, means forever.
569 if (s->delaytimer)
571 if (s->delaytimer == 9999)
573 UnlockInventoryInput();
575 else
577 s->delaytimer--;
580 return;
583 // pause while WAS (wait until standing) is in effect.
584 if (s->wait_standing)
586 if (!player->blockd) return;
587 s->wait_standing = false;
590 // main execution loop
591 for(;;)
593 cmdip = s->ip++;
594 cmd = s->program[cmdip];
595 mnemonic = (char *)cmd_table[cmd].mnemonic;
597 if (cmd != OP_TEXT)
599 sprintf(debugbuffer, "%04x <%s ", cmd, mnemonic);
600 for(i=0;i<cmd_table[cmd].nparams;i++)
602 val = ((int)s->program[s->ip++]) << 8;
603 val |= s->program[s->ip++];
604 parm[i] = val;
605 sprintf(debugbuffer, "%s %04d", debugbuffer, val);
608 #ifdef TRACE_SCRIPT
609 else
611 char debugbuffer2[10000];
612 crtoslashn((char *)&s->program[s->ip], debugbuffer2);
613 sprintf(debugbuffer, "TEXT '%s'", debugbuffer2);
616 if (cmd == OP_TEXT && !textbox.IsVisible() && !strcmp(debugbuffer, "TEXT '\n'")) { }
617 else
619 stat("%04d:%d %s", s->scriptno, cmdip, debugbuffer);
621 #endif
623 switch(cmd)
625 case OP_END: StopScript(s); return;
627 case OP_FAI: fade.Start(FADE_IN, parm[0], SPR_FADE_DIAMOND); return;
628 case OP_FAO: fade.Start(FADE_OUT, parm[0], SPR_FADE_DIAMOND); return;
629 case OP_FLA: flashscreen.Start(); break;
631 case OP_SOU: sound(parm[0]); break;
632 case OP_CMU: music(parm[0]); break;
633 case OP_RMU: music(music_lastsong()); break;
634 case OP_FMU: org_fade(); break;
636 case OP_SSS: StartStreamSound(parm[0]); break;
637 case OP_SPS: StartPropSound(); break;
639 case OP_CSS: // these seem identical-- either one will
640 case OP_CPS: // in fact stop the other.
642 StopLoopSounds();
644 break;
646 // free menu selector in Inventory. It also undoes <PRI,
647 // as can be seen at the entrance to Sacred Ground.
648 case OP_FRE:
650 game.frozen = false;
651 player->inputs_locked = false;
652 UnlockInventoryInput();
654 break;
656 case OP_PRI: // freeze entire game (players + NPCs)
658 game.frozen = true;
659 player->inputs_locked = false;
660 statusbar.xpflashcount = 0; // looks odd if this happens after a long <PRI, even though it's technically correct
662 break;
664 case OP_KEY: // lock players input but NPC/objects still run
666 game.frozen = false;
667 player->inputs_locked = true;
669 break;
671 case OP_MOV:
672 player->x = (parm[0] * TILE_W) << CSF;
673 player->y = (parm[1] * TILE_H) << CSF;
674 player->xinertia = player->yinertia = 0;
675 player->lookaway = false;
676 break;
678 case OP_UNI:
679 player->movementmode = parm[0];
680 map_scroll_lock(parm[0]); // locks on anything other than 0
681 break;
683 case OP_MNA: // show map name (as used on entry)
684 map_show_map_name();
685 break;
687 case OP_MLP: // bring up Map System
688 game.setmode(GM_MAP_SYSTEM, game.mode);
689 break;
691 case OP_TRA:
693 bool waslocked = (player->inputs_locked || game.frozen);
695 stat("******* Executing <TRA to stage %d", parm[0]);
696 game.switchstage.mapno = parm[0];
697 game.switchstage.eventonentry = parm[1];
698 game.switchstage.playerx = parm[2];
699 game.switchstage.playery = parm[3];
700 StopScript(s);
702 if (game.switchstage.mapno != 0)
704 // KEY is maintained across TRA as if the TRA
705 // were a jump instead of a restart; but if the
706 // game is in PRI then it is downgraded to a KEY.
707 // See entrance to Yamashita Farm.
708 if (waslocked)
710 player->inputs_locked = true;
711 game.frozen = false;
715 return;
717 break;
719 case OP_AMPLUS: GetWeapon(parm[0], parm[1]); lastammoinc = parm[1]; break;
720 case OP_AMMINUS: LoseWeapon(parm[0]); break;
721 case OP_TAM: TradeWeapon(parm[0], parm[1], parm[2]); break;
722 case OP_AMJ: JUMP_IF(player->weapons[parm[0]].hasWeapon); break;
724 case OP_ZAM: // drop all weapons to level 1
726 for(int i=0;i<WPN_COUNT;i++)
728 player->weapons[i].xp = 0;
729 player->weapons[i].level = 0;
732 break;
734 case OP_EVE: JumpScript(parm[0]); break; // unconditional jump to event
736 case OP_FLPLUS: game.flags[parm[0]] = 1; break;
737 case OP_FLMINUS: game.flags[parm[0]] = 0; break;
738 case OP_FLJ: JUMP_IF(game.flags[parm[0]]); break;
740 case OP_ITPLUS: AddInventory(parm[0]); break;
741 case OP_ITMINUS: DelInventory(parm[0]); break;
742 case OP_ITJ: JUMP_IF((FindInventory(parm[0]) != -1)); break;
744 // the PSelectSprite is a hack so when the Mimiga Mask is taken
745 // it disappears immediately even though the game is in <PRI.
746 case OP_EQPLUS: player->equipmask |= parm[0]; PSelectSprite(); break;
747 case OP_EQMINUS: player->equipmask &= ~parm[0]; PSelectSprite(); break;
749 case OP_SKPLUS: game.skipflags[parm[0]] = 1; break;
750 case OP_SKMINUS: game.skipflags[parm[0]] = 0; break;
751 case OP_SKJ: JUMP_IF(game.skipflags[parm[0]]); break;
753 case OP_PSPLUS: textbox.StageSelect.SetSlot(parm[0], parm[1]); break;
755 case OP_NCJ:
756 JUMP_IF(CountObjectsOfType(parm[0]) > 0);
757 break;
759 case OP_ECJ: // unused but valid
760 JUMP_IF(FindObjectByID2(parm[0]));
761 break;
763 // life capsule--add to max life
764 case OP_MLPLUS:
765 player->maxHealth += parm[0];
766 player->hp = player->maxHealth;
767 break;
769 case OP_FON: // focus on NPC
771 if ((o = FindObjectByID2(parm[0])))
773 map_focus(o, parm[1]);
776 break;
777 case OP_FOB: // focus on boss
779 if (game.stageboss.object)
780 map_focus(game.stageboss.object, parm[1]);
781 else
782 staterr("tsc: <FOB without stage boss");
784 break;
785 case OP_FOM: // focus back to player (mychar)
787 map_focus(NULL, parm[0]);
789 break;
791 case OP_DNA: // delete all objects of type parm1
793 Object *o = firstobject;
794 while(o)
796 if (o->type == parm[0]) o->Delete();
797 o = o->next;
800 break;
802 case OP_ANP: NPCDo(parm[0], parm[1], parm[2], DoANP); break;
803 case OP_CNP: NPCDo(parm[0], parm[1], parm[2], DoCNP); break;
804 case OP_DNP: NPCDo(parm[0], parm[1], parm[2], DoDNP); break;
806 case OP_MNP: // move object X to (Y,Z) with direction W
807 if ((o = FindObjectByID2(parm[0])))
809 SetCSDir(o, parm[3]);
810 o->x = (parm[1] * TILE_W) << CSF;
811 o->y = (parm[2] * TILE_H) << CSF;
813 break;
815 case OP_BOA: // set boss state
817 game.stageboss.SetState(parm[0]);
819 break;
820 case OP_BSL: // bring up boss bar
822 Object *target;
823 if (parm[0] == 0)
824 { // <BSL0000 means the stage boss
825 target = game.stageboss.object;
826 if (!game.stageboss.object)
827 staterr("<BSL0000 but no stage boss present");
829 else
831 target = FindObjectByID2(parm[0]);
834 if (target)
836 game.bossbar.object = target;
837 game.bossbar.defeated = false;
838 game.bossbar.starting_hp = target->hp;
839 game.bossbar.bar.displayed_value = target->hp;
841 else
843 staterr("Target of <BSL not found");
846 break;
848 case OP_MM0: player->xinertia = 0; break;
849 case OP_MYD: SetPDir(parm[0]); break;
850 case OP_MYB:
852 player->lookaway = 0;
853 player->yinertia = -0x200;
854 int dir = parm[0];
856 if (dir >= 10) // bump away from the object in parm
858 o = FindObjectByID2(dir);
859 if (o)
861 if (player->CenterX() > o->CenterX())
862 dir = 0;
863 else
864 dir = 2;
868 if (dir == 0)
870 player->dir = LEFT;
871 player->xinertia = 0x200;
873 else if (dir == 2)
875 player->dir = RIGHT;
876 player->xinertia = -0x200;
879 break;
881 case OP_WAI: s->delaytimer = parm[0]; return;
882 case OP_WAS: s->wait_standing = true; return; // wait until player has blockd
884 case OP_SMP: map.tiles[parm[0]][parm[1]]--; break;
886 case OP_CMP: // change map tile at x:y to z and create smoke
888 int x = parm[0];
889 int y = parm[1];
890 map.tiles[x][y] = parm[2];
892 // get smoke coords
893 x = ((x * TILE_W) + (TILE_W / 2)) << CSF;
894 y = ((y * TILE_H) + (TILE_H / 2)) << CSF;
895 // when tiles are CMP'd during a PRI the smoke is not visible
896 // until the game is released, so I came up with this scheme
897 // to make that happen. See the "you see a button" destroyable
898 // box on the 2nd level of Maze M.
899 if (game.frozen)
901 o = CreateObject(x, y, OBJ_SMOKE_DROPPER);
902 o->timer2 = 4; // amount of smoke
904 else
906 SmokeXY(x, y, 4, TILE_W/2, TILE_H/2);
909 break;
911 case OP_QUA: s->nod_delay = game.quaketime = parm[0]; break;
913 case OP_LIPLUS: AddHealth(parm[0]); break;
914 case OP_AEPLUS: RefillAllAmmo(); break; // refills missiles
916 case OP_INI: game.switchstage.mapno = NEW_GAME; break; // restart game from beginning
917 case OP_STC: niku_save(game.counter); break;
919 case OP_SVP:
921 if (!settings->multisave)
923 if (!Replay::IsPlaying())
924 game_save(settings->last_save_slot);
926 else
928 textbox.SaveSelect.SetVisible(true, SS_SAVING);
929 s->delaytimer = 9999;
930 return;
933 break;
934 case OP_LDP:
935 game.switchstage.mapno = LOAD_GAME;
936 break;
938 case OP_HMC: player->hide = true; break;
939 case OP_SMC: player->hide = false; break;
941 // ---------------------------------------
943 case OP_MSG: // bring up text box
945 // required for post-Ballos cutscene
946 textbox.SetFlags(TUR_PARAMS, false);
947 textbox.SetVisible(true, TB_DEFAULTS);
949 break;
951 case OP_MS2: // bring up text box, at top, with no border
953 textbox.SetFace(0); // required for Undead Core intro
954 textbox.SetFlags(TUR_PARAMS, false);
955 textbox.SetVisible(true, TB_DRAW_AT_TOP | TB_NO_BORDER);
957 break;
959 case OP_MS3: // bring up text box, at top
961 textbox.SetFlags(TUR_PARAMS, false);
962 textbox.SetVisible(true, TB_DRAW_AT_TOP);
964 break;
966 case OP_CLO: // dismiss text box.
967 textbox.SetVisible(false);
968 textbox.ClearText();
969 // ...don't ResetState(), or it'll clear <FAC during Momorin dialog (Hideout)
970 break;
972 case OP_TEXT: // text to be displayed
974 str = (char *)&s->program[s->ip];
975 s->ip += (strlen(str) + 1);
977 textbox.AddText(str);
979 // must yield execution, because the message is busy now.
980 // however, if the message contains only CR's, then we don't yield,
981 // because CR's take no time to display.
982 if (contains_non_cr(str))
983 return;
985 break;
987 case OP_CLR: // erase all text in box
988 textbox.ClearText();
989 break;
991 case OP_FAC: // set and slide in given character face
992 textbox.SetFace(parm[0]);
993 break;
995 case OP_NOD: // pause till user presses key
997 if (textbox.IsVisible())
999 s->waitforkey = true; // pause exec till key pressed
1000 s->keysdown = true; // don't release immediately if key already down
1002 textbox.ShowCursor(true);
1005 return;
1007 case OP_YNJ: // prompt Yes or No and jump to given script if No
1009 textbox.YesNoPrompt.SetVisible(true);
1010 s->ynj_jump = parm[0];
1012 return;
1014 break;
1016 case OP_SAT: // disables typing animation
1017 case OP_CAT: // unused synonym
1019 textbox.SetFlags(TB_LINE_AT_ONCE | TB_CURSOR_NEVER_SHOWN, true);
1021 break;
1023 case OP_TUR: // set text mode to that used for signs
1025 textbox.SetFlags(TUR_PARAMS, true);
1026 textbox.SetCanSpeedUp(false);
1028 break;
1030 case OP_GIT: // show item graphic
1032 if (parm[0] != 0)
1034 int sprite, frame;
1036 if (parm[0] >= 1000)
1037 { // an item
1038 sprite = SPR_ITEMIMAGE;
1039 frame = (parm[0] - 1000);
1041 else
1042 { // a weapon
1043 sprite = SPR_ARMSICONS;
1044 frame = parm[0];
1047 textbox.ItemImage.SetSprite(sprite, frame);
1048 textbox.ItemImage.SetVisible(true);
1050 else
1052 textbox.ItemImage.SetVisible(false);
1055 break;
1057 case OP_NUM:
1058 { // seems to show the last value that was used with "AM+"
1059 char buf[16];
1060 sprintf(buf, "%d", lastammoinc);
1062 textbox.AddText(buf);
1064 break;
1066 case OP_SLP: // bring up teleporter menu
1068 textbox.StageSelect.SetVisible(true);
1069 return;
1071 break;
1073 case OP_ESC:
1075 StopScript(s);
1076 game.reset();
1078 break;
1080 // ---------------------------------------
1082 // trigger island-falling cinematic
1083 // if the parameter is 0, the island crashes (good ending);
1084 // if the parameter is 1, the island survives (best ending)
1085 case OP_XX1:
1087 game.setmode(GM_ISLAND, parm[0]);
1088 return;
1090 break;
1092 case OP_CRE:
1094 game.setmode(GM_CREDITS);
1095 return;
1097 break;
1099 case OP_SIL:
1100 credit_set_image(parm[0]);
1101 break;
1103 case OP_CIL:
1104 credit_clear_image();
1105 break;
1107 default:
1108 console.Print("- unimplemented opcode %s; script %04d halted.", cmd_table[cmd].mnemonic, s->scriptno);
1109 StopScript(s);
1110 return;
1116 void c------------------------------() {}
1119 int CVTDir(int csdir)
1121 const int cdir_to_nxdir[4] = { LEFT, UP, RIGHT, DOWN };
1123 if (csdir >= 0 && csdir < 4)
1124 return cdir_to_nxdir[csdir];
1126 staterr("CVTDir: invalid direction %d, returning LEFT", csdir);
1127 return LEFT;
1130 const char *DescribeCSDir(int csdir)
1132 switch(csdir)
1134 case 0: return "LEFT";
1135 case 1: return "UP";
1136 case 2: return "RIGHT";
1137 case 3: return "DOWN";
1138 case 4: return "FACE_PLAYER";
1139 case 5: return "NO_CHANGE";
1140 default: return stprintf("Invalid CS Dir %d", csdir);
1144 // converts from a CS direction (0123 = left,up,right,down)
1145 // into a NXEngine direction (0123 = right,left,up,down),
1146 // and applies the converted direction to the object.
1147 void SetCSDir(Object *o, int csdir)
1149 if (csdir < 4)
1151 o->dir = CVTDir(csdir);
1153 else if (csdir == 4)
1154 { // face towards player
1155 o->dir = (o->x >= player->x) ? LEFT : RIGHT;
1157 else if (csdir == 5)
1158 { // no-change, used with e.g. ANP
1160 else
1162 staterr("SetCSDir: warning: invalid direction %04d passed as dirparam only", csdir);
1165 // a few late-game objects, such as statues in the statue room,
1166 // use ANP/CNP's direction parameter as an extra generic parameter
1167 // to the object. I didn't feel it was safe to set a dir of say 200
1168 // in our engine as it may cause crashes somewhere if the sprite was
1169 // ever tried to be drawn using that dir. There's also the complication
1170 // that we're about to munge the requested values since our direction
1171 // constants don't have the same numerical values as CS's engine.
1172 // So is dirparam holds the raw value of the last dir that a script
1173 // tried to set.
1174 o->dirparam = csdir;
1177 void SetPDir(int d)
1179 if (d == 3)
1180 { // look away
1181 player->lookaway = 1;
1183 else
1185 player->lookaway = 0;
1187 if (d < 10)
1188 { // set direction - left/right/up/down
1189 SetCSDir(player, d);
1191 else
1192 { // face the object in parm
1193 Object *o;
1195 if ((o = FindObjectByID2(d)))
1197 player->dir = (player->x > o->x) ? LEFT:RIGHT;
1202 player->xinertia = 0;
1203 PSelectFrame();
1207 void c------------------------------() {}
1210 // call action_function on all NPCs with id2 matching "id2".
1211 void NPCDo(int id2, int p1, int p2, void (*action_function)(Object *o, int p1, int p2))
1213 // make a list first, as during <CNP, changing the
1214 // object type may call BringToFront and break stuff
1215 // if there are multiple hits.
1216 Object *hits[MAX_OBJECTS], *o;
1217 int numhits = 0;
1219 FOREACH_OBJECT(o)
1221 if (o->id2 == id2 && o != player)
1223 if (numhits < MAX_OBJECTS)
1224 hits[numhits++] = o;
1228 for(int i=0;i<numhits;i++)
1229 (*action_function)(hits[i], p1, p2);
1233 void DoANP(Object *o, int p1, int p2) // ANIMATE (set) object's state to p1 and set dir to p2
1235 #ifdef TRACE_SCRIPT
1236 stat("ANP: Obj %08x (%s): setting state: %d and dir: %s", \
1237 o, DescribeObjectType(o->type), p1, DescribeCSDir(p2));
1238 #endif
1240 o->state = p1;
1241 SetCSDir(o, p2);
1244 void DoCNP(Object *o, int p1, int p2) // CHANGE object to p1 and set dir to p2
1246 #ifdef TRACE_SCRIPT
1247 stat("CNP: Obj %08x changing from %s to %s, new dir = %s",
1248 o, DescribeObjectType(o->type), DescribeObjectType(p1), DescribeCSDir(p2));
1249 #endif
1251 // Must set direction BEFORE changing type, so that the Carried Puppy object
1252 // gets priority over the direction to use while the game is <PRI'd.
1253 SetCSDir(o, p2);
1254 o->ChangeType(p1);
1257 void DoDNP(Object *o, int p1, int p2) // DELETE object
1259 #ifdef TRACE_SCRIPT
1260 stat("DNP: %08x (%s) deleted", o, DescribeObjectType(o->type));
1261 #endif
1263 o->Delete();
1267 void c------------------------------() {}
1270 // delimit real newlines in 'in' to "\n"'s.
1271 void crtoslashn(const char *in, char *out)
1273 int i, j;
1275 for(i=j=0;in[i];i++)
1277 if (in[i] == 13)
1279 out[j++] = '\\';
1280 out[j++] = 'n';
1282 else
1284 out[j++] = in[i];
1288 out[j] = 0;
1291 bool contains_non_cr(const char *str)
1293 for(int i=0;str[i];i++)
1295 if (str[i] != '\r' && str[i] != '\n')
1296 return true;
1299 return false;