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