/** @file finaleinterpreter.cpp InFine animation system Finale script interpreter. * * @authors Copyright © 2003-2017 Jaakko Keränen * @authors Copyright © 2005-2015 Daniel Swanson * * @par License * GPL: http://www.gnu.org/licenses/gpl.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. This program is distributed in the hope that it * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. You should have received a copy of the GNU * General Public License along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "de_base.h" #include "ui/infine/finaleinterpreter.h" #include #include #include #include #include #include #include #include #include #include #include "dd_def.h" #include "def_main.h" // ::defs #include "api_material.h" #include "api_render.h" #include "api_resource.h" #include "network/net_main.h" #include "ui/infine/finalewidget.h" #include "ui/infine/finaleanimwidget.h" #include "ui/infine/finalepagewidget.h" #include "ui/infine/finaletextwidget.h" #ifdef __CLIENT__ # include "api_fontrender.h" # include "client/cl_infine.h" # include "gl/gl_main.h" # include "gl/gl_texmanager.h" # include "gl/texturecontent.h" # include "gl/sys_opengl.h" // TODO: get rid of this #endif #ifdef __SERVER__ # include "server/sv_infine.h" #endif #define MAX_TOKEN_LENGTH (8192) #define FRACSECS_TO_TICKS(sec) (int(sec * TICSPERSEC + 0.5)) using namespace de; enum fi_operand_type_t { FVT_INT, FVT_FLOAT, FVT_STRING, FVT_URI }; static fi_operand_type_t operandTypeForCharCode(char code) { switch (code) { case 'i': return FVT_INT; case 'f': return FVT_FLOAT; case 's': return FVT_STRING; case 'u': return FVT_URI; default: throw Error("operandTypeForCharCode", String("Unknown char-code %1").arg(code)); } } // Helper macro for accessing the value of an operand. #define OP_INT(n) (ops[n].data.integer) #define OP_FLOAT(n) (ops[n].data.flt) #define OP_CSTRING(n) (ops[n].data.cstring) #define OP_URI(n) (ops[n].data.uri) struct fi_operand_t { fi_operand_type_t type; union { int integer; float flt; char const *cstring; uri_s *uri; } data; }; // Helper macro for defining infine command functions. #define DEFFC(name) void FIC_##name(command_t const &cmd, fi_operand_t const *ops, FinaleInterpreter &fi) /** * @defgroup finaleInterpreterCommandDirective Finale Interpreter Command Directive * @ingroup infine */ /*@{*/ #define FID_NORMAL 0 #define FID_ONLOAD 0x1 /*@}*/ struct command_t { char const *token; char const *operands; typedef void (*Func) (command_t const &, fi_operand_t const *, FinaleInterpreter &); Func func; struct command_flags_s { char when_skipping:1; char when_condition_skipping:1; // Skipping because condition failed. } flags; /// Command execution directives NOT supported by this command. /// @see finaleInterpreterCommandDirective int excludeDirectives; }; // Command functions. DEFFC(Do); DEFFC(End); DEFFC(BGMaterial); DEFFC(NoBGMaterial); DEFFC(Wait); DEFFC(WaitText); DEFFC(WaitAnim); DEFFC(Tic); DEFFC(InTime); DEFFC(Color); DEFFC(ColorAlpha); DEFFC(Pause); DEFFC(CanSkip); DEFFC(NoSkip); DEFFC(SkipHere); DEFFC(Events); DEFFC(NoEvents); DEFFC(OnKey); DEFFC(UnsetKey); DEFFC(If); DEFFC(IfNot); DEFFC(Else); DEFFC(GoTo); DEFFC(Marker); DEFFC(Image); DEFFC(ImageAt); DEFFC(XImage); DEFFC(Delete); DEFFC(Patch); DEFFC(SetPatch); DEFFC(Anim); DEFFC(AnimImage); DEFFC(StateAnim); DEFFC(Repeat); DEFFC(ClearAnim); DEFFC(PicSound); DEFFC(ObjectOffX); DEFFC(ObjectOffY); DEFFC(ObjectOffZ); DEFFC(ObjectScaleX); DEFFC(ObjectScaleY); DEFFC(ObjectScaleZ); DEFFC(ObjectScale); DEFFC(ObjectScaleXY); DEFFC(ObjectScaleXYZ); DEFFC(ObjectRGB); DEFFC(ObjectAlpha); DEFFC(ObjectAngle); DEFFC(Rect); DEFFC(FillColor); DEFFC(EdgeColor); DEFFC(OffsetX); DEFFC(OffsetY); DEFFC(Sound); DEFFC(SoundAt); DEFFC(SeeSound); DEFFC(DieSound); DEFFC(Music); DEFFC(MusicOnce); DEFFC(Filter); DEFFC(Text); DEFFC(TextFromDef); DEFFC(TextFromLump); DEFFC(SetText); DEFFC(SetTextDef); DEFFC(DeleteText); DEFFC(Font); DEFFC(FontA); DEFFC(FontB); DEFFC(PredefinedColor); DEFFC(PredefinedFont); DEFFC(TextRGB); DEFFC(TextAlpha); DEFFC(TextOffX); DEFFC(TextOffY); DEFFC(TextCenter); DEFFC(TextNoCenter); DEFFC(TextScroll); DEFFC(TextPos); DEFFC(TextRate); DEFFC(TextLineHeight); DEFFC(NoMusic); DEFFC(TextScaleX); DEFFC(TextScaleY); DEFFC(TextScale); DEFFC(PlayDemo); DEFFC(Command); DEFFC(ShowMenu); DEFFC(NoShowMenu); /** * Time is measured in seconds. * Colors are floating point and [0..1]. * * @todo This data should be pre-processed (i.e., parsed) during module init * and any symbolic references or other indirections resolved once * rather than repeatedly during script interpretation. * * At this time the command names could also be hashed and chained to * improve performance. -ds */ static command_t const *findCommand(char const *name) { static command_t const commands[] = { // Run Control { "DO", "", FIC_Do, true, true }, { "END", "", FIC_End }, { "IF", "s", FIC_If }, // if (value-id) { "IFNOT", "s", FIC_IfNot }, // ifnot (value-id) { "ELSE", "", FIC_Else }, { "GOTO", "s", FIC_GoTo }, // goto (marker) { "MARKER", "s", FIC_Marker, true }, { "in", "f", FIC_InTime }, // in (time) { "pause", "", FIC_Pause }, { "tic", "", FIC_Tic }, { "wait", "f", FIC_Wait }, // wait (time) { "waittext", "s", FIC_WaitText }, // waittext (id) { "waitanim", "s", FIC_WaitAnim }, // waitanim (id) { "canskip", "", FIC_CanSkip }, { "noskip", "", FIC_NoSkip }, { "skiphere", "", FIC_SkipHere, true }, { "events", "", FIC_Events }, { "noevents", "", FIC_NoEvents }, { "onkey", "ss", FIC_OnKey }, // onkey (keyname) (marker) { "unsetkey", "s", FIC_UnsetKey }, // unsetkey (keyname) // Screen Control { "color", "fff", FIC_Color }, // color (red) (green) (blue) { "coloralpha", "ffff", FIC_ColorAlpha }, // coloralpha (r) (g) (b) (a) { "flat", "u(flats:)", FIC_BGMaterial }, // flat (flat-id) { "texture", "u(textures:)", FIC_BGMaterial }, // texture (texture-id) { "noflat", "", FIC_NoBGMaterial }, { "notexture", "", FIC_NoBGMaterial }, { "offx", "f", FIC_OffsetX }, // offx (x) { "offy", "f", FIC_OffsetY }, // offy (y) { "filter", "ffff", FIC_Filter }, // filter (r) (g) (b) (a) // Audio { "sound", "s", FIC_Sound }, // sound (snd) { "soundat", "sf", FIC_SoundAt }, // soundat (snd) (vol:0-1) { "seesound", "s", FIC_SeeSound }, // seesound (mobjtype) { "diesound", "s", FIC_DieSound }, // diesound (mobjtype) { "music", "s", FIC_Music }, // music (musicname) { "musiconce", "s", FIC_MusicOnce }, // musiconce (musicname) { "nomusic", "", FIC_NoMusic }, // Objects { "del", "s", FIC_Delete }, // del (wi) { "x", "sf", FIC_ObjectOffX }, // x (wi) (x) { "y", "sf", FIC_ObjectOffY }, // y (wi) (y) { "z", "sf", FIC_ObjectOffZ }, // z (wi) (z) { "sx", "sf", FIC_ObjectScaleX }, // sx (wi) (x) { "sy", "sf", FIC_ObjectScaleY }, // sy (wi) (y) { "sz", "sf", FIC_ObjectScaleZ }, // sz (wi) (z) { "scale", "sf", FIC_ObjectScale }, // scale (wi) (factor) { "scalexy", "sff", FIC_ObjectScaleXY }, // scalexy (wi) (x) (y) { "scalexyz", "sfff", FIC_ObjectScaleXYZ }, // scalexyz (wi) (x) (y) (z) { "rgb", "sfff", FIC_ObjectRGB }, // rgb (wi) (r) (g) (b) { "alpha", "sf", FIC_ObjectAlpha }, // alpha (wi) (alpha) { "angle", "sf", FIC_ObjectAngle }, // angle (wi) (degrees) // Rects { "rect", "sffff", FIC_Rect }, // rect (hndl) (x) (y) (w) (h) { "fillcolor", "ssffff", FIC_FillColor }, // fillcolor (wi) (top/bottom/both) (r) (g) (b) (a) { "edgecolor", "ssffff", FIC_EdgeColor }, // edgecolor (wi) (top/bottom/both) (r) (g) (b) (a) // Pics { "image", "ss", FIC_Image }, // image (id) (raw-image-lump) { "imageat", "sffs", FIC_ImageAt }, // imageat (id) (x) (y) (raw) { "ximage", "ss", FIC_XImage }, // ximage (id) (ext-gfx-filename) { "patch", "sffs", FIC_Patch }, // patch (id) (x) (y) (patch) { "set", "ss", FIC_SetPatch }, // set (id) (lump) { "clranim", "s", FIC_ClearAnim }, // clranim (wi) { "anim", "ssf", FIC_Anim }, // anim (id) (patch) (time) { "imageanim", "ssf", FIC_AnimImage }, // imageanim (id) (raw-img) (time) { "picsound", "ss", FIC_PicSound }, // picsound (id) (sound) { "repeat", "s", FIC_Repeat }, // repeat (id) { "states", "ssi", FIC_StateAnim }, // states (id) (state) (count) // Text { "text", "sffs", FIC_Text }, // text (hndl) (x) (y) (string) { "textdef", "sffs", FIC_TextFromDef }, // textdef (hndl) (x) (y) (txt-id) { "textlump", "sffs", FIC_TextFromLump }, // textlump (hndl) (x) (y) (lump) { "settext", "ss", FIC_SetText }, // settext (id) (newtext) { "settextdef", "ss", FIC_SetTextDef }, // settextdef (id) (txt-id) { "center", "s", FIC_TextCenter }, // center (id) { "nocenter", "s", FIC_TextNoCenter }, // nocenter (id) { "scroll", "sf", FIC_TextScroll }, // scroll (id) (speed) { "pos", "si", FIC_TextPos }, // pos (id) (pos) { "rate", "si", FIC_TextRate }, // rate (id) (rate) { "font", "su", FIC_Font }, // font (id) (font) { "linehgt", "sf", FIC_TextLineHeight }, // linehgt (hndl) (hgt) // Game Control { "playdemo", "s", FIC_PlayDemo }, // playdemo (filename) { "cmd", "s", FIC_Command }, // cmd (console command) { "trigger", "", FIC_ShowMenu }, { "notrigger", "", FIC_NoShowMenu }, // Misc. { "precolor", "ifff", FIC_PredefinedColor }, // precolor (num) (r) (g) (b) { "prefont", "iu", FIC_PredefinedFont }, // prefont (num) (font) // Deprecated Font commands { "fonta", "s", FIC_FontA }, // fonta (id) { "fontb", "s", FIC_FontB }, // fontb (id) // Deprecated Pic commands { "delpic", "s", FIC_Delete }, // delpic (wi) // Deprecated Text commands { "deltext", "s", FIC_DeleteText }, // deltext (wi) { "textrgb", "sfff", FIC_TextRGB }, // textrgb (id) (r) (g) (b) { "textalpha", "sf", FIC_TextAlpha }, // textalpha (id) (alpha) { "tx", "sf", FIC_TextOffX }, // tx (id) (x) { "ty", "sf", FIC_TextOffY }, // ty (id) (y) { "tsx", "sf", FIC_TextScaleX }, // tsx (id) (x) { "tsy", "sf", FIC_TextScaleY }, // tsy (id) (y) { "textscale", "sf", FIC_TextScale }, // textscale (id) (x) (y) { nullptr, 0, nullptr } // Terminate. }; for (size_t i = 0; commands[i].token; ++i) { command_t const *cmd = &commands[i]; if (!qstricmp(cmd->token, name)) { return cmd; } } return nullptr; // Not found. } DENG2_PIMPL(FinaleInterpreter) { struct Flags { char stopped:1; char suspended:1; char paused:1; char can_skip:1; char eat_events:1; /// Script will eat all input events. char show_menu:1; } flags; finaleid_t id = 0; ///< Unique identifier. ddstring_t *script = nullptr; ///< The script to be interpreted. char const *scriptBegin = nullptr; ///< Beginning of the script (after any directive blocks). char const *cp = nullptr; ///< Current position in the script. char token[MAX_TOKEN_LENGTH]; ///< Script token read/parse buffer. /// Pages containing the widgets used to visualize the script objects. std::unique_ptr pages[2]; bool cmdExecuted = false; ///< Set to true after first command is executed. bool skipping = false; bool lastSkipped = false; bool skipNext = false; bool gotoEnd = false; bool gotoSkip = false; String gotoTarget; int doLevel = 0; ///< Level of DO-skipping. uint timer = 0; int wait = 0; int inTime = 0; FinaleAnimWidget *waitAnim = nullptr; FinaleTextWidget *waitText = nullptr; #ifdef __CLIENT__ struct EventHandler { ddevent_t ev; // Template. String gotoMarker; explicit EventHandler(ddevent_t const *evTemplate = nullptr, String const &gotoMarker = "") : gotoMarker(gotoMarker) { std::memcpy(&ev, &evTemplate, sizeof(ev)); } EventHandler(EventHandler const &other) : gotoMarker(other.gotoMarker) { std::memcpy(&ev, &other.ev, sizeof(ev)); } }; typedef QList EventHandlers; EventHandlers eventHandlers; #endif // __CLIENT__ Impl(Public *i, finaleid_t id) : Base(i), id(id) { de::zap(flags); de::zap(token); } ~Impl() { stop(); releaseScript(); } void initDefaultState() { flags.suspended = false; flags.paused = false; flags.show_menu = true; // Unhandled events will show a menu. flags.can_skip = true; // By default skipping is enabled. cmdExecuted = false; // Nothing is drawn until a cmd has been executed. skipping = false; wait = 0; // Not waiting for anything. inTime = 0; // Interpolation is off. timer = 0; gotoSkip = false; gotoEnd = false; skipNext = false; waitText = nullptr; waitAnim = nullptr; gotoTarget.clear(); #ifdef __CLIENT__ eventHandlers.clear(); #endif } void releaseScript() { Str_Delete(script); script = nullptr; scriptBegin = nullptr; cp = nullptr; } void stop() { if (flags.stopped) return; flags.stopped = true; LOGDEV_SCR_MSG("Finale End - id:%i '%.30s'") << id << scriptBegin; #ifdef __SERVER__ if (::isServer && !(FI_ScriptFlags(id) & FF_LOCAL)) { // Tell clients to stop the finale. Sv_Finale(id, FINF_END, 0); } #endif // Any hooks? DoomsdayApp::plugins().callAllHooks(HOOK_FINALE_SCRIPT_STOP, id); } bool atEnd() const { DENG2_ASSERT(script); return (cp - Str_Text(script)) >= Str_Length(script); } void findBegin() { char const *tok; while (!gotoEnd && 0 != (tok = nextToken()) && qstricmp(tok, "{")) {} } void findEnd() { char const *tok; while (!gotoEnd && 0 != (tok = nextToken()) && qstricmp(tok, "}")) {} } char const *nextToken() { // Skip whitespace. while (!atEnd() && isspace(*cp)) { cp++; } // Have we reached the end? if (atEnd()) return nullptr; char *out = token; if (*cp == '"') // A string? { for (cp++; !atEnd(); cp++) { if (*cp == '"') { cp++; // Convert double quotes to single ones. if (*cp != '"') break; } *out++ = *cp; } } else { while (!isspace(*cp) && !atEnd()) { *out++ = *cp++; } } *out++ = 0; return token; } /// @return @c true if the end of the script was reached. bool executeNextCommand() { if (char const *tok = nextToken()) { executeCommand(tok, FID_NORMAL); // Time to unhide the object page(s)? if (cmdExecuted) { pages[Anims]->makeVisible(); pages[Texts]->makeVisible(); } return false; } return true; } static inline char const *findDefaultValueEnd(char const *str) { char const *defaultValueEnd; for (defaultValueEnd = str; defaultValueEnd && *defaultValueEnd != ')'; defaultValueEnd++) {} DENG2_ASSERT(defaultValueEnd < str + qstrlen(str)); return defaultValueEnd; } static char const *nextOperand(char const *operands) { if (operands && operands[0]) { // Some operands might include a default value. int len = qstrlen(operands); if (len > 1 && operands[1] == '(') { // A default value begins. Find the end. return findDefaultValueEnd(operands + 2) + 1; } return operands + 1; } return nullptr; // No more operands. } /// @return Total number of command operands in the control string @a operands. static int countCommandOperands(char const *operands) { int count = 0; while (operands && operands[0]) { count += 1; operands = nextOperand(operands); } return count; } /** * Prepare the command operands from the script. If successfull, a ptr to a new * vector of @c fi_operand_t objects is returned. Ownership of the vector is * given to the caller. * * @return Array of @c fi_operand_t else @c nullptr. Must be free'd with M_Free(). */ fi_operand_t *prepareCommandOperands(command_t const *cmd, int *count) { DENG2_ASSERT(cmd); char const *origCursorPos = cp; int const operandCount = countCommandOperands(cmd->operands); if (operandCount <= 0) return nullptr; fi_operand_t *operands = (fi_operand_t *) M_Malloc(sizeof(*operands) * operandCount); char const *opRover = cmd->operands; for (fi_operand_t *op = operands; opRover && opRover[0]; opRover = nextOperand(opRover), op++) { char const charCode = *opRover; op->type = operandTypeForCharCode(charCode); bool const opHasDefaultValue = (opRover < cmd->operands + (qstrlen(cmd->operands) - 2) && opRover[1] == '('); bool const haveValue = !!nextToken(); if (!haveValue && !opHasDefaultValue) { cp = origCursorPos; if (operands) M_Free(operands); if (count) *count = 0; App_Error("prepareCommandOperands: Too few operands for command '%s'.\n", cmd->token); return 0; // Unreachable. } switch (op->type) { case FVT_INT: { char const *valueStr = haveValue? token : nullptr; if (!valueStr) { // Use the default. int const defaultValueLen = (findDefaultValueEnd(opRover + 2) - opRover) - 1; AutoStr *defaultValue = Str_PartAppend(AutoStr_NewStd(), opRover + 2, 0, defaultValueLen); valueStr = Str_Text(defaultValue); } op->data.integer = strtol(valueStr, nullptr, 0); break; } case FVT_FLOAT: { char const *valueStr = haveValue? token : nullptr; if (!valueStr) { // Use the default. int const defaultValueLen = (findDefaultValueEnd(opRover + 2) - opRover) - 1; AutoStr *defaultValue = Str_PartAppend(AutoStr_NewStd(), opRover + 2, 0, defaultValueLen); valueStr = Str_Text(defaultValue); } op->data.flt = strtod(valueStr, nullptr); break; } case FVT_STRING: { char const *valueStr = haveValue? token : nullptr; int valueLen = haveValue? qstrlen(token) : 0; if (!valueStr) { // Use the default. int const defaultValueLen = (findDefaultValueEnd(opRover + 2) - opRover) - 1; AutoStr *defaultValue = Str_PartAppend(AutoStr_NewStd(), opRover + 2, 0, defaultValueLen); valueStr = Str_Text(defaultValue); valueLen = defaultValueLen; } op->data.cstring = (char *)M_Malloc(valueLen + 1); qstrcpy((char *)op->data.cstring, valueStr); break; } case FVT_URI: { uri_s *uri = Uri_New(); // Always apply the default as it may contain a default scheme. if (opHasDefaultValue) { int const defaultValueLen = (findDefaultValueEnd(opRover + 2) - opRover) - 1; AutoStr *defaultValue = Str_PartAppend(AutoStr_NewStd(), opRover + 2, 0, defaultValueLen); Uri_SetUri2(uri, Str_Text(defaultValue), RC_NULL); } if (haveValue) { Uri_SetUri2(uri, token, RC_NULL); } op->data.uri = uri; break; } default: break; // Unreachable. } } if (count) *count = operandCount; return operands; } bool skippingCommand(command_t const *cmd) { DENG2_ASSERT(cmd); if ((skipNext && !cmd->flags.when_condition_skipping) || ((skipping || gotoSkip) && !cmd->flags.when_skipping)) { // While not DO-skipping, the condskip has now been done. if (!doLevel) { if (skipNext) lastSkipped = true; skipNext = false; } return true; } return false; } /** * Execute one (the next) command, advance script cursor. */ bool executeCommand(char const *commandString, int directive) { DENG2_ASSERT(commandString); bool didSkip = false; // Semicolon terminates DO-blocks. if (!qstrcmp(commandString, ";")) { if (doLevel > 0) { if (--doLevel == 0) { // The DO-skip has been completed. skipNext = false; lastSkipped = true; } } return true; // Success! } // We're now going to execute a command. cmdExecuted = true; // Is this a command we know how to execute? if (command_t const *cmd = findCommand(commandString)) { bool const requiredOperands = (cmd->operands && cmd->operands[0]); // Is this command supported for this directive? if (directive != 0 && cmd->excludeDirectives != 0 && (cmd->excludeDirectives & directive) == 0) App_Error("executeCommand: Command \"%s\" is not supported for directive %i.", cmd->token, directive); // Check that there are enough operands. /// @todo Dynamic memory allocation during script interpretation should be avoided. int numOps = 0; fi_operand_t *ops = nullptr; if (!requiredOperands || (ops = prepareCommandOperands(cmd, &numOps))) { // Should we skip this command? if (!(didSkip = skippingCommand(cmd))) { // Execute forthwith! cmd->func(*cmd, ops, self()); } } if (!didSkip) { if (gotoEnd) { wait = 1; } else { // Now we've executed the latest command. lastSkipped = false; } } if (ops) { for (int i = 0; i < numOps; ++i) { fi_operand_t *op = &ops[i]; switch (op->type) { case FVT_STRING: M_Free((char *)op->data.cstring); break; case FVT_URI: Uri_Delete(op->data.uri); break; default: break; } } M_Free(ops); } } return !didSkip; } static inline PageIndex choosePageFor(FinaleWidget &widget) { return is(widget)? Anims : Texts; } static inline PageIndex choosePageFor(fi_obtype_e type) { return type == FI_ANIM? Anims : Texts; } FinaleWidget *locateWidget(fi_obtype_e type, String const &name) { if (!name.isEmpty()) { FinalePageWidget::Children const &children = pages[choosePageFor(type)]->children(); for (FinaleWidget *widget : children) { if (!widget->name().compareWithoutCase(name)) { return widget; } } } return nullptr; // Not found. } FinaleWidget *makeWidget(fi_obtype_e type, String const &name) { if (type == FI_ANIM) { return new FinaleAnimWidget(name); } if (type == FI_TEXT) { auto *wi = new FinaleTextWidget(name); // Configure the text to use the Page's font and color. wi->setPageFont(1) .setPageColor(1); return wi; } return nullptr; } #if __CLIENT__ EventHandler *findEventHandler(ddevent_t const &ev) const { for (EventHandler const &eh : eventHandlers) { if (eh.ev.device != ev.device && eh.ev.type != ev.type) continue; switch (eh.ev.type) { case E_TOGGLE: if (eh.ev.toggle.id != ev.toggle.id) continue; break; case E_AXIS: if (eh.ev.axis.id != ev.axis.id) continue; break; case E_ANGLE: if (eh.ev.angle.id != ev.angle.id) continue; break; default: App_Error("Internal error: Invalid event template (type=%i) in finale event handler.", int(eh.ev.type)); } return &const_cast(eh); } return nullptr; } #endif // __CLIENT__ }; FinaleInterpreter::FinaleInterpreter(finaleid_t id) : d(new Impl(this, id)) {} finaleid_t FinaleInterpreter::id() const { return d->id; } void FinaleInterpreter::loadScript(char const *script) { DENG2_ASSERT(script && script[0]); d->pages[Anims].reset(new FinalePageWidget); d->pages[Texts].reset(new FinalePageWidget); // Hide our pages until command interpretation begins. d->pages[Anims]->makeVisible(false); d->pages[Texts]->makeVisible(false); // Take a copy of the script. d->script = Str_Set(Str_NewStd(), script); d->scriptBegin = Str_Text(d->script); d->cp = Str_Text(d->script); // Init cursor. d->initDefaultState(); // Locate the start of the script. if (d->nextToken()) { // The start of the script may include blocks of event directive // commands. These commands are automatically executed in response // to their associated events. if (!qstricmp(d->token, "OnLoad")) { d->findBegin(); forever { d->nextToken(); if (!qstricmp(d->token, "}")) goto end_read; if (!d->executeCommand(d->token, FID_ONLOAD)) App_Error("FinaleInterpreter::LoadScript: Unknown error" "occured executing directive \"OnLoad\"."); } d->findEnd(); end_read: // Skip over any trailing whitespace to position the read cursor // on the first token. while (*d->cp && isspace(*d->cp)) { d->cp++; } // Calculate the new script entry point and restore default state. d->scriptBegin = Str_Text(d->script) + (d->cp - Str_Text(d->script)); d->cp = d->scriptBegin; d->initDefaultState(); } } // Any hooks? DoomsdayApp::plugins().callAllHooks(HOOK_FINALE_SCRIPT_BEGIN, d->id); } void FinaleInterpreter::resume() { if (!d->flags.suspended) return; d->flags.suspended = false; d->pages[Anims]->pause(false); d->pages[Texts]->pause(false); // Do we need to unhide any pages? if (d->cmdExecuted) { d->pages[Anims]->makeVisible(); d->pages[Texts]->makeVisible(); } } void FinaleInterpreter::suspend() { LOG_AS("FinaleInterpreter"); if (d->flags.suspended) return; d->flags.suspended = true; // While suspended, all pages will be paused and hidden. d->pages[Anims]->pause(); d->pages[Anims]->makeVisible(false); d->pages[Texts]->pause(); d->pages[Texts]->makeVisible(false); } void FinaleInterpreter::terminate() { d->stop(); #ifdef __CLIENT__ d->eventHandlers.clear(); #endif d->releaseScript(); } bool FinaleInterpreter::isMenuTrigger() const { if (d->flags.paused || d->flags.can_skip) { // We want events to be used for unpausing/skipping. return false; } // If skipping is not allowed, we should show the menu, too. return (d->flags.show_menu != 0); } bool FinaleInterpreter::isSuspended() const { return (d->flags.suspended != 0); } void FinaleInterpreter::allowSkip(bool yes) { d->flags.can_skip = yes; } bool FinaleInterpreter::canSkip() const { return (d->flags.can_skip != 0); } bool FinaleInterpreter::commandExecuted() const { return d->cmdExecuted; } static bool runOneTick(FinaleInterpreter &fi) { ddhook_finale_script_ticker_paramaters_t parm; de::zap(parm); parm.runTick = true; parm.canSkip = fi.canSkip(); DoomsdayApp::plugins().callAllHooks(HOOK_FINALE_SCRIPT_TICKER, fi.id(), &parm); return parm.runTick; } bool FinaleInterpreter::runTicks(timespan_t timeDelta, bool processCommands) { LOG_AS("FinaleInterpreter"); // All pages tick unless paused. page(Anims).runTicks(timeDelta); page(Texts).runTicks(timeDelta); if (!processCommands) return false; if (d->flags.stopped) return false; if (d->flags.suspended) return false; d->timer++; if (!runOneTick(*this)) return false; // If waiting do not execute commands. if (d->wait && --d->wait) return false; // If paused there is nothing further to do. if (d->flags.paused) return false; // If waiting on a text to finish typing, do nothing. if (d->waitText) { if (!d->waitText->animationComplete()) return false; d->waitText = nullptr; } // Waiting for an animation to reach its end? if (d->waitAnim) { if (!d->waitAnim->animationComplete()) return false; d->waitAnim = nullptr; } // Execute commands until a wait time is set or we reach the end of // the script. If the end is reached, the finale really ends (terminates). bool foundEnd = false; while (!d->gotoEnd && !d->wait && !d->waitText && !d->waitAnim && !foundEnd) { foundEnd = d->executeNextCommand(); } return (d->gotoEnd || (foundEnd && d->flags.can_skip)); } bool FinaleInterpreter::skip() { LOG_AS("FinaleInterpreter"); if (d->waitText && d->flags.can_skip && !d->flags.paused) { // Instead of skipping, just complete the text. d->waitText->accelerate(); return true; } // Stop waiting for objects. d->waitText = nullptr; d->waitAnim = nullptr; if (d->flags.paused) { d->flags.paused = false; d->wait = 0; return true; } if (d->flags.can_skip) { d->skipping = true; // Start skipping ahead. d->wait = 0; return true; } return (d->flags.eat_events != 0); } bool FinaleInterpreter::skipToMarker(String const &marker) { LOG_AS("FinaleInterpreter"); if (marker.isEmpty()) return false; d->gotoTarget = marker; d->gotoSkip = true; // Start skipping until the marker is found. d->wait = 0; // Stop any waiting. d->waitText = nullptr; d->waitAnim = nullptr; // Rewind the script so we can jump anywhere. d->cp = d->scriptBegin; return true; } bool FinaleInterpreter::skipInProgress() const { return d->skipNext; } bool FinaleInterpreter::lastSkipped() const { return d->lastSkipped; } int FinaleInterpreter::handleEvent(ddevent_t const &ev) { LOG_AS("FinaleInterpreter"); if (d->flags.suspended) return false; // During the first ~second disallow all events/skipping. if (d->timer < 20) return false; if (!::isClient) { #ifdef __CLIENT__ // Any handlers for this event? if (IS_KEY_DOWN(&ev)) { if (Impl::EventHandler *eh = d->findEventHandler(ev)) { skipToMarker(eh->gotoMarker); // Never eat up events. if (IS_TOGGLE_UP(&ev)) return false; return (d->flags.eat_events != 0); } } #endif } // If we can't skip, there's no interaction of any kind. if (!d->flags.can_skip && !d->flags.paused) return false; // We are only interested in key/button down presses. if (!IS_TOGGLE_DOWN(&ev)) return false; #ifdef __CLIENT__ if (::isClient) { // Request skip from the server. Cl_RequestFinaleSkip(); return true; } #endif #ifdef __SERVER__ // Tell clients to skip. Sv_Finale(d->id, FINF_SKIP, 0); #endif return skip(); } #ifdef __CLIENT__ void FinaleInterpreter::addEventHandler(ddevent_t const &evTemplate, String const &gotoMarker) { // Does a handler already exist for this? if (d->findEventHandler(evTemplate)) return; d->eventHandlers.append(Impl::EventHandler(&evTemplate, gotoMarker)); } void FinaleInterpreter::removeEventHandler(ddevent_t const &evTemplate) { if (Impl::EventHandler *eh = d->findEventHandler(evTemplate)) { int index = 0; while (&d->eventHandlers.at(index) != eh) { index++; } d->eventHandlers.removeAt(index); } } #endif // __CLIENT__ FinalePageWidget &FinaleInterpreter::page(PageIndex index) { if (index >= Anims && index <= Texts) { DENG2_ASSERT(d->pages[index]); return *d->pages[index]; } throw MissingPageError("FinaleInterpreter::page", "Unknown page #" + String::number(int(index))); } FinalePageWidget const &FinaleInterpreter::page(PageIndex index) const { return const_cast(const_cast(this)->page(index)); } FinaleWidget *FinaleInterpreter::tryFindWidget(String const &name) { // Perhaps an Anim? if (FinaleWidget *found = d->locateWidget(FI_ANIM, name)) { return found; } // Perhaps a Text? if (FinaleWidget *found = d->locateWidget(FI_TEXT, name)) { return found; } return nullptr; } FinaleWidget &FinaleInterpreter::findWidget(fi_obtype_e type, String const &name) { if (FinaleWidget *foundWidget = d->locateWidget(type, name)) { return *foundWidget; } throw MissingWidgetError("FinaleInterpeter::findWidget", "Failed locating widget for name:'" + name + "'"); } FinaleWidget &FinaleInterpreter::findOrCreateWidget(fi_obtype_e type, String const &name) { DENG2_ASSERT(type >= FI_ANIM && type <= FI_TEXT); DENG2_ASSERT(!name.isEmpty()); if (FinaleWidget *foundWidget = d->locateWidget(type, name)) { return *foundWidget; } FinaleWidget *newWidget = d->makeWidget(type, name); if (!newWidget) throw Error("FinaleInterpreter::findOrCreateWidget", "Failed making widget for type:" + String::number(int(type))); return *page(d->choosePageFor(*newWidget)).addChild(newWidget); } void FinaleInterpreter::beginDoSkipMode() { if (!skipInProgress()) return; // A conditional skip has been issued. // We'll go into DO-skipping mode. skipnext won't be cleared // until the matching semicolon is found. d->doLevel++; } void FinaleInterpreter::gotoEnd() { d->gotoEnd = true; } void FinaleInterpreter::pause() { d->flags.paused = true; wait(1); } void FinaleInterpreter::wait(int ticksToWait) { d->wait = ticksToWait; } void FinaleInterpreter::foundSkipHere() { d->skipping = false; } void FinaleInterpreter::foundSkipMarker(String const &marker) { // Does it match the current goto torget? if (!d->gotoTarget.compareWithoutCase(marker)) { d->gotoSkip = false; } } int FinaleInterpreter::inTime() const { return d->inTime; } void FinaleInterpreter::setInTime(int seconds) { d->inTime = seconds; } void FinaleInterpreter::setHandleEvents(bool yes) { d->flags.eat_events = yes; } void FinaleInterpreter::setShowMenu(bool yes) { d->flags.show_menu = yes; } void FinaleInterpreter::setSkip(bool allowed) { d->flags.can_skip = allowed; } void FinaleInterpreter::setSkipNext(bool yes) { d->skipNext = yes; } void FinaleInterpreter::setWaitAnim(FinaleAnimWidget *newWaitAnim) { d->waitAnim = newWaitAnim; } void FinaleInterpreter::setWaitText(FinaleTextWidget *newWaitText) { d->waitText = newWaitText; } /// @note This command is called even when condition-skipping. DEFFC(Do) { DENG2_UNUSED2(cmd, ops); fi.beginDoSkipMode(); } DEFFC(End) { DENG2_UNUSED2(cmd, ops); fi.gotoEnd(); } static void changePageBackground(FinalePageWidget &page, world::Material *newMaterial) { // If the page does not yet have a background set we must setup the color+alpha. if (newMaterial && !page.backgroundMaterial()) { page.setBackgroundTopColorAndAlpha (Vector4f(1, 1, 1, 1)) .setBackgroundBottomColorAndAlpha(Vector4f(1, 1, 1, 1)); } page.setBackgroundMaterial(newMaterial); } DEFFC(BGMaterial) { DENG2_UNUSED(cmd); // First attempt to resolve as a Values URI (which defines the material URI). world::Material *material = nullptr; try { if (ded_value_t *value = DED_Definitions()->getValueByUri(*reinterpret_cast(OP_URI(0)))) { material = &world::Materials::get().material(de::makeUri(value->text)); } else { material = &world::Materials::get().material(*reinterpret_cast(OP_URI(0))); } } catch (world::MaterialManifest::MissingMaterialError const &) {} // Ignore this error. catch (Resources::MissingResourceManifestError const &) {} // Ignore this error. changePageBackground(fi.page(FinaleInterpreter::Anims), material); } DEFFC(NoBGMaterial) { DENG2_UNUSED2(cmd, ops); changePageBackground(fi.page(FinaleInterpreter::Anims), 0); } DEFFC(InTime) { DENG2_UNUSED(cmd); fi.setInTime(FRACSECS_TO_TICKS(OP_FLOAT(0))); } DEFFC(Tic) { DENG2_UNUSED2(cmd, ops); fi.wait(); } DEFFC(Wait) { DENG2_UNUSED(cmd); fi.wait(FRACSECS_TO_TICKS(OP_FLOAT(0))); } DEFFC(WaitText) { DENG2_UNUSED(cmd); fi.setWaitText(&fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as()); } DEFFC(WaitAnim) { DENG2_UNUSED(cmd); fi.setWaitAnim(&fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as()); } DEFFC(Color) { DENG2_UNUSED(cmd); fi.page(FinaleInterpreter::Anims) .setBackgroundTopColor (Vector3f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2)), fi.inTime()) .setBackgroundBottomColor(Vector3f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2)), fi.inTime()); } DEFFC(ColorAlpha) { DENG2_UNUSED(cmd); fi.page(FinaleInterpreter::Anims) .setBackgroundTopColorAndAlpha (Vector4f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()) .setBackgroundBottomColorAndAlpha(Vector4f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()); } DEFFC(Pause) { DENG2_UNUSED2(cmd, ops); fi.pause(); } DEFFC(CanSkip) { DENG2_UNUSED2(cmd, ops); fi.setSkip(true); } DEFFC(NoSkip) { DENG2_UNUSED2(cmd, ops); fi.setSkip(false); } DEFFC(SkipHere) { DENG2_UNUSED2(cmd, ops); fi.foundSkipHere(); } DEFFC(Events) { DENG2_UNUSED2(cmd, ops); fi.setHandleEvents(); } DEFFC(NoEvents) { DENG2_UNUSED2(cmd, ops); fi.setHandleEvents(false); } DEFFC(OnKey) { #ifdef __CLIENT__ DENG2_UNUSED(cmd); // Construct a template event for this handler. ddevent_t ev; de::zap(ev); ev.device = IDEV_KEYBOARD; ev.type = E_TOGGLE; ev.toggle.id = DD_GetKeyCode(OP_CSTRING(0)); ev.toggle.state = ETOG_DOWN; fi.addEventHandler(ev, OP_CSTRING(1)); #else DENG2_UNUSED3(cmd, ops, fi); #endif } DEFFC(UnsetKey) { #ifdef __CLIENT__ DENG2_UNUSED(cmd); // Construct a template event for what we want to "unset". ddevent_t ev; de::zap(ev); ev.device = IDEV_KEYBOARD; ev.type = E_TOGGLE; ev.toggle.id = DD_GetKeyCode(OP_CSTRING(0)); ev.toggle.state = ETOG_DOWN; fi.removeEventHandler(ev); #else DENG2_UNUSED3(cmd, ops, fi); #endif } DEFFC(If) { DENG2_UNUSED2(cmd, ops); LOG_AS("FIC_If"); char const *token = OP_CSTRING(0); bool val = false; // Built-in conditions. if (!qstricmp(token, "netgame")) { val = netGame; } else if (!qstrnicmp(token, "mode:", 5)) { if (App_GameLoaded()) val = !String(token + 5).compareWithoutCase(App_CurrentGame().id()); else val = 0; } // Any hooks? else if (Plug_CheckForHook(HOOK_FINALE_EVAL_IF)) { ddhook_finale_script_evalif_paramaters_t p; de::zap(p); p.token = token; p.returnVal = 0; if (DoomsdayApp::plugins().callAllHooks(HOOK_FINALE_EVAL_IF, fi.id(), (void *) &p)) { val = p.returnVal; LOG_SCR_XVERBOSE("HOOK_FINALE_EVAL_IF: %s => %i", token << val); } else { LOG_SCR_XVERBOSE("HOOK_FINALE_EVAL_IF: no hook (for %s)", token); } } else { LOG_SCR_WARNING("Unknown condition '%s'") << token; } // Skip the next command if the value is false. fi.setSkipNext(!val); } DEFFC(IfNot) { FIC_If(cmd, ops, fi); fi.setSkipNext(!fi.skipInProgress()); } /// @note The only time the ELSE condition does not skip is immediately after a skip. DEFFC(Else) { DENG2_UNUSED2(cmd, ops); fi.setSkipNext(!fi.lastSkipped()); } DEFFC(GoTo) { DENG2_UNUSED(cmd); fi.skipToMarker(OP_CSTRING(0)); } DEFFC(Marker) { DENG2_UNUSED(cmd); fi.foundSkipMarker(OP_CSTRING(0)); } DEFFC(Delete) { DENG2_UNUSED(cmd); delete fi.tryFindWidget(OP_CSTRING(0)); } DEFFC(Image) { DENG2_UNUSED(cmd); LOG_AS("FIC_Image"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); anim.clearAllFrames(); #ifdef __CLIENT__ char const *name = OP_CSTRING(1); lumpnum_t lumpNum = App_FileSystem().lumpNumForName(name); if (rawtex_t *rawTex = ClientResources::get().declareRawTexture(lumpNum)) { anim.newFrame(FinaleAnimWidget::Frame::PFT_RAW, -1, &rawTex->lumpNum, 0, false); return; } LOG_SCR_WARNING("Missing lump '%s'") << name; #endif } DEFFC(ImageAt) { DENG2_UNUSED(cmd); LOG_AS("FIC_ImageAt"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); float x = OP_FLOAT(1); float y = OP_FLOAT(2); anim.clearAllFrames() .setOrigin(Vector2f(x, y)); #ifdef __CLIENT__ char const *name = OP_CSTRING(3); lumpnum_t lumpNum = App_FileSystem().lumpNumForName(name); if (rawtex_t *rawTex = App_Resources().declareRawTexture(lumpNum)) { anim.newFrame(FinaleAnimWidget::Frame::PFT_RAW, -1, &rawTex->lumpNum, 0, false); return; } LOG_SCR_WARNING("Missing lump '%s'") << name; #endif } #ifdef __CLIENT__ static DGLuint loadAndPrepareExtTexture(char const *fileName) { image_t image; DGLuint glTexName = 0; if (GL_LoadExtImage(image, fileName, LGM_NORMAL)) { // Loaded successfully and converted accordingly. // Upload the image to GL. glTexName = GL_NewTextureWithParams( ( image.pixelSize == 2 ? DGL_LUMINANCE_PLUS_A8 : image.pixelSize == 3 ? DGL_RGB : image.pixelSize == 4 ? DGL_RGBA : DGL_LUMINANCE ), image.size.x, image.size.y, image.pixels, (image.size.x < 128 && image.size.y < 128? TXCF_NO_COMPRESSION : 0), 0, GL_LINEAR, GL_LINEAR, 0 /*no anisotropy*/, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE); Image_ClearPixelData(image); } return glTexName; } #endif // __CLIENT__ DEFFC(XImage) { DENG2_UNUSED(cmd); LOG_AS("FIC_XImage"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); #ifdef __CLIENT__ char const *fileName = OP_CSTRING(1); #endif anim.clearAllFrames(); #ifdef __CLIENT__ // Load the external resource. if (DGLuint tex = loadAndPrepareExtTexture(fileName)) { anim.newFrame(FinaleAnimWidget::Frame::PFT_XIMAGE, -1, &tex, 0, false); } else { LOG_SCR_WARNING("Missing graphic '%s'") << fileName; } #endif // __CLIENT__ } DEFFC(Patch) { DENG2_UNUSED(cmd); LOG_AS("FIC_Patch"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); char const *encodedName = OP_CSTRING(3); anim.setOrigin(Vector2f(OP_FLOAT(1), OP_FLOAT(2))); anim.clearAllFrames(); patchid_t patchId = R_DeclarePatch(encodedName); if (patchId) { anim.newFrame(FinaleAnimWidget::Frame::PFT_PATCH, -1, (void *)&patchId, 0, 0); } else { LOG_SCR_WARNING("Missing Patch '%s'") << encodedName; } } DEFFC(SetPatch) { DENG2_UNUSED(cmd); LOG_AS("FIC_SetPatch"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); char const *encodedName = OP_CSTRING(1); patchid_t patchId = R_DeclarePatch(encodedName); if (patchId == 0) { LOG_SCR_WARNING("Missing Patch '%s'") << encodedName; return; } if (!anim.frameCount()) { anim.newFrame(FinaleAnimWidget::Frame::PFT_PATCH, -1, (void *)&patchId, 0, false); return; } // Convert the first frame. FinaleAnimWidget::Frame *f = anim.allFrames().first(); f->type = FinaleAnimWidget::Frame::PFT_PATCH; f->texRef.patch = patchId; f->tics = -1; f->sound = 0; } DEFFC(ClearAnim) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { if (FinaleAnimWidget *anim = maybeAs(wi)) { anim->clearAllFrames(); } } } DEFFC(Anim) { DENG2_UNUSED(cmd); LOG_AS("FIC_Anim"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); char const *encodedName = OP_CSTRING(1); int const tics = FRACSECS_TO_TICKS(OP_FLOAT(2)); patchid_t patchId = R_DeclarePatch(encodedName); if (!patchId) { LOG_SCR_WARNING("Patch '%s' not found") << encodedName; return; } anim.newFrame(FinaleAnimWidget::Frame::PFT_PATCH, tics, (void *)&patchId, 0, false); } DEFFC(AnimImage) { DENG2_UNUSED(cmd); LOG_AS("FIC_AnimImage"); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); #ifdef __CLIENT__ char const *encodedName = OP_CSTRING(1); int const tics = FRACSECS_TO_TICKS(OP_FLOAT(2)); lumpnum_t lumpNum = App_FileSystem().lumpNumForName(encodedName); if (rawtex_t *rawTex = App_Resources().declareRawTexture(lumpNum)) { anim.newFrame(FinaleAnimWidget::Frame::PFT_RAW, tics, &rawTex->lumpNum, 0, false); return; } LOG_SCR_WARNING("Lump '%s' not found") << encodedName; #else DENG2_UNUSED(anim); #endif } DEFFC(Repeat) { DENG2_UNUSED(cmd); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); anim.setLooping(); } DEFFC(StateAnim) { DENG2_UNUSED(cmd); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); #if !defined(__CLIENT__) DENG2_UNUSED(anim); #endif // Animate N states starting from the given one. dint stateId = DED_Definitions()->getStateNum(OP_CSTRING(1)); for (dint count = OP_INT(2); count > 0 && stateId > 0; count--) { state_t *st = &runtimeDefs.states[stateId]; #ifdef __CLIENT__ spriteinfo_t sinf; R_GetSpriteInfo(st->sprite, st->frame & 0x7fff, &sinf); anim.newFrame(FinaleAnimWidget::Frame::PFT_MATERIAL, (st->tics <= 0? 1 : st->tics), sinf.material, 0, sinf.flip); #endif // Go to the next state. stateId = st->nextState; } } DEFFC(PicSound) { DENG2_UNUSED(cmd); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); int const sound = DED_Definitions()->getSoundNum(OP_CSTRING(1)); if (!anim.frameCount()) { anim.newFrame(FinaleAnimWidget::Frame::PFT_MATERIAL, -1, 0, sound, false); return; } anim.allFrames().at(anim.frameCount() - 1)->sound = sound; } DEFFC(ObjectOffX) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setOriginX(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectOffY) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setOriginY(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectOffZ) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setOriginZ(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectRGB) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { Vector3f const color(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)); if (FinaleTextWidget *text = maybeAs(wi)) { text->setColor(color, fi.inTime()); } if (FinaleAnimWidget *anim = maybeAs(wi)) { // This affects all the colors. anim->setColor (color, fi.inTime()) .setEdgeColor (color, fi.inTime()) .setOtherColor (color, fi.inTime()) .setOtherEdgeColor(color, fi.inTime()); } } } DEFFC(ObjectAlpha) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { float const alpha = OP_FLOAT(1); if (FinaleTextWidget *text = maybeAs(wi)) { text->setAlpha(alpha, fi.inTime()); } if (FinaleAnimWidget *anim = maybeAs(wi)) { anim->setAlpha (alpha, fi.inTime()) .setOtherAlpha(alpha, fi.inTime()); } } } DEFFC(ObjectScaleX) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setScaleX(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectScaleY) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setScaleY(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectScaleZ) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setScaleZ(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectScale) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setScaleX(OP_FLOAT(1), fi.inTime()) .setScaleY(OP_FLOAT(1), fi.inTime()); } } DEFFC(ObjectScaleXY) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setScaleX(OP_FLOAT(1), fi.inTime()) .setScaleY(OP_FLOAT(2), fi.inTime()); } } DEFFC(ObjectScaleXYZ) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setScale(Vector3f(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()); } } DEFFC(ObjectAngle) { DENG2_UNUSED(cmd); if (FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0))) { wi->setAngle(OP_FLOAT(1), fi.inTime()); } } DEFFC(Rect) { DENG2_UNUSED(cmd); FinaleAnimWidget &anim = fi.findOrCreateWidget(FI_ANIM, OP_CSTRING(0)).as(); /// @note We may be converting an existing Pic to a Rect, so re-init the expected /// default state accordingly. anim.clearAllFrames() .resetAllColors() .setLooping(false) // Yeah? .setOrigin(Vector3f(OP_FLOAT(1), OP_FLOAT(2), 0)) .setScale(Vector3f(OP_FLOAT(3), OP_FLOAT(4), 1)); } DEFFC(FillColor) { DENG2_UNUSED(cmd); FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)); if (!wi || !is(wi)) return; FinaleAnimWidget &anim = wi->as(); // Which colors to modify? int which = 0; if (!qstricmp(OP_CSTRING(1), "top")) which |= 1; else if (!qstricmp(OP_CSTRING(1), "bottom")) which |= 2; else which = 3; Vector4f color; for (int i = 0; i < 4; ++i) { color[i] = OP_FLOAT(2 + i); } if (which & 1) anim.setColorAndAlpha(color, fi.inTime()); if (which & 2) anim.setOtherColorAndAlpha(color, fi.inTime()); } DEFFC(EdgeColor) { DENG2_UNUSED(cmd); FinaleWidget *wi = fi.tryFindWidget(OP_CSTRING(0)); if (!wi || !is(wi)) return; FinaleAnimWidget &anim = wi->as(); // Which colors to modify? int which = 0; if (!qstricmp(OP_CSTRING(1), "top")) which |= 1; else if (!qstricmp(OP_CSTRING(1), "bottom")) which |= 2; else which = 3; Vector4f color; for (int i = 0; i < 4; ++i) { color[i] = OP_FLOAT(2 + i); } if (which & 1) anim.setEdgeColorAndAlpha(color, fi.inTime()); if (which & 2) anim.setOtherEdgeColorAndAlpha(color, fi.inTime()); } DEFFC(OffsetX) { DENG2_UNUSED(cmd); fi.page(FinaleInterpreter::Anims).setOffsetX(OP_FLOAT(0), fi.inTime()); } DEFFC(OffsetY) { DENG2_UNUSED(cmd); fi.page(FinaleInterpreter::Anims).setOffsetY(OP_FLOAT(0), fi.inTime()); } DEFFC(Sound) { DENG2_UNUSED2(cmd, fi); S_LocalSound(DED_Definitions()->getSoundNum(OP_CSTRING(0)), nullptr); } DEFFC(SoundAt) { DENG2_UNUSED2(cmd, fi); dint const soundId = DED_Definitions()->getSoundNum(OP_CSTRING(0)); dfloat const vol = de::min(OP_FLOAT(1), 1.f); S_LocalSoundAtVolume(soundId, nullptr, vol); } DEFFC(SeeSound) { DENG2_UNUSED2(cmd, fi); dint num = DED_Definitions()->getMobjNum(OP_CSTRING(0)); if (num >= 0 && ::runtimeDefs.mobjInfo[num].seeSound > 0) { S_LocalSound(runtimeDefs.mobjInfo[num].seeSound, nullptr); } } DEFFC(DieSound) { DENG2_UNUSED2(cmd, fi); dint num = DED_Definitions()->getMobjNum(OP_CSTRING(0)); if (num >= 0 && ::runtimeDefs.mobjInfo[num].deathSound > 0) { S_LocalSound(runtimeDefs.mobjInfo[num].deathSound, nullptr); } } DEFFC(Music) { DENG2_UNUSED3(cmd, ops, fi); S_StartMusic(OP_CSTRING(0), true); } DEFFC(MusicOnce) { DENG2_UNUSED3(cmd, ops, fi); S_StartMusic(OP_CSTRING(0), false); } DEFFC(Filter) { DENG2_UNUSED(cmd); fi.page(FinaleInterpreter::Texts).setFilterColorAndAlpha(Vector4f(OP_FLOAT(0), OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()); } DEFFC(Text) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setText(OP_CSTRING(3)) .setCursorPos(0) // Restart the text. .setOrigin(Vector3f(OP_FLOAT(1), OP_FLOAT(2), 0)); } DEFFC(TextFromDef) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); int textIdx = DED_Definitions()->getTextNum((char *)OP_CSTRING(3)); text.setText(textIdx >= 0? DED_Definitions()->text[textIdx].text : "(undefined)") .setCursorPos(0) // Restart the type-in animation (if any). .setOrigin(Vector3f(OP_FLOAT(1), OP_FLOAT(2), 0)); } DEFFC(TextFromLump) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setOrigin(Vector3f(OP_FLOAT(1), OP_FLOAT(2), 0)); lumpnum_t lumpNum = App_FileSystem().lumpNumForName(OP_CSTRING(3)); if (lumpNum >= 0) { File1 &lump = App_FileSystem().lump(lumpNum); uint8_t const *rawStr = lump.cache(); AutoStr *str = AutoStr_NewStd(); Str_Reserve(str, lump.size() * 2); char *out = Str_Text(str); for (size_t i = 0; i < lump.size(); ++i) { char ch = (char)(rawStr[i]); if (ch == '\r') continue; if (ch == '\n') { *out++ = '\\'; *out++ = 'n'; } else { *out++ = ch; } } lump.unlock(); text.setText(Str_Text(str)); } else { text.setText("(not found)"); } text.setCursorPos(0); // Restart. } DEFFC(SetText) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setText(OP_CSTRING(1)); } DEFFC(SetTextDef) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); int textIdx = DED_Definitions()->getTextNum((char *)OP_CSTRING(1)); text.setText(textIdx >= 0? DED_Definitions()->text[textIdx].text : "(undefined)"); } DEFFC(DeleteText) { DENG2_UNUSED(cmd); delete fi.tryFindWidget(OP_CSTRING(0)); } DEFFC(PredefinedColor) { DENG2_UNUSED(cmd); fi.page(FinaleInterpreter::Texts) .setPredefinedColor(de::clamp(1, OP_INT(0), FIPAGE_NUM_PREDEFINED_COLORS) - 1, Vector3f(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()); fi.page(FinaleInterpreter::Anims) .setPredefinedColor(de::clamp(1, OP_INT(0), FIPAGE_NUM_PREDEFINED_COLORS) - 1, Vector3f(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()); } DEFFC(PredefinedFont) { #ifdef __CLIENT__ DENG2_UNUSED(cmd); LOG_AS("FIC_PredefinedFont"); fontid_t const fontNum = Fonts_ResolveUri(OP_URI(1)); if (fontNum) { int const idx = de::clamp(1, OP_INT(0), FIPAGE_NUM_PREDEFINED_FONTS) - 1; fi.page(FinaleInterpreter::Anims).setPredefinedFont(idx, fontNum); fi.page(FinaleInterpreter::Texts).setPredefinedFont(idx, fontNum); return; } AutoStr *fontPath = Uri_ToString(OP_URI(1)); LOG_SCR_WARNING("Unknown font '%s'") << Str_Text(fontPath); #else DENG2_UNUSED3(cmd, ops, fi); #endif } DEFFC(TextRGB) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setColor(Vector3f(OP_FLOAT(1), OP_FLOAT(2), OP_FLOAT(3)), fi.inTime()); } DEFFC(TextAlpha) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setAlpha(OP_FLOAT(1), fi.inTime()); } DEFFC(TextOffX) { DENG2_UNUSED(cmd); FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)); wi.setOriginX(OP_FLOAT(1), fi.inTime()); } DEFFC(TextOffY) { DENG2_UNUSED(cmd); FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)); wi.setOriginY(OP_FLOAT(1), fi.inTime()); } DEFFC(TextCenter) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setAlignment(text.alignment() & ~(ALIGN_LEFT | ALIGN_RIGHT)); } DEFFC(TextNoCenter) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setAlignment(text.alignment() | ALIGN_LEFT); } DEFFC(TextScroll) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setScrollRate(OP_INT(1)); } DEFFC(TextPos) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setCursorPos(OP_INT(1)); } DEFFC(TextRate) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setTypeInRate(OP_INT(1)); } DEFFC(TextLineHeight) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setLineHeight(OP_FLOAT(1)); } DEFFC(Font) { #ifdef __CLIENT__ DENG2_UNUSED(cmd); LOG_AS("FIC_Font"); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); fontid_t fontNum = Fonts_ResolveUri(OP_URI(1)); if (fontNum) { text.setFont(fontNum); return; } AutoStr *fontPath = Uri_ToString(OP_URI(1)); LOG_SCR_WARNING("Unknown font '%s'") << Str_Text(fontPath); #else DENG2_UNUSED3(cmd, ops, fi); #endif } DEFFC(FontA) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setFont(fi.page(FinaleInterpreter::Texts).predefinedFont(0)); } DEFFC(FontB) { DENG2_UNUSED(cmd); FinaleTextWidget &text = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)).as(); text.setFont(fi.page(FinaleInterpreter::Texts).predefinedFont(1)); } DEFFC(NoMusic) { DENG2_UNUSED3(cmd, ops, fi); S_StopMusic(); } DEFFC(TextScaleX) { DENG2_UNUSED(cmd); FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)); wi.setScaleX(OP_FLOAT(1), fi.inTime()); } DEFFC(TextScaleY) { DENG2_UNUSED(cmd); FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)); wi.setScaleY(OP_FLOAT(1), fi.inTime()); } DEFFC(TextScale) { DENG2_UNUSED(cmd); FinaleWidget &wi = fi.findOrCreateWidget(FI_TEXT, OP_CSTRING(0)); wi.setScaleX(OP_FLOAT(1), fi.inTime()) .setScaleY(OP_FLOAT(2), fi.inTime()); } DEFFC(PlayDemo) { /// @todo Demos are not supported at the moment. -jk #if 0 // While playing a demo we suspend command interpretation. fi.suspend(); // Start the demo. if (!Con_Executef(CMDS_DDAY, true, "playdemo \"%s\"", OP_CSTRING(0))) { // Demo playback failed. Here we go again... fi.resume(); } #else DENG2_UNUSED3(cmd, ops, fi); #endif } DEFFC(Command) { DENG2_UNUSED2(cmd, fi); Con_Executef(CMDS_SCRIPT, false, "%s", OP_CSTRING(0)); } DEFFC(ShowMenu) { DENG2_UNUSED2(cmd, ops); fi.setShowMenu(); } DEFFC(NoShowMenu) { DENG2_UNUSED2(cmd, ops); fi.setShowMenu(false); }