1
2 // TSC script parser & executor
3
4 #include "tsc.h"
5
6 #include "ObjManager.h"
7 #include "ResourceManager.h"
8 #include "ai/sym/smoke.h"
9 #include "common/misc.h"
10 #include "Utils/Logger.h"
11 #include "console.h"
12 #include "debug.h"
13 #include "endgame/credits.h"
14 #include "game.h"
15 #include "inventory.h"
16 #include "map.h"
17 #include "niku.h"
18 #include "nx.h"
19 #include "player.h"
20 #include "playerstats.h"
21 #include "screeneffect.h"
22 #include "settings.h"
23 #include "sound/SoundManager.h"
24 #include "graphics/Renderer.h"
25
26 #include <fstream>
27 #include <map>
28 #include <vector>
29
30 // which textbox options are enabled by the "<TUR" script command.
31 #define TUR_PARAMS (TB_LINE_AT_ONCE | TB_VARIABLE_WIDTH_CHARS | TB_CURSOR_NEVER_SHOWN)
32
33 /*
34 void c------------------------------() {}
35 */
36
37 struct TSCCommandTable
38 {
39 const char *mnemonic;
40 int nparams;
41 };
42 const TSCCommandTable cmd_table[] = {
43 {"AE+", 0}, {"AM+", 2}, {"AM-", 1}, {"AMJ", 2}, {"ANP", 3}, {"BOA", 1}, {"BSL", 1}, {"CAT", 0}, {"CIL", 0},
44 {"CLO", 0}, {"CLR", 0}, {"CMP", 3}, {"CMU", 1}, {"CNP", 3}, {"CPS", 0}, {"CRE", 0}, {"CSS", 0}, {"DNA", 1},
45 {"DNP", 1}, {"ECJ", 2}, {"END", 0}, {"EQ+", 1}, {"EQ-", 1}, {"ESC", 0}, {"EVE", 1}, {"FAC", 1}, {"FAI", 1},
46 {"FAO", 1}, {"FL+", 1}, {"FL-", 1}, {"FLA", 0}, {"FLJ", 2}, {"FMU", 0}, {"FOB", 2}, {"FOM", 1}, {"FON", 2},
47 {"FRE", 0}, {"GIT", 1}, {"HMC", 0}, {"INI", 0}, {"INP", 3}, {"IT+", 1}, {"IT-", 1}, {"ITJ", 2}, {"KEY", 0},
48 {"LDP", 0}, {"LI+", 1}, {"ML+", 1}, {"MLP", 0}, {"MM0", 0}, {"MNA", 0}, {"MNP", 4}, {"MOV", 2}, {"MP+", 1},
49 {"MPJ", 1}, {"MS2", 0}, {"MS3", 0}, {"MSG", 0}, {"MYB", 1}, {"MYD", 1}, {"NCJ", 2}, {"NOD", 0}, {"NUM", 1},
50 {"PRI", 0}, {"PS+", 2}, {"QUA", 1}, {"RMU", 0}, {"SAT", 0}, {"SIL", 1}, {"SK+", 1}, {"SK-", 1}, {"SKJ", 2},
51 {"SLP", 0}, {"SMC", 0}, {"SMP", 2}, {"SNP", 4}, {"SOU", 1}, {"SPS", 0}, {"SSS", 1}, {"STC", 0}, {"SVP", 0},
52 {"TAM", 3}, {"TRA", 4}, {"TUR", 0}, {"UNI", 1}, {"UNJ", 1}, {"WAI", 1}, {"WAS", 0}, {"XX1", 1}, {"YNJ", 1},
53 {"ZAM", 0}, {"ACH", 1},
54 };
55
56 unsigned char codealphabet[] = {"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123+-"};
57 unsigned char letter_to_code[256];
58 unsigned char mnemonic_lookup[32 * 32 * 32];
59
MnemonicToIndex(const char * str)60 static int MnemonicToIndex(const char *str)
61 {
62 int l1, l2, l3;
63
64 l1 = letter_to_code[(uint8_t)str[0]];
65 l2 = letter_to_code[(uint8_t)str[1]];
66 l3 = letter_to_code[(uint8_t)str[2]];
67 if (l1 == 0xff || l2 == 0xff || l3 == 0xff)
68 return -1;
69
70 return (l1 << 10) | (l2 << 5) | l3;
71 }
72
GenLTC(void)73 static void GenLTC(void)
74 {
75 int i;
76 uint8_t ch;
77
78 memset(letter_to_code, 0xff, sizeof(letter_to_code));
79 for (i = 0;; i++)
80 {
81 if (!(ch = codealphabet[i]))
82 break;
83 letter_to_code[ch] = i;
84 }
85
86 memset(mnemonic_lookup, 0xff, sizeof(mnemonic_lookup));
87 for (i = 0; i < OP_COUNT; i++)
88 {
89 mnemonic_lookup[MnemonicToIndex(cmd_table[i].mnemonic)] = i;
90 }
91 }
92
MnemonicToOpcode(char * str)93 static int MnemonicToOpcode(char *str)
94 {
95 int index = MnemonicToIndex(str);
96 if (index != -1)
97 {
98 index = mnemonic_lookup[index];
99 if (index != 0xff)
100 return index;
101 }
102
103 LOG_ERROR("MnemonicToOpcode: No such command '{}'", str);
104 return -1;
105 }
106
107 /*
108 void c------------------------------() {}
109 */
110
TSC()111 TSC::TSC() {}
112
~TSC()113 TSC::~TSC() {}
114
Init(void)115 bool TSC::Init(void)
116 {
117 LOG_INFO("Script engine init.");
118 GenLTC();
119 _curscript.running = false;
120
121 // load the "common" TSC scripts available to all maps
122 if (!Load(ResourceManager::getInstance()->getPath("Head.tsc"), ScriptPages::SP_HEAD))
123 return false;
124
125 // load the inventory screen scripts
126 if (!Load(ResourceManager::getInstance()->getPath("ArmsItem.tsc"), ScriptPages::SP_ARMSITEM))
127 return false;
128
129 // load stage select/teleporter scripts
130 if (!Load(ResourceManager::getInstance()->getPath("StageSelect.tsc"), ScriptPages::SP_STAGESELECT))
131 return false;
132
133 return true;
134 }
135
Close(void)136 void TSC::Close(void)
137 {
138 LOG_INFO("Script engine shutdown.");
139 // free all loaded scripts
140 for (int i = 0; i < NUM_SCRIPT_PAGES; i++)
141 _script_pages[i].Clear();
142 }
143
144 // load a tsc file and return the highest script # in the file
Load(const std::string & fname,ScriptPages pageno)145 bool TSC::Load(const std::string &fname, ScriptPages pageno)
146 {
147 ScriptPage *page = &_script_pages[(int)pageno];
148 int fsize;
149 std::string buf;
150 bool result;
151
152 LOG_DEBUG("TSC::load: loading '{}' to page {}", fname, (int)pageno);
153
154 if (_curscript.running && _curscript.pageno == (int)pageno)
155 StopScript(&_curscript);
156
157 page->Clear();
158
159 // load the raw script text
160 buf = Decrypt(fname, &fsize);
161 if (buf.empty())
162 {
163 LOG_ERROR("tsc_load: failed to load file: '{}'", fname);
164 return false;
165 }
166
167 // now "compile" all the scripts in the TSC
168 // int top_script = CompileScripts(buf, fsize, base);
169 result = Compile(buf.c_str(), fsize, pageno);
170
171 return result;
172 }
173
Decrypt(const std::string & fname,int * fsize_out)174 std::string TSC::Decrypt(const std::string &fname, int *fsize_out)
175 {
176 int fsize, i;
177 std::ifstream ifs;
178 if (fsize_out)
179 *fsize_out = 0;
180
181 ifs.open(widen(fname), std::ifstream::binary);
182
183 if (!ifs)
184 {
185 LOG_ERROR("tsc_decrypt: no such file: '{}'!", fname);
186 return "";
187 }
188
189 ifs.seekg(0, ifs.end);
190 fsize = ifs.tellg();
191 ifs.seekg(0, ifs.beg);
192
193 // load file
194 uint8_t *buf = new uint8_t[fsize + 1];
195 ifs.read((char *)buf, fsize);
196 buf[fsize] = 0;
197 ifs.close();
198
199 // get decryption key, which is actually part of the text
200 int keypos = (fsize / 2);
201 uint8_t key = buf[keypos];
202
203 // everything EXCEPT the key is encrypted
204 for (i = 0; i < keypos; i++)
205 {
206 buf[i] = (buf[i] - key);
207 }
208 for (i++; i < fsize; i++)
209 {
210 buf[i] = (buf[i] - key);
211 }
212
213 if (fsize_out)
214 *fsize_out = fsize;
215
216 std::string ret((char *)buf);
217 delete[] buf;
218 return ret;
219 }
220
221 /*
222 void c------------------------------() {}
223 */
224
nextchar(const char ** buf,const char * buf_end)225 static char nextchar(const char **buf, const char *buf_end)
226 {
227 if (*buf <= buf_end)
228 return *(*buf)++;
229
230 return 0;
231 }
232
ReadNumber(const char ** buf,const char * buf_end)233 static int ReadNumber(const char **buf, const char *buf_end)
234 {
235 static char num[5] = {0};
236 int i = 0;
237
238 while (i < 4)
239 {
240 num[i] = nextchar(buf, buf_end);
241 if (!isdigit(num[i]))
242 {
243 (*buf)--;
244 break;
245 }
246
247 i++;
248 }
249
250 return atoi(num);
251 }
252
ReadText(std::vector<uint8_t> * script,const char ** buf,const char * buf_end)253 static void ReadText(std::vector<uint8_t> *script, const char **buf, const char *buf_end)
254 {
255 while (*buf <= buf_end)
256 {
257 char ch = nextchar(buf, buf_end);
258 if (ch == '<' || ch == '#')
259 {
260 (*buf)--;
261 break;
262 }
263
264 if (ch != 10)
265 script->push_back(ch);
266 }
267
268 script->push_back('\0');
269 }
270
271 // compile a tsc file--a set of scripts in raw text format--into 'bytecode',
272 // and place the finished scripts into the given page.
Compile(const char * buf,int bufsize,ScriptPages pageno)273 bool TSC::Compile(const char *buf, int bufsize, ScriptPages pageno)
274 {
275 ScriptPage *page = &_script_pages[(int)pageno];
276 const char *buf_end = (buf + (bufsize - 1));
277 std::vector<uint8_t> *script = NULL;
278 char cmdbuf[4] = {0};
279
280 LOG_TRACE("tsc_compile bufsize = {} pageno = {}", bufsize, (int)pageno);
281
282 while (buf <= buf_end)
283 {
284 char ch = *(buf++);
285
286 if (ch == '#')
287 { // start of a scriptzz
288 if (script)
289 {
290 script->push_back(OP_END);
291 script = NULL;
292 }
293
294 int scriptno = ReadNumber(&buf, buf_end);
295 if (scriptno >= 10000 || scriptno < 0)
296 {
297 LOG_ERROR("tsc_compile: invalid script number: {}", scriptno);
298 return false;
299 }
300
301 // skip the CR after the script #
302 while (buf < buf_end)
303 {
304 if (*buf != '\r' && *buf != '\n')
305 break;
306 buf++;
307 }
308
309 // stat("Parsing script #%04d", scriptno);
310 if (page->scripts.find(scriptno) != page->scripts.end())
311 {
312 LOG_WARN("tsc_compile: duplicate script #{:#04d}; ignoring", scriptno);
313 // because script is left null, we'll ignore everything until we see another #
314 }
315 else
316 {
317 page->scripts.insert(std::pair<uint16_t, std::vector<uint8_t>>(scriptno, {}));
318 script = &(page->scripts[scriptno]);
319 }
320 }
321 else if (ch == '<' && script)
322 {
323 // read the command type
324 cmdbuf[0] = nextchar(&buf, buf_end);
325 cmdbuf[1] = nextchar(&buf, buf_end);
326 cmdbuf[2] = nextchar(&buf, buf_end);
327
328 int cmd = MnemonicToOpcode(cmdbuf);
329 if (cmd == -1)
330 return false;
331
332 // stat("Command '%s', parameters %d", cmdbuf, cmd_table[cmd].nparams);
333 script->push_back(cmd);
334
335 // read all parameters expected by that command
336 int nparams = cmd_table[cmd].nparams;
337 for (int i = 0; i < nparams; i++)
338 {
339 int val = ReadNumber(&buf, buf_end);
340
341 script->push_back(val >> 8);
342 script->push_back(val & 0xff);
343
344 // colon between params
345 if (i < (nparams - 1))
346 buf++;
347 }
348 }
349 else if (script)
350 { // text for message boxes
351 buf--;
352 script->push_back(OP_TEXT);
353 ReadText(script, &buf, buf_end);
354 }
355 }
356
357 if (script)
358 script->push_back(OP_END);
359
360 return true;
361 }
362
363 /*
364 void c------------------------------() {}
365 */
366
RunScripts(void)367 void TSC::RunScripts(void)
368 {
369 if (_curscript.running)
370 ExecScript(&_curscript);
371 }
372
StopScripts(void)373 void TSC::StopScripts(void)
374 {
375
376
377
378 if (_curscript.running)
379 StopScript(&_curscript);
380 }
381
GetCurrentScript(void)382 int TSC::GetCurrentScript(void)
383 {
384 if (_curscript.running)
385 return _curscript.scriptno;
386
387 return -1;
388 }
389
GetCurrentScriptInstance()390 ScriptInstance *TSC::GetCurrentScriptInstance()
391 {
392 if (_curscript.running)
393 return &_curscript;
394
395 return NULL;
396 }
397
398 /*
399 void c------------------------------() {}
400 */
401
402 // returns a pointer to the executable data/bytecode of the given script.
403 // handles looking on head, etc.
FindScriptData(int scriptno,ScriptPages pageno,ScriptPages * page_out)404 const uint8_t *TSC::FindScriptData(int scriptno, ScriptPages pageno, ScriptPages *page_out)
405 {
406 ScriptPage *page = &_script_pages[(int)pageno];
407
408 LOG_TRACE("Looking for script #{:#04d} in {} ({})", scriptno, (int)pageno, page->scripts.size());
409 if (page->scripts.find(scriptno) == page->scripts.end())
410 {
411 if (pageno != ScriptPages::SP_HEAD)
412 { // try to find the script in head.tsc
413 LOG_TRACE("Looking for script #{:#04d} in head", scriptno);
414 return FindScriptData(scriptno, ScriptPages::SP_HEAD, page_out);
415 }
416 else
417 {
418 return NULL;
419 }
420 }
421
422 if (page_out)
423 *page_out = pageno;
424 return &(page->scripts.find(scriptno)->second.front());
425 }
426
StartScript(int scriptno,ScriptPages pageno)427 bool TSC::StartScript(int scriptno, ScriptPages pageno)
428 {
429 const uint8_t *program;
430 ScriptPages found_pageno;
431
432 program = FindScriptData(scriptno, pageno, &found_pageno);
433 if (!program)
434 {
435 LOG_ERROR("StartScript: no script at position #{:#04d} page {}!", scriptno, (int)pageno);
436 return false;
437 }
438
439 // don't start regular map scripts (e.g. hvtrigger) if player is dead
440 if (player->dead && found_pageno != ScriptPages::SP_HEAD)
441 {
442 LOG_DEBUG("Not starting script {}; player is dead", scriptno);
443 return false;
444 }
445
446 // set the script
447 memset(&_curscript, 0, sizeof(ScriptInstance));
448
449 _curscript.program = program;
450 _curscript.scriptno = scriptno;
451 _curscript.pageno = (int)found_pageno;
452
453 _curscript.ynj_jump = -1;
454 _curscript.running = true;
455
456 textbox.ResetState();
457 player->hurt_time = 0;
458 player->hurt_flash_state = 0;
459
460 should_set_tao = false;
461
462 LOG_DEBUG("Started script #{:#04d}", scriptno);
463
464 RunScripts();
465 return true;
466 }
467
StopScript(ScriptInstance * s)468 void TSC::StopScript(ScriptInstance *s)
469 {
470 if (!s->running)
471 return;
472
473 should_set_tao = false;
474
475 s->running = false;
476 LOG_DEBUG("Stopped script #{:#04d}", s->scriptno);
477
478 // TRA is really supposed to be a jump, not a script restart--
479 // in that in maintains KEY/PRI across the stage transition.
480 // Emulate this by leaving the script state alone until the
481 // on-entry script starts.
482 player->inputs_locked = false;
483 game.frozen = false;
484 player->lookaway = false;
485
486 textbox.ResetState();
487 }
488
489 /*
490 void c------------------------------() {}
491 */
492
JumpScript(int newscriptno,ScriptPages pageno)493 bool TSC::JumpScript(int newscriptno, ScriptPages pageno)
494 {
495 ScriptInstance *s = &_curscript;
496
497 if (pageno == ScriptPages::SP_NULL)
498 pageno = (ScriptPages)s->pageno;
499
500 LOG_DEBUG("JumpScript: moving to script #{:#04d} page {}", newscriptno, (int)pageno);
501
502 s->program = FindScriptData(newscriptno, pageno, &pageno);
503 s->pageno = (int)pageno;
504 s->scriptno = newscriptno;
505 s->ip = 0;
506
507 if (!s->program)
508 {
509 LOG_ERROR("JumpScript: missing script #{:#04d}! Script terminated.", newscriptno);
510 StopScript(s);
511 return 1;
512 }
513
514 s->delaytimer = 0;
515 s->waitforkey = false;
516 s->wait_standing = false;
517
518 // <EVE doesn't clear textbox mode or the face etc
519 if (textbox.IsVisible())
520 {
521 textbox.ClearText();
522
523 // see entrance to Sacred Grounds when you have the Nikumaru Counter
524 // to witness that EVE clears TUR.
525 // textbox.SetFlags(TB_LINE_AT_ONCE, false);
526 // textbox.SetFlags(TB_VARIABLE_WIDTH_CHARS, false);
527 // textbox.SetFlags(TB_CURSOR_NEVER_SHOWN, false);
528 }
529
530 return 0;
531 }
532
ExecScript(ScriptInstance * s)533 void TSC::ExecScript(ScriptInstance *s)
534 {
535 char debugbuffer[512];
536 int cmd;
537 int val;
538 int parm[6] = {0, 0, 0, 0, 0, 0};
539 int i;
540 Object *o;
541 const char *mnemonic;
542 char *str;
543 int cmdip;
544
545
546 #define JUMP_IF(cond) \
547 { \
548 if (cond) \
549 { \
550 if (JumpScript(parm[1])) \
551 return; \
552 } \
553 }
554
555 // pause script while FAI/FAO still working
556 if (fade.getstate() == FS_FADING)
557 return;
558 if (game.mode == GM_ISLAND)
559 return;
560
561 // waiting for an answer from a Yes/No prompt?
562 if (s->ynj_jump != -1)
563 {
564 if (textbox.YesNoPrompt.ResultReady())
565 {
566 if (textbox.YesNoPrompt.GetResult() == NO)
567 JumpScript(s->ynj_jump);
568
569 textbox.YesNoPrompt.SetVisible(false);
570 s->ynj_jump = -1;
571 }
572 else
573 { // pause script until answer is receieved
574 return;
575 }
576 }
577
578 // pause script while text is still displaying
579 if (textbox.IsBusy())
580 return;
581
582 // pause while NOD is in effect
583 if (s->waitforkey)
584 {
585 if (s->nod_delay) // used to pause during <QUA without freezing textboxes in Hell
586 {
587 s->nod_delay--;
588 }
589 else
590 {
591 // if key was just pressed release nod.
592 // check them separately to allow holding X while
593 // tapping Z to keep text scrolling fast.
594 if ((inputs[JUMPKEY] && !s->lastjump) || (inputs[FIREKEY] && !s->lastfire))
595 {
596 // hide the fact that the key was just pushed
597 // so player doesn't jump/fire stupidly when dismissing textboxes
598 lastinputs[JUMPKEY] |= inputs[JUMPKEY];
599 lastinputs[FIREKEY] |= inputs[FIREKEY];
600 lastpinputs[JUMPKEY] |= inputs[JUMPKEY];
601 lastpinputs[FIREKEY] |= inputs[FIREKEY];
602
603 s->waitforkey = false;
604 textbox.ShowCursor(false);
605 }
606
607 s->lastjump = inputs[JUMPKEY];
608 s->lastfire = inputs[FIREKEY];
609 }
610
611 // if still on return
612 if (s->waitforkey)
613 return;
614 }
615
616 // pause scripts while WAI is in effect.
617 // <WAI9999, used in inventory/stage-select screen, means forever.
618 if (s->delaytimer)
619 {
620 if (s->delaytimer == 9999)
621 {
622 UnlockInventoryInput();
623 }
624 else
625 {
626 s->delaytimer--;
627 }
628
629 return;
630 }
631
632 // pause while WAS (wait until standing) is in effect.
633 if (s->wait_standing)
634 {
635 if (!player->blockd)
636 return;
637 s->wait_standing = false;
638 }
639
640
641
642 // stat("<> Entering script execution loop at ip = %d", s->ip);
643
644 // main execution loop
645 for (;;)
646 {
647 char debugbuffer2[256];
648 cmdip = s->ip++;
649 cmd = s->program[cmdip];
650 if (cmd < OP_COUNT)
651 mnemonic = cmd_table[cmd].mnemonic;
652 else
653 mnemonic = "???";
654
655 if (cmd != OP_TEXT)
656 {
657 snprintf(debugbuffer2, sizeof(debugbuffer2), "%04x <%s ", cmd, mnemonic);
658 for (i = 0; i < cmd_table[cmd].nparams; i++)
659 {
660 val = ((int)s->program[s->ip++]) << 8;
661 val |= s->program[s->ip++];
662 parm[i] = val;
663 snprintf(debugbuffer, sizeof(debugbuffer), "%s %04d", debugbuffer2, val);
664 }
665 }
666 else
667 {
668 crtoslashn((char *)&s->program[s->ip], debugbuffer2);
669 snprintf(debugbuffer, sizeof(debugbuffer), "TEXT '%s'", debugbuffer2);
670 }
671
672 if (cmd == OP_TEXT && !textbox.IsVisible() && !strcmp(debugbuffer, "TEXT '\n'"))
673 {
674 }
675 else
676 {
677 LOG_TRACE("{:#04d}:{} {}", s->scriptno, cmdip, debugbuffer);
678 }
679
680 switch (cmd)
681 {
682 case OP_END:
683 StopScript(s);
684 return;
685
686 case OP_FAI:
687 fade.Start(FADE_IN, parm[0], SPR_FADE_DIAMOND);
688 return;
689 case OP_FAO:
690 fade.Start(FADE_OUT, parm[0], SPR_FADE_DIAMOND);
691 return;
692 case OP_FLA:
693 flashscreen.Start();
694 break;
695
696 case OP_SOU:
697 NXE::Sound::SoundManager::getInstance()->playSfx((NXE::Sound::SFX)parm[0]);
698 break;
699 case OP_CMU:
700 NXE::Sound::SoundManager::getInstance()->music(parm[0]);
701 break;
702 case OP_RMU:
703 NXE::Sound::SoundManager::getInstance()->music(NXE::Sound::SoundManager::getInstance()->lastSong(), true);
704 break;
705 case OP_FMU:
706 NXE::Sound::SoundManager::getInstance()->fadeMusic();
707 break;
708
709 case OP_SSS:
710 NXE::Sound::SoundManager::getInstance()->startStreamSound(parm[0]);
711 break;
712 case OP_SPS:
713 NXE::Sound::SoundManager::getInstance()->startPropSound();
714 break;
715
716 case OP_CSS: // these seem identical-- either one will
717 case OP_CPS: // in fact stop the other.
718 {
719 NXE::Sound::SoundManager::getInstance()->stopLoopSfx();
720 }
721 break;
722
723 // free menu selector in Inventory. It also undoes <PRI,
724 // as can be seen at the entrance to Sacred Ground.
725 case OP_FRE:
726 {
727 game.frozen = false;
728 player->inputs_locked = false;
729 UnlockInventoryInput();
730 }
731 break;
732
733 case OP_PRI: // freeze entire game (players + NPCs)
734 {
735 game.frozen = true;
736 player->inputs_locked = false;
737 player->hurt_time = 0;
738 player->hurt_flash_state = 0;
739 statusbar.xpflashcount = 0; // looks odd if this happens after a long <PRI, even though it's technically correct
740 }
741 break;
742
743 case OP_KEY: // lock players input but NPC/objects still run
744 {
745 game.frozen = false;
746 player->inputs_locked = true;
747 player->hurt_time = 0;
748 player->hurt_flash_state = 0;
749 }
750 break;
751
752 case OP_MOV:
753 player->x = (parm[0] * TILE_W) * CSFI;
754 player->y = (parm[1] * TILE_H) * CSFI;
755 player->xinertia = player->yinertia = 0;
756 player->lookaway = false;
757 break;
758
759 case OP_UNI:
760 player->movementmode = parm[0];
761 map_scroll_lock(parm[0]); // locks on anything other than 0
762 break;
763
764 case OP_MNA: // show map name (as used on entry)
765 map_show_map_name();
766 break;
767
768 case OP_MLP: // bring up Map System
769 game.setmode(GM_MAP_SYSTEM, game.mode);
770 break;
771
772 case OP_TRA:
773 {
774 bool waslocked = (player->inputs_locked || game.frozen);
775
776 LOG_DEBUG("Executing <TRA to stage {}", parm[0]);
777 game.switchstage.mapno = parm[0];
778 game.switchstage.eventonentry = parm[1];
779 game.switchstage.playerx = parm[2];
780 game.switchstage.playery = parm[3];
781 StopScript(s);
782
783 if (game.switchstage.mapno != 0)
784 {
785 // KEY is maintained across TRA as if the TRA
786 // were a jump instead of a restart; but if the
787 // game is in PRI then it is downgraded to a KEY.
788 // See entrance to Yamashita Farm.
789 if (waslocked)
790 {
791 player->inputs_locked = true;
792 game.frozen = false;
793 }
794 }
795
796 return;
797 }
798 break;
799
800 case OP_AMPLUS:
801 GetWeapon(parm[0], parm[1]);
802 _lastammoinc = parm[1];
803 break;
804 case OP_AMMINUS:
805 LoseWeapon(parm[0]);
806 break;
807 case OP_TAM:
808 TradeWeapon(parm[0], parm[1], parm[2]);
809 break;
810 case OP_AMJ:
811 JUMP_IF(player->weapons[parm[0]].hasWeapon);
812 break;
813
814 case OP_ZAM: // drop all weapons to level 1
815 {
816 for (int i = 0; i < WPN_COUNT; i++)
817 {
818 player->weapons[i].xp = 0;
819 player->weapons[i].level = 0;
820 }
821 }
822 break;
823
824 case OP_EVE:
825 JumpScript(parm[0]);
826 break; // unconditional jump to event
827
828 case OP_FLPLUS:
829 game.flags[parm[0]] = 1;
830 break;
831 case OP_FLMINUS:
832 game.flags[parm[0]] = 0;
833 break;
834 case OP_FLJ:
835 JUMP_IF(game.flags[parm[0]]);
836 break;
837
838 case OP_ITPLUS:
839 AddInventory(parm[0]);
840 break;
841 case OP_ITMINUS:
842 DelInventory(parm[0]);
843 break;
844 case OP_ITJ:
845 JUMP_IF((FindInventory(parm[0]) != -1));
846 break;
847
848 // the PSelectSprite is a hack so when the Mimiga Mask is taken
849 // it disappears immediately even though the game is in <PRI.
850 case OP_EQPLUS:
851 player->equipmask |= parm[0];
852 PSelectSprite();
853 break;
854 case OP_EQMINUS:
855 player->equipmask &= ~parm[0];
856 PSelectSprite();
857 break;
858
859 case OP_SKPLUS:
860 game.skipflags[parm[0]] = 1;
861 break;
862 case OP_SKMINUS:
863 game.skipflags[parm[0]] = 0;
864 break;
865 case OP_SKJ:
866 JUMP_IF(game.skipflags[parm[0]]);
867 break;
868
869 case OP_PSPLUS:
870 textbox.StageSelect.SetSlot(parm[0], parm[1]);
871 break;
872
873 case OP_NCJ:
874 JUMP_IF(CountObjectsOfType(parm[0]) > 0);
875 break;
876
877 case OP_ECJ: // unused but valid
878 JUMP_IF(FindObjectByID2(parm[0]));
879 break;
880
881 // life capsule--add to max life
882 case OP_MLPLUS:
883 player->maxHealth += parm[0];
884 player->hp += parm[0];
885 break;
886
887 case OP_FON: // focus on NPC
888 {
889 if ((o = FindObjectByID2(parm[0])))
890 {
891 map_focus(o, parm[1]);
892 }
893 }
894 break;
895 case OP_FOB: // focus on boss
896 {
897 if (game.stageboss.object)
898 map_focus(game.stageboss.object, parm[1]);
899 else
900 LOG_ERROR("tsc: <FOB without stage boss");
901 }
902 break;
903 case OP_FOM: // focus back to player (mychar)
904 {
905 map_focus(NULL, parm[0]);
906 }
907 break;
908
909 case OP_DNA: // delete all objects of type parm1
910 {
911 Object *o = firstobject;
912 while (o)
913 {
914 if (o->type == parm[0])
915 o->Delete();
916 o = o->next;
917 }
918 }
919 break;
920
921 case OP_ANP:
922 _NPCDo(parm[0], parm[1], parm[2], _DoANP);
923 break;
924 case OP_CNP:
925 _NPCDo(parm[0], parm[1], parm[2], _DoCNP);
926 break;
927 case OP_DNP:
928 _NPCDo(parm[0], parm[1], parm[2], _DoDNP);
929 break;
930
931 case OP_MNP: // move object X to (Y,Z) with direction W
932 if ((o = FindObjectByID2(parm[0])))
933 {
934 _SetCSDir(o, parm[3]);
935 o->x = (parm[1] * TILE_W) * CSFI;
936 o->y = (parm[2] * TILE_H) * CSFI;
937 }
938 break;
939
940 case OP_BOA: // set boss state
941 {
942 game.stageboss.SetState(parm[0]);
943 }
944 break;
945 case OP_BSL: // bring up boss bar
946 {
947 Object *target;
948 if (parm[0] == 0)
949 { // <BSL0000 means the stage boss
950 target = game.stageboss.object;
951 if (!game.stageboss.object)
952 LOG_ERROR("<BSL0000 but no stage boss present");
953 }
954 else
955 {
956 target = FindObjectByID2(parm[0]);
957 }
958
959 if (target)
960 {
961 game.bossbar.object = target;
962 game.bossbar.defeated = false;
963 game.bossbar.starting_hp = target->hp;
964 game.bossbar.bar.displayed_value = target->hp;
965 }
966 else
967 {
968 LOG_ERROR("Target of <BSL not found");
969 }
970 }
971 break;
972
973 case OP_MM0:
974 player->xinertia = 0;
975 break;
976 case OP_MYD:
977 _SetPDir(parm[0]);
978 break;
979 case OP_MYB:
980 {
981 player->lookaway = 0;
982 player->yinertia = -0x200;
983 // nudge a little more in plantation
984 if (game.curmap == 56 && GetCurrentScript() == 480)
985 player->yinertia = -0x400;
986 int dir = parm[0];
987
988 if (dir >= 10) // bump away from the object in parm
989 {
990 o = FindObjectByID2(dir);
991 if (o)
992 {
993 if (player->CenterX() > o->CenterX())
994 dir = 0;
995 else
996 dir = 2;
997 }
998 }
999
1000 if (dir == 0)
1001 {
1002 player->dir = LEFT;
1003 player->xinertia = 0x200;
1004 }
1005 else if (dir == 2)
1006 {
1007 player->dir = RIGHT;
1008 player->xinertia = -0x200;
1009 }
1010 }
1011 break;
1012
1013 case OP_WAI:
1014 s->delaytimer = parm[0];
1015
1016 if (NXE::Graphics::Renderer::getInstance()->widescreen)
1017 {
1018 // sue running out in dark place
1019 if (game.curmap == 68 && s->scriptno == 600 && parm[0] == 100)
1020 {
1021 s->delaytimer = 130;
1022 }
1023 // sue running out in balcony
1024 if (game.curmap == 70 && s->scriptno == 310 && parm[0] == 60)
1025 {
1026 s->delaytimer = 110;
1027 }
1028 // Curly in Almond
1029 if (game.curmap == 47 && s->scriptno == 200 && parm[0] == 32)
1030 {
1031 if (s->program[s->ip] == OP_DNP)
1032 {
1033 int val = 0;
1034 val = ((int)s->program[s->ip+1]) << 8;
1035 val |= s->program[s->ip+2];
1036 if (val == 300)
1037 {
1038 s->delaytimer = 153;
1039 }
1040 }
1041 }
1042 }
1043 return;
1044 case OP_WAS:
1045 s->wait_standing = true;
1046 return; // wait until player has blockd
1047
1048 case OP_SMP:
1049 map.tiles[parm[0]][parm[1]]--;
1050 break;
1051 case OP_SNP:
1052 {
1053 (void)CreateObject((parm[1] * TILE_W) * CSFI, (parm[2] * TILE_H) * CSFI, parm[0], 0, 0, CVTDir(parm[3]), NULL,
1054 CF_NO_SPAWN_EVENT);
1055 }
1056 break;
1057
1058 case OP_CMP: // change map tile at x:y to z and create smoke
1059 {
1060 int x = parm[0];
1061 int y = parm[1];
1062 map.tiles[x][y] = parm[2];
1063
1064 // get smoke coords
1065 x = ((x * TILE_W) + (TILE_W / 2)) * CSFI;
1066 y = ((y * TILE_H) + (TILE_H / 2)) * CSFI;
1067 // when tiles are CMP'd during a PRI the smoke is not visible
1068 // until the game is released, so I came up with this scheme
1069 // to make that happen. See the "you see a button" destroyable
1070 // box on the 2nd level of Maze M.
1071 if (game.frozen)
1072 {
1073 o = CreateObject(x, y, OBJ_SMOKE_DROPPER);
1074 o->timer2 = 4; // amount of smoke
1075 }
1076 else
1077 {
1078 SmokeXY(x, y, 4, TILE_W / 2, TILE_H / 2);
1079 }
1080 }
1081 break;
1082
1083 case OP_QUA:
1084 {
1085 s->nod_delay = parm[0];
1086 quake(parm[0], NXE::Sound::SFX::SND_NULL);
1087 }
1088 break;
1089
1090 case OP_LIPLUS:
1091 AddHealth(parm[0]);
1092 break;
1093 case OP_AEPLUS:
1094 RefillAllAmmo();
1095 break; // refills missiles
1096
1097 case OP_INI:
1098 game.switchstage.mapno = NEW_GAME;
1099 break; // restart game from beginning
1100 case OP_STC:
1101 niku_save(game.counter);
1102 break;
1103
1104 case OP_SVP:
1105 {
1106 textbox.SaveSelect.SetVisible(true, SS_SAVING);
1107 s->delaytimer = 9999;
1108 return;
1109 }
1110 break;
1111 case OP_LDP:
1112 game.switchstage.mapno = LOAD_GAME;
1113 break;
1114
1115 case OP_HMC:
1116 player->hide = true;
1117 break;
1118 case OP_SMC:
1119 player->hide = false;
1120 break;
1121
1122 // ---------------------------------------
1123
1124 case OP_MSG: // bring up text box
1125 {
1126 LOG_DEBUG("Should set tao: {}", should_set_tao);
1127 // required for post-Ballos cutscene
1128 if (should_set_tao) {
1129 textbox.SetFlags(TUR_PARAMS, true);
1130 } else {
1131 textbox.SetFlags(TUR_PARAMS, false);
1132 }
1133 textbox.SetVisible(true, TB_DEFAULTS);
1134 textbox.SetCanSpeedUp(true);
1135 }
1136 break;
1137
1138 case OP_MS2: // bring up text box, at top, with no border
1139 {
1140 textbox.SetFace(0); // required for Undead Core intro
1141 if (should_set_tao) {
1142 textbox.SetFlags(TUR_PARAMS, true);
1143 } else {
1144 textbox.SetFlags(TUR_PARAMS, false);
1145 }
1146 textbox.SetVisible(true, TB_DRAW_AT_TOP | TB_NO_BORDER);
1147 textbox.SetCanSpeedUp(true);
1148 }
1149 break;
1150
1151 case OP_MS3: // bring up text box, at top
1152 {
1153 if (should_set_tao) {
1154 textbox.SetFlags(TUR_PARAMS, true);
1155 } else {
1156 textbox.SetFlags(TUR_PARAMS, false);
1157 }
1158 textbox.SetVisible(true, TB_DRAW_AT_TOP);
1159 textbox.SetCanSpeedUp(true);
1160 }
1161 break;
1162
1163 case OP_CLO: // dismiss text box.
1164 textbox.SetVisible(false);
1165 textbox.ClearText();
1166 // ...don't ResetState(), or it'll clear <FAC during Momorin dialog (Hideout)
1167 break;
1168
1169 case OP_TEXT: // text to be displayed
1170 {
1171 str = (char *)&s->program[s->ip];
1172 s->ip += (strlen(str) + 1);
1173
1174 textbox.AddText(str);
1175
1176 // must yield execution, because the message is busy now.
1177 // however, if the message contains only CR's, then we don't yield,
1178 // because CR's take no time to display.
1179 if (contains_non_cr(str))
1180 {
1181 // stat("<> Pausing script execution to display message.");
1182 return;
1183 }
1184 /*else
1185 {
1186 stat("<> Message is only CR's, continuing script...");
1187 }*/
1188 }
1189 break;
1190
1191 case OP_CLR: // erase all text in box
1192 textbox.ClearText();
1193 break;
1194
1195 case OP_FAC: // set and slide in given character face
1196 textbox.SetFace(parm[0]);
1197 break;
1198
1199 case OP_NOD: // pause till user presses key
1200 {
1201 if (textbox.IsVisible())
1202 {
1203 s->waitforkey = true; // pause exec till key pressed
1204 // don't release immediately if keys already down
1205 s->lastjump = true;
1206 s->lastfire = true;
1207
1208 textbox.ShowCursor(true);
1209 }
1210 }
1211 return;
1212
1213 case OP_YNJ: // prompt Yes or No and jump to given script if No
1214 {
1215 textbox.YesNoPrompt.SetVisible(true);
1216 s->ynj_jump = parm[0];
1217
1218 return;
1219 }
1220 break;
1221
1222 case OP_SAT: // disables typing animation
1223 case OP_CAT: // unused synonym
1224 {
1225 should_set_tao = true;
1226 // textbox.SetFlags(TB_LINE_AT_ONCE | TB_CURSOR_NEVER_SHOWN, true);
1227 }
1228 break;
1229
1230 case OP_TUR: // set text mode to that used for signs
1231 {
1232 textbox.SetFlags(TUR_PARAMS, true);
1233 // should_set_tao = true;
1234 textbox.SetCanSpeedUp(false);
1235 }
1236 break;
1237
1238 case OP_GIT: // show item graphic
1239 {
1240 if (parm[0] != 0)
1241 {
1242 int sprite, frame;
1243
1244 if (parm[0] >= 1000)
1245 { // an item
1246 sprite = SPR_ITEMIMAGE;
1247 frame = (parm[0] - 1000);
1248 }
1249 else
1250 { // a weapon
1251 sprite = SPR_ARMSICONS;
1252 frame = parm[0];
1253 }
1254
1255 textbox.ItemImage.SetSprite(sprite, frame);
1256 textbox.ItemImage.SetVisible(true);
1257 }
1258 else
1259 {
1260 textbox.ItemImage.SetVisible(false);
1261 }
1262 }
1263 break;
1264
1265 case OP_NUM:
1266 { // seems to show the last value that was used with "AM+"
1267 char buf[16];
1268 sprintf(buf, "%d", _lastammoinc);
1269
1270 textbox.AddText(buf);
1271 }
1272 break;
1273
1274 case OP_SLP: // bring up teleporter menu
1275 {
1276 textbox.StageSelect.SetVisible(true);
1277 return;
1278 }
1279 break;
1280
1281 case OP_ESC:
1282 {
1283 StopScript(s);
1284 game.reset();
1285 }
1286 break;
1287
1288 // ---------------------------------------
1289
1290 // trigger island-falling cinematic
1291 // if the parameter is 0, the island crashes (good ending);
1292 // if the parameter is 1, the island survives (best ending)
1293 case OP_XX1:
1294 {
1295 game.setmode(GM_ISLAND, parm[0]);
1296 return;
1297 }
1298 break;
1299
1300 case OP_CRE:
1301 {
1302 game.setmode(GM_CREDITS);
1303 return;
1304 }
1305 break;
1306
1307 case OP_SIL:
1308 credit_set_image(parm[0]);
1309 break;
1310
1311 case OP_CIL:
1312 credit_clear_image();
1313 break;
1314
1315 case OP_ACH:
1316 break;
1317
1318 default:
1319 {
1320 if (cmd < OP_COUNT)
1321 {
1322 LOG_WARN("- unimplemented opcode %s; script #{:#04d} halted.", cmd_table[cmd].mnemonic, s->scriptno);
1323 }
1324 else
1325 {
1326 LOG_WARN("- unimplemented opcode {:02x}; script #{:#04d} halted.", cmd, s->scriptno);
1327 }
1328
1329 StopScript(s);
1330 return;
1331 }
1332 }
1333 }
1334 }
1335
_DescribeCSDir(int csdir)1336 std::string _DescribeCSDir(int csdir)
1337 {
1338 switch (csdir)
1339 {
1340 case 0:
1341 return "LEFT";
1342 case 1:
1343 return "UP";
1344 case 2:
1345 return "RIGHT";
1346 case 3:
1347 return "DOWN";
1348 case 4:
1349 return "FACE_PLAYER";
1350 case 5:
1351 return "NO_CHANGE";
1352 default:
1353 return "Invalid CS Dir" + std::to_string(csdir);
1354 }
1355 }
1356
1357 // converts from a CS direction (0123 = left,up,right,down)
1358 // into a NXEngine direction (0123 = right,left,up,down),
1359 // and applies the converted direction to the object.
_SetCSDir(Object * o,int csdir)1360 void _SetCSDir(Object *o, int csdir)
1361 {
1362 if (csdir < 4)
1363 {
1364 o->dir = CVTDir(csdir);
1365 }
1366 else if (csdir == 4)
1367 { // face towards player
1368 o->dir = (o->x >= player->x) ? LEFT : RIGHT;
1369 }
1370 else if (csdir == 5)
1371 { // no-change, used with e.g. ANP
1372 }
1373 else
1374 {
1375 LOG_ERROR("SetCSDir: warning: invalid direction {:#04d} passed as dirparam only", csdir);
1376 }
1377
1378 // a few late-game objects, such as statues in the statue room,
1379 // use ANP/CNP's direction parameter as an extra generic parameter
1380 // to the object. I didn't feel it was safe to set a dir of say 200
1381 // in our engine as it may cause crashes somewhere if the sprite was
1382 // ever tried to be drawn using that dir. There's also the complication
1383 // that we're about to munge the requested values since our direction
1384 // constants don't have the same numerical values as CS's engine.
1385 // So is dirparam holds the raw value of the last dir that a script
1386 // tried to set.
1387 o->dirparam = csdir;
1388 }
1389
_SetPDir(int d)1390 void _SetPDir(int d)
1391 {
1392 if (d == 3)
1393 { // look away
1394 player->lookaway = 1;
1395 }
1396 else
1397 {
1398 player->lookaway = 0;
1399
1400 if (d < 10)
1401 { // set direction - left/right/up/down
1402 _SetCSDir(player, d);
1403 }
1404 else
1405 { // face the object in parm
1406 Object *o;
1407
1408 if ((o = FindObjectByID2(d)))
1409 {
1410 player->dir = (player->x > o->x) ? LEFT : RIGHT;
1411 }
1412 }
1413 }
1414
1415 player->xinertia = 0;
1416 PSelectFrame();
1417 }
1418
1419 /*
1420 void c------------------------------() {}
1421 */
1422
1423 // call action_function on all NPCs with id2 matching "id2".
_NPCDo(int id2,int p1,int p2,void (* action_function)(Object * o,int p1,int p2))1424 void TSC::_NPCDo(int id2, int p1, int p2, void (*action_function)(Object *o, int p1, int p2))
1425 {
1426 // make a list first, as during <CNP, changing the
1427 // object type may call BringToFront and break stuff
1428 // if there are multiple hits.
1429 Object *hits[MAX_OBJECTS], *o;
1430 int numhits = 0;
1431
1432 FOREACH_OBJECT(o)
1433 {
1434 if (o->id2 == id2 && o != player)
1435 {
1436 if (numhits < MAX_OBJECTS)
1437 hits[numhits++] = o;
1438 }
1439 }
1440
1441 for (int i = 0; i < numhits; i++)
1442 (*action_function)(hits[i], p1, p2);
1443 }
1444
_DoANP(Object * o,int p1,int p2)1445 void TSC::_DoANP(Object *o, int p1, int p2) // ANIMATE (set) object's state to p1 and set dir to p2
1446 {
1447 LOG_TRACE("ANP: Obj {:#08x} ({}): setting state: {} and dir: {}", (uint64_t)o, DescribeObjectType(o->type), p1,
1448 _DescribeCSDir(p2).c_str());
1449
1450 o->state = p1;
1451 _SetCSDir(o, p2);
1452 }
1453
_DoCNP(Object * o,int p1,int p2)1454 void TSC::_DoCNP(Object *o, int p1, int p2) // CHANGE object to p1 and set dir to p2
1455 {
1456 LOG_TRACE("CNP: Obj {:#08x} changing from {} to {}, new dir = {}", (uint64_t)o, DescribeObjectType(o->type), DescribeObjectType(p1),
1457 _DescribeCSDir(p2).c_str());
1458
1459 // Must set direction BEFORE changing type, so that the Carried Puppy object
1460 // gets priority over the direction to use while the game is <PRI'd.
1461 _SetCSDir(o, p2);
1462 o->ChangeType(p1);
1463 }
1464
_DoDNP(Object * o,int p1,int p2)1465 void TSC::_DoDNP(Object *o, int p1, int p2) // DELETE object
1466 {
1467 LOG_TRACE("DNP: {:#08x} ({}) deleted", (uint64_t)o, DescribeObjectType(o->type));
1468
1469 o->Delete();
1470 }
1471