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