1 /** @file finaleinterpreter.cpp  InFine animation system Finale script interpreter.
2  *
3  * @authors Copyright © 2003-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  * @authors Copyright © 2005-2015 Daniel Swanson <danij@dengine.net>
5  *
6  * @par License
7  * GPL: http://www.gnu.org/licenses/gpl.html
8  *
9  * <small>This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by the
11  * Free Software Foundation; either version 2 of the License, or (at your
12  * option) any later version. This program is distributed in the hope that it
13  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
15  * Public License for more details. You should have received a copy of the GNU
16  * General Public License along with this program; if not, write to the Free
17  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18  * 02110-1301 USA</small>
19  */
20 
21 #include "de_base.h"
22 #include "ui/infine/finaleinterpreter.h"
23 
24 #include <QList>
25 #include <de/memory.h>
26 #include <de/timer.h>
27 #include <de/LogBuffer>
28 #include <doomsday/doomsdayapp.h>
29 #include <doomsday/console/cmd.h>
30 #include <doomsday/console/exec.h>
31 #include <doomsday/filesys/fs_main.h>
32 #include <doomsday/world/Materials>
33 #include <doomsday/Game>
34 
35 #include "dd_def.h"
36 #include "def_main.h"  // ::defs
37 
38 #include "api_material.h"
39 #include "api_render.h"
40 #include "api_resource.h"
41 
42 #include "network/net_main.h"
43 
44 #include "ui/infine/finalewidget.h"
45 #include "ui/infine/finaleanimwidget.h"
46 #include "ui/infine/finalepagewidget.h"
47 #include "ui/infine/finaletextwidget.h"
48 
49 #ifdef __CLIENT__
50 #  include "api_fontrender.h"
51 #  include "client/cl_infine.h"
52 
53 #  include "gl/gl_main.h"
54 #  include "gl/gl_texmanager.h"
55 #  include "gl/texturecontent.h"
56 #  include "gl/sys_opengl.h" // TODO: get rid of this
57 #endif
58 
59 #ifdef __SERVER__
60 #  include "server/sv_infine.h"
61 #endif
62 
63 #define MAX_TOKEN_LENGTH        (8192)
64 
65 #define FRACSECS_TO_TICKS(sec)  (int(sec * TICSPERSEC + 0.5))
66 
67 using namespace de;
68 
69 enum fi_operand_type_t
70 {
71     FVT_INT,
72     FVT_FLOAT,
73     FVT_STRING,
74     FVT_URI
75 };
76 
operandTypeForCharCode(char code)77 static fi_operand_type_t operandTypeForCharCode(char code)
78 {
79     switch (code)
80     {
81     case 'i': return FVT_INT;
82     case 'f': return FVT_FLOAT;
83     case 's': return FVT_STRING;
84     case 'u': return FVT_URI;
85 
86     default: throw Error("operandTypeForCharCode", String("Unknown char-code %1").arg(code));
87     }
88 }
89 
90 // Helper macro for accessing the value of an operand.
91 #define OP_INT(n)           (ops[n].data.integer)
92 #define OP_FLOAT(n)         (ops[n].data.flt)
93 #define OP_CSTRING(n)       (ops[n].data.cstring)
94 #define OP_URI(n)           (ops[n].data.uri)
95 
96 struct fi_operand_t
97 {
98     fi_operand_type_t type;
99     union {
100         int         integer;
101         float       flt;
102         char const *cstring;
103         uri_s      *uri;
104     } data;
105 };
106 
107 // Helper macro for defining infine command functions.
108 #define DEFFC(name) void FIC_##name(command_t const &cmd, fi_operand_t const *ops, FinaleInterpreter &fi)
109 
110 /**
111  * @defgroup finaleInterpreterCommandDirective Finale Interpreter Command Directive
112  * @ingroup infine
113  */
114 /*@{*/
115 #define FID_NORMAL          0
116 #define FID_ONLOAD          0x1
117 /*@}*/
118 
119 struct command_t
120 {
121     char const *token;
122     char const *operands;
123 
124     typedef void (*Func) (command_t const &, fi_operand_t const *, FinaleInterpreter &);
125     Func func;
126 
127     struct command_flags_s {
128         char when_skipping:1;
129         char when_condition_skipping:1; // Skipping because condition failed.
130     } flags;
131 
132     /// Command execution directives NOT supported by this command.
133     /// @see finaleInterpreterCommandDirective
134     int excludeDirectives;
135 };
136 
137 // Command functions.
138 DEFFC(Do);
139 DEFFC(End);
140 DEFFC(BGMaterial);
141 DEFFC(NoBGMaterial);
142 DEFFC(Wait);
143 DEFFC(WaitText);
144 DEFFC(WaitAnim);
145 DEFFC(Tic);
146 DEFFC(InTime);
147 DEFFC(Color);
148 DEFFC(ColorAlpha);
149 DEFFC(Pause);
150 DEFFC(CanSkip);
151 DEFFC(NoSkip);
152 DEFFC(SkipHere);
153 DEFFC(Events);
154 DEFFC(NoEvents);
155 DEFFC(OnKey);
156 DEFFC(UnsetKey);
157 DEFFC(If);
158 DEFFC(IfNot);
159 DEFFC(Else);
160 DEFFC(GoTo);
161 DEFFC(Marker);
162 DEFFC(Image);
163 DEFFC(ImageAt);
164 DEFFC(XImage);
165 DEFFC(Delete);
166 DEFFC(Patch);
167 DEFFC(SetPatch);
168 DEFFC(Anim);
169 DEFFC(AnimImage);
170 DEFFC(StateAnim);
171 DEFFC(Repeat);
172 DEFFC(ClearAnim);
173 DEFFC(PicSound);
174 DEFFC(ObjectOffX);
175 DEFFC(ObjectOffY);
176 DEFFC(ObjectOffZ);
177 DEFFC(ObjectScaleX);
178 DEFFC(ObjectScaleY);
179 DEFFC(ObjectScaleZ);
180 DEFFC(ObjectScale);
181 DEFFC(ObjectScaleXY);
182 DEFFC(ObjectScaleXYZ);
183 DEFFC(ObjectRGB);
184 DEFFC(ObjectAlpha);
185 DEFFC(ObjectAngle);
186 DEFFC(Rect);
187 DEFFC(FillColor);
188 DEFFC(EdgeColor);
189 DEFFC(OffsetX);
190 DEFFC(OffsetY);
191 DEFFC(Sound);
192 DEFFC(SoundAt);
193 DEFFC(SeeSound);
194 DEFFC(DieSound);
195 DEFFC(Music);
196 DEFFC(MusicOnce);
197 DEFFC(Filter);
198 DEFFC(Text);
199 DEFFC(TextFromDef);
200 DEFFC(TextFromLump);
201 DEFFC(SetText);
202 DEFFC(SetTextDef);
203 DEFFC(DeleteText);
204 DEFFC(Font);
205 DEFFC(FontA);
206 DEFFC(FontB);
207 DEFFC(PredefinedColor);
208 DEFFC(PredefinedFont);
209 DEFFC(TextRGB);
210 DEFFC(TextAlpha);
211 DEFFC(TextOffX);
212 DEFFC(TextOffY);
213 DEFFC(TextCenter);
214 DEFFC(TextNoCenter);
215 DEFFC(TextScroll);
216 DEFFC(TextPos);
217 DEFFC(TextRate);
218 DEFFC(TextLineHeight);
219 DEFFC(NoMusic);
220 DEFFC(TextScaleX);
221 DEFFC(TextScaleY);
222 DEFFC(TextScale);
223 DEFFC(PlayDemo);
224 DEFFC(Command);
225 DEFFC(ShowMenu);
226 DEFFC(NoShowMenu);
227 
228 /**
229  * Time is measured in seconds.
230  * Colors are floating point and [0..1].
231  *
232  * @todo This data should be pre-processed (i.e., parsed) during module init
233  *       and any symbolic references or other indirections resolved once
234  *       rather than repeatedly during script interpretation.
235  *
236  *       At this time the command names could also be hashed and chained to
237  *       improve performance. -ds
238  */
findCommand(char const * name)239 static command_t const *findCommand(char const *name)
240 {
241     static command_t const commands[] = {
242         // Run Control
243         { "DO",         "", FIC_Do, true, true },
244         { "END",        "", FIC_End },
245         { "IF",         "s", FIC_If }, // if (value-id)
246         { "IFNOT",      "s", FIC_IfNot }, // ifnot (value-id)
247         { "ELSE",       "", FIC_Else },
248         { "GOTO",       "s", FIC_GoTo }, // goto (marker)
249         { "MARKER",     "s", FIC_Marker, true },
250         { "in",         "f", FIC_InTime }, // in (time)
251         { "pause",      "", FIC_Pause },
252         { "tic",        "", FIC_Tic },
253         { "wait",       "f", FIC_Wait }, // wait (time)
254         { "waittext",   "s", FIC_WaitText }, // waittext (id)
255         { "waitanim",   "s", FIC_WaitAnim }, // waitanim (id)
256         { "canskip",    "", FIC_CanSkip },
257         { "noskip",     "", FIC_NoSkip },
258         { "skiphere",   "", FIC_SkipHere, true },
259         { "events",     "", FIC_Events },
260         { "noevents",   "", FIC_NoEvents },
261         { "onkey",      "ss", FIC_OnKey }, // onkey (keyname) (marker)
262         { "unsetkey",   "s", FIC_UnsetKey }, // unsetkey (keyname)
263 
264         // Screen Control
265         { "color",      "fff", FIC_Color }, // color (red) (green) (blue)
266         { "coloralpha", "ffff", FIC_ColorAlpha }, // coloralpha (r) (g) (b) (a)
267         { "flat",       "u(flats:)", FIC_BGMaterial }, // flat (flat-id)
268         { "texture",    "u(textures:)", FIC_BGMaterial }, // texture (texture-id)
269         { "noflat",     "", FIC_NoBGMaterial },
270         { "notexture",  "", FIC_NoBGMaterial },
271         { "offx",       "f", FIC_OffsetX }, // offx (x)
272         { "offy",       "f", FIC_OffsetY }, // offy (y)
273         { "filter",     "ffff", FIC_Filter }, // filter (r) (g) (b) (a)
274 
275         // Audio
276         { "sound",      "s", FIC_Sound }, // sound (snd)
277         { "soundat",    "sf", FIC_SoundAt }, // soundat (snd) (vol:0-1)
278         { "seesound",   "s", FIC_SeeSound }, // seesound (mobjtype)
279         { "diesound",   "s", FIC_DieSound }, // diesound (mobjtype)
280         { "music",      "s", FIC_Music }, // music (musicname)
281         { "musiconce",  "s", FIC_MusicOnce }, // musiconce (musicname)
282         { "nomusic",    "", FIC_NoMusic },
283 
284         // Objects
285         { "del",        "s", FIC_Delete }, // del (wi)
286         { "x",          "sf", FIC_ObjectOffX }, // x (wi) (x)
287         { "y",          "sf", FIC_ObjectOffY }, // y (wi) (y)
288         { "z",          "sf", FIC_ObjectOffZ }, // z (wi) (z)
289         { "sx",         "sf", FIC_ObjectScaleX }, // sx (wi) (x)
290         { "sy",         "sf", FIC_ObjectScaleY }, // sy (wi) (y)
291         { "sz",         "sf", FIC_ObjectScaleZ }, // sz (wi) (z)
292         { "scale",      "sf", FIC_ObjectScale }, // scale (wi) (factor)
293         { "scalexy",    "sff", FIC_ObjectScaleXY }, // scalexy (wi) (x) (y)
294         { "scalexyz",   "sfff", FIC_ObjectScaleXYZ }, // scalexyz (wi) (x) (y) (z)
295         { "rgb",        "sfff", FIC_ObjectRGB }, // rgb (wi) (r) (g) (b)
296         { "alpha",      "sf", FIC_ObjectAlpha }, // alpha (wi) (alpha)
297         { "angle",      "sf", FIC_ObjectAngle }, // angle (wi) (degrees)
298 
299         // Rects
300         { "rect",       "sffff", FIC_Rect }, // rect (hndl) (x) (y) (w) (h)
301         { "fillcolor",  "ssffff", FIC_FillColor }, // fillcolor (wi) (top/bottom/both) (r) (g) (b) (a)
302         { "edgecolor",  "ssffff", FIC_EdgeColor }, // edgecolor (wi) (top/bottom/both) (r) (g) (b) (a)
303 
304         // Pics
305         { "image",      "ss", FIC_Image }, // image (id) (raw-image-lump)
306         { "imageat",    "sffs", FIC_ImageAt }, // imageat (id) (x) (y) (raw)
307         { "ximage",     "ss", FIC_XImage }, // ximage (id) (ext-gfx-filename)
308         { "patch",      "sffs", FIC_Patch }, // patch (id) (x) (y) (patch)
309         { "set",        "ss", FIC_SetPatch }, // set (id) (lump)
310         { "clranim",    "s", FIC_ClearAnim }, // clranim (wi)
311         { "anim",       "ssf", FIC_Anim }, // anim (id) (patch) (time)
312         { "imageanim",  "ssf", FIC_AnimImage }, // imageanim (id) (raw-img) (time)
313         { "picsound",   "ss", FIC_PicSound }, // picsound (id) (sound)
314         { "repeat",     "s", FIC_Repeat }, // repeat (id)
315         { "states",     "ssi", FIC_StateAnim }, // states (id) (state) (count)
316 
317         // Text
318         { "text",       "sffs", FIC_Text }, // text (hndl) (x) (y) (string)
319         { "textdef",    "sffs", FIC_TextFromDef }, // textdef (hndl) (x) (y) (txt-id)
320         { "textlump",   "sffs", FIC_TextFromLump }, // textlump (hndl) (x) (y) (lump)
321         { "settext",    "ss", FIC_SetText }, // settext (id) (newtext)
322         { "settextdef", "ss", FIC_SetTextDef }, // settextdef (id) (txt-id)
323         { "center",     "s", FIC_TextCenter }, // center (id)
324         { "nocenter",   "s", FIC_TextNoCenter }, // nocenter (id)
325         { "scroll",     "sf", FIC_TextScroll }, // scroll (id) (speed)
326         { "pos",        "si", FIC_TextPos }, // pos (id) (pos)
327         { "rate",       "si", FIC_TextRate }, // rate (id) (rate)
328         { "font",       "su", FIC_Font }, // font (id) (font)
329         { "linehgt",    "sf", FIC_TextLineHeight }, // linehgt (hndl) (hgt)
330 
331         // Game Control
332         { "playdemo",   "s", FIC_PlayDemo }, // playdemo (filename)
333         { "cmd",        "s", FIC_Command }, // cmd (console command)
334         { "trigger",    "", FIC_ShowMenu },
335         { "notrigger",  "", FIC_NoShowMenu },
336 
337         // Misc.
338         { "precolor",   "ifff", FIC_PredefinedColor }, // precolor (num) (r) (g) (b)
339         { "prefont",    "iu", FIC_PredefinedFont }, // prefont (num) (font)
340 
341         // Deprecated Font commands
342         { "fonta",      "s", FIC_FontA }, // fonta (id)
343         { "fontb",      "s", FIC_FontB }, // fontb (id)
344 
345         // Deprecated Pic commands
346         { "delpic",     "s", FIC_Delete }, // delpic (wi)
347 
348         // Deprecated Text commands
349         { "deltext",    "s", FIC_DeleteText }, // deltext (wi)
350         { "textrgb",    "sfff", FIC_TextRGB }, // textrgb (id) (r) (g) (b)
351         { "textalpha",  "sf", FIC_TextAlpha }, // textalpha (id) (alpha)
352         { "tx",         "sf", FIC_TextOffX }, // tx (id) (x)
353         { "ty",         "sf", FIC_TextOffY }, // ty (id) (y)
354         { "tsx",        "sf", FIC_TextScaleX }, // tsx (id) (x)
355         { "tsy",        "sf", FIC_TextScaleY }, // tsy (id) (y)
356         { "textscale",  "sf", FIC_TextScale }, // textscale (id) (x) (y)
357 
358         { nullptr, 0, nullptr } // Terminate.
359     };
360     for (size_t i = 0; commands[i].token; ++i)
361     {
362         command_t const *cmd = &commands[i];
363         if (!qstricmp(cmd->token, name))
364         {
365             return cmd;
366         }
367     }
368     return nullptr; // Not found.
369 }
370 
DENG2_PIMPL(FinaleInterpreter)371 DENG2_PIMPL(FinaleInterpreter)
372 {
373     struct Flags
374     {
375         char stopped:1;
376         char suspended:1;
377         char paused:1;
378         char can_skip:1;
379         char eat_events:1; /// Script will eat all input events.
380         char show_menu:1;
381     } flags;
382 
383     finaleid_t id = 0;                  ///< Unique identifier.
384 
385     ddstring_t *script      = nullptr;  ///< The script to be interpreted.
386     char const *scriptBegin = nullptr;  ///< Beginning of the script (after any directive blocks).
387     char const *cp          = nullptr;  ///< Current position in the script.
388     char token[MAX_TOKEN_LENGTH];       ///< Script token read/parse buffer.
389 
390     /// Pages containing the widgets used to visualize the script objects.
391     std::unique_ptr<FinalePageWidget> pages[2];
392 
393     bool cmdExecuted = false;  ///< Set to true after first command is executed.
394 
395     bool skipping    = false;
396     bool lastSkipped = false;
397     bool skipNext    = false;
398     bool gotoEnd     = false;
399     bool gotoSkip    = false;
400     String gotoTarget;
401 
402     int doLevel = 0;  ///< Level of DO-skipping.
403     uint timer  = 0;
404     int wait    = 0;
405     int inTime  = 0;
406 
407     FinaleAnimWidget *waitAnim = nullptr;
408     FinaleTextWidget *waitText = nullptr;
409 
410 #ifdef __CLIENT__
411     struct EventHandler
412     {
413         ddevent_t ev; // Template.
414         String gotoMarker;
415 
416         explicit EventHandler(ddevent_t const *evTemplate = nullptr,
417                               String const &gotoMarker    = "")
418             : gotoMarker(gotoMarker) {
419             std::memcpy(&ev, &evTemplate, sizeof(ev));
420         }
421         EventHandler(EventHandler const &other)
422             : gotoMarker(other.gotoMarker) {
423             std::memcpy(&ev, &other.ev, sizeof(ev));
424         }
425     };
426 
427     typedef QList<EventHandler> EventHandlers;
428     EventHandlers eventHandlers;
429 #endif // __CLIENT__
430 
431     Impl(Public *i, finaleid_t id) : Base(i), id(id)
432     {
433         de::zap(flags);
434         de::zap(token);
435     }
436 
437     ~Impl()
438     {
439         stop();
440         releaseScript();
441     }
442 
443     void initDefaultState()
444     {
445         flags.suspended = false;
446         flags.paused    = false;
447         flags.show_menu = true; // Unhandled events will show a menu.
448         flags.can_skip  = true; // By default skipping is enabled.
449 
450         cmdExecuted = false; // Nothing is drawn until a cmd has been executed.
451         skipping    = false;
452         wait        = 0;     // Not waiting for anything.
453         inTime      = 0;     // Interpolation is off.
454         timer       = 0;
455         gotoSkip    = false;
456         gotoEnd     = false;
457         skipNext    = false;
458         waitText    = nullptr;
459         waitAnim    = nullptr;
460         gotoTarget.clear();
461 
462 #ifdef __CLIENT__
463         eventHandlers.clear();
464 #endif
465     }
466 
467     void releaseScript()
468     {
469         Str_Delete(script); script = nullptr;
470         scriptBegin = nullptr;
471         cp          = nullptr;
472     }
473 
474     void stop()
475     {
476         if (flags.stopped) return;
477 
478         flags.stopped = true;
479         LOGDEV_SCR_MSG("Finale End - id:%i '%.30s'") << id << scriptBegin;
480 
481 #ifdef __SERVER__
482         if (::isServer && !(FI_ScriptFlags(id) & FF_LOCAL))
483         {
484             // Tell clients to stop the finale.
485             Sv_Finale(id, FINF_END, 0);
486         }
487 #endif
488 
489         // Any hooks?
490         DoomsdayApp::plugins().callAllHooks(HOOK_FINALE_SCRIPT_STOP, id);
491     }
492 
493     bool atEnd() const
494     {
495         DENG2_ASSERT(script);
496         return (cp - Str_Text(script)) >= Str_Length(script);
497     }
498 
499     void findBegin()
500     {
501         char const *tok;
502         while (!gotoEnd && 0 != (tok = nextToken()) && qstricmp(tok, "{")) {}
503     }
504 
505     void findEnd()
506     {
507         char const *tok;
508         while (!gotoEnd && 0 != (tok = nextToken()) && qstricmp(tok, "}")) {}
509     }
510 
511     char const *nextToken()
512     {
513         // Skip whitespace.
514         while (!atEnd() && isspace(*cp)) { cp++; }
515 
516         // Have we reached the end?
517         if (atEnd()) return nullptr;
518 
519         char *out = token;
520         if (*cp == '"') // A string?
521         {
522             for (cp++; !atEnd(); cp++)
523             {
524                 if (*cp == '"')
525                 {
526                     cp++;
527                     // Convert double quotes to single ones.
528                     if (*cp != '"') break;
529                 }
530                 *out++ = *cp;
531             }
532         }
533         else
534         {
535             while (!isspace(*cp) && !atEnd()) { *out++ = *cp++; }
536         }
537         *out++ = 0;
538 
539         return token;
540     }
541 
542     /// @return  @c true if the end of the script was reached.
543     bool executeNextCommand()
544     {
545         if (char const *tok = nextToken())
546         {
547             executeCommand(tok, FID_NORMAL);
548             // Time to unhide the object page(s)?
549             if (cmdExecuted)
550             {
551                 pages[Anims]->makeVisible();
552                 pages[Texts]->makeVisible();
553             }
554             return false;
555         }
556         return true;
557     }
558 
559     static inline char const *findDefaultValueEnd(char const *str)
560     {
561         char const *defaultValueEnd;
562         for (defaultValueEnd = str; defaultValueEnd && *defaultValueEnd != ')'; defaultValueEnd++)
563         {}
564         DENG2_ASSERT(defaultValueEnd < str + qstrlen(str));
565         return defaultValueEnd;
566     }
567 
568     static char const *nextOperand(char const *operands)
569     {
570         if (operands && operands[0])
571         {
572             // Some operands might include a default value.
573             int len = qstrlen(operands);
574             if (len > 1 && operands[1] == '(')
575             {
576                 // A default value begins. Find the end.
577                 return findDefaultValueEnd(operands + 2) + 1;
578             }
579             return operands + 1;
580         }
581         return nullptr; // No more operands.
582     }
583 
584     /// @return Total number of command operands in the control string @a operands.
585     static int countCommandOperands(char const *operands)
586     {
587         int count = 0;
588         while (operands && operands[0])
589         {
590             count += 1;
591             operands = nextOperand(operands);
592         }
593         return count;
594     }
595 
596     /**
597      * Prepare the command operands from the script. If successfull, a ptr to a new
598      * vector of @c fi_operand_t objects is returned. Ownership of the vector is
599      * given to the caller.
600      *
601      * @return  Array of @c fi_operand_t else @c nullptr. Must be free'd with M_Free().
602      */
603     fi_operand_t *prepareCommandOperands(command_t const *cmd, int *count)
604     {
605         DENG2_ASSERT(cmd);
606 
607         char const *origCursorPos = cp;
608         int const operandCount    = countCommandOperands(cmd->operands);
609         if (operandCount <= 0) return nullptr;
610 
611         fi_operand_t *operands = (fi_operand_t *) M_Malloc(sizeof(*operands) * operandCount);
612         char const *opRover    = cmd->operands;
613         for (fi_operand_t *op = operands; opRover && opRover[0]; opRover = nextOperand(opRover), op++)
614         {
615             char const charCode = *opRover;
616 
617             op->type = operandTypeForCharCode(charCode);
618             bool const opHasDefaultValue = (opRover < cmd->operands + (qstrlen(cmd->operands) - 2) && opRover[1] == '(');
619             bool const haveValue         = !!nextToken();
620 
621             if (!haveValue && !opHasDefaultValue)
622             {
623                 cp = origCursorPos;
624 
625                 if (operands) M_Free(operands);
626                 if (count) *count = 0;
627 
628                 App_Error("prepareCommandOperands: Too few operands for command '%s'.\n", cmd->token);
629                 return 0; // Unreachable.
630             }
631 
632             switch (op->type)
633             {
634             case FVT_INT: {
635                 char const *valueStr = haveValue? token : nullptr;
636                 if (!valueStr)
637                 {
638                     // Use the default.
639                     int const defaultValueLen = (findDefaultValueEnd(opRover + 2) - opRover) - 1;
640                     AutoStr *defaultValue     = Str_PartAppend(AutoStr_NewStd(), opRover + 2, 0, defaultValueLen);
641                     valueStr = Str_Text(defaultValue);
642                 }
643                 op->data.integer = strtol(valueStr, nullptr, 0);
644                 break; }
645 
646             case FVT_FLOAT: {
647                 char const *valueStr = haveValue? token : nullptr;
648                 if (!valueStr)
649                 {
650                     // Use the default.
651                     int const defaultValueLen = (findDefaultValueEnd(opRover + 2) - opRover) - 1;
652                     AutoStr *defaultValue     = Str_PartAppend(AutoStr_NewStd(), opRover + 2, 0, defaultValueLen);
653                     valueStr = Str_Text(defaultValue);
654                 }
655                 op->data.flt = strtod(valueStr, nullptr);
656                 break; }
657 
658             case FVT_STRING: {
659                 char const *valueStr = haveValue? token : nullptr;
660                 int valueLen         = haveValue? qstrlen(token) : 0;
661                 if (!valueStr)
662                 {
663                     // Use the default.
664                     int const defaultValueLen = (findDefaultValueEnd(opRover + 2) - opRover) - 1;
665                     AutoStr *defaultValue     = Str_PartAppend(AutoStr_NewStd(), opRover + 2, 0, defaultValueLen);
666                     valueStr = Str_Text(defaultValue);
667                     valueLen = defaultValueLen;
668                 }
669                 op->data.cstring = (char *)M_Malloc(valueLen + 1);
670                 qstrcpy((char *)op->data.cstring, valueStr);
671                 break; }
672 
673             case FVT_URI: {
674                 uri_s *uri = Uri_New();
675                 // Always apply the default as it may contain a default scheme.
676                 if (opHasDefaultValue)
677                 {
678                     int const defaultValueLen = (findDefaultValueEnd(opRover + 2) - opRover) - 1;
679                     AutoStr *defaultValue     = Str_PartAppend(AutoStr_NewStd(), opRover + 2, 0, defaultValueLen);
680                     Uri_SetUri2(uri, Str_Text(defaultValue), RC_NULL);
681                 }
682                 if (haveValue)
683                 {
684                     Uri_SetUri2(uri, token, RC_NULL);
685                 }
686                 op->data.uri = uri;
687                 break; }
688 
689             default: break; // Unreachable.
690             }
691         }
692 
693         if (count) *count = operandCount;
694 
695         return operands;
696     }
697 
698     bool skippingCommand(command_t const *cmd)
699     {
700         DENG2_ASSERT(cmd);
701         if ((skipNext && !cmd->flags.when_condition_skipping) ||
702            ((skipping || gotoSkip) && !cmd->flags.when_skipping))
703         {
704             // While not DO-skipping, the condskip has now been done.
705             if (!doLevel)
706             {
707                 if (skipNext)
708                     lastSkipped = true;
709                 skipNext = false;
710             }
711             return true;
712         }
713         return false;
714     }
715 
716     /**
717      * Execute one (the next) command, advance script cursor.
718      */
719     bool executeCommand(char const *commandString, int directive)
720     {
721         DENG2_ASSERT(commandString);
722         bool didSkip = false;
723 
724         // Semicolon terminates DO-blocks.
725         if (!qstrcmp(commandString, ";"))
726         {
727             if (doLevel > 0)
728             {
729                 if (--doLevel == 0)
730                 {
731                     // The DO-skip has been completed.
732                     skipNext    = false;
733                     lastSkipped = true;
734                 }
735             }
736             return true; // Success!
737         }
738 
739         // We're now going to execute a command.
740         cmdExecuted = true;
741 
742         // Is this a command we know how to execute?
743         if (command_t const *cmd = findCommand(commandString))
744         {
745             bool const requiredOperands = (cmd->operands && cmd->operands[0]);
746 
747             // Is this command supported for this directive?
748             if (directive != 0 && cmd->excludeDirectives != 0 &&
749                (cmd->excludeDirectives & directive) == 0)
750                 App_Error("executeCommand: Command \"%s\" is not supported for directive %i.",
751                           cmd->token, directive);
752 
753             // Check that there are enough operands.
754             /// @todo Dynamic memory allocation during script interpretation should be avoided.
755             int numOps        = 0;
756             fi_operand_t *ops = nullptr;
757             if (!requiredOperands || (ops = prepareCommandOperands(cmd, &numOps)))
758             {
759                 // Should we skip this command?
760                 if (!(didSkip = skippingCommand(cmd)))
761                 {
762                     // Execute forthwith!
763                     cmd->func(*cmd, ops, self());
764                 }
765             }
766 
767             if (!didSkip)
768             {
769                 if (gotoEnd)
770                 {
771                     wait = 1;
772                 }
773                 else
774                 {
775                     // Now we've executed the latest command.
776                     lastSkipped = false;
777                 }
778             }
779 
780             if (ops)
781             {
782                 for (int i = 0; i < numOps; ++i)
783                 {
784                     fi_operand_t *op = &ops[i];
785                     switch (op->type)
786                     {
787                     case FVT_STRING: M_Free((char *)op->data.cstring); break;
788                     case FVT_URI:    Uri_Delete(op->data.uri);         break;
789 
790                     default: break;
791                     }
792                 }
793                 M_Free(ops);
794             }
795         }
796 
797         return !didSkip;
798     }
799 
800     static inline PageIndex choosePageFor(FinaleWidget &widget)
801     {
802         return is<FinaleAnimWidget>(widget)? Anims : Texts;
803     }
804 
805     static inline PageIndex choosePageFor(fi_obtype_e type)
806     {
807         return type == FI_ANIM? Anims : Texts;
808     }
809 
810     FinaleWidget *locateWidget(fi_obtype_e type, String const &name)
811     {
812         if (!name.isEmpty())
813         {
814             FinalePageWidget::Children const &children = pages[choosePageFor(type)]->children();
815             for (FinaleWidget *widget : children)
816             {
817                 if (!widget->name().compareWithoutCase(name))
818                 {
819                     return widget;
820                 }
821             }
822         }
823         return nullptr; // Not found.
824     }
825 
826     FinaleWidget *makeWidget(fi_obtype_e type, String const &name)
827     {
828         if (type == FI_ANIM)
829         {
830             return new FinaleAnimWidget(name);
831         }
832         if (type == FI_TEXT)
833         {
834             auto *wi = new FinaleTextWidget(name);
835             // Configure the text to use the Page's font and color.
836             wi->setPageFont(1)
837                .setPageColor(1);
838             return wi;
839         }
840         return nullptr;
841     }
842 
843 #if __CLIENT__
844     EventHandler *findEventHandler(ddevent_t const &ev) const
845     {
846         for (EventHandler const &eh : eventHandlers)
847         {
848             if (eh.ev.device != ev.device && eh.ev.type != ev.type)
849                 continue;
850 
851             switch (eh.ev.type)
852             {
853             case E_TOGGLE:
854                 if (eh.ev.toggle.id != ev.toggle.id)
855                     continue;
856                 break;
857 
858             case E_AXIS:
859                 if (eh.ev.axis.id != ev.axis.id)
860                     continue;
861                 break;
862 
863             case E_ANGLE:
864                 if (eh.ev.angle.id != ev.angle.id)
865                     continue;
866                 break;
867 
868             default:
869                 App_Error("Internal error: Invalid event template (type=%i) in finale event handler.", int(eh.ev.type));
870             }
871             return &const_cast<EventHandler &>(eh);
872         }
873         return nullptr;
874     }
875 #endif // __CLIENT__
876 };
877 
FinaleInterpreter(finaleid_t id)878 FinaleInterpreter::FinaleInterpreter(finaleid_t id) : d(new Impl(this, id))
879 {}
880 
id() const881 finaleid_t FinaleInterpreter::id() const
882 {
883     return d->id;
884 }
885 
loadScript(char const * script)886 void FinaleInterpreter::loadScript(char const *script)
887 {
888     DENG2_ASSERT(script && script[0]);
889 
890     d->pages[Anims].reset(new FinalePageWidget);
891     d->pages[Texts].reset(new FinalePageWidget);
892 
893     // Hide our pages until command interpretation begins.
894     d->pages[Anims]->makeVisible(false);
895     d->pages[Texts]->makeVisible(false);
896 
897     // Take a copy of the script.
898     d->script      = Str_Set(Str_NewStd(), script);
899     d->scriptBegin = Str_Text(d->script);
900     d->cp          = Str_Text(d->script); // Init cursor.
901 
902     d->initDefaultState();
903 
904     // Locate the start of the script.
905     if (d->nextToken())
906     {
907         // The start of the script may include blocks of event directive
908         // commands. These commands are automatically executed in response
909         // to their associated events.
910         if (!qstricmp(d->token, "OnLoad"))
911         {
912             d->findBegin();
913             forever
914             {
915                 d->nextToken();
916                 if (!qstricmp(d->token, "}"))
917                     goto end_read;
918 
919                 if (!d->executeCommand(d->token, FID_ONLOAD))
920                     App_Error("FinaleInterpreter::LoadScript: Unknown error"
921                               "occured executing directive \"OnLoad\".");
922             }
923             d->findEnd();
924 end_read:
925 
926             // Skip over any trailing whitespace to position the read cursor
927             // on the first token.
928             while (*d->cp && isspace(*d->cp)) { d->cp++; }
929 
930             // Calculate the new script entry point and restore default state.
931             d->scriptBegin = Str_Text(d->script) + (d->cp - Str_Text(d->script));
932             d->cp          = d->scriptBegin;
933             d->initDefaultState();
934         }
935     }
936 
937     // Any hooks?
938     DoomsdayApp::plugins().callAllHooks(HOOK_FINALE_SCRIPT_BEGIN, d->id);
939 }
940 
resume()941 void FinaleInterpreter::resume()
942 {
943     if (!d->flags.suspended) return;
944 
945     d->flags.suspended = false;
946     d->pages[Anims]->pause(false);
947     d->pages[Texts]->pause(false);
948     // Do we need to unhide any pages?
949     if (d->cmdExecuted)
950     {
951         d->pages[Anims]->makeVisible();
952         d->pages[Texts]->makeVisible();
953     }
954 }
955 
suspend()956 void FinaleInterpreter::suspend()
957 {
958     LOG_AS("FinaleInterpreter");
959 
960     if (d->flags.suspended) return;
961 
962     d->flags.suspended = true;
963     // While suspended, all pages will be paused and hidden.
964     d->pages[Anims]->pause();
965     d->pages[Anims]->makeVisible(false);
966     d->pages[Texts]->pause();
967     d->pages[Texts]->makeVisible(false);
968 }
969 
terminate()970 void FinaleInterpreter::terminate()
971 {
972     d->stop();
973 #ifdef __CLIENT__
974     d->eventHandlers.clear();
975 #endif
976     d->releaseScript();
977 }
978 
isMenuTrigger() const979 bool FinaleInterpreter::isMenuTrigger() const
980 {
981     if (d->flags.paused || d->flags.can_skip)
982     {
983         // We want events to be used for unpausing/skipping.
984         return false;
985     }
986     // If skipping is not allowed, we should show the menu, too.
987     return (d->flags.show_menu != 0);
988 }
989 
isSuspended() const990 bool FinaleInterpreter::isSuspended() const
991 {
992     return (d->flags.suspended != 0);
993 }
994 
allowSkip(bool yes)995 void FinaleInterpreter::allowSkip(bool yes)
996 {
997     d->flags.can_skip = yes;
998 }
999 
canSkip() const1000 bool FinaleInterpreter::canSkip() const
1001 {
1002     return (d->flags.can_skip != 0);
1003 }
1004 
commandExecuted() const1005 bool FinaleInterpreter::commandExecuted() const
1006 {
1007     return d->cmdExecuted;
1008 }
1009 
runOneTick(FinaleInterpreter & fi)1010 static bool runOneTick(FinaleInterpreter &fi)
1011 {
1012     ddhook_finale_script_ticker_paramaters_t parm;
1013     de::zap(parm);
1014     parm.runTick = true;
1015     parm.canSkip = fi.canSkip();
1016     DoomsdayApp::plugins().callAllHooks(HOOK_FINALE_SCRIPT_TICKER, fi.id(), &parm);
1017     return parm.runTick;
1018 }
1019 
runTicks(timespan_t timeDelta,bool processCommands)1020 bool FinaleInterpreter::runTicks(timespan_t timeDelta, bool processCommands)
1021 {
1022     LOG_AS("FinaleInterpreter");
1023 
1024     // All pages tick unless paused.
1025     page(Anims).runTicks(timeDelta);
1026     page(Texts).runTicks(timeDelta);
1027 
1028     if (!processCommands)   return false;
1029     if (d->flags.stopped)   return false;
1030     if (d->flags.suspended) return false;
1031 
1032     d->timer++;
1033 
1034     if (!runOneTick(*this))
1035         return false;
1036 
1037     // If waiting do not execute commands.
1038     if (d->wait && --d->wait)
1039         return false;
1040 
1041     // If paused there is nothing further to do.
1042     if (d->flags.paused)
1043         return false;
1044 
1045     // If waiting on a text to finish typing, do nothing.
1046     if (d->waitText)
1047     {
1048         if (!d->waitText->animationComplete())
1049             return false;
1050 
1051         d->waitText = nullptr;
1052     }
1053 
1054     // Waiting for an animation to reach its end?
1055     if (d->waitAnim)
1056     {
1057         if (!d->waitAnim->animationComplete())
1058             return false;
1059 
1060         d->waitAnim = nullptr;
1061     }
1062 
1063     // Execute commands until a wait time is set or we reach the end of
1064     // the script. If the end is reached, the finale really ends (terminates).
1065     bool foundEnd = false;
1066     while (!d->gotoEnd && !d->wait && !d->waitText && !d->waitAnim && !foundEnd)
1067     {
1068         foundEnd = d->executeNextCommand();
1069     }
1070     return (d->gotoEnd || (foundEnd && d->flags.can_skip));
1071 }
1072 
skip()1073 bool FinaleInterpreter::skip()
1074 {
1075     LOG_AS("FinaleInterpreter");
1076 
1077     if (d->waitText && d->flags.can_skip && !d->flags.paused)
1078     {
1079         // Instead of skipping, just complete the text.
1080         d->waitText->accelerate();
1081         return true;
1082     }
1083 
1084     // Stop waiting for objects.
1085     d->waitText = nullptr;
1086     d->waitAnim = nullptr;
1087     if (d->flags.paused)
1088     {
1089         d->flags.paused = false;
1090         d->wait = 0;
1091         return true;
1092     }
1093 
1094     if (d->flags.can_skip)
1095     {
1096         d->skipping = true; // Start skipping ahead.
1097         d->wait     = 0;
1098         return true;
1099     }
1100 
1101     return (d->flags.eat_events != 0);
1102 }
1103 
skipToMarker(String const & marker)1104 bool FinaleInterpreter::skipToMarker(String const &marker)
1105 {
1106     LOG_AS("FinaleInterpreter");
1107 
1108     if (marker.isEmpty()) return false;
1109 
1110     d->gotoTarget = marker;
1111     d->gotoSkip   = true;    // Start skipping until the marker is found.
1112     d->wait       = 0;       // Stop any waiting.
1113     d->waitText   = nullptr;
1114     d->waitAnim   = nullptr;
1115 
1116     // Rewind the script so we can jump anywhere.
1117     d->cp = d->scriptBegin;
1118     return true;
1119 }
1120 
skipInProgress() const1121 bool FinaleInterpreter::skipInProgress() const
1122 {
1123     return d->skipNext;
1124 }
1125 
lastSkipped() const1126 bool FinaleInterpreter::lastSkipped() const
1127 {
1128     return d->lastSkipped;
1129 }
1130 
handleEvent(ddevent_t const & ev)1131 int FinaleInterpreter::handleEvent(ddevent_t const &ev)
1132 {
1133     LOG_AS("FinaleInterpreter");
1134 
1135     if (d->flags.suspended)
1136         return false;
1137 
1138     // During the first ~second disallow all events/skipping.
1139     if (d->timer < 20)
1140         return false;
1141 
1142     if (!::isClient)
1143     {
1144 #ifdef __CLIENT__
1145         // Any handlers for this event?
1146         if (IS_KEY_DOWN(&ev))
1147         {
1148             if (Impl::EventHandler *eh = d->findEventHandler(ev))
1149             {
1150                 skipToMarker(eh->gotoMarker);
1151 
1152                 // Never eat up events.
1153                 if (IS_TOGGLE_UP(&ev)) return false;
1154 
1155                 return (d->flags.eat_events != 0);
1156             }
1157         }
1158 #endif
1159     }
1160 
1161     // If we can't skip, there's no interaction of any kind.
1162     if (!d->flags.can_skip && !d->flags.paused)
1163         return false;
1164 
1165     // We are only interested in key/button down presses.
1166     if (!IS_TOGGLE_DOWN(&ev))
1167         return false;
1168 
1169 #ifdef __CLIENT__
1170     if (::isClient)
1171     {
1172         // Request skip from the server.
1173         Cl_RequestFinaleSkip();
1174         return true;
1175     }
1176 #endif
1177 #ifdef __SERVER__
1178     // Tell clients to skip.
1179     Sv_Finale(d->id, FINF_SKIP, 0);
1180 #endif
1181     return skip();
1182 }
1183 
1184 #ifdef __CLIENT__
addEventHandler(ddevent_t const & evTemplate,String const & gotoMarker)1185 void FinaleInterpreter::addEventHandler(ddevent_t const &evTemplate, String const &gotoMarker)
1186 {
1187     // Does a handler already exist for this?
1188     if (d->findEventHandler(evTemplate)) return;
1189 
1190     d->eventHandlers.append(Impl::EventHandler(&evTemplate, gotoMarker));
1191 }
1192 
removeEventHandler(ddevent_t const & evTemplate)1193 void FinaleInterpreter::removeEventHandler(ddevent_t const &evTemplate)
1194 {
1195     if (Impl::EventHandler *eh = d->findEventHandler(evTemplate))
1196     {
1197         int index = 0;
1198         while (&d->eventHandlers.at(index) != eh) { index++; }
1199         d->eventHandlers.removeAt(index);
1200     }
1201 }
1202 #endif // __CLIENT__
1203 
page(PageIndex index)1204 FinalePageWidget &FinaleInterpreter::page(PageIndex index)
1205 {
1206     if (index >= Anims && index <= Texts)
1207     {
1208         DENG2_ASSERT(d->pages[index]);
1209         return *d->pages[index];
1210     }
1211     throw MissingPageError("FinaleInterpreter::page", "Unknown page #" + String::number(int(index)));
1212 }
1213 
page(PageIndex index) const1214 FinalePageWidget const &FinaleInterpreter::page(PageIndex index) const
1215 {
1216     return const_cast<FinalePageWidget const &>(const_cast<FinaleInterpreter *>(this)->page(index));
1217 }
1218 
tryFindWidget(String const & name)1219 FinaleWidget *FinaleInterpreter::tryFindWidget(String const &name)
1220 {
1221     // Perhaps an Anim?
1222     if (FinaleWidget *found = d->locateWidget(FI_ANIM, name))
1223     {
1224         return found;
1225     }
1226     // Perhaps a Text?
1227     if (FinaleWidget *found = d->locateWidget(FI_TEXT, name))
1228     {
1229         return found;
1230     }
1231     return nullptr;
1232 }
1233 
findWidget(fi_obtype_e type,String const & name)1234 FinaleWidget &FinaleInterpreter::findWidget(fi_obtype_e type, String const &name)
1235 {
1236     if (FinaleWidget *foundWidget = d->locateWidget(type, name))
1237     {
1238         return *foundWidget;
1239     }
1240     throw MissingWidgetError("FinaleInterpeter::findWidget", "Failed locating widget for name:'" + name + "'");
1241 }
1242 
findOrCreateWidget(fi_obtype_e type,String const & name)1243 FinaleWidget &FinaleInterpreter::findOrCreateWidget(fi_obtype_e type, String const &name)
1244 {
1245     DENG2_ASSERT(type >= FI_ANIM && type <= FI_TEXT);
1246     DENG2_ASSERT(!name.isEmpty());
1247     if (FinaleWidget *foundWidget = d->locateWidget(type, name))
1248     {
1249         return *foundWidget;
1250     }
1251 
1252     FinaleWidget *newWidget = d->makeWidget(type, name);
1253     if (!newWidget) throw Error("FinaleInterpreter::findOrCreateWidget", "Failed making widget for type:" + String::number(int(type)));
1254 
1255     return *page(d->choosePageFor(*newWidget)).addChild(newWidget);
1256 }
1257 
beginDoSkipMode()1258 void FinaleInterpreter::beginDoSkipMode()
1259 {
1260     if (!skipInProgress()) return;
1261 
1262     // A conditional skip has been issued.
1263     // We'll go into DO-skipping mode. skipnext won't be cleared
1264     // until the matching semicolon is found.
1265     d->doLevel++;
1266 }
1267 
gotoEnd()1268 void FinaleInterpreter::gotoEnd()
1269 {
1270     d->gotoEnd = true;
1271 }
1272 
pause()1273 void FinaleInterpreter::pause()
1274 {
1275     d->flags.paused = true;
1276     wait(1);
1277 }
1278 
wait(int ticksToWait)1279 void FinaleInterpreter::wait(int ticksToWait)
1280 {
1281     d->wait = ticksToWait;
1282 }
1283 
foundSkipHere()1284 void FinaleInterpreter::foundSkipHere()
1285 {
1286     d->skipping = false;
1287 }
1288 
foundSkipMarker(String const & marker)1289 void FinaleInterpreter::foundSkipMarker(String const &marker)
1290 {
1291     // Does it match the current goto torget?
1292     if (!d->gotoTarget.compareWithoutCase(marker))
1293     {
1294         d->gotoSkip = false;
1295     }
1296 }
1297 
inTime() const1298 int FinaleInterpreter::inTime() const
1299 {
1300     return d->inTime;
1301 }
1302 
setInTime(int seconds)1303 void FinaleInterpreter::setInTime(int seconds)
1304 {
1305     d->inTime = seconds;
1306 }
1307 
setHandleEvents(bool yes)1308 void FinaleInterpreter::setHandleEvents(bool yes)
1309 {
1310     d->flags.eat_events = yes;
1311 }
1312 
setShowMenu(bool yes)1313 void FinaleInterpreter::setShowMenu(bool yes)
1314 {
1315     d->flags.show_menu = yes;
1316 }
1317 
setSkip(bool allowed)1318 void FinaleInterpreter::setSkip(bool allowed)
1319 {
1320     d->flags.can_skip = allowed;
1321 }
1322 
setSkipNext(bool yes)1323 void FinaleInterpreter::setSkipNext(bool yes)
1324 {
1325    d->skipNext = yes;
1326 }
1327 
setWaitAnim(FinaleAnimWidget * newWaitAnim)1328 void FinaleInterpreter::setWaitAnim(FinaleAnimWidget *newWaitAnim)
1329 {
1330     d->waitAnim = newWaitAnim;
1331 }
1332 
setWaitText(FinaleTextWidget * newWaitText)1333 void FinaleInterpreter::setWaitText(FinaleTextWidget *newWaitText)
1334 {
1335     d->waitText = newWaitText;
1336 }
1337 
1338 /// @note This command is called even when condition-skipping.
DEFFC(Do)1339 DEFFC(Do)
1340 {
1341     DENG2_UNUSED2(cmd, ops);
1342     fi.beginDoSkipMode();
1343 }
1344 
DEFFC(End)1345 DEFFC(End)
1346 {
1347     DENG2_UNUSED2(cmd, ops);
1348     fi.gotoEnd();
1349 }
1350 
changePageBackground(FinalePageWidget & page,world::Material * newMaterial)1351 static void changePageBackground(FinalePageWidget &page, world::Material *newMaterial)
1352 {
1353     // If the page does not yet have a background set we must setup the color+alpha.
1354     if (newMaterial && !page.backgroundMaterial())
1355     {
1356         page.setBackgroundTopColorAndAlpha   (Vector4f(1, 1, 1, 1))
1357             .setBackgroundBottomColorAndAlpha(Vector4f(1, 1, 1, 1));
1358     }
1359     page.setBackgroundMaterial(newMaterial);
1360 }
1361 
DEFFC(BGMaterial)1362 DEFFC(BGMaterial)
1363 {
1364     DENG2_UNUSED(cmd);
1365 
1366     // First attempt to resolve as a Values URI (which defines the material URI).
1367     world::Material *material = nullptr;
1368     try
1369     {
1370         if (ded_value_t *value = DED_Definitions()->getValueByUri(*reinterpret_cast<de::Uri const *>(OP_URI(0))))
1371         {
1372             material = &world::Materials::get().material(de::makeUri(value->text));
1373         }
1374         else
1375         {
1376             material = &world::Materials::get().material(*reinterpret_cast<de::Uri const *>(OP_URI(0)));
1377         }
1378     }
1379     catch (world::MaterialManifest::MissingMaterialError const &)
1380     {} // Ignore this error.
1381     catch (Resources::MissingResourceManifestError const &)
1382     {} // Ignore this error.
1383 
1384     changePageBackground(fi.page(FinaleInterpreter::Anims), material);
1385 }
1386 
DEFFC(NoBGMaterial)1387 DEFFC(NoBGMaterial)
1388 {
1389     DENG2_UNUSED2(cmd, ops);
1390     changePageBackground(fi.page(FinaleInterpreter::Anims), 0);
1391 }
1392 
DEFFC(InTime)1393 DEFFC(InTime)
1394 {
1395     DENG2_UNUSED(cmd);
1396     fi.setInTime(FRACSECS_TO_TICKS(OP_FLOAT(0)));
1397 }
1398 
DEFFC(Tic)1399 DEFFC(Tic)
1400 {
1401     DENG2_UNUSED2(cmd, ops);
1402     fi.wait();
1403 }
1404 
DEFFC(Wait)1405 DEFFC(Wait)
1406 {
1407     DENG2_UNUSED(cmd);
1408     fi.wait(FRACSECS_TO_TICKS(OP_FLOAT(0)));
1409 }
1410 
DEFFC(WaitText)1411 DEFFC(WaitText)
1412 {
1413     DENG2_UNUSED(cmd);
1414     fi.setWaitText(&fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>());
1415 }
1416 
DEFFC(WaitAnim)1417 DEFFC(WaitAnim)
1418 {
1419     DENG2_UNUSED(cmd);
1420     fi.setWaitAnim(&fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as<FinaleAnimWidget>());
1421 }
1422 
DEFFC(Color)1423 DEFFC(Color)
1424 {
1425     DENG2_UNUSED(cmd);
1426     fi.page(FinaleInterpreter::Anims)
1427             .setBackgroundTopColor   (Vector3f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2)), fi.inTime())
1428             .setBackgroundBottomColor(Vector3f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2)), fi.inTime());
1429 }
1430 
DEFFC(ColorAlpha)1431 DEFFC(ColorAlpha)
1432 {
1433     DENG2_UNUSED(cmd);
1434     fi.page(FinaleInterpreter::Anims)
1435             .setBackgroundTopColorAndAlpha   (Vector4f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime())
1436             .setBackgroundBottomColorAndAlpha(Vector4f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime());
1437 }
1438 
DEFFC(Pause)1439 DEFFC(Pause)
1440 {
1441     DENG2_UNUSED2(cmd, ops);
1442     fi.pause();
1443 }
1444 
DEFFC(CanSkip)1445 DEFFC(CanSkip)
1446 {
1447     DENG2_UNUSED2(cmd, ops);
1448     fi.setSkip(true);
1449 }
1450 
DEFFC(NoSkip)1451 DEFFC(NoSkip)
1452 {
1453     DENG2_UNUSED2(cmd, ops);
1454     fi.setSkip(false);
1455 }
1456 
DEFFC(SkipHere)1457 DEFFC(SkipHere)
1458 {
1459     DENG2_UNUSED2(cmd, ops);
1460     fi.foundSkipHere();
1461 }
1462 
DEFFC(Events)1463 DEFFC(Events)
1464 {
1465     DENG2_UNUSED2(cmd, ops);
1466     fi.setHandleEvents();
1467 }
1468 
DEFFC(NoEvents)1469 DEFFC(NoEvents)
1470 {
1471     DENG2_UNUSED2(cmd, ops);
1472     fi.setHandleEvents(false);
1473 }
1474 
DEFFC(OnKey)1475 DEFFC(OnKey)
1476 {
1477 #ifdef __CLIENT__
1478     DENG2_UNUSED(cmd);
1479 
1480     // Construct a template event for this handler.
1481     ddevent_t ev; de::zap(ev);
1482     ev.device = IDEV_KEYBOARD;
1483     ev.type   = E_TOGGLE;
1484     ev.toggle.id    = DD_GetKeyCode(OP_CSTRING(0));
1485     ev.toggle.state = ETOG_DOWN;
1486 
1487     fi.addEventHandler(ev, OP_CSTRING(1));
1488 #else
1489     DENG2_UNUSED3(cmd, ops, fi);
1490 #endif
1491 }
1492 
DEFFC(UnsetKey)1493 DEFFC(UnsetKey)
1494 {
1495 #ifdef __CLIENT__
1496     DENG2_UNUSED(cmd);
1497 
1498     // Construct a template event for what we want to "unset".
1499     ddevent_t ev; de::zap(ev);
1500     ev.device = IDEV_KEYBOARD;
1501     ev.type   = E_TOGGLE;
1502     ev.toggle.id    = DD_GetKeyCode(OP_CSTRING(0));
1503     ev.toggle.state = ETOG_DOWN;
1504 
1505     fi.removeEventHandler(ev);
1506 #else
1507     DENG2_UNUSED3(cmd, ops, fi);
1508 #endif
1509 }
1510 
DEFFC(If)1511 DEFFC(If)
1512 {
1513     DENG2_UNUSED2(cmd, ops);
1514     LOG_AS("FIC_If");
1515 
1516     char const *token = OP_CSTRING(0);
1517     bool val          = false;
1518 
1519     // Built-in conditions.
1520     if (!qstricmp(token, "netgame"))
1521     {
1522         val = netGame;
1523     }
1524     else if (!qstrnicmp(token, "mode:", 5))
1525     {
1526         if (App_GameLoaded())
1527             val = !String(token + 5).compareWithoutCase(App_CurrentGame().id());
1528         else
1529             val = 0;
1530     }
1531     // Any hooks?
1532     else if (Plug_CheckForHook(HOOK_FINALE_EVAL_IF))
1533     {
1534         ddhook_finale_script_evalif_paramaters_t p; de::zap(p);
1535         p.token     = token;
1536         p.returnVal = 0;
1537         if (DoomsdayApp::plugins().callAllHooks(HOOK_FINALE_EVAL_IF, fi.id(), (void *) &p))
1538         {
1539             val = p.returnVal;
1540             LOG_SCR_XVERBOSE("HOOK_FINALE_EVAL_IF: %s => %i", token << val);
1541         }
1542         else
1543         {
1544             LOG_SCR_XVERBOSE("HOOK_FINALE_EVAL_IF: no hook (for %s)", token);
1545         }
1546     }
1547     else
1548     {
1549         LOG_SCR_WARNING("Unknown condition '%s'") << token;
1550     }
1551 
1552     // Skip the next command if the value is false.
1553     fi.setSkipNext(!val);
1554 }
1555 
DEFFC(IfNot)1556 DEFFC(IfNot)
1557 {
1558     FIC_If(cmd, ops, fi);
1559     fi.setSkipNext(!fi.skipInProgress());
1560 }
1561 
1562 /// @note The only time the ELSE condition does not skip is immediately after a skip.
DEFFC(Else)1563 DEFFC(Else)
1564 {
1565     DENG2_UNUSED2(cmd, ops);
1566     fi.setSkipNext(!fi.lastSkipped());
1567 }
1568 
DEFFC(GoTo)1569 DEFFC(GoTo)
1570 {
1571     DENG2_UNUSED(cmd);
1572     fi.skipToMarker(OP_CSTRING(0));
1573 }
1574 
DEFFC(Marker)1575 DEFFC(Marker)
1576 {
1577     DENG2_UNUSED(cmd);
1578     fi.foundSkipMarker(OP_CSTRING(0));
1579 }
1580 
DEFFC(Delete)1581 DEFFC(Delete)
1582 {
1583     DENG2_UNUSED(cmd);
1584     delete fi.tryFindWidget(OP_CSTRING(0));
1585 }
1586 
DEFFC(Image)1587 DEFFC(Image)
1588 {
1589     DENG2_UNUSED(cmd);
1590     LOG_AS("FIC_Image");
1591 
1592     FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as<FinaleAnimWidget>();
1593     anim.clearAllFrames();
1594 
1595 #ifdef __CLIENT__
1596     char const *name  = OP_CSTRING(1);
1597     lumpnum_t lumpNum = App_FileSystem().lumpNumForName(name);
1598 
1599     if (rawtex_t *rawTex = ClientResources::get().declareRawTexture(lumpNum))
1600     {
1601         anim.newFrame(FinaleAnimWidget::Frame::PFT_RAW, -1, &rawTex->lumpNum, 0, false);
1602         return;
1603     }
1604 
1605     LOG_SCR_WARNING("Missing lump '%s'") << name;
1606 #endif
1607 }
1608 
DEFFC(ImageAt)1609 DEFFC(ImageAt)
1610 {
1611     DENG2_UNUSED(cmd);
1612     LOG_AS("FIC_ImageAt");
1613 
1614     FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as<FinaleAnimWidget>();
1615     float x = OP_FLOAT(1);
1616     float y = OP_FLOAT(2);
1617 
1618     anim.clearAllFrames()
1619         .setOrigin(Vector2f(x, y));
1620 
1621 #ifdef __CLIENT__
1622     char const *name  = OP_CSTRING(3);
1623     lumpnum_t lumpNum = App_FileSystem().lumpNumForName(name);
1624 
1625     if (rawtex_t *rawTex = App_Resources().declareRawTexture(lumpNum))
1626     {
1627         anim.newFrame(FinaleAnimWidget::Frame::PFT_RAW, -1, &rawTex->lumpNum, 0, false);
1628         return;
1629     }
1630 
1631     LOG_SCR_WARNING("Missing lump '%s'") << name;
1632 #endif
1633 }
1634 
1635 #ifdef __CLIENT__
loadAndPrepareExtTexture(char const * fileName)1636 static DGLuint loadAndPrepareExtTexture(char const *fileName)
1637 {
1638     image_t image;
1639     DGLuint glTexName = 0;
1640 
1641     if (GL_LoadExtImage(image, fileName, LGM_NORMAL))
1642     {
1643         // Loaded successfully and converted accordingly.
1644         // Upload the image to GL.
1645         glTexName = GL_NewTextureWithParams(
1646             ( image.pixelSize == 2 ? DGL_LUMINANCE_PLUS_A8 :
1647               image.pixelSize == 3 ? DGL_RGB :
1648               image.pixelSize == 4 ? DGL_RGBA : DGL_LUMINANCE ),
1649             image.size.x, image.size.y, image.pixels,
1650             (image.size.x < 128 && image.size.y < 128? TXCF_NO_COMPRESSION : 0),
1651             0, GL_LINEAR, GL_LINEAR, 0 /*no anisotropy*/,
1652             GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE);
1653 
1654         Image_ClearPixelData(image);
1655     }
1656 
1657     return glTexName;
1658 }
1659 #endif // __CLIENT__
1660 
DEFFC(XImage)1661 DEFFC(XImage)
1662 {
1663     DENG2_UNUSED(cmd);
1664 
1665     LOG_AS("FIC_XImage");
1666 
1667     FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as<FinaleAnimWidget>();
1668 #ifdef __CLIENT__
1669     char const *fileName  = OP_CSTRING(1);
1670 #endif
1671 
1672     anim.clearAllFrames();
1673 
1674 #ifdef __CLIENT__
1675     // Load the external resource.
1676     if (DGLuint tex = loadAndPrepareExtTexture(fileName))
1677     {
1678         anim.newFrame(FinaleAnimWidget::Frame::PFT_XIMAGE, -1, &tex, 0, false);
1679     }
1680     else
1681     {
1682         LOG_SCR_WARNING("Missing graphic '%s'") << fileName;
1683     }
1684 #endif // __CLIENT__
1685 }
1686 
DEFFC(Patch)1687 DEFFC(Patch)
1688 {
1689     DENG2_UNUSED(cmd);
1690     LOG_AS("FIC_Patch");
1691 
1692     FinaleAnimWidget &anim  = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as<FinaleAnimWidget>();
1693     char const *encodedName = OP_CSTRING(3);
1694 
1695     anim.setOrigin(Vector2f(OP_FLOAT(1), OP_FLOAT(2)));
1696     anim.clearAllFrames();
1697 
1698     patchid_t patchId = R_DeclarePatch(encodedName);
1699     if (patchId)
1700     {
1701         anim.newFrame(FinaleAnimWidget::Frame::PFT_PATCH, -1, (void *)&patchId, 0, 0);
1702     }
1703     else
1704     {
1705         LOG_SCR_WARNING("Missing Patch '%s'") << encodedName;
1706     }
1707 }
1708 
DEFFC(SetPatch)1709 DEFFC(SetPatch)
1710 {
1711     DENG2_UNUSED(cmd);
1712     LOG_AS("FIC_SetPatch");
1713 
1714     FinaleAnimWidget &anim  = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as<FinaleAnimWidget>();
1715     char const *encodedName = OP_CSTRING(1);
1716 
1717     patchid_t patchId = R_DeclarePatch(encodedName);
1718     if (patchId == 0)
1719     {
1720         LOG_SCR_WARNING("Missing Patch '%s'") << encodedName;
1721         return;
1722     }
1723 
1724     if (!anim.frameCount())
1725     {
1726         anim.newFrame(FinaleAnimWidget::Frame::PFT_PATCH, -1, (void *)&patchId, 0, false);
1727         return;
1728     }
1729 
1730     // Convert the first frame.
1731     FinaleAnimWidget::Frame *f = anim.allFrames().first();
1732     f->type  = FinaleAnimWidget::Frame::PFT_PATCH;
1733     f->texRef.patch = patchId;
1734     f->tics  = -1;
1735     f->sound = 0;
1736 }
1737 
DEFFC(ClearAnim)1738 DEFFC(ClearAnim)
1739 {
1740     DENG2_UNUSED(cmd);
1741     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1742     {
1743         if (FinaleAnimWidget *anim = maybeAs<FinaleAnimWidget>(wi))
1744         {
1745             anim->clearAllFrames();
1746         }
1747     }
1748 }
1749 
DEFFC(Anim)1750 DEFFC(Anim)
1751 {
1752     DENG2_UNUSED(cmd);
1753     LOG_AS("FIC_Anim");
1754 
1755     FinaleAnimWidget &anim  = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as<FinaleAnimWidget>();
1756     char const *encodedName = OP_CSTRING(1);
1757     int const tics          = FRACSECS_TO_TICKS(OP_FLOAT(2));
1758 
1759     patchid_t patchId = R_DeclarePatch(encodedName);
1760     if (!patchId)
1761     {
1762         LOG_SCR_WARNING("Patch '%s' not found") << encodedName;
1763         return;
1764     }
1765 
1766     anim.newFrame(FinaleAnimWidget::Frame::PFT_PATCH, tics, (void *)&patchId, 0, false);
1767 }
1768 
DEFFC(AnimImage)1769 DEFFC(AnimImage)
1770 {
1771     DENG2_UNUSED(cmd);
1772     LOG_AS("FIC_AnimImage");
1773 
1774     FinaleAnimWidget &anim  = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as<FinaleAnimWidget>();
1775 
1776 #ifdef __CLIENT__
1777     char const *encodedName = OP_CSTRING(1);
1778     int const tics          = FRACSECS_TO_TICKS(OP_FLOAT(2));
1779     lumpnum_t lumpNum       = App_FileSystem().lumpNumForName(encodedName);
1780     if (rawtex_t *rawTex = App_Resources().declareRawTexture(lumpNum))
1781     {
1782         anim.newFrame(FinaleAnimWidget::Frame::PFT_RAW, tics, &rawTex->lumpNum, 0, false);
1783         return;
1784     }
1785     LOG_SCR_WARNING("Lump '%s' not found") << encodedName;
1786 #else
1787     DENG2_UNUSED(anim);
1788 #endif
1789 }
1790 
DEFFC(Repeat)1791 DEFFC(Repeat)
1792 {
1793     DENG2_UNUSED(cmd);
1794     FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as<FinaleAnimWidget>();
1795     anim.setLooping();
1796 }
1797 
DEFFC(StateAnim)1798 DEFFC(StateAnim)
1799 {
1800     DENG2_UNUSED(cmd);
1801     FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as<FinaleAnimWidget>();
1802 #if !defined(__CLIENT__)
1803     DENG2_UNUSED(anim);
1804 #endif
1805 
1806     // Animate N states starting from the given one.
1807     dint stateId = DED_Definitions()->getStateNum(OP_CSTRING(1));
1808     for (dint count = OP_INT(2); count > 0 && stateId > 0; count--)
1809     {
1810         state_t *st = &runtimeDefs.states[stateId];
1811 #ifdef __CLIENT__
1812         spriteinfo_t sinf;
1813         R_GetSpriteInfo(st->sprite, st->frame & 0x7fff, &sinf);
1814         anim.newFrame(FinaleAnimWidget::Frame::PFT_MATERIAL, (st->tics <= 0? 1 : st->tics), sinf.material, 0, sinf.flip);
1815 #endif
1816 
1817         // Go to the next state.
1818         stateId = st->nextState;
1819     }
1820 }
1821 
DEFFC(PicSound)1822 DEFFC(PicSound)
1823 {
1824     DENG2_UNUSED(cmd);
1825     FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as<FinaleAnimWidget>();
1826     int const sound        = DED_Definitions()->getSoundNum(OP_CSTRING(1));
1827 
1828     if (!anim.frameCount())
1829     {
1830         anim.newFrame(FinaleAnimWidget::Frame::PFT_MATERIAL, -1, 0, sound, false);
1831         return;
1832     }
1833 
1834     anim.allFrames().at(anim.frameCount() - 1)->sound = sound;
1835 }
1836 
DEFFC(ObjectOffX)1837 DEFFC(ObjectOffX)
1838 {
1839     DENG2_UNUSED(cmd);
1840     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1841     {
1842         wi->setOriginX(OP_FLOAT(1), fi.inTime());
1843     }
1844 }
1845 
DEFFC(ObjectOffY)1846 DEFFC(ObjectOffY)
1847 {
1848     DENG2_UNUSED(cmd);
1849     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1850     {
1851         wi->setOriginY(OP_FLOAT(1), fi.inTime());
1852     }
1853 }
1854 
DEFFC(ObjectOffZ)1855 DEFFC(ObjectOffZ)
1856 {
1857     DENG2_UNUSED(cmd);
1858     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1859     {
1860         wi->setOriginZ(OP_FLOAT(1), fi.inTime());
1861     }
1862 }
1863 
DEFFC(ObjectRGB)1864 DEFFC(ObjectRGB)
1865 {
1866     DENG2_UNUSED(cmd);
1867     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1868     {
1869         Vector3f const color(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3));
1870         if (FinaleTextWidget *text = maybeAs<FinaleTextWidget>(wi))
1871         {
1872             text->setColor(color, fi.inTime());
1873         }
1874         if (FinaleAnimWidget *anim = maybeAs<FinaleAnimWidget>(wi))
1875         {
1876             // This affects all the colors.
1877             anim->setColor         (color, fi.inTime())
1878                  .setEdgeColor     (color, fi.inTime())
1879                  .setOtherColor    (color, fi.inTime())
1880                  .setOtherEdgeColor(color, fi.inTime());
1881         }
1882     }
1883 }
1884 
DEFFC(ObjectAlpha)1885 DEFFC(ObjectAlpha)
1886 {
1887     DENG2_UNUSED(cmd);
1888     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1889     {
1890         float const alpha = OP_FLOAT(1);
1891         if (FinaleTextWidget *text = maybeAs<FinaleTextWidget>(wi))
1892         {
1893             text->setAlpha(alpha, fi.inTime());
1894         }
1895         if (FinaleAnimWidget *anim = maybeAs<FinaleAnimWidget>(wi))
1896         {
1897             anim->setAlpha     (alpha, fi.inTime())
1898                  .setOtherAlpha(alpha, fi.inTime());
1899         }
1900     }
1901 }
1902 
DEFFC(ObjectScaleX)1903 DEFFC(ObjectScaleX)
1904 {
1905     DENG2_UNUSED(cmd);
1906     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1907     {
1908         wi->setScaleX(OP_FLOAT(1), fi.inTime());
1909     }
1910 }
1911 
DEFFC(ObjectScaleY)1912 DEFFC(ObjectScaleY)
1913 {
1914     DENG2_UNUSED(cmd);
1915     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1916     {
1917         wi->setScaleY(OP_FLOAT(1), fi.inTime());
1918     }
1919 }
1920 
DEFFC(ObjectScaleZ)1921 DEFFC(ObjectScaleZ)
1922 {
1923     DENG2_UNUSED(cmd);
1924     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1925     {
1926         wi->setScaleZ(OP_FLOAT(1), fi.inTime());
1927     }
1928 }
1929 
DEFFC(ObjectScale)1930 DEFFC(ObjectScale)
1931 {
1932     DENG2_UNUSED(cmd);
1933     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1934     {
1935         wi->setScaleX(OP_FLOAT(1), fi.inTime())
1936            .setScaleY(OP_FLOAT(1), fi.inTime());
1937     }
1938 }
1939 
DEFFC(ObjectScaleXY)1940 DEFFC(ObjectScaleXY)
1941 {
1942     DENG2_UNUSED(cmd);
1943     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1944     {
1945         wi->setScaleX(OP_FLOAT(1), fi.inTime())
1946            .setScaleY(OP_FLOAT(2), fi.inTime());
1947     }
1948 }
1949 
DEFFC(ObjectScaleXYZ)1950 DEFFC(ObjectScaleXYZ)
1951 {
1952     DENG2_UNUSED(cmd);
1953     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1954     {
1955         wi->setScale(Vector3f(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime());
1956     }
1957 }
1958 
DEFFC(ObjectAngle)1959 DEFFC(ObjectAngle)
1960 {
1961     DENG2_UNUSED(cmd);
1962     if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)))
1963     {
1964         wi->setAngle(OP_FLOAT(1), fi.inTime());
1965     }
1966 }
1967 
DEFFC(Rect)1968 DEFFC(Rect)
1969 {
1970     DENG2_UNUSED(cmd);
1971     FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as<FinaleAnimWidget>();
1972 
1973     /// @note We may be converting an existing Pic to a Rect, so re-init the expected
1974     /// default state accordingly.
1975 
1976     anim.clearAllFrames()
1977         .resetAllColors()
1978         .setLooping(false) // Yeah?
1979         .setOrigin(Vector3f(OP_FLOAT(1), OP_FLOAT(2), 0))
1980         .setScale(Vector3f(OP_FLOAT(3), OP_FLOAT(4), 1));
1981 }
1982 
DEFFC(FillColor)1983 DEFFC(FillColor)
1984 {
1985     DENG2_UNUSED(cmd);
1986     FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0));
1987     if (!wi || !is<FinaleAnimWidget>(wi)) return;
1988     FinaleAnimWidget &anim = wi->as<FinaleAnimWidget>();
1989 
1990     // Which colors to modify?
1991     int which = 0;
1992     if (!qstricmp(OP_CSTRING(1), "top"))         which |= 1;
1993     else if (!qstricmp(OP_CSTRING(1), "bottom")) which |= 2;
1994     else                                         which = 3;
1995 
1996     Vector4f color;
1997     for (int i = 0; i < 4; ++i)
1998     {
1999         color[i] = OP_FLOAT(2 + i);
2000     }
2001 
2002     if (which & 1)
2003         anim.setColorAndAlpha(color, fi.inTime());
2004     if (which & 2)
2005         anim.setOtherColorAndAlpha(color, fi.inTime());
2006 }
2007 
DEFFC(EdgeColor)2008 DEFFC(EdgeColor)
2009 {
2010     DENG2_UNUSED(cmd);
2011     FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0));
2012     if (!wi || !is<FinaleAnimWidget>(wi)) return;
2013     FinaleAnimWidget &anim = wi->as<FinaleAnimWidget>();
2014 
2015     // Which colors to modify?
2016     int which = 0;
2017     if (!qstricmp(OP_CSTRING(1), "top"))         which |= 1;
2018     else if (!qstricmp(OP_CSTRING(1), "bottom")) which |= 2;
2019     else                                        which = 3;
2020 
2021     Vector4f color;
2022     for (int i = 0; i < 4; ++i)
2023     {
2024         color[i] = OP_FLOAT(2 + i);
2025     }
2026 
2027     if (which & 1)
2028         anim.setEdgeColorAndAlpha(color, fi.inTime());
2029     if (which & 2)
2030         anim.setOtherEdgeColorAndAlpha(color, fi.inTime());
2031 }
2032 
DEFFC(OffsetX)2033 DEFFC(OffsetX)
2034 {
2035     DENG2_UNUSED(cmd);
2036     fi.page(FinaleInterpreter::Anims).setOffsetX(OP_FLOAT(0), fi.inTime());
2037 }
2038 
DEFFC(OffsetY)2039 DEFFC(OffsetY)
2040 {
2041     DENG2_UNUSED(cmd);
2042     fi.page(FinaleInterpreter::Anims).setOffsetY(OP_FLOAT(0), fi.inTime());
2043 }
2044 
DEFFC(Sound)2045 DEFFC(Sound)
2046 {
2047     DENG2_UNUSED2(cmd, fi);
2048     S_LocalSound(DED_Definitions()->getSoundNum(OP_CSTRING(0)), nullptr);
2049 }
2050 
DEFFC(SoundAt)2051 DEFFC(SoundAt)
2052 {
2053     DENG2_UNUSED2(cmd, fi);
2054     dint const soundId = DED_Definitions()->getSoundNum(OP_CSTRING(0));
2055     dfloat const vol   = de::min(OP_FLOAT(1), 1.f);
2056     S_LocalSoundAtVolume(soundId, nullptr, vol);
2057 }
2058 
DEFFC(SeeSound)2059 DEFFC(SeeSound)
2060 {
2061     DENG2_UNUSED2(cmd, fi);
2062     dint num = DED_Definitions()->getMobjNum(OP_CSTRING(0));
2063     if (num >= 0 && ::runtimeDefs.mobjInfo[num].seeSound > 0)
2064     {
2065         S_LocalSound(runtimeDefs.mobjInfo[num].seeSound, nullptr);
2066     }
2067 }
2068 
DEFFC(DieSound)2069 DEFFC(DieSound)
2070 {
2071     DENG2_UNUSED2(cmd, fi);
2072     dint num = DED_Definitions()->getMobjNum(OP_CSTRING(0));
2073     if (num >= 0 && ::runtimeDefs.mobjInfo[num].deathSound > 0)
2074     {
2075         S_LocalSound(runtimeDefs.mobjInfo[num].deathSound, nullptr);
2076     }
2077 }
2078 
DEFFC(Music)2079 DEFFC(Music)
2080 {
2081     DENG2_UNUSED3(cmd, ops, fi);
2082     S_StartMusic(OP_CSTRING(0), true);
2083 }
2084 
DEFFC(MusicOnce)2085 DEFFC(MusicOnce)
2086 {
2087     DENG2_UNUSED3(cmd, ops, fi);
2088     S_StartMusic(OP_CSTRING(0), false);
2089 }
2090 
DEFFC(Filter)2091 DEFFC(Filter)
2092 {
2093     DENG2_UNUSED(cmd);
2094     fi.page(FinaleInterpreter::Texts).setFilterColorAndAlpha(Vector4f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime());
2095 }
2096 
DEFFC(Text)2097 DEFFC(Text)
2098 {
2099     DENG2_UNUSED(cmd);
2100     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2101 
2102     text.setText(OP_CSTRING(3))
2103         .setCursorPos(0) // Restart the text.
2104         .setOrigin(Vector3f(OP_FLOAT(1), OP_FLOAT(2), 0));
2105 }
2106 
DEFFC(TextFromDef)2107 DEFFC(TextFromDef)
2108 {
2109     DENG2_UNUSED(cmd);
2110     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2111     int textIdx          = DED_Definitions()->getTextNum((char *)OP_CSTRING(3));
2112 
2113     text.setText(textIdx >= 0? DED_Definitions()->text[textIdx].text : "(undefined)")
2114         .setCursorPos(0) // Restart the type-in animation (if any).
2115         .setOrigin(Vector3f(OP_FLOAT(1), OP_FLOAT(2), 0));
2116 }
2117 
DEFFC(TextFromLump)2118 DEFFC(TextFromLump)
2119 {
2120     DENG2_UNUSED(cmd);
2121     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2122 
2123     text.setOrigin(Vector3f(OP_FLOAT(1), OP_FLOAT(2), 0));
2124 
2125     lumpnum_t lumpNum = App_FileSystem().lumpNumForName(OP_CSTRING(3));
2126     if (lumpNum >= 0)
2127     {
2128         File1 &lump            = App_FileSystem().lump(lumpNum);
2129         uint8_t const *rawStr = lump.cache();
2130 
2131         AutoStr *str = AutoStr_NewStd();
2132         Str_Reserve(str, lump.size() * 2);
2133 
2134         char *out = Str_Text(str);
2135         for (size_t i = 0; i < lump.size(); ++i)
2136         {
2137             char ch = (char)(rawStr[i]);
2138             if (ch == '\r') continue;
2139             if (ch == '\n')
2140             {
2141                 *out++ = '\\';
2142                 *out++ = 'n';
2143             }
2144             else
2145             {
2146                 *out++ = ch;
2147             }
2148         }
2149         lump.unlock();
2150 
2151         text.setText(Str_Text(str));
2152     }
2153     else
2154     {
2155         text.setText("(not found)");
2156     }
2157     text.setCursorPos(0); // Restart.
2158 }
2159 
DEFFC(SetText)2160 DEFFC(SetText)
2161 {
2162     DENG2_UNUSED(cmd);
2163     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2164     text.setText(OP_CSTRING(1));
2165 }
2166 
DEFFC(SetTextDef)2167 DEFFC(SetTextDef)
2168 {
2169     DENG2_UNUSED(cmd);
2170     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2171     int textIdx            = DED_Definitions()->getTextNum((char *)OP_CSTRING(1));
2172 
2173     text.setText(textIdx >= 0? DED_Definitions()->text[textIdx].text : "(undefined)");
2174 }
2175 
DEFFC(DeleteText)2176 DEFFC(DeleteText)
2177 {
2178     DENG2_UNUSED(cmd);
2179     delete fi.tryFindWidget(OP_CSTRING(0));
2180 }
2181 
DEFFC(PredefinedColor)2182 DEFFC(PredefinedColor)
2183 {
2184     DENG2_UNUSED(cmd);
2185     fi.page(FinaleInterpreter::Texts)
2186             .setPredefinedColor(de::clamp(1, OP_INT(0), FIPAGE_NUM_PREDEFINED_COLORS) - 1,
2187                                 Vector3f(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime());
2188     fi.page(FinaleInterpreter::Anims)
2189             .setPredefinedColor(de::clamp(1, OP_INT(0), FIPAGE_NUM_PREDEFINED_COLORS) - 1,
2190                                 Vector3f(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime());
2191 }
2192 
DEFFC(PredefinedFont)2193 DEFFC(PredefinedFont)
2194 {
2195 #ifdef __CLIENT__
2196     DENG2_UNUSED(cmd);
2197     LOG_AS("FIC_PredefinedFont");
2198 
2199     fontid_t const fontNum = Fonts_ResolveUri(OP_URI(1));
2200     if (fontNum)
2201     {
2202         int const idx = de::clamp(1, OP_INT(0), FIPAGE_NUM_PREDEFINED_FONTS) - 1;
2203         fi.page(FinaleInterpreter::Anims).setPredefinedFont(idx, fontNum);
2204         fi.page(FinaleInterpreter::Texts).setPredefinedFont(idx, fontNum);
2205         return;
2206     }
2207 
2208     AutoStr *fontPath = Uri_ToString(OP_URI(1));
2209     LOG_SCR_WARNING("Unknown font '%s'") << Str_Text(fontPath);
2210 #else
2211     DENG2_UNUSED3(cmd, ops, fi);
2212 #endif
2213 }
2214 
DEFFC(TextRGB)2215 DEFFC(TextRGB)
2216 {
2217     DENG2_UNUSED(cmd);
2218     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2219     text.setColor(Vector3f(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime());
2220 }
2221 
DEFFC(TextAlpha)2222 DEFFC(TextAlpha)
2223 {
2224     DENG2_UNUSED(cmd);
2225     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2226     text.setAlpha(OP_FLOAT(1), fi.inTime());
2227 }
2228 
DEFFC(TextOffX)2229 DEFFC(TextOffX)
2230 {
2231     DENG2_UNUSED(cmd);
2232     FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0));
2233     wi.setOriginX(OP_FLOAT(1), fi.inTime());
2234 }
2235 
DEFFC(TextOffY)2236 DEFFC(TextOffY)
2237 {
2238     DENG2_UNUSED(cmd);
2239     FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0));
2240     wi.setOriginY(OP_FLOAT(1), fi.inTime());
2241 }
2242 
DEFFC(TextCenter)2243 DEFFC(TextCenter)
2244 {
2245     DENG2_UNUSED(cmd);
2246     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2247     text.setAlignment(text.alignment() & ~(ALIGN_LEFT | ALIGN_RIGHT));
2248 }
2249 
DEFFC(TextNoCenter)2250 DEFFC(TextNoCenter)
2251 {
2252     DENG2_UNUSED(cmd);
2253     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2254     text.setAlignment(text.alignment() | ALIGN_LEFT);
2255 }
2256 
DEFFC(TextScroll)2257 DEFFC(TextScroll)
2258 {
2259     DENG2_UNUSED(cmd);
2260     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2261     text.setScrollRate(OP_INT(1));
2262 }
2263 
DEFFC(TextPos)2264 DEFFC(TextPos)
2265 {
2266     DENG2_UNUSED(cmd);
2267     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2268     text.setCursorPos(OP_INT(1));
2269 }
2270 
DEFFC(TextRate)2271 DEFFC(TextRate)
2272 {
2273     DENG2_UNUSED(cmd);
2274     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2275     text.setTypeInRate(OP_INT(1));
2276 }
2277 
DEFFC(TextLineHeight)2278 DEFFC(TextLineHeight)
2279 {
2280     DENG2_UNUSED(cmd);
2281     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2282     text.setLineHeight(OP_FLOAT(1));
2283 }
2284 
DEFFC(Font)2285 DEFFC(Font)
2286 {
2287 #ifdef __CLIENT__
2288     DENG2_UNUSED(cmd);
2289     LOG_AS("FIC_Font");
2290 
2291     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2292     fontid_t fontNum       = Fonts_ResolveUri(OP_URI(1));
2293     if (fontNum)
2294     {
2295         text.setFont(fontNum);
2296         return;
2297     }
2298 
2299     AutoStr *fontPath = Uri_ToString(OP_URI(1));
2300     LOG_SCR_WARNING("Unknown font '%s'") << Str_Text(fontPath);
2301 #else
2302     DENG2_UNUSED3(cmd, ops, fi);
2303 #endif
2304 }
2305 
DEFFC(FontA)2306 DEFFC(FontA)
2307 {
2308     DENG2_UNUSED(cmd);
2309     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2310     text.setFont(fi.page(FinaleInterpreter::Texts).predefinedFont(0));
2311 }
2312 
DEFFC(FontB)2313 DEFFC(FontB)
2314 {
2315     DENG2_UNUSED(cmd);
2316     FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as<FinaleTextWidget>();
2317     text.setFont(fi.page(FinaleInterpreter::Texts).predefinedFont(1));
2318 }
2319 
DEFFC(NoMusic)2320 DEFFC(NoMusic)
2321 {
2322     DENG2_UNUSED3(cmd, ops, fi);
2323     S_StopMusic();
2324 }
2325 
DEFFC(TextScaleX)2326 DEFFC(TextScaleX)
2327 {
2328     DENG2_UNUSED(cmd);
2329     FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0));
2330     wi.setScaleX(OP_FLOAT(1), fi.inTime());
2331 }
2332 
DEFFC(TextScaleY)2333 DEFFC(TextScaleY)
2334 {
2335     DENG2_UNUSED(cmd);
2336     FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0));
2337     wi.setScaleY(OP_FLOAT(1), fi.inTime());
2338 }
2339 
DEFFC(TextScale)2340 DEFFC(TextScale)
2341 {
2342     DENG2_UNUSED(cmd);
2343     FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0));
2344     wi.setScaleX(OP_FLOAT(1), fi.inTime())
2345       .setScaleY(OP_FLOAT(2), fi.inTime());
2346 }
2347 
DEFFC(PlayDemo)2348 DEFFC(PlayDemo)
2349 {
2350     /// @todo Demos are not supported at the moment. -jk
2351 #if 0
2352     // While playing a demo we suspend command interpretation.
2353     fi.suspend();
2354 
2355     // Start the demo.
2356     if (!Con_Executef(CMDS_DDAY, true, "playdemo \"%s\"", OP_CSTRING(0)))
2357     {
2358         // Demo playback failed. Here we go again...
2359         fi.resume();
2360     }
2361 #else
2362     DENG2_UNUSED3(cmd, ops, fi);
2363 #endif
2364 }
2365 
DEFFC(Command)2366 DEFFC(Command)
2367 {
2368     DENG2_UNUSED2(cmd, fi);
2369     Con_Executef(CMDS_SCRIPT, false, "%s", OP_CSTRING(0));
2370 }
2371 
DEFFC(ShowMenu)2372 DEFFC(ShowMenu)
2373 {
2374     DENG2_UNUSED2(cmd, ops);
2375     fi.setShowMenu();
2376 }
2377 
DEFFC(NoShowMenu)2378 DEFFC(NoShowMenu)
2379 {
2380     DENG2_UNUSED2(cmd, ops);
2381     fi.setShowMenu(false);
2382 }
2383